Initial commit
This commit is contained in:
commit
1ca54d6114
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
shelves.db
|
||||
shelves.db*
|
||||
shelves
|
||||
10
Makefile
Normal file
10
Makefile
Normal 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
34
backend/bin/shelves.go
Normal 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
155
backend/db/db.go
Normal 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
106
backend/http/middleware.go
Normal 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
7
backend/routes/home.go
Normal 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
22
backend/routes/routes.go
Normal 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
24
backend/routes/setup.go
Normal 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
39
backend/templates/form.go
Normal 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
15
backend/templates/page.go
Normal 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)
|
||||
}
|
||||
28
backend/templates/templates.go
Normal file
28
backend/templates/templates.go
Normal 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())
|
||||
}
|
||||
30
backend/templates/tmpls/form.tmpl.html
Normal file
30
backend/templates/tmpls/form.tmpl.html
Normal 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>
|
||||
12
backend/templates/tmpls/page_base.tmpl.html
Normal file
12
backend/templates/tmpls/page_base.tmpl.html
Normal 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
82
flake.lock
Normal 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
31
flake.nix
Normal 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
6
frontend.go
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
package shelves
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed frontend/*
|
||||
var Frontend embed.FS
|
||||
0
frontend/css/mu.css
Normal file
0
frontend/css/mu.css
Normal file
24
go.mod
Normal file
24
go.mod
Normal 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
42
go.sum
Normal 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
48
gomod2nix.toml
Normal 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="
|
||||
Loading…
Reference in a new issue