From f144006daea838f3095880444d48e2941447e4e7 Mon Sep 17 00:00:00 2001
From: Přemysl Eric Janouch
Date: Wed, 13 Dec 2023 09:33:47 +0100
Subject: Add some API methods
---
main.go | 219 ++++++++++++++++++++++++++++++++++++++++++++++++++----
public/gallery.js | 32 ++++++++
test.sh | 14 ++++
3 files changed, 250 insertions(+), 15 deletions(-)
diff --git a/main.go b/main.go
index ba7d6f9..d00dcec 100644
--- a/main.go
+++ b/main.go
@@ -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(`
+
+
{{ .Name }}
{{ range .Children }}
@@ -164,6 +167,8 @@ var page = template.Must(template.New("/").Parse(`
`))
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
// 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,
+ })
+})
diff --git a/test.sh b/test.sh
index 13cb028..920bae9 100755
--- a/test.sh
+++ b/test.sh
@@ -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
--
cgit v1.2.3-70-g09d2