backea/internal/backup/kopia_providers.go
2025-03-20 22:14:45 +01:00

278 lines
8.2 KiB
Go

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"
}