Add background mount path helpers
This commit is contained in:
@@ -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{}}
|
||||
|
||||
Reference in New Issue
Block a user