211 lines
7.6 KiB
Go
211 lines
7.6 KiB
Go
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
|
|
}
|