From fc19fa9b4915ccdd5ccafc0defb1088b8fcc191a Mon Sep 17 00:00:00 2001 From: Přemysl Eric Janouch Date: Tue, 26 Dec 2023 09:45:50 +0100 Subject: Make it possible to sync to basenames --- main.go | 167 +++++++++++++++++++++++++++++++++++++--------------------------- 1 file changed, 97 insertions(+), 70 deletions(-) diff --git a/main.go b/main.go index 546e3ff..db57896 100644 --- a/main.go +++ b/main.go @@ -1545,7 +1545,7 @@ func syncDirectory(c *syncContext, dbParent int64, fsPath string) error { return nil } -func syncRoot(c *syncContext, fsPath string) error { +func syncRoot(c *syncContext, dbPath []string, fsPath string) error { // TODO: Support synchronizing individual files. // This can only be treated as 0 → F, F → F, or D → F, that is, // a variation on current syncEnqueue(), but dbParent must be nullable. @@ -1554,8 +1554,7 @@ func syncRoot(c *syncContext, fsPath string) error { // also because we don't know yet if the argument is a directory). // // Synchronizing F → D or * → F are special cases not worth implementing. - crumbs := decodeWebPath(filepath.ToSlash(fsPath)) - dbParent, err := idForDirectoryPath(c.tx, crumbs, true) + dbParent, err := idForDirectoryPath(c.tx, dbPath, true) if err != nil { return err } @@ -1610,6 +1609,60 @@ func syncRoot(c *syncContext, fsPath string) error { } } +type syncPath struct { + db []string // database path, in terms of nodes + fs string // normalized filesystem path +} + +// syncResolveRoots normalizes filesystem paths given in command line arguments, +// and figures out a database path for each. Duplicates are skipped or rejected. +func syncResolveRoots(args []string, fullpaths bool) ( + roots []*syncPath, err error) { + for i := range args { + fs, err := filepath.Abs(filepath.Clean(args[i])) + if err != nil { + return nil, err + } + + roots = append(roots, + &syncPath{decodeWebPath(filepath.ToSlash(fs)), fs}) + } + + if fullpaths { + // Filter out duplicates. In this case, they're just duplicated work. + slices.SortFunc(roots, func(a, b *syncPath) int { + return strings.Compare(a.fs, b.fs) + }) + roots = slices.CompactFunc(roots, func(a, b *syncPath) bool { + if a.fs != b.fs && !strings.HasPrefix(b.fs, a.fs+"/") { + return false + } + log.Printf("asking to sync path twice: %s\n", b.fs) + return true + }) + } else { + // Keep just the basenames. + for _, path := range roots { + if len(path.db) > 0 { + path.db = path.db[len(path.db)-1:] + } + } + + // Different filesystem paths mapping to the same DB location + // are definitely a problem we would like to avoid, + // otherwise we don't care. + slices.SortFunc(roots, func(a, b *syncPath) int { + return slices.Compare(a.db, b.db) + }) + for i := 1; i < len(roots); i++ { + if slices.Equal(roots[i-1].db, roots[i].db) { + return nil, fmt.Errorf("duplicate root: %v", roots[i].db) + } + } + } + return +} + const disposeCTE = `WITH RECURSIVE root(id, sha1, parent, path) AS ( SELECT id, sha1, parent, name FROM node WHERE id = ? @@ -1636,11 +1689,48 @@ const disposeCTE = `WITH RECURSIVE HAVING count = total )` -func syncRun(ctx context.Context, tx *sql.Tx, roots []string) error { +// cmdSync ensures the given (sub)roots are accurately reflected +// in the database. +func cmdSync(fs *flag.FlagSet, args []string) error { + fullpaths := fs.Bool("fullpaths", false, "don't basename arguments") + if err := fs.Parse(args); err != nil { + return err + } + if fs.NArg() < 2 { + return errWrongUsage + } + if err := openDB(fs.Arg(0)); err != nil { + return err + } + + roots, err := syncResolveRoots(fs.Args()[1:], *fullpaths) + if err != nil { + return err + } + + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) + defer stop() + + // In case of a failure during processing, the only retained side effects + // on the filesystem tree are: + // - Fixing dead symlinks to images. + // - Creating symlinks to images that aren't used by anything. + tx, err := db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + + // Mild hack: upgrade the transaction to a write one straight away, + // in order to rule out deadlocks (preventable failure). + if _, err := tx.Exec(`END TRANSACTION; + BEGIN IMMEDIATE TRANSACTION`); err != nil { + return err + } + c := syncContext{ctx: ctx, tx: tx, pb: newProgressBar(-1)} defer c.pb.Stop() - var err error if c.stmtOrphan, err = c.tx.Prepare(disposeCTE + ` INSERT OR IGNORE INTO orphan(sha1, path) SELECT sha1, path FROM orphaned`); err != nil { @@ -1678,74 +1768,11 @@ func syncRun(ctx context.Context, tx *sql.Tx, roots []string) error { // i.e., it is for the result of the task that syncEnqueue() spawns. c.info = make(chan syncFileInfo, cap(taskSemaphore)+1) - for _, path := range roots { - if err := syncRoot(&c, path); err != nil { + for _, root := range roots { + if err := syncRoot(&c, root.db, root.fs); err != nil { return err } } - return nil -} - -// cmdSync ensures the given (sub)roots are accurately reflected -// in the database. -func cmdSync(fs *flag.FlagSet, args []string) error { - fullpaths := fs.Bool("fullpaths", false, "don't basename the arguments") - if err := fs.Parse(args); err != nil { - return err - } - if fs.NArg() < 2 { - return errWrongUsage - } - if err := openDB(fs.Arg(0)); err != nil { - return err - } - - ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) - defer stop() - - // In case of a failure during processing, the only retained side effects - // on the filesystem tree are: - // - Fixing dead symlinks to images. - // - Creating symlinks to images that aren't used by anything. - tx, err := db.BeginTx(ctx, nil) - if err != nil { - return err - } - defer tx.Rollback() - - // Mild hack: upgrade the transaction to a write one straight away, - // in order to rule out deadlocks (preventable failure). - if _, err := tx.Exec(`END TRANSACTION; - BEGIN IMMEDIATE TRANSACTION`); err != nil { - return err - } - - // TODO: Handle sensibly. - _ = fullpaths - - // Normalize arguments. - // At least for now, turn all roots into absolute paths. - roots := fs.Args()[1:] - for i := range roots { - roots[i], err = filepath.Abs(filepath.Clean(roots[i])) - if err != nil { - return err - } - } - - // Filter out duplicates. - sort.Strings(roots) - roots = slices.CompactFunc(roots, func(a, b string) bool { - if a != b && !strings.HasPrefix(b, a+"/") { - return false - } - log.Printf("asking to sync path twice: %s\n", b) - return true - }) - - if err := syncRun(ctx, tx, roots); err != nil { - return err - } return tx.Commit() } -- cgit v1.2.3-70-g09d2