Initial commit

This commit is contained in:
soup 2024-11-14 17:39:31 -05:00
commit 1ca54d6114
21 changed files with 725 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
shelves.db
shelves.db*
shelves

10
Makefile Normal file
View file

@ -0,0 +1,10 @@
.PHONY: build
build:
go build backend/bin/shelves.go
run:
go run backend/bin/shelves.go
watch:
fd . . | entr -cr make run

34
backend/bin/shelves.go Normal file
View file

@ -0,0 +1,34 @@
package main
import (
"fmt"
"log"
"net/http"
"os"
_ "github.com/mattn/go-sqlite3"
"shelves/backend/db"
shelvesHttp "shelves/backend/http"
"shelves/backend/routes"
)
func main() {
os.Remove("./shelves.db")
db, migrateResult, err := db.Open("./shelves.db")
if err != nil {
log.Fatalf("Failed to open DB: %v", err)
}
if migrateResult.MigrationError != nil {
log.Printf("An error was encountered while upgrading the database schema. You are on version %v, but the latest is %v. The error was: %v", migrateResult.SchemaVerNew, migrateResult.SchemaVerLatest, migrateResult.MigrationError)
}
schemaNeedsUpdating := migrateResult.SchemaVerNew != migrateResult.SchemaVerLatest
if schemaNeedsUpdating {
log.Printf("Your database schema needs to be updated. The application will continue to run, but you may encounter errors.\n")
}
_ = db
routes := routes.Routes()
fmt.Println("Listening on localhost:8999")
http.ListenAndServe("localhost:8999", shelvesHttp.Log(shelvesHttp.WithCtx(db, routes)))
}

155
backend/db/db.go Normal file
View file

@ -0,0 +1,155 @@
package db
import (
"database/sql"
"fmt"
_ "github.com/mattn/go-sqlite3"
)
var _MIGRATIONS = [...]string{
`
create table owner_settings (
id integer primary key,
display_name text not null,
password_hash blob not null,
password_salt blob not null,
ts_updated text not null default current_timestamp
) strict;
create trigger owner_settings_ts_update
after update on owner_settings
for each row
begin
update owner_settings set ts_updated = current_timestamp where rowid = new.rowid;
end;
create table blob (
id integer primary key,
data blob not null,
ts_created text not null default current_timestamp,
ts_updated text not null default current_timestamp
) strict;
create trigger blob_ts_update
after update on blob
for each row
begin
update blob set ts_updated = current_timestamp where rowid = new.rowid;
end;
create table item (
id integer primary key,
name text not null,
description text not null,
rating integer,
ts_created text not null default current_timestamp,
ts_updated text not null default current_timestamp,
image_blob_id integer not null,
foreign key (image_blob_id) references blob(id) on delete cascade
) strict;
create trigger item_ts_update
after update on item
for each row
begin
update item set ts_updated = current_timestamp where rowid = new.rowid;
end;
create table feed_event (
id integer primary key,
feed_event_type text not null,
data_json text not null,
ts_created text not null default current_timestamp,
item_id integer not null,
foreign key (item_id) references item(id) on delete cascade
) strict;
create table session (
id integer primary key,
session_id text not null unique
) strict;
`,
}
type MigrateResult struct {
SchemaVerPrev int
SchemaVerNew int
SchemaVerLatest int
MigrationError error
}
func migrate(db *sql.DB) (MigrateResult, error) {
result := MigrateResult{
SchemaVerLatest: len(_MIGRATIONS),
}
err := db.QueryRow("PRAGMA user_version").Scan(&result.SchemaVerPrev)
if err != nil {
return result, err
}
migrationsToRun := _MIGRATIONS[result.SchemaVerPrev:]
for i, m := range migrationsToRun {
_, err = db.Exec(m)
if err != nil {
result.MigrationError = err
break
}
result.SchemaVerNew = result.SchemaVerPrev + i + 1
}
updateQueryStr := fmt.Sprintf("PRAGMA user_version = %d;", result.SchemaVerNew)
_, err = db.Exec(updateQueryStr)
if err != nil {
return result, err
}
return result, nil
}
func initConnection(db *sql.DB) error {
_, err := db.Exec(`
PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;
PRAGMA synchronous = NORMAL;
PRAGMA cache_size = 1000000000;
PRAGMA foreign_keys = true;
PRAGMA temp_store = memory;
`)
return err
}
func Open(path string) (*sql.DB, MigrateResult, error) {
if path == "" {
path = "./shelves.db"
}
db, err := sql.Open("sqlite3", path)
if err != nil {
return nil, MigrateResult{}, err
}
err = initConnection(db)
if err != nil {
return nil, MigrateResult{}, err
}
result, err := migrate(db)
if err != nil {
return nil, result, err
}
return db, result, nil
}

