+
Import from rclone Config
- Upload your rclone configuration file to import storage providers. You'll be able to preview and select which remotes to import.
+ Upload your rclone configuration file to import storage providers. You'll be able to preview and select which remotes to import, and add any missing credentials.
+
+
+
+
+
Instructions:
+
+ Upload your rclone.conf file (usually found in ~/.config/rclone/ or %USERPROFILE%\.config\rclone\)
+ Review the detected remotes and select which ones to import
+ Add any missing credentials that may not be in your config file
+ Use the "Add Custom Field" button to add any provider-specific options
+
+
+
+
+
+
+
+
+
Common Configuration Values by Provider Type
+
+
+
SFTP
+
+ host: Server hostname or IP
+ user: Username
+ pass: Password (if not using key)
+ port: SSH port (default: 22)
+ key_file: Path to private key
+
+
+
+
S3 / Wasabi / Minio
+
+ access_key_id: Access key
+ secret_access_key: Secret key
+ region: Region name
+ endpoint: Custom endpoint URL
+ bucket: Bucket name
+
+
+
+
FTP
+
+ host: Server hostname or IP
+ user: Username
+ pass: Password
+ port: FTP port (default: 21)
+ tls: Use FTPS (true/false)
+
+
+
+
Google Drive
+
+ client_id: OAuth client ID
+ client_secret: OAuth client secret
+ refresh_token: OAuth refresh token
+ team_drive: Team Drive ID (optional)
+
+
+
+
OneDrive
+
+ client_id: OAuth client ID
+ client_secret: OAuth client secret
+ refresh_token: OAuth refresh token
+ drive_id: Drive ID (optional)
+
+
+
+
WebDAV / Nextcloud
+
+ url: WebDAV URL
+ user: Username
+ pass: Password
+ vendor: nextcloud/owncloud/etc
+
+
+
+
Hide reference
+
} else {
diff --git a/internal/web/handlers/storage_provider_handlers.go b/internal/web/handlers/storage_provider_handlers.go
index 01f7cf7..2d9e27a 100644
--- a/internal/web/handlers/storage_provider_handlers.go
+++ b/internal/web/handlers/storage_provider_handlers.go
@@ -6,6 +6,7 @@ import (
"io"
"log"
"net/http"
+ "regexp"
"strconv"
"strings"
"time"
@@ -13,6 +14,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/starfleetcptn/gomft/components"
"github.com/starfleetcptn/gomft/internal/db"
+ "github.com/starfleetcptn/gomft/internal/encryption"
"github.com/starfleetcptn/gomft/internal/storage"
)
@@ -455,10 +457,16 @@ func (h *Handlers) HandleStorageProvidersImportPreview(c *gin.Context) {
// Handler for confirming import of selected remotes
func (h *Handlers) HandleStorageProvidersImportConfirm(c *gin.Context) {
+ // Ensure encryption service is available
+ _, err := encryption.GetGlobalCredentialEncryptor()
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to initialize encryption: " + err.Error()})
+ return
+ }
userID := c.GetUint("userID")
// Make sure form is parsed
- err := c.Request.ParseMultipartForm(32 << 20) // 32MB max memory
+ err = c.Request.ParseMultipartForm(32 << 20) // 32MB max memory
if err != nil {
log.Printf("Error parsing form: %v", err)
}
@@ -516,9 +524,21 @@ func (h *Handlers) HandleStorageProvidersImportConfirm(c *gin.Context) {
case "client_id":
provider.ClientID = fieldValue
case "client_secret":
- provider.ClientSecret = fieldValue
+ // Check if the value is already encrypted from rclone
+ if isRcloneEncryptedValue(fieldValue) {
+ // Store directly in EncryptedClientSecret to avoid double encryption
+ provider.EncryptedClientSecret = fieldValue
+ } else {
+ provider.ClientSecret = fieldValue
+ }
case "refresh_token":
- provider.RefreshToken = fieldValue
+ // Check if the value is already encrypted from rclone
+ if isRcloneEncryptedValue(fieldValue) {
+ // Store directly in EncryptedRefreshToken to avoid double encryption
+ provider.EncryptedRefreshToken = fieldValue
+ } else {
+ provider.RefreshToken = fieldValue
+ }
case "token":
// Token is a JSON object containing access_token, refresh_token, etc.
// Extract refresh_token if not already set
@@ -547,7 +567,13 @@ func (h *Handlers) HandleStorageProvidersImportConfirm(c *gin.Context) {
case "access_key_id", "access_key":
provider.AccessKey = fieldValue
case "secret_access_key", "secret_key":
- provider.SecretKey = fieldValue
+ // Check if the value is already encrypted from rclone
+ if isRcloneEncryptedValue(fieldValue) {
+ // Store directly in EncryptedSecretKey to avoid double encryption
+ provider.EncryptedSecretKey = fieldValue
+ } else {
+ provider.SecretKey = fieldValue
+ }
case "endpoint":
provider.Endpoint = fieldValue
case "region":
@@ -565,7 +591,13 @@ func (h *Handlers) HandleStorageProvidersImportConfirm(c *gin.Context) {
case "user", "username":
provider.Username = fieldValue
case "pass", "password":
- provider.Password = fieldValue
+ // Check if the value is already encrypted from rclone
+ if isRcloneEncryptedValue(fieldValue) {
+ // Store directly in EncryptedPassword to avoid double encryption
+ provider.EncryptedPassword = fieldValue
+ } else {
+ provider.Password = fieldValue
+ }
case "port":
if port, err := strconv.Atoi(fieldValue); err == nil {
provider.Port = port
@@ -581,7 +613,13 @@ func (h *Handlers) HandleStorageProvidersImportConfirm(c *gin.Context) {
case "user", "username":
provider.Username = fieldValue
case "pass", "password":
- provider.Password = fieldValue
+ // Check if the value is already encrypted from rclone
+ if isRcloneEncryptedValue(fieldValue) {
+ // Store directly in EncryptedPassword to avoid double encryption
+ provider.EncryptedPassword = fieldValue
+ } else {
+ provider.Password = fieldValue
+ }
case "domain":
provider.Domain = fieldValue
case "share":
@@ -595,9 +633,21 @@ func (h *Handlers) HandleStorageProvidersImportConfirm(c *gin.Context) {
case "client_id":
provider.ClientID = fieldValue
case "client_secret":
- provider.ClientSecret = fieldValue
+ // Check if the value is already encrypted from rclone
+ if isRcloneEncryptedValue(fieldValue) {
+ // Store directly in EncryptedClientSecret to avoid double encryption
+ provider.EncryptedClientSecret = fieldValue
+ } else {
+ provider.ClientSecret = fieldValue
+ }
case "refresh_token":
- provider.RefreshToken = fieldValue
+ // Check if the value is already encrypted from rclone
+ if isRcloneEncryptedValue(fieldValue) {
+ // Store directly in EncryptedRefreshToken to avoid double encryption
+ provider.EncryptedRefreshToken = fieldValue
+ } else {
+ provider.RefreshToken = fieldValue
+ }
case "drive_id":
provider.DriveID = fieldValue
}
@@ -743,6 +793,12 @@ func (h *Handlers) HandleImportRcloneConfig(c *gin.Context) {
errors = append(errors, name+": "+err.Error())
continue
}
+ // Log that we're handling already encrypted values from rclone
+ if provider.EncryptedPassword != "" || provider.EncryptedSecretKey != "" ||
+ provider.EncryptedClientSecret != "" || provider.EncryptedRefreshToken != "" {
+ log.Printf("Processing provider %s with pre-encrypted values from rclone", provider.Name)
+ }
+
// Try to create or update
err = createOrUpdateStorageProvider(h.DB, &provider)
if err != nil {
@@ -787,6 +843,27 @@ func parseRcloneConfig(content []byte) (map[string]map[string]string, error) {
// Helper: create or update provider (fallback if DB method not present)
func createOrUpdateStorageProvider(dbh *db.DB, provider *db.StorageProvider) error {
+ // Handle already-encrypted fields from rclone
+ // If we have values in the encrypted fields, we need to ensure they're properly formatted
+ // for our database storage mechanism
+ if provider.EncryptedPassword != "" && !strings.HasPrefix(provider.EncryptedPassword, encryption.EncryptedPrefix) {
+ // Add our encryption prefix to make it compatible with our system
+ provider.EncryptedPassword = encryption.EncryptedPrefix + provider.EncryptedPassword
+ }
+
+ if provider.EncryptedSecretKey != "" && !strings.HasPrefix(provider.EncryptedSecretKey, encryption.EncryptedPrefix) {
+ provider.EncryptedSecretKey = encryption.EncryptedPrefix + provider.EncryptedSecretKey
+ }
+
+ if provider.EncryptedClientSecret != "" && !strings.HasPrefix(provider.EncryptedClientSecret, encryption.EncryptedPrefix) {
+ provider.EncryptedClientSecret = encryption.EncryptedPrefix + provider.EncryptedClientSecret
+ }
+
+ if provider.EncryptedRefreshToken != "" && !strings.HasPrefix(provider.EncryptedRefreshToken, encryption.EncryptedPrefix) {
+ provider.EncryptedRefreshToken = encryption.EncryptedPrefix + provider.EncryptedRefreshToken
+ }
+
+ // Now proceed with create/update
existing, err := dbh.GetStorageProviderByNameAndUser(provider.Name, provider.CreatedBy)
if err == nil && existing != nil {
provider.ID = existing.ID
@@ -795,6 +872,24 @@ func createOrUpdateStorageProvider(dbh *db.DB, provider *db.StorageProvider) err
return dbh.CreateStorageProvider(provider)
}
+// isRcloneEncryptedValue checks if a value from rclone config is already encrypted
+func isRcloneEncryptedValue(value string) bool {
+ // Rclone typically uses these patterns for encrypted values
+ // 1. Base64-encoded values with specific patterns
+ if strings.HasPrefix(value, "enc_") || strings.HasPrefix(value, "mcrypt_") {
+ return true
+ }
+ // 2. Values that look like encrypted data (base64 with specific length)
+ if len(value) > 20 && regexp.MustCompile(`^[A-Za-z0-9+/=]+$`).MatchString(value) {
+ return true
+ }
+ // 3. Values with typical encryption markers
+ if strings.Contains(value, "ENCRYPTED") || strings.Contains(value, "ENCR:") {
+ return true
+ }
+ return false
+}
+
// Helper: convert rclone section to StorageProvider
func storageProviderFromRcloneSection(name string, section map[string]string, userID uint) (db.StorageProvider, error) {
providerType, ok := section["type"]
@@ -821,7 +916,13 @@ func storageProviderFromRcloneSection(name string, section map[string]string, us
case "user":
provider.Username = v
case "pass":
- provider.Password = v
+ // Handle potentially encrypted password
+ if isRcloneEncryptedValue(v) {
+ // Store directly in EncryptedPassword to avoid double encryption
+ provider.EncryptedPassword = v
+ } else {
+ provider.Password = v
+ }
case "port":
if port, err := strconv.Atoi(v); err == nil {
provider.Port = port
@@ -833,11 +934,35 @@ func storageProviderFromRcloneSection(name string, section map[string]string, us
case "access_key_id":
provider.AccessKey = v
case "secret_access_key":
- provider.SecretKey = v
+ // Handle potentially encrypted secret key
+ if isRcloneEncryptedValue(v) {
+ // Store directly in EncryptedSecretKey to avoid double encryption
+ provider.EncryptedSecretKey = v
+ } else {
+ provider.SecretKey = v
+ }
case "endpoint":
provider.Endpoint = v
case "domain":
provider.Domain = v
+ case "client_id":
+ provider.ClientID = v
+ case "client_secret":
+ // Handle potentially encrypted client secret
+ if isRcloneEncryptedValue(v) {
+ // Store directly in EncryptedClientSecret to avoid double encryption
+ provider.EncryptedClientSecret = v
+ } else {
+ provider.ClientSecret = v
+ }
+ case "refresh_token":
+ // Handle potentially encrypted refresh token
+ if isRcloneEncryptedValue(v) {
+ // Store directly in EncryptedRefreshToken to avoid double encryption
+ provider.EncryptedRefreshToken = v
+ } else {
+ provider.RefreshToken = v
+ }
// Add more mappings as needed
}
}