Add configurable mount query
This commit is contained in:
@@ -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`.
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user