package cli import ( "context" "fmt" "net/url" "os" "os/signal" "path/filepath" "strings" "syscall" "github.com/spf13/cobra" "larc.wejust.rest/larc/internal/mount" "larc.wejust.rest/larc/internal/protocol" ) /* Mount commands: mount, unmount * Mount a remote repository as a read-only FUSE filesystem. */ // MountCmd creates the mount command func MountCmd() *cobra.Command { cmd := &cobra.Command{ Use: "mount ", Short: "Mount a remote repository", Long: `Mount a remote larc repository as a read-only filesystem. Files are fetched lazily and cached locally in ~/.cache/larc/blobs. Examples: larc mount https://example.com/repo /mnt/repo larc mount -r 42 https://example.com/repo /mnt/repo`, Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { repoURL := args[0] mountPoint := args[1] revision, _ := cmd.Flags().GetInt64("revision") foreground, _ := cmd.Flags().GetBool("foreground") return runMount(repoURL, mountPoint, revision, foreground) }, } cmd.Flags().Int64P("revision", "r", 0, "revision to mount (default: latest)") cmd.Flags().BoolP("foreground", "f", false, "run in foreground") return cmd } func runMount(repoURL, mountPoint string, revision int64, foreground bool) error { /* parse URL */ parsed, err := url.Parse(repoURL) if err != nil { return fmt.Errorf("invalid URL: %w", err) } repoName := strings.TrimPrefix(parsed.Path, "/") repoName = strings.TrimSuffix(repoName, "/") baseURL := fmt.Sprintf("%s://%s", parsed.Scheme, parsed.Host) /* create client */ client := protocol.NewClient(baseURL) if parsed.User != nil { password, _ := parsed.User.Password() client.SetAuth(parsed.User.Username(), password) } /* get repo info */ info, err := client.GetInfo(repoName) if err != nil { return fmt.Errorf("failed to connect: %w", err) } /* determine revision to mount */ targetRev := revision if targetRev == 0 { targetRev = info.LatestRev } if targetRev > info.LatestRev { return fmt.Errorf("revision r%d does not exist (latest: r%d)", targetRev, info.LatestRev) } fmt.Printf("Mounting %s at r%d to %s\n", repoName, targetRev, mountPoint) /* get tree for revision */ tree, err := client.GetTree(repoName, targetRev) if err != nil { return fmt.Errorf("failed to get tree: %w", err) } fmt.Printf(" %d files in tree\n", len(tree.Entries)) /* create mount point if needed */ mountPoint, err = filepath.Abs(mountPoint) if err != nil { return err } if err := os.MkdirAll(mountPoint, 0755); err != nil { return fmt.Errorf("create mount point: %w", err) } /* create cache */ cache, err := mount.NewBlobCache() if err != nil { return fmt.Errorf("create cache: %w", err) } fmt.Printf(" cache: %s\n", cache.Dir()) /* create filesystem */ fs := mount.NewLarcFS(client, repoName, targetRev, tree, cache) /* mount */ mfs, err := mount.Mount(mountPoint, fs) if err != nil { return fmt.Errorf("mount failed: %w", err) } fmt.Printf("Mounted successfully. Use 'larc unmount %s' to unmount.\n", mountPoint) /* setup signal handler */ sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) if foreground { fmt.Println("Press Ctrl+C to unmount...") } /* wait for unmount or signal */ done := make(chan struct{}) go func() { mfs.Join(context.Background()) close(done) }() select { case <-sigChan: fmt.Println("\nUnmounting...") mount.Unmount(mountPoint) <-done case <-done: /* filesystem was unmounted externally */ } return nil } // UnmountCmd creates the unmount command func UnmountCmd() *cobra.Command { return &cobra.Command{ Use: "unmount ", Aliases: []string{"umount"}, Short: "Unmount a mounted repository", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { mountPoint := args[0] mountPoint, err := filepath.Abs(mountPoint) if err != nil { return err } if err := mount.Unmount(mountPoint); err != nil { return fmt.Errorf("unmount failed: %w", err) } fmt.Printf("Unmounted %s\n", mountPoint) return nil }, } }