Add export command and shared item tree

Introduces a pure, FUSE-independent item tree (buildItemTree) shared by the mount and export commands. The new export subcommand downloads Docspell search results into a local directory, mirroring the mount's friendly folder/year/month hierarchy without the by-id directory or metadata JSON file, for archival or migration.

- itemtree.go: pure item tree builder shared by mount and export
- mount.go: buildMountTree now converts the pure tree into FUSE nodes, with a shared fileNode per attachment across friendly and by-id paths
- export.go: export command with concurrent downloads, --dry-run, --skip-existing, --concurrency, --query, --limit, --auth-header
- main.go: wires up the export subcommand and its flags
- export_test.go: tests for plan collection, writes, skip-existing, failures, modtime
- README.md: documents the export command
This commit is contained in:
Jan Bader committed 2026-07-21 23:35:03 +02:00
1 parent c4a2e9d085
commit 5e88eaea97
6 files changed
+696 -98

No files matched your search

+20 -1
View File
@@ -60,6 +60,12 @@ func main() {
fmt.Fprintln(os.Stderr, "mount failed:", err)
os.Exit(1)
}
case commands.exportCommand.Used:
cfg, password := loadConfigAndPassword()
if err := runExportCommand(os.Args[2:], cfg, password); err != nil {
fmt.Fprintln(os.Stderr, "export failed:", err)
os.Exit(1)
}
case commands.getPathCommand.Used:
cfg, password := loadConfigAndPassword()
if err := runGetPathCommand(os.Args[2:], cfg, password); err != nil {
@@ -80,6 +86,7 @@ func main() {
type cliCommands struct {
importCommand *flaggy.Subcommand
mountCommand *flaggy.Subcommand
exportCommand *flaggy.Subcommand
getPathCommand *flaggy.Subcommand
getURLCommand *flaggy.Subcommand
}
@@ -93,28 +100,40 @@ func newCLIParser() (*flaggy.Parser, cliCommands) {
commands := cliCommands{
importCommand: flaggy.NewSubcommand("import"),
mountCommand: flaggy.NewSubcommand("mount"),
exportCommand: flaggy.NewSubcommand("export"),
getPathCommand: flaggy.NewSubcommand("get-path"),
getURLCommand: flaggy.NewSubcommand("get-url"),
}
commands.importCommand.Description = "Scan configured import folders, archive confirmed files, and optionally upload missing files"
commands.mountCommand.Description = "Expose Docspell search results as a read-only FUSE filesystem"
commands.exportCommand.Description = "Download Docspell search results into a local directory, mirroring the mount hierarchy"
commands.getPathCommand.Description = "Resolve a Docspell item ID or URL to the local mounted path"
commands.getURLCommand.Description = "Resolve a local mounted path, item ID, or item URL to a Docspell web URL"
var uploadMissing bool
commands.importCommand.Bool(&uploadMissing, "", "upload-missing", "true, to upload files to docspell that do not yet exist there")
var query, mountPoint, itemIDOrURL, pathOrID string
var query, mountPoint, exportDest, itemIDOrURL, pathOrID string
var exportConcurrency int
var exportDryRun, exportSkipExisting bool
authHeader := defaultDocspellAuthHeader
limit := 1000
commands.mountCommand.String(&query, "", "query", "Docspell item query to expose")
commands.mountCommand.Int(&limit, "", "limit", "number of items to fetch per API request")
commands.mountCommand.String(&authHeader, "", "auth-header", "HTTP header used for the Docspell auth token")
commands.mountCommand.AddPositionalValue(&mountPoint, "mountpoint", 1, false, "mount point, overriding mountPath from config")
commands.exportCommand.String(&query, "", "query", "Docspell item query to expose")
commands.exportCommand.Int(&limit, "", "limit", "number of items to fetch per API request")
commands.exportCommand.String(&authHeader, "", "auth-header", "HTTP header used for the Docspell auth token")
commands.exportCommand.AddPositionalValue(&exportDest, "destination", 1, false, "destination directory for the export")
commands.exportCommand.Int(&exportConcurrency, "", "concurrency", "number of attachments to download in parallel")
commands.exportCommand.Bool(&exportDryRun, "", "dry-run", "print the files that would be written without downloading")
commands.exportCommand.Bool(&exportSkipExisting, "", "skip-existing", "skip files that already exist in the destination")
commands.getPathCommand.AddPositionalValue(&itemIDOrURL, "item-id-or-url", 1, true, "Docspell item ID or web URL")
commands.getURLCommand.AddPositionalValue(&pathOrID, "local-path-or-item-id", 1, true, "local mounted path, item ID, or item URL")
parser.AttachSubcommand(commands.importCommand, 1)
parser.AttachSubcommand(commands.mountCommand, 1)
parser.AttachSubcommand(commands.exportCommand, 1)
parser.AttachSubcommand(commands.getPathCommand, 1)
parser.AttachSubcommand(commands.getURLCommand, 1)
return parser, commands