feat(admin): startup hash backfill and hash-conflict resolution API
Release / build-and-push (push) Successful in 2m48s
Release / build-and-push (push) Successful in 2m48s
Complete the SHA-256 lifecycle for preexisting databases: items imported before hashing existed get hashed automatically, and any content duplicates discovered in the process land on the new admin Hash Conflicts page for an explicit keep/merge decision. HashBackfillService (runs once 30s after startup, independent of auto-scan): - hashes every media_items row where file_sha256 IS NULL, resolving each path through LibraryService; per-item failures are logged and skipped so one unreadable file cannot block the pass - no-op once everything is hashed (logged and skipped) - finishes with a conflict sweep flagging every content-duplicate group via FindHashConflictGroups + CreateHashConflict; the sweep runs after the per-item pass because a preexisting pair only becomes detectable once both sides have their hash API (admin-only): - GET /api/admin/hash-conflicts - pending groups with member items and usage counts - POST /api/admin/hash-conflicts/:id/resolve - action=keep_all, or action=keep with keep_uuid: validates the uuid belongs to the group, re-parents every other copy's child rows onto the kept item (reparent_media_item_children), deletes the losers, and records the resolution + resolving admin; accepts form or JSON bodies and returns the htmx resolved fragment Page route /admin/hash-conflicts (admin-only) renders the template with hydrated conflict data; HashConflictsHandler wired into the router Config and constructed in main. Verified end-to-end against the live database: duplicate detection, pending listing, keep_all resolution, merge path (re-parent + delete), and - critically - a resolved group is not re-flagged by a later sweep (upsert no-op). Database restored afterward.
This commit is contained in:
@@ -47,6 +47,7 @@ type Config struct {
|
||||
MediaHandler *handlers.MediaHandler
|
||||
MatchingHandler *handlers.MatchingHandler
|
||||
ProcessingIssuesHandler *handlers.ProcessingIssuesHandler
|
||||
HashConflictsHandler *handlers.HashConflictsHandler
|
||||
KOReaderHandler *handlers.KOReaderHandler
|
||||
WSHandler *handlers.WSHandler
|
||||
ConflictHandler *handlers.ConflictHandler
|
||||
@@ -294,6 +295,17 @@ func RegisterRoutes(cfg *Config) *handlers.Handler {
|
||||
}
|
||||
}()
|
||||
|
||||
// One-time hash backfill: compute and store SHA-256 for media items
|
||||
// imported before hashing existed, then flag any content-duplicate groups
|
||||
// for admin review on the Hash Conflicts page. Runs independently of
|
||||
// auto-scan (it is a one-shot self-heal, not a recurring scan) and is a
|
||||
// no-op once every item is hashed. Delayed so it does not compete with
|
||||
// startup scans for disk I/O.
|
||||
go func() {
|
||||
time.Sleep(30 * time.Second)
|
||||
services.NewHashBackfillService(cfg.Queries).Run(context.Background())
|
||||
}()
|
||||
|
||||
// Register progress routes with actual handler
|
||||
registerProgressRoutes(cfg, scannerHandler)
|
||||
|
||||
@@ -301,5 +313,9 @@ func RegisterRoutes(cfg *Config) *handlers.Handler {
|
||||
admin := protected.Group("", handlers.AdminMiddleware)
|
||||
registerScannerRoutes(admin, scannerHandler)
|
||||
|
||||
// Hash conflict routes (admin only)
|
||||
admin.GET("/api/admin/hash-conflicts", cfg.HashConflictsHandler.ListHashConflicts)
|
||||
admin.POST("/api/admin/hash-conflicts/:id/resolve", cfg.HashConflictsHandler.ResolveHashConflict)
|
||||
|
||||
return scannerHandler
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user