From 86402584c13afd3fd40423829acbcb934e061645 Mon Sep 17 00:00:00 2001 From: javi11 Date: Wed, 22 Apr 2026 21:14:06 +0200 Subject: [PATCH 1/2] fix(nzbdav): lowercase blob ID when constructing blob path nzbdav's C# BlobStore writes blob files using Guid.ToString("N") and Guid.ToString(), which always emit lowercase hex. The SQLite DB stores NzbBlobId uppercase (EF Core default TEXT Guid format), so on case-sensitive filesystems every os.Open failed with "no such file or directory" and the scan reported total_files: 0. Normalize the blob ID to lowercase before building the path, and add a regression test using a real uppercase-hyphenated UUID in the DB and a lowercase path on disk. Fixes the "Failed to open blob file" errors during NZBDav import scans. --- internal/nzbdav/parser.go | 3 +- internal/nzbdav/parser_test.go | 62 ++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/internal/nzbdav/parser.go b/internal/nzbdav/parser.go index 5934d72f..e79a447f 100644 --- a/internal/nzbdav/parser.go +++ b/internal/nzbdav/parser.go @@ -253,7 +253,8 @@ func (p *Parser) parseBlobs(db *sql.DB, tree *davTree, out chan<- *ParsedNzb, er parentPath := trimLastSegment(releaseParentPath) category, relPath := p.splitPath(parentPath) - blobPath := filepath.Join(p.blobsPath, blobId[0:2], blobId[2:4], blobId) + lowerBlobID := strings.ToLower(blobId) + blobPath := filepath.Join(p.blobsPath, lowerBlobID[0:2], lowerBlobID[2:4], lowerBlobID) blobFile, err := os.Open(blobPath) if err != nil { slog.ErrorContext(context.Background(), "Failed to open blob file", "path", blobPath, "error", err) diff --git a/internal/nzbdav/parser_test.go b/internal/nzbdav/parser_test.go index 4d65b1fb..f11842f0 100644 --- a/internal/nzbdav/parser_test.go +++ b/internal/nzbdav/parser_test.go @@ -235,6 +235,68 @@ func TestParser_Parse_Blobs(t *testing.T) { assert.Equal(t, nzbContent, string(body)) } +// TestParser_Parse_Blobs_UppercaseUUID verifies that blob IDs stored uppercase in +// the SQLite database (real nzbdav format, e.g. "0AA2BD24-B90C-4E06-A301-DD0D296AD86C") +// are matched against their lowercase on-disk layout, since nzbdav's C# BlobStore +// writes paths using Guid.ToString("N") which is always lowercase. +func TestParser_Parse_Blobs_UppercaseUUID(t *testing.T) { + tmpDir := t.TempDir() + blobsDir := filepath.Join(tmpDir, "blobs") + dbPath := filepath.Join(tmpDir, "blobs_upper.db") + db, err := sql.Open("sqlite3", dbPath) + require.NoError(t, err) + defer db.Close() + + _, err = db.Exec(` + CREATE TABLE DavItems ( + Id TEXT PRIMARY KEY, + ParentId TEXT, + Name TEXT, + FileSize INTEGER, + Path TEXT, + NzbBlobId TEXT, + SubType INTEGER + ); + CREATE TABLE NzbNames ( + Id TEXT PRIMARY KEY, + FileName TEXT + ); + `) + require.NoError(t, err) + + // DB stores the UUID uppercase with hyphens (default EF Core Guid TEXT format). + dbBlobID := "0AA2BD24-B90C-4E06-A301-DD0D296AD86C" + // Disk stores it lowercase (Guid.ToString("N") / Guid.ToString()). + diskBlobID := "0aa2bd24-b90c-4e06-a301-dd0d296ad86c" + blobPath := filepath.Join(blobsDir, diskBlobID[0:2], diskBlobID[2:4], diskBlobID) + nzbContent := ` + + + alt.binaries.test + msgid@test + +` + writeZstdBlob(t, blobPath, []byte(nzbContent)) + + _, err = db.Exec(` + INSERT INTO NzbNames (Id, FileName) VALUES (?, 'My Movie.nzb'); + INSERT INTO DavItems (Id, ParentId, Name, Path, NzbBlobId, SubType) VALUES + ('root', NULL, '/', '/', NULL, 1), + ('movies', 'root', 'movies', '/movies', NULL, 1), + ('folder', 'movies', 'My Movie', '/movies/My Movie', NULL, 1), + ('item1', 'folder', 'My Movie.mkv', '/movies/My Movie/My Movie.mkv', ?, 203); + `, dbBlobID, dbBlobID) + require.NoError(t, err) + + out, errChan := NewParser(dbPath, blobsDir).Parse() + got := collect(t, out, errChan) + + require.Len(t, got, 1) + assert.Equal(t, "item1", got[0].ID) + body, _ := io.ReadAll(got[0].Content) + assert.Equal(t, nzbContent, string(body)) +} + func TestParser_Parse_Blobs_Uncompressed(t *testing.T) { tmpDir := t.TempDir() blobsDir := filepath.Join(tmpDir, "blobs") From 2c82f810b9668be0b89d9f35ea0ad14463f8f04b Mon Sep 17 00:00:00 2001 From: javi11 Date: Wed, 22 Apr 2026 21:54:50 +0200 Subject: [PATCH 2/2] feat(nzbdav): skip post-import symlink/STRM creation and show migration steps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `skip_post_import_links` flag on import queue items so nzbdav migration imports don't create per-item symlinks or STRM files — library symlinks are rewritten separately in Phase 2. Surface the 4-step migration workflow (import, backup, verify mount, run migration) at the top of the NZBDav import UI. --- .../src/components/queue/ImportMethods.tsx | 14 ++++++ .../025_add_skip_post_import_links.sql | 9 ++++ .../sqlite/025_add_skip_post_import_links.sql | 9 ++++ internal/database/models.go | 1 + internal/database/queue_repository.go | 28 +++++------ internal/database/testing.go | 1 + .../importer/postprocessor/coordinator.go | 47 ++++++++++++------- .../coordinator_skip_links_test.go | 26 ++++++++++ internal/importer/scanner/nzbdav.go | 6 ++- 9 files changed, 107 insertions(+), 34 deletions(-) create mode 100644 internal/database/migrations/postgres/025_add_skip_post_import_links.sql create mode 100644 internal/database/migrations/sqlite/025_add_skip_post_import_links.sql create mode 100644 internal/importer/postprocessor/coordinator_skip_links_test.go diff --git a/frontend/src/components/queue/ImportMethods.tsx b/frontend/src/components/queue/ImportMethods.tsx index e19b7d4c..8c935ce1 100644 --- a/frontend/src/components/queue/ImportMethods.tsx +++ b/frontend/src/components/queue/ImportMethods.tsx @@ -8,6 +8,7 @@ import { FileIcon, FileText, FolderOpen, + Info, Link, Play, Search, @@ -777,6 +778,19 @@ function NzbDavImportSection() { return (
+
+
+ +

Migration Steps

+
+
    +
  1. Import the files
  2. +
  3. Backup library symlinks (very important)
  4. +
  5. Make sure AltMount mount is there
  6. +
  7. Run the symlink migration
  8. +
+
+