feat(db): archive lifecycle schema and queries for missing media

Add the storage behind the archive-instead-of-delete lifecycle:

- media_items.missing_scan_count (INT, default 0) and archived_at
  (TIMESTAMPTZ, partial index), both as idempotent ADD COLUMN IF NOT
  EXISTS backfills for existing installs.
- MarkMediaItemMissing / ArchiveMediaItem / ClearMediaItemArchive plus
  PurgeExpiredArchivedMediaItems (retention cutoff) and
  PurgeAllArchivedMediaItems (manual bulk), with CountArchivedMediaItems
  for the admin UI.
- Archived items are hidden from every user-facing listing:
  ListMediaItems, ListMediaItemsByLibrary, ListMediaItemsSorted,
  SearchMediaItems, SearchMediaItemsUnified, the next_books CTE, the
  library media counts, and the search autocomplete value lists. Detail
  lookups by id/path/sha are intentionally unfiltered, and a dedicated
  ListMediaItemsByLibraryIncludingArchived feeds the scanner so the
  lifecycle pass can see and restore archived rows.
This commit is contained in:
John O'Keefe
2026-09-12 17:50:01 -04:00
parent 44b98f3fc3
commit e6aceae0da
5 changed files with 482 additions and 46 deletions
+15
View File
@@ -20,6 +20,9 @@ type Querier interface {
AddBookToKoboShelf(ctx context.Context, arg AddBookToKoboShelfParams) (KoboShelves, error)
// Library Folders queries
AddLibraryFolder(ctx context.Context, arg AddLibraryFolderParams) (LibraryFolders, error)
// Second consecutive missing scan: hide the item from all browsing while
// preserving reading history in case the file returns.
ArchiveMediaItem(ctx context.Context, id pgtype.UUID) error
// Bulk update format group for all media items
BulkUpdateFormatGroups(ctx context.Context) error
BulkUpdateProgressFromSync(ctx context.Context, arg BulkUpdateProgressFromSyncParams) ([]interface{}, error)
@@ -30,10 +33,14 @@ type Querier interface {
ClearDeviceSyncQueue(ctx context.Context, deviceID pgtype.UUID) error
ClearKoboShelf(ctx context.Context, deviceID pgtype.UUID) error
ClearKoboShelfByName(ctx context.Context, arg ClearKoboShelfByNameParams) error
// File is back on disk (by path or content hash): restore visibility and
// reset the missing-scan counter. No-op for items that were never archived.
ClearMediaItemArchive(ctx context.Context, id pgtype.UUID) error
// Reset an item to scanned defaults: clears per-field user overrides so the
// next rescan can freely overwrite user-customized metadata.
ClearMediaItemMetadataOverrides(ctx context.Context, id pgtype.UUID) error
CountAdmins(ctx context.Context) (int64, error)
CountArchivedMediaItems(ctx context.Context) (int64, error)
// Count unlinked books for a device
CountUnlinkedBooks(ctx context.Context, deviceID pgtype.UUID) (int64, error)
CountUserDevices(ctx context.Context, userID pgtype.UUID) (int64, error)
@@ -343,6 +350,7 @@ type Querier interface {
ListLibraries(ctx context.Context) ([]ListLibrariesRow, error)
ListMediaItems(ctx context.Context, arg ListMediaItemsParams) ([]ListMediaItemsRow, error)
ListMediaItemsByLibrary(ctx context.Context, libraryID pgtype.UUID) ([]ListMediaItemsByLibraryRow, error)
ListMediaItemsByLibraryIncludingArchived(ctx context.Context, libraryID pgtype.UUID) ([]ListMediaItemsByLibraryIncludingArchivedRow, error)
// List all media items sharing a SHA-256 hash within a library (hash conflict group)
ListMediaItemsBySHA256AndLibrary(ctx context.Context, arg ListMediaItemsBySHA256AndLibraryParams) ([]MediaItems, error)
// List media items that have no stored SHA-256 (imported before hashing existed)
@@ -356,6 +364,13 @@ type Querier interface {
// List unresolved unlinked books with pagination
ListUnresolvedUnlinkedBooks(ctx context.Context, arg ListUnresolvedUnlinkedBooksParams) ([]ListUnresolvedUnlinkedBooksRow, error)
ListUsers(ctx context.Context) ([]ListUsersRow, error)
// First consecutive scan that cannot find the file on disk.
MarkMediaItemMissing(ctx context.Context, id pgtype.UUID) error
// Manual bulk purge from the library admin page.
PurgeAllArchivedMediaItems(ctx context.Context) ([]PurgeAllArchivedMediaItemsRow, error)
// Retention sweep at library-scan time: hard-delete archived items older
// than the cutoff. Cascades remove reading history with the row.
PurgeExpiredArchivedMediaItems(ctx context.Context, archivedAt pgtype.Timestamptz) ([]PurgeExpiredArchivedMediaItemsRow, error)
PurgeExpiredBookmarkTombstones(ctx context.Context, deletedAt pgtype.Timestamptz) error
PurgeExpiredHighlightTombstones(ctx context.Context, deletedAt pgtype.Timestamptz) error
PurgeExpiredNoteTombstones(ctx context.Context, deletedAt pgtype.Timestamptz) error