From 11f8631929145d35367fede8e679524b55d8b1a5 Mon Sep 17 00:00:00 2001 From: StarFleetCPTN Date: Tue, 8 Apr 2025 17:30:35 -0700 Subject: [PATCH] feat: Add support for Backblaze B2 and Wasabi cloud storage - Introduced new source and destination forms for Backblaze B2 and Wasabi, allowing users to configure their cloud storage settings. - Updated the configuration generation logic to handle B2 and Wasabi, including necessary parameters such as access keys, bucket names, and endpoints. - Enhanced the user interface to provide clear instructions and security notes for both storage options, improving user experience and security awareness. --- components/config_form.templ | 49 +++++++- components/providers/common/common.templ | 4 + components/providers/destination/b2.templ | 97 +++++++++++++++ components/providers/destination/wasabi.templ | 112 ++++++++++++++++++ components/providers/source/b2.templ | 97 +++++++++++++++ components/providers/source/wasabi.templ | 112 ++++++++++++++++++ internal/db/transfer_config_store.go | 74 ++++++++++++ internal/rclone_service/rclone_service.go | 30 +++++ 8 files changed, 571 insertions(+), 4 deletions(-) create mode 100644 components/providers/destination/b2.templ create mode 100644 components/providers/destination/wasabi.templ create mode 100644 components/providers/source/b2.templ create mode 100644 components/providers/source/wasabi.templ diff --git a/components/config_form.templ b/components/config_form.templ index ca6b74e..7068c13 100644 --- a/components/config_form.templ +++ b/components/config_form.templ @@ -509,15 +509,23 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) { - + + + + + - + @@ -525,7 +533,7 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) { - + @@ -533,7 +541,7 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) { - + @@ -589,6 +597,14 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) { @destination.S3DestinationForm() + + + + @@ -626,6 +642,31 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) { @common.ArchiveOptions() +
+ +
+
+ +
+ +
+

+ Directory where files will be saved + Remote path on the server + Path within bucket. Leave empty for bucket root. + Share directory path + Path on remote server + Folder in Google Drive + Album name in Google Photos +

+
+ diff --git a/components/providers/common/common.templ b/components/providers/common/common.templ index ddde8ea..a2df517 100644 --- a/components/providers/common/common.templ +++ b/components/providers/common/common.templ @@ -195,6 +195,8 @@ templ SourceSelection() { + + @@ -219,6 +221,8 @@ templ DestinationSelection() { + + diff --git a/components/providers/destination/b2.templ b/components/providers/destination/b2.templ new file mode 100644 index 0000000..f563e71 --- /dev/null +++ b/components/providers/destination/b2.templ @@ -0,0 +1,97 @@ +package destination + +templ B2DestinationForm() { +
+ + +
+ +
+
+ +
+ +
+

+ Name of your Backblaze B2 bucket (case-sensitive) +

+
+ +
+ +
+
+ +
+ +
+

+ Your Backblaze B2 Account ID +

+
+ +
+ +
+
+ +
+ +
+

+ Your Backblaze B2 Application Key +

+
+ +
+ +
+
+ +
+ +
+

+ Custom endpoint URL (only needed for non-standard regions or private endpoints) +

+
+ +
+ +
+
+ +
+ +
+

+ Path prefix within the bucket (e.g., "backups/"). Leave empty to access the entire bucket. +

+
+ + +
+} \ No newline at end of file diff --git a/components/providers/destination/wasabi.templ b/components/providers/destination/wasabi.templ new file mode 100644 index 0000000..6cabf92 --- /dev/null +++ b/components/providers/destination/wasabi.templ @@ -0,0 +1,112 @@ +package destination + +templ WasabiDestinationForm() { +
+ + +
+ +
+
+ +
+ +
+

+ Wasabi region where your bucket is located (e.g., us-east-1, us-west-1, eu-central-1) +

+
+ +
+ +
+
+ +
+ +
+

+ Name of your Wasabi bucket (case-sensitive) +

+
+ +
+ +
+
+ +
+ +
+

+ Your Wasabi Access Key +

+
+ +
+ +
+
+ +
+ +
+

+ Your Wasabi Secret Key +

+
+ +
+ +
+
+ +
+ +
+

+ Wasabi endpoint URL (usually s3.wasabisys.com or region-specific endpoints) +

+
+ +
+ +
+
+ +
+ +
+

+ Path prefix within the bucket (e.g., "backups/"). Leave empty to access the entire bucket. +

+
+ + +
+} \ No newline at end of file diff --git a/components/providers/source/b2.templ b/components/providers/source/b2.templ new file mode 100644 index 0000000..a75f327 --- /dev/null +++ b/components/providers/source/b2.templ @@ -0,0 +1,97 @@ +package source + +templ B2SourceForm() { +
+ + +
+ +
+
+ +
+ +
+

+ Name of your Backblaze B2 bucket (case-sensitive) +

+
+ +
+ +
+
+ +
+ +
+

+ Your Backblaze B2 Account ID +

+
+ +
+ +
+
+ +
+ +
+

+ Your Backblaze B2 Application Key +

+
+ +
+ +
+
+ +
+ +
+

+ Custom endpoint URL (only needed for non-standard regions or private endpoints) +

+
+ +
+ +
+
+ +
+ +
+

+ Path prefix within the bucket (e.g., "backups/"). Leave empty to access the entire bucket. +

+
+ + +
+} \ No newline at end of file diff --git a/components/providers/source/wasabi.templ b/components/providers/source/wasabi.templ new file mode 100644 index 0000000..84292f1 --- /dev/null +++ b/components/providers/source/wasabi.templ @@ -0,0 +1,112 @@ +package source + +templ WasabiSourceForm() { +
+ + +
+ +
+
+ +
+ +
+

+ Wasabi region where your bucket is located (e.g., us-east-1, us-west-1, eu-central-1) +

+
+ +
+ +
+
+ +
+ +
+

+ Name of your Wasabi bucket (case-sensitive) +

+
+ +
+ +
+
+ +
+ +
+

+ Your Wasabi Access Key +

+
+ +
+ +
+
+ +
+ +
+

+ Your Wasabi Secret Key +

+
+ +
+ +
+
+ +
+ +
+

+ Wasabi endpoint URL (usually s3.wasabisys.com or region-specific endpoints) +

+
+ +
+ +
+
+ +
+ +
+

+ Path prefix within the bucket (e.g., "backups/"). Leave empty to access the entire bucket. +

+
+ + +
+} \ No newline at end of file diff --git a/internal/db/transfer_config_store.go b/internal/db/transfer_config_store.go index 3712782..19834ab 100644 --- a/internal/db/transfer_config_store.go +++ b/internal/db/transfer_config_store.go @@ -130,6 +130,43 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error { if output, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("failed to create source config (s3): %v\nOutput: %s", err, output) } + case "wasabi": + args := []string{ + "config", "create", sourceName, "s3", + "provider", "Wasabi", + "env_auth", "false", + "access_key_id", config.SourceAccessKey, + "secret_access_key", config.SourceSecretKey, + "region", config.SourceRegion, + "--non-interactive", + "--config", configPath, + "--log-level", "ERROR", + } + endpoint := config.SourceEndpoint + if endpoint == "" { + endpoint = "s3.wasabisys.com" + } + args = append(args, "endpoint", endpoint) + cmd := exec.Command(rclonePath, args...) + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("failed to create source config (wasabi): %v\nOutput: %s", err, output) + } + case "b2": + args := []string{ + "config", "create", sourceName, "b2", + "account", config.SourceAccessKey, // B2 Account ID + "key", config.SourceSecretKey, // B2 Application Key + "--non-interactive", + "--config", configPath, + "--log-level", "ERROR", + } + if config.SourceEndpoint != "" { + args = append(args, "endpoint", config.SourceEndpoint) + } + cmd := exec.Command(rclonePath, args...) + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("failed to create source config (b2): %v\nOutput: %s", err, output) + } case "minio": args := []string{ "config", "create", sourceName, "s3", @@ -247,6 +284,43 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error { if output, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("failed to create destination config (s3): %v\nOutput: %s", err, output) } + case "wasabi": + args := []string{ + "config", "create", destName, "s3", + "provider", "Wasabi", + "env_auth", "false", + "access_key_id", config.DestAccessKey, + "secret_access_key", config.DestSecretKey, + "region", config.DestRegion, + "--non-interactive", + "--config", configPath, + "--log-level", "ERROR", + } + endpoint := config.DestEndpoint + if endpoint == "" { + endpoint = "s3.wasabisys.com" + } + args = append(args, "endpoint", endpoint) + cmd := exec.Command(rclonePath, args...) + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("failed to create destination config (wasabi): %v\nOutput: %s", err, output) + } + case "b2": + args := []string{ + "config", "create", destName, "b2", + "account", config.DestAccessKey, // B2 Account ID + "key", config.DestSecretKey, // B2 Application Key + "--non-interactive", + "--config", configPath, + "--log-level", "ERROR", + } + if config.DestEndpoint != "" { + args = append(args, "endpoint", config.DestEndpoint) + } + cmd := exec.Command(rclonePath, args...) + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("failed to create destination config (b2): %v\nOutput: %s", err, output) + } case "minio": args := []string{ "config", "create", destName, "s3", diff --git a/internal/rclone_service/rclone_service.go b/internal/rclone_service/rclone_service.go index e527a01..002fecb 100644 --- a/internal/rclone_service/rclone_service.go +++ b/internal/rclone_service/rclone_service.go @@ -130,6 +130,36 @@ func TestRcloneConnection(config db.TransferConfig, providerType string, dbInsta if endpoint != "" { createArgs = append(createArgs, "endpoint", endpoint) } + case "wasabi": + createArgs = append(createArgs, "provider", "Wasabi", "env_auth", "false") + if accessKey != "" { + createArgs = append(createArgs, "access_key_id", accessKey) + } + if secretKey != "" { + createArgs = append(createArgs, "secret_access_key", secretKey) + } + if region != "" { + createArgs = append(createArgs, "region", region) + } + endpointValue := endpoint + if endpointValue == "" { + endpointValue = "s3.wasabisys.com" + } + createArgs = append(createArgs, "endpoint", endpointValue) + case "b2": + createArgs = append(createArgs, "provider", "B2", "env_auth", "false") + if accessKey != "" { + createArgs = append(createArgs, "account", accessKey) + } + if secretKey != "" { + createArgs = append(createArgs, "key", secretKey) + } + if endpoint != "" { + createArgs = append(createArgs, "endpoint", endpoint) + } + if region != "" { + createArgs = append(createArgs, "region", region) + } case "minio": createArgs = append(createArgs, "provider", "Minio", "env_auth", "false") if accessKey != "" {