From 9b5a9899b8ce7cc70f22117d8b0bf5a8fc56fd7d Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Sun, 19 Jul 2026 19:57:35 +0200 Subject: [PATCH] Add configurable mount query --- README.md | 8 ++-- docspell-import-example.json | 1 + main.go | 1 + mount.go | 71 ++++++++++++++++++++++++++++++------ 4 files changed, 66 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 7bd0be4..6313f62 100644 --- a/README.md +++ b/README.md @@ -26,13 +26,13 @@ Erstellen Sie eine `docspell-import.json` Datei in Ihrem Home-Verzeichnis: [Beis The `mount` subcommand exposes a Docspell collection as a read-only filesystem. It uses the same `~/docspell-import.json` configuration and password command as the import command. -Set `mountPath` in the config to define the default mount point. +Set `mountPath` in the config to define the default mount point and `mountQuery` to define the default Docspell query. ```bash mkdir -p ~/mnt/docspell -docspell-import mount --query '*' ~/mnt/docspell # starts in the background +docspell-import mount ~/mnt/docspell # starts in the background # or, with mountPath configured: -docspell-import mount --query '*' +docspell-import mount # unmount when done fusermount -u ~/mnt/docspell ``` @@ -45,7 +45,7 @@ Mounted files are grouped as: Options: -- `--query`: Docspell item query to expose, default `*`. +- `--query`: Docspell item query to expose, defaults to `mountQuery` from config. - `--limit`: number of items fetched per API request, default `1000`. - `--auth-header`: token header, default `X-Docspell-Auth`. diff --git a/docspell-import-example.json b/docspell-import-example.json index 5ba73d6..f1a71ae 100644 --- a/docspell-import-example.json +++ b/docspell-import-example.json @@ -4,6 +4,7 @@ "user": "admin@example.com", "docspellURL": "https://docspell.example.com", "mountPath": "/home/user/mnt/docspell", + "mountQuery": "", "archiveDirectory": "/home/user/Documents/Archive", "importDirectories": [ "/home/user/Documents/Inbox", diff --git a/main.go b/main.go index 778b56f..d56d9b3 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,7 @@ type config struct { User string DocspellURL string MountPath string + MountQuery string ArchiveDirectory string ImportDirectories []string } diff --git a/mount.go b/mount.go index 2b978a7..c951b7c 100644 --- a/mount.go +++ b/mount.go @@ -37,7 +37,7 @@ type fileNode struct { func runMountCommand(args []string, cfg config, password string) error { fsFlags := flag.NewFlagSet("mount", flag.ExitOnError) - query := fsFlags.String("query", "*", "Docspell item query to expose") + query := fsFlags.String("query", cfg.MountQuery, "Docspell item query to expose") limit := fsFlags.Int("limit", 1000, "number of items to fetch per API request") authHeader := fsFlags.String("auth-header", defaultDocspellAuthHeader, "HTTP header used for the Docspell auth token") if err := fsFlags.Parse(args); err != nil { @@ -86,6 +86,7 @@ func runMountCommand(args []string, cfg config, password string) error { } func startMountInBackground(args []string, mountPoint string, quiet bool) error { + mountPoint = cleanMountPath(mountPoint) if isMounted(mountPoint) { if !quiet { fmt.Printf("Already mounted at %s\n", mountPoint) @@ -94,8 +95,13 @@ func startMountInBackground(args []string, mountPoint string, quiet bool) error } cmd := exec.Command(os.Args[0], append([]string{"mount"}, args...)...) cmd.Env = append(os.Environ(), "DOCSPELL_IMPORT_MOUNT_FOREGROUND=1") - cmd.Stdout = nil - cmd.Stderr = nil + logFile, err := mountLogFile() + if err != nil { + return err + } + defer logFile.Close() + cmd.Stdout = logFile + cmd.Stderr = logFile if err := cmd.Start(); err != nil { return err } @@ -153,23 +159,42 @@ func runGetURLCommand(args []string, cfg config, password string) error { } func ensureMounted(cfg config, password string) (string, error) { - mountPoint := cfg.MountPath + mountPoint := cleanMountPath(cfg.MountPath) if mountPoint == "" { return "", fmt.Errorf("MountPath missing in config") } + if isMounted(mountPoint) { + if isStaleMount(mountPoint) { + _ = unmount(mountPoint) + } else { + return mountPoint, nil + } + } if isMounted(mountPoint) { return mountPoint, nil } if err := startMountInBackground([]string{mountPoint}, mountPoint, true); err != nil { return "", err } - for i := 0; i < 50; i++ { + for i := 0; i < 300; i++ { if isMounted(mountPoint) { return mountPoint, nil } time.Sleep(100 * time.Millisecond) } - return "", fmt.Errorf("mount did not become ready at %s", mountPoint) + return "", fmt.Errorf("mount did not become ready at %s; see %s", mountPoint, mountLogPath()) +} + +func isStaleMount(mountPoint string) bool { + _, err := os.ReadDir(mountPoint) + return err != nil && strings.Contains(err.Error(), "transport endpoint is not connected") +} + +func unmount(mountPoint string) error { + if err := exec.Command("fusermount", "-u", mountPoint).Run(); err == nil { + return nil + } + return exec.Command("fusermount3", "-u", mountPoint).Run() } func isMounted(mountPoint string) bool { @@ -177,16 +202,43 @@ func isMounted(mountPoint string) bool { if err != nil { return false } - mountPoint, _ = filepath.Abs(mountPoint) + mountPoint = cleanMountPath(mountPoint) for _, line := range strings.Split(string(data), "\n") { fields := strings.Fields(line) - if len(fields) >= 2 && fields[1] == mountPoint { + if len(fields) >= 2 && cleanMountPath(fields[1]) == mountPoint { return true } } return false } +func cleanMountPath(path string) string { + if path == "" { + return "" + } + abs, err := filepath.Abs(path) + if err != nil { + return filepath.Clean(path) + } + return filepath.Clean(abs) +} + +func mountLogPath() string { + cacheDir, err := os.UserCacheDir() + if err != nil || cacheDir == "" { + return filepath.Join(os.TempDir(), "docspell-import-mount.log") + } + return filepath.Join(cacheDir, "docspell-import", "mount.log") +} + +func mountLogFile() (*os.File, error) { + path := mountLogPath() + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + return nil, err + } + return os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) +} + func itemIDFromURLOrID(s string) string { s = strings.TrimSpace(s) if idx := strings.LastIndex(s, "/app/item/"); idx >= 0 { @@ -241,9 +293,6 @@ func buildMountTree(ctx context.Context, client *DocspellClient, items []SearchI byID := &dirNode{name: "by-id", children: map[string]fs.Node{}} root.children["by-id"] = byID for _, item := range items { - if full, err := client.GetItem(ctx, item.ID); err == nil { - item = *full - } folderName := "No Folder" if item.Folder != nil && item.Folder.Name != "" { folderName = item.Folder.Name