Skip to content

feat(par2): generate single PAR2 set per folder with relative paths in FileDesc#225

Merged
javi11 merged 3 commits into
mainfrom
feat/folder-structure-par2-filenames
May 11, 2026
Merged

feat(par2): generate single PAR2 set per folder with relative paths in FileDesc#225
javi11 merged 3 commits into
mainfrom
feat/folder-structure-par2-filenames

Conversation

@javi11
Copy link
Copy Markdown
Owner

@javi11 javi11 commented May 10, 2026

Summary

Fixes #219 — SABnzbd and NZBGet now reconstruct the original folder hierarchy after downloading a postie-generated NZB, matching the behavior nyuu users expect.

Root cause

SABnzbd's quick_check_set() reads the filename field of each PAR2 FileDesc packet and calls renamer(..., create_local_directories=True) with that value. If the field holds a relative path like folder2/file.txt, the subdirectory is recreated on disk. If it holds only file.txt, everything lands flat.

Two bugs conspired:

  1. par2go v0.0.7 hardcoded filepath.Base(path) in quickScanFile, so FileDesc always contained bare filenames.
  2. postie ran CreateInDirectory once per file, producing N independent PAR2 sets instead of one set covering the whole folder. A single shared set is required to carry the folder-relative paths.

What changed

New CreateSet method on Par2Executor

internal/par2/par2.go (NativeExecutor) and internal/par2/binary.go (BinaryExecutor) both gain:

CreateSet(ctx context.Context, files []fileinfo.FileInfo, outputDir, setName string) ([]string, error)
  • NativeExecutor uses the new par2go v0.0.8 CreateWithNames API. Each input file is paired with its RelativePath (forward-slash separated) so par2go embeds that path verbatim in the FileDesc packet.
  • BinaryExecutor invokes parpar with --filepath-base <commonRoot> --filepath-format outrel so parpar derives the relative path automatically from each file's disk path.
  • checkExistingPar2SetInPath detects a pre-existing set by name to skip redundant regeneration.

Caller — pkg/postie/postie.go

postFolder replaces the two separate CreateInDirectory calls with a single CreateSet(ctx, files, par2OutputDir, folderName).

Dependency

github.com/javi11/par2go bumped from v0.0.7 → v0.0.8, which adds InputFile / CreateWithNames (PR javi11/par2go#5).

Tests

File What it tests
internal/par2/par2_integration_test.go Creates a real folder1/folder2/file.txt tree, runs NativeExecutor.CreateSet, parses the resulting PAR2 binary, asserts each FileDesc contains the correct relative path
pkg/postie/postfolder_test.go mockPar2Executor gains a CreateSet stub so existing unit tests compile and pass

Backward compatibility

  • Single-file posts (postFile) still use the existing per-file CreateInDirectory path — no behaviour change.
  • MaintainPar2Files mode continues to work; CreateSet respects the same outputDir routing.
  • Pre-existing PAR2 files in the source folder are still detected and reused.
Screenshot 2026-05-11 at 09 01 15

javi11 added 3 commits May 10, 2026 16:57
…eDesc

Fixes #219 — SABnzbd/NZBGet now reconstruct the original folder tree when
downloading a postie-generated NZB, matching the behavior of nyuu.

## Root cause

* SABnzbd's `quick_check_set()` reads PAR2 FileDesc filenames and calls
  `renamer(..., create_local_directories=True)` using those names.
  If the FileDesc contains a relative path (e.g. `folder2/file.txt`)
  the directory is recreated on disk.
* par2go v0.0.7 hardcoded `filepath.Base(path)` in `quickScanFile`, so
  every FileDesc carried only the bare filename — no folder tree.
* postie also generated a separate PAR2 set per file instead of one set
  for the whole folder, so there was no shared set to carry the tree.

## Changes

### `Par2Executor` interface — new `CreateSet` method
`internal/par2/par2.go` and `internal/par2/binary.go` gain a
`CreateSet(ctx, files, outputDir, setName)` entrypoint that generates
**one PAR2 set covering all files** in a folder, with each FileDesc
carrying the file's `RelativePath` (forward-slash separated).

* `NativeExecutor.CreateSet` uses the new `par2go v0.0.8`
  `CreateWithNames` API to embed relative paths in FileDesc packets.
* `BinaryExecutor.CreateSet` invokes parpar with
  `--filepath-base <commonRoot> --filepath-format outrel` so parpar
  derives the relative path from each input's disk path.
* `checkExistingPar2SetInPath` detects pre-existing sets by set name,
  avoiding redundant regeneration.

### Caller update — `pkg/postie/postie.go`
`postFolder` now calls `CreateSet` with the folder name as the set name
instead of two separate `CreateInDirectory` calls.

### Dependency bump
`github.com/javi11/par2go` v0.0.7 → v0.0.8, which adds
`InputFile` / `CreateWithNames` for per-file logical name overrides.

### Tests
* `internal/par2/par2_integration_test.go` — integration test that
  creates a real multi-file folder tree, runs `NativeExecutor.CreateSet`,
  then parses the resulting PAR2 binary to verify each FileDesc contains
  the correct relative path (e.g. `folder2/file2.txt`).
* `pkg/postie/postfolder_test.go` — `mockPar2Executor` gains a
  `CreateSet` stub so existing postFolder unit tests continue to compile
  and pass.
- Add `folderDir string` parameter to `Par2Executor.CreateSet` interface
  so both executors receive the on-disk root of the folder being posted.

- NativeExecutor: compute each file's FileDesc name via
  `filepath.Rel(folderDir, file.Path)` instead of using RelativePath
  verbatim. RelativePath includes the top-level folder name as a prefix
  (e.g. "ShowS01/extras/bonus.mkv") which would cause SABnzbd to
  double-nest the reconstructed directory tree. The Rel-based path
  produces "extras/bonus.mkv" as SABnzbd expects.

- BinaryExecutor: pass folderDir directly as --filepath-base instead of
  the fragile commonParentDir() derivation, which silently dropped the
  subdirectory prefix when all input files were in the same subdir.
  Remove the now-unused commonParentDir / commonPrefixDir helpers.

- postie.go: both CreateSet call sites now derive
  `folderDir = filepath.Join(rootDir, folderName)` and forward it.

- Integration test: update expected FileDesc names to reflect the fix
  and add TestIntegration_NativeExecutor_CreateSet_AllFilesInSubdir to
  guard the single-subdirectory edge case.
@javi11 javi11 merged commit 3ac5d93 into main May 11, 2026
3 checks passed
@javi11 javi11 deleted the feat/folder-structure-par2-filenames branch May 11, 2026 07:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Folder Structure not preserved

1 participant