Merge pull request #16 from StarFleetCPTN/development

Fixes for S3 and build errors
This commit is contained in:
StarFleetCPTN
2025-03-08 16:54:00 -08:00
committed by GitHub
7 changed files with 870 additions and 14 deletions
+2 -1
View File
@@ -44,6 +44,7 @@ Thumbs.db
# Ignore all Go files in the components directory
components/*.go
!components/components.go
# Ignore the data directory
data/
@@ -60,4 +61,4 @@ destination/
archive/
# Ignore binaries
gomft
gomft
+3
View File
@@ -0,0 +1,3 @@
// Package components contains the UI components for the GoMFT application.
// This file serves as a marker for the components package to ensure it's properly recognized by Go.
package components
+599 -5
View File
@@ -38,6 +38,11 @@ func getInitialData(config *db.TransferConfig) string {
sourceDomain := ""
// FTP fields
sourcePassiveMode := true
// OneDrive and Google Drive fields
sourceClientId := ""
sourceClientSecret := ""
sourceDriveId := ""
sourceTeamDrive := ""
filePattern := "*"
outputPattern := ""
destinationType := "local"
@@ -58,6 +63,11 @@ func getInitialData(config *db.TransferConfig) string {
destDomain := ""
// FTP fields
destPassiveMode := true
// OneDrive and Google Drive fields
destClientId := ""
destClientSecret := ""
destDriveId := ""
destTeamDrive := ""
archivePath := ""
archiveEnabled := false
deleteAfterTransfer := false
@@ -83,6 +93,11 @@ func getInitialData(config *db.TransferConfig) string {
sourceDomain = config.SourceDomain
// FTP fields
sourcePassiveMode = config.SourcePassiveMode
// OneDrive and Google Drive fields
sourceClientId = config.SourceClientID
sourceClientSecret = config.SourceClientSecret
sourceDriveId = config.SourceDriveID
sourceTeamDrive = config.SourceTeamDrive
filePattern = config.FilePattern
outputPattern = config.OutputPattern
destinationType = config.DestinationType
@@ -103,6 +118,11 @@ func getInitialData(config *db.TransferConfig) string {
destDomain = config.DestDomain
// FTP fields
destPassiveMode = config.DestPassiveMode
// OneDrive and Google Drive fields
destClientId = config.DestClientID
destClientSecret = config.DestClientSecret
destDriveId = config.DestDriveID
destTeamDrive = config.DestTeamDrive
archivePath = config.ArchivePath
archiveEnabled = config.ArchiveEnabled
deleteAfterTransfer = config.DeleteAfterTransfer
@@ -126,6 +146,10 @@ func getInitialData(config *db.TransferConfig) string {
sourceShare: '%s',
sourceDomain: '%s',
sourcePassiveMode: %v,
sourceClientId: '%s',
sourceClientSecret: '%s',
sourceDriveId: '%s',
sourceTeamDrive: '%s',
filePattern: '%s',
outputPattern: '%s',
destinationType: '%s',
@@ -146,12 +170,18 @@ func getInitialData(config *db.TransferConfig) string {
destDomain: '%s',
// FTP fields
destPassiveMode: %v,
// OneDrive and Google Drive fields
destClientId: '%s',
destClientSecret: '%s',
destDriveId: '%s',
destTeamDrive: '%s',
// Existing fields
archivePath: '%s',
archiveEnabled: %v,
deleteAfterTransfer: %v,
rcloneFlags: '%s',
loading: false,
validate() {
if (this.sourceType === 'sftp') {
if (!this.sourceHost || !this.sourceUser || (!this.sourcePassword && !this.sourceKeyFile)) {
@@ -177,6 +207,22 @@ func getInitialData(config *db.TransferConfig) string {
if (!this.sourceHost || !this.sourceUser || !this.sourcePassword) {
return false;
}
} else if (this.sourceType === 'webdav') {
if (!this.sourceHost || !this.sourceUser || !this.sourcePassword) {
return false;
}
} else if (this.sourceType === 'nextcloud') {
if (!this.sourceHost || !this.sourceUser || !this.sourcePassword) {
return false;
}
} else if (this.sourceType === 'onedrive') {
if (!this.sourceClientId || !this.sourceClientSecret) {
return false;
}
} else if (this.sourceType === 'google_drive') {
if (!this.sourceClientId || !this.sourceClientSecret) {
return false;
}
}
if (this.destinationType === 'sftp') {
@@ -203,15 +249,33 @@ func getInitialData(config *db.TransferConfig) string {
if (!this.destHost || !this.destUser || !this.destPassword) {
return false;
}
} else if (this.destinationType === 'webdav') {
if (!this.destHost || !this.destUser || !this.destPassword) {
return false;
}
} else if (this.destinationType === 'nextcloud') {
if (!this.destHost || !this.destUser || !this.destPassword) {
return false;
}
} else if (this.destinationType === 'onedrive') {
if (!this.destClientId || !this.destClientSecret) {
return false;
}
} else if (this.destinationType === 'google_drive') {
if (!this.destClientId || !this.destClientSecret) {
return false;
}
}
return this.name && this.sourcePath && this.filePattern && this.destinationPath && (!this.archiveEnabled || this.archivePath);
}
}`, name, sourceType, sourcePath, sourceHost, sourcePort, sourceUser, sourcePassword, sourceKeyFile,
sourceBucket, sourceRegion, sourceAccessKey, sourceSecretKey, sourceEndpoint,
sourceShare, sourceDomain, sourcePassiveMode,
sourceClientId, sourceClientSecret, sourceDriveId, sourceTeamDrive,
filePattern, outputPattern, destinationType, destinationPath, destHost, destPort, destUser, destPassword, destKeyFile,
destBucket, destRegion, destAccessKey, destSecretKey, destEndpoint,
destShare, destDomain, destPassiveMode,
destClientId, destClientSecret, destDriveId, destTeamDrive,
archivePath, archiveEnabled, deleteAfterTransfer, rcloneFlags)
}
@@ -284,6 +348,10 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
<option value="b2">Backblaze B2</option>
<option value="smb">SMB</option>
<option value="ftp">FTP</option>
<option value="webdav">WebDAV</option>
<option value="nextcloud">NextCloud</option>
//<option value="onedrive">OneDrive</option>
//<option value="google_drive">Google Drive</option>
</select>
</div>
</div>
@@ -728,6 +796,268 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
</div>
</template>
<template x-if="sourceType === 'webdav'">
<div class="sm:col-span-6 space-y-6">
<div class="sm:col-span-4">
<label for="source_host" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">WebDAV URL</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-globe text-secondary-400 dark:text-secondary-600"></i>
</div>
<input
type="text"
name="source_host"
id="source_host"
x-model="sourceHost"
required
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
placeholder="https://webdav.example.com/remote.php/webdav/"/>
</div>
</div>
<div class="sm:col-span-4">
<label for="source_user" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Username</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-user text-secondary-400 dark:text-secondary-600"></i>
</div>
<input
type="text"
name="source_user"
id="source_user"
x-model="sourceUser"
required
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"/>
</div>
</div>
<div class="sm:col-span-4">
<label for="source_password" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Password</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
</div>
<input
type="password"
name="source_password"
id="source_password"
x-model="sourcePassword"
required
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
placeholder="Password"/>
</div>
<input type="hidden" name="source_password" :value="sourcePassword"/>
</div>
</div>
</template>
<template x-if="sourceType === 'nextcloud'">
<div class="sm:col-span-6 space-y-6">
<div class="sm:col-span-4">
<label for="source_host" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">NextCloud URL</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-cloud text-secondary-400 dark:text-secondary-600"></i>
</div>
<input
type="text"
name="source_host"
id="source_host"
x-model="sourceHost"
required
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
placeholder="https://nextcloud.example.com"/>
</div>
</div>
<div class="sm:col-span-4">
<label for="source_user" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Username</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-user text-secondary-400 dark:text-secondary-600"></i>
</div>
<input
type="text"
name="source_user"
id="source_user"
x-model="sourceUser"
required
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"/>
</div>
</div>
<div class="sm:col-span-4">
<label for="source_password" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Password or App Password</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
</div>
<input
type="password"
name="source_password"
id="source_password"
x-model="sourcePassword"
required
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
placeholder="Password"/>
</div>
<input type="hidden" name="source_password" :value="sourcePassword"/>
<p class="mt-2 text-sm text-secondary-500 dark:text-secondary-400">Using an app password is recommended for security</p>
</div>
</div>
</template>
// <template x-if="sourceType === 'onedrive'">
// <div class="sm:col-span-6 space-y-6">
// <div class="relative p-4 border border-yellow-300 bg-yellow-50 dark:bg-yellow-900/30 dark:border-yellow-700 rounded-lg mb-4">
// <div class="flex">
// <div class="flex-shrink-0">
// <i class="fas fa-info-circle text-yellow-400"></i>
// </div>
// <div class="ml-3">
// <h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-300">OneDrive Authentication</h3>
// <div class="mt-2 text-sm text-yellow-700 dark:text-yellow-400">
// <p>OneDrive requires OAuth authentication. You'll need to:</p>
// <ol class="list-decimal ml-4 mt-1 space-y-1">
// <li>Register an application in the Microsoft Azure portal</li>
// <li>Add redirect URI: http://localhost:53682/</li>
// <li>Save the application details below</li>
// </ol>
// </div>
// </div>
// </div>
// </div>
// <div class="sm:col-span-4">
// <label for="source_client_id" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Client ID</label>
// <div class="relative">
// <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
// <i class="fas fa-id-card text-secondary-400 dark:text-secondary-600"></i>
// </div>
// <input
// type="text"
// name="source_client_id"
// id="source_client_id"
// x-model="sourceClientId"
// required
// class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
// placeholder="Azure application client ID"/>
// </div>
// </div>
// <div class="sm:col-span-4">
// <label for="source_client_secret" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Client Secret</label>
// <div class="relative">
// <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
// <i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
// </div>
// <input
// type="password"
// name="source_client_secret"
// id="source_client_secret"
// x-model="sourceClientSecret"
// required
// class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
// placeholder="Azure application client secret"/>
// </div>
// <input type="hidden" name="source_client_secret" :value="sourceClientSecret"/>
// </div>
// <div class="sm:col-span-4">
// <label for="source_drive_id" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Drive ID (Optional)</label>
// <div class="relative">
// <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
// <i class="fas fa-hdd text-secondary-400 dark:text-secondary-600"></i>
// </div>
// <input
// type="text"
// name="source_drive_id"
// id="source_drive_id"
// x-model="sourceDriveId"
// class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
// placeholder="OneDrive drive ID (leave empty for default)"/>
// </div>
// <p class="mt-2 text-sm text-secondary-500 dark:text-secondary-400">For shared or business drives</p>
// </div>
// </div>
// </template>
// <template x-if="sourceType === 'google_drive'">
// <div class="sm:col-span-6 space-y-6">
// <div class="relative p-4 border border-yellow-300 bg-yellow-50 dark:bg-yellow-900/30 dark:border-yellow-700 rounded-lg mb-4">
// <div class="flex">
// <div class="flex-shrink-0">
// <i class="fas fa-info-circle text-yellow-400"></i>
// </div>
// <div class="ml-3">
// <h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-300">Google Drive Authentication</h3>
// <div class="mt-2 text-sm text-yellow-700 dark:text-yellow-400">
// <p>Google Drive requires OAuth authentication. You'll need to:</p>
// <ol class="list-decimal ml-4 mt-1 space-y-1">
// <li>Create a project in Google Cloud Console</li>
// <li>Enable Google Drive API</li>
// <li>Create OAuth credentials</li>
// <li>Add redirect URI: http://localhost:53682/</li>
// <li>Save the client details below</li>
// </ol>
// </div>
// </div>
// </div>
// </div>
// <div class="sm:col-span-4">
// <label for="source_client_id" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Client ID</label>
// <div class="relative">
// <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
// <i class="fas fa-id-card text-secondary-400 dark:text-secondary-600"></i>
// </div>
// <input
// type="text"
// name="source_client_id"
// id="source_client_id"
// x-model="sourceClientId"
// required
// class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
// placeholder="Google OAuth client ID"/>
// </div>
// </div>
// <div class="sm:col-span-4">
// <label for="source_client_secret" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Client Secret</label>
// <div class="relative">
// <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
// <i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
// </div>
// <input
// type="password"
// name="source_client_secret"
// id="source_client_secret"
// x-model="sourceClientSecret"
// required
// class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
// placeholder="Google OAuth client secret"/>
// </div>
// <input type="hidden" name="source_client_secret" :value="sourceClientSecret"/>
// </div>
// <div class="sm:col-span-4">
// <label for="source_team_drive" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Team Drive ID (Optional)</label>
// <div class="relative">
// <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
// <i class="fas fa-users text-secondary-400 dark:text-secondary-600"></i>
// </div>
// <input
// type="text"
// name="source_team_drive"
// id="source_team_drive"
// x-model="sourceTeamDrive"
// class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
// placeholder="Team Drive ID (leave empty for personal drive)"/>
// </div>
// </div>
// </div>
// </template>
<div class="sm:col-span-6">
<label for="source_path" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Source Path</label>
<div class="relative">
@@ -814,6 +1144,10 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
<option value="b2">Backblaze B2</option>
<option value="smb">SMB</option>
<option value="ftp">FTP</option>
<option value="webdav">WebDAV</option>
<option value="nextcloud">NextCloud</option>
// <option value="onedrive">OneDrive</option>
// <option value="google_drive">Google Drive</option>
</select>
</div>
</div>
@@ -1258,15 +1592,277 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
</div>
</template>
<template x-if="destinationType === 'webdav'">
<div class="sm:col-span-6 space-y-6">
<div class="sm:col-span-4">
<label for="dest_host" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">WebDAV URL</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-globe text-secondary-400 dark:text-secondary-600"></i>
</div>
<input
type="text"
name="dest_host"
id="dest_host"
x-model="destHost"
required
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
placeholder="https://webdav.example.com/remote.php/webdav/"/>
</div>
</div>
<div class="sm:col-span-4">
<label for="dest_user" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Username</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-user text-secondary-400 dark:text-secondary-600"></i>
</div>
<input
type="text"
name="dest_user"
id="dest_user"
x-model="destUser"
required
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"/>
</div>
</div>
<div class="sm:col-span-4">
<label for="dest_password" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Password</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
</div>
<input
type="password"
name="dest_password"
id="dest_password"
x-model="destPassword"
required
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
placeholder="Password"/>
</div>
<input type="hidden" name="dest_password" :value="destPassword"/>
</div>
</div>
</template>
<template x-if="destinationType === 'nextcloud'">
<div class="sm:col-span-6 space-y-6">
<div class="sm:col-span-4">
<label for="dest_host" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">NextCloud URL</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-cloud text-secondary-400 dark:text-secondary-600"></i>
</div>
<input
type="text"
name="dest_host"
id="dest_host"
x-model="destHost"
required
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
placeholder="https://nextcloud.example.com"/>
</div>
</div>
<div class="sm:col-span-4">
<label for="dest_user" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Username</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-user text-secondary-400 dark:text-secondary-600"></i>
</div>
<input
type="text"
name="dest_user"
id="dest_user"
x-model="destUser"
required
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"/>
</div>
</div>
<div class="sm:col-span-4">
<label for="dest_password" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Password or App Password</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
</div>
<input
type="password"
name="dest_password"
id="dest_password"
x-model="destPassword"
required
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
placeholder="Password"/>
</div>
<input type="hidden" name="dest_password" :value="destPassword"/>
<p class="mt-2 text-sm text-secondary-500 dark:text-secondary-400">Using an app password is recommended for security</p>
</div>
</div>
</template>
// <template x-if="destinationType === 'onedrive'">
// <div class="sm:col-span-6 space-y-6">
// <div class="relative p-4 border border-yellow-300 bg-yellow-50 dark:bg-yellow-900/30 dark:border-yellow-700 rounded-lg mb-4">
// <div class="flex">
// <div class="flex-shrink-0">
// <i class="fas fa-info-circle text-yellow-400"></i>
// </div>
// <div class="ml-3">
// <h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-300">OneDrive Authentication</h3>
// <div class="mt-2 text-sm text-yellow-700 dark:text-yellow-400">
// <p>OneDrive requires OAuth authentication. You'll need to:</p>
// <ol class="list-decimal ml-4 mt-1 space-y-1">
// <li>Register an application in the Microsoft Azure portal</li>
// <li>Add redirect URI: http://localhost:53682/</li>
// <li>Save the application details below</li>
// </ol>
// </div>
// </div>
// </div>
// </div>
// <div class="sm:col-span-4">
// <label for="dest_client_id" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Client ID</label>
// <div class="relative">
// <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
// <i class="fas fa-id-card text-secondary-400 dark:text-secondary-600"></i>
// </div>
// <input
// type="text"
// name="dest_client_id"
// id="dest_client_id"
// x-model="destClientId"
// required
// class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
// placeholder="Azure application client ID"/>
// </div>
// </div>
// <div class="sm:col-span-4">
// <label for="dest_client_secret" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Client Secret</label>
// <div class="relative">
// <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
// <i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
// </div>
// <input
// type="password"
// name="dest_client_secret"
// id="dest_client_secret"
// x-model="destClientSecret"
// required
// class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
// placeholder="Azure application client secret"/>
// </div>
// <input type="hidden" name="dest_client_secret" :value="destClientSecret"/>
// </div>
// <div class="sm:col-span-4">
// <label for="dest_drive_id" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Drive ID (Optional)</label>
// <div class="relative">
// <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
// <i class="fas fa-hdd text-secondary-400 dark:text-secondary-600"></i>
// </div>
// <input
// type="text"
// name="dest_drive_id"
// id="dest_drive_id"
// x-model="destDriveId"
// class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
// placeholder="OneDrive drive ID (leave empty for default)"/>
// </div>
// <p class="mt-2 text-sm text-secondary-500 dark:text-secondary-400">For shared or business drives</p>
// </div>
// </div>
// </template>
// <template x-if="destinationType === 'google_drive'">
// <div class="sm:col-span-6 space-y-6">
// <div class="relative p-4 border border-yellow-300 bg-yellow-50 dark:bg-yellow-900/30 dark:border-yellow-700 rounded-lg mb-4">
// <div class="flex">
// <div class="flex-shrink-0">
// <i class="fas fa-info-circle text-yellow-400"></i>
// </div>
// <div class="ml-3">
// <h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-300">Google Drive Authentication</h3>
// <div class="mt-2 text-sm text-yellow-700 dark:text-yellow-400">
// <p>Google Drive requires OAuth authentication. You'll need to:</p>
// <ol class="list-decimal ml-4 mt-1 space-y-1">
// <li>Create a project in Google Cloud Console</li>
// <li>Enable Google Drive API</li>
// <li>Create OAuth credentials</li>
// <li>Add redirect URI: http://localhost:53682/</li>
// <li>Save the client details below</li>
// </ol>
// </div>
// </div>
// </div>
// </div>
// <div class="sm:col-span-4">
// <label for="dest_client_id" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Client ID</label>
// <div class="relative">
// <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
// <i class="fas fa-id-card text-secondary-400 dark:text-secondary-600"></i>
// </div>
// <input
// type="text"
// name="dest_client_id"
// id="dest_client_id"
// x-model="destClientId"
// required
// class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
// placeholder="Google OAuth client ID"/>
// </div>
// </div>
// <div class="sm:col-span-4">
// <label for="dest_client_secret" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Client Secret</label>
// <div class="relative">
// <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
// <i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
// </div>
// <input
// type="password"
// name="dest_client_secret"
// id="dest_client_secret"
// x-model="destClientSecret"
// required
// class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
// placeholder="Google OAuth client secret"/>
// </div>
// <input type="hidden" name="dest_client_secret" :value="destClientSecret"/>
// </div>
// <div class="sm:col-span-4">
// <label for="dest_team_drive" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Team Drive ID (Optional)</label>
// <div class="relative">
// <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
// <i class="fas fa-users text-secondary-400 dark:text-secondary-600"></i>
// </div>
// <input
// type="text"
// name="dest_team_drive"
// id="dest_team_drive"
// x-model="destTeamDrive"
// class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
// placeholder="Team Drive ID (leave empty for personal drive)"/>
// </div>
// </div>
// </div>
// </template>
<div class="sm:col-span-6">
<label for="destination_path" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Destination Path</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-folder-open text-secondary-400 dark:text-secondary-600"></i>
</div>
<input
type="text"
name="destination_path"
<input
type="text"
name="destination_path"
id="destination_path"
x-model="destinationPath"
required
@@ -1302,7 +1898,6 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
placeholder="/path/to/archive"/>
</div>
<p class="mt-2 text-sm text-secondary-500 dark:text-secondary-400">Location to move files after successful transfer</p>
</div>
</div>
@@ -1339,7 +1934,6 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
class="form-input pl-10 w-full rounded-lg border-secondary-300 dark:border-secondary-700 dark:bg-secondary-800 dark:text-secondary-100 focus:ring-primary-500 focus:border-primary-500"
placeholder="--transfers 4 --checkers 8"/>
</div>
<p class="mt-2 text-sm text-secondary-500 dark:text-secondary-400">Optional: Additional rclone flags for fine-tuning the transfer.</p>
</div>
</div>
</div>
+144
View File
@@ -66,6 +66,11 @@ type TransferConfig struct {
SourceDomain string `form:"source_domain"`
// FTP source fields
SourcePassiveMode bool `gorm:"default:true" form:"source_passive_mode"`
// OneDrive and Google Drive source fields
SourceClientID string `form:"source_client_id"`
SourceClientSecret string `form:"source_client_secret" gorm:"-"` // Not stored in DB, only used for form
SourceDriveID string `form:"source_drive_id"` // For OneDrive
SourceTeamDrive string `form:"source_team_drive"` // For Google Drive
// General fields
FilePattern string `gorm:"default:'*'" form:"file_pattern"`
OutputPattern string `form:"output_pattern"` // Pattern for output filenames with date variables
@@ -87,6 +92,11 @@ type TransferConfig struct {
DestDomain string `form:"dest_domain"`
// FTP destination fields
DestPassiveMode bool `gorm:"default:true" form:"dest_passive_mode"`
// OneDrive and Google Drive destination fields
DestClientID string `form:"dest_client_id"`
DestClientSecret string `form:"dest_client_secret" gorm:"-"` // Not stored in DB, only used for form
DestDriveID string `form:"dest_drive_id"` // For OneDrive
DestTeamDrive string `form:"dest_team_drive"` // For Google Drive
// General fields
ArchivePath string `form:"archive_path"`
ArchiveEnabled bool `gorm:"default:false" form:"archive_enabled"`
@@ -426,6 +436,73 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
args = append(args, "passive", "true")
}
cmd := exec.Command(rclonePath, args...)
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to create source config: %v\nOutput: %s", err, output)
}
case "webdav":
args := []string{
"config", "create", sourceName, "webdav",
"url", config.SourceHost,
"user", config.SourceUser,
"pass", config.SourcePassword,
"--non-interactive",
"--config", configPath,
"--log-level", "ERROR",
}
cmd := exec.Command(rclonePath, args...)
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to create source config: %v\nOutput: %s", err, output)
}
case "nextcloud":
args := []string{
"config", "create", sourceName, "webdav",
"url", config.SourceHost,
"user", config.SourceUser,
"pass", config.SourcePassword,
"vendor", "nextcloud",
"--non-interactive",
"--config", configPath,
"--log-level", "ERROR",
}
cmd := exec.Command(rclonePath, args...)
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to create source config: %v\nOutput: %s", err, output)
}
case "onedrive":
args := []string{
"config", "create", sourceName, "onedrive",
"client_id", config.SourceClientID,
"client_secret", config.SourceClientSecret,
"--non-interactive",
"--config", configPath,
"--log-level", "ERROR",
}
if config.SourceDriveID != "" {
args = append(args, "drive_id", config.SourceDriveID)
}
cmd := exec.Command(rclonePath, args...)
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to create source config: %v\nOutput: %s", err, output)
}
case "google_drive":
args := []string{
"config", "create", sourceName, "drive",
"client_id", config.SourceClientID,
"client_secret", config.SourceClientSecret,
"--non-interactive",
"--config", configPath,
"--log-level", "ERROR",
}
if config.SourceTeamDrive != "" {
args = append(args, "team_drive", config.SourceTeamDrive)
}
cmd := exec.Command(rclonePath, args...)
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to create source config: %v\nOutput: %s", err, output)
@@ -547,6 +624,73 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
args = append(args, "passive", "true")
}
cmd := exec.Command(rclonePath, args...)
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to create destination config: %v\nOutput: %s", err, output)
}
case "webdav":
args := []string{
"config", "create", destName, "webdav",
"url", config.DestHost,
"user", config.DestUser,
"pass", config.DestPassword,
"--non-interactive",
"--config", configPath,
"--log-level", "ERROR",
}
cmd := exec.Command(rclonePath, args...)
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to create destination config: %v\nOutput: %s", err, output)
}
case "nextcloud":
args := []string{
"config", "create", destName, "webdav",
"url", config.DestHost,
"user", config.DestUser,
"pass", config.DestPassword,
"vendor", "nextcloud",
"--non-interactive",
"--config", configPath,
"--log-level", "ERROR",
}
cmd := exec.Command(rclonePath, args...)
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to create destination config: %v\nOutput: %s", err, output)
}
case "onedrive":
args := []string{
"config", "create", destName, "onedrive",
"client_id", config.DestClientID,
"client_secret", config.DestClientSecret,
"--non-interactive",
"--config", configPath,
"--log-level", "ERROR",
}
if config.DestDriveID != "" {
args = append(args, "drive_id", config.DestDriveID)
}
cmd := exec.Command(rclonePath, args...)
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to create destination config: %v\nOutput: %s", err, output)
}
case "google_drive":
args := []string{
"config", "create", destName, "drive",
"client_id", config.DestClientID,
"client_secret", config.DestClientSecret,
"--non-interactive",
"--config", configPath,
"--log-level", "ERROR",
}
if config.DestTeamDrive != "" {
args = append(args, "team_drive", config.DestTeamDrive)
}
cmd := exec.Command(rclonePath, args...)
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to create destination config: %v\nOutput: %s", err, output)
@@ -0,0 +1,63 @@
package migrations
import (
"github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm"
)
// AddCloudStorageFields adds fields for WebDAV, NextCloud, OneDrive, and Google Drive
func AddCloudStorageFields() *gormigrate.Migration {
return &gormigrate.Migration{
ID: "add_cloud_storage_fields",
Migrate: func(tx *gorm.DB) error {
// Add source fields
if err := tx.Exec("ALTER TABLE transfer_configs ADD COLUMN source_client_id VARCHAR(255)").Error; err != nil {
return err
}
if err := tx.Exec("ALTER TABLE transfer_configs ADD COLUMN source_drive_id VARCHAR(255)").Error; err != nil {
return err
}
if err := tx.Exec("ALTER TABLE transfer_configs ADD COLUMN source_team_drive VARCHAR(255)").Error; err != nil {
return err
}
// Add destination fields
if err := tx.Exec("ALTER TABLE transfer_configs ADD COLUMN dest_client_id VARCHAR(255)").Error; err != nil {
return err
}
if err := tx.Exec("ALTER TABLE transfer_configs ADD COLUMN dest_drive_id VARCHAR(255)").Error; err != nil {
return err
}
if err := tx.Exec("ALTER TABLE transfer_configs ADD COLUMN dest_team_drive VARCHAR(255)").Error; err != nil {
return err
}
return nil
},
Rollback: func(tx *gorm.DB) error {
// Drop source fields
if err := tx.Exec("ALTER TABLE transfer_configs DROP COLUMN source_client_id").Error; err != nil {
return err
}
if err := tx.Exec("ALTER TABLE transfer_configs DROP COLUMN source_drive_id").Error; err != nil {
return err
}
if err := tx.Exec("ALTER TABLE transfer_configs DROP COLUMN source_team_drive").Error; err != nil {
return err
}
// Drop destination fields
if err := tx.Exec("ALTER TABLE transfer_configs DROP COLUMN dest_client_id").Error; err != nil {
return err
}
if err := tx.Exec("ALTER TABLE transfer_configs DROP COLUMN dest_drive_id").Error; err != nil {
return err
}
if err := tx.Exec("ALTER TABLE transfer_configs DROP COLUMN dest_team_drive").Error; err != nil {
return err
}
return nil
},
}
}
+2 -1
View File
@@ -6,10 +6,11 @@ import (
)
// InitMigrations initializes the migrations
func InitMigrations() *gormigrate.Gormigrate {
func InitMigrations(db *gorm.DB) *gormigrate.Gormigrate {
migrations := []*gormigrate.Migration{
// ... existing migrations
AddDeleteAfterTransferColumn(),
AddCloudStorageFields(),
}
return gormigrate.New(db, gormigrate.DefaultOptions, migrations)
+57 -7
View File
@@ -161,8 +161,21 @@ func (s *Scheduler) executeJob(jobID uint) {
"--config", configPath,
"size",
"--include", job.Config.FilePattern,
fmt.Sprintf("source_%d:%s", job.Config.ID, job.Config.SourcePath),
}
// Add source path with bucket for S3-compatible storage
var sourceSizePath string
if job.Config.SourceType == "s3" || job.Config.SourceType == "minio" || job.Config.SourceType == "b2" {
sourceSizePath = fmt.Sprintf("source_%d:%s", job.Config.ID, job.Config.SourceBucket)
if job.Config.SourcePath != "" && job.Config.SourcePath != "/" {
sourceSizePath = fmt.Sprintf("source_%d:%s/%s", job.Config.ID, job.Config.SourceBucket, job.Config.SourcePath)
}
} else {
sourceSizePath = fmt.Sprintf("source_%d:%s", job.Config.ID, job.Config.SourcePath)
}
sizeArgs = append(sizeArgs, sourceSizePath)
// Get the rclone path from the environment variable or use the default path
rclonePath := os.Getenv("RCLONE_PATH")
if rclonePath == "" {
@@ -202,9 +215,21 @@ func (s *Scheduler) executeJob(jobID uint) {
"--config", configPath,
"lsf",
"--include", job.Config.FilePattern,
fmt.Sprintf("source_%d:%s", job.Config.ID, job.Config.SourcePath),
}
// Add source path with bucket for S3-compatible storage
var sourceLsPath string
if job.Config.SourceType == "s3" || job.Config.SourceType == "minio" || job.Config.SourceType == "b2" {
sourceLsPath = fmt.Sprintf("source_%d:%s", job.Config.ID, job.Config.SourceBucket)
if job.Config.SourcePath != "" && job.Config.SourcePath != "/" {
sourceLsPath = fmt.Sprintf("source_%d:%s/%s", job.Config.ID, job.Config.SourceBucket, job.Config.SourcePath)
}
} else {
sourceLsPath = fmt.Sprintf("source_%d:%s", job.Config.ID, job.Config.SourcePath)
}
listArgs = append(listArgs, sourceLsPath)
fmt.Printf("Listing files for job %d: rclone %s\n", jobID, strings.Join(listArgs, " "))
// Get the rclone path from the environment variable or use the default path
rclonePath := os.Getenv("RCLONE_PATH")
@@ -247,8 +272,26 @@ func (s *Scheduler) executeJob(jobID uint) {
}
// Source and destination paths
sourcePath := fmt.Sprintf("source_%d:%s/%s", job.Config.ID, job.Config.SourcePath, file)
destPath := fmt.Sprintf("dest_%d:%s/%s", job.Config.ID, job.Config.DestinationPath, file)
var sourcePath, destPath string
// For S3, MinIO, and B2, include the bucket in the path
if job.Config.SourceType == "s3" || job.Config.SourceType == "minio" || job.Config.SourceType == "b2" {
sourcePath = fmt.Sprintf("source_%d:%s/%s", job.Config.ID, job.Config.SourceBucket, file)
if job.Config.SourcePath != "" && job.Config.SourcePath != "/" {
sourcePath = fmt.Sprintf("source_%d:%s/%s/%s", job.Config.ID, job.Config.SourceBucket, job.Config.SourcePath, file)
}
} else {
sourcePath = fmt.Sprintf("source_%d:%s/%s", job.Config.ID, job.Config.SourcePath, file)
}
if job.Config.DestinationType == "s3" || job.Config.DestinationType == "minio" || job.Config.DestinationType == "b2" {
destPath = fmt.Sprintf("dest_%d:%s/%s", job.Config.ID, job.Config.DestBucket, file)
if job.Config.DestinationPath != "" && job.Config.DestinationPath != "/" {
destPath = fmt.Sprintf("dest_%d:%s/%s/%s", job.Config.ID, job.Config.DestBucket, job.Config.DestinationPath, file)
}
} else {
destPath = fmt.Sprintf("dest_%d:%s/%s", job.Config.ID, job.Config.DestinationPath, file)
}
// Add output filename pattern if specified
if job.Config.OutputPattern != "" {
@@ -299,9 +342,18 @@ func (s *Scheduler) executeJob(jobID uint) {
"--config", configPath,
"copyto",
sourcePath,
fmt.Sprintf("source_%d:%s/%s", job.Config.ID, job.Config.ArchivePath, file),
}
// Construct archive path with bucket if needed
var archiveDest string
if job.Config.SourceType == "s3" || job.Config.SourceType == "minio" || job.Config.SourceType == "b2" {
archiveDest = fmt.Sprintf("source_%d:%s/%s/%s", job.Config.ID, job.Config.SourceBucket, job.Config.ArchivePath, file)
} else {
archiveDest = fmt.Sprintf("source_%d:%s/%s", job.Config.ID, job.Config.ArchivePath, file)
}
archiveArgs = append(archiveArgs, archiveDest)
fmt.Printf("Executing rclone archive command for job %d, file %s: rclone %s\n",
jobID, file, strings.Join(archiveArgs, " "))
// Get the rclone path from the environment variable or use the default path
@@ -320,8 +372,6 @@ func (s *Scheduler) executeJob(jobID uint) {
fmt.Printf("Warning: Error archiving file %s for job %d: %v\n", file, jobID, archiveErr)
transferErrors = append(transferErrors,
fmt.Sprintf("Archive error for file %s: %v", file, archiveErr))
transferErrors = append(transferErrors,
fmt.Sprintf("Archive error for file %s: %v", file, archiveErr))
}
}
if job.Config.DeleteAfterTransfer {