mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-09-08 15:41:20 +02:00
feat: Enhance configuration forms and validation for storage providers
- Added hidden input fields to ensure critical values are submitted with the form for S3 and other storage providers. - Implemented JavaScript functionality to auto-populate hidden fields based on user input for S3-compatible providers. - Updated validation logic for WebDAV and SMB providers to ensure required fields are checked. - Enhanced the configuration handler to explicitly set fields for various storage provider types, including S3, Wasabi, MinIO, and B2. - Improved error handling and logging for form submissions and provider credential management.
This commit is contained in:
@@ -457,6 +457,29 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
|||||||
|
|
||||||
// Initialize command requirements
|
// Initialize command requirements
|
||||||
updateCommandRequirements();
|
updateCommandRequirements();
|
||||||
|
|
||||||
|
// Initialize hidden input fields with their values to ensure they're included in form submission
|
||||||
|
document.getElementById('hidden_name').value = name;
|
||||||
|
document.getElementById('hidden_source_path').value = sourcePath;
|
||||||
|
document.getElementById('hidden_destination_path').value = destinationPath;
|
||||||
|
|
||||||
|
// Initialize S3 source fields if applicable
|
||||||
|
if (sourceType === 's3' || sourceType === 'b2' || sourceType === 'wasabi' || sourceType === 'minio') {
|
||||||
|
document.getElementById('hidden_source_access_key').value = sourceAccessKey;
|
||||||
|
document.getElementById('hidden_source_secret_key').value = sourceSecretKey;
|
||||||
|
document.getElementById('hidden_source_endpoint').value = sourceEndpoint;
|
||||||
|
document.getElementById('hidden_source_bucket').value = sourceBucket;
|
||||||
|
document.getElementById('hidden_source_region').value = sourceRegion;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize S3 destination fields if applicable
|
||||||
|
if (destinationType === 's3' || destinationType === 'b2' || destinationType === 'wasabi' || destinationType === 'minio') {
|
||||||
|
document.getElementById('hidden_dest_access_key').value = destAccessKey;
|
||||||
|
document.getElementById('hidden_dest_secret_key').value = destSecretKey;
|
||||||
|
document.getElementById('hidden_dest_endpoint').value = destEndpoint;
|
||||||
|
document.getElementById('hidden_dest_bucket').value = destBucket;
|
||||||
|
document.getElementById('hidden_dest_region').value = destRegion;
|
||||||
|
}
|
||||||
})"
|
})"
|
||||||
x-effect="if (sourceType === 'sftp' && (sourcePort === 0 || sourcePort === 21 || sourcePort === 23)) {
|
x-effect="if (sourceType === 'sftp' && (sourcePort === 0 || sourcePort === 21 || sourcePort === 23)) {
|
||||||
sourcePort = 22;
|
sourcePort = 22;
|
||||||
@@ -478,8 +501,50 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
|||||||
console.log('Updating destination port to 23 for Hetzner');
|
console.log('Updating destination port to 23 for Hetzner');
|
||||||
}"
|
}"
|
||||||
@formvalidation
|
@formvalidation
|
||||||
|
@submit="
|
||||||
|
// Basic fields
|
||||||
|
document.getElementById('hidden_name').value = name;
|
||||||
|
document.getElementById('hidden_source_path').value = sourcePath;
|
||||||
|
document.getElementById('hidden_destination_path').value = destinationPath;
|
||||||
|
|
||||||
|
// S3 source fields
|
||||||
|
if (sourceType === 's3' || sourceType === 'b2' || sourceType === 'wasabi' || sourceType === 'minio') {
|
||||||
|
document.getElementById('hidden_source_access_key').value = sourceAccessKey;
|
||||||
|
document.getElementById('hidden_source_secret_key').value = sourceSecretKey;
|
||||||
|
document.getElementById('hidden_source_endpoint').value = sourceEndpoint;
|
||||||
|
document.getElementById('hidden_source_bucket').value = sourceBucket;
|
||||||
|
document.getElementById('hidden_source_region').value = sourceRegion;
|
||||||
|
}
|
||||||
|
|
||||||
|
// S3 destination fields
|
||||||
|
if (destinationType === 's3' || destinationType === 'b2' || destinationType === 'wasabi' || destinationType === 'minio') {
|
||||||
|
document.getElementById('hidden_dest_access_key').value = destAccessKey;
|
||||||
|
document.getElementById('hidden_dest_secret_key').value = destSecretKey;
|
||||||
|
document.getElementById('hidden_dest_endpoint').value = destEndpoint;
|
||||||
|
document.getElementById('hidden_dest_bucket').value = destBucket;
|
||||||
|
document.getElementById('hidden_dest_region').value = destRegion;
|
||||||
|
}
|
||||||
|
"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
<!-- Hidden fields to ensure values are submitted with the form -->
|
||||||
|
<input type="hidden" id="hidden_name" name="name" />
|
||||||
|
<input type="hidden" id="hidden_source_path" name="source_path" />
|
||||||
|
<input type="hidden" id="hidden_destination_path" name="destination_path" />
|
||||||
|
|
||||||
|
<!-- Hidden fields for S3-compatible providers -->
|
||||||
|
<input type="hidden" id="hidden_source_access_key" name="source_access_key" />
|
||||||
|
<input type="hidden" id="hidden_source_secret_key" name="source_secret_key" />
|
||||||
|
<input type="hidden" id="hidden_source_endpoint" name="source_endpoint" />
|
||||||
|
<input type="hidden" id="hidden_source_bucket" name="source_bucket" />
|
||||||
|
<input type="hidden" id="hidden_source_region" name="source_region" />
|
||||||
|
|
||||||
|
<input type="hidden" id="hidden_dest_access_key" name="dest_access_key" />
|
||||||
|
<input type="hidden" id="hidden_dest_secret_key" name="dest_secret_key" />
|
||||||
|
<input type="hidden" id="hidden_dest_endpoint" name="dest_endpoint" />
|
||||||
|
<input type="hidden" id="hidden_dest_bucket" name="dest_bucket" />
|
||||||
|
<input type="hidden" id="hidden_dest_region" name="dest_region" />
|
||||||
|
|
||||||
<!-- Form Error Container -->
|
<!-- Form Error Container -->
|
||||||
<div id="form-errors" class="hidden p-4 mb-6 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-red-800/20 dark:text-red-400 border border-red-200 dark:border-red-900" role="alert">
|
<div id="form-errors" class="hidden p-4 mb-6 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-red-800/20 dark:text-red-400 border border-red-200 dark:border-red-900" role="alert">
|
||||||
<div class="flex items-center mb-2">
|
<div class="flex items-center mb-2">
|
||||||
@@ -510,6 +575,7 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
|||||||
id="name"
|
id="name"
|
||||||
name="name"
|
name="name"
|
||||||
x-model="name"
|
x-model="name"
|
||||||
|
@input="document.getElementById('hidden_name').value = name"
|
||||||
required
|
required
|
||||||
aria-required="true"
|
aria-required="true"
|
||||||
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"
|
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"
|
||||||
@@ -645,6 +711,7 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
|||||||
id="source_path"
|
id="source_path"
|
||||||
name="source_path"
|
name="source_path"
|
||||||
x-model="sourcePath"
|
x-model="sourcePath"
|
||||||
|
@input="document.getElementById('hidden_source_path').value = sourcePath"
|
||||||
@blur="checkPath(sourcePath, 'source')"
|
@blur="checkPath(sourcePath, 'source')"
|
||||||
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"
|
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"
|
||||||
placeholder="/path/to/source"
|
placeholder="/path/to/source"
|
||||||
@@ -775,6 +842,7 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
|||||||
id="destination_path"
|
id="destination_path"
|
||||||
name="destination_path"
|
name="destination_path"
|
||||||
x-model="destinationPath"
|
x-model="destinationPath"
|
||||||
|
@input="document.getElementById('hidden_destination_path').value = destinationPath"
|
||||||
@blur="checkPath(destinationPath, 'dest')"
|
@blur="checkPath(destinationPath, 'dest')"
|
||||||
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"
|
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"
|
||||||
placeholder="/path/to/destination"
|
placeholder="/path/to/destination"
|
||||||
|
|||||||
@@ -161,12 +161,12 @@ templ formFields(data StorageProviderFormData) {
|
|||||||
selected="selected"
|
selected="selected"
|
||||||
}
|
}
|
||||||
>OneDrive</option>
|
>OneDrive</option>
|
||||||
<option value="gdrive"
|
<option value="google_drive"
|
||||||
if data.Provider.Type == db.ProviderTypeGoogleDrive {
|
if data.Provider.Type == db.ProviderTypeGoogleDrive {
|
||||||
selected="selected"
|
selected="selected"
|
||||||
}
|
}
|
||||||
>Google Drive</option>
|
>Google Drive</option>
|
||||||
<option value="gphotos"
|
<option value="google_photo"
|
||||||
if data.Provider.Type == db.ProviderTypeGooglePhoto {
|
if data.Provider.Type == db.ProviderTypeGooglePhoto {
|
||||||
selected="selected"
|
selected="selected"
|
||||||
}
|
}
|
||||||
@@ -250,32 +250,60 @@ templ formFields(data StorageProviderFormData) {
|
|||||||
|
|
||||||
<!-- Endpoint -->
|
<!-- Endpoint -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="endpoint" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Endpoint <span class="text-red-500">*</span></label>
|
<label for="endpoint" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
<input type="text" id="endpoint" name="endpoint" value={ data.Provider.Endpoint } 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="e.g., s3.amazonaws.com, s3.us-west-1.wasabisys.com" />
|
Endpoint
|
||||||
|
<span id="endpoint-required" class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" id="endpoint" name="endpoint" value={ data.Provider.Endpoint }
|
||||||
|
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="e.g., s3.amazonaws.com, s3.us-west-1.wasabisys.com" />
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Custom endpoint URL (only needed for non-standard regions or non-AWS S3-compatible services). Optional for B2.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Region -->
|
<!-- Region -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="region" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Region <span class="text-red-500">*</span></label>
|
<label for="region" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
<input type="text" id="region" name="region" value={ data.Provider.Region } 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="e.g., us-east-1, eu-central-1" />
|
Region
|
||||||
|
<span id="region-required" class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" id="region" name="region" value={ data.Provider.Region }
|
||||||
|
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="e.g., us-east-1, eu-central-1" />
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
AWS Region where your S3 bucket is located (e.g., us-east-1, eu-west-1). Optional for B2 and Wasabi.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bucket -->
|
<!-- Bucket -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="bucket" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Bucket <span class="text-red-500">*</span></label>
|
<label for="bucket" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Bucket <span class="text-red-500">*</span></label>
|
||||||
<input type="text" id="bucket" name="bucket" value={ data.Provider.Bucket } 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="Your bucket name" />
|
<input type="text" id="bucket" name="bucket" value={ data.Provider.Bucket }
|
||||||
|
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="Your bucket name" />
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Name of your S3 bucket (case-sensitive)
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Access Key -->
|
<!-- Access Key -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="accessKey" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Access Key <span class="text-red-500">*</span></label>
|
<label for="accessKey" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Access Key <span class="text-red-500">*</span></label>
|
||||||
<input type="text" id="accessKey" name="accessKey" value={ data.Provider.AccessKey } 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="Your access key/key ID" />
|
<input type="text" id="accessKey" name="accessKey" value={ data.Provider.AccessKey }
|
||||||
|
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="Your access key/key ID" />
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Your AWS Access Key ID
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Secret Key -->
|
<!-- Secret Key -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="secretKey" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Secret Key <span class="text-red-500">*</span></label>
|
<label for="secretKey" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Secret Key <span class="text-red-500">*</span></label>
|
||||||
<input type="password" id="secretKey" name="secretKey" 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="Your secret access key" />
|
<input type="password" id="secretKey" name="secretKey"
|
||||||
|
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="Your secret access key" />
|
||||||
if data.IsEdit {
|
if data.IsEdit {
|
||||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Leave empty to keep the current secret key</p>
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Leave empty to keep the current secret key</p>
|
||||||
}
|
}
|
||||||
@@ -354,6 +382,17 @@ templ formFields(data StorageProviderFormData) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Hidden fields for S3 form data to ensure it gets submitted correctly -->
|
||||||
|
<input type="hidden" id="hidden_endpoint" name="endpoint" value={ data.Provider.Endpoint } />
|
||||||
|
<input type="hidden" id="hidden_region" name="region" value={ data.Provider.Region } />
|
||||||
|
<input type="hidden" id="hidden_bucket" name="bucket" value={ data.Provider.Bucket } />
|
||||||
|
<input type="hidden" id="hidden_accessKey" name="accessKey" value={ data.Provider.AccessKey } />
|
||||||
|
<input type="hidden" id="hidden_secretKey" name="secretKey" />
|
||||||
|
|
||||||
|
<!-- Hidden fields for Google Drive and Google Photos -->
|
||||||
|
<input type="hidden" id="hidden_clientID" name="clientID" value={ data.Provider.ClientID } />
|
||||||
|
<input type="hidden" id="hidden_clientSecret" name="clientSecret" />
|
||||||
}
|
}
|
||||||
|
|
||||||
// JavaScript helper for toggling provider fields
|
// JavaScript helper for toggling provider fields
|
||||||
@@ -369,6 +408,133 @@ templ providerFormScript() {
|
|||||||
// Mark the field as user-modified
|
// Mark the field as user-modified
|
||||||
this.dataset.userModified = 'true';
|
this.dataset.userModified = 'true';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add event listeners for S3 fields
|
||||||
|
document.getElementById('endpoint').addEventListener('input', function() {
|
||||||
|
document.getElementById('hidden_endpoint').value = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('region').addEventListener('input', function() {
|
||||||
|
document.getElementById('hidden_region').value = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('bucket').addEventListener('input', function() {
|
||||||
|
document.getElementById('hidden_bucket').value = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('accessKey').addEventListener('input', function() {
|
||||||
|
document.getElementById('hidden_accessKey').value = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('secretKey').addEventListener('input', function() {
|
||||||
|
document.getElementById('hidden_secretKey').value = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add event listeners for Google Drive/Photos fields
|
||||||
|
document.getElementById('clientID').addEventListener('input', function() {
|
||||||
|
document.getElementById('hidden_clientID').value = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('clientSecret').addEventListener('input', function() {
|
||||||
|
document.getElementById('hidden_clientSecret').value = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add form submit listener to ensure all hidden fields are populated
|
||||||
|
const forms = document.querySelectorAll('form');
|
||||||
|
forms.forEach(form => {
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
// Find active provider type
|
||||||
|
const providerType = document.getElementById('type').value;
|
||||||
|
|
||||||
|
// If S3-compatible, update hidden fields
|
||||||
|
if (['s3', 'wasabi', 'minio', 'b2'].includes(providerType)) {
|
||||||
|
document.getElementById('hidden_endpoint').value = document.getElementById('endpoint').value;
|
||||||
|
document.getElementById('hidden_region').value = document.getElementById('region').value;
|
||||||
|
document.getElementById('hidden_bucket').value = document.getElementById('bucket').value;
|
||||||
|
document.getElementById('hidden_accessKey').value = document.getElementById('accessKey').value;
|
||||||
|
|
||||||
|
// Make sure secretKey is always copied to the hidden field
|
||||||
|
// This is especially important for B2 which uses this as Application Key
|
||||||
|
const secretKeyValue = document.getElementById('secretKey').value;
|
||||||
|
document.getElementById('hidden_secretKey').value = secretKeyValue;
|
||||||
|
|
||||||
|
// For validation - ensure we have appropriate fields for each provider type
|
||||||
|
if (providerType === 'b2') {
|
||||||
|
// B2 does not require region or endpoint
|
||||||
|
if (!document.getElementById('hidden_bucket').value) {
|
||||||
|
alert('Bucket name is required');
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!document.getElementById('hidden_accessKey').value) {
|
||||||
|
alert('Account ID is required');
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!secretKeyValue && !document.getElementById('hidden_secretKey').value) {
|
||||||
|
alert('Application Key is required');
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (providerType === 'wasabi') {
|
||||||
|
// Wasabi does not require region
|
||||||
|
if (!document.getElementById('hidden_endpoint').value) {
|
||||||
|
alert('Endpoint is required for Wasabi');
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!document.getElementById('hidden_bucket').value) {
|
||||||
|
alert('Bucket name is required');
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!document.getElementById('hidden_accessKey').value) {
|
||||||
|
alert('Access key is required');
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!secretKeyValue && !document.getElementById('hidden_secretKey').value) {
|
||||||
|
alert('Secret key is required');
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Submitting S3-compatible form with:', {
|
||||||
|
provider: providerType,
|
||||||
|
endpoint: document.getElementById('hidden_endpoint').value,
|
||||||
|
region: document.getElementById('hidden_region').value,
|
||||||
|
bucket: document.getElementById('hidden_bucket').value,
|
||||||
|
accessKey: document.getElementById('hidden_accessKey').value,
|
||||||
|
secretKey: document.getElementById('hidden_secretKey').value ? '[PRESENT]' : '[EMPTY]',
|
||||||
|
secretKeyLength: document.getElementById('hidden_secretKey').value.length
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If Google Drive or Google Photos, update hidden fields
|
||||||
|
if (['google_drive', 'google_photo', 'onedrive'].includes(providerType)) {
|
||||||
|
document.getElementById('hidden_clientID').value = document.getElementById('clientID').value;
|
||||||
|
|
||||||
|
// Make sure clientSecret is always copied to the hidden field
|
||||||
|
const clientSecretValue = document.getElementById('clientSecret').value;
|
||||||
|
document.getElementById('hidden_clientSecret').value = clientSecretValue;
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!document.getElementById('hidden_clientID').value) {
|
||||||
|
alert('Client ID is required');
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Submitting cloud storage form with:', {
|
||||||
|
provider: providerType,
|
||||||
|
clientID: document.getElementById('hidden_clientID').value,
|
||||||
|
clientSecret: document.getElementById('hidden_clientSecret').value ? '[PRESENT]' : '[EMPTY]',
|
||||||
|
clientSecretLength: document.getElementById('hidden_clientSecret').value.length
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function toggleProviderFields() {
|
function toggleProviderFields() {
|
||||||
@@ -437,20 +603,48 @@ templ providerFormScript() {
|
|||||||
// S3-compatible storage
|
// S3-compatible storage
|
||||||
if (['s3', 'wasabi', 'minio', 'b2'].includes(provider)) {
|
if (['s3', 'wasabi', 'minio', 'b2'].includes(provider)) {
|
||||||
document.getElementById('s3-fields').classList.remove('hidden');
|
document.getElementById('s3-fields').classList.remove('hidden');
|
||||||
|
|
||||||
|
// Set region and endpoint requirements based on provider type
|
||||||
|
const regionRequired = document.getElementById('region-required');
|
||||||
|
const endpointRequired = document.getElementById('endpoint-required');
|
||||||
|
const regionInput = document.getElementById('region');
|
||||||
|
const endpointInput = document.getElementById('endpoint');
|
||||||
|
|
||||||
|
// For B2: endpoint and region are optional
|
||||||
|
if (provider === 'b2') {
|
||||||
|
regionRequired.style.display = 'none';
|
||||||
|
endpointRequired.style.display = 'none';
|
||||||
|
regionInput.removeAttribute('required');
|
||||||
|
endpointInput.removeAttribute('required');
|
||||||
|
}
|
||||||
|
// For Wasabi: region is optional
|
||||||
|
else if (provider === 'wasabi') {
|
||||||
|
regionRequired.style.display = 'none';
|
||||||
|
endpointRequired.style.display = 'inline';
|
||||||
|
regionInput.removeAttribute('required');
|
||||||
|
endpointInput.setAttribute('required', 'required');
|
||||||
|
}
|
||||||
|
// For S3 and MinIO: both are required
|
||||||
|
else {
|
||||||
|
regionRequired.style.display = 'inline';
|
||||||
|
endpointRequired.style.display = 'inline';
|
||||||
|
regionInput.setAttribute('required', 'required');
|
||||||
|
endpointInput.setAttribute('required', 'required');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Google services
|
// Google services
|
||||||
if (['gdrive', 'gphotos'].includes(provider)) {
|
if (['google_drive', 'google_photo'].includes(provider)) {
|
||||||
document.getElementById('cloud-fields').classList.remove('hidden');
|
document.getElementById('cloud-fields').classList.remove('hidden');
|
||||||
|
|
||||||
// Google Drive specific fields
|
// Google Drive specific fields
|
||||||
if (provider === 'gdrive') {
|
if (provider === 'google_drive') {
|
||||||
document.getElementById('drive-id-field').classList.remove('hidden');
|
document.getElementById('drive-id-field').classList.remove('hidden');
|
||||||
document.getElementById('team-drive-field').classList.remove('hidden');
|
document.getElementById('team-drive-field').classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Google Photos specific fields
|
// Google Photos specific fields
|
||||||
if (provider === 'gphotos') {
|
if (provider === 'google_photo') {
|
||||||
document.getElementById('readonly-field').classList.remove('hidden');
|
document.getElementById('readonly-field').classList.remove('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,9 +169,7 @@ func (sp *StorageProvider) SetAuthenticated(value bool) {
|
|||||||
func (sp *StorageProvider) IsOAuthProvider() bool {
|
func (sp *StorageProvider) IsOAuthProvider() bool {
|
||||||
return sp.Type == ProviderTypeOneDrive ||
|
return sp.Type == ProviderTypeOneDrive ||
|
||||||
sp.Type == ProviderTypeGoogleDrive ||
|
sp.Type == ProviderTypeGoogleDrive ||
|
||||||
sp.Type == ProviderTypeGooglePhoto ||
|
sp.Type == ProviderTypeGooglePhoto
|
||||||
sp.Type == ProviderTypeWebDAV ||
|
|
||||||
sp.Type == ProviderTypeNextcloud
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequiresEncryption returns true if the provider has sensitive fields that need encryption
|
// RequiresEncryption returns true if the provider has sensitive fields that need encryption
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ func (sp *StorageProvider) Validate() error {
|
|||||||
return sp.validateS3()
|
return sp.validateS3()
|
||||||
case ProviderTypeFTP:
|
case ProviderTypeFTP:
|
||||||
return sp.validateFTP()
|
return sp.validateFTP()
|
||||||
|
case ProviderTypeWebDAV, ProviderTypeNextcloud:
|
||||||
|
return sp.validateWebDAV()
|
||||||
case ProviderTypeSMB:
|
case ProviderTypeSMB:
|
||||||
return sp.validateSMB()
|
return sp.validateSMB()
|
||||||
case ProviderTypeOneDrive:
|
case ProviderTypeOneDrive:
|
||||||
@@ -67,6 +69,24 @@ func (sp *StorageProvider) validateSFTP() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validateWebDAV validates WebDAV-specific fields
|
||||||
|
func (sp *StorageProvider) validateWebDAV() error {
|
||||||
|
if strings.TrimSpace(sp.Host) == "" {
|
||||||
|
return errors.New("host is required for WebDAV provider")
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(sp.Username) == "" {
|
||||||
|
return errors.New("username is required for WebDAV provider")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Either password or encrypted password must be provided
|
||||||
|
if strings.TrimSpace(sp.Password) == "" && strings.TrimSpace(sp.EncryptedPassword) == "" {
|
||||||
|
return errors.New("password is required for WebDAV provider")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// validateS3 validates S3-specific fields
|
// validateS3 validates S3-specific fields
|
||||||
func (sp *StorageProvider) validateS3() error {
|
func (sp *StorageProvider) validateS3() error {
|
||||||
// For S3, either AccessKey or Username is used
|
// For S3, either AccessKey or Username is used
|
||||||
@@ -115,10 +135,6 @@ func (sp *StorageProvider) validateSMB() error {
|
|||||||
return errors.New("host is required for SMB provider")
|
return errors.New("host is required for SMB provider")
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.TrimSpace(sp.Share) == "" {
|
|
||||||
return errors.New("share is required for SMB provider")
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.TrimSpace(sp.Username) == "" {
|
if strings.TrimSpace(sp.Username) == "" {
|
||||||
return errors.New("username is required for SMB provider")
|
return errors.New("username is required for SMB provider")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,6 +191,56 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
|
|||||||
if output, err := cmd.CombinedOutput(); err != nil {
|
if output, err := cmd.CombinedOutput(); err != nil {
|
||||||
return fmt.Errorf("failed to create source config (sftp): %v\nOutput: %s", err, output)
|
return fmt.Errorf("failed to create source config (sftp): %v\nOutput: %s", err, output)
|
||||||
}
|
}
|
||||||
|
case "smb":
|
||||||
|
args := []string{
|
||||||
|
"config", "create", sourceName, "smb",
|
||||||
|
"host", getStringValue(sourceCredentials, "host", config.SourceHost),
|
||||||
|
"user", getStringValue(sourceCredentials, "username", config.SourceUser),
|
||||||
|
"--non-interactive",
|
||||||
|
"--config", configPath,
|
||||||
|
"--log-level", "ERROR",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get domain if provided
|
||||||
|
domain := getStringValue(sourceCredentials, "domain", config.SourceDomain)
|
||||||
|
if domain != "" {
|
||||||
|
args = append(args, "domain", domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get port if specified (default is 445)
|
||||||
|
port := getIntValue(sourceCredentials, "port", config.SourcePort)
|
||||||
|
if port > 0 && port != 445 {
|
||||||
|
args = append(args, "port", fmt.Sprintf("%d", port))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get share if provided
|
||||||
|
share := getStringValue(sourceCredentials, "share", config.SourceShare)
|
||||||
|
if share != "" {
|
||||||
|
args = append(args, "share", share)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
return fmt.Errorf("failed to create source config (smb): %v\nOutput: %s", err, output)
|
||||||
|
}
|
||||||
case "s3":
|
case "s3":
|
||||||
args := []string{
|
args := []string{
|
||||||
"config", "create", sourceName, "s3",
|
"config", "create", sourceName, "s3",
|
||||||
@@ -269,12 +319,36 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
|
|||||||
case "b2":
|
case "b2":
|
||||||
args := []string{
|
args := []string{
|
||||||
"config", "create", sourceName, "b2",
|
"config", "create", sourceName, "b2",
|
||||||
"account", config.SourceAccessKey, // B2 Account ID
|
|
||||||
"key", config.SourceSecretKey, // B2 Application Key
|
|
||||||
"--non-interactive",
|
"--non-interactive",
|
||||||
"--config", configPath,
|
"--config", configPath,
|
||||||
"--log-level", "ERROR",
|
"--log-level", "ERROR",
|
||||||
}
|
}
|
||||||
|
// Handle account ID (access key)
|
||||||
|
accountID := getStringValue(sourceCredentials, "access_key", config.SourceAccessKey)
|
||||||
|
if accountID != "" {
|
||||||
|
args = append(args, "account", accountID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle secret key with proper decryption if from provider
|
||||||
|
secretKey := ""
|
||||||
|
if config.SourceSecretKey != "" {
|
||||||
|
// Direct input from form (transient)
|
||||||
|
secretKey = config.SourceSecretKey
|
||||||
|
} else if encryptedSecret, ok := sourceCredentials["encrypted_secret_key"].(string); ok && encryptedSecret != "" {
|
||||||
|
// Provider reference with encrypted secret
|
||||||
|
decryptedSecret, err := db.DecryptCredential(encryptedSecret)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to decrypt source secret key: %v", err)
|
||||||
|
}
|
||||||
|
secretKey = decryptedSecret
|
||||||
|
} else if secretVal, ok := sourceCredentials["secret_key"].(string); ok && secretVal != "" {
|
||||||
|
// Backward compatibility
|
||||||
|
secretKey = secretVal
|
||||||
|
}
|
||||||
|
|
||||||
|
if secretKey != "" {
|
||||||
|
args = append(args, "key", secretKey)
|
||||||
|
}
|
||||||
if config.SourceEndpoint != "" {
|
if config.SourceEndpoint != "" {
|
||||||
args = append(args, "endpoint", config.SourceEndpoint)
|
args = append(args, "endpoint", config.SourceEndpoint)
|
||||||
}
|
}
|
||||||
@@ -288,12 +362,32 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
|
|||||||
"provider", "Minio",
|
"provider", "Minio",
|
||||||
"env_auth", "false",
|
"env_auth", "false",
|
||||||
"access_key_id", getStringValue(sourceCredentials, "access_key", config.SourceAccessKey),
|
"access_key_id", getStringValue(sourceCredentials, "access_key", config.SourceAccessKey),
|
||||||
"secret_access_key", getStringOrDefault(sourceCredentials, "secret_key", config.SourceSecretKey),
|
|
||||||
"endpoint", getStringValue(sourceCredentials, "endpoint", config.SourceEndpoint),
|
"endpoint", getStringValue(sourceCredentials, "endpoint", config.SourceEndpoint),
|
||||||
"--non-interactive",
|
"--non-interactive",
|
||||||
"--config", configPath,
|
"--config", configPath,
|
||||||
"--log-level", "ERROR",
|
"--log-level", "ERROR",
|
||||||
}
|
}
|
||||||
|
// Handle secret key with proper decryption if from provider
|
||||||
|
secretKey := ""
|
||||||
|
if config.SourceSecretKey != "" {
|
||||||
|
// Direct input from form (transient)
|
||||||
|
secretKey = config.SourceSecretKey
|
||||||
|
} else if encryptedSecret, ok := sourceCredentials["encrypted_secret_key"].(string); ok && encryptedSecret != "" {
|
||||||
|
// Provider reference with encrypted secret
|
||||||
|
decryptedSecret, err := db.DecryptCredential(encryptedSecret)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to decrypt source secret key: %v", err)
|
||||||
|
}
|
||||||
|
secretKey = decryptedSecret
|
||||||
|
} else if secretVal, ok := sourceCredentials["secret_key"].(string); ok && secretVal != "" {
|
||||||
|
// Backward compatibility
|
||||||
|
secretKey = secretVal
|
||||||
|
}
|
||||||
|
|
||||||
|
if secretKey != "" {
|
||||||
|
args = append(args, "secret_access_key", secretKey)
|
||||||
|
}
|
||||||
|
|
||||||
// Add region if specified
|
// Add region if specified
|
||||||
if getStringValue(sourceCredentials, "region", config.SourceRegion) != "" {
|
if getStringValue(sourceCredentials, "region", config.SourceRegion) != "" {
|
||||||
args = append(args, "region", getStringValue(sourceCredentials, "region", config.SourceRegion))
|
args = append(args, "region", getStringValue(sourceCredentials, "region", config.SourceRegion))
|
||||||
@@ -413,19 +507,89 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
|
|||||||
if output, err := cmd.CombinedOutput(); err != nil {
|
if output, err := cmd.CombinedOutput(); err != nil {
|
||||||
return fmt.Errorf("failed to create destination config (sftp): %v\nOutput: %s", err, output)
|
return fmt.Errorf("failed to create destination config (sftp): %v\nOutput: %s", err, output)
|
||||||
}
|
}
|
||||||
|
case "smb":
|
||||||
|
args := []string{
|
||||||
|
"config", "create", destName, "smb",
|
||||||
|
"host", getStringValue(destCredentials, "host", config.DestHost),
|
||||||
|
"user", getStringValue(destCredentials, "username", config.DestUser),
|
||||||
|
"--non-interactive",
|
||||||
|
"--config", configPath,
|
||||||
|
"--log-level", "ERROR",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get domain if provided
|
||||||
|
domain := getStringValue(destCredentials, "domain", config.DestDomain)
|
||||||
|
if domain != "" {
|
||||||
|
args = append(args, "domain", domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get port if specified (default is 445)
|
||||||
|
port := getIntValue(destCredentials, "port", config.DestPort)
|
||||||
|
if port > 0 && port != 445 {
|
||||||
|
args = append(args, "port", fmt.Sprintf("%d", port))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get share if provided
|
||||||
|
share := getStringValue(destCredentials, "share", config.DestShare)
|
||||||
|
if share != "" {
|
||||||
|
args = append(args, "share", share)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle password
|
||||||
|
password := ""
|
||||||
|
if config.DestPassword != "" {
|
||||||
|
password = config.DestPassword
|
||||||
|
} else if encryptedPwd, ok := destCredentials["encrypted_password"].(string); ok && encryptedPwd != "" {
|
||||||
|
decryptedPwd, err := db.DecryptCredential(encryptedPwd)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to decrypt destination password: %v", err)
|
||||||
|
}
|
||||||
|
password = decryptedPwd
|
||||||
|
} else if pwVal, ok := destCredentials["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 {
|
||||||
|
return fmt.Errorf("failed to create destination config (smb): %v\nOutput: %s", err, output)
|
||||||
|
}
|
||||||
case "s3":
|
case "s3":
|
||||||
args := []string{
|
args := []string{
|
||||||
"config", "create", destName, "s3",
|
"config", "create", destName, "s3",
|
||||||
"provider", "AWS", // Assuming AWS provider
|
"provider", "AWS", // Assuming AWS provider
|
||||||
"env_auth", "false",
|
"env_auth", "false",
|
||||||
"access_key_id", getStringValue(destCredentials, "access_key", config.DestAccessKey),
|
"access_key_id", getStringValue(destCredentials, "access_key", config.DestAccessKey),
|
||||||
"secret_access_key", getStringOrDefault(destCredentials, "secret_key", config.DestSecretKey),
|
|
||||||
"region", getStringValue(destCredentials, "region", config.DestRegion),
|
"region", getStringValue(destCredentials, "region", config.DestRegion),
|
||||||
"--non-interactive",
|
"--non-interactive",
|
||||||
"--config", configPath,
|
"--config", configPath,
|
||||||
"--log-level", "ERROR",
|
"--log-level", "ERROR",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle secret key with proper decryption if from provider
|
||||||
|
secretKey := ""
|
||||||
|
if config.DestSecretKey != "" {
|
||||||
|
// Direct input from form (transient)
|
||||||
|
secretKey = config.DestSecretKey
|
||||||
|
} else if encryptedSecret, ok := destCredentials["encrypted_secret_key"].(string); ok && encryptedSecret != "" {
|
||||||
|
// Provider reference with encrypted secret
|
||||||
|
decryptedSecret, err := db.DecryptCredential(encryptedSecret)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to decrypt destination secret key: %v", err)
|
||||||
|
}
|
||||||
|
secretKey = decryptedSecret
|
||||||
|
} else if secretVal, ok := destCredentials["secret_key"].(string); ok && secretVal != "" {
|
||||||
|
// Backward compatibility
|
||||||
|
secretKey = secretVal
|
||||||
|
}
|
||||||
|
|
||||||
|
if secretKey != "" {
|
||||||
|
args = append(args, "secret_access_key", secretKey)
|
||||||
|
}
|
||||||
|
|
||||||
endpoint := getStringValue(destCredentials, "endpoint", config.DestEndpoint)
|
endpoint := getStringValue(destCredentials, "endpoint", config.DestEndpoint)
|
||||||
if endpoint != "" {
|
if endpoint != "" {
|
||||||
args = append(args, "endpoint", endpoint)
|
args = append(args, "endpoint", endpoint)
|
||||||
@@ -441,13 +605,33 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
|
|||||||
"provider", "Wasabi",
|
"provider", "Wasabi",
|
||||||
"env_auth", "false",
|
"env_auth", "false",
|
||||||
"access_key_id", getStringValue(destCredentials, "access_key", config.DestAccessKey),
|
"access_key_id", getStringValue(destCredentials, "access_key", config.DestAccessKey),
|
||||||
"secret_access_key", getStringOrDefault(destCredentials, "secret_key", config.DestSecretKey),
|
|
||||||
"region", getStringValue(destCredentials, "region", config.DestRegion),
|
"region", getStringValue(destCredentials, "region", config.DestRegion),
|
||||||
"--non-interactive",
|
"--non-interactive",
|
||||||
"--config", configPath,
|
"--config", configPath,
|
||||||
"--log-level", "ERROR",
|
"--log-level", "ERROR",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle secret key with proper decryption if from provider
|
||||||
|
secretKey := ""
|
||||||
|
if config.DestSecretKey != "" {
|
||||||
|
// Direct input from form (transient)
|
||||||
|
secretKey = config.DestSecretKey
|
||||||
|
} else if encryptedSecret, ok := destCredentials["encrypted_secret_key"].(string); ok && encryptedSecret != "" {
|
||||||
|
// Provider reference with encrypted secret
|
||||||
|
decryptedSecret, err := db.DecryptCredential(encryptedSecret)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to decrypt destination secret key: %v", err)
|
||||||
|
}
|
||||||
|
secretKey = decryptedSecret
|
||||||
|
} else if secretVal, ok := destCredentials["secret_key"].(string); ok && secretVal != "" {
|
||||||
|
// Backward compatibility
|
||||||
|
secretKey = secretVal
|
||||||
|
}
|
||||||
|
|
||||||
|
if secretKey != "" {
|
||||||
|
args = append(args, "secret_access_key", secretKey)
|
||||||
|
}
|
||||||
|
|
||||||
endpoint := getStringValue(destCredentials, "endpoint", config.DestEndpoint)
|
endpoint := getStringValue(destCredentials, "endpoint", config.DestEndpoint)
|
||||||
if endpoint == "" {
|
if endpoint == "" {
|
||||||
endpoint = "s3.wasabisys.com"
|
endpoint = "s3.wasabisys.com"
|
||||||
@@ -472,6 +656,27 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
|
|||||||
args = append(args, "account", accountID)
|
args = append(args, "account", accountID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle secret key with proper decryption if from provider
|
||||||
|
secretKey := ""
|
||||||
|
if config.DestSecretKey != "" {
|
||||||
|
// Direct input from form (transient)
|
||||||
|
secretKey = config.DestSecretKey
|
||||||
|
} else if encryptedSecret, ok := destCredentials["encrypted_secret_key"].(string); ok && encryptedSecret != "" {
|
||||||
|
// Provider reference with encrypted secret
|
||||||
|
decryptedSecret, err := db.DecryptCredential(encryptedSecret)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to decrypt destination secret key: %v", err)
|
||||||
|
}
|
||||||
|
secretKey = decryptedSecret
|
||||||
|
} else if secretVal, ok := destCredentials["secret_key"].(string); ok && secretVal != "" {
|
||||||
|
// Backward compatibility
|
||||||
|
secretKey = secretVal
|
||||||
|
}
|
||||||
|
|
||||||
|
if secretKey != "" {
|
||||||
|
args = append(args, "key", secretKey)
|
||||||
|
}
|
||||||
|
|
||||||
// Handle application key (secret key) with proper decryption if from provider
|
// Handle application key (secret key) with proper decryption if from provider
|
||||||
appKey := ""
|
appKey := ""
|
||||||
if config.DestSecretKey != "" {
|
if config.DestSecretKey != "" {
|
||||||
@@ -508,12 +713,33 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
|
|||||||
"provider", "Minio",
|
"provider", "Minio",
|
||||||
"env_auth", "false",
|
"env_auth", "false",
|
||||||
"access_key_id", getStringValue(destCredentials, "access_key", config.DestAccessKey),
|
"access_key_id", getStringValue(destCredentials, "access_key", config.DestAccessKey),
|
||||||
"secret_access_key", getStringOrDefault(destCredentials, "secret_key", config.DestSecretKey),
|
|
||||||
"endpoint", getStringValue(destCredentials, "endpoint", config.DestEndpoint),
|
"endpoint", getStringValue(destCredentials, "endpoint", config.DestEndpoint),
|
||||||
"--non-interactive",
|
"--non-interactive",
|
||||||
"--config", configPath,
|
"--config", configPath,
|
||||||
"--log-level", "ERROR",
|
"--log-level", "ERROR",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle secret key with proper decryption if from provider
|
||||||
|
secretKey := ""
|
||||||
|
if config.DestSecretKey != "" {
|
||||||
|
// Direct input from form (transient)
|
||||||
|
secretKey = config.DestSecretKey
|
||||||
|
} else if encryptedSecret, ok := destCredentials["encrypted_secret_key"].(string); ok && encryptedSecret != "" {
|
||||||
|
// Provider reference with encrypted secret
|
||||||
|
decryptedSecret, err := db.DecryptCredential(encryptedSecret)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to decrypt destination secret key: %v", err)
|
||||||
|
}
|
||||||
|
secretKey = decryptedSecret
|
||||||
|
} else if secretVal, ok := destCredentials["secret_key"].(string); ok && secretVal != "" {
|
||||||
|
// Backward compatibility
|
||||||
|
secretKey = secretVal
|
||||||
|
}
|
||||||
|
|
||||||
|
if secretKey != "" {
|
||||||
|
args = append(args, "secret_access_key", secretKey)
|
||||||
|
}
|
||||||
|
|
||||||
// Add region if specified
|
// Add region if specified
|
||||||
if getStringValue(destCredentials, "region", config.DestRegion) != "" {
|
if getStringValue(destCredentials, "region", config.DestRegion) != "" {
|
||||||
args = append(args, "region", getStringValue(destCredentials, "region", config.DestRegion))
|
args = append(args, "region", getStringValue(destCredentials, "region", config.DestRegion))
|
||||||
|
|||||||
@@ -104,7 +104,15 @@ func createTempTransferConfig(provider *db.StorageProvider) *db.TransferConfig {
|
|||||||
config.SetSourcePassiveMode(passive)
|
config.SetSourcePassiveMode(passive)
|
||||||
}
|
}
|
||||||
|
|
||||||
case db.ProviderTypeS3:
|
case db.ProviderTypeS3, db.ProviderTypeWasabi, db.ProviderTypeMinio:
|
||||||
|
config.SourceAccessKey = provider.AccessKey
|
||||||
|
config.SourceSecretKey = provider.SecretKey
|
||||||
|
config.SourceBucket = provider.Bucket
|
||||||
|
config.SourceRegion = provider.Region
|
||||||
|
config.SourceEndpoint = provider.Endpoint
|
||||||
|
|
||||||
|
case db.ProviderTypeB2:
|
||||||
|
// B2 uses AccessKey as account and SecretKey as application key
|
||||||
config.SourceAccessKey = provider.AccessKey
|
config.SourceAccessKey = provider.AccessKey
|
||||||
config.SourceSecretKey = provider.SecretKey
|
config.SourceSecretKey = provider.SecretKey
|
||||||
config.SourceBucket = provider.Bucket
|
config.SourceBucket = provider.Bucket
|
||||||
@@ -162,13 +170,18 @@ func (s *ConnectorService) decryptProviderCredentials(provider *db.StorageProvid
|
|||||||
provider.Password = password
|
provider.Password = password
|
||||||
}
|
}
|
||||||
|
|
||||||
case db.ProviderTypeS3:
|
case db.ProviderTypeS3, db.ProviderTypeWasabi, db.ProviderTypeMinio, db.ProviderTypeB2:
|
||||||
if provider.EncryptedSecretKey != "" {
|
if provider.EncryptedSecretKey != "" {
|
||||||
secretKey, err := s.credentialEncryptor.Decrypt(provider.EncryptedSecretKey)
|
secretKey, err := s.credentialEncryptor.Decrypt(provider.EncryptedSecretKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to decrypt secret key: %w", err)
|
return fmt.Errorf("failed to decrypt secret key: %w", err)
|
||||||
}
|
}
|
||||||
provider.SecretKey = secretKey
|
provider.SecretKey = secretKey
|
||||||
|
log.Printf("DEBUG: Decrypted secret key for %s provider (ID: %d, Type: %s) with length: %d",
|
||||||
|
provider.Name, provider.ID, provider.Type, len(provider.SecretKey))
|
||||||
|
} else {
|
||||||
|
log.Printf("WARNING: No encrypted secret key found for %s provider (ID: %d, Type: %s)",
|
||||||
|
provider.Name, provider.ID, provider.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
case db.ProviderTypeOneDrive, db.ProviderTypeGoogleDrive, db.ProviderTypeGooglePhoto:
|
case db.ProviderTypeOneDrive, db.ProviderTypeGoogleDrive, db.ProviderTypeGooglePhoto:
|
||||||
|
|||||||
@@ -147,6 +147,112 @@ func (h *Handlers) HandleCreateConfig(c *gin.Context) {
|
|||||||
userID := c.GetUint("userID")
|
userID := c.GetUint("userID")
|
||||||
config.CreatedBy = userID
|
config.CreatedBy = userID
|
||||||
|
|
||||||
|
// Explicitly set critical fields
|
||||||
|
config.Name = c.PostForm("name")
|
||||||
|
config.SourcePath = c.PostForm("source_path")
|
||||||
|
config.DestinationPath = c.PostForm("destination_path")
|
||||||
|
config.SourceType = c.PostForm("source_type")
|
||||||
|
config.DestinationType = c.PostForm("destination_type")
|
||||||
|
|
||||||
|
// Debug logs
|
||||||
|
log.Printf("DEBUG: Config data from form - Name: '%s', SourcePath: '%s', DestPath: '%s', SourceType: '%s', DestType: '%s'",
|
||||||
|
config.Name, config.SourcePath, config.DestinationPath, config.SourceType, config.DestinationType)
|
||||||
|
|
||||||
|
// Additional fields we need to explicitly set
|
||||||
|
if config.SourceType == "sftp" || config.SourceType == "ftp" || config.SourceType == "hetzner" {
|
||||||
|
config.SourceHost = c.PostForm("source_host")
|
||||||
|
portStr := c.PostForm("source_port")
|
||||||
|
if portStr != "" {
|
||||||
|
port, err := strconv.Atoi(portStr)
|
||||||
|
if err == nil {
|
||||||
|
config.SourcePort = port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
config.SourceUser = c.PostForm("source_user")
|
||||||
|
config.SourcePassword = c.PostForm("source_password")
|
||||||
|
config.SourceKeyFile = c.PostForm("source_key_file")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.DestinationType == "sftp" || config.DestinationType == "ftp" || config.DestinationType == "hetzner" {
|
||||||
|
config.DestHost = c.PostForm("dest_host")
|
||||||
|
portStr := c.PostForm("dest_port")
|
||||||
|
if portStr != "" {
|
||||||
|
port, err := strconv.Atoi(portStr)
|
||||||
|
if err == nil {
|
||||||
|
config.DestPort = port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
config.DestUser = c.PostForm("dest_user")
|
||||||
|
config.DestPassword = c.PostForm("dest_password")
|
||||||
|
config.DestKeyFile = c.PostForm("dest_key_file")
|
||||||
|
}
|
||||||
|
|
||||||
|
// S3 and similar providers
|
||||||
|
if config.SourceType == "s3" || config.SourceType == "wasabi" || config.SourceType == "minio" || config.SourceType == "b2" {
|
||||||
|
config.SourceBucket = c.PostForm("source_bucket")
|
||||||
|
config.SourceRegion = c.PostForm("source_region")
|
||||||
|
config.SourceAccessKey = c.PostForm("source_access_key")
|
||||||
|
config.SourceSecretKey = c.PostForm("source_secret_key")
|
||||||
|
config.SourceEndpoint = c.PostForm("source_endpoint")
|
||||||
|
|
||||||
|
// Debug logging for S3-compatible providers
|
||||||
|
log.Printf("DEBUG: S3-compatible source provider details - Type: %s, Bucket: %s, Region: %s, Endpoint: %s, Has Access Key: %t, Has Secret Key: %t",
|
||||||
|
config.SourceType,
|
||||||
|
config.SourceBucket,
|
||||||
|
config.SourceRegion,
|
||||||
|
config.SourceEndpoint,
|
||||||
|
config.SourceAccessKey != "",
|
||||||
|
config.SourceSecretKey != "")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.DestinationType == "s3" || config.DestinationType == "wasabi" || config.DestinationType == "minio" || config.DestinationType == "b2" {
|
||||||
|
config.DestBucket = c.PostForm("dest_bucket")
|
||||||
|
config.DestRegion = c.PostForm("dest_region")
|
||||||
|
config.DestAccessKey = c.PostForm("dest_access_key")
|
||||||
|
config.DestSecretKey = c.PostForm("dest_secret_key")
|
||||||
|
config.DestEndpoint = c.PostForm("dest_endpoint")
|
||||||
|
|
||||||
|
// Debug logging for S3-compatible providers
|
||||||
|
log.Printf("DEBUG: S3-compatible destination provider details - Type: %s, Bucket: %s, Region: %s, Endpoint: %s, Has Access Key: %t, Has Secret Key: %t",
|
||||||
|
config.DestinationType,
|
||||||
|
config.DestBucket,
|
||||||
|
config.DestRegion,
|
||||||
|
config.DestEndpoint,
|
||||||
|
config.DestAccessKey != "",
|
||||||
|
config.DestSecretKey != "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SMB specific fields
|
||||||
|
if config.SourceType == "smb" {
|
||||||
|
config.SourceShare = c.PostForm("source_share")
|
||||||
|
config.SourceDomain = c.PostForm("source_domain")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.DestinationType == "smb" {
|
||||||
|
config.DestShare = c.PostForm("dest_share")
|
||||||
|
config.DestDomain = c.PostForm("dest_domain")
|
||||||
|
}
|
||||||
|
|
||||||
|
// File pattern fields
|
||||||
|
config.FilePattern = c.PostForm("file_pattern")
|
||||||
|
config.OutputPattern = c.PostForm("output_pattern")
|
||||||
|
|
||||||
|
// Archive path
|
||||||
|
config.ArchivePath = c.PostForm("archive_path")
|
||||||
|
|
||||||
|
// Max concurrent transfers
|
||||||
|
maxConcurrentStr := c.PostForm("max_concurrent_transfers")
|
||||||
|
if maxConcurrentStr != "" {
|
||||||
|
maxConcurrent, err := strconv.Atoi(maxConcurrentStr)
|
||||||
|
if err == nil && maxConcurrent > 0 {
|
||||||
|
config.MaxConcurrentTransfers = maxConcurrent
|
||||||
|
} else {
|
||||||
|
config.MaxConcurrentTransfers = 4 // Default value
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
config.MaxConcurrentTransfers = 4 // Default value
|
||||||
|
}
|
||||||
|
|
||||||
// Process Boolean fields
|
// Process Boolean fields
|
||||||
skipProcessedVal := c.Request.FormValue("skip_processed_files")
|
skipProcessedVal := c.Request.FormValue("skip_processed_files")
|
||||||
skipProcessedValue := skipProcessedVal == "on" || skipProcessedVal == "true"
|
skipProcessedValue := skipProcessedVal == "on" || skipProcessedVal == "true"
|
||||||
|
|||||||
@@ -441,7 +441,12 @@ func (h *Handlers) parseProviderFromForm(c *gin.Context) (db.StorageProvider, er
|
|||||||
return provider, fmt.Errorf("host is required")
|
return provider, fmt.Errorf("host is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
case db.ProviderTypeS3:
|
case db.ProviderTypeWebDAV, db.ProviderTypeNextcloud:
|
||||||
|
// WebDAV specific fields
|
||||||
|
provider.Username = c.PostForm("username")
|
||||||
|
provider.Password = c.PostForm("password")
|
||||||
|
|
||||||
|
case db.ProviderTypeS3, db.ProviderTypeWasabi, db.ProviderTypeMinio, db.ProviderTypeB2:
|
||||||
// S3 specific fields
|
// S3 specific fields
|
||||||
provider.AccessKey = c.PostForm("accessKey")
|
provider.AccessKey = c.PostForm("accessKey")
|
||||||
provider.SecretKey = c.PostForm("secretKey")
|
provider.SecretKey = c.PostForm("secretKey")
|
||||||
@@ -458,8 +463,13 @@ func (h *Handlers) parseProviderFromForm(c *gin.Context) (db.StorageProvider, er
|
|||||||
return provider, fmt.Errorf("bucket is required")
|
return provider, fmt.Errorf("bucket is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
if provider.Region == "" {
|
// Validate region based on provider type
|
||||||
return provider, fmt.Errorf("region is required")
|
// B2 doesn't require region or endpoint
|
||||||
|
// Wasabi doesn't require region
|
||||||
|
if provider.Type == db.ProviderTypeS3 || provider.Type == db.ProviderTypeMinio {
|
||||||
|
if provider.Region == "" {
|
||||||
|
return provider, fmt.Errorf("region is required for %s", provider.Type)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case db.ProviderTypeOneDrive, db.ProviderTypeGoogleDrive, db.ProviderTypeGooglePhoto:
|
case db.ProviderTypeOneDrive, db.ProviderTypeGoogleDrive, db.ProviderTypeGooglePhoto:
|
||||||
|
|||||||
Reference in New Issue
Block a user