package cmd import ( "bytes" "context" "fmt" "os/exec" "strings" "time" "github.com/cloudmigration/drive-migrator/internal/tracker" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) var ( sourceName string sourceType string destName string destType string trackFile string resume bool include []string exclude []string // Source remote config flags sourceURL string sourceUser string sourcePass string sourceSAFile string // Dest remote config flags destURL string destUser string destPass string destSAFile string ) func init() { migrateCmd := &cobra.Command{ Use: "migrate", Short: "Run migration between two cloud drives (auto-creates remotes if needed)", RunE: runMigrate, } migrateCmd.Flags().StringVar(&sourceName, "source-name", "", "Nama remote/source di rclone (akan dibuat otomatis jika belum ada)") migrateCmd.Flags().StringVar(&sourceType, "source-type", "", "Tipe cloud drive sumber (nextcloud, webdav, gdrive, onedrive, dll)") migrateCmd.Flags().StringVar(&destName, "dest-name", "", "Nama remote/tujuan di rclone (akan dibuat otomatis jika belum ada)") migrateCmd.Flags().StringVar(&destType, "dest-type", "", "Tipe cloud drive tujuan") migrateCmd.Flags().StringVar(&trackFile, "track-file", "migration.db", "Lokasi file tracking (bolt)") migrateCmd.Flags().BoolVar(&resume, "resume", false, "Lanjutkan migrasi yang terhenti") migrateCmd.Flags().StringSliceVar(&include, "include", nil, "Pola file/folder yang akan dimigrasikan (bisa diulang)") migrateCmd.Flags().StringSliceVar(&exclude, "exclude", nil, "Pola file/folder yang akan dikecualikan (bisa diulang)") // Source remote extra flags migrateCmd.Flags().StringVar(&sourceURL, "source-url", "", "URL untuk remote sumber (Nextcloud/WebDAV)") migrateCmd.Flags().StringVar(&sourceUser, "source-user", "", "Username remote sumber") migrateCmd.Flags().StringVar(&sourcePass, "source-pass", "", "Password remote sumber") migrateCmd.Flags().StringVar(&sourceSAFile, "source-sa-file", "", "Service account JSON untuk GDrive sumber") // Dest remote extra flags migrateCmd.Flags().StringVar(&destURL, "dest-url", "", "URL untuk remote tujuan (Nextcloud/WebDAV)") migrateCmd.Flags().StringVar(&destUser, "dest-user", "", "Username remote tujuan") migrateCmd.Flags().StringVar(&destPass, "dest-pass", "", "Password remote tujuan") migrateCmd.Flags().StringVar(&destSAFile, "dest-sa-file", "", "Service account JSON untuk GDrive tujuan") migrateCmd.MarkFlagRequired("source-name") migrateCmd.MarkFlagRequired("dest-name") rootCmd.AddCommand(migrateCmd) } // ensureRemoteExists checks if remote exists and if not, creates it using provided parameters. func ensureRemoteExists(ctx context.Context, name, rtype, url, user, pass, saFile string) error { // Check if remote exists using rclone listremotes listCmd := exec.CommandContext(ctx, "rclone", "listremotes") var out bytes.Buffer listCmd.Stdout = &out if err := listCmd.Run(); err != nil { return fmt.Errorf("failed to list rclone remotes: %w", err) } remotes := strings.Split(out.String(), "\n") for _, r := range remotes { if strings.TrimSuffix(r, ":") == name { logrus.Debugf("Remote %s already exists", name) return nil } } logrus.Infof("Remote %s not found, creating...", name) switch strings.ToLower(rtype) { case "nextcloud", "webdav": if url == "" { return fmt.Errorf("url is required to create webdav/nextcloud remote") } // If pass provided, obscure it first encPass := pass if pass != "" { obscCmd := exec.CommandContext(ctx, "rclone", "obscure", pass) var ob bytes.Buffer obscCmd.Stdout = &ob if err := obscCmd.Run(); err != nil { return fmt.Errorf("failed to obscure password: %w", err) } encPass = strings.TrimSpace(ob.String()) } args := []string{"config", "create", name, "webdav", "vendor", "nextcloud", "url", url} if user != "" { args = append(args, "user", user) } if encPass != "" { args = append(args, "pass", encPass) } createCmd := exec.CommandContext(ctx, "rclone", args...) createCmd.Stdout = logrus.StandardLogger().Writer() createCmd.Stderr = logrus.StandardLogger().Writer() if err := createCmd.Run(); err != nil { return fmt.Errorf("failed to create remote: %w", err) } case "drive", "gdrive": if saFile == "" { return fmt.Errorf("sa-file is required to create gdrive remote") } args := []string{"config", "create", name, "drive", "service_account_file", saFile} createCmd := exec.CommandContext(ctx, "rclone", args...) createCmd.Stdout = logrus.StandardLogger().Writer() createCmd.Stderr = logrus.StandardLogger().Writer() if err := createCmd.Run(); err != nil { return fmt.Errorf("failed to create gdrive remote: %w", err) } default: return fmt.Errorf("auto create not supported for remote type: %s", rtype) } logrus.Infof("Remote %s created successfully", name) return nil } func runMigrate(cmd *cobra.Command, args []string) error { ctx := context.Background() // Ensure remotes exist or create them automatically if err := ensureRemoteExists(ctx, sourceName, sourceType, sourceURL, sourceUser, sourcePass, sourceSAFile); err != nil { return err } if err := ensureRemoteExists(ctx, destName, destType, destURL, destUser, destPass, destSAFile); err != nil { return err } // Initialize tracker t, err := tracker.NewTracker(trackFile) if err != nil { return fmt.Errorf("failed to initialize tracker: %w", err) } defer t.Close() // Create job ID jobID := fmt.Sprintf("%s-to-%s-%d", sourceName, destName, time.Now().Unix()) job := &tracker.MigrationJob{ ID: jobID, SourceName: sourceName, DestName: destName, Status: tracker.StatusInProgress, StartTime: time.Now(), } if err := t.CreateJob(job); err != nil { return fmt.Errorf("failed to create job: %w", err) } logrus.Infof("Starting migration from %s to %s (Job ID: %s)", sourceName, destName, jobID) // Build rclone arguments rcloneArgs := []string{"copy", sourceName + ":", destName + ":", "--progress", "--stats", "10s"} for _, pattern := range include { rcloneArgs = append(rcloneArgs, "--include", pattern) } for _, pattern := range exclude { rcloneArgs = append(rcloneArgs, "--exclude", pattern) } if resume { rcloneArgs = append(rcloneArgs, "--ignore-existing") logrus.Info("Resume mode: ignoring existing files") } cmdRclone := exec.CommandContext(ctx, "rclone", rcloneArgs...) cmdRclone.Stdout = logrus.StandardLogger().Writer() cmdRclone.Stderr = logrus.StandardLogger().Writer() start := time.Now() if err := cmdRclone.Run(); err != nil { job.Status = tracker.StatusFailed job.EndTime = time.Now() job.ErrorMessage = err.Error() t.UpdateJob(job) return fmt.Errorf("rclone finished with error: %w", err) } job.Status = tracker.StatusCompleted job.EndTime = time.Now() t.UpdateJob(job) elapsed := time.Since(start) logrus.Infof("Migration completed in %s (Job ID: %s)", elapsed, jobID) t.LogProgress(jobID) return nil }