gallery: clean up, search in a transaction

This commit is contained in:
Přemysl Eric Janouch 2024-01-22 19:49:51 +01:00
parent 083739fd4e
commit 5e0e9f8a42
Signed by: p
GPG Key ID: A0420B94F92B9493
1 changed files with 75 additions and 69 deletions

144
main.go
View File

@ -317,49 +317,7 @@ func cmdInit(fs *flag.FlagSet, args []string) error {
return nil return nil
} }
// --- Web --------------------------------------------------------------------- // --- API: Browse -------------------------------------------------------------
var hashRE = regexp.MustCompile(`^/.*?/([0-9a-f]{40})$`)
var staticHandler http.Handler
var page = template.Must(template.New("/").Parse(`<!DOCTYPE html><html><head>
<title>Gallery</title>
<meta charset="utf-8" />
<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>
<script src=mithril.js></script>
<script src=gallery.js></script>
</body></html>`))
func handleRequest(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
staticHandler.ServeHTTP(w, r)
return
}
if err := page.Execute(w, nil); err != nil {
log.Println(err)
}
}
func handleImages(w http.ResponseWriter, r *http.Request) {
if m := hashRE.FindStringSubmatch(r.URL.Path); m == nil {
http.NotFound(w, r)
} else {
http.ServeFile(w, r, imagePath(m[1]))
}
}
func handleThumbs(w http.ResponseWriter, r *http.Request) {
if m := hashRE.FindStringSubmatch(r.URL.Path); m == nil {
http.NotFound(w, r)
} else {
http.ServeFile(w, r, thumbPath(m[1]))
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
func getSubdirectories(tx *sql.Tx, parent int64) (names []string, err error) { func getSubdirectories(tx *sql.Tx, parent int64) (names []string, err error) {
return dbCollectStrings(`SELECT name FROM node return dbCollectStrings(`SELECT name FROM node
@ -439,7 +397,7 @@ func handleAPIBrowse(w http.ResponseWriter, r *http.Request) {
} }
} }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // --- API: Tags ---------------------------------------------------------------
type webTagNamespace struct { type webTagNamespace struct {
Description string `json:"description"` Description string `json:"description"`
@ -525,7 +483,7 @@ func handleAPITags(w http.ResponseWriter, r *http.Request) {
} }
} }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // --- API: Duplicates ---------------------------------------------------------
type webDuplicateImage struct { type webDuplicateImage struct {
SHA1 string `json:"sha1"` SHA1 string `json:"sha1"`
@ -668,7 +626,7 @@ func handleAPIDuplicates(w http.ResponseWriter, r *http.Request) {
} }
} }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // --- API: Orphans ------------------------------------------------------------
type webOrphanImage struct { type webOrphanImage struct {
SHA1 string `json:"sha1"` SHA1 string `json:"sha1"`
@ -765,7 +723,7 @@ func handleAPIOrphans(w http.ResponseWriter, r *http.Request) {
} }
} }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // --- API: Image view ---------------------------------------------------------
func getImageDimensions(sha1 string) (w int64, h int64, err error) { func getImageDimensions(sha1 string) (w int64, h int64, err error) {
err = db.QueryRow(`SELECT width, height FROM image WHERE sha1 = ?`, err = db.QueryRow(`SELECT width, height FROM image WHERE sha1 = ?`,
@ -868,7 +826,7 @@ func handleAPIInfo(w http.ResponseWriter, r *http.Request) {
} }
} }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // --- API: Image similar ------------------------------------------------------
type webSimilarImage struct { type webSimilarImage struct {
SHA1 string `json:"sha1"` SHA1 string `json:"sha1"`
@ -978,8 +936,8 @@ func handleAPISimilar(w http.ResponseWriter, r *http.Request) {
} }
} }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // --- API: Search -------------------------------------------------------------
// This is the most miserable part of the whole program. // The SQL building is the most miserable part of the whole program.
const searchCTE1 = `WITH const searchCTE1 = `WITH
matches(sha1, thumbw, thumbh, score) AS ( matches(sha1, thumbw, thumbh, score) AS (
@ -992,18 +950,18 @@ const searchCTE1 = `WITH
const searchCTEMulti = `WITH const searchCTEMulti = `WITH
positive(tag) AS (VALUES %s), positive(tag) AS (VALUES %s),
candidates(sha1) AS (%s), filtered(sha1) AS (%s),
matches(sha1, thumbw, thumbh, score) AS ( matches(sha1, thumbw, thumbh, score) AS (
SELECT i.sha1, i.thumbw, i.thumbh, SELECT i.sha1, i.thumbw, i.thumbh,
product(IFNULL(ta.weight, 0)) AS score product(IFNULL(ta.weight, 0)) AS score
FROM image AS i, positive AS p FROM image AS i, positive AS p
JOIN candidates AS c ON i.sha1 = c.sha1 JOIN filtered AS c ON i.sha1 = c.sha1
LEFT JOIN tag_assignment AS ta ON ta.sha1 = i.sha1 AND ta.tag = p.tag LEFT JOIN tag_assignment AS ta ON ta.sha1 = i.sha1 AND ta.tag = p.tag
GROUP BY i.sha1 GROUP BY i.sha1
) )
` `
func parseQuery(query string) (string, error) { func searchQueryToCTE(tx *sql.Tx, query string) (string, error) {
positive, negative := []int64{}, []int64{} positive, negative := []int64{}, []int64{}
for _, word := range strings.Split(query, " ") { for _, word := range strings.Split(query, " ") {
if word == "" { if word == "" {
@ -1019,7 +977,7 @@ func parseQuery(query string) (string, error) {
} }
var tagID int64 var tagID int64
err := db.QueryRow(` err := tx.QueryRow(`
SELECT t.id FROM tag AS t SELECT t.id FROM tag AS t
JOIN tag_space AS ts ON t.space = ts.id JOIN tag_space AS ts ON t.space = ts.id
WHERE ts.name = ? AND t.name = ?`, space, tag).Scan(&tagID) WHERE ts.name = ? AND t.name = ?`, space, tag).Scan(&tagID)
@ -1045,19 +1003,19 @@ func parseQuery(query string) (string, error) {
} }
values := fmt.Sprintf(`(%d)`, positive[0]) values := fmt.Sprintf(`(%d)`, positive[0])
candidates := fmt.Sprintf( filtered := fmt.Sprintf(
`SELECT sha1 FROM tag_assignment WHERE tag = %d`, positive[0]) `SELECT sha1 FROM tag_assignment WHERE tag = %d`, positive[0])
for _, tagID := range positive[1:] { for _, tagID := range positive[1:] {
values += fmt.Sprintf(`, (%d)`, tagID) values += fmt.Sprintf(`, (%d)`, tagID)
candidates += fmt.Sprintf(` INTERSECT filtered += fmt.Sprintf(` INTERSECT
SELECT sha1 FROM tag_assignment WHERE tag = %d`, tagID) SELECT sha1 FROM tag_assignment WHERE tag = %d`, tagID)
} }
for _, tagID := range negative { for _, tagID := range negative {
candidates += fmt.Sprintf(` EXCEPT filtered += fmt.Sprintf(` EXCEPT
SELECT sha1 FROM tag_assignment WHERE tag = %d`, tagID) SELECT sha1 FROM tag_assignment WHERE tag = %d`, tagID)
} }
return fmt.Sprintf(searchCTEMulti, values, candidates), nil return fmt.Sprintf(searchCTEMulti, values, filtered), nil
} }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@ -1069,8 +1027,8 @@ type webTagMatch struct {
Score float32 `json:"score"` Score float32 `json:"score"`
} }
func getTagMatches(cte string) (matches []webTagMatch, err error) { func getTagMatches(tx *sql.Tx, cte string) (matches []webTagMatch, err error) {
rows, err := db.Query(cte + ` rows, err := tx.Query(cte + `
SELECT sha1, IFNULL(thumbw, 0), IFNULL(thumbh, 0), score SELECT sha1, IFNULL(thumbw, 0), IFNULL(thumbh, 0), score
FROM matches`) FROM matches`)
if err != nil { if err != nil {
@ -1096,8 +1054,9 @@ type webTagSupertag struct {
score float32 score float32
} }
func getTagSupertags(cte string) (result map[int64]*webTagSupertag, err error) { func getTagSupertags(tx *sql.Tx, cte string) (
rows, err := db.Query(cte + ` result map[int64]*webTagSupertag, err error) {
rows, err := tx.Query(cte + `
SELECT DISTINCT ta.tag, ts.name, t.name SELECT DISTINCT ta.tag, ts.name, t.name
FROM tag_assignment AS ta FROM tag_assignment AS ta
JOIN matches AS m ON m.sha1 = ta.sha1 JOIN matches AS m ON m.sha1 = ta.sha1
@ -1127,15 +1086,15 @@ type webTagRelated struct {
Score float32 `json:"score"` Score float32 `json:"score"`
} }
func getTagRelated(cte string, matches int) ( func getTagRelated(tx *sql.Tx, cte string, matches int) (
result map[string][]webTagRelated, err error) { result map[string][]webTagRelated, err error) {
// Not sure if this level of efficiency is achievable directly in SQL. // Not sure if this level of efficiency is achievable directly in SQL.
supertags, err := getTagSupertags(cte) supertags, err := getTagSupertags(tx, cte)
if err != nil { if err != nil {
return nil, err return nil, err
} }
rows, err := db.Query(cte + ` rows, err := tx.Query(cte + `
SELECT ta.tag, ta.weight SELECT ta.tag, ta.weight
FROM tag_assignment AS ta FROM tag_assignment AS ta
JOIN matches AS m ON m.sha1 = ta.sha1`) JOIN matches AS m ON m.sha1 = ta.sha1`)
@ -1179,7 +1138,14 @@ func handleAPISearch(w http.ResponseWriter, r *http.Request) {
Related map[string][]webTagRelated `json:"related"` Related map[string][]webTagRelated `json:"related"`
} }
cte, err := parseQuery(params.Query) tx, err := db.Begin()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer tx.Rollback()
cte, err := searchQueryToCTE(tx, params.Query)
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
http.Error(w, err.Error(), http.StatusNotFound) http.Error(w, err.Error(), http.StatusNotFound)
return return
@ -1188,11 +1154,11 @@ func handleAPISearch(w http.ResponseWriter, r *http.Request) {
return return
} }
if result.Matches, err = getTagMatches(cte); err != nil { if result.Matches, err = getTagMatches(tx, cte); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
if result.Related, err = getTagRelated(cte, if result.Related, err = getTagRelated(tx, cte,
len(result.Matches)); err != nil { len(result.Matches)); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@ -1203,7 +1169,47 @@ func handleAPISearch(w http.ResponseWriter, r *http.Request) {
} }
} }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // --- Web ---------------------------------------------------------------------
var hashRE = regexp.MustCompile(`^/.*?/([0-9a-f]{40})$`)
var staticHandler http.Handler
var page = template.Must(template.New("/").Parse(`<!DOCTYPE html><html><head>
<title>Gallery</title>
<meta charset="utf-8" />
<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>
<script src=mithril.js></script>
<script src=gallery.js></script>
</body></html>`))
func handleRequest(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
staticHandler.ServeHTTP(w, r)
return
}
if err := page.Execute(w, nil); err != nil {
log.Println(err)
}
}
func handleImages(w http.ResponseWriter, r *http.Request) {
if m := hashRE.FindStringSubmatch(r.URL.Path); m == nil {
http.NotFound(w, r)
} else {
http.ServeFile(w, r, imagePath(m[1]))
}
}
func handleThumbs(w http.ResponseWriter, r *http.Request) {
if m := hashRE.FindStringSubmatch(r.URL.Path); m == nil {
http.NotFound(w, r)
} else {
http.ServeFile(w, r, thumbPath(m[1]))
}
}
// cmdWeb runs a web UI against GD on ADDRESS. // cmdWeb runs a web UI against GD on ADDRESS.
func cmdWeb(fs *flag.FlagSet, args []string) error { func cmdWeb(fs *flag.FlagSet, args []string) error {