106
backend/http/middleware.go Normal file
View file

@ -0,0 +1,106 @@
package http
import (
"context"
"database/sql"
"log"
"net/http"
"time"
"shelves/backend/routes"
)
type LogWrapper struct {
statusCode *int
http.ResponseWriter
}
func (w LogWrapper) WriteHeader(statusCode int) {
*w.statusCode = statusCode
w.ResponseWriter.WriteHeader(statusCode)
}
func Log(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
statusCode := 200
wrapper := LogWrapper{statusCode: &statusCode, ResponseWriter: w}
start := time.Now()
next.ServeHTTP(wrapper, r)
log.Println(r.Method, r.URL.Path, statusCode, time.Since(start))
})
}
type AuthInfo struct {
is_admin bool
}
func checkAuthed(db *sql.DB, r *http.Request) (AuthInfo, error) {
cookie, err := r.Cookie("SHELVES_OWNER_SESSION_ID")
if err == http.ErrNoCookie {
return AuthInfo{}, nil
}
if err != nil {
return AuthInfo{}, err
}
sessionId := cookie.Value
sessionCount := 0
err = db.QueryRow("select count(*) from session where session_id = ?", sessionId).Scan(&sessionCount)
if err != nil {
return AuthInfo{}, err
}
return AuthInfo{is_admin: sessionCount == 1}, nil
}
func queryNeedsOwnerSetup(db *sql.DB) (bool, error) {
count := 0
err := db.QueryRow("select count(*) from owner_settings").Scan(&count)
if err != nil {
return false, err
}
return count != 1, nil
}
type Ctx struct {
DB *sql.DB
Auth AuthInfo
}
func WithCtx(db *sql.DB, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
needsOwnerSetup, err := queryNeedsOwnerSetup(db)
if err != nil {
log.Printf("Error while querying owner_settings: %v\n", err)
}
if needsOwnerSetup {
if r.URL.Path != "/setup" {
w.Header().Add("Location", "/setup")
w.WriteHeader(http.StatusSeeOther)
return
} else {
routes.SetupGet(w, r)
return
}
}
ctx := r.Context()
auth, err := checkAuthed(db, r)
if err != nil {
log.Printf("Error while querying auth info: %v\n", err)
}
ctx = context.WithValue(ctx, "__ctx", Ctx{
DB: db,
Auth: auth,
})
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}

7
backend/routes/home.go Normal file
View file

@ -0,0 +1,7 @@
package routes
import "net/http"
func HomeGet(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("Hello, world"))
}

22
backend/routes/routes.go Normal file
View file

@ -0,0 +1,22 @@
package routes
import (
"embed"
"html/template"
"net/http"
"shelves"
)
func html(w http.ResponseWriter, s template.HTML) {
w.Header().Add("Content-Type", "text/html")
w.Write([]byte(s))
}
func Routes() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("GET /", HomeGet)
mux.Handle("GET /static", http.FileServerFS(shelves.Frontend))
return mux
}

24
backend/routes/setup.go Normal file
View file

