diff --git a/cbconvert.go b/cbconvert.go index a4c4021..f85bcd1 100644 --- a/cbconvert.go +++ b/cbconvert.go @@ -93,6 +93,8 @@ type Converter struct { Workdir string // Page name prefix, set per input when combining prefix string + // Input root for the current file, used to build recursive output paths + root string // Number of files Nfiles int // Index of the current file @@ -115,6 +117,7 @@ type Converter struct { type File struct { Name string Path string + Root string Stat os.FileInfo SizeHuman string } @@ -157,11 +160,13 @@ func (c *Converter) Cancel() { // Files returns list of found comic files. func (c *Converter) Files(args []string) ([]File, error) { var files []File + var root string toFile := func(fp string, f os.FileInfo) File { var file File file.Name = filepath.Base(fp) file.Path = fp + file.Root = root file.Stat = f file.SizeHuman = humanize.IBytes(uint64(f.Size())) return file @@ -214,12 +219,14 @@ func (c *Converter) Files(args []string) ([]File, error) { } if !stat.IsDir() { + root = filepath.Dir(path) if isArchive(path) || isDocument(path) { if isSize(int64(c.Opts.Size), stat.Size()) { files = append(files, toFile(path, stat)) } } } else { + root = path if c.Opts.Recursive { if err := filepath.Walk(path, walkFiles); err != nil { return files, fmt.Errorf("%s: %w", arg, err) @@ -261,9 +268,26 @@ func (c *Converter) Files(args []string) ([]File, error) { return files, nil } +// recursiveDir mirrors the source path under OutDir, relative to the input root. +func (c *Converter) recursiveDir(fileName string) string { + dir := filepath.Dir(fileName) + + if c.root != "" { + if rel, err := filepath.Rel(c.root, dir); err == nil { + return filepath.Join(c.Opts.OutDir, rel) + } + } + + dir = strings.TrimPrefix(dir[len(filepath.VolumeName(dir)):], string(os.PathSeparator)) + + return filepath.Join(c.Opts.OutDir, dir) +} + // Cover extracts cover. -func (c *Converter) Cover(fileName string, fileInfo os.FileInfo) error { +func (c *Converter) Cover(file File) error { c.CurrFile++ + c.root = file.Root + fileName, fileInfo := file.Path, file.Stat cover, err := c.coverImage(fileName, fileInfo) if err != nil { @@ -285,13 +309,12 @@ func (c *Converter) Cover(fileName string, fileInfo os.FileInfo) error { var fName string if c.Opts.Recursive { - fDir := strings.Split(filepath.Dir(fileName), string(os.PathSeparator))[1:] - err := os.MkdirAll(filepath.Join(c.Opts.OutDir, filepath.Join(fDir...)), 0755) - if err != nil { + outDir := c.recursiveDir(fileName) + if err := os.MkdirAll(outDir, 0755); err != nil { return fmt.Errorf("%s: %w", fileName, err) } - fName = filepath.Join(c.Opts.OutDir, filepath.Join(fDir...), fmt.Sprintf("%s.%s", baseNoExt(fileName), ext)) + fName = filepath.Join(outDir, fmt.Sprintf("%s.%s", baseNoExt(fileName), ext)) } else { fName = filepath.Join(c.Opts.OutDir, fmt.Sprintf("%s.%s", baseNoExt(fileName), ext)) } @@ -310,8 +333,10 @@ func (c *Converter) Cover(fileName string, fileInfo os.FileInfo) error { } // Thumbnail extracts thumbnail. -func (c *Converter) Thumbnail(fileName string, fileInfo os.FileInfo) error { +func (c *Converter) Thumbnail(file File) error { c.CurrFile++ + c.root = file.Root + fileName, fileInfo := file.Path, file.Stat cover, err := c.coverImage(fileName, fileInfo) if err != nil { @@ -352,13 +377,12 @@ func (c *Converter) Thumbnail(fileName string, fileInfo os.FileInfo) error { fURI = "file://" + fileName if c.Opts.Recursive { - fDir := strings.Split(filepath.Dir(fileName), string(os.PathSeparator))[1:] - err := os.MkdirAll(filepath.Join(c.Opts.OutDir, filepath.Join(fDir...)), 0755) - if err != nil { + outDir := c.recursiveDir(fileName) + if err := os.MkdirAll(outDir, 0755); err != nil { return fmt.Errorf("%s: %w", fileName, err) } - fName = filepath.Join(c.Opts.OutDir, filepath.Join(fDir...), fmt.Sprintf("%x.png", md5.Sum([]byte(fURI)))) + fName = filepath.Join(outDir, fmt.Sprintf("%x.png", md5.Sum([]byte(fURI)))) } else { fName = filepath.Join(c.Opts.OutDir, fmt.Sprintf("%x.png", md5.Sum([]byte(fURI)))) } @@ -496,8 +520,10 @@ func (c *Converter) Preview(fileName string, fileInfo os.FileInfo, width, height } // Convert converts a comic book. -func (c *Converter) Convert(fileName string, fileInfo os.FileInfo) error { +func (c *Converter) Convert(file File) error { c.CurrFile++ + c.root = file.Root + fileName, fileInfo := file.Path, file.Stat ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -541,6 +567,8 @@ func (c *Converter) Combine(files []File) error { return nil } + c.root = "" + ctx, cancel := context.WithCancel(context.Background()) defer cancel() diff --git a/cbconvert_archive.go b/cbconvert_archive.go index d7baa2e..0fe3eac 100644 --- a/cbconvert_archive.go +++ b/cbconvert_archive.go @@ -33,13 +33,12 @@ func (c *Converter) archiveSaveZip(fileName string) error { var zipName string if c.Opts.Recursive { - fDir := strings.Split(filepath.Dir(fileName), string(os.PathSeparator))[1:] - err := os.MkdirAll(filepath.Join(c.Opts.OutDir, filepath.Join(fDir...)), 0755) - if err != nil { + outDir := c.recursiveDir(fileName) + if err := os.MkdirAll(outDir, 0755); err != nil { return fmt.Errorf("archiveSaveZip: %w", err) } - zipName = filepath.Join(c.Opts.OutDir, filepath.Join(fDir...), fmt.Sprintf("%s%s.cbz", baseNoExt(fileName), c.Opts.Suffix)) + zipName = filepath.Join(outDir, fmt.Sprintf("%s%s.cbz", baseNoExt(fileName), c.Opts.Suffix)) } else { zipName = filepath.Join(c.Opts.OutDir, fmt.Sprintf("%s%s.cbz", baseNoExt(fileName), c.Opts.Suffix)) } @@ -108,13 +107,12 @@ func (c *Converter) archiveSaveTar(fileName string) error { var tarName string if c.Opts.Recursive { - fDir := strings.Split(filepath.Dir(fileName), string(os.PathSeparator))[1:] - err := os.MkdirAll(filepath.Join(c.Opts.OutDir, filepath.Join(fDir...)), 0755) - if err != nil { + outDir := c.recursiveDir(fileName) + if err := os.MkdirAll(outDir, 0755); err != nil { return fmt.Errorf("archiveSaveTar: %w", err) } - tarName = filepath.Join(c.Opts.OutDir, filepath.Join(fDir...), fmt.Sprintf("%s%s.cbt", baseNoExt(fileName), c.Opts.Suffix)) + tarName = filepath.Join(outDir, fmt.Sprintf("%s%s.cbt", baseNoExt(fileName), c.Opts.Suffix)) } else { tarName = filepath.Join(c.Opts.OutDir, fmt.Sprintf("%s%s.cbt", baseNoExt(fileName), c.Opts.Suffix)) } diff --git a/cbconvert_test.go b/cbconvert_test.go index a69ea35..cecba62 100644 --- a/cbconvert_test.go +++ b/cbconvert_test.go @@ -29,7 +29,7 @@ func TestConvert(t *testing.T) { for _, file := range files { conv.Opts.Suffix = fmt.Sprintf("_%s%s", format, filepath.Ext(file.Path)) - err = conv.Convert(file.Path, file.Stat) + err = conv.Convert(file) if err != nil { t.Errorf("format %s: file %s: %v", format, file.Name, err) } @@ -59,7 +59,7 @@ func TestCover(t *testing.T) { } for _, file := range files { - err = conv.Cover(file.Path, file.Stat) + err = conv.Cover(file) if err != nil { t.Error(err) } @@ -88,7 +88,7 @@ func TestThumbnail(t *testing.T) { } for _, file := range files { - err = conv.Thumbnail(file.Path, file.Stat) + err = conv.Thumbnail(file) if err != nil { t.Error(err) } @@ -99,3 +99,56 @@ func TestThumbnail(t *testing.T) { t.Error(err) } } + +func TestRecursive(t *testing.T) { + inDir, err := os.MkdirTemp(os.TempDir(), "cbc-in") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(inDir) + + sub := filepath.Join(inDir, "chapter1") + if err := os.MkdirAll(sub, 0755); err != nil { + t.Fatal(err) + } + + src, err := os.ReadFile("testdata/test.cbz") + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(sub, "test.cbz"), src, 0644); err != nil { + t.Fatal(err) + } + + outDir, err := os.MkdirTemp(os.TempDir(), "cbc-out") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(outDir) + + opts := NewOptions() + opts.OutDir = outDir + opts.Recursive = true + + conv := New(opts) + + files, err := conv.Files([]string{inDir}) + if err != nil { + t.Fatal(err) + } + if len(files) != 1 { + t.Fatalf("expected 1 file, got %d", len(files)) + } + + for _, file := range files { + if err := conv.Convert(file); err != nil { + t.Error(err) + } + } + + // output must mirror the input subtree relative to the input root, not the absolute path + want := filepath.Join(outDir, "chapter1", "test.cbz") + if _, err := os.Stat(want); err != nil { + t.Errorf("expected output relative to input root at %s: %v", want, err) + } +} diff --git a/cmd/cbconvert-gui/main.go b/cmd/cbconvert-gui/main.go index 4c42822..855b67b 100644 --- a/cmd/cbconvert-gui/main.go +++ b/cmd/cbconvert-gui/main.go @@ -1024,7 +1024,7 @@ func onThumbnail(ih iup.Ihandle) int { break } - if err := c.Thumbnail(file.Path, file.Stat); err != nil { + if err := c.Thumbnail(file); err != nil { iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0) fmt.Println(err) @@ -1067,7 +1067,7 @@ func onCover(ih iup.Ihandle) int { break } - if err := c.Cover(file.Path, file.Stat); err != nil { + if err := c.Cover(file); err != nil { iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0) fmt.Println(err) @@ -1131,7 +1131,7 @@ func onConvert(ih iup.Ihandle) int { } } else { for _, file := range files { - if err := c.Convert(file.Path, file.Stat); err != nil { + if err := c.Convert(file); err != nil { convertErr(err) if errors.Is(err, context.Canceled) { break diff --git a/cmd/cbconvert/main.go b/cmd/cbconvert/main.go index 3686140..c007a4c 100644 --- a/cmd/cbconvert/main.go +++ b/cmd/cbconvert/main.go @@ -68,8 +68,8 @@ func main() { if _, err := os.Stat(opts.OutDir); err != nil { if err := os.MkdirAll(opts.OutDir, 0775); err != nil { fmt.Println(err) + os.Exit(1) } - os.Exit(1) } files, err := conv.Files(args) @@ -142,14 +142,14 @@ func main() { continue case opts.Cover: - if err := conv.Cover(file.Path, file.Stat); err != nil { + if err := conv.Cover(file); err != nil { fmt.Println(err) os.Exit(1) } continue case opts.Thumbnail: - if err = conv.Thumbnail(file.Path, file.Stat); err != nil { + if err = conv.Thumbnail(file); err != nil { fmt.Println(err) os.Exit(1) } @@ -157,7 +157,7 @@ func main() { continue } - if err := conv.Convert(file.Path, file.Stat); err != nil { + if err := conv.Convert(file); err != nil { fmt.Println(err) os.Exit(1) } @@ -279,27 +279,27 @@ func parseFlags() (cbconvert.Options, []string) { switch os.Args[1] { case "convert": - _ = convert.Parse(os.Args[2:]) + operands := parseArgs(convert, os.Args[2:]) if !pipe { - args = convert.Args() + args = operands } case "cover": opts.Cover = true - _ = cover.Parse(os.Args[2:]) + operands := parseArgs(cover, os.Args[2:]) if !pipe { - args = cover.Args() + args = operands } case "thumbnail": opts.Thumbnail = true - _ = thumbnail.Parse(os.Args[2:]) + operands := parseArgs(thumbnail, os.Args[2:]) if !pipe { - args = thumbnail.Args() + args = operands } case "meta": opts.Meta = true - _ = meta.Parse(os.Args[2:]) + operands := parseArgs(meta, os.Args[2:]) if !pipe { - args = meta.Args() + args = operands } case "version": opts.Version = true @@ -314,6 +314,19 @@ func parseFlags() (cbconvert.Options, []string) { return opts, args } +// parseArgs parses flags interspersed with file/dir operands. +func parseArgs(fs *flag.FlagSet, args []string) []string { + var operands []string + + _ = fs.Parse(args) + for fs.NArg() > 0 { + operands = append(operands, fs.Arg(0)) + _ = fs.Parse(fs.Args()[1:]) + } + + return operands +} + // piped checks if we have piped stdin. func piped() bool { f, err := os.Stdin.Stat()