mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-09-13 01:50:44 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66623693df | ||
|
|
e2ab2eb603 | ||
|
|
87bdfc5e91 | ||
|
|
78fe589fcf | ||
|
|
bcd09af2d6 | ||
|
|
de97ad6b50 | ||
|
|
417841b3d2 | ||
|
|
11f8631929 |
@@ -44,6 +44,9 @@ GoMFT is a web-based managed file transfer application built with Go, leveraging
|
|||||||
- SFTP
|
- SFTP
|
||||||
- FTP
|
- FTP
|
||||||
- SMB/CIFS shares
|
- SMB/CIFS shares
|
||||||
|
- Hetzner Storage Box
|
||||||
|
- Backblaze B2
|
||||||
|
- Wasabi
|
||||||
- Local filesystem
|
- Local filesystem
|
||||||
- And more via rclone
|
- And more via rclone
|
||||||
- **Webhook Notifications**: Receive real-time notifications of job events:
|
- **Webhook Notifications**: Receive real-time notifications of job events:
|
||||||
@@ -525,6 +528,10 @@ The following fields have been added to the `users` table:
|
|||||||
- Local filesystem
|
- Local filesystem
|
||||||
- Amazon S3
|
- Amazon S3
|
||||||
- MinIO (S3-compatible storage)
|
- MinIO (S3-compatible storage)
|
||||||
|
- NextCloud
|
||||||
|
- Backblaze B2
|
||||||
|
- Wasabi
|
||||||
|
- Hetzner Storage Box
|
||||||
- SFTP
|
- SFTP
|
||||||
- FTP
|
- FTP
|
||||||
- SMB/CIFS shares
|
- SMB/CIFS shares
|
||||||
|
|||||||
@@ -398,6 +398,8 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
|||||||
sourcePort = 22;
|
sourcePort = 22;
|
||||||
} else if (sourceType === 'ftp') {
|
} else if (sourceType === 'ftp') {
|
||||||
sourcePort = 21;
|
sourcePort = 21;
|
||||||
|
} else if (sourceType === 'hetzner') {
|
||||||
|
sourcePort = 23;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,25 +408,32 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
|||||||
destPort = 22;
|
destPort = 22;
|
||||||
} else if (destinationType === 'ftp') {
|
} else if (destinationType === 'ftp') {
|
||||||
destPort = 21;
|
destPort = 21;
|
||||||
|
} else if (destinationType === 'hetzner') {
|
||||||
|
destPort = 23;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize command requirements
|
// Initialize command requirements
|
||||||
updateCommandRequirements();
|
updateCommandRequirements();
|
||||||
})"
|
})"
|
||||||
x-effect="if (sourceType === 'sftp' && (sourcePort === 0 || sourcePort === 21)) {
|
x-effect="if (sourceType === 'sftp' && (sourcePort === 0 || sourcePort === 21 || sourcePort === 23)) {
|
||||||
sourcePort = 22;
|
sourcePort = 22;
|
||||||
console.log('Updating source port to 22 for SFTP');
|
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;
|
sourcePort = 21;
|
||||||
console.log('Updating source port to 21 for FTP');
|
console.log('Updating source port to 21 for FTP');
|
||||||
}"
|
} else if (sourceType === 'hetzner' && (sourcePort === 0 || sourcePort === 22 || sourcePort === 21)) {
|
||||||
x-effect="if (destinationType === 'sftp' && (destPort === 0 || destPort === 21)) {
|
sourcePort = 23;
|
||||||
|
console.log('Updating source port to 23 for Hetzner');
|
||||||
|
} else if (destinationType === 'sftp' && (destPort === 0 || destPort === 21 || destPort === 23)) {
|
||||||
destPort = 22;
|
destPort = 22;
|
||||||
console.log('Updating destination port to 22 for SFTP');
|
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;
|
destPort = 21;
|
||||||
console.log('Updating destination port to 21 for FTP');
|
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');
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
|
|
||||||
@@ -509,15 +518,23 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
|||||||
<template x-if="sourceType === 'ftp'">
|
<template x-if="sourceType === 'ftp'">
|
||||||
@source.FTPSourceForm()
|
@source.FTPSourceForm()
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template x-if="sourceType === 's3'">
|
<template x-if="sourceType === 's3'">
|
||||||
@source.S3SourceForm()
|
@source.S3SourceForm()
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template x-if="sourceType === 'b2'">
|
||||||
|
@source.B2SourceForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="sourceType === 'wasabi'">
|
||||||
|
@source.WasabiSourceForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
<template x-if="sourceType === 'minio'">
|
<template x-if="sourceType === 'minio'">
|
||||||
@source.MinIOSourceForm()
|
@source.MinIOSourceForm()
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template x-if="sourceType === 'smb'">
|
<template x-if="sourceType === 'smb'">
|
||||||
@source.SMBSourceForm()
|
@source.SMBSourceForm()
|
||||||
</template>
|
</template>
|
||||||
@@ -525,7 +542,7 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
|||||||
<template x-if="sourceType === 'webdav'">
|
<template x-if="sourceType === 'webdav'">
|
||||||
@source.WebDAVSourceForm()
|
@source.WebDAVSourceForm()
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template x-if="sourceType === 'nextcloud'">
|
<template x-if="sourceType === 'nextcloud'">
|
||||||
@source.NextCloudSourceForm()
|
@source.NextCloudSourceForm()
|
||||||
</template>
|
</template>
|
||||||
@@ -533,10 +550,14 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
|||||||
<template x-if="sourceType === 'gdrive'">
|
<template x-if="sourceType === 'gdrive'">
|
||||||
@source.GoogleDriveSourceForm()
|
@source.GoogleDriveSourceForm()
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template x-if="sourceType === 'gphotos'">
|
<template x-if="sourceType === 'gphotos'">
|
||||||
@source.GooglePhotosSourceForm()
|
@source.GooglePhotosSourceForm()
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template x-if="sourceType === 'hetzner'">
|
||||||
|
@source.HetznerSourceForm()
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- File Pattern Section -->
|
<!-- File Pattern Section -->
|
||||||
@@ -589,6 +610,14 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
|||||||
@destination.S3DestinationForm()
|
@destination.S3DestinationForm()
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template x-if="destinationType === 'b2'">
|
||||||
|
@destination.B2DestinationForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="destinationType === 'wasabi'">
|
||||||
|
@destination.WasabiDestinationForm()
|
||||||
|
</template>
|
||||||
|
|
||||||
<template x-if="destinationType === 'minio'">
|
<template x-if="destinationType === 'minio'">
|
||||||
@destination.MinIODestinationForm()
|
@destination.MinIODestinationForm()
|
||||||
</template>
|
</template>
|
||||||
@@ -612,6 +641,10 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
|||||||
<template x-if="destinationType === 'gphotos'">
|
<template x-if="destinationType === 'gphotos'">
|
||||||
@destination.GooglePhotosDestinationForm()
|
@destination.GooglePhotosDestinationForm()
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template x-if="destinationType === 'hetzner'">
|
||||||
|
@destination.HetznerDestinationForm()
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Advanced Options Section -->
|
<!-- Advanced Options Section -->
|
||||||
@@ -627,6 +660,8 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Form Actions -->
|
<!-- Form Actions -->
|
||||||
|
|||||||
@@ -195,12 +195,15 @@ templ SourceSelection() {
|
|||||||
<option value="sftp">SFTP</option>
|
<option value="sftp">SFTP</option>
|
||||||
<option value="ftp">FTP</option>
|
<option value="ftp">FTP</option>
|
||||||
<option value="s3">S3</option>
|
<option value="s3">S3</option>
|
||||||
|
<option value="b2">Backblaze B2</option>
|
||||||
|
<option value="wasabi">Wasabi</option>
|
||||||
<option value="minio">MinIO</option>
|
<option value="minio">MinIO</option>
|
||||||
<option value="smb">SMB</option>
|
<option value="smb">SMB</option>
|
||||||
<option value="nextcloud">NextCloud</option>
|
<option value="nextcloud">NextCloud</option>
|
||||||
<option value="webdav">WebDAV</option>
|
<option value="webdav">WebDAV</option>
|
||||||
<option value="gdrive">Google Drive (BETA)</option>
|
<option value="gdrive">Google Drive (BETA)</option>
|
||||||
<option value="gphotos">Google Photos (BETA)</option>
|
<option value="gphotos">Google Photos (BETA)</option>
|
||||||
|
<option value="hetzner">Hetzner Storage Box</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -219,12 +222,15 @@ templ DestinationSelection() {
|
|||||||
<option value="sftp">SFTP</option>
|
<option value="sftp">SFTP</option>
|
||||||
<option value="ftp">FTP</option>
|
<option value="ftp">FTP</option>
|
||||||
<option value="s3">S3</option>
|
<option value="s3">S3</option>
|
||||||
|
<option value="b2">Backblaze B2</option>
|
||||||
|
<option value="wasabi">Wasabi</option>
|
||||||
<option value="minio">MinIO</option>
|
<option value="minio">MinIO</option>
|
||||||
<option value="smb">SMB</option>
|
<option value="smb">SMB</option>
|
||||||
<option value="nextcloud">NextCloud</option>
|
<option value="nextcloud">NextCloud</option>
|
||||||
<option value="webdav">WebDAV</option>
|
<option value="webdav">WebDAV</option>
|
||||||
<option value="gdrive">Google Drive (BETA)</option>
|
<option value="gdrive">Google Drive (BETA)</option>
|
||||||
<option value="gphotos">Google Photos (BETA)</option>
|
<option value="gphotos">Google Photos (BETA)</option>
|
||||||
|
<option value="hetzner">Hetzner Storage Box</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package destination
|
||||||
|
|
||||||
|
templ B2DestinationForm() {
|
||||||
|
<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 Backblaze B2 details below. You'll need your account ID, application key, and bucket information.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="dest_bucket" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Bucket Name</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-archive text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="dest_bucket" name="dest_bucket" x-model="destBucket" 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="my-b2-bucket" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Name of your Backblaze B2 bucket (case-sensitive)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="dest_access_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Account ID</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-id-card text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="dest_access_key" name="dest_access_key" x-model="destAccessKey" 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="0123456789abcdef0123456789" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Your Backblaze B2 Account ID
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="dest_secret_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Application Key</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_secret_key" name="dest_secret_key" x-model="destSecretKey" 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="K0123456789abcdef0123456789abcdef0123456789" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Your Backblaze B2 Application Key
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="dest_endpoint" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Endpoint (Optional)</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_endpoint" name="dest_endpoint" x-model="destEndpoint"
|
||||||
|
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="s3.us-west-000.backblazeb2.com" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Custom endpoint URL (only needed for non-standard regions or private endpoints)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="destination_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Path in Bucket</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"
|
||||||
|
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/files/" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Path prefix within the bucket (e.g., "backups/"). Leave empty to access the entire bucket.
|
||||||
|
</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">It's recommended to create an application key with restricted permissions for this configuration. The application key should only have access to the specific B2 bucket and operations needed.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package destination
|
||||||
|
|
||||||
|
templ WasabiDestinationForm() {
|
||||||
|
<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 Wasabi Cloud Storage details below. You'll need your access key, secret key, region, and bucket information.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="dest_region" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Wasabi Region</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-globe text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="dest_region" name="dest_region" x-model="destRegion" 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="us-east-1" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Wasabi region where your bucket is located (e.g., us-east-1, us-west-1, eu-central-1)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="dest_bucket" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Bucket Name</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-archive text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="dest_bucket" name="dest_bucket" x-model="destBucket" 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="my-wasabi-bucket" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Name of your Wasabi bucket (case-sensitive)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="dest_access_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Access Key</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_access_key" name="dest_access_key" x-model="destAccessKey" 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="ABCDEFGHIJKLMNOPQRST" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Your Wasabi Access Key
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="dest_secret_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Secret Key</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_secret_key" name="dest_secret_key" x-model="destSecretKey" 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="abcdefghijklmnopqrstuvwxyz1234567890ABCD" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Your Wasabi Secret Key
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="dest_endpoint" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Endpoint (Optional)</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_endpoint" name="dest_endpoint" x-model="destEndpoint"
|
||||||
|
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="s3.wasabisys.com" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Wasabi endpoint URL (usually s3.wasabisys.com or region-specific endpoints)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="destination_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Path in Bucket</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"
|
||||||
|
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/files/" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Path prefix within the bucket (e.g., "backups/"). Leave empty to access the entire bucket.
|
||||||
|
</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">It's recommended to use an IAM user with restricted permissions for this configuration. The IAM user should only have access to the specific Wasabi bucket and operations needed.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package source
|
||||||
|
|
||||||
|
templ B2SourceForm() {
|
||||||
|
<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 Backblaze B2 details below. You'll need your account ID, application key, and bucket information.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="source_bucket" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Bucket Name</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-archive text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="source_bucket" name="source_bucket" x-model="sourceBucket" 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="my-b2-bucket" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Name of your Backblaze B2 bucket (case-sensitive)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="source_access_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Account ID</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-id-card text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="source_access_key" name="source_access_key" x-model="sourceAccessKey" 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="0123456789abcdef0123456789" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Your Backblaze B2 Account ID
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="source_secret_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Application Key</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_secret_key" name="source_secret_key" x-model="sourceSecretKey" 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="K0123456789abcdef0123456789abcdef0123456789" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Your Backblaze B2 Application Key
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="source_endpoint" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Endpoint (Optional)</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_endpoint" name="source_endpoint" x-model="sourceEndpoint"
|
||||||
|
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="s3.us-west-000.backblazeb2.com" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Custom endpoint URL (only needed for non-standard regions or private endpoints)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="source_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Path in Bucket</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"
|
||||||
|
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/files/" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Path prefix within the bucket (e.g., "backups/"). Leave empty to access the entire bucket.
|
||||||
|
</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">It's recommended to create an application key with restricted permissions for this configuration. The application key should only have access to the specific B2 bucket and operations needed.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package source
|
||||||
|
|
||||||
|
templ WasabiSourceForm() {
|
||||||
|
<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 Wasabi Cloud Storage details below. You'll need your access key, secret key, region, and bucket information.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="source_region" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Wasabi Region</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-globe text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="source_region" name="source_region" x-model="sourceRegion" 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="us-east-1" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Wasabi region where your bucket is located (e.g., us-east-1, us-west-1, eu-central-1)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="source_bucket" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Bucket Name</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-archive text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="source_bucket" name="source_bucket" x-model="sourceBucket" 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="my-wasabi-bucket" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Name of your Wasabi bucket (case-sensitive)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="source_access_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Access Key</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_access_key" name="source_access_key" x-model="sourceAccessKey" 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="ABCDEFGHIJKLMNOPQRST" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Your Wasabi Access Key
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="source_secret_key" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Secret Key</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_secret_key" name="source_secret_key" x-model="sourceSecretKey" 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="abcdefghijklmnopqrstuvwxyz1234567890ABCD" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Your Wasabi Secret Key
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="source_endpoint" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Endpoint (Optional)</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_endpoint" name="source_endpoint" x-model="sourceEndpoint"
|
||||||
|
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="s3.wasabisys.com" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Wasabi endpoint URL (usually s3.wasabisys.com or region-specific endpoints)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="source_path" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Path in Bucket</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"
|
||||||
|
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/files/" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Path prefix within the bucket (e.g., "backups/"). Leave empty to access the entire bucket.
|
||||||
|
</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">It's recommended to use an IAM user with restricted permissions for this configuration. The IAM user should only have access to the specific Wasabi bucket and operations needed.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
+56
-19
@@ -26,12 +26,22 @@ if [ -n "${PUID}" ] && [ -n "${PGID}" ]; then
|
|||||||
deluser ${USERNAME} > /dev/null 2>&1 || true
|
deluser ${USERNAME} > /dev/null 2>&1 || true
|
||||||
delgroup ${USERNAME} > /dev/null 2>&1 || true
|
delgroup ${USERNAME} > /dev/null 2>&1 || true
|
||||||
|
|
||||||
# Add group with the specified GID
|
# Check if a group with the target GID already exists
|
||||||
echo "Adding group ${USERNAME} with GID ${PGID}"
|
EXISTING_GROUP=$(getent group ${PGID} | cut -d: -f1 || echo "")
|
||||||
if ! addgroup -g ${PGID} ${USERNAME}; then
|
|
||||||
echo "⚠️ Failed to add group ${USERNAME} with GID ${PGID}."
|
if [ -n "${EXISTING_GROUP}" ]; then
|
||||||
# Exiting because user creation will likely fail
|
echo "Group with GID ${PGID} already exists as '${EXISTING_GROUP}', will use this group"
|
||||||
exit 1
|
# Set USERNAME_GROUP to the existing group name
|
||||||
|
USERNAME_GROUP="${EXISTING_GROUP}"
|
||||||
|
else
|
||||||
|
# Add group with the specified GID
|
||||||
|
echo "Adding group ${USERNAME} with GID ${PGID}"
|
||||||
|
if ! addgroup -g ${PGID} ${USERNAME}; then
|
||||||
|
echo "⚠️ Failed to add group ${USERNAME} with GID ${PGID}."
|
||||||
|
# Exiting because user creation will likely fail
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
USERNAME_GROUP="${USERNAME}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Add user with the specified UID and GID
|
# Add user with the specified UID and GID
|
||||||
@@ -39,16 +49,16 @@ if [ -n "${PUID}" ] && [ -n "${PGID}" ]; then
|
|||||||
# Use -h /app for home directory (consistent with expectations)
|
# Use -h /app for home directory (consistent with expectations)
|
||||||
# Use -s /bin/sh for shell
|
# Use -s /bin/sh for shell
|
||||||
# Use -D for no password (system user)
|
# Use -D for no password (system user)
|
||||||
echo "Adding user ${USERNAME} with UID ${PUID}"
|
echo "Adding user ${USERNAME} with UID ${PUID} and group ${USERNAME_GROUP}"
|
||||||
if ! adduser -u ${PUID} -G ${USERNAME} -h /app -s /bin/sh -D ${USERNAME}; then
|
if ! adduser -u ${PUID} -G ${USERNAME_GROUP} -h /app -s /bin/sh -D ${USERNAME}; then
|
||||||
echo "⚠️ Failed to add user ${USERNAME} with UID ${PUID} and group ${USERNAME}."
|
echo "⚠️ Failed to add user ${USERNAME} with UID ${PUID} and group ${USERNAME_GROUP}."
|
||||||
# Exiting because the application cannot run as the correct user
|
# Exiting because the application cannot run as the correct user
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Verify the change
|
# Verify the change
|
||||||
FINAL_UID=$(id -u ${USERNAME} 2>/dev/null || echo "error")
|
FINAL_UID=$(id -u ${USERNAME} 2>/dev/null || echo "error")
|
||||||
FINAL_GID=$(getent group ${USERNAME} | cut -d: -f3 2>/dev/null || echo "error")
|
FINAL_GID=$(id -g ${USERNAME} 2>/dev/null || echo "error")
|
||||||
|
|
||||||
if [ "${FINAL_UID}" = "${PUID}" ] && [ "${FINAL_GID}" = "${PGID}" ]; then
|
if [ "${FINAL_UID}" = "${PUID}" ] && [ "${FINAL_GID}" = "${PGID}" ]; then
|
||||||
echo "✅ Successfully updated UID/GID to ${PUID}:${PGID}"
|
echo "✅ Successfully updated UID/GID to ${PUID}:${PGID}"
|
||||||
@@ -79,13 +89,40 @@ if [ -n "${PUID}" ] && [ -n "${PGID}" ]; then
|
|||||||
groupdel ${USERNAME} 2>/dev/null || true
|
groupdel ${USERNAME} 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Recreate group and user in the correct order
|
# Check if a group with the target GID already exists
|
||||||
echo "Creating group ${USERNAME} with GID ${PGID}"
|
EXISTING_GROUP=$(getent group ${PGID} | cut -d: -f1 || echo "")
|
||||||
groupadd -g ${PGID} ${USERNAME} 2>/dev/null || groupadd ${USERNAME} 2>/dev/null || true
|
|
||||||
|
|
||||||
echo "Creating user ${USERNAME} with UID ${PUID}"
|
if [ -n "${EXISTING_GROUP}" ]; then
|
||||||
useradd -u ${PUID} -g ${USERNAME} -s /bin/sh ${USERNAME} 2>/dev/null ||
|
echo "Group with GID ${PGID} already exists as '${EXISTING_GROUP}', will use this group"
|
||||||
useradd -g ${USERNAME} -s /bin/sh ${USERNAME} 2>/dev/null || true
|
# Set USERNAME_GROUP to the existing group name
|
||||||
|
USERNAME_GROUP="${EXISTING_GROUP}"
|
||||||
|
else
|
||||||
|
# Recreate group with the specified GID
|
||||||
|
echo "Creating group ${USERNAME} with GID ${PGID}"
|
||||||
|
groupadd -g ${PGID} ${USERNAME} 2>/dev/null || groupadd ${USERNAME} 2>/dev/null || true
|
||||||
|
USERNAME_GROUP="${USERNAME}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if a user with the target UID already exists
|
||||||
|
EXISTING_USER=$(getent passwd ${PUID} | cut -d: -f1 || echo "")
|
||||||
|
|
||||||
|
if [ -n "${EXISTING_USER}" ] && [ "${EXISTING_USER}" != "${USERNAME}" ]; then
|
||||||
|
echo "⚠️ Warning: User with UID ${PUID} already exists as '${EXISTING_USER}'. Using a different username may cause issues."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Creating user ${USERNAME} with UID ${PUID} and group ${USERNAME_GROUP}"
|
||||||
|
useradd -u ${PUID} -g ${USERNAME_GROUP} -s /bin/sh ${USERNAME} 2>/dev/null ||
|
||||||
|
useradd -g ${USERNAME_GROUP} -s /bin/sh ${USERNAME} 2>/dev/null || true
|
||||||
|
|
||||||
|
# Verify the change
|
||||||
|
FINAL_UID=$(id -u ${USERNAME} 2>/dev/null || echo "error")
|
||||||
|
FINAL_GID=$(id -g ${USERNAME} 2>/dev/null || echo "error")
|
||||||
|
|
||||||
|
if [ "${FINAL_UID}" = "${PUID}" ] && [ "${FINAL_GID}" = "${PGID}" ]; then
|
||||||
|
echo "✅ Successfully updated UID/GID to ${PUID}:${PGID}"
|
||||||
|
else
|
||||||
|
echo "⚠️ Warning: Verification failed. Target: ${PUID}:${PGID}, Actual: ${FINAL_UID}:${FINAL_GID}"
|
||||||
|
fi
|
||||||
} || {
|
} || {
|
||||||
echo "⚠️ Warning: Failed to update UID/GID, continuing with built-in user"
|
echo "⚠️ Warning: Failed to update UID/GID, continuing with built-in user"
|
||||||
}
|
}
|
||||||
@@ -93,17 +130,17 @@ if [ -n "${PUID}" ] && [ -n "${PGID}" ]; then
|
|||||||
|
|
||||||
# Fix ownership of app directories
|
# Fix ownership of app directories
|
||||||
echo "Setting ownership of app directories"
|
echo "Setting ownership of app directories"
|
||||||
chown -R ${USERNAME}:${USERNAME} /app/data /app/backups || echo "⚠️ Warning: Failed to change ownership"
|
chown -R ${USERNAME}:${USERNAME_GROUP:-${USERNAME}} /app/data /app/backups || echo "⚠️ Warning: Failed to change ownership"
|
||||||
|
|
||||||
# Ensure .env file exists and has correct permissions
|
# Ensure .env file exists and has correct permissions
|
||||||
if [ -f /app/.env ]; then
|
if [ -f /app/.env ]; then
|
||||||
echo "Found .env file, setting permissions..."
|
echo "Found .env file, setting permissions..."
|
||||||
chown ${USERNAME}:${USERNAME} /app/.env || echo "⚠️ Warning: Failed to change .env ownership"
|
chown ${USERNAME}:${USERNAME_GROUP:-${USERNAME}} /app/.env || echo "⚠️ Warning: Failed to change .env ownership"
|
||||||
chmod 644 /app/.env || echo "⚠️ Warning: Failed to change .env permissions"
|
chmod 644 /app/.env || echo "⚠️ Warning: Failed to change .env permissions"
|
||||||
else
|
else
|
||||||
echo "No .env file found, creating empty one..."
|
echo "No .env file found, creating empty one..."
|
||||||
touch /app/.env
|
touch /app/.env
|
||||||
chown ${USERNAME}:${USERNAME} /app/.env || echo "⚠️ Warning: Failed to change .env ownership"
|
chown ${USERNAME}:${USERNAME_GROUP:-${USERNAME}} /app/.env || echo "⚠️ Warning: Failed to change .env ownership"
|
||||||
chmod 644 /app/.env || echo "⚠️ Warning: Failed to change .env permissions"
|
chmod 644 /app/.env || echo "⚠️ Warning: Failed to change .env permissions"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -12,9 +12,9 @@ import (
|
|||||||
// table might have been left renamed as _transfer_configs_old.
|
// table might have been left renamed as _transfer_configs_old.
|
||||||
func RecoverTransferConfigsRename() *gormigrate.Migration {
|
func RecoverTransferConfigsRename() *gormigrate.Migration {
|
||||||
return &gormigrate.Migration{
|
return &gormigrate.Migration{
|
||||||
ID: "011a_recover_transfer_configs_rename",
|
ID: "012a_recover_transfer_configs_rename",
|
||||||
Migrate: func(tx *gorm.DB) error {
|
Migrate: func(tx *gorm.DB) error {
|
||||||
fmt.Println("Running migration 011a: Checking for transfer_configs rename recovery...")
|
fmt.Println("Running migration 012a: Checking for transfer_configs rename recovery...")
|
||||||
|
|
||||||
var oldTableExists int
|
var oldTableExists int
|
||||||
tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='_transfer_configs_old'").Scan(&oldTableExists)
|
tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='_transfer_configs_old'").Scan(&oldTableExists)
|
||||||
@@ -40,7 +40,7 @@ func RecoverTransferConfigsRename() *gormigrate.Migration {
|
|||||||
},
|
},
|
||||||
Rollback: func(tx *gorm.DB) error {
|
Rollback: func(tx *gorm.DB) error {
|
||||||
// Rollback doesn't make sense for a recovery step.
|
// Rollback doesn't make sense for a recovery step.
|
||||||
fmt.Println("Rollback for migration 011a_recover_transfer_configs_rename is not applicable.")
|
fmt.Println("Rollback for migration 012a_recover_transfer_configs_rename is not applicable.")
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
+3
-3
@@ -12,9 +12,9 @@ import (
|
|||||||
// table might have been left renamed as _notification_services_old.
|
// table might have been left renamed as _notification_services_old.
|
||||||
func RecoverNotificationServicesRename() *gormigrate.Migration {
|
func RecoverNotificationServicesRename() *gormigrate.Migration {
|
||||||
return &gormigrate.Migration{
|
return &gormigrate.Migration{
|
||||||
ID: "011b_recover_notification_services_rename",
|
ID: "012b_recover_notification_services_rename",
|
||||||
Migrate: func(tx *gorm.DB) error {
|
Migrate: func(tx *gorm.DB) error {
|
||||||
fmt.Println("Running migration 011b: Checking for notification_services rename recovery...")
|
fmt.Println("Running migration 012b: Checking for notification_services rename recovery...")
|
||||||
|
|
||||||
var oldTableExists int
|
var oldTableExists int
|
||||||
tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='_notification_services_old'").Scan(&oldTableExists)
|
tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='_notification_services_old'").Scan(&oldTableExists)
|
||||||
@@ -38,7 +38,7 @@ func RecoverNotificationServicesRename() *gormigrate.Migration {
|
|||||||
},
|
},
|
||||||
Rollback: func(tx *gorm.DB) error {
|
Rollback: func(tx *gorm.DB) error {
|
||||||
// Rollback doesn't make sense for a recovery step.
|
// Rollback doesn't make sense for a recovery step.
|
||||||
fmt.Println("Rollback for migration 011b_recover_notification_services_rename is not applicable.")
|
fmt.Println("Rollback for migration 012b_recover_notification_services_rename is not applicable.")
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
+3
-3
@@ -12,9 +12,9 @@ import (
|
|||||||
// table might have been left renamed as _auth_providers_old.
|
// table might have been left renamed as _auth_providers_old.
|
||||||
func RecoverAuthProvidersRename() *gormigrate.Migration {
|
func RecoverAuthProvidersRename() *gormigrate.Migration {
|
||||||
return &gormigrate.Migration{
|
return &gormigrate.Migration{
|
||||||
ID: "011c_recover_auth_providers_rename",
|
ID: "012c_recover_auth_providers_rename",
|
||||||
Migrate: func(tx *gorm.DB) error {
|
Migrate: func(tx *gorm.DB) error {
|
||||||
fmt.Println("Running migration 011c: Checking for auth_providers rename recovery...")
|
fmt.Println("Running migration 012c: Checking for auth_providers rename recovery...")
|
||||||
|
|
||||||
var oldTableExists int
|
var oldTableExists int
|
||||||
tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='_auth_providers_old'").Scan(&oldTableExists)
|
tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='_auth_providers_old'").Scan(&oldTableExists)
|
||||||
@@ -38,7 +38,7 @@ func RecoverAuthProvidersRename() *gormigrate.Migration {
|
|||||||
},
|
},
|
||||||
Rollback: func(tx *gorm.DB) error {
|
Rollback: func(tx *gorm.DB) error {
|
||||||
// Rollback doesn't make sense for a recovery step.
|
// Rollback doesn't make sense for a recovery step.
|
||||||
fmt.Println("Rollback for migration 011c_recover_auth_providers_rename is not applicable.")
|
fmt.Println("Rollback for migration 012c_recover_auth_providers_rename is not applicable.")
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -22,10 +22,10 @@ func GetMigrations(db *gorm.DB) *gormigrate.Gormigrate {
|
|||||||
AddRcloneTables(), // 009
|
AddRcloneTables(), // 009
|
||||||
AddRcloneCommandToConfig(), // 010
|
AddRcloneCommandToConfig(), // 010
|
||||||
AddAuthProviders(), // 011
|
AddAuthProviders(), // 011
|
||||||
RecoverTransferConfigsRename(), // 011a
|
|
||||||
RecoverNotificationServicesRename(), // 011b
|
|
||||||
RecoverAuthProvidersRename(), // 011c
|
|
||||||
AlterBooleanDefaults(), // 012
|
AlterBooleanDefaults(), // 012
|
||||||
|
RecoverTransferConfigsRename(), // 012a
|
||||||
|
RecoverNotificationServicesRename(), // 012b
|
||||||
|
RecoverAuthProvidersRename(), // 012c
|
||||||
CleanupInvalidBooleans(), // 013
|
CleanupInvalidBooleans(), // 013
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
|
|||||||
sourceName := fmt.Sprintf("source_%d", config.ID)
|
sourceName := fmt.Sprintf("source_%d", config.ID)
|
||||||
// Generate rclone config using rclone CLI for source
|
// Generate rclone config using rclone CLI for source
|
||||||
switch config.SourceType {
|
switch config.SourceType {
|
||||||
case "sftp":
|
case "sftp", "hetzner":
|
||||||
args := []string{
|
args := []string{
|
||||||
"config", "create", sourceName, "sftp",
|
"config", "create", sourceName, "sftp",
|
||||||
"host", config.SourceHost,
|
"host", config.SourceHost,
|
||||||
@@ -130,6 +130,43 @@ 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 (s3): %v\nOutput: %s", err, output)
|
return fmt.Errorf("failed to create source config (s3): %v\nOutput: %s", err, output)
|
||||||
}
|
}
|
||||||
|
case "wasabi":
|
||||||
|
args := []string{
|
||||||
|
"config", "create", sourceName, "s3",
|
||||||
|
"provider", "Wasabi",
|
||||||
|
"env_auth", "false",
|
||||||
|
"access_key_id", config.SourceAccessKey,
|
||||||
|
"secret_access_key", config.SourceSecretKey,
|
||||||
|
"region", config.SourceRegion,
|
||||||
|
"--non-interactive",
|
||||||
|
"--config", configPath,
|
||||||
|
"--log-level", "ERROR",
|
||||||
|
}
|
||||||
|
endpoint := config.SourceEndpoint
|
||||||
|
if endpoint == "" {
|
||||||
|
endpoint = "s3.wasabisys.com"
|
||||||
|
}
|
||||||
|
args = append(args, "endpoint", endpoint)
|
||||||
|
cmd := exec.Command(rclonePath, args...)
|
||||||
|
if output, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("failed to create source config (wasabi): %v\nOutput: %s", err, output)
|
||||||
|
}
|
||||||
|
case "b2":
|
||||||
|
args := []string{
|
||||||
|
"config", "create", sourceName, "b2",
|
||||||
|
"account", config.SourceAccessKey, // B2 Account ID
|
||||||
|
"key", config.SourceSecretKey, // B2 Application Key
|
||||||
|
"--non-interactive",
|
||||||
|
"--config", configPath,
|
||||||
|
"--log-level", "ERROR",
|
||||||
|
}
|
||||||
|
if config.SourceEndpoint != "" {
|
||||||
|
args = append(args, "endpoint", config.SourceEndpoint)
|
||||||
|
}
|
||||||
|
cmd := exec.Command(rclonePath, args...)
|
||||||
|
if output, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("failed to create source config (b2): %v\nOutput: %s", err, output)
|
||||||
|
}
|
||||||
case "minio":
|
case "minio":
|
||||||
args := []string{
|
args := []string{
|
||||||
"config", "create", sourceName, "s3",
|
"config", "create", sourceName, "s3",
|
||||||
@@ -208,7 +245,7 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
|
|||||||
destName := fmt.Sprintf("dest_%d", config.ID)
|
destName := fmt.Sprintf("dest_%d", config.ID)
|
||||||
// Generate rclone config using rclone CLI for destination
|
// Generate rclone config using rclone CLI for destination
|
||||||
switch config.DestinationType {
|
switch config.DestinationType {
|
||||||
case "sftp":
|
case "sftp", "hetzner":
|
||||||
args := []string{
|
args := []string{
|
||||||
"config", "create", destName, "sftp",
|
"config", "create", destName, "sftp",
|
||||||
"host", config.DestHost,
|
"host", config.DestHost,
|
||||||
@@ -247,6 +284,43 @@ 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 (s3): %v\nOutput: %s", err, output)
|
return fmt.Errorf("failed to create destination config (s3): %v\nOutput: %s", err, output)
|
||||||
}
|
}
|
||||||
|
case "wasabi":
|
||||||
|
args := []string{
|
||||||
|
"config", "create", destName, "s3",
|
||||||
|
"provider", "Wasabi",
|
||||||
|
"env_auth", "false",
|
||||||
|
"access_key_id", config.DestAccessKey,
|
||||||
|
"secret_access_key", config.DestSecretKey,
|
||||||
|
"region", config.DestRegion,
|
||||||
|
"--non-interactive",
|
||||||
|
"--config", configPath,
|
||||||
|
"--log-level", "ERROR",
|
||||||
|
}
|
||||||
|
endpoint := config.DestEndpoint
|
||||||
|
if endpoint == "" {
|
||||||
|
endpoint = "s3.wasabisys.com"
|
||||||
|
}
|
||||||
|
args = append(args, "endpoint", endpoint)
|
||||||
|
cmd := exec.Command(rclonePath, args...)
|
||||||
|
if output, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("failed to create destination config (wasabi): %v\nOutput: %s", err, output)
|
||||||
|
}
|
||||||
|
case "b2":
|
||||||
|
args := []string{
|
||||||
|
"config", "create", destName, "b2",
|
||||||
|
"account", config.DestAccessKey, // B2 Account ID
|
||||||
|
"key", config.DestSecretKey, // B2 Application Key
|
||||||
|
"--non-interactive",
|
||||||
|
"--config", configPath,
|
||||||
|
"--log-level", "ERROR",
|
||||||
|
}
|
||||||
|
if config.DestEndpoint != "" {
|
||||||
|
args = append(args, "endpoint", config.DestEndpoint)
|
||||||
|
}
|
||||||
|
cmd := exec.Command(rclonePath, args...)
|
||||||
|
if output, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("failed to create destination config (b2): %v\nOutput: %s", err, output)
|
||||||
|
}
|
||||||
case "minio":
|
case "minio":
|
||||||
args := []string{
|
args := []string{
|
||||||
"config", "create", destName, "s3",
|
"config", "create", destName, "s3",
|
||||||
|
|||||||
@@ -90,8 +90,20 @@ func TestRcloneConnection(config db.TransferConfig, providerType string, dbInsta
|
|||||||
rclonePath = "rclone"
|
rclonePath = "rclone"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
primaryProvider := provider
|
||||||
|
|
||||||
|
// Convert hetzner to sftp
|
||||||
|
if provider == "hetzner" {
|
||||||
|
primaryProvider = "sftp"
|
||||||
|
}
|
||||||
|
|
||||||
|
// S3 compatible providers
|
||||||
|
if provider == "minio" || provider == "wasabi" {
|
||||||
|
primaryProvider = "s3"
|
||||||
|
}
|
||||||
|
|
||||||
createArgs := []string{
|
createArgs := []string{
|
||||||
"config", "create", remoteName, provider,
|
"config", "create", remoteName, primaryProvider,
|
||||||
"--config", tempConfigPath,
|
"--config", tempConfigPath,
|
||||||
"--non-interactive",
|
"--non-interactive",
|
||||||
"--log-level", "DEBUG",
|
"--log-level", "DEBUG",
|
||||||
@@ -105,7 +117,7 @@ func TestRcloneConnection(config db.TransferConfig, providerType string, dbInsta
|
|||||||
var createCmd *exec.Cmd
|
var createCmd *exec.Cmd
|
||||||
|
|
||||||
switch provider {
|
switch provider {
|
||||||
case "sftp":
|
case "sftp", "hetzner":
|
||||||
createArgs = append(createArgs, "host", host, "user", user)
|
createArgs = append(createArgs, "host", host, "user", user)
|
||||||
if port != 0 {
|
if port != 0 {
|
||||||
createArgs = append(createArgs, "port", fmt.Sprintf("%d", port))
|
createArgs = append(createArgs, "port", fmt.Sprintf("%d", port))
|
||||||
@@ -130,6 +142,20 @@ func TestRcloneConnection(config db.TransferConfig, providerType string, dbInsta
|
|||||||
if endpoint != "" {
|
if endpoint != "" {
|
||||||
createArgs = append(createArgs, "endpoint", endpoint)
|
createArgs = append(createArgs, "endpoint", endpoint)
|
||||||
}
|
}
|
||||||
|
case "wasabi":
|
||||||
|
createArgs = append(createArgs, "provider", "Wasabi", "env_auth", "false")
|
||||||
|
if accessKey != "" {
|
||||||
|
createArgs = append(createArgs, "access_key_id", accessKey)
|
||||||
|
}
|
||||||
|
if secretKey != "" {
|
||||||
|
createArgs = append(createArgs, "secret_access_key", secretKey)
|
||||||
|
}
|
||||||
|
if region != "" {
|
||||||
|
createArgs = append(createArgs, "region", region)
|
||||||
|
}
|
||||||
|
if endpoint != "" {
|
||||||
|
createArgs = append(createArgs, "endpoint", endpoint)
|
||||||
|
}
|
||||||
case "minio":
|
case "minio":
|
||||||
createArgs = append(createArgs, "provider", "Minio", "env_auth", "false")
|
createArgs = append(createArgs, "provider", "Minio", "env_auth", "false")
|
||||||
if accessKey != "" {
|
if accessKey != "" {
|
||||||
@@ -138,6 +164,20 @@ func TestRcloneConnection(config db.TransferConfig, providerType string, dbInsta
|
|||||||
if secretKey != "" {
|
if secretKey != "" {
|
||||||
createArgs = append(createArgs, "secret_access_key", secretKey)
|
createArgs = append(createArgs, "secret_access_key", secretKey)
|
||||||
}
|
}
|
||||||
|
if region != "" {
|
||||||
|
createArgs = append(createArgs, "region", region)
|
||||||
|
}
|
||||||
|
if endpoint != "" {
|
||||||
|
createArgs = append(createArgs, "endpoint", endpoint)
|
||||||
|
}
|
||||||
|
case "b2":
|
||||||
|
createArgs = append(createArgs, "provider", "B2", "env_auth", "false")
|
||||||
|
if accessKey != "" {
|
||||||
|
createArgs = append(createArgs, "account", accessKey)
|
||||||
|
}
|
||||||
|
if secretKey != "" {
|
||||||
|
createArgs = append(createArgs, "key", secretKey)
|
||||||
|
}
|
||||||
if endpoint != "" {
|
if endpoint != "" {
|
||||||
createArgs = append(createArgs, "endpoint", endpoint)
|
createArgs = append(createArgs, "endpoint", endpoint)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user