fix(converter): preserve OriginalName through conversion and fix race

- Fix critical bug: convertPageFile and splitAndConvert now copy
  OriginalName to the output PageFile so --keep-filenames works
  end-to-end on actually-converted pages.
- Fix concurrency race: ExtractChapter deduplicates OriginalName when
  source archive has same base filename in different subdirectories.
- Fix collision fallback: resolvePageName loops until an unused
  indexed name is found instead of single-shot assignment.
- Fix gofmt indentation in optimize_command.go.
This commit is contained in:
Antoine Aflalo
2026-07-21 19:45:25 -04:00
parent f3ba585c13
commit 21f92bc6f5
5 changed files with 147 additions and 19 deletions
+40 -1
View File
@@ -168,6 +168,18 @@ func ExtractChapter(ctx context.Context, filePath string, keepFilenames bool) (*
TempDir: tempDir,
}
// usedOriginalNames tracks every OriginalName handed out in this chapter
// so it stays unique across pages, even when the source archive contains
// the same base filename in different subdirectories (e.g. vol1/page.png
// and vol2/page.png — legal in zip). Uniqueness at the extraction
// boundary prevents a downstream race in pkg/converter/webp, where two
// pages with the same stem would write the same intermediate file.
// Allocated lazily so the keepFilenames=false path stays allocation-free.
var usedOriginalNames map[string]struct{}
if keepFilenames {
usedOriginalNames = make(map[string]struct{})
}
// For CBZ files, read metadata from zip comment
pathLower := strings.ToLower(filepath.Ext(filePath))
if pathLower == ".cbz" {
@@ -286,7 +298,7 @@ func ExtractChapter(ctx context.Context, filePath string, keepFilenames bool) (*
FilePath: outputPath,
}
if keepFilenames {
page.OriginalName = filepath.Base(path)
page.OriginalName = allocateUniqueBaseName(filepath.Base(path), pageIndex, usedOriginalNames)
}
chapter.Pages = append(chapter.Pages, page)
@@ -328,6 +340,33 @@ func isJunkFile(path string) bool {
return false
}
// allocateUniqueBaseName returns baseName when it is not already taken by an
// earlier page in this chapter, or a collision-resolved variant otherwise.
//
// On collision the resolved name matches the existing fallback style used by
// cbz_creator's resolvePageName: stem + "_%04d" + extension. The starting
// suffix is pageIndex so the resolved name stays in the same neighborhood as
// the page's archive position. If that candidate is itself already taken
// (astronomically rare: the source archive would need both the original name
// and a matching indexed name), the suffix is incremented until a free name
// is found. The chosen name is recorded in usedNames so subsequent calls
// cannot pick it again.
func allocateUniqueBaseName(baseName string, pageIndex uint16, usedNames map[string]struct{}) string {
if _, taken := usedNames[baseName]; !taken {
usedNames[baseName] = struct{}{}
return baseName
}
ext := filepath.Ext(baseName)
stem := strings.TrimSuffix(baseName, ext)
for suffix := int(pageIndex); ; suffix++ {
candidate := fmt.Sprintf("%s_%04d%s", stem, suffix, ext)
if _, taken := usedNames[candidate]; !taken {
usedNames[candidate] = struct{}{}
return candidate
}
}
}
// LoadChapter extracts the chapter from a CBZ/CBR file to disk.
// It delegates to ExtractChapter with keepFilenames=false and always
// extracts all pages. Use IsAlreadyConverted for a fast conversion status