From 2011f2b198871b6acb4bbc50012b8bd6801b6a7f Mon Sep 17 00:00:00 2001 From: Přemysl Eric Janouch Date: Thu, 21 Dec 2023 04:48:56 +0100 Subject: WIP: FS to DB sync --- main.go | 79 +++++++++++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 57 insertions(+), 22 deletions(-) (limited to 'main.go') diff --git a/main.go b/main.go index 8a3de6d..897ae1e 100644 --- a/main.go +++ b/main.go @@ -134,6 +134,11 @@ func (pb *progressBar) Stop() { } func (pb *progressBar) update() { + if pb.target < 0 { + fmt.Printf("\r%d/?", pb.current) + return + } + var fraction int if pb.target != 0 { fraction = int(float32(pb.current) / float32(pb.target) * 100) @@ -806,6 +811,7 @@ type syncContext struct { ctx context.Context tx *sql.Tx info chan syncFileInfo + pb *progressBar } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -915,17 +921,12 @@ func syncDequeue(c *syncContext) error { } } -// TODO: [[B]]: A function to collect references. -// -// - This is almost always combined with [[A1]] or [[A2]], -// so perhaps combine these. +// TODO: Implement. // // - When collecting node subtrees, we need to delete bottom-up // because of foreign key constraints, // so maybe in reverse order of recursive CTE results. // -// - Orphans will keep their GD/thumbs file, as evidence. -// // - Sadly, this can't be done with a DB trigger. (What and why?) // // - One of the inputs needs to be the FS path, for the orphan table. @@ -933,20 +934,51 @@ func syncDequeue(c *syncContext) error { // syncDispose creates orphan records for the entire subtree given by nodeID // as appropriate, then deletes all nodes within the subtree. The subtree root // node is not deleted if "keepNode" is true. +// +// Orphans keep their thumbnail files, as evidence. func syncDispose(c *syncContext, nodeID int64, keepNode bool) error { return nil } +func syncImage(c *syncContext, info syncFileInfo) error { + if _, err := c.tx.Exec(`INSERT INTO image(sha1, width, height) + VALUES (?, ?, ?) ON CONFLICT(sha1) DO NOTHING`, + info.sha1, info.width, info.height); err != nil { + return err + } + + // Fast path: it may already there, and not be a dead symlink. + path := imagePath(info.sha1) + if _, err := os.Stat(path); err == nil { + return nil + } + + dirname, _ := filepath.Split(path) + if err := os.MkdirAll(dirname, 0755); err != nil { + return err + } + + for { + err := os.Symlink(info.fsPath, path) + if !errors.Is(err, fs.ErrExist) { + return err + } + + // Try to remove anything standing in the way, and try again. + if err = os.Remove(path); err != nil { + return err + } + } +} + func syncPostProcess(c *syncContext, info syncFileInfo) error { - // TODO: - // - When replacing a file, which needn't become orphaned, - // we could offer copying all tags, but this needs another table - // to track it (if it's equivalent enough, the dhash will stay the same, - // so user can resolve this through the duplicates feature). - // - When creating or updating a node, - // try inserting an image record first as well. - // And test that we have a symlink, and that it works. - // (Just do os.Stat() on it, which fails on both dead and missing links.) + defer c.pb.Step() + + // TODO: When replacing an image node (whether it has or doesn't have + // other links to keep it alive), we could offer copying all tags, + // though this needs another table to track it. + // (If it's equivalent enough, the dhash will stay the same, + // so user can resolve this through the duplicates feature.) switch { case info.err != nil: // * → error @@ -963,8 +995,9 @@ func syncPostProcess(c *syncContext, info syncFileInfo) error { case info.dbID == 0: // 0 → F - // TODO: Add an image entry. - + if err := syncImage(c, info); err != nil { + return err + } if _, err := c.tx.Exec(`INSERT INTO node(parent, name, mtime, sha1) VALUES (?, ?, ?, ?)`, info.dbParent, info.dbName, info.fsMtime, info.sha1); err != nil { @@ -978,11 +1011,10 @@ func syncPostProcess(c *syncContext, info syncFileInfo) error { return err } - // TODO: Ideally, do nothing if the hash doesn't change. - // But that will be a rare situation. - - // TODO: Add an image entry. - + // Even if the hash didn't change, we may fix any broken symlinks. + if err := syncImage(c, info); err != nil { + return err + } if _, err := c.tx.Exec(`UPDATE node SET mtime = ?, sha1 = ? WHERE id = ?`, info.fsMtime, info.sha1, info.dbID); err != nil { return err @@ -1098,6 +1130,9 @@ func syncDirectory(c *syncContext, dbParent int64, fsPath string) error { func syncArgument(ctx context.Context, tx *sql.Tx, path string) error { c := syncContext{ctx: ctx, tx: tx} + c.pb = newProgressBar(-1) + defer c.pb.Stop() + // Info tasks take a position in the task semaphore channel. // then fill the info channel. // -- cgit v1.2.3-70-g09d2