1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
package main

import (
	"flag"
	"fmt"
	"os"
	"os/signal"
	"path/filepath"
	"syscall"
)

// =============================================================================
// hosty fs-serve — mount _hosty_fs as a FUSE filesystem (companion unit)
// =============================================================================

func cmdFSServe(args []string) error {
	fset := flag.NewFlagSet("fs-serve", flag.ExitOnError)
	fset.Usage = func() {
		fmt.Print(`Usage: hosty fs-serve -name NAME [-state-dir DIR] [-fs-dir DIR] [-allow-other]

Mount the _hosty_fs table from data.hosty as a FUSE filesystem at
<stateDir>/<name>/fs/ (or -fs-dir, if given). Blocks until unmounted or killed.

This command is intended to be run as a companion systemd unit generated
by 'hosty start'. The HOSTY_FS environment variable is set to the mount
point by the app service unit.

-fs-dir overrides the mount point. System mode uses a RuntimeDirectory= under
/run so the path is stable for DynamicUser services (StateDirectory= would be
relocated to /var/lib/private and break the shared path).

-allow-other lifts FUSE's default same-uid-only access restriction, needed when
the app runs as a different user (DynamicUser) than this mount. Requires
'user_allow_other' in /etc/fuse.conf.

Options:
`)
		fset.PrintDefaults()
	}
	name := fset.String("name", "", "App name (required)")
	stateDir := fset.String("state-dir", "", "State directory (default: $XDG_STATE_HOME/hosty)")
	fsDirFlag := fset.String("fs-dir", "", "Mount point (default: <state-dir>/<name>/fs)")
	allowOther := fset.Bool("allow-other", false, "Allow other users to access the mount (FUSE allow_other)")

	if err := fset.Parse(args); err != nil {
		return err
	}
	if *name == "" {
		fset.Usage()
		return fmt.Errorf("-name is required")
	}

	// -state-dir is supplied by the generated unit, so a relative value here
	// means hosty produced a bad unit; fail loudly rather than mounting the
	// filesystem at a path that depends on the process's working directory.
	sd, err := resolveStateDir(*stateDir, false)
	if err != nil {
		return err
	}

	dataPath := filepath.Join(sd, *name, "data.hosty")
	fsDir := *fsDirFlag
	if fsDir == "" {
		fsDir = filepath.Join(sd, *name, "fs")
	}

	if !fileExists(dataPath) {
		return fmt.Errorf("no data.hosty found at %s — is %q installed?", dataPath, *name)
	}

	if err := os.MkdirAll(fsDir, 0755); err != nil {
		return fmt.Errorf("creating fs mount dir: %w", err)
	}

	fmt.Fprintf(os.Stderr, "hosty fs-serve: mounting _hosty_fs at %s\n", fsDir)
	server, err := mountHostyFSOpts(dataPath, fsDir, *allowOther)
	if err != nil {
		return fmt.Errorf("mounting hosty fs: %w", err)
	}

	// Handle signals for clean unmount.
	sigs := make(chan os.Signal, 1)
	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
	go func() {
		<-sigs
		fmt.Fprintf(os.Stderr, "hosty fs-serve: unmounting %s\n", fsDir)
		_ = server.Unmount()
	}()

	server.Wait()
	fmt.Fprintf(os.Stderr, "hosty fs-serve: unmounted\n")
	return nil
}