package storage import ( "fmt" "net/url" "strings" ) // StorageURL represents a parsed storage URL. type StorageURL struct { Scheme string // "s3" or "file" Bucket string // S3 bucket name (empty for file) Prefix string // Path within bucket or filesystem base path Endpoint string // S3 endpoint (optional, default AWS) Region string // S3 region (optional) UseSSL bool // Use HTTPS for S3 (default true) } // ParseStorageURL parses a storage URL string. // Supported formats: // - s3://bucket/prefix?endpoint=host®ion=us-east-1&ssl=true // - file:///absolute/path/to/backup func ParseStorageURL(rawURL string) (*StorageURL, error) { if rawURL == "" { return nil, fmt.Errorf("storage URL is empty") } // Handle file:// URLs if strings.HasPrefix(rawURL, "file://") { path := strings.TrimPrefix(rawURL, "file://") if path == "" { return nil, fmt.Errorf("file URL path is empty") } return &StorageURL{ Scheme: "file", Prefix: path, }, nil } // Handle s3:// URLs if strings.HasPrefix(rawURL, "s3://") { u, err := url.Parse(rawURL) if err != nil { return nil, fmt.Errorf("invalid URL: %w", err) } bucket := u.Host if bucket == "" { return nil, fmt.Errorf("s3 URL missing bucket name") } prefix := strings.TrimPrefix(u.Path, "/") query := u.Query() useSSL := true if query.Get("ssl") == "false" { useSSL = false } return &StorageURL{ Scheme: "s3", Bucket: bucket, Prefix: prefix, Endpoint: query.Get("endpoint"), Region: query.Get("region"), UseSSL: useSSL, }, nil } return nil, fmt.Errorf("unsupported URL scheme: must start with s3:// or file://") } // String returns a human-readable representation of the storage URL. func (u *StorageURL) String() string { switch u.Scheme { case "file": return fmt.Sprintf("file://%s", u.Prefix) case "s3": endpoint := u.Endpoint if endpoint == "" { endpoint = "s3.amazonaws.com" } if u.Prefix != "" { return fmt.Sprintf("s3://%s/%s (endpoint: %s)", u.Bucket, u.Prefix, endpoint) } return fmt.Sprintf("s3://%s (endpoint: %s)", u.Bucket, endpoint) default: return fmt.Sprintf("%s://?", u.Scheme) } }