@ -0,0 +1,24 @@
package routes
import (
"net/http"
"shelves/backend/templates"
)
func SetupGet(w http.ResponseWriter, r *http.Request) {
html(w, templates.PageBase{Title: "Set up", Body: templates.Form{
Action: "/setup",
Fields: []templates.Field{
templates.Field{
Label: "Password",
Type: "password",
Name: "password",
},
templates.Field{
Label: "Confirm password",
Type: "password",
Name: "password-confirmation",
},
},
}.HTML()}.HTML())
}

39
backend/templates/form.go Normal file
View file

@ -0,0 +1,39 @@
package templates
import "html/template"
type Field struct {
Label string
Name string
Error string
Type string
Value string
Placeholder string
Valid bool
}
func (f *Field) init() {
if f.Type == "" {
f.Type = "text"
}
f.Valid = f.Valid || f.Error == ""
}
type Form struct {
Action string
Method string
Fields []Field
}
func (f Form) HTML() template.HTML {
if f.Method == "" {
f.Method = "POST"
}
for _, f := range f.Fields {
f.init()
}
return tmpls.renderHtml("form.tmpl.html", f)
}

15
backend/templates/page.go Normal file
View file

@ -0,0 +1,15 @@
package templates
import "html/template"
type PageBase struct {
Title string
Head template.HTML
Body template.HTML
BodyBefore template.HTML
BodyAfter template.HTML
}
func (pb PageBase) HTML() template.HTML {
return tmpls.renderHtml("page_base.tmpl.html", pb)
}

View file

@ -0,0 +1,28 @@
package templates
import (
"embed"
"fmt"
"html/template"
"strings"
)
//go:embed tmpls/*
var files embed.FS
type Template struct {
*template.Template
}
var tmpls = Template{template.Must(template.ParseFS(files, "tmpls/*"))}
func (tmpl Template) renderHtml(name string, data any) template.HTML {
writer := &strings.Builder{}
err := tmpl.ExecuteTemplate(writer, name, data)
if err != nil {
fmt.Fprint(writer, err)
}
return template.HTML(writer.String())
}

View file

@ -0,0 +1,30 @@
{{define "attrs"}}
placeholder="{{.Placeholder}}"
aria-invalid="{{.Valid}}"
{{if .Error}}
aria-describedby="{{.Name}}-error"
{{end}}
{{end}}
<form action="{{.Action}}" method="{{.Method}}">
{{range .Fields}}
<label>
{{.Label}}
{{if eq .Type "textarea"}}
<textarea
{{template "attrs" .}}
>{{.Value}}</textarea>
{{else}}
<input
type="{{.Type}}"
value="{{.Value}}"
{{template "attrs" .}}
>
{{end}}
</label>
{{if .Error}}
<small id="{{.Name}}-error">{{.Error}}</small>
{{end}}
{{end}}
</form>

View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<title>{{.Title}} - Shelves</title>
{{.Head}}
</head>
<body>
{{.BodyBefore}}
{{.Body}}
{{.BodyAfter}}
</body>
</html>

82
flake.lock Normal file
View file

