Add background mount path helpers

This commit is contained in:
Jan Bader
2026-07-19 19:48:49 +02:00
parent ef8caef37b
commit 7edd1b8d7f
5 changed files with 227 additions and 12 deletions
+169 -4
View File
@@ -5,6 +5,7 @@ import (
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
@@ -42,8 +43,21 @@ func runMountCommand(args []string, cfg config, password string) error {
if err := fsFlags.Parse(args); err != nil {
return err
}
if fsFlags.NArg() != 1 {
return fmt.Errorf("usage: %s mount [flags] <mountpoint>", filepath.Base(os.Args[0]))
mountPoint := cfg.MountPath
if fsFlags.NArg() > 1 {
return fmt.Errorf("usage: %s mount [flags] [mountpoint]", filepath.Base(os.Args[0]))
}
if fsFlags.NArg() == 1 {
mountPoint = fsFlags.Arg(0)
}
if mountPoint == "" {
return fmt.Errorf("mount point missing: pass one or set MountPath in config")
}
if os.Getenv("DOCSPELL_IMPORT_MOUNT_FOREGROUND") != "1" {
return startMountInBackground(args, mountPoint, false)
}
if err := os.MkdirAll(mountPoint, 0755); err != nil {
return err
}
client := NewDocspellClient(cfg.DocspellURL, *authHeader)
@@ -57,7 +71,7 @@ func runMountCommand(args []string, cfg config, password string) error {
root := buildMountTree(context.Background(), client, items)
conn, err := fuse.Mount(
fsFlags.Arg(0),
mountPoint,
fuse.FSName("docspell"),
fuse.Subtype("docspell-import"),
fuse.ReadOnly(),
@@ -67,10 +81,161 @@ func runMountCommand(args []string, cfg config, password string) error {
}
defer conn.Close()
fmt.Printf("Mounted %d items read-only at %s. Unmount with: fusermount -u %s\n", len(items), fsFlags.Arg(0), fsFlags.Arg(0))
fmt.Printf("Mounted %d items read-only at %s. Unmount with: fusermount -u %s\n", len(items), mountPoint, mountPoint)
return fs.Serve(conn, &docspellFS{root: root})
}
func startMountInBackground(args []string, mountPoint string, quiet bool) error {
if isMounted(mountPoint) {
if !quiet {
fmt.Printf("Already mounted at %s\n", mountPoint)
}
return nil
}
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
if err := cmd.Start(); err != nil {
return err
}
if err := cmd.Process.Release(); err != nil {
return err
}
if !quiet {
fmt.Printf("Mounting %s in background. Unmount with: fusermount -u %s\n", mountPoint, mountPoint)
}
return nil
}
func runGetLocalPathCommand(args []string, cfg config, password string) error {
flags := flag.NewFlagSet("get-local-path", flag.ExitOnError)
if err := flags.Parse(args); err != nil {
return err
}
if flags.NArg() != 1 {
return fmt.Errorf("usage: %s get-local-path <item-id-or-url>", filepath.Base(os.Args[0]))
}
mountPoint, err := ensureMounted(cfg, password)
if err != nil {
return err
}
id := itemIDFromURLOrID(flags.Arg(0))
matches, err := filepath.Glob(filepath.Join(mountPoint, "by-id", idShard(id), id+".*"))
if err != nil {
return err
}
if len(matches) == 0 {
return fmt.Errorf("entry %s not found under mounted by-id tree", id)
}
fmt.Println(matches[0])
return nil
}
func runGetURLCommand(args []string, cfg config, password string) error {
flags := flag.NewFlagSet("get-url", flag.ExitOnError)
if err := flags.Parse(args); err != nil {
return err
}
if flags.NArg() != 1 {
return fmt.Errorf("usage: %s get-url <local-path-or-item-id>", filepath.Base(os.Args[0]))
}
id := flags.Arg(0)
if strings.Contains(id, string(os.PathSeparator)) || strings.HasPrefix(id, ".") {
var err error
id, err = itemIDFromLocalPath(id, cfg)
if err != nil {
return err
}
}
fmt.Printf("%s/app/item/%s\n", strings.TrimRight(cfg.DocspellURL, "/"), itemIDFromURLOrID(id))
return nil
}
func ensureMounted(cfg config, password string) (string, error) {
mountPoint := cfg.MountPath
if mountPoint == "" {
return "", fmt.Errorf("MountPath missing in config")
}
if isMounted(mountPoint) {
return mountPoint, nil
}
if err := startMountInBackground([]string{mountPoint}, mountPoint, true); err != nil {
return "", err
}
for i := 0; i < 50; i++ {
if isMounted(mountPoint) {
return mountPoint, nil
}
time.Sleep(100 * time.Millisecond)
}
return "", fmt.Errorf("mount did not become ready at %s", mountPoint)
}
func isMounted(mountPoint string) bool {
data, err := os.ReadFile("/proc/mounts")
if err != nil {
return false
}
mountPoint, _ = filepath.Abs(mountPoint)
for _, line := range strings.Split(string(data), "\n") {
fields := strings.Fields(line)
if len(fields) >= 2 && fields[1] == mountPoint {
return true
}
}
return false
}
func itemIDFromURLOrID(s string) string {
s = strings.TrimSpace(s)
if idx := strings.LastIndex(s, "/app/item/"); idx >= 0 {
return strings.Trim(strings.TrimPrefix(s[idx:], "/app/item/"), "/")
}
return strings.Trim(s, "/")
}
func itemIDFromLocalPath(path string, cfg config) (string, error) {
abs, err := filepath.Abs(path)
if err != nil {
return "", err
}
mountPoint, err := filepath.Abs(cfg.MountPath)
if err != nil {
return "", err
}
rel, err := filepath.Rel(mountPoint, abs)
if err != nil || strings.HasPrefix(rel, "..") {
return "", fmt.Errorf("path is not under MountPath %s", cfg.MountPath)
}
parts := strings.Split(rel, string(os.PathSeparator))
if len(parts) >= 3 && parts[0] == "by-id" {
return strings.TrimSuffix(parts[2], filepath.Ext(parts[2])), nil
}
info, err := os.Stat(abs)
if err != nil {
return "", err
}
var found string
err = filepath.Walk(filepath.Join(mountPoint, "by-id"), func(p string, fi os.FileInfo, err error) error {
if err != nil || fi == nil || fi.IsDir() {
return nil
}
if os.SameFile(info, fi) {
found = strings.TrimSuffix(filepath.Base(p), filepath.Ext(p))
return filepath.SkipAll
}
return nil
})
if err != nil {
return "", err
}
if found == "" {
return "", fmt.Errorf("could not map mounted path to item id; try a by-id path")
}
return found, nil
}
func buildMountTree(ctx context.Context, client *DocspellClient, items []SearchItem) *dirNode {
root := &dirNode{name: "", children: map[string]fs.Node{}}
byID := &dirNode{name: "by-id", children: map[string]fs.Node{}}