diff --git a/components/providers/provider_form.templ b/components/providers/provider_form.templ deleted file mode 100644 index 5f7534a..0000000 --- a/components/providers/provider_form.templ +++ /dev/null @@ -1,191 +0,0 @@ -package providers - -import ( - "fmt" - "strings" - - "github.com/starfleetcptn/gomft/components/providers/common" - "github.com/starfleetcptn/gomft/components/providers/source" - "github.com/starfleetcptn/gomft/components/providers/destination" -) - -// Returns the form ID based on the form type and whether it's a source or destination -func formID(formType string, isSource bool) string { - if isSource { - return "source_config_form" - } - return "destination_config_form" -} - -// Returns a user-friendly display name for the provider -func providerDisplayName(provider string) string { - switch provider { - case "sftp": - return "SFTP" - case "local": - return "Local Filesystem" - case "s3": - return "Amazon S3" - case "ftp": - return "FTP" - case "azure": - return "Azure Blob Storage" - default: - return strings.Title(provider) - } -} - -templ ProviderForm(formType string, providers []string, isSource bool) { -
-} - -script formAlpineInit() { - return { - initProviderForm() { - // Initialize with values if editing existing config - if (window.editData && window.editData.configs) { - const config = window.editData.configs.find(c => - isSource ? (c.id === window.editData.source_config_id) : (c.id === window.editData.destination_config_id) - ); - - if (config) { - this[formType + 'Provider'] = config.provider; - this.name = config.name; - - // Provider-specific fields - if (config.provider === 'sftp') { - this.host = config.host; - this.port = config.port; - this.username = config.username; - this.path = config.path; - - if (config.key_file && config.key_file !== '') { - this.authType = 'key_file'; - this.keyFile = config.key_file; - } else { - this.authType = 'password'; - // Password is not included in edit data for security - } - } else if (config.provider === 'local') { - this.path = config.path; - } else if (config.provider === 's3') { - this.bucket = config.bucket; - this.region = config.region; - this.path = config.path; - this.accessKey = config.access_key; - - if (config.endpoint && config.endpoint !== '') { - this.useCustomEndpoint = true; - this.endpoint = config.endpoint; - } else { - this.useCustomEndpoint = false; - } - } else if (config.provider === 'ftp') { - this.host = config.host; - this.port = config.port; - this.username = config.username; - this.path = config.path; - this.useFTPS = config.use_ftps; - } - - // Advanced options - if (config.include_pattern) this.filePattern = config.include_pattern; - if (config.exclude_pattern) this.excludePattern = config.exclude_pattern; - - if (isSource && config.extract_archives) { - this.extractArchives = true; - this.deleteArchives = config.delete_archives; - } - } - } - }, - - providerChanged() { - console.log("Provider changed to: " + this[formType + 'Provider']); - } - }; -} \ No newline at end of file diff --git a/components/storage_provider_form.templ b/components/storage_provider_form.templ index 6134dd1..c57ea23 100644 --- a/components/storage_provider_form.templ +++ b/components/storage_provider_form.templ @@ -196,7 +196,7 @@ templ formFields(data StorageProviderFormData) { -Leave empty for default (SFTP: 22, FTP: 21, SMB: 445, WebDAV: 80/443)
@@ -533,6 +533,17 @@ templ providerFormScript() { clientSecretLength: document.getElementById('hidden_clientSecret').value.length }); } + + // Debug WebDAV form submission + if (['webdav', 'nextcloud'].includes(providerType)) { + console.log('Submitting WebDAV form with:', { + provider: providerType, + host: document.getElementById('host').value, + username: document.getElementById('username').value, + password: document.getElementById('password').value ? '[PRESENT]' : '[EMPTY]', + passwordLength: document.getElementById('password').value.length + }); + } }); }); }); @@ -580,6 +591,59 @@ templ providerFormScript() { // SFTP/Hetzner-specific fields if (provider === 'sftp' || provider === 'hetzner') { document.getElementById('key-file-field').classList.remove('hidden'); + + // Update placeholders for Hetzner + if (provider === 'hetzner') { + // Update host field + const hostField = document.getElementById('host'); + if (hostField) { + hostField.placeholder = "uXXXXXX.your-storagebox.de"; + + // Update host label and description + const hostLabel = document.querySelector('label[for="host"]'); + if (hostLabel) { + hostLabel.textContent = "Storage Box Host"; + } + + const hostDescription = hostField.nextElementSibling; + if (hostDescription?.tagName === 'P') { + hostDescription.textContent = "Your Hetzner Storage Box hostname (e.g., uXXXXXX.your-storagebox.de)"; + } + } + + // Update username field + const usernameField = document.getElementById('username'); + if (usernameField) { + usernameField.placeholder = "uXXXXXX"; + + // Update username description + const usernameDescription = usernameField.nextElementSibling; + if (usernameDescription?.tagName === 'P') { + usernameDescription.textContent = "Your Hetzner Storage Box username (typically matches your Storage Box number)"; + } + } + + // Update key file field + const keyFileField = document.getElementById('keyFile'); + if (keyFileField) { + keyFileField.placeholder = "/path/to/id_rsa"; + + // Update key file description + const keyFileDescription = keyFileField.nextElementSibling; + if (keyFileDescription?.tagName === 'P') { + keyFileDescription.textContent = "Path to your SSH private key file for Hetzner Storage Box authentication"; + } + } + + // Update port field description + const portField = document.getElementById('port'); + if (portField) { + const portDescription = portField.nextElementSibling; + if (portDescription?.tagName === 'P') { + portDescription.textContent = "Connection port for Hetzner Storage Box (default: 23)"; + } + } + } } // FTP-specific fields @@ -591,7 +655,141 @@ templ providerFormScript() { // WebDAV-based fields if (['webdav', 'nextcloud'].includes(provider)) { document.getElementById('sftp-ftp-fields').classList.remove('hidden'); - // No special fields currently for WebDAV + // Hide fields that WebDAV doesn't use + document.getElementById('port-field').classList.add('hidden'); + + // Update placeholders for WebDAV + const hostField = document.getElementById('host'); + if (hostField) { + hostField.placeholder = "https://webdav.example.com"; + } + + // Update label for host field + const hostLabel = document.querySelector('label[for="host"]'); + if (hostLabel) { + hostLabel.textContent = "WebDAV URL"; + } + + // Update description for host field + const hostDescription = hostField?.nextElementSibling; + if (hostDescription?.tagName === 'P') { + hostDescription.textContent = provider === 'webdav' ? + "Full URL to your WebDAV server including protocol (https://)" : + "Full URL to your Nextcloud WebDAV endpoint (e.g., https://nextcloud.example.com/remote.php/dav/files/username/)"; + } + + // Update username field + const usernameField = document.getElementById('username'); + if (usernameField) { + usernameField.placeholder = provider === 'nextcloud' ? "nextcloud_username" : "webdav_username"; + + // Update username description + const usernameDescription = usernameField.nextElementSibling; + if (usernameDescription?.tagName === 'P') { + usernameDescription.textContent = provider === 'nextcloud' ? + "Your Nextcloud username for authentication" : + "Your WebDAV username for authentication"; + } + } + + // Update password field + const passwordField = document.getElementById('password'); + if (passwordField) { + passwordField.name = "password"; // Ensure name is set correctly + + // Update password description + const passwordDescription = passwordField.nextElementSibling; + if (passwordDescription?.tagName === 'P') { + // Check if we're in edit mode + const editMode = passwordDescription.textContent.includes("Leave empty to keep"); + + if (editMode) { + // Edit mode - tell user they can leave password empty to keep current one + passwordDescription.textContent = provider === 'nextcloud' ? + "Leave empty to keep the current Nextcloud password" : + "Leave empty to keep the current WebDAV password"; + } else { + // New provider - show regular password help text + passwordDescription.textContent = provider === 'nextcloud' ? + "Your Nextcloud password or app-specific password" : + "Your WebDAV password for authentication"; + } + } + } + + if (document.getElementById('key-file-field')) { + document.getElementById('key-file-field').classList.add('hidden'); + } + if (document.getElementById('domain-field')) { + document.getElementById('domain-field').classList.add('hidden'); + } + } else { + // Reset placeholders for other providers + const hostField = document.getElementById('host'); + if (hostField && ['sftp', 'ftp', 'smb'].includes(provider)) { + hostField.placeholder = provider === 'sftp' ? "sftp.example.com" : + provider === 'ftp' ? "ftp.example.com" : + "192.168.1.10"; + } + + // Reset label for host field + const hostLabel = document.querySelector('label[for="host"]'); + if (hostLabel) { + hostLabel.textContent = "Host"; + } + + // Reset username field + const usernameField = document.getElementById('username'); + if (usernameField) { + usernameField.placeholder = provider === 'sftp' ? "sftp_username" : + provider === 'ftp' ? "ftp_username" : + provider === 'smb' ? "smb_username" : "username"; + + // Reset username description + const usernameDescription = usernameField.nextElementSibling; + if (usernameDescription?.tagName === 'P') { + usernameDescription.textContent = "Your login username"; + } + } + + // Reset password field + const passwordField = document.getElementById('password'); + if (passwordField) { + // Reset password description + const passwordDescription = passwordField.nextElementSibling; + if (passwordDescription?.tagName === 'P') { + // Check if we're in edit mode + const editMode = passwordDescription.textContent.includes("Leave empty to keep"); + + if (editMode) { + // Edit mode - tell user they can leave password empty to keep current one + if (provider === 'sftp') { + passwordDescription.textContent = "Leave empty to keep the current SFTP password"; + } else if (provider === 'ftp') { + passwordDescription.textContent = "Leave empty to keep the current FTP password"; + } else if (provider === 'smb') { + passwordDescription.textContent = "Leave empty to keep the current SMB password"; + } else if (provider === 'hetzner') { + passwordDescription.textContent = "Leave empty to keep the current Hetzner Storage Box password"; + } else { + passwordDescription.textContent = "Leave empty to keep the current password"; + } + } else { + // New provider - show regular password help text + if (provider === 'sftp') { + passwordDescription.textContent = "Your SFTP server password"; + } else if (provider === 'ftp') { + passwordDescription.textContent = "Your FTP server password"; + } else if (provider === 'smb') { + passwordDescription.textContent = "Your SMB/CIFS share password"; + } else if (provider === 'hetzner') { + passwordDescription.textContent = "Your Hetzner Storage Box password"; + } else { + passwordDescription.textContent = "Your login password"; + } + } + } + } } // SMB-specific fields @@ -610,6 +808,40 @@ templ providerFormScript() { const regionInput = document.getElementById('region'); const endpointInput = document.getElementById('endpoint'); + // Update Secret Key field description based on provider type + const secretKeyField = document.getElementById('secretKey'); + if (secretKeyField) { + const secretKeyDescription = secretKeyField.nextElementSibling; + if (secretKeyDescription?.tagName === 'P') { + // Check if in edit mode + const editMode = secretKeyDescription.textContent.includes("Leave empty to keep"); + + if (editMode) { + // Edit mode - provider specific text + if (provider === 'b2') { + secretKeyDescription.textContent = "Leave empty to keep the current B2 Application Key"; + } else if (provider === 'wasabi') { + secretKeyDescription.textContent = "Leave empty to keep the current Wasabi Secret Key"; + } else if (provider === 'minio') { + secretKeyDescription.textContent = "Leave empty to keep the current MinIO Secret Key"; + } else { + secretKeyDescription.textContent = "Leave empty to keep the current AWS Secret Access Key"; + } + } else { + // New provider - provider specific text + if (provider === 'b2') { + secretKeyDescription.textContent = "Your Backblaze B2 Application Key"; + } else if (provider === 'wasabi') { + secretKeyDescription.textContent = "Your Wasabi Secret Key"; + } else if (provider === 'minio') { + secretKeyDescription.textContent = "Your MinIO Secret Key"; + } else { + secretKeyDescription.textContent = "Your AWS Secret Access Key"; + } + } + } + } + // For B2: endpoint and region are optional if (provider === 'b2') { regionRequired.style.display = 'none'; @@ -636,6 +868,26 @@ templ providerFormScript() { // Google services if (['google_drive', 'google_photo'].includes(provider)) { document.getElementById('cloud-fields').classList.remove('hidden'); + document.getElementById('built-in-auth-field').classList.remove('hidden'); + + // Update client secret field description + const clientSecretField = document.getElementById('clientSecret'); + if (clientSecretField) { + const clientSecretDescription = clientSecretField.nextElementSibling; + if (clientSecretDescription?.tagName === 'P') { + // Check if in edit mode + const editMode = clientSecretDescription.textContent.includes("Leave empty to keep"); + + if (editMode) { + // Edit mode - provider specific text + if (provider === 'google_drive') { + clientSecretDescription.textContent = "Leave empty to keep the current Google Drive client secret"; + } else if (provider === 'google_photo') { + clientSecretDescription.textContent = "Leave empty to keep the current Google Photos client secret"; + } + } + } + } // Google Drive specific fields if (provider === 'google_drive') { @@ -645,14 +897,29 @@ templ providerFormScript() { // Google Photos specific fields if (provider === 'google_photo') { - document.getElementById('readonly-field').classList.remove('hidden'); + document.getElementById('gphotos-options-field').classList.remove('hidden'); } } // OneDrive if (provider === 'onedrive') { document.getElementById('cloud-fields').classList.remove('hidden'); - // No special fields for OneDrive currently + + // Update client secret field description + const clientSecretField = document.getElementById('clientSecret'); + if (clientSecretField) { + const clientSecretDescription = clientSecretField.nextElementSibling; + if (clientSecretDescription?.tagName === 'P') { + // Check if in edit mode + const editMode = clientSecretDescription.textContent.includes("Leave empty to keep"); + + if (editMode) { + clientSecretDescription.textContent = "Leave empty to keep the current OneDrive client secret"; + } else { + clientSecretDescription.textContent = "Your Microsoft Azure OAuth client secret"; + } + } + } } // Local filesystem fields diff --git a/internal/db/storage_provider.go b/internal/db/storage_provider.go index 243cbf6..a0bc6c1 100644 --- a/internal/db/storage_provider.go +++ b/internal/db/storage_provider.go @@ -184,7 +184,7 @@ func (sp *StorageProvider) GetSensitiveFields() map[string]string { // Add fields based on provider type switch sp.Type { - case ProviderTypeSFTP, ProviderTypeFTP, ProviderTypeSMB, ProviderTypeHetzner: + case ProviderTypeSFTP, ProviderTypeFTP, ProviderTypeSMB, ProviderTypeHetzner, ProviderTypeWebDAV, ProviderTypeNextcloud: if sp.Password != "" { sensitiveFields["Password"] = sp.Password } diff --git a/internal/db/storage_provider_validator.go b/internal/db/storage_provider_validator.go index 141ebf7..fd88832 100644 --- a/internal/db/storage_provider_validator.go +++ b/internal/db/storage_provider_validator.go @@ -75,6 +75,11 @@ func (sp *StorageProvider) validateWebDAV() error { return errors.New("host is required for WebDAV provider") } + // Host must include the protocol + if !strings.HasPrefix(sp.Host, "http://") && !strings.HasPrefix(sp.Host, "https://") { + return errors.New("host must include the protocol (http:// or https://)") + } + if strings.TrimSpace(sp.Username) == "" { return errors.New("username is required for WebDAV provider") } diff --git a/internal/db/transfer_config_store.go b/internal/db/transfer_config_store.go index ec2f169..76310c3 100644 --- a/internal/db/transfer_config_store.go +++ b/internal/db/transfer_config_store.go @@ -399,7 +399,8 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error { case "webdav", "nextcloud": // Handle both webdav and nextcloud similarly // Construct the WebDAV URL // Parse the provided source URL, assuming it includes the scheme - inputURL := config.SourceHost + inputURL := getStringValue(sourceCredentials, "host", config.SourceHost) + fmt.Printf("Input URL: %s\n", inputURL) parsedURL, err := url.Parse(inputURL) if err != nil { return fmt.Errorf("failed to parse source URL '%s': %v", inputURL, err) @@ -424,12 +425,30 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error { "config", "create", sourceName, "webdav", "url", webdavURL, "vendor", vendor, - "user", config.SourceUser, - "pass", config.SourcePassword, // rclone obscures this + "user", getStringValue(sourceCredentials, "username", config.SourceUser), "--non-interactive", "--config", configPath, "--log-level", "ERROR", } + + // Handle password + password := "" + if config.SourcePassword != "" { + password = config.SourcePassword + } else if encryptedPwd, ok := sourceCredentials["encrypted_password"].(string); ok && encryptedPwd != "" { + decryptedPwd, err := db.DecryptCredential(encryptedPwd) + if err != nil { + return fmt.Errorf("failed to decrypt source password: %v", err) + } + password = decryptedPwd + } else if pwVal, ok := sourceCredentials["password"].(string); ok && pwVal != "" { + password = pwVal + } + + if password != "" { + args = append(args, "pass", password) + } + cmd := exec.Command(rclonePath, args...) if output, err := cmd.CombinedOutput(); err != nil { errorMsg := fmt.Sprintf("failed to create source config (%s): %v", config.SourceType, err) @@ -751,7 +770,7 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error { case "webdav", "nextcloud": // Combined case for WebDAV and Nextcloud // Parse and reconstruct the WebDAV URL robustly // Parse the provided destination URL, assuming it includes the scheme - inputURL := config.DestHost + inputURL := getStringValue(destCredentials, "host", config.DestHost) parsedURL, err := url.Parse(inputURL) if err != nil { return fmt.Errorf("failed to parse destination URL '%s': %v", inputURL, err) diff --git a/internal/web/handlers/storage_provider_handlers.go b/internal/web/handlers/storage_provider_handlers.go index bcaf461..fe5142e 100644 --- a/internal/web/handlers/storage_provider_handlers.go +++ b/internal/web/handlers/storage_provider_handlers.go @@ -2,6 +2,7 @@ package handlers import ( "fmt" + "log" "net/http" "strconv" "strings" @@ -446,6 +447,19 @@ func (h *Handlers) parseProviderFromForm(c *gin.Context) (db.StorageProvider, er provider.Username = c.PostForm("username") provider.Password = c.PostForm("password") + // Log for debugging + log.Printf("WebDAV provider: username=%s, password present=%v", + provider.Username, provider.Password != "") + + // WebDAV uses Host without port (full URL) + if provider.Host == "" { + return provider, fmt.Errorf("host/URL is required for WebDAV provider") + } + + if provider.Username == "" { + return provider, fmt.Errorf("username is required for WebDAV provider") + } + case db.ProviderTypeS3, db.ProviderTypeWasabi, db.ProviderTypeMinio, db.ProviderTypeB2: // S3 specific fields provider.AccessKey = c.PostForm("accessKey")