@ -0,0 +1,82 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"gomod2nix": {
"inputs": {
"flake-utils": [
"flake-utils"
],
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1729448365,
"narHash": "sha256-oquZeWTYWTr5IxfwEzgsxjtD8SSFZYLdO9DaQb70vNU=",
"owner": "nix-community",
"repo": "gomod2nix",
"rev": "5d387097aa716f35dd99d848dc26d8d5b62a104c",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "gomod2nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1725930920,
"narHash": "sha256-RVhD9hnlTT2nJzPHlAqrWqCkA7T6CYrP41IoVRkciZM=",
"path": "/nix/store/20yis5w6g397plssim663hqxdiiah2wr-source",
"rev": "44a71ff39c182edaf25a7ace5c9454e7cba2c658",
"type": "path"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"gomod2nix": "gomod2nix",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

31
flake.nix Normal file
View file

@ -0,0 +1,31 @@
{
inputs.nixpkgs.url = "nixpkgs";
inputs.flake-utils.url = "github:numtide/flake-utils";
inputs.gomod2nix = {
url = "github:nix-community/gomod2nix";
inputs.nixpkgs.follows = "nixpkgs";
inputs.flake-utils.follows = "flake-utils";
};
outputs = inputs:
(inputs.flake-utils.lib.eachDefaultSystem (system:
let
pkgs = inputs.nixpkgs.legacyPackages.${system};
pkgsG = inputs.gomod2nix.legacyPackages.${system};
in {
devShells.default =
let goEnv = pkgsG.mkGoEnv { pwd = ./.; };
in pkgs.mkShell {
packages = with pkgs; [
gnumake
entr
fd
goEnv
(sqlite.override { interactive = true; })
pkgsG.gomod2nix
];
};
}
));
}

6
frontend.go Normal file
View file

@ -0,0 +1,6 @@
package shelves
import "embed"
//go:embed frontend/*
var Frontend embed.FS

0
frontend/css/mu.css Normal file
View file

24
go.mod Normal file
View file

@ -0,0 +1,24 @@
module shelves
go 1.22
require (
github.com/mattn/go-sqlite3 v1.14.24
golang.org/x/tools/gopls v0.16.2
)
require (
github.com/BurntSushi/toml v1.2.1 // indirect
github.com/google/go-cmp v0.6.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338 // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.23.0 // indirect
golang.org/x/telemetry v0.0.0-20240829154258-f29ab539cc98 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/tools v0.22.1-0.20240829175637-39126e24d653 // indirect
golang.org/x/vuln v1.0.4 // indirect
honnef.co/go/tools v0.4.7 // indirect
mvdan.cc/gofumpt v0.6.0 // indirect
mvdan.cc/xurls/v2 v2.5.0 // indirect
)

42
go.sum Normal file
View file

@ -0,0 +1,42 @@
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/safehtml v0.1.0 h1:EwLKo8qawTKfsi0orxcQAZzu07cICaBeFMegAU9eaT8=
github.com/google/safehtml v0.1.0/go.mod h1:L4KWwDsUJdECRAEpZoBn3O64bQaywRscowZjJAzjHnU=
github.com/jba/templatecheck v0.7.0 h1:wjTb/VhGgSFeim5zjWVePBdaMo28X74bGLSABZV+zIA=
github.com/jba/templatecheck v0.7.0/go.mod h1:n1Etw+Rrw1mDDD8dDRsEKTwMZsJ98EkktgNJC6wLUGo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338 h1:2O2DON6y3XMJiQRAS1UWU+54aec2uopH3x7MAiqGW6Y=
golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240829154258-f29ab539cc98 h1:Wm3cG5X6sZ0RSVRc/H1/sciC4AT6HAKgLCSH2lbpR/c=
golang.org/x/telemetry v0.0.0-20240829154258-f29ab539cc98/go.mod h1:m7R/r+o5h7UvF2JD9n2iLSGY4v8v+zNSyTJ6xynLrqs=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/tools v0.22.1-0.20240829175637-39126e24d653 h1:6bJEg2w2kUHWlfdJaESYsmNfI1LKAZQi6zCa7LUn7eI=
golang.org/x/tools v0.22.1-0.20240829175637-39126e24d653/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/tools/gopls v0.16.2 h1:K1z03MlikHfaMTtG01cUeL5FAOTJnITuNe0TWOcg8tM=
golang.org/x/tools/gopls v0.16.2/go.mod h1:Hj8YxzfHfFyRK5muTZy5oO6/0nL7CZWu28ZNac7tXF0=
golang.org/x/vuln v1.0.4 h1:SP0mPeg2PmGCu03V+61EcQiOjmpri2XijexKdzv8Z1I=
golang.org/x/vuln v1.0.4/go.mod h1:NbJdUQhX8jY++FtuhrXs2Eyx0yePo9pF7nPlIjo9aaQ=
honnef.co/go/tools v0.4.7 h1:9MDAWxMoSnB6QoSqiVr7P5mtkT9pOc1kSxchzPCnqJs=
honnef.co/go/tools v0.4.7/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0=
mvdan.cc/gofumpt v0.6.0 h1:G3QvahNDmpD+Aek/bNOLrFR2XC6ZAdo62dZu65gmwGo=
mvdan.cc/gofumpt v0.6.0/go.mod h1:4L0wf+kgIPZtcCWXynNS2e6bhmj73umwnuXSZarixzA=
mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=

48
gomod2nix.toml Normal file
View file

@ -0,0 +1,48 @@
schema = 3
[mod]
[mod."github.com/BurntSushi/toml"]
version = "v1.2.1"
hash = "sha256-Z1dlsUTjF8SJZCknYKt7ufJz8NPGg9P9+W17DQn+LO0="
[mod."github.com/google/go-cmp"]
version = "v0.6.0"
hash = "sha256-qgra5jze4iPGP0JSTVeY5qV5AvEnEu39LYAuUCIkMtg="
[mod."github.com/mattn/go-sqlite3"]
version = "v1.14.24"
hash = "sha256-taGKFZFQlR5++5b2oZ1dYS3RERKv6yh1gniNWhb4egg="
[mod."golang.org/x/exp/typeparams"]
version = "v0.0.0-20221212164502-fae10dda9338"
hash = "sha256-IdVdYrszhwtJ63OmlyzuqJ261dGyYulHd/MW9RKkIZg="
[mod."golang.org/x/mod"]
version = "v0.20.0"
hash = "sha256-nXYnY2kpbVkaZ/7Mf7FmxwGDX7N4cID3gKjGghmVRp4="
[mod."golang.org/x/sync"]
version = "v0.8.0"
hash = "sha256-usvF0z7gq1vsX58p4orX+8WHlv52pdXgaueXlwj2Wss="
[mod."golang.org/x/sys"]
version = "v0.23.0"
hash = "sha256-tC6QVLu72bADgINz26FUGdmYqKgsU45bHPg7sa0ZV7w="
[mod."golang.org/x/telemetry"]
version = "v0.0.0-20240829154258-f29ab539cc98"
hash = "sha256-j7MO14OZekObngeg84+nrDTSX8Op7piYL1ljAsdQsig="
[mod."golang.org/x/text"]
version = "v0.16.0"
hash = "sha256-hMTO45upjEuA4sJzGplJT+La2n3oAfHccfYWZuHcH+8="
[mod."golang.org/x/tools"]
version = "v0.22.1-0.20240829175637-39126e24d653"
hash = "sha256-rSWFraJ8stahqWf4VdJd/ueiR/0acy7F2+hBKDOz9gA="
[mod."golang.org/x/tools/gopls"]
version = "v0.16.2"
hash = "sha256-26/OFkFEpXEzWYeW/oMfHf+ejkNJgNpxcckdkH6OZcg="
[mod."golang.org/x/vuln"]
version = "v1.0.4"
hash = "sha256-1HadvTIdPSLPtKtODD3hhl0OByTAN+csvDNUQagzAUw="
[mod."honnef.co/go/tools"]
version = "v0.4.7"
hash = "sha256-aE44owPaESdPFiCyKUoUmUPchfHASwTuF/LZDlwLGVY="
[mod."mvdan.cc/gofumpt"]
version = "v0.6.0"
hash = "sha256-2M9yKlAN0Oe88vBfMED0SJE0XMJfzOMd8Bd2ot/ZMko="
[mod."mvdan.cc/xurls/v2"]
version = "v2.5.0"
hash = "sha256-9hPXZ/t15+LG9fji1gyeWhUrYOr6eGyKYg3a1SmHJpQ="

7
tools.go Normal file
View file

@ -0,0 +1,7 @@
// +build tools
package main
import (
_ "golang.org/x/tools/gopls"
)