package backup import ( b2_client "backea/internal/client" "context" "fmt" "log" "os" "os/exec" "path/filepath" "strings" ) // KopiaProvider defines the interface for different Kopia storage backends type KopiaProvider interface { // Connect connects to an existing repository or creates a new one Connect(ctx context.Context, serviceName string, password string, configPath string) error // GetRepositoryParams returns the parameters needed for repository operations GetRepositoryParams(serviceName string) ([]string, error) // GetBucketName returns the storage identifier (bucket name or path) GetBucketName(serviceName string) string // GetProviderType returns a string identifying the provider type GetProviderType() string } // KopiaB2Provider implements the KopiaProvider interface for Backblaze B2 type KopiaB2Provider struct{} // NewKopiaB2Provider creates a new B2 provider func NewKopiaB2Provider() *KopiaB2Provider { return &KopiaB2Provider{} } // Connect connects to a B2 repository func (p *KopiaB2Provider) Connect(ctx context.Context, serviceName string, password string, configPath string) error { // Load environment variables for B2 credentials keyID := os.Getenv("B2_KEY_ID") applicationKey := os.Getenv("B2_APPLICATION_KEY") if keyID == "" || applicationKey == "" { return fmt.Errorf("B2_KEY_ID and B2_APPLICATION_KEY must be set in .env file") } bucketName := p.GetBucketName(serviceName) // Create B2 client and check if bucket exists, create if not B2Client, _ := b2_client.NewClientFromEnv() if B2Client == nil { return fmt.Errorf("B2 client not initialized") } _, err := B2Client.GetBucket(ctx, bucketName) if err != nil { log.Printf("Bucket %s not found, creating...", bucketName) _, err = B2Client.CreateBucket(ctx, bucketName, false) if err != nil { return fmt.Errorf("failed to create bucket: %w", err) } log.Printf("Created bucket: %s", bucketName) } // Try to connect to existing repository with config file connectCmd := exec.CommandContext( ctx, "kopia", "--config-file", configPath, "repository", "connect", "b2", "--bucket", bucketName, "--key-id", keyID, "--key", applicationKey, "--password", password, ) err = connectCmd.Run() if err != nil { // Connection failed, create new repository log.Printf("Creating new B2 repository for %s", serviceName) createCmd := exec.CommandContext( ctx, "kopia", "--config-file", configPath, "repository", "create", "b2", "--bucket", bucketName, "--key-id", keyID, "--key", applicationKey, "--password", password, ) createOutput, err := createCmd.CombinedOutput() if err != nil { return fmt.Errorf("failed to create repository: %w\nOutput: %s", err, createOutput) } } return nil } // GetRepositoryParams returns parameters for B2 operations func (p *KopiaB2Provider) GetRepositoryParams(serviceName string) ([]string, error) { keyID := os.Getenv("B2_KEY_ID") applicationKey := os.Getenv("B2_APPLICATION_KEY") if keyID == "" || applicationKey == "" { return nil, fmt.Errorf("B2_KEY_ID and B2_APPLICATION_KEY must be set in .env file") } bucketName := p.GetBucketName(serviceName) return []string{ "b2", "--bucket", bucketName, "--key-id", keyID, "--key", applicationKey, }, nil } // Modify the GetBucketName method in your KopiaB2Provider func (p *KopiaB2Provider) GetBucketName(serviceName string) string { // Backblaze dont allow . sanitized := strings.ReplaceAll(serviceName, ".", "-") // Add a prefix to make the bucket name unique return fmt.Sprintf("backea-%s", sanitized) } // GetProviderType returns the provider type identifier func (p *KopiaB2Provider) GetProviderType() string { return "b2" } // KopiaLocalProvider implements the KopiaProvider interface for local storage type KopiaLocalProvider struct { BasePath string } // NewKopiaLocalProvider creates a new local provider func NewKopiaLocalProvider(basePath string) *KopiaLocalProvider { // If basePath is empty, use a default location if basePath == "" { basePath = filepath.Join(os.Getenv("HOME"), ".backea", "repos") } return &KopiaLocalProvider{ BasePath: basePath, } } // Connect connects to a local repository func (p *KopiaLocalProvider) Connect(ctx context.Context, serviceName string, password string, configPath string) error { repoPath := filepath.Join(p.BasePath, serviceName) log.Printf("Connecting to local repository at %s with config: %s", repoPath, configPath) // Ensure the directory exists if err := os.MkdirAll(repoPath, 0755); err != nil { return fmt.Errorf("failed to create repository directory: %w", err) } // Try to connect to existing repository with config file connectCmd := exec.CommandContext( ctx, "kopia", "--config-file", configPath, "repository", "connect", "filesystem", "--path", repoPath, "--password", password, ) err := connectCmd.Run() if err != nil { // Connection failed, create new repository log.Printf("Creating new local repository for %s at destination: %s", serviceName, repoPath) createCmd := exec.CommandContext( ctx, "kopia", "--config-file", configPath, "repository", "create", "filesystem", "--path", repoPath, "--password", password, ) createOutput, err := createCmd.CombinedOutput() if err != nil { return fmt.Errorf("failed to create repository: %w\nOutput: %s", err, createOutput) } } return nil } // GetRepositoryParams returns parameters for local filesystem operations func (p *KopiaLocalProvider) GetRepositoryParams(serviceName string) ([]string, error) { repoPath := filepath.Join(p.BasePath, serviceName) return []string{ "filesystem", "--path", repoPath, }, nil } // GetBucketName returns the path for a service func (p *KopiaLocalProvider) GetBucketName(serviceName string) string { return filepath.Join(p.BasePath, serviceName) } // GetProviderType returns the provider type identifier func (p *KopiaLocalProvider) GetProviderType() string { return "local" } // KopiaSFTPProvider implements the KopiaProvider interface for SFTP remote storage type KopiaSFTPProvider struct { Host string BasePath string Username string KeyFile string } // NewKopiaSFTPProvider creates a new SFTP provider func NewKopiaSFTPProvider(host, basePath, username, keyFile string) *KopiaSFTPProvider { return &KopiaSFTPProvider{ Host: host, BasePath: basePath, Username: username, KeyFile: keyFile, } } // Connect connects to an SFTP repository func (p *KopiaSFTPProvider) Connect(ctx context.Context, serviceName string, password string, configPath string) error { repoPath := fmt.Sprintf("%s@%s:%s/%s", p.Username, p.Host, p.BasePath, serviceName) // Try to connect to existing repository with config file connectCmd := exec.CommandContext( ctx, "kopia", "--config-file", configPath, "repository", "connect", "sftp", "--path", repoPath, "--keyfile", p.KeyFile, "--known-hosts", filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts"), "--password", password, ) err := connectCmd.Run() if err != nil { // Connection failed, create new repository log.Printf("Creating new SFTP repository for %s at %s", serviceName, repoPath) createCmd := exec.CommandContext( ctx, "kopia", "--config-file", configPath, "repository", "create", "sftp", "--path", repoPath, "--keyfile", p.KeyFile, "--known-hosts", filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts"), "--password", password, ) createOutput, err := createCmd.CombinedOutput() if err != nil { return fmt.Errorf("failed to create repository: %w\nOutput: %s", err, createOutput) } } return nil } // GetRepositoryParams returns parameters for SFTP operations func (p *KopiaSFTPProvider) GetRepositoryParams(serviceName string) ([]string, error) { repoPath := fmt.Sprintf("%s@%s:%s/%s", p.Username, p.Host, p.BasePath, serviceName) return []string{ "sftp", "--path", repoPath, "--keyfile", p.KeyFile, "--known-hosts", filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts"), }, nil } // GetBucketName returns the path for a service func (p *KopiaSFTPProvider) GetBucketName(serviceName string) string { return fmt.Sprintf("%s/%s", p.BasePath, serviceName) } // GetProviderType returns the provider type identifier func (p *KopiaSFTPProvider) GetProviderType() string { return "sftp" }