larc r5

174 lines ยท 4.1 KB Raw
1 package cli
2
3 import (
4 "context"
5 "fmt"
6 "net/url"
7 "os"
8 "os/signal"
9 "path/filepath"
10 "strings"
11 "syscall"
12
13 "github.com/spf13/cobra"
14
15 "github.com/lain/larc/internal/mount"
16 "github.com/lain/larc/internal/protocol"
17 )
18
19 /* Mount commands: mount, unmount
20 * Mount a remote repository as a read-only FUSE filesystem. */
21
22 // MountCmd creates the mount command
23 func MountCmd() *cobra.Command {
24 cmd := &cobra.Command{
25 Use: "mount <url> <mountpoint>",
26 Short: "Mount a remote repository",
27 Long: `Mount a remote larc repository as a read-only filesystem.
28 Files are fetched lazily and cached locally in ~/.cache/larc/blobs.
29
30 Examples:
31 larc mount https://example.com/repo /mnt/repo
32 larc mount -r 42 https://example.com/repo /mnt/repo`,
33 Args: cobra.ExactArgs(2),
34 RunE: func(cmd *cobra.Command, args []string) error {
35 repoURL := args[0]
36 mountPoint := args[1]
37
38 revision, _ := cmd.Flags().GetInt64("revision")
39 foreground, _ := cmd.Flags().GetBool("foreground")
40
41 return runMount(repoURL, mountPoint, revision, foreground)
42 },
43 }
44
45 cmd.Flags().Int64P("revision", "r", 0, "revision to mount (default: latest)")
46 cmd.Flags().BoolP("foreground", "f", false, "run in foreground")
47
48 return cmd
49 }
50
51 func runMount(repoURL, mountPoint string, revision int64, foreground bool) error {
52 /* parse URL */
53 parsed, err := url.Parse(repoURL)
54 if err != nil {
55 return fmt.Errorf("invalid URL: %w", err)
56 }
57
58 repoName := strings.TrimPrefix(parsed.Path, "/")
59 repoName = strings.TrimSuffix(repoName, "/")
60 baseURL := fmt.Sprintf("%s://%s", parsed.Scheme, parsed.Host)
61
62 /* create client */
63 client := protocol.NewClient(baseURL)
64 if parsed.User != nil {
65 password, _ := parsed.User.Password()
66 client.SetAuth(parsed.User.Username(), password)
67 }
68
69 /* get repo info */
70 info, err := client.GetInfo(repoName)
71 if err != nil {
72 return fmt.Errorf("failed to connect: %w", err)
73 }
74
75 /* determine revision to mount */
76 targetRev := revision
77 if targetRev == 0 {
78 targetRev = info.LatestRev
79 }
80
81 if targetRev > info.LatestRev {
82 return fmt.Errorf("revision r%d does not exist (latest: r%d)", targetRev, info.LatestRev)
83 }
84
85 fmt.Printf("Mounting %s at r%d to %s\n", repoName, targetRev, mountPoint)
86
87 /* get tree for revision */
88 tree, err := client.GetTree(repoName, targetRev)
89 if err != nil {
90 return fmt.Errorf("failed to get tree: %w", err)
91 }
92
93 fmt.Printf(" %d files in tree\n", len(tree.Entries))
94
95 /* create mount point if needed */
96 mountPoint, err = filepath.Abs(mountPoint)
97 if err != nil {
98 return err
99 }
100
101 if err := os.MkdirAll(mountPoint, 0755); err != nil {
102 return fmt.Errorf("create mount point: %w", err)
103 }
104
105 /* create cache */
106 cache, err := mount.NewBlobCache()
107 if err != nil {
108 return fmt.Errorf("create cache: %w", err)
109 }
110 fmt.Printf(" cache: %s\n", cache.Dir())
111
112 /* create filesystem */
113 fs := mount.NewLarcFS(client, repoName, targetRev, tree, cache)
114
115 /* mount */
116 mfs, err := mount.Mount(mountPoint, fs)
117 if err != nil {
118 return fmt.Errorf("mount failed: %w", err)
119 }
120
121 fmt.Printf("Mounted successfully. Use 'larc unmount %s' to unmount.\n", mountPoint)
122
123 /* setup signal handler */
124 sigChan := make(chan os.Signal, 1)
125 signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
126
127 if foreground {
128 fmt.Println("Press Ctrl+C to unmount...")
129 }
130
131 /* wait for unmount or signal */
132 done := make(chan struct{})
133 go func() {
134 mfs.Join(context.Background())
135 close(done)
136 }()
137
138 select {
139 case <-sigChan:
140 fmt.Println("\nUnmounting...")
141 mount.Unmount(mountPoint)
142 <-done
143 case <-done:
144 /* filesystem was unmounted externally */
145 }
146
147 return nil
148 }
149
150 // UnmountCmd creates the unmount command
151 func UnmountCmd() *cobra.Command {
152 return &cobra.Command{
153 Use: "unmount <mountpoint>",
154 Aliases: []string{"umount"},
155 Short: "Unmount a mounted repository",
156 Args: cobra.ExactArgs(1),
157 RunE: func(cmd *cobra.Command, args []string) error {
158 mountPoint := args[0]
159
160 mountPoint, err := filepath.Abs(mountPoint)
161 if err != nil {
162 return err
163 }
164
165 if err := mount.Unmount(mountPoint); err != nil {
166 return fmt.Errorf("unmount failed: %w", err)
167 }
168
169 fmt.Printf("Unmounted %s\n", mountPoint)
170 return nil
171 },
172 }
173 }
174