added proton drive integration

This commit is contained in:
Bartal Laearsson
2026-08-05 10:28:43 +01:00
parent 4992bcc7f7
commit 54f1c2e355
3 changed files with 169 additions and 31 deletions
+46 -8
View File
@@ -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,18 +66,26 @@ 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,
MaxSize: maxSize, MaxSize: maxSize,
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
} }
+35 -23
View File
@@ -53,15 +53,17 @@ func (s *stringSlice) Set(v string) error {
} }
type Config struct { type Config struct {
InputPath string InputPath string
OutputPath string OutputPath string
AppendMode bool AppendMode bool
SingleFile bool SingleFile bool
MaxSize int64 MaxSize int64
MaxDepth int MaxDepth int
Ignores []string Ignores []string
Extensions []string Extensions []string
AbsRoot string AbsRoot string
ProtonDrive bool
DrivePath string
} }
func PrintUsage() { func PrintUsage() {
@@ -74,14 +76,16 @@ func PrintUsage() {
func Parse() (*Config, error) { func Parse() (*Config, error) {
var ( var (
input string input string
output string output string
appendMd bool appendMd bool
maxSize int64 maxSize int64
maxDepth int maxDepth int
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,13 +111,19 @@ 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,
MaxSize: maxSize, MaxSize: maxSize,
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 != "" {
+88
View File
@@ -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)
}