diff options
author | Přemysl Eric Janouch <p@janouch.name> | 2023-12-13 09:33:47 +0100 |
---|---|---|
committer | Přemysl Eric Janouch <p@janouch.name> | 2023-12-13 11:56:27 +0100 |
commit | f144006daea838f3095880444d48e2941447e4e7 (patch) | |
tree | 00d6ced5702337e55063a32fdaf7e4c04d9348f8 | |
parent | 286b43d1733e00c49e6246669896cde0340f990b (diff) | |
download | gallery-f144006daea838f3095880444d48e2941447e4e7.tar.gz gallery-f144006daea838f3095880444d48e2941447e4e7.tar.xz gallery-f144006daea838f3095880444d48e2941447e4e7.zip |
Add some API methods
-rw-r--r-- | main.go | 219 | ||||
-rw-r--r-- | public/gallery.js | 32 | ||||
-rwxr-xr-x | test.sh | 14 |
3 files changed, 250 insertions, 15 deletions
@@ -7,6 +7,7 @@ import ( "crypto/sha1" "database/sql" "encoding/hex" + "encoding/json" "errors" "fmt" "html/template" @@ -147,6 +148,8 @@ var page = template.Must(template.New("/").Parse(`<!DOCTYPE html><html><head> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel=stylesheet href=style.css> </head><body> + <noscript>This is a web application, and requires Javascript.</noscript> + <h1>{{ .Name }}</h1> <ul> {{ range .Children }} @@ -164,6 +167,8 @@ var page = template.Must(template.New("/").Parse(`<!DOCTYPE html><html><head> <script src=gallery.js></script> </body></html>`)) +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // XXX: This is preliminary. type entry struct { Parent int64 @@ -241,7 +246,7 @@ func handleRequest(w http.ResponseWriter, r *http.Request) { id, _ := strconv.ParseInt(r.URL.Query().Get("id"), 10, 64) d, err := dbCollectDirectory(id) if err != nil { - http.Error(w, err.Error(), 500) + http.Error(w, err.Error(), http.StatusInternalServerError) return } if err := page.Execute(w, d); err != nil { @@ -265,6 +270,182 @@ func handleThumbs(w http.ResponseWriter, r *http.Request) { } } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +func getSubdirectories(tx *sql.Tx, parent int64) (names []string, err error) { + // TODO: This is like dbCollectStrings(), just needs an argument. + rows, err := tx.Query(`SELECT name FROM directory WHERE parent = ?`, + parent) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var name string + if err := rows.Scan(&name); err != nil { + return nil, err + } + names = append(names, name) + } + return names, rows.Err() +} + +type webEntry struct { + SHA1 string `json:"sha1"` + Name string `json:"name"` + Modified int64 `json:"modified"` + ThumbW int `json:"thumbW"` + ThumbH int `json:"thumbH"` +} + +func getSubentries(tx *sql.Tx, parent int64) (entries []webEntry, err error) { + rows, err := tx.Query(` + SELECT i.sha1, e.name, e.mtime, IFNULL(i.thumbw, 0), IFNULL(i.thumbh, 0) + FROM entry AS e + JOIN image AS i ON e.sha1 = i.sha1 WHERE e.parent = ?`, parent) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var e webEntry + if err := rows.Scan( + &e.SHA1, &e.Name, &e.Modified, &e.ThumbW, &e.ThumbH); err != nil { + return nil, err + } + entries = append(entries, e) + } + return entries, rows.Err() +} + +func handleAPIBrowse(w http.ResponseWriter, r *http.Request) { + var params struct { + Path string + } + if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + var result struct { + Subdirectories []string `json:"subdirectories"` + Entries []webEntry `json:"entries"` + } + + tx, err := db.Begin() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer tx.Rollback() + + parent, err := idForPath(tx, decodeWebPath(params.Path), false) + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + + result.Subdirectories, err = getSubdirectories(tx, parent) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + result.Entries, err = getSubentries(tx, parent) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if err := json.NewEncoder(w).Encode(result); err != nil { + log.Println(err) + } +} + +func getImagePaths(sha1 string) (paths []string, err error) { + rows, err := db.Query(`WITH RECURSIVE paths(parent, path) AS ( + SELECT parent, name AS path FROM entry WHERE sha1 = ? + UNION ALL + SELECT d.parent, d.name || '/' || p.path + FROM directory AS d JOIN paths AS p ON d.id = p.parent + ) SELECT '/' || path FROM paths WHERE parent IS NULL`, sha1) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var path string + if err := rows.Scan(&path); err != nil { + return nil, err + } + paths = append(paths, path) + } + return paths, rows.Err() +} + +func getImageTags(sha1 string) (map[string]map[string]float32, error) { + rows, err := db.Query(` + SELECT ts.name, t.name, ta.weight FROM tag_assignment AS ta + JOIN tag AS t ON t.id = ta.tag + JOIN tag_space AS ts ON ts.id = t.space + WHERE ta.sha1 = ?`, sha1) + if err != nil { + return nil, err + } + defer rows.Close() + + result := make(map[string]map[string]float32) + for rows.Next() { + var ( + space, tag string + weight float32 + ) + if err := rows.Scan(&space, &tag, &weight); err != nil { + return nil, err + } + + tags := result[space] + if tags == nil { + tags = make(map[string]float32) + result[space] = tags + } + tags[tag] = weight + } + return result, rows.Err() +} + +func handleAPIInfo(w http.ResponseWriter, r *http.Request) { + var params struct { + SHA1 string + } + if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + var result struct { + Paths []string `json:"paths"` + Tags map[string]map[string]float32 `json:"tags"` + // TODO: Maybe add perceptual hash collisions. + } + + var err error + result.Paths, err = getImagePaths(params.SHA1) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + result.Tags, err = getImageTags(params.SHA1) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if err := json.NewEncoder(w).Encode(result); err != nil { + log.Println(err) + } +} + // cmdRun runs a web UI against GD on ADDRESS. func cmdRun(args []string) error { if len(args) != 2 { @@ -280,11 +461,11 @@ func cmdRun(args []string) error { // but having an elementary level of security doesn't hurt either. staticHandler = http.FileServer(http.Dir("public")) - // TODO: Make sure the database handle isn't used concurrently. http.HandleFunc("/", handleRequest) http.HandleFunc("/image/", handleImages) http.HandleFunc("/thumb/", handleThumbs) - // TODO: Add a few API endpoints. + http.HandleFunc("/api/browse", handleAPIBrowse) + http.HandleFunc("/api/info", handleAPIInfo) host, port, err := net.SplitHostPort(address) if err != nil { @@ -306,12 +487,7 @@ func cmdRun(args []string) error { // --- Import ------------------------------------------------------------------ -type directoryManager struct { - cache map[string]int64 // Unix-style paths to directory.id -} - -func (dm *directoryManager) uncachedIDForPath( - tx *sql.Tx, path []string) (int64, error) { +func idForPath(tx *sql.Tx, path []string, create bool) (int64, error) { var parent sql.NullInt64 for _, name := range path { if err := tx.QueryRow( @@ -320,6 +496,8 @@ func (dm *directoryManager) uncachedIDForPath( continue } else if !errors.Is(err, sql.ErrNoRows) { return 0, err + } else if !create { + return 0, err } if result, err := tx.Exec( @@ -335,15 +513,26 @@ func (dm *directoryManager) uncachedIDForPath( return parent.Int64, nil } -func (dm *directoryManager) IDForDirectoryPath( - tx *sql.Tx, path string) (int64, error) { +func decodeWebPath(path string) []string { // Relative paths could be handled differently, // but right now, they're assumed to start at the root. - path = filepath.ToSlash(filepath.Clean(path)) list := strings.Split(path, "/") if len(list) > 1 && list[0] == "" { list = list[1:] } + return list +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +type directoryManager struct { + cache map[string]int64 // Unix-style paths to directory.id +} + +func (dm *directoryManager) IDForDirectoryPath( + tx *sql.Tx, path string) (int64, error) { + path = filepath.ToSlash(filepath.Clean(path)) + list := decodeWebPath(path) if len(list) == 0 { return 0, nil } @@ -354,7 +543,7 @@ func (dm *directoryManager) IDForDirectoryPath( return id, nil } - id, err := dm.uncachedIDForPath(tx, list) + id, err := idForPath(tx, list, true) if err != nil { return 0, err } @@ -434,8 +623,8 @@ func (i *importer) Import(path string) error { return err } - // We can't multiplex transactions on a single connection, - // and the directoryManager isn't thread-safe. + // The directoryManager isn't thread-safe. + // This lock also simulates a timeout-less BEGIN EXCLUSIVE. i.dmMutex.Lock() defer i.dmMutex.Unlock() diff --git a/public/gallery.js b/public/gallery.js index ccacec3..1935220 100644 --- a/public/gallery.js +++ b/public/gallery.js @@ -1 +1,33 @@ 'use strict' + +function call(method, params) { + return m.request({ + method: "POST", + url: `/api/{method}`, + body: params, + }) +} + +let Browse = { + view: vnode => { + return m('') + }, +} + +let View = { + view: vnode => { + return m('') + }, +} + +window.addEventListener('load', () => { + m.route(document.body, "/browse/", { + "/browse/:path": Browse, + "/view/:sha1": View, + + "/similar/:sha1": undefined, + "/tags": undefined, + "/tags/:space": undefined, + "/tags/:space/:tag": undefined, + }) +}) @@ -5,7 +5,21 @@ rm -rf $target $input mkdir -p $target cp -ra $HOME/Pictures/Anime $input + ./gallery init $target ./gallery import $target $input ./gallery thumbnail $target ./gallery dhash $target $HOME/Projects/fiv/build/hash +./gallery tag $target autotagger "DanBooru autotagger" \ + < ../build-db/autotagger.tsv + +./gallery run $target :8080 & +web=$! + +echo '{"path":"/tmp/Gi"}' | \ +curl http://localhost:8080/api/browse -X POST --data-binary @- +echo '{"sha1":"d53fc82162fd19a6e7b92b401b08b7505dbf3dfd"}' | \ +curl http://localhost:8080/api/info -X POST --data-binary @- + +kill $web +wait $web |