added proton drive integration
This commit is contained in:
+40
-2
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"dirmd/internal/config"
|
"dirmd/internal/config"
|
||||||
|
"dirmd/internal/drive"
|
||||||
"dirmd/internal/renderer"
|
"dirmd/internal/renderer"
|
||||||
"dirmd/internal/walker"
|
"dirmd/internal/walker"
|
||||||
)
|
)
|
||||||
@@ -30,7 +31,13 @@ Examples:
|
|||||||
dirmd -a -i README.md -o existing.md
|
dirmd -a -i README.md -o existing.md
|
||||||
|
|
||||||
# Append another directory
|
# Append another directory
|
||||||
dirmd -a -o existing.md ~/repos/anotherproject`,
|
dirmd -a -o existing.md ~/repos/anotherproject
|
||||||
|
|
||||||
|
# Generate and upload to Proton Drive (default remote path /my-files/md/<filename>)
|
||||||
|
dirmd -o output.md ~/repos/myproject --proton-drive
|
||||||
|
|
||||||
|
# Generate and upload to a specific remote path
|
||||||
|
dirmd -o output.md ~/repos/myproject --proton-drive --drive-path /my-files/docs/api.md`,
|
||||||
RunE: run,
|
RunE: run,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +48,8 @@ Examples:
|
|||||||
maxDepth int
|
maxDepth int
|
||||||
ignorePatterns []string
|
ignorePatterns []string
|
||||||
extensions []string
|
extensions []string
|
||||||
|
protonDrive bool
|
||||||
|
drivePath string
|
||||||
)
|
)
|
||||||
|
|
||||||
func Execute() {
|
func Execute() {
|
||||||
@@ -57,11 +66,17 @@ func init() {
|
|||||||
rootCmd.Flags().IntVar(&maxDepth, "max-depth", config.DefaultMaxDepth, "max directory recursion depth")
|
rootCmd.Flags().IntVar(&maxDepth, "max-depth", config.DefaultMaxDepth, "max directory recursion depth")
|
||||||
rootCmd.Flags().StringArrayVar(&ignorePatterns, "ignore", nil, "additional ignore patterns (glob)")
|
rootCmd.Flags().StringArrayVar(&ignorePatterns, "ignore", nil, "additional ignore patterns (glob)")
|
||||||
rootCmd.Flags().StringArrayVar(&extensions, "extensions", nil, "additional file extensions to skip (repeatable)")
|
rootCmd.Flags().StringArrayVar(&extensions, "extensions", nil, "additional file extensions to skip (repeatable)")
|
||||||
|
rootCmd.Flags().BoolVar(&protonDrive, "proton-drive", false, "upload output to Proton Drive after writing locally")
|
||||||
|
rootCmd.Flags().StringVar(&drivePath, "drive-path", "", "full remote path on Proton Drive (e.g. /my-files/md/report.md). Requires --proton-drive")
|
||||||
|
|
||||||
rootCmd.MarkPersistentFlagRequired("output")
|
rootCmd.MarkPersistentFlagRequired("output")
|
||||||
}
|
}
|
||||||
|
|
||||||
func run(cmd *cobra.Command, args []string) error {
|
func run(cmd *cobra.Command, args []string) error {
|
||||||
|
if drivePath != "" && !protonDrive {
|
||||||
|
return fmt.Errorf("--drive-path requires --proton-drive")
|
||||||
|
}
|
||||||
|
|
||||||
cfg := &config.Config{
|
cfg := &config.Config{
|
||||||
OutputPath: outputPath,
|
OutputPath: outputPath,
|
||||||
AppendMode: appendMode,
|
AppendMode: appendMode,
|
||||||
@@ -69,6 +84,8 @@ func run(cmd *cobra.Command, args []string) error {
|
|||||||
MaxDepth: maxDepth,
|
MaxDepth: maxDepth,
|
||||||
Ignores: append(config.DefaultIgnores, ignorePatterns...),
|
Ignores: append(config.DefaultIgnores, ignorePatterns...),
|
||||||
Extensions: append(config.DefaultExts, extensions...),
|
Extensions: append(config.DefaultExts, extensions...),
|
||||||
|
ProtonDrive: protonDrive,
|
||||||
|
DrivePath: drivePath,
|
||||||
}
|
}
|
||||||
|
|
||||||
if singleFile != "" {
|
if singleFile != "" {
|
||||||
@@ -111,6 +128,27 @@ func run(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(os.Stderr, "wrote %d entries to %s\n", len(result.Entries), outputPath)
|
fmt.Fprintf(os.Stderr, "wrote %d entries to %s\n", len(result.Entries), outputPath)
|
||||||
|
|
||||||
|
if protonDrive {
|
||||||
|
remotePath := drivePath
|
||||||
|
if remotePath == "" {
|
||||||
|
remotePath = "/my-files/md/" + filepath.Base(outputPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := drive.CheckAuth(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||||
|
fmt.Fprintln(os.Stderr, "local file was written successfully, but upload was skipped")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := drive.Upload(outputPath, remotePath); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||||
|
fmt.Fprintln(os.Stderr, "local file was written successfully, but upload failed")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(os.Stderr, "uploaded %s to %s on Proton Drive\n", outputPath, remotePath)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ type Config struct {
|
|||||||
Ignores []string
|
Ignores []string
|
||||||
Extensions []string
|
Extensions []string
|
||||||
AbsRoot string
|
AbsRoot string
|
||||||
|
ProtonDrive bool
|
||||||
|
DrivePath string
|
||||||
}
|
}
|
||||||
|
|
||||||
func PrintUsage() {
|
func PrintUsage() {
|
||||||
@@ -82,6 +84,8 @@ func Parse() (*Config, error) {
|
|||||||
ignores stringSlice
|
ignores stringSlice
|
||||||
exts stringSlice
|
exts stringSlice
|
||||||
showVer bool
|
showVer bool
|
||||||
|
protonDrive bool
|
||||||
|
drivePath string
|
||||||
)
|
)
|
||||||
|
|
||||||
flag.StringVar(&input, "i", "", "single file input (append mode only)")
|
flag.StringVar(&input, "i", "", "single file input (append mode only)")
|
||||||
@@ -92,6 +96,8 @@ func Parse() (*Config, error) {
|
|||||||
flag.Var(&ignores, "ignore", "additional ignore patterns (glob, repeatable)")
|
flag.Var(&ignores, "ignore", "additional ignore patterns (glob, repeatable)")
|
||||||
flag.Var(&exts, "extensions", "additional file extensions to skip (repeatable)")
|
flag.Var(&exts, "extensions", "additional file extensions to skip (repeatable)")
|
||||||
flag.BoolVar(&showVer, "version", false, "print version and exit")
|
flag.BoolVar(&showVer, "version", false, "print version and exit")
|
||||||
|
flag.BoolVar(&protonDrive, "proton-drive", false, "upload output to Proton Drive after writing locally")
|
||||||
|
flag.StringVar(&drivePath, "drive-path", "", "full remote path on Proton Drive (requires --proton-drive)")
|
||||||
|
|
||||||
flag.Usage = PrintUsage
|
flag.Usage = PrintUsage
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
@@ -105,6 +111,10 @@ func Parse() (*Config, error) {
|
|||||||
return nil, fmt.Errorf("-o is required")
|
return nil, fmt.Errorf("-o is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if drivePath != "" && !protonDrive {
|
||||||
|
return nil, fmt.Errorf("--drive-path requires --proton-drive")
|
||||||
|
}
|
||||||
|
|
||||||
cfg := &Config{
|
cfg := &Config{
|
||||||
OutputPath: output,
|
OutputPath: output,
|
||||||
AppendMode: appendMd,
|
AppendMode: appendMd,
|
||||||
@@ -112,6 +122,8 @@ func Parse() (*Config, error) {
|
|||||||
MaxDepth: maxDepth,
|
MaxDepth: maxDepth,
|
||||||
Ignores: append(DefaultIgnores, ignores...),
|
Ignores: append(DefaultIgnores, ignores...),
|
||||||
Extensions: append(DefaultExts, exts...),
|
Extensions: append(DefaultExts, exts...),
|
||||||
|
ProtonDrive: protonDrive,
|
||||||
|
DrivePath: drivePath,
|
||||||
}
|
}
|
||||||
|
|
||||||
if input != "" {
|
if input != "" {
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package drive
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const binaryName = "proton-drive"
|
||||||
|
|
||||||
|
// Lookup returns the full path to the proton-drive binary in PATH.
|
||||||
|
func Lookup() (string, error) {
|
||||||
|
return exec.LookPath(binaryName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckAuth verifies that the user is authenticated by listing /my-files.
|
||||||
|
func CheckAuth() error {
|
||||||
|
binary, err := Lookup()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("proton-drive not found in PATH: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(binary, "filesystem", "list", "/my-files")
|
||||||
|
cmd.Stdout = nil
|
||||||
|
cmd.Stderr = nil
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("not authenticated with proton-drive (run 'proton-drive auth login'): %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload uploads localPath to remotePath on Proton Drive.
|
||||||
|
// remotePath is a full path including filename (e.g. /my-files/md/report.md).
|
||||||
|
// The CLI uploads to a parent directory and preserves the local filename,
|
||||||
|
// so if the remote filename differs from the local one, a temp copy is staged.
|
||||||
|
// Existing remote files are overwritten (--conflict-strategy replace).
|
||||||
|
func Upload(localPath, remotePath string) error {
|
||||||
|
binary, err := Lookup()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("proton-drive not found in PATH: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteDir := filepath.Dir(remotePath)
|
||||||
|
remoteName := filepath.Base(remotePath)
|
||||||
|
localName := filepath.Base(localPath)
|
||||||
|
|
||||||
|
if remoteName == localName {
|
||||||
|
return runUpload(binary, localPath, remoteDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpDir, err := os.MkdirTemp("", "dirmd-upload-*")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot create temp dir: %w", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
tmpFile := filepath.Join(tmpDir, remoteName)
|
||||||
|
if err := copyFile(localPath, tmpFile); err != nil {
|
||||||
|
return fmt.Errorf("cannot stage temp file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return runUpload(binary, tmpFile, remoteDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runUpload(binary, localPath, remoteDir string) error {
|
||||||
|
cmd := exec.Command(binary, "filesystem", "upload", localPath, remoteDir,
|
||||||
|
"--conflict-strategy", "replace")
|
||||||
|
|
||||||
|
var stderr strings.Builder
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("upload to proton-drive failed: %w: %s",
|
||||||
|
err, strings.TrimSpace(stderr.String()))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyFile(src, dst string) error {
|
||||||
|
data, err := os.ReadFile(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(dst, data, 0644)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user