Add configurable mount query

This commit is contained in:
Jan Bader
2026-07-19 19:57:35 +02:00
parent 7edd1b8d7f
commit 9b5a9899b8
4 changed files with 66 additions and 15 deletions
+4 -4
View File
@@ -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. 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. 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 ```bash
mkdir -p ~/mnt/docspell 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: # or, with mountPath configured:
docspell-import mount --query '*' docspell-import mount
# unmount when done # unmount when done
fusermount -u ~/mnt/docspell fusermount -u ~/mnt/docspell
``` ```
@@ -45,7 +45,7 @@ Mounted files are grouped as:
Options: 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`. - `--limit`: number of items fetched per API request, default `1000`.
- `--auth-header`: token header, default `X-Docspell-Auth`. - `--auth-header`: token header, default `X-Docspell-Auth`.
+1
View File
@@ -4,6 +4,7 @@
"user": "admin@example.com", "user": "admin@example.com",
"docspellURL": "https://docspell.example.com", "docspellURL": "https://docspell.example.com",
"mountPath": "/home/user/mnt/docspell", "mountPath": "/home/user/mnt/docspell",
"mountQuery": "",
"archiveDirectory": "/home/user/Documents/Archive", "archiveDirectory": "/home/user/Documents/Archive",
"importDirectories": [ "importDirectories": [
"/home/user/Documents/Inbox", "/home/user/Documents/Inbox",
+1
View File
@@ -17,6 +17,7 @@ type config struct {
User string User string
DocspellURL string DocspellURL string
MountPath string MountPath string
MountQuery string
ArchiveDirectory string ArchiveDirectory string
ImportDirectories []string ImportDirectories []string
} }
+60 -11
View File
@@ -37,7 +37,7 @@ type fileNode struct {
func runMountCommand(args []string, cfg config, password string) error { func runMountCommand(args []string, cfg config, password string) error {
fsFlags := flag.NewFlagSet("mount", flag.ExitOnError) 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") 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") authHeader := fsFlags.String("auth-header", defaultDocspellAuthHeader, "HTTP header used for the Docspell auth token")
if err := fsFlags.Parse(args); err != nil { 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 { func startMountInBackground(args []string, mountPoint string, quiet bool) error {
mountPoint = cleanMountPath(mountPoint)
if isMounted(mountPoint) { if isMounted(mountPoint) {
if !quiet { if !quiet {
fmt.Printf("Already mounted at %s\n", mountPoint) 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 := exec.Command(os.Args[0], append([]string{"mount"}, args...)...)
cmd.Env = append(os.Environ(), "DOCSPELL_IMPORT_MOUNT_FOREGROUND=1") cmd.Env = append(os.Environ(), "DOCSPELL_IMPORT_MOUNT_FOREGROUND=1")
cmd.Stdout = nil logFile, err := mountLogFile()
cmd.Stderr = nil if err != nil {
return err
}
defer logFile.Close()
cmd.Stdout = logFile
cmd.Stderr = logFile
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
return err return err
} }
@@ -153,23 +159,42 @@ func runGetURLCommand(args []string, cfg config, password string) error {
} }
func ensureMounted(cfg config, password string) (string, error) { func ensureMounted(cfg config, password string) (string, error) {
mountPoint := cfg.MountPath mountPoint := cleanMountPath(cfg.MountPath)
if mountPoint == "" { if mountPoint == "" {
return "", fmt.Errorf("MountPath missing in config") return "", fmt.Errorf("MountPath missing in config")
} }
if isMounted(mountPoint) {
if isStaleMount(mountPoint) {
_ = unmount(mountPoint)
} else {
return mountPoint, nil
}
}
if isMounted(mountPoint) { if isMounted(mountPoint) {
return mountPoint, nil return mountPoint, nil
} }
if err := startMountInBackground([]string{mountPoint}, mountPoint, true); err != nil { if err := startMountInBackground([]string{mountPoint}, mountPoint, true); err != nil {
return "", err return "", err
} }
for i := 0; i < 50; i++ { for i := 0; i < 300; i++ {
if isMounted(mountPoint) { if isMounted(mountPoint) {
return mountPoint, nil return mountPoint, nil
} }
time.Sleep(100 * time.Millisecond) 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 { func isMounted(mountPoint string) bool {
@@ -177,16 +202,43 @@ func isMounted(mountPoint string) bool {
if err != nil { if err != nil {
return false return false
} }
mountPoint, _ = filepath.Abs(mountPoint) mountPoint = cleanMountPath(mountPoint)
for _, line := range strings.Split(string(data), "\n") { for _, line := range strings.Split(string(data), "\n") {
fields := strings.Fields(line) fields := strings.Fields(line)
if len(fields) >= 2 && fields[1] == mountPoint { if len(fields) >= 2 && cleanMountPath(fields[1]) == mountPoint {
return true return true
} }
} }
return false 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 { func itemIDFromURLOrID(s string) string {
s = strings.TrimSpace(s) s = strings.TrimSpace(s)
if idx := strings.LastIndex(s, "/app/item/"); idx >= 0 { 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{}} byID := &dirNode{name: "by-id", children: map[string]fs.Node{}}
root.children["by-id"] = byID root.children["by-id"] = byID
for _, item := range items { for _, item := range items {
if full, err := client.GetItem(ctx, item.ID); err == nil {
item = *full
}
folderName := "No Folder" folderName := "No Folder"
if item.Folder != nil && item.Folder.Name != "" { if item.Folder != nil && item.Folder.Name != "" {
folderName = item.Folder.Name folderName = item.Folder.Name