From c471a64ab8a4d3e90fb0050b5b4f68a187c2b02e Mon Sep 17 00:00:00 2001 From: Přemysl Eric Janouch Date: Sun, 17 Dec 2023 20:44:03 +0100 Subject: Implement the check command --- main.go | 125 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file 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 } -- cgit v1.2.3-70-g09d2