feat: Update storage provider form and validation enhancements

- Added new HTML elements and JavaScript functionality to improve the user experience for WebDAV and Hetzner storage providers.
- Updated form field descriptions and placeholders for clarity, ensuring users understand the required input.
- Enhanced validation logic for WebDAV to ensure the host includes the protocol.
- Improved logging for debugging during form submissions, particularly for WebDAV credentials.
- Removed the deprecated provider form component to streamline the codebase.
This commit is contained in:
StarFleetCPTN
2025-04-18 03:54:48 -07:00
parent dc665b9fa4
commit b819162732
6 changed files with 314 additions and 200 deletions
-191
View File
@@ -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) {
<form
id={formID(formType, isSource)}
x-data={fmt.Sprintf("{ %sProvider: '', showAdvanced: false }", formType)}
class="space-y-8">
<div class="grid grid-cols-1 sm:grid-cols-12 gap-y-6 gap-x-4">
@common.NameField()
<div class="sm:col-span-4">
<label for={fmt.Sprintf("%s_provider", formType)} class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Provider Type</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-server text-secondary-400 dark:text-secondary-600"></i>
</div>
<select
id={fmt.Sprintf("%s_provider", formType)}
name={fmt.Sprintf("%s_provider", formType)}
x-model={fmt.Sprintf("%sProvider", formType)}
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
<option value="" disabled selected>Select provider type</option>
for _, provider := range providers {
<option value={provider}>{providerDisplayName(provider)}</option>
}
</select>
</div>
</div>
<div class="sm:col-span-6" x-show={fmt.Sprintf("%sProvider === 'sftp'", formType)}>
if isSource {
@source.SFTPSourceForm()
} else {
@destination.SFTPDestinationForm()
}
</div>
<div class="sm:col-span-6" x-show={fmt.Sprintf("%sProvider === 'local'", formType)}>
if isSource {
@source.LocalSourceForm()
} else {
@destination.LocalDestinationForm()
}
</div>
<div class="sm:col-span-6" x-show={fmt.Sprintf("%sProvider === 's3'", formType)}>
if isSource {
@source.S3SourceForm()
} else {
@destination.S3DestinationForm()
}
</div>
<div class="sm:col-span-6" x-show={fmt.Sprintf("%sProvider === 'ftp'", formType)}>
if isSource {
@source.FTPSourceForm()
} else {
@destination.FTPDestinationForm()
}
</div>
<div class="sm:col-span-12" x-show={fmt.Sprintf("%sProvider", formType)}>
<div class="mt-6">
<label for="show_advanced" class="flex items-center cursor-pointer">
<div class="relative">
<input id="show_advanced" type="checkbox" x-model="showAdvanced" class="sr-only" />
<div class="block bg-gray-200 w-14 h-8 rounded-full"></div>
<div class="dot absolute left-1 top-1 bg-white w-6 h-6 rounded-full transition"
:class="showAdvanced ? 'transform translate-x-6 bg-primary-500' : ''"></div>
</div>
<div class="ml-3 text-gray-700 font-medium">
Show Advanced Options
</div>
</label>
</div>
<div x-show="showAdvanced">
<div class="grid grid-cols-1 sm:grid-cols-12 gap-y-6 gap-x-4 mt-6">
@common.FilePatternFields()
if isSource {
@common.ArchiveOptions()
}
</div>
</div>
</div>
</div>
</form>
}
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']);
}
};
}
+271 -4
View File
@@ -196,7 +196,7 @@ templ formFields(data StorageProviderFormData) {
</div>
<!-- Port -->
<div class="mb-4">
<div id="port-field" class="mb-4">
<label for="port" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Port</label>
<input type="number" id="port" name="port" value={ fmt.Sprint(data.Provider.Port) } class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="22" />
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Leave empty for default (SFTP: 22, FTP: 21, SMB: 445, WebDAV: 80/443)</p>
@@ -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
+1 -1
View File
@@ -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
}
@@ -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")
}
+23 -4
View File
@@ -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)
@@ -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")