aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--main.go125
1 files changed, 122 insertions, 3 deletions
diff --git a/main.go b/main.go
index 44214b9..0a96007 100644
--- a/main.go
+++ b/main.go
@@ -22,6 +22,7 @@ import (
"path/filepath"
"regexp"
"runtime"
+ "slices"
"strconv"
"strings"
"sync"
@@ -853,7 +854,74 @@ func cmdTag(args []string) error {
// --- Check -------------------------------------------------------------------
-// cmdCheck checks if all files tracked in the DB are accessible.
+func isValidSHA1(hash string) bool {
+ if len(hash) != sha1.Size*2 || strings.ToLower(hash) != hash {
+ return false
+ }
+ if _, err := hex.DecodeString(hash); err != nil {
+ return false
+ }
+ return true
+}
+
+func hashesToFileListing(root, suffix string, hashes []string) []string {
+ // Note that we're semi-duplicating {image,thumb}Path().
+ paths := []string{root}
+ for _, hash := range hashes {
+ dir := filepath.Join(root, hash[:2])
+ paths = append(paths, dir, filepath.Join(dir, hash+suffix))
+ }
+ slices.Sort(paths)
+ return slices.Compact(paths)
+}
+
+func collectFileListing(root string) (paths []string, err error) {
+ err = filepath.WalkDir(root,
+ func(path string, d fs.DirEntry, err error) error {
+ paths = append(paths, path)
+ return err
+ })
+
+ // Even though it should already be sorted somehow.
+ slices.Sort(paths)
+ return
+}
+
+func checkFiles(root, suffix string, hashes []string) (bool, []string, error) {
+ db := hashesToFileListing(root, suffix, hashes)
+ fs, err := collectFileListing(root)
+ if err != nil {
+ return false, nil, err
+ }
+
+ iDB, iFS, ok, intersection := 0, 0, true, []string{}
+ for iDB < len(db) && iFS < len(fs) {
+ if db[iDB] == fs[iFS] {
+ intersection = append(intersection, db[iDB])
+ iDB++
+ iFS++
+ } else if db[iDB] < fs[iFS] {
+ ok = false
+ fmt.Printf("only in DB: %s\n", db[iDB])
+ iDB++
+ } else {
+ ok = false
+ fmt.Printf("only in FS: %s\n", fs[iFS])
+ iFS++
+ }
+ }
+ for _, path := range db[iDB:] {
+ ok = false
+ fmt.Printf("only in DB: %s\n", path)
+ }
+ for _, path := range fs[iFS:] {
+ ok = false
+ fmt.Printf("only in FS: %s\n", path)
+ }
+ return ok, intersection, nil
+}
+
+// cmdCheck carries out various database consistency checks.
func cmdCheck(args []string) error {
if len(args) != 1 {
return errors.New("usage: GD")
@@ -862,8 +930,59 @@ func cmdCheck(args []string) error {
return err
}
- // TODO: Check if all hashes of DB entries have a statable image file,
- // and that all images with thumb{w,h} have a thumbnail file. Perhaps.
+ // Check if hashes are in the right format.
+ log.Println("checking image hashes")
+
+ allSHA1, err := dbCollectStrings(`SELECT sha1 FROM image`)
+ if err != nil {
+ return err
+ }
+
+ ok := true
+ for _, hash := range allSHA1 {
+ if !isValidSHA1(hash) {
+ ok = false
+ fmt.Printf("invalid image SHA1: %s\n", hash)
+ }
+ }
+
+ // This is, rather obviously, just a strict subset.
+ // Although it doesn't run in the same transaction.
+ thumbSHA1, err := dbCollectStrings(`SELECT sha1 FROM image
+ WHERE thumbw IS NOT NULL OR thumbh IS NOT NULL`)
+ if err != nil {
+ return err
+ }
+
+ // This somewhat duplicates {image,thumb}Path().
+ log.Println("checking SQL against filesystem")
+ okImages, intersection, err := checkFiles(
+ filepath.Join(galleryDirectory, "images"), "", allSHA1)
+ if err != nil {
+ return err
+ }
+
+ okThumbs, _, err := checkFiles(
+ filepath.Join(galleryDirectory, "thumbs"), ".webp", thumbSHA1)
+ if err != nil {
+ return err
+ }
+ if !okImages || !okThumbs {
+ ok = false
+ }
+
+ // NOTE: We could also compare mtime, and on mismatch the current SHA1,
+ // though that's more of a "sync" job.
+ log.Println("checking for dead symlinks")
+ for _, path := range intersection {
+ if _, err := os.Stat(path); err != nil {
+ ok = false
+ fmt.Printf("%s: %s\n", path, err)
+ }
+ }
+ if !ok {
+ return errors.New("detected inconsistencies")
+ }
return nil
}