feat: Add support for Hetzner Storage Box integration

- Introduced new source and destination forms for Hetzner Storage Box, allowing users to configure their storage settings.
- Updated configuration generation logic to handle Hetzner, including necessary parameters such as host, port, username, and authentication type.
- Enhanced user interface with clear instructions and security notes for Hetzner, improving user experience and security awareness.
- Updated existing logic to accommodate Hetzner in rclone configuration generation for both source and destination types.
This commit is contained in:
StarFleetCPTN
2025-04-08 20:24:13 -07:00
parent 417841b3d2
commit de97ad6b50
6 changed files with 294 additions and 31 deletions
+23 -29
View File
@@ -398,6 +398,8 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
sourcePort = 22;
} else if (sourceType === 'ftp') {
sourcePort = 21;
} else if (sourceType === 'hetzner') {
sourcePort = 23;
}
}
@@ -406,25 +408,32 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
destPort = 22;
} else if (destinationType === 'ftp') {
destPort = 21;
} else if (destinationType === 'hetzner') {
destPort = 23;
}
}
// Initialize command requirements
updateCommandRequirements();
})"
x-effect="if (sourceType === 'sftp' && (sourcePort === 0 || sourcePort === 21)) {
x-effect="if (sourceType === 'sftp' && (sourcePort === 0 || sourcePort === 21 || sourcePort === 23)) {
sourcePort = 22;
console.log('Updating source port to 22 for SFTP');
} else if (sourceType === 'ftp' && (sourcePort === 0 || sourcePort === 22)) {
} else if (sourceType === 'ftp' && (sourcePort === 0 || sourcePort === 22 || sourcePort === 23)) {
sourcePort = 21;
console.log('Updating source port to 21 for FTP');
}"
x-effect="if (destinationType === 'sftp' && (destPort === 0 || destPort === 21)) {
} else if (sourceType === 'hetzner' && (sourcePort === 0 || sourcePort === 22 || sourcePort === 21)) {
sourcePort = 23;
console.log('Updating source port to 23 for Hetzner');
} else if (destinationType === 'sftp' && (destPort === 0 || destPort === 21 || destPort === 23)) {
destPort = 22;
console.log('Updating destination port to 22 for SFTP');
} else if (destinationType === 'ftp' && (destPort === 0 || destPort === 22)) {
} else if (destinationType === 'ftp' && (destPort === 0 || destPort === 22 || destPort === 23)) {
destPort = 21;
console.log('Updating destination port to 21 for FTP');
} else if (destinationType === 'hetzner' && (destPort === 0 || destPort === 22 || destPort === 21)) {
destPort = 23;
console.log('Updating destination port to 23 for Hetzner');
}"
>
@@ -545,6 +554,10 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
<template x-if="sourceType === 'gphotos'">
@source.GooglePhotosSourceForm()
</template>
<template x-if="sourceType === 'hetzner'">
@source.HetznerSourceForm()
</template>
</div>
<!-- File Pattern Section -->
@@ -628,6 +641,10 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
<template x-if="destinationType === 'gphotos'">
@destination.GooglePhotosDestinationForm()
</template>
<template x-if="destinationType === 'hetzner'">
@destination.HetznerDestinationForm()
</template>
</div>
<!-- Advanced Options Section -->
@@ -642,30 +659,7 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
@common.ArchiveOptions()
</div>
<div class="mb-6">
<label for="destination_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
<span>Destination Path</span>
<span x-show="destinationType === 's3' || destinationType === 'b2' || destinationType === 'wasabi' || destinationType === 'minio'" class="text-xs text-gray-500">(Optional)</span>
</label>
<div class="relative">
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-folder text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="destination_path" name="destination_path" x-model="destinationPath"
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="getDestinationPathPlaceholder()"
x-bind:required="requiresDestination && (destinationType !== 's3' && destinationType !== 'b2' && destinationType !== 'wasabi' && destinationType !== 'minio')" />
</div>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
<span x-show="destinationType === 'local'">Directory where files will be saved</span>
<span x-show="destinationType === 'sftp' || destinationType === 'ftp'">Remote path on the server</span>
<span x-show="destinationType === 's3' || destinationType === 'b2' || destinationType === 'wasabi' || destinationType === 'minio'">Path within bucket. Leave empty for bucket root.</span>
<span x-show="destinationType === 'smb'">Share directory path</span>
<span x-show="destinationType === 'nextcloud' || destinationType === 'webdav'">Path on remote server</span>
<span x-show="destinationType === 'gdrive'">Folder in Google Drive</span>
<span x-show="destinationType === 'gphotos'">Album name in Google Photos</span>
</p>
</div>
</div>
+2
View File
@@ -203,6 +203,7 @@ templ SourceSelection() {
<option value="webdav">WebDAV</option>
<option value="gdrive">Google Drive (BETA)</option>
<option value="gphotos">Google Photos (BETA)</option>
<option value="hetzner">Hetzner Storage Box</option>
</select>
</div>
</div>
@@ -229,6 +230,7 @@ templ DestinationSelection() {
<option value="webdav">WebDAV</option>
<option value="gdrive">Google Drive (BETA)</option>
<option value="gphotos">Google Photos (BETA)</option>
<option value="hetzner">Hetzner Storage Box</option>
</select>
</div>
</div>
@@ -0,0 +1,128 @@
package destination
templ HetznerDestinationForm() {
<div class="space-y-6 mt-4">
<div class="p-4 mb-4 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-blue-900/30 dark:text-blue-300" role="alert">
<div class="flex items-center">
<i class="fas fa-info-circle mr-2"></i>
<span>Configure your Hetzner Storage Box details below. You'll need your server details, username, and password.</span>
</div>
</div>
<div class="mb-6">
<label for="dest_host" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Storage Box Host</label>
<div class="relative">
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_host" name="dest_host" x-model="destHost" x-bind:required="requiresDestination"
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="uXXXXXX.your-storagebox.de" />
</div>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
Your Hetzner Storage Box hostname (e.g., uXXXXXX.your-storagebox.de)
</p>
</div>
<div class="mb-6">
<label for="dest_port" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Port</label>
<div class="relative">
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-plug text-gray-400 dark:text-gray-500"></i>
</div>
<input type="number" id="dest_port" name="dest_port" x-model="destPort" x-bind:required="requiresDestination"
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="23" min="1" max="65535" value="23" />
</div>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
Connection port (default: 23)
</p>
</div>
<div class="mb-6">
<label for="dest_auth_type" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Authentication Type</label>
<div class="flex">
<div class="flex items-center me-4">
<input id="dest_auth_password" type="radio" value="password" name="dest_auth_type" x-model="destAuthType"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="dest_auth_password" class="ms-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
</div>
<div class="flex items-center me-4">
<input id="dest_auth_key" type="radio" value="key" name="dest_auth_type" x-model="destAuthType"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="dest_auth_key" class="ms-2 text-sm font-medium text-gray-900 dark:text-white">SSH Key</label>
</div>
</div>
</div>
<div class="mb-6">
<label for="dest_user" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Username</label>
<div class="relative">
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-user text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_user" name="dest_user" x-model="destUser" x-bind:required="requiresDestination"
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="uXXXXXX" />
</div>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
Your Hetzner Storage Box username (typically matches your Storage Box number)
</p>
</div>
<div x-show="destAuthType === 'password'" class="mb-6">
<label for="dest_password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
<div class="relative">
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
</div>
<input type="password" id="dest_password" name="dest_password" x-model="destPassword" x-bind:required="requiresDestination && destAuthType === 'password'"
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="Your password" />
</div>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
Your Hetzner Storage Box password
</p>
</div>
<div x-show="destAuthType === 'key'" class="mb-6">
<label for="dest_key_file" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">SSH Key File</label>
<div class="relative">
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-key text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_key_file" name="dest_key_file" x-model="destKeyFile" x-bind:required="requiresDestination && destAuthType === 'key'"
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/id_rsa" />
</div>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
Path to your SSH private key file (must be readable by the application)
</p>
</div>
<div class="mb-6">
<label for="destination_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Remote Path</label>
<div class="relative">
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-folder-open text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="destination_path" name="destination_path" x-model="destinationPath" x-bind:required="requiresDestination"
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="/backups" />
</div>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
Path on your Storage Box (e.g., /backups, /path/to/files)
</p>
</div>
<div class="p-4 mb-4 text-sm text-yellow-800 rounded-lg bg-yellow-50 dark:bg-yellow-900/30 dark:text-yellow-300" role="alert">
<div class="flex">
<i class="fas fa-shield-alt mr-2 flex-shrink-0"></i>
<div>
<h3 class="font-medium">Security Note</h3>
<p class="mt-1">For increased security, consider using SSH key authentication instead of password authentication when possible.</p>
</div>
</div>
</div>
</div>
}
+128
View File
@@ -0,0 +1,128 @@
package source
templ HetznerSourceForm() {
<div class="space-y-6 mt-4">
<div class="p-4 mb-4 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-blue-900/30 dark:text-blue-300" role="alert">
<div class="flex items-center">
<i class="fas fa-info-circle mr-2"></i>
<span>Configure your Hetzner Storage Box details below. You'll need your server details, username, and password.</span>
</div>
</div>
<div class="mb-6">
<label for="source_host" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Storage Box Host</label>
<div class="relative">
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="source_host" name="source_host" x-model="sourceHost" required
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="uXXXXXX.your-storagebox.de" />
</div>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
Your Hetzner Storage Box hostname (e.g., uXXXXXX.your-storagebox.de)
</p>
</div>
<div class="mb-6">
<label for="source_port" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Port</label>
<div class="relative">
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-plug text-gray-400 dark:text-gray-500"></i>
</div>
<input type="number" id="source_port" name="source_port" x-model="sourcePort" required
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="23" min="1" max="65535" value="23" />
</div>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
Connection port (default: 23)
</p>
</div>
<div class="mb-6">
<label for="source_auth_type" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Authentication Type</label>
<div class="flex">
<div class="flex items-center me-4">
<input id="source_auth_password" type="radio" value="password" name="source_auth_type" x-model="sourceAuthType"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="source_auth_password" class="ms-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
</div>
<div class="flex items-center me-4">
<input id="source_auth_key" type="radio" value="key" name="source_auth_type" x-model="sourceAuthType"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="source_auth_key" class="ms-2 text-sm font-medium text-gray-900 dark:text-white">SSH Key</label>
</div>
</div>
</div>
<div class="mb-6">
<label for="source_user" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Username</label>
<div class="relative">
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-user text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="source_user" name="source_user" x-model="sourceUser" required
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="uXXXXXX" />
</div>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
Your Hetzner Storage Box username (typically matches your Storage Box number)
</p>
</div>
<div x-show="sourceAuthType === 'password'" class="mb-6">
<label for="source_password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
<div class="relative">
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
</div>
<input type="password" id="source_password" name="source_password" x-model="sourcePassword" x-bind:required="sourceAuthType === 'password'"
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="Your password" />
</div>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
Your Hetzner Storage Box password
</p>
</div>
<div x-show="sourceAuthType === 'key'" class="mb-6">
<label for="source_key_file" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">SSH Key File</label>
<div class="relative">
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-key text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="source_key_file" name="source_key_file" x-model="sourceKeyFile" x-bind:required="sourceAuthType === 'key'"
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/id_rsa" />
</div>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
Path to your SSH private key file (must be readable by the application)
</p>
</div>
<div class="mb-6">
<label for="source_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Remote Path</label>
<div class="relative">
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-folder-open text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="source_path" name="source_path" x-model="sourcePath" required
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="/backups" />
</div>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
Path on your Storage Box (e.g., /backups, /path/to/files)
</p>
</div>
<div class="p-4 mb-4 text-sm text-yellow-800 rounded-lg bg-yellow-50 dark:bg-yellow-900/30 dark:text-yellow-300" role="alert">
<div class="flex">
<i class="fas fa-shield-alt mr-2 flex-shrink-0"></i>
<div>
<h3 class="font-medium">Security Note</h3>
<p class="mt-1">For increased security, consider using SSH key authentication instead of password authentication when possible.</p>
</div>
</div>
</div>
</div>
}
+2 -2
View File
@@ -91,7 +91,7 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
sourceName := fmt.Sprintf("source_%d", config.ID)
// Generate rclone config using rclone CLI for source
switch config.SourceType {
case "sftp":
case "sftp", "hetzner":
args := []string{
"config", "create", sourceName, "sftp",
"host", config.SourceHost,
@@ -245,7 +245,7 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
destName := fmt.Sprintf("dest_%d", config.ID)
// Generate rclone config using rclone CLI for destination
switch config.DestinationType {
case "sftp":
case "sftp", "hetzner":
args := []string{
"config", "create", destName, "sftp",
"host", config.DestHost,
+11
View File
@@ -116,6 +116,17 @@ func TestRcloneConnection(config db.TransferConfig, providerType string, dbInsta
if keyFile != "" {
createArgs = append(createArgs, "key_file", keyFile)
}
case "hetzner":
createArgs = append(createArgs, "host", host, "user", user, "pass", pass, "key_file", keyFile)
if port != 0 {
createArgs = append(createArgs, "port", fmt.Sprintf("%d", port))
}
if pass != "" {
createArgs = append(createArgs, "pass", pass)
}
if keyFile != "" {
createArgs = append(createArgs, "key_file", keyFile)
}
case "s3":
createArgs = append(createArgs, "provider", "AWS", "env_auth", "false")
if accessKey != "" {