mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-11-12 06:48:08 +01:00
feat: Refactor Google Drive and Google Photos authentication handling
- Separate built-in authentication options for source and destination in configuration forms. - Update UI components to reflect changes in authentication handling for Google Drive and Google Photos. - Implement path validation for local directories in the configuration forms, enhancing user experience. - Revise README to clarify supported source and destination types, including updated terminology for Google Drive. - Introduce new API endpoint for path validation, improving error handling and user feedback.
This commit is contained in:
1 parent
522699d96a
commit
b1db53b89e
32 files changed
+676
-548
No files matched your search
@@ -348,8 +348,8 @@ User management features:
|
|||||||
### Transfer Configuration Options
|
### Transfer Configuration Options
|
||||||
|
|
||||||
1. **Source/Destination Types**:
|
1. **Source/Destination Types**:
|
||||||
- Google Drive (with built-in or custom authentication)
|
- Google Drive
|
||||||
- Google Photos (with built-in or custom authentication)
|
- Google Photos
|
||||||
- Local filesystem
|
- Local filesystem
|
||||||
- Amazon S3
|
- Amazon S3
|
||||||
- MinIO (S3-compatible storage)
|
- MinIO (S3-compatible storage)
|
||||||
|
|||||||
@@ -84,7 +84,8 @@ func getInitialData(config *db.TransferConfig) string {
|
|||||||
skipProcessedFiles := true
|
skipProcessedFiles := true
|
||||||
maxConcurrentTransfers := 4
|
maxConcurrentTransfers := 4
|
||||||
rcloneFlags := ""
|
rcloneFlags := ""
|
||||||
useBuiltinAuth := true
|
useBuiltinAuthSource := true
|
||||||
|
useBuiltinAuthDest := true
|
||||||
|
|
||||||
// If editing an existing config, populate with those values
|
// If editing an existing config, populate with those values
|
||||||
if config != nil {
|
if config != nil {
|
||||||
@@ -162,14 +163,19 @@ func getInitialData(config *db.TransferConfig) string {
|
|||||||
skipProcessedFiles = config.GetSkipProcessedFiles()
|
skipProcessedFiles = config.GetSkipProcessedFiles()
|
||||||
maxConcurrentTransfers = config.MaxConcurrentTransfers
|
maxConcurrentTransfers = config.MaxConcurrentTransfers
|
||||||
rcloneFlags = config.RcloneFlags
|
rcloneFlags = config.RcloneFlags
|
||||||
if config.UseBuiltinAuth != nil {
|
if config.UseBuiltinAuthSource != nil {
|
||||||
useBuiltinAuth = *config.UseBuiltinAuth
|
useBuiltinAuthSource = *config.UseBuiltinAuthSource
|
||||||
|
} else if sourceClientId != "" || sourceClientSecret != "" {
|
||||||
|
useBuiltinAuthSource = false
|
||||||
|
}
|
||||||
|
if config.UseBuiltinAuthDest != nil {
|
||||||
|
useBuiltinAuthDest = *config.UseBuiltinAuthDest
|
||||||
} else if destClientId != "" || destClientSecret != "" {
|
} else if destClientId != "" || destClientSecret != "" {
|
||||||
useBuiltinAuth = false
|
useBuiltinAuthDest = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the JSON-formatted string with all the data
|
// Return the JSON-formatted string with all the data, add new path validation states
|
||||||
return fmt.Sprintf(`{
|
return fmt.Sprintf(`{
|
||||||
name: '%s',
|
name: '%s',
|
||||||
sourceType: '%s',
|
sourceType: '%s',
|
||||||
@@ -223,7 +229,8 @@ func getInitialData(config *db.TransferConfig) string {
|
|||||||
destStartYear: %d,
|
destStartYear: %d,
|
||||||
destIncludeArchived: %v,
|
destIncludeArchived: %v,
|
||||||
|
|
||||||
useBuiltinAuth: %v,
|
useBuiltinAuthSource: %v,
|
||||||
|
useBuiltinAuthDest: %v,
|
||||||
|
|
||||||
archivePath: '%s',
|
archivePath: '%s',
|
||||||
archiveEnabled: %v,
|
archiveEnabled: %v,
|
||||||
@@ -231,6 +238,32 @@ func getInitialData(config *db.TransferConfig) string {
|
|||||||
skipProcessedFiles: %v,
|
skipProcessedFiles: %v,
|
||||||
maxConcurrentTransfers: %d,
|
maxConcurrentTransfers: %d,
|
||||||
rcloneFlags: '%s',
|
rcloneFlags: '%s',
|
||||||
|
|
||||||
|
// Path validation states
|
||||||
|
sourcePathValid: null,
|
||||||
|
sourcePathError: '',
|
||||||
|
destPathValid: null,
|
||||||
|
destPathError: '',
|
||||||
|
|
||||||
|
// Methods for path validation
|
||||||
|
checkPath(path, type) {
|
||||||
|
if (!path) {
|
||||||
|
this[type + 'PathValid'] = false;
|
||||||
|
this[type + 'PathError'] = 'Path cannot be empty';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/check-path?path=' + encodeURIComponent(path))
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
this[type + 'PathValid'] = data.valid;
|
||||||
|
this[type + 'PathError'] = data.error || '';
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this[type + 'PathValid'] = false;
|
||||||
|
this[type + 'PathError'] = 'Error checking path: ' + error.message;
|
||||||
|
});
|
||||||
|
}
|
||||||
}`,
|
}`,
|
||||||
name, sourceType, sourcePath, sourceHost, sourcePort, sourceUser, sourcePassword, sourceKeyFile, sourceAuthType,
|
name, sourceType, sourcePath, sourceHost, sourcePort, sourceUser, sourcePassword, sourceKeyFile, sourceAuthType,
|
||||||
sourceBucket, sourceRegion, sourceAccessKey, sourceSecretKey, sourceEndpoint, sourceShare, sourceDomain, sourcePassiveMode,
|
sourceBucket, sourceRegion, sourceAccessKey, sourceSecretKey, sourceEndpoint, sourceShare, sourceDomain, sourcePassiveMode,
|
||||||
@@ -241,7 +274,7 @@ func getInitialData(config *db.TransferConfig) string {
|
|||||||
destBucket, destRegion, destAccessKey, destSecretKey, destEndpoint, destShare, destDomain, destPassiveMode,
|
destBucket, destRegion, destAccessKey, destSecretKey, destEndpoint, destShare, destDomain, destPassiveMode,
|
||||||
destClientId, destClientSecret, destDriveId, destTeamDrive,
|
destClientId, destClientSecret, destDriveId, destTeamDrive,
|
||||||
destReadOnly, destStartYear, destIncludeArchived,
|
destReadOnly, destStartYear, destIncludeArchived,
|
||||||
useBuiltinAuth,
|
useBuiltinAuthSource, useBuiltinAuthDest,
|
||||||
archivePath, archiveEnabled, deleteAfterTransfer, skipProcessedFiles, maxConcurrentTransfers, rcloneFlags)
|
archivePath, archiveEnabled, deleteAfterTransfer, skipProcessedFiles, maxConcurrentTransfers, rcloneFlags)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,7 +354,7 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
|||||||
@source.NextCloudSourceForm()
|
@source.NextCloudSourceForm()
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template x-if="sourceType === 'google_drive'">
|
<template x-if="sourceType === 'gdrive'">
|
||||||
@source.GoogleDriveSourceForm()
|
@source.GoogleDriveSourceForm()
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ templ Configs(ctx context.Context, data ConfigsData) {
|
|||||||
<!-- Google Drive Authentication Button -->
|
<!-- Google Drive Authentication Button -->
|
||||||
if (config.DestinationType == "gdrive" || config.SourceType == "gdrive" || config.SourceType == "gphotos" || config.DestinationType == "gphotos") && !config.GetGoogleAuthenticated() {
|
if (config.DestinationType == "gdrive" || config.SourceType == "gdrive" || config.SourceType == "gphotos" || config.DestinationType == "gphotos") && !config.GetGoogleAuthenticated() {
|
||||||
<a href={ templ.SafeURL(fmt.Sprintf("/configs/%d/gdrive-auth", config.ID)) } class="btn-warning btn-sm">
|
<a href={ templ.SafeURL(fmt.Sprintf("/configs/%d/gdrive-auth", config.ID)) } class="btn-warning btn-sm">
|
||||||
<i class="fab fa-google-drive mr-1"></i>
|
<i class="fab fa-google mr-1"></i>
|
||||||
Authenticate
|
Authenticate
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
@@ -356,8 +356,8 @@ templ Configs(ctx context.Context, data ConfigsData) {
|
|||||||
<!-- Google Drive Auth Help -->
|
<!-- Google Drive Auth Help -->
|
||||||
<div class="mt-4 text-center">
|
<div class="mt-4 text-center">
|
||||||
<p class="text-sm text-secondary-500 dark:text-secondary-400">
|
<p class="text-sm text-secondary-500 dark:text-secondary-400">
|
||||||
<i class="fab fa-google-drive mr-1 text-blue-500 inline-flex items-center"></i>
|
<i class="fab fa-google mr-1 text-blue-500 inline-flex items-center"></i>
|
||||||
Google Drive configurations require authentication. Click the "Authenticate" button to complete setup.
|
Google Drive and Google Photos configurations require authentication. Click the "Authenticate" button to complete setup.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ templ SourceSelection() {
|
|||||||
<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="google_drive">Google Drive</option>
|
<option value="gdrive">Google Drive</option>
|
||||||
<option value="gphotos">Google Photos</option>
|
<option value="gphotos">Google Photos</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
package destination
|
package destination
|
||||||
|
|
||||||
templ GoogleDriveDestinationForm() {
|
templ GoogleDriveDestinationForm() {
|
||||||
<div class="space-y-6" x-init="$watch('useBuiltinAuth', value => {
|
<div class="space-y-6" x-init="$watch('useBuiltinAuthDest', value => {
|
||||||
if(value) {
|
if(value) {
|
||||||
destClientId = '';
|
destClientId = '';
|
||||||
destClientSecret = '';
|
destClientSecret = '';
|
||||||
}
|
}
|
||||||
})">
|
})">
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<label for="use_builtin_auth" class="flex items-center cursor-pointer">
|
<label for="use_builtin_auth_dest" class="flex items-center cursor-pointer">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<input id="use_builtin_auth" name="use_builtin_auth" type="checkbox"
|
<input id="use_builtin_auth_dest" name="use_builtin_auth_dest" type="checkbox"
|
||||||
class="sr-only"
|
class="sr-only"
|
||||||
x-model="useBuiltinAuth"
|
x-model="useBuiltinAuthDest"
|
||||||
|
:value="useBuiltinAuthDest ? 'true' : 'false'"
|
||||||
/>
|
/>
|
||||||
<div class="block bg-secondary-200 dark:bg-secondary-700 w-14 h-8 rounded-full"></div>
|
<div class="block bg-secondary-200 dark:bg-secondary-700 w-14 h-8 rounded-full"></div>
|
||||||
<div class="dot absolute left-1 top-1 bg-white dark:bg-secondary-100 w-6 h-6 rounded-full transition"
|
<div class="dot absolute left-1 top-1 bg-white dark:bg-secondary-100 w-6 h-6 rounded-full transition"
|
||||||
:class="useBuiltinAuth ? 'transform translate-x-6 bg-primary-500' : ''"></div>
|
:class="useBuiltinAuthDest ? 'transform translate-x-6 bg-primary-500' : ''"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-3 text-secondary-700 dark:text-secondary-300 font-medium">
|
<div class="ml-3 text-secondary-700 dark:text-secondary-300 font-medium">
|
||||||
Use rclone's built-in Google authentication (recommended)
|
Use rclone's built-in Google authentication (recommended)
|
||||||
@@ -27,11 +28,11 @@ templ GoogleDriveDestinationForm() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div x-bind:class="{ 'opacity-50': useBuiltinAuth }">
|
<div x-bind:class="{ 'opacity-50': useBuiltinAuthDest }">
|
||||||
<div>
|
<div>
|
||||||
<label for="dest_client_id" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
<label for="dest_client_id" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
||||||
Google Client ID
|
Google Client ID
|
||||||
<span x-show="useBuiltinAuth" class="text-secondary-400 dark:text-secondary-600 text-xs font-normal">(Using rclone default)</span>
|
<span x-show="useBuiltinAuthDest" class="text-secondary-400 dark:text-secondary-600 text-xs font-normal">(Using rclone default)</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
@@ -39,7 +40,7 @@ templ GoogleDriveDestinationForm() {
|
|||||||
</div>
|
</div>
|
||||||
<input type="text" id="dest_client_id" name="dest_client_id" x-model="destClientId"
|
<input type="text" id="dest_client_id" name="dest_client_id" x-model="destClientId"
|
||||||
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"
|
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"
|
||||||
x-bind:disabled="useBuiltinAuth"
|
x-bind:disabled="useBuiltinAuthDest"
|
||||||
placeholder="Google Drive OAuth Client ID" />
|
placeholder="Google Drive OAuth Client ID" />
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
||||||
@@ -50,7 +51,7 @@ templ GoogleDriveDestinationForm() {
|
|||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<label for="dest_client_secret" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
<label for="dest_client_secret" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
||||||
Google Client Secret
|
Google Client Secret
|
||||||
<span x-show="useBuiltinAuth" class="text-secondary-400 dark:text-secondary-600 text-xs font-normal">(Using rclone default)</span>
|
<span x-show="useBuiltinAuthDest" class="text-secondary-400 dark:text-secondary-600 text-xs font-normal">(Using rclone default)</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
@@ -58,7 +59,7 @@ templ GoogleDriveDestinationForm() {
|
|||||||
</div>
|
</div>
|
||||||
<input type="password" id="dest_client_secret" name="dest_client_secret" x-model="destClientSecret"
|
<input type="password" id="dest_client_secret" name="dest_client_secret" x-model="destClientSecret"
|
||||||
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"
|
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"
|
||||||
x-bind:disabled="useBuiltinAuth"
|
x-bind:disabled="useBuiltinAuthDest"
|
||||||
placeholder="Google Drive OAuth Client Secret" />
|
placeholder="Google Drive OAuth Client Secret" />
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
||||||
@@ -140,7 +141,7 @@ templ GoogleDriveDestinationForm() {
|
|||||||
<div class="ml-3">
|
<div class="ml-3">
|
||||||
<h3 class="text-sm font-medium text-blue-800 dark:text-blue-200">Authentication Information</h3>
|
<h3 class="text-sm font-medium text-blue-800 dark:text-blue-200">Authentication Information</h3>
|
||||||
<div class="mt-2 text-sm text-blue-700 dark:text-blue-300">
|
<div class="mt-2 text-sm text-blue-700 dark:text-blue-300">
|
||||||
<template x-if="useBuiltinAuth">
|
<template x-if="useBuiltinAuthDest">
|
||||||
<div>
|
<div>
|
||||||
<p>You're using rclone's built-in authentication, which simplifies the setup process:</p>
|
<p>You're using rclone's built-in authentication, which simplifies the setup process:</p>
|
||||||
<ul class="list-disc list-inside mt-1 space-y-1">
|
<ul class="list-disc list-inside mt-1 space-y-1">
|
||||||
@@ -155,7 +156,7 @@ templ GoogleDriveDestinationForm() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template x-if="!useBuiltinAuth">
|
<template x-if="!useBuiltinAuthDest">
|
||||||
<div>
|
<div>
|
||||||
<p>To use Google Drive with your own credentials:</p>
|
<p>To use Google Drive with your own credentials:</p>
|
||||||
<ol class="list-decimal list-inside mt-1 space-y-1">
|
<ol class="list-decimal list-inside mt-1 space-y-1">
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
package destination
|
package destination
|
||||||
|
|
||||||
templ GooglePhotosDestinationForm() {
|
templ GooglePhotosDestinationForm() {
|
||||||
<div class="space-y-6" x-init="$watch('useBuiltinAuth', value => {
|
<div class="space-y-6" x-init="$watch('useBuiltinAuthDest', value => {
|
||||||
if(value) {
|
if(value) {
|
||||||
destClientId = '';
|
destClientId = '';
|
||||||
destClientSecret = '';
|
destClientSecret = '';
|
||||||
}
|
}
|
||||||
})">
|
})">
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<label for="use_builtin_auth" class="flex items-center cursor-pointer">
|
<label for="use_builtin_auth_dest" class="flex items-center cursor-pointer">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<input id="use_builtin_auth" name="use_builtin_auth" type="checkbox"
|
<input id="use_builtin_auth_dest" name="use_builtin_auth_dest" type="checkbox"
|
||||||
class="sr-only"
|
class="sr-only"
|
||||||
x-model="useBuiltinAuth"
|
x-model="useBuiltinAuthDest"
|
||||||
|
:value="useBuiltinAuthDest ? 'true' : 'false'"
|
||||||
/>
|
/>
|
||||||
<div class="block bg-secondary-200 dark:bg-secondary-700 w-14 h-8 rounded-full"></div>
|
<div class="block bg-secondary-200 dark:bg-secondary-700 w-14 h-8 rounded-full"></div>
|
||||||
<div class="dot absolute left-1 top-1 bg-white dark:bg-secondary-100 w-6 h-6 rounded-full transition"
|
<div class="dot absolute left-1 top-1 bg-white dark:bg-secondary-100 w-6 h-6 rounded-full transition"
|
||||||
:class="useBuiltinAuth ? 'transform translate-x-6 bg-primary-500' : ''"></div>
|
:class="useBuiltinAuthDest ? 'transform translate-x-6 bg-primary-500' : ''"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-3 text-secondary-700 dark:text-secondary-300 font-medium">
|
<div class="ml-3 text-secondary-700 dark:text-secondary-300 font-medium">
|
||||||
Use rclone's built-in Google authentication (recommended)
|
Use rclone's built-in Google authentication (recommended)
|
||||||
@@ -27,11 +28,11 @@ templ GooglePhotosDestinationForm() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div x-bind:class="{ 'opacity-50': useBuiltinAuth }">
|
<div x-bind:class="{ 'opacity-50': useBuiltinAuthDest }">
|
||||||
<div>
|
<div>
|
||||||
<label for="dest_client_id" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
<label for="dest_client_id" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
||||||
Google Client ID
|
Google Client ID
|
||||||
<span x-show="useBuiltinAuth" class="text-secondary-400 dark:text-secondary-600 text-xs font-normal">(Using rclone default)</span>
|
<span x-show="useBuiltinAuthDest" class="text-secondary-400 dark:text-secondary-600 text-xs font-normal">(Using rclone default)</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
@@ -39,7 +40,7 @@ templ GooglePhotosDestinationForm() {
|
|||||||
</div>
|
</div>
|
||||||
<input type="text" id="dest_client_id" name="dest_client_id" x-model="destClientId"
|
<input type="text" id="dest_client_id" name="dest_client_id" x-model="destClientId"
|
||||||
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"
|
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"
|
||||||
x-bind:disabled="useBuiltinAuth"
|
x-bind:disabled="useBuiltinAuthDest"
|
||||||
placeholder="Google Photos OAuth Client ID" />
|
placeholder="Google Photos OAuth Client ID" />
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
||||||
@@ -50,7 +51,7 @@ templ GooglePhotosDestinationForm() {
|
|||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<label for="dest_client_secret" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
<label for="dest_client_secret" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
||||||
Google Client Secret
|
Google Client Secret
|
||||||
<span x-show="useBuiltinAuth" class="text-secondary-400 dark:text-secondary-600 text-xs font-normal">(Using rclone default)</span>
|
<span x-show="useBuiltinAuthDest" class="text-secondary-400 dark:text-secondary-600 text-xs font-normal">(Using rclone default)</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
@@ -58,7 +59,7 @@ templ GooglePhotosDestinationForm() {
|
|||||||
</div>
|
</div>
|
||||||
<input type="password" id="dest_client_secret" name="dest_client_secret" x-model="destClientSecret"
|
<input type="password" id="dest_client_secret" name="dest_client_secret" x-model="destClientSecret"
|
||||||
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"
|
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"
|
||||||
x-bind:disabled="useBuiltinAuth"
|
x-bind:disabled="useBuiltinAuthDest"
|
||||||
placeholder="Google Photos OAuth Client Secret" />
|
placeholder="Google Photos OAuth Client Secret" />
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
||||||
@@ -130,10 +131,10 @@ templ GooglePhotosDestinationForm() {
|
|||||||
</div>
|
</div>
|
||||||
<input type="text" id="destination_path" name="destination_path" x-model="destinationPath"
|
<input type="text" id="destination_path" name="destination_path" x-model="destinationPath"
|
||||||
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"
|
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="Album path (e.g., /album/my-photos)" />
|
placeholder="Album path (e.g., album/my-photos)" />
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
||||||
Path within Google Photos where files will be uploaded
|
Path within Google Photos where files will be uploaded.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -165,7 +166,7 @@ templ GooglePhotosDestinationForm() {
|
|||||||
<div class="ml-3">
|
<div class="ml-3">
|
||||||
<h3 class="text-sm font-medium text-blue-800 dark:text-blue-200">Authentication Information</h3>
|
<h3 class="text-sm font-medium text-blue-800 dark:text-blue-200">Authentication Information</h3>
|
||||||
<div class="mt-2 text-sm text-blue-700 dark:text-blue-300">
|
<div class="mt-2 text-sm text-blue-700 dark:text-blue-300">
|
||||||
<template x-if="useBuiltinAuth">
|
<template x-if="useBuiltinAuthDest">
|
||||||
<div>
|
<div>
|
||||||
<p>You're using rclone's built-in authentication, which simplifies the setup process:</p>
|
<p>You're using rclone's built-in authentication, which simplifies the setup process:</p>
|
||||||
<ul class="list-disc list-inside mt-1 space-y-1">
|
<ul class="list-disc list-inside mt-1 space-y-1">
|
||||||
@@ -180,7 +181,7 @@ templ GooglePhotosDestinationForm() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template x-if="!useBuiltinAuth">
|
<template x-if="!useBuiltinAuthDest">
|
||||||
<div>
|
<div>
|
||||||
<p>To use Google Photos with your own credentials:</p>
|
<p>To use Google Photos with your own credentials:</p>
|
||||||
<ol class="list-decimal list-inside mt-1 space-y-1">
|
<ol class="list-decimal list-inside mt-1 space-y-1">
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ templ LocalDestinationForm() {
|
|||||||
<label for="destination_path" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Local Path</label>
|
<label for="destination_path" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Local Path</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
<i class="fas fa-folder text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-folder-open text-secondary-400 dark:text-secondary-600"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -14,13 +14,26 @@ templ LocalDestinationForm() {
|
|||||||
id="destination_path"
|
id="destination_path"
|
||||||
x-model="destinationPath"
|
x-model="destinationPath"
|
||||||
required
|
required
|
||||||
aria-describedby="destination_path_help"
|
:class="{ 'border-red-300 dark:border-red-700': destPathValid === false, 'border-green-300 dark:border-green-700': destPathValid === true }"
|
||||||
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"
|
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/destination"/>
|
placeholder="Local directory path (e.g., /path/to/destination)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p id="destination_path_help" class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
||||||
Absolute path to the local directory where files will be saved.
|
Full path to the local directory where files will be saved
|
||||||
</p>
|
</p>
|
||||||
|
<!-- Path validation message -->
|
||||||
|
<template x-if="destPathValid !== null">
|
||||||
|
<p x-show="destPathError" class="mt-1 text-sm" :class="destPathValid ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'" x-text="destPathError"></p>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-secondary h-10 whitespace-nowrap"
|
||||||
|
@click="checkPath(destinationPath, 'dest')"
|
||||||
|
>
|
||||||
|
<i class="fas fa-check-circle mr-2"></i>
|
||||||
|
Check Location
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package source
|
package source
|
||||||
|
|
||||||
templ GoogleDriveSourceForm() {
|
templ GoogleDriveSourceForm() {
|
||||||
<div class="space-y-6" x-init="$watch('useBuiltinAuth', value => {
|
<div class="space-y-6" x-init="$watch('useBuiltinAuthSource', value => {
|
||||||
if(value) {
|
if(value) {
|
||||||
sourceClientId = '';
|
sourceClientId = '';
|
||||||
sourceClientSecret = '';
|
sourceClientSecret = '';
|
||||||
@@ -12,11 +12,12 @@ templ GoogleDriveSourceForm() {
|
|||||||
<div class="relative">
|
<div class="relative">
|
||||||
<input id="use_builtin_auth_source" name="use_builtin_auth_source" type="checkbox"
|
<input id="use_builtin_auth_source" name="use_builtin_auth_source" type="checkbox"
|
||||||
class="sr-only"
|
class="sr-only"
|
||||||
x-model="useBuiltinAuth"
|
x-model="useBuiltinAuthSource"
|
||||||
|
:value="useBuiltinAuthSource ? 'true' : 'false'"
|
||||||
/>
|
/>
|
||||||
<div class="block bg-secondary-200 dark:bg-secondary-700 w-14 h-8 rounded-full"></div>
|
<div class="block bg-secondary-200 dark:bg-secondary-700 w-14 h-8 rounded-full"></div>
|
||||||
<div class="dot absolute left-1 top-1 bg-white dark:bg-secondary-100 w-6 h-6 rounded-full transition"
|
<div class="dot absolute left-1 top-1 bg-white dark:bg-secondary-100 w-6 h-6 rounded-full transition"
|
||||||
:class="useBuiltinAuth ? 'transform translate-x-6 bg-primary-500' : ''"></div>
|
:class="useBuiltinAuthSource ? 'transform translate-x-6 bg-primary-500' : ''"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-3 text-secondary-700 dark:text-secondary-300 font-medium">
|
<div class="ml-3 text-secondary-700 dark:text-secondary-300 font-medium">
|
||||||
Use rclone's built-in Google authentication (recommended)
|
Use rclone's built-in Google authentication (recommended)
|
||||||
@@ -27,11 +28,11 @@ templ GoogleDriveSourceForm() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div x-bind:class="{ 'opacity-50': useBuiltinAuth }">
|
<div x-bind:class="{ 'opacity-50': useBuiltinAuthSource }">
|
||||||
<div>
|
<div>
|
||||||
<label for="source_client_id" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
<label for="source_client_id" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
||||||
Google Client ID
|
Google Client ID
|
||||||
<span x-show="useBuiltinAuth" class="text-secondary-400 dark:text-secondary-600 text-xs font-normal">(Using rclone default)</span>
|
<span x-show="useBuiltinAuthSource" class="text-secondary-400 dark:text-secondary-600 text-xs font-normal">(Using rclone default)</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
@@ -39,7 +40,7 @@ templ GoogleDriveSourceForm() {
|
|||||||
</div>
|
</div>
|
||||||
<input type="text" id="source_client_id" name="source_client_id" x-model="sourceClientId"
|
<input type="text" id="source_client_id" name="source_client_id" x-model="sourceClientId"
|
||||||
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"
|
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"
|
||||||
x-bind:disabled="useBuiltinAuth"
|
x-bind:disabled="useBuiltinAuthSource"
|
||||||
placeholder="Google Drive OAuth Client ID" />
|
placeholder="Google Drive OAuth Client ID" />
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
||||||
@@ -50,7 +51,7 @@ templ GoogleDriveSourceForm() {
|
|||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<label for="source_client_secret" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
<label for="source_client_secret" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
||||||
Google Client Secret
|
Google Client Secret
|
||||||
<span x-show="useBuiltinAuth" class="text-secondary-400 dark:text-secondary-600 text-xs font-normal">(Using rclone default)</span>
|
<span x-show="useBuiltinAuthSource" class="text-secondary-400 dark:text-secondary-600 text-xs font-normal">(Using rclone default)</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
@@ -58,7 +59,7 @@ templ GoogleDriveSourceForm() {
|
|||||||
</div>
|
</div>
|
||||||
<input type="password" id="source_client_secret" name="source_client_secret" x-model="sourceClientSecret"
|
<input type="password" id="source_client_secret" name="source_client_secret" x-model="sourceClientSecret"
|
||||||
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"
|
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"
|
||||||
x-bind:disabled="useBuiltinAuth"
|
x-bind:disabled="useBuiltinAuthSource"
|
||||||
placeholder="Google Drive OAuth Client Secret" />
|
placeholder="Google Drive OAuth Client Secret" />
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
||||||
@@ -140,7 +141,7 @@ templ GoogleDriveSourceForm() {
|
|||||||
<div class="ml-3">
|
<div class="ml-3">
|
||||||
<h3 class="text-sm font-medium text-blue-800 dark:text-blue-200">Authentication Information</h3>
|
<h3 class="text-sm font-medium text-blue-800 dark:text-blue-200">Authentication Information</h3>
|
||||||
<div class="mt-2 text-sm text-blue-700 dark:text-blue-300">
|
<div class="mt-2 text-sm text-blue-700 dark:text-blue-300">
|
||||||
<template x-if="useBuiltinAuth">
|
<template x-if="useBuiltinAuthSource">
|
||||||
<div>
|
<div>
|
||||||
<p>You're using rclone's built-in authentication, which simplifies the setup process:</p>
|
<p>You're using rclone's built-in authentication, which simplifies the setup process:</p>
|
||||||
<ul class="list-disc list-inside mt-1 space-y-1">
|
<ul class="list-disc list-inside mt-1 space-y-1">
|
||||||
@@ -155,7 +156,7 @@ templ GoogleDriveSourceForm() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template x-if="!useBuiltinAuth">
|
<template x-if="!useBuiltinAuthSource">
|
||||||
<div>
|
<div>
|
||||||
<p>To use Google Drive with your own credentials:</p>
|
<p>To use Google Drive with your own credentials:</p>
|
||||||
<ol class="list-decimal list-inside mt-1 space-y-1">
|
<ol class="list-decimal list-inside mt-1 space-y-1">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package source
|
package source
|
||||||
|
|
||||||
templ GooglePhotosSourceForm() {
|
templ GooglePhotosSourceForm() {
|
||||||
<div class="space-y-6" x-init="$watch('useBuiltinAuth', value => {
|
<div class="space-y-6" x-init="$watch('useBuiltinAuthSource', value => {
|
||||||
if(value) {
|
if(value) {
|
||||||
sourceClientId = '';
|
sourceClientId = '';
|
||||||
sourceClientSecret = '';
|
sourceClientSecret = '';
|
||||||
@@ -12,11 +12,12 @@ templ GooglePhotosSourceForm() {
|
|||||||
<div class="relative">
|
<div class="relative">
|
||||||
<input id="use_builtin_auth_source" name="use_builtin_auth_source" type="checkbox"
|
<input id="use_builtin_auth_source" name="use_builtin_auth_source" type="checkbox"
|
||||||
class="sr-only"
|
class="sr-only"
|
||||||
x-model="useBuiltinAuth"
|
x-model="useBuiltinAuthSource"
|
||||||
|
:value="useBuiltinAuthSource ? 'true' : 'false'"
|
||||||
/>
|
/>
|
||||||
<div class="block bg-secondary-200 dark:bg-secondary-700 w-14 h-8 rounded-full"></div>
|
<div class="block bg-secondary-200 dark:bg-secondary-700 w-14 h-8 rounded-full"></div>
|
||||||
<div class="dot absolute left-1 top-1 bg-white dark:bg-secondary-100 w-6 h-6 rounded-full transition"
|
<div class="dot absolute left-1 top-1 bg-white dark:bg-secondary-100 w-6 h-6 rounded-full transition"
|
||||||
:class="useBuiltinAuth ? 'transform translate-x-6 bg-primary-500' : ''"></div>
|
:class="useBuiltinAuthSource ? 'transform translate-x-6 bg-primary-500' : ''"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-3 text-secondary-700 dark:text-secondary-300 font-medium">
|
<div class="ml-3 text-secondary-700 dark:text-secondary-300 font-medium">
|
||||||
Use rclone's built-in Google authentication (recommended)
|
Use rclone's built-in Google authentication (recommended)
|
||||||
@@ -27,11 +28,11 @@ templ GooglePhotosSourceForm() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div x-bind:class="{ 'opacity-50': useBuiltinAuth }">
|
<div x-bind:class="{ 'opacity-50': useBuiltinAuthSource }">
|
||||||
<div>
|
<div>
|
||||||
<label for="source_client_id" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
<label for="source_client_id" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
||||||
Google Client ID
|
Google Client ID
|
||||||
<span x-show="useBuiltinAuth" class="text-secondary-400 dark:text-secondary-600 text-xs font-normal">(Using rclone default)</span>
|
<span x-show="useBuiltinAuthSource" class="text-secondary-400 dark:text-secondary-600 text-xs font-normal">(Using rclone default)</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
@@ -39,7 +40,7 @@ templ GooglePhotosSourceForm() {
|
|||||||
</div>
|
</div>
|
||||||
<input type="text" id="source_client_id" name="source_client_id" x-model="sourceClientId"
|
<input type="text" id="source_client_id" name="source_client_id" x-model="sourceClientId"
|
||||||
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"
|
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"
|
||||||
x-bind:disabled="useBuiltinAuth"
|
x-bind:disabled="useBuiltinAuthSource"
|
||||||
placeholder="Google Photos OAuth Client ID" />
|
placeholder="Google Photos OAuth Client ID" />
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
||||||
@@ -50,7 +51,7 @@ templ GooglePhotosSourceForm() {
|
|||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<label for="source_client_secret" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
<label for="source_client_secret" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
||||||
Google Client Secret
|
Google Client Secret
|
||||||
<span x-show="useBuiltinAuth" class="text-secondary-400 dark:text-secondary-600 text-xs font-normal">(Using rclone default)</span>
|
<span x-show="useBuiltinAuthSource" class="text-secondary-400 dark:text-secondary-600 text-xs font-normal">(Using rclone default)</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
@@ -58,7 +59,7 @@ templ GooglePhotosSourceForm() {
|
|||||||
</div>
|
</div>
|
||||||
<input type="password" id="source_client_secret" name="source_client_secret" x-model="sourceClientSecret"
|
<input type="password" id="source_client_secret" name="source_client_secret" x-model="sourceClientSecret"
|
||||||
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"
|
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"
|
||||||
x-bind:disabled="useBuiltinAuth"
|
x-bind:disabled="useBuiltinAuthSource"
|
||||||
placeholder="Google Photos OAuth Client Secret" />
|
placeholder="Google Photos OAuth Client Secret" />
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
||||||
@@ -130,7 +131,7 @@ templ GooglePhotosSourceForm() {
|
|||||||
</div>
|
</div>
|
||||||
<input type="text" id="source_path" name="source_path" x-model="sourcePath"
|
<input type="text" id="source_path" name="source_path" x-model="sourcePath"
|
||||||
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"
|
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="Album path (e.g., /album/my-photos)" />
|
placeholder="Album path (e.g., album/my-photos)" />
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
||||||
Path within Google Photos to download files from
|
Path within Google Photos to download files from
|
||||||
@@ -165,7 +166,7 @@ templ GooglePhotosSourceForm() {
|
|||||||
<div class="ml-3">
|
<div class="ml-3">
|
||||||
<h3 class="text-sm font-medium text-blue-800 dark:text-blue-200">Authentication Information</h3>
|
<h3 class="text-sm font-medium text-blue-800 dark:text-blue-200">Authentication Information</h3>
|
||||||
<div class="mt-2 text-sm text-blue-700 dark:text-blue-300">
|
<div class="mt-2 text-sm text-blue-700 dark:text-blue-300">
|
||||||
<template x-if="useBuiltinAuth">
|
<template x-if="useBuiltinAuthSource">
|
||||||
<div>
|
<div>
|
||||||
<p>You're using rclone's built-in authentication, which simplifies the setup process:</p>
|
<p>You're using rclone's built-in authentication, which simplifies the setup process:</p>
|
||||||
<ul class="list-disc list-inside mt-1 space-y-1">
|
<ul class="list-disc list-inside mt-1 space-y-1">
|
||||||
@@ -180,7 +181,7 @@ templ GooglePhotosSourceForm() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template x-if="!useBuiltinAuth">
|
<template x-if="!useBuiltinAuthSource">
|
||||||
<div>
|
<div>
|
||||||
<p>To use Google Photos with your own credentials:</p>
|
<p>To use Google Photos with your own credentials:</p>
|
||||||
<ol class="list-decimal list-inside mt-1 space-y-1">
|
<ol class="list-decimal list-inside mt-1 space-y-1">
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ templ LocalSourceForm() {
|
|||||||
<label for="source_path" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Local Path</label>
|
<label for="source_path" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Local Path</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
<i class="fas fa-folder text-secondary-400 dark:text-secondary-600"></i>
|
<i class="fas fa-folder-open text-secondary-400 dark:text-secondary-600"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -14,13 +14,26 @@ templ LocalSourceForm() {
|
|||||||
id="source_path"
|
id="source_path"
|
||||||
x-model="sourcePath"
|
x-model="sourcePath"
|
||||||
required
|
required
|
||||||
aria-describedby="source_path_help"
|
:class="{ 'border-red-300 dark:border-red-700': sourcePathValid === false, 'border-green-300 dark:border-green-700': sourcePathValid === true }"
|
||||||
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"
|
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/source"/>
|
placeholder="Local directory path (e.g., /path/to/files)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p id="source_path_help" class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
||||||
Absolute path to the local directory containing the files to transfer.
|
Full path to the local directory containing your files
|
||||||
</p>
|
</p>
|
||||||
|
<!-- Path validation message -->
|
||||||
|
<template x-if="sourcePathValid !== null">
|
||||||
|
<p x-show="sourcePathError" class="mt-1 text-sm" :class="sourcePathValid ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'" x-text="sourcePathError"></p>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-secondary h-10 whitespace-nowrap"
|
||||||
|
@click="checkPath(sourcePath, 'source')"
|
||||||
|
>
|
||||||
|
<i class="fas fa-check-circle mr-2"></i>
|
||||||
|
Check Location
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
+1
-1
@@ -711,7 +711,7 @@ func handleListHistory(database *db.DB) gin.HandlerFunc {
|
|||||||
// For now, just return the most recent 100 history entries for the user's jobs
|
// For now, just return the most recent 100 history entries for the user's jobs
|
||||||
var history []db.JobHistory
|
var history []db.JobHistory
|
||||||
err := database.DB.
|
err := database.DB.
|
||||||
Joins("JOIN jobs ON job_histories.job_id = jobs.id").
|
Joins("JOIN jobs ON job_history.job_id = jobs.id").
|
||||||
Where("jobs.created_by = ?", userID).
|
Where("jobs.created_by = ?", userID).
|
||||||
Order("start_time DESC").
|
Order("start_time DESC").
|
||||||
Limit(100).
|
Limit(100).
|
||||||
|
|||||||
+34
-25
@@ -11,7 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/glebarez/sqlite"
|
"github.com/glebarez/sqlite"
|
||||||
"github.com/starfleetcptn/gomft/internal/auth"
|
"github.com/starfleetcptn/gomft/internal/db/migrations"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -109,7 +109,8 @@ type TransferConfig struct {
|
|||||||
DestStartYear int `form:"dest_start_year"` // For Google Photos
|
DestStartYear int `form:"dest_start_year"` // For Google Photos
|
||||||
DestIncludeArchived *bool `form:"dest_include_archived"` // For Google Photos
|
DestIncludeArchived *bool `form:"dest_include_archived"` // For Google Photos
|
||||||
// Security fields
|
// Security fields
|
||||||
UseBuiltinAuth *bool `form:"use_builtin_auth"` // For Google and other OAuth services
|
UseBuiltinAuthSource *bool `form:"use_builtin_auth_source"` // For Google and other OAuth services
|
||||||
|
UseBuiltinAuthDest *bool `form:"use_builtin_auth_dest"` // For Google and other OAuth services
|
||||||
GoogleDriveAuthenticated *bool // Whether Google Drive auth is completed
|
GoogleDriveAuthenticated *bool // Whether Google Drive auth is completed
|
||||||
// General fields
|
// General fields
|
||||||
ArchivePath string `form:"archive_path"`
|
ArchivePath string `form:"archive_path"`
|
||||||
@@ -250,10 +251,10 @@ func Initialize(dbPath string) (*DB, error) {
|
|||||||
return nil, fmt.Errorf("failed to connect to database: %v", err)
|
return nil, fmt.Errorf("failed to connect to database: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto migrate the schema
|
// Initialize and run migrations
|
||||||
err = db.AutoMigrate(&User{}, &auth.PasswordHistory{}, &PasswordResetToken{}, &TransferConfig{}, &Job{}, &JobHistory{}, &FileMetadata{})
|
m := migrations.InitMigrations(db)
|
||||||
if err != nil {
|
if err := m.Migrate(); err != nil {
|
||||||
return nil, fmt.Errorf("failed to migrate database: %v", err)
|
return nil, fmt.Errorf("failed to run migrations: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &DB{DB: db}, nil
|
return &DB{DB: db}, nil
|
||||||
@@ -627,7 +628,7 @@ 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: %v\nOutput: %s", err, output)
|
return fmt.Errorf("failed to create source config: %v\nOutput: %s", err, output)
|
||||||
}
|
}
|
||||||
case "google_drive":
|
case "gdrive":
|
||||||
args := []string{
|
args := []string{
|
||||||
"config", "create", sourceName, "drive",
|
"config", "create", sourceName, "drive",
|
||||||
"client_id", config.SourceClientID,
|
"client_id", config.SourceClientID,
|
||||||
@@ -906,24 +907,6 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
|
|||||||
args = append(args, "include_archived", "true")
|
args = append(args, "include_archived", "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 "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...)
|
cmd := exec.Command(rclonePath, args...)
|
||||||
if output, err := cmd.CombinedOutput(); err != nil {
|
if output, err := cmd.CombinedOutput(); err != nil {
|
||||||
return fmt.Errorf("failed to create destination config: %v\nOutput: %s", err, output)
|
return fmt.Errorf("failed to create destination config: %v\nOutput: %s", err, output)
|
||||||
@@ -1461,3 +1444,29 @@ func (db *DB) GetGDriveCredentialsFromConfig(config *TransferConfig) (string, st
|
|||||||
|
|
||||||
return "", ""
|
return "", ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUseBuiltinAuthSource returns the value of UseBuiltinAuthSource with a default if nil
|
||||||
|
func (tc *TransferConfig) GetUseBuiltinAuthSource() bool {
|
||||||
|
if tc.UseBuiltinAuthSource == nil {
|
||||||
|
return true // Default to true if not set
|
||||||
|
}
|
||||||
|
return *tc.UseBuiltinAuthSource
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUseBuiltinAuthSource sets the UseBuiltinAuthSource field
|
||||||
|
func (tc *TransferConfig) SetUseBuiltinAuthSource(value bool) {
|
||||||
|
tc.UseBuiltinAuthSource = &value
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUseBuiltinAuthDest returns the value of UseBuiltinAuthDest with a default if nil
|
||||||
|
func (tc *TransferConfig) GetUseBuiltinAuthDest() bool {
|
||||||
|
if tc.UseBuiltinAuthDest == nil {
|
||||||
|
return true // Default to true if not set
|
||||||
|
}
|
||||||
|
return *tc.UseBuiltinAuthDest
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUseBuiltinAuthDest sets the UseBuiltinAuthDest field
|
||||||
|
func (tc *TransferConfig) SetUseBuiltinAuthDest(value bool) {
|
||||||
|
tc.UseBuiltinAuthDest = &value
|
||||||
|
}
|
||||||
+29
-26
@@ -853,7 +853,7 @@ func TestGoogleDriveTransferConfig(t *testing.T) {
|
|||||||
// Test 1: Create config with Google Drive as source
|
// Test 1: Create config with Google Drive as source
|
||||||
googleSourceConfig := &TransferConfig{
|
googleSourceConfig := &TransferConfig{
|
||||||
Name: "Google Drive Source Test",
|
Name: "Google Drive Source Test",
|
||||||
SourceType: "google_drive",
|
SourceType: "gdrive",
|
||||||
SourcePath: "/path/in/google/drive",
|
SourcePath: "/path/in/google/drive",
|
||||||
SourceClientID: "google_client_id",
|
SourceClientID: "google_client_id",
|
||||||
SourceClientSecret: "google_client_secret",
|
SourceClientSecret: "google_client_secret",
|
||||||
@@ -878,7 +878,7 @@ func TestGoogleDriveTransferConfig(t *testing.T) {
|
|||||||
Name: "Google Drive Destination Test",
|
Name: "Google Drive Destination Test",
|
||||||
SourceType: "local",
|
SourceType: "local",
|
||||||
SourcePath: "/local/source/path",
|
SourcePath: "/local/source/path",
|
||||||
DestinationType: "google_drive",
|
DestinationType: "gdrive",
|
||||||
DestinationPath: "/path/in/google/drive",
|
DestinationPath: "/path/in/google/drive",
|
||||||
DestClientID: "google_client_id",
|
DestClientID: "google_client_id",
|
||||||
DestClientSecret: "google_client_secret",
|
DestClientSecret: "google_client_secret",
|
||||||
@@ -898,12 +898,12 @@ func TestGoogleDriveTransferConfig(t *testing.T) {
|
|||||||
// Test 3: Create config with Google Drive as both source and destination
|
// Test 3: Create config with Google Drive as both source and destination
|
||||||
googleBothConfig := &TransferConfig{
|
googleBothConfig := &TransferConfig{
|
||||||
Name: "Google Drive Both Test",
|
Name: "Google Drive Both Test",
|
||||||
SourceType: "google_drive",
|
SourceType: "gdrive",
|
||||||
SourcePath: "/source/path/in/google/drive",
|
SourcePath: "/source/path/in/google/drive",
|
||||||
SourceClientID: "source_google_client_id",
|
SourceClientID: "source_google_client_id",
|
||||||
SourceClientSecret: "source_google_client_secret",
|
SourceClientSecret: "source_google_client_secret",
|
||||||
SourceTeamDrive: "source_team_drive_id",
|
SourceTeamDrive: "source_team_drive_id",
|
||||||
DestinationType: "google_drive",
|
DestinationType: "gdrive",
|
||||||
DestinationPath: "/dest/path/in/google/drive",
|
DestinationPath: "/dest/path/in/google/drive",
|
||||||
DestClientID: "dest_google_client_id",
|
DestClientID: "dest_google_client_id",
|
||||||
DestClientSecret: "dest_google_client_secret",
|
DestClientSecret: "dest_google_client_secret",
|
||||||
@@ -923,7 +923,7 @@ func TestGoogleDriveTransferConfig(t *testing.T) {
|
|||||||
// Test retrieving and verifying Google Drive configs
|
// Test retrieving and verifying Google Drive configs
|
||||||
retrievedSourceConfig, err := db.GetTransferConfig(googleSourceConfig.ID)
|
retrievedSourceConfig, err := db.GetTransferConfig(googleSourceConfig.ID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "google_drive", retrievedSourceConfig.SourceType)
|
assert.Equal(t, "gdrive", retrievedSourceConfig.SourceType)
|
||||||
assert.Equal(t, "/path/in/google/drive", retrievedSourceConfig.SourcePath)
|
assert.Equal(t, "/path/in/google/drive", retrievedSourceConfig.SourcePath)
|
||||||
assert.Equal(t, "google_client_id", retrievedSourceConfig.SourceClientID)
|
assert.Equal(t, "google_client_id", retrievedSourceConfig.SourceClientID)
|
||||||
assert.Equal(t, "team_drive_id", retrievedSourceConfig.SourceTeamDrive)
|
assert.Equal(t, "team_drive_id", retrievedSourceConfig.SourceTeamDrive)
|
||||||
@@ -931,7 +931,7 @@ func TestGoogleDriveTransferConfig(t *testing.T) {
|
|||||||
|
|
||||||
retrievedDestConfig, err := db.GetTransferConfig(googleDestConfig.ID)
|
retrievedDestConfig, err := db.GetTransferConfig(googleDestConfig.ID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "google_drive", retrievedDestConfig.DestinationType)
|
assert.Equal(t, "gdrive", retrievedDestConfig.DestinationType)
|
||||||
assert.Equal(t, "/path/in/google/drive", retrievedDestConfig.DestinationPath)
|
assert.Equal(t, "/path/in/google/drive", retrievedDestConfig.DestinationPath)
|
||||||
assert.Equal(t, "google_client_id", retrievedDestConfig.DestClientID)
|
assert.Equal(t, "google_client_id", retrievedDestConfig.DestClientID)
|
||||||
assert.Equal(t, "team_drive_id", retrievedDestConfig.DestTeamDrive)
|
assert.Equal(t, "team_drive_id", retrievedDestConfig.DestTeamDrive)
|
||||||
@@ -986,7 +986,7 @@ func TestGoogleDriveJobExecution(t *testing.T) {
|
|||||||
// Create a test transfer config with Google Drive as source
|
// Create a test transfer config with Google Drive as source
|
||||||
googleConfig := &TransferConfig{
|
googleConfig := &TransferConfig{
|
||||||
Name: "Google Drive Job Test",
|
Name: "Google Drive Job Test",
|
||||||
SourceType: "google_drive",
|
SourceType: "gdrive",
|
||||||
SourcePath: "/source/path/in/google/drive",
|
SourcePath: "/source/path/in/google/drive",
|
||||||
SourceClientID: "google_client_id",
|
SourceClientID: "google_client_id",
|
||||||
SourceClientSecret: "google_client_secret",
|
SourceClientSecret: "google_client_secret",
|
||||||
@@ -1149,7 +1149,7 @@ func TestGoogleDriveAuthentication(t *testing.T) {
|
|||||||
// Create a Google Drive config that requires authentication
|
// Create a Google Drive config that requires authentication
|
||||||
googleConfig := &TransferConfig{
|
googleConfig := &TransferConfig{
|
||||||
Name: "Google Drive Auth Test",
|
Name: "Google Drive Auth Test",
|
||||||
SourceType: "google_drive",
|
SourceType: "gdrive",
|
||||||
SourcePath: "/source/path",
|
SourcePath: "/source/path",
|
||||||
SourceClientID: "test_client_id",
|
SourceClientID: "test_client_id",
|
||||||
SourceClientSecret: "test_client_secret",
|
SourceClientSecret: "test_client_secret",
|
||||||
@@ -1234,7 +1234,7 @@ func TestGoogleDriveErrorHandling(t *testing.T) {
|
|||||||
// Test 1: Create a config with missing required fields
|
// Test 1: Create a config with missing required fields
|
||||||
incompleteConfig := &TransferConfig{
|
incompleteConfig := &TransferConfig{
|
||||||
Name: "Incomplete Google Drive Config",
|
Name: "Incomplete Google Drive Config",
|
||||||
SourceType: "google_drive",
|
SourceType: "gdrive",
|
||||||
SourcePath: "", // Missing path
|
SourcePath: "", // Missing path
|
||||||
DestinationType: "local",
|
DestinationType: "local",
|
||||||
DestinationPath: "/local/path",
|
DestinationPath: "/local/path",
|
||||||
@@ -1249,7 +1249,7 @@ func TestGoogleDriveErrorHandling(t *testing.T) {
|
|||||||
// Test 2: Config with invalid Team Drive ID
|
// Test 2: Config with invalid Team Drive ID
|
||||||
invalidTeamDriveConfig := &TransferConfig{
|
invalidTeamDriveConfig := &TransferConfig{
|
||||||
Name: "Invalid Team Drive Config",
|
Name: "Invalid Team Drive Config",
|
||||||
SourceType: "google_drive",
|
SourceType: "gdrive",
|
||||||
SourcePath: "/test/path",
|
SourcePath: "/test/path",
|
||||||
SourceClientID: "test_client_id",
|
SourceClientID: "test_client_id",
|
||||||
SourceClientSecret: "test_client_secret",
|
SourceClientSecret: "test_client_secret",
|
||||||
@@ -1279,7 +1279,7 @@ func TestGoogleDriveErrorHandling(t *testing.T) {
|
|||||||
// Test 3: Test authentication error scenario - using malformed token
|
// Test 3: Test authentication error scenario - using malformed token
|
||||||
badTokenConfig := &TransferConfig{
|
badTokenConfig := &TransferConfig{
|
||||||
Name: "Bad Token Config",
|
Name: "Bad Token Config",
|
||||||
SourceType: "google_drive",
|
SourceType: "gdrive",
|
||||||
SourcePath: "/test/path",
|
SourcePath: "/test/path",
|
||||||
SourceClientID: "test_client_id",
|
SourceClientID: "test_client_id",
|
||||||
SourceClientSecret: "test_client_secret",
|
SourceClientSecret: "test_client_secret",
|
||||||
@@ -1325,7 +1325,7 @@ func TestGoogleDriveTeamDrive(t *testing.T) {
|
|||||||
// Test 1: Configure source with Team Drive
|
// Test 1: Configure source with Team Drive
|
||||||
teamDriveSourceConfig := &TransferConfig{
|
teamDriveSourceConfig := &TransferConfig{
|
||||||
Name: "Team Drive Source Test",
|
Name: "Team Drive Source Test",
|
||||||
SourceType: "google_drive",
|
SourceType: "gdrive",
|
||||||
SourcePath: "/shared/documents",
|
SourcePath: "/shared/documents",
|
||||||
SourceClientID: "test_client_id",
|
SourceClientID: "test_client_id",
|
||||||
SourceClientSecret: "test_client_secret",
|
SourceClientSecret: "test_client_secret",
|
||||||
@@ -1350,7 +1350,7 @@ func TestGoogleDriveTeamDrive(t *testing.T) {
|
|||||||
Name: "Team Drive Destination Test",
|
Name: "Team Drive Destination Test",
|
||||||
SourceType: "local",
|
SourceType: "local",
|
||||||
SourcePath: "/local/source",
|
SourcePath: "/local/source",
|
||||||
DestinationType: "google_drive",
|
DestinationType: "gdrive",
|
||||||
DestinationPath: "/team/drive/path",
|
DestinationPath: "/team/drive/path",
|
||||||
DestClientID: "test_client_id",
|
DestClientID: "test_client_id",
|
||||||
DestClientSecret: "test_client_secret",
|
DestClientSecret: "test_client_secret",
|
||||||
@@ -1370,12 +1370,12 @@ func TestGoogleDriveTeamDrive(t *testing.T) {
|
|||||||
// Test 3: Configure both source and destination with Team Drive
|
// Test 3: Configure both source and destination with Team Drive
|
||||||
teamDriveBothConfig := &TransferConfig{
|
teamDriveBothConfig := &TransferConfig{
|
||||||
Name: "Team Drive Both Test",
|
Name: "Team Drive Both Test",
|
||||||
SourceType: "google_drive",
|
SourceType: "gdrive",
|
||||||
SourcePath: "/source/team/drive/path",
|
SourcePath: "/source/team/drive/path",
|
||||||
SourceClientID: "source_client_id",
|
SourceClientID: "source_client_id",
|
||||||
SourceClientSecret: "source_client_secret",
|
SourceClientSecret: "source_client_secret",
|
||||||
SourceTeamDrive: "source_team_drive_id",
|
SourceTeamDrive: "source_team_drive_id",
|
||||||
DestinationType: "google_drive",
|
DestinationType: "gdrive",
|
||||||
DestinationPath: "/dest/team/drive/path",
|
DestinationPath: "/dest/team/drive/path",
|
||||||
DestClientID: "dest_client_id",
|
DestClientID: "dest_client_id",
|
||||||
DestClientSecret: "dest_client_secret",
|
DestClientSecret: "dest_client_secret",
|
||||||
@@ -1434,7 +1434,7 @@ func TestGooglePhotosTransferConfig(t *testing.T) {
|
|||||||
// Test 1: Create config with Google Photos as source
|
// Test 1: Create config with Google Photos as source
|
||||||
sourceReadOnly := true
|
sourceReadOnly := true
|
||||||
sourceIncludeArchived := false
|
sourceIncludeArchived := false
|
||||||
useBuiltinAuth := true
|
useBuiltinAuthSource := true
|
||||||
googleSourceConfig := &TransferConfig{
|
googleSourceConfig := &TransferConfig{
|
||||||
Name: "Google Photos Source Test",
|
Name: "Google Photos Source Test",
|
||||||
SourceType: "gphotos",
|
SourceType: "gphotos",
|
||||||
@@ -1444,7 +1444,7 @@ func TestGooglePhotosTransferConfig(t *testing.T) {
|
|||||||
SourceReadOnly: &sourceReadOnly,
|
SourceReadOnly: &sourceReadOnly,
|
||||||
SourceStartYear: 2015,
|
SourceStartYear: 2015,
|
||||||
SourceIncludeArchived: &sourceIncludeArchived,
|
SourceIncludeArchived: &sourceIncludeArchived,
|
||||||
UseBuiltinAuth: &useBuiltinAuth,
|
UseBuiltinAuthSource: &useBuiltinAuthSource,
|
||||||
DestinationType: "local",
|
DestinationType: "local",
|
||||||
DestinationPath: "/local/destination/path",
|
DestinationPath: "/local/destination/path",
|
||||||
FilePattern: "*.jpg",
|
FilePattern: "*.jpg",
|
||||||
@@ -1463,6 +1463,7 @@ func TestGooglePhotosTransferConfig(t *testing.T) {
|
|||||||
// Test 2: Create config with Google Photos as destination
|
// Test 2: Create config with Google Photos as destination
|
||||||
destReadOnly := false
|
destReadOnly := false
|
||||||
destIncludeArchived := true
|
destIncludeArchived := true
|
||||||
|
useBuiltinAuthDest := true
|
||||||
googleDestConfig := &TransferConfig{
|
googleDestConfig := &TransferConfig{
|
||||||
Name: "Google Photos Destination Test",
|
Name: "Google Photos Destination Test",
|
||||||
SourceType: "local",
|
SourceType: "local",
|
||||||
@@ -1474,7 +1475,7 @@ func TestGooglePhotosTransferConfig(t *testing.T) {
|
|||||||
DestReadOnly: &destReadOnly,
|
DestReadOnly: &destReadOnly,
|
||||||
DestStartYear: 2018,
|
DestStartYear: 2018,
|
||||||
DestIncludeArchived: &destIncludeArchived,
|
DestIncludeArchived: &destIncludeArchived,
|
||||||
UseBuiltinAuth: &useBuiltinAuth,
|
UseBuiltinAuthDest: &useBuiltinAuthDest,
|
||||||
FilePattern: "*.png",
|
FilePattern: "*.png",
|
||||||
CreatedBy: testUser.ID,
|
CreatedBy: testUser.ID,
|
||||||
}
|
}
|
||||||
@@ -1504,7 +1505,8 @@ func TestGooglePhotosTransferConfig(t *testing.T) {
|
|||||||
DestReadOnly: &destReadOnly,
|
DestReadOnly: &destReadOnly,
|
||||||
DestStartYear: 2020,
|
DestStartYear: 2020,
|
||||||
DestIncludeArchived: &destIncludeArchived,
|
DestIncludeArchived: &destIncludeArchived,
|
||||||
UseBuiltinAuth: &useBuiltinAuth,
|
UseBuiltinAuthSource: &useBuiltinAuthSource,
|
||||||
|
UseBuiltinAuthDest: &useBuiltinAuthDest,
|
||||||
FilePattern: "*.jpeg",
|
FilePattern: "*.jpeg",
|
||||||
CreatedBy: testUser.ID,
|
CreatedBy: testUser.ID,
|
||||||
}
|
}
|
||||||
@@ -1524,7 +1526,7 @@ func TestGooglePhotosTransferConfig(t *testing.T) {
|
|||||||
assert.Equal(t, sourceReadOnly, *retrievedConfig.SourceReadOnly)
|
assert.Equal(t, sourceReadOnly, *retrievedConfig.SourceReadOnly)
|
||||||
assert.Equal(t, 2015, retrievedConfig.SourceStartYear)
|
assert.Equal(t, 2015, retrievedConfig.SourceStartYear)
|
||||||
assert.Equal(t, sourceIncludeArchived, *retrievedConfig.SourceIncludeArchived)
|
assert.Equal(t, sourceIncludeArchived, *retrievedConfig.SourceIncludeArchived)
|
||||||
assert.Equal(t, useBuiltinAuth, *retrievedConfig.UseBuiltinAuth)
|
assert.Equal(t, useBuiltinAuthSource, *retrievedConfig.UseBuiltinAuthSource)
|
||||||
assert.Equal(t, true, retrievedConfig.GetGoogleAuthenticated())
|
assert.Equal(t, true, retrievedConfig.GetGoogleAuthenticated())
|
||||||
|
|
||||||
retrievedConfig, err = db.GetTransferConfig(googleDestConfig.ID)
|
retrievedConfig, err = db.GetTransferConfig(googleDestConfig.ID)
|
||||||
@@ -1533,7 +1535,7 @@ func TestGooglePhotosTransferConfig(t *testing.T) {
|
|||||||
assert.Equal(t, destReadOnly, *retrievedConfig.DestReadOnly)
|
assert.Equal(t, destReadOnly, *retrievedConfig.DestReadOnly)
|
||||||
assert.Equal(t, 2018, retrievedConfig.DestStartYear)
|
assert.Equal(t, 2018, retrievedConfig.DestStartYear)
|
||||||
assert.Equal(t, destIncludeArchived, *retrievedConfig.DestIncludeArchived)
|
assert.Equal(t, destIncludeArchived, *retrievedConfig.DestIncludeArchived)
|
||||||
assert.Equal(t, useBuiltinAuth, *retrievedConfig.UseBuiltinAuth)
|
assert.Equal(t, useBuiltinAuthDest, *retrievedConfig.UseBuiltinAuthDest)
|
||||||
assert.Equal(t, true, retrievedConfig.GetGoogleAuthenticated())
|
assert.Equal(t, true, retrievedConfig.GetGoogleAuthenticated())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1573,7 +1575,7 @@ func TestGooglePhotosRcloneConfig(t *testing.T) {
|
|||||||
// TEST 1: Google Photos as source with standard options
|
// TEST 1: Google Photos as source with standard options
|
||||||
sourceReadOnly := true
|
sourceReadOnly := true
|
||||||
sourceIncludeArchived := false
|
sourceIncludeArchived := false
|
||||||
useBuiltinAuth := true
|
useBuiltinAuthSource := true
|
||||||
gphotosSourceConfig := &TransferConfig{
|
gphotosSourceConfig := &TransferConfig{
|
||||||
ID: 1, // Force ID for predictable config path
|
ID: 1, // Force ID for predictable config path
|
||||||
Name: "Google Photos Source Config",
|
Name: "Google Photos Source Config",
|
||||||
@@ -1584,7 +1586,7 @@ func TestGooglePhotosRcloneConfig(t *testing.T) {
|
|||||||
SourceReadOnly: &sourceReadOnly,
|
SourceReadOnly: &sourceReadOnly,
|
||||||
SourceStartYear: 2015,
|
SourceStartYear: 2015,
|
||||||
SourceIncludeArchived: &sourceIncludeArchived,
|
SourceIncludeArchived: &sourceIncludeArchived,
|
||||||
UseBuiltinAuth: &useBuiltinAuth,
|
UseBuiltinAuthSource: &useBuiltinAuthSource,
|
||||||
DestinationType: "local",
|
DestinationType: "local",
|
||||||
DestinationPath: "/tmp/destination",
|
DestinationPath: "/tmp/destination",
|
||||||
FilePattern: "*.jpg",
|
FilePattern: "*.jpg",
|
||||||
@@ -1617,6 +1619,7 @@ func TestGooglePhotosRcloneConfig(t *testing.T) {
|
|||||||
// TEST 2: Google Photos as destination with authenticated token
|
// TEST 2: Google Photos as destination with authenticated token
|
||||||
destReadOnly := false
|
destReadOnly := false
|
||||||
destIncludeArchived := true
|
destIncludeArchived := true
|
||||||
|
useBuiltinAuthDest := true
|
||||||
gphotosDestConfig := &TransferConfig{
|
gphotosDestConfig := &TransferConfig{
|
||||||
ID: 2, // Force ID for predictable config path
|
ID: 2, // Force ID for predictable config path
|
||||||
Name: "Google Photos Destination Config",
|
Name: "Google Photos Destination Config",
|
||||||
@@ -1629,7 +1632,7 @@ func TestGooglePhotosRcloneConfig(t *testing.T) {
|
|||||||
DestReadOnly: &destReadOnly,
|
DestReadOnly: &destReadOnly,
|
||||||
DestStartYear: 2018,
|
DestStartYear: 2018,
|
||||||
DestIncludeArchived: &destIncludeArchived,
|
DestIncludeArchived: &destIncludeArchived,
|
||||||
UseBuiltinAuth: &useBuiltinAuth,
|
UseBuiltinAuthDest: &useBuiltinAuthDest,
|
||||||
FilePattern: "*.png",
|
FilePattern: "*.png",
|
||||||
CreatedBy: testUser.ID,
|
CreatedBy: testUser.ID,
|
||||||
}
|
}
|
||||||
@@ -1686,7 +1689,7 @@ func TestGooglePhotosAuthentication(t *testing.T) {
|
|||||||
// Create a transfer config with Google Photos
|
// Create a transfer config with Google Photos
|
||||||
readOnly := true
|
readOnly := true
|
||||||
includeArchived := false
|
includeArchived := false
|
||||||
useBuiltinAuth := true
|
useBuiltinAuthSource := true
|
||||||
gPhotosConfig := &TransferConfig{
|
gPhotosConfig := &TransferConfig{
|
||||||
Name: "Test Google Photos Auth",
|
Name: "Test Google Photos Auth",
|
||||||
SourceType: "gphotos",
|
SourceType: "gphotos",
|
||||||
@@ -1696,7 +1699,7 @@ func TestGooglePhotosAuthentication(t *testing.T) {
|
|||||||
SourceReadOnly: &readOnly,
|
SourceReadOnly: &readOnly,
|
||||||
SourceStartYear: 2015,
|
SourceStartYear: 2015,
|
||||||
SourceIncludeArchived: &includeArchived,
|
SourceIncludeArchived: &includeArchived,
|
||||||
UseBuiltinAuth: &useBuiltinAuth,
|
UseBuiltinAuthSource: &useBuiltinAuthSource,
|
||||||
DestinationType: "local",
|
DestinationType: "local",
|
||||||
DestinationPath: "/tmp/destination",
|
DestinationPath: "/tmp/destination",
|
||||||
FilePattern: "*.jpg",
|
FilePattern: "*.jpg",
|
||||||
|
|||||||
@@ -0,0 +1,278 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-gormigrate/gormigrate/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitialSchema() *gormigrate.Migration {
|
||||||
|
return &gormigrate.Migration{
|
||||||
|
ID: "001_initial_schema",
|
||||||
|
Migrate: func(tx *gorm.DB) error {
|
||||||
|
// Check if any tables exist (indicating an existing database)
|
||||||
|
var count int64
|
||||||
|
if err := tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").Scan(&count).Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to check for existing tables: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If tables exist, create a backup
|
||||||
|
if count > 0 {
|
||||||
|
// Get the database path
|
||||||
|
sqlDB, err := tx.DB()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get underlying database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var seq int
|
||||||
|
var name, dbPath string
|
||||||
|
if err := sqlDB.QueryRow("PRAGMA database_list").Scan(&seq, &name, &dbPath); err != nil {
|
||||||
|
return fmt.Errorf("failed to get database path: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create backup file with timestamp
|
||||||
|
backupFile := fmt.Sprintf("%s.backup.%s", dbPath, time.Now().Format("20060102_150405"))
|
||||||
|
|
||||||
|
// Read original database
|
||||||
|
data, err := os.ReadFile(dbPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read database for backup: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write backup
|
||||||
|
if err := os.WriteFile(backupFile, data, 0600); err != nil {
|
||||||
|
return fmt.Errorf("failed to create database backup: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Created database backup at: %s\n", backupFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable foreign key constraints while creating tables
|
||||||
|
if err := tx.Exec("PRAGMA foreign_keys = OFF").Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to disable foreign key constraints: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := tx.Exec("PRAGMA foreign_keys = ON").Error; err != nil {
|
||||||
|
fmt.Printf("Warning: failed to re-enable foreign key constraints: %v\n", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Change the table name from job_histories to job_history
|
||||||
|
if err := tx.Raw("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='job_histories'").Scan(&count).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if count > 0 {
|
||||||
|
if err := tx.Exec("ALTER TABLE job_histories RENAME TO job_history").Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change the table name from password_histories to password_history
|
||||||
|
count = 0
|
||||||
|
if err := tx.Raw("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='password_histories'").Scan(&count).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if count > 0 {
|
||||||
|
if err := tx.Exec("ALTER TABLE password_histories RENAME TO password_history").Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Users table
|
||||||
|
if err := tx.Exec(`CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
email VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
|
is_admin BOOLEAN DEFAULT FALSE,
|
||||||
|
last_password_change DATETIME,
|
||||||
|
failed_login_attempts INTEGER DEFAULT 0,
|
||||||
|
account_locked BOOLEAN DEFAULT FALSE,
|
||||||
|
lockout_until DATETIME,
|
||||||
|
theme VARCHAR(255) DEFAULT 'light',
|
||||||
|
created_at DATETIME,
|
||||||
|
updated_at DATETIME
|
||||||
|
)`).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create PasswordHistory table
|
||||||
|
if err := tx.Exec(`CREATE TABLE IF NOT EXISTS password_history (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
|
created_at DATETIME,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
|
)`).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create PasswordResetToken table
|
||||||
|
if err := tx.Exec(`CREATE TABLE IF NOT EXISTS password_reset_tokens (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
token VARCHAR(255) NOT NULL,
|
||||||
|
expires_at DATETIME NOT NULL,
|
||||||
|
used BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at DATETIME,
|
||||||
|
updated_at DATETIME,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
|
)`).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create TransferConfigs table
|
||||||
|
if err := tx.Exec(`CREATE TABLE IF NOT EXISTS transfer_configs (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
source_type VARCHAR(255) NOT NULL,
|
||||||
|
source_path TEXT NOT NULL,
|
||||||
|
source_host VARCHAR(255),
|
||||||
|
source_port INTEGER DEFAULT 22,
|
||||||
|
source_user VARCHAR(255),
|
||||||
|
source_key_file TEXT,
|
||||||
|
source_bucket VARCHAR(255),
|
||||||
|
source_region VARCHAR(255),
|
||||||
|
source_access_key VARCHAR(255),
|
||||||
|
source_endpoint VARCHAR(255),
|
||||||
|
source_share VARCHAR(255),
|
||||||
|
source_domain VARCHAR(255),
|
||||||
|
source_passive_mode BOOLEAN DEFAULT TRUE,
|
||||||
|
source_client_id VARCHAR(255),
|
||||||
|
source_drive_id VARCHAR(255),
|
||||||
|
source_team_drive VARCHAR(255),
|
||||||
|
source_read_only BOOLEAN,
|
||||||
|
source_start_year INTEGER,
|
||||||
|
source_include_archived BOOLEAN,
|
||||||
|
file_pattern VARCHAR(255) DEFAULT '*',
|
||||||
|
output_pattern TEXT,
|
||||||
|
destination_type VARCHAR(255) NOT NULL,
|
||||||
|
destination_path TEXT NOT NULL,
|
||||||
|
dest_host VARCHAR(255),
|
||||||
|
dest_port INTEGER DEFAULT 22,
|
||||||
|
dest_user VARCHAR(255),
|
||||||
|
dest_key_file TEXT,
|
||||||
|
dest_bucket VARCHAR(255),
|
||||||
|
dest_region VARCHAR(255),
|
||||||
|
dest_access_key VARCHAR(255),
|
||||||
|
dest_endpoint VARCHAR(255),
|
||||||
|
dest_share VARCHAR(255),
|
||||||
|
dest_domain VARCHAR(255),
|
||||||
|
dest_passive_mode BOOLEAN DEFAULT TRUE,
|
||||||
|
dest_client_id VARCHAR(255),
|
||||||
|
dest_drive_id VARCHAR(255),
|
||||||
|
dest_team_drive VARCHAR(255),
|
||||||
|
dest_read_only BOOLEAN,
|
||||||
|
dest_start_year INTEGER,
|
||||||
|
dest_include_archived BOOLEAN,
|
||||||
|
use_builtin_auth_source BOOLEAN DEFAULT TRUE,
|
||||||
|
use_builtin_auth_dest BOOLEAN DEFAULT TRUE,
|
||||||
|
google_drive_authenticated BOOLEAN,
|
||||||
|
archive_path TEXT,
|
||||||
|
archive_enabled BOOLEAN DEFAULT FALSE,
|
||||||
|
rclone_flags TEXT,
|
||||||
|
delete_after_transfer BOOLEAN DEFAULT FALSE,
|
||||||
|
skip_processed_files BOOLEAN DEFAULT TRUE,
|
||||||
|
max_concurrent_transfers INTEGER DEFAULT 4,
|
||||||
|
created_by INTEGER,
|
||||||
|
created_at DATETIME,
|
||||||
|
updated_at DATETIME,
|
||||||
|
FOREIGN KEY (created_by) REFERENCES users(id)
|
||||||
|
)`).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Jobs table
|
||||||
|
if err := tx.Exec(`CREATE TABLE IF NOT EXISTS jobs (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name VARCHAR(255),
|
||||||
|
config_id INTEGER NOT NULL,
|
||||||
|
config_ids TEXT,
|
||||||
|
schedule VARCHAR(255) NOT NULL,
|
||||||
|
enabled BOOLEAN DEFAULT TRUE,
|
||||||
|
last_run DATETIME,
|
||||||
|
next_run DATETIME,
|
||||||
|
webhook_enabled BOOLEAN DEFAULT FALSE,
|
||||||
|
webhook_url TEXT,
|
||||||
|
webhook_secret TEXT,
|
||||||
|
webhook_headers TEXT,
|
||||||
|
notify_on_success BOOLEAN DEFAULT TRUE,
|
||||||
|
notify_on_failure BOOLEAN DEFAULT TRUE,
|
||||||
|
created_by INTEGER,
|
||||||
|
created_at DATETIME,
|
||||||
|
updated_at DATETIME,
|
||||||
|
FOREIGN KEY (config_id) REFERENCES transfer_configs(id),
|
||||||
|
FOREIGN KEY (created_by) REFERENCES users(id)
|
||||||
|
)`).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create JobHistory table
|
||||||
|
if err := tx.Exec(`CREATE TABLE IF NOT EXISTS job_history (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
job_id INTEGER NOT NULL,
|
||||||
|
config_id INTEGER DEFAULT 0,
|
||||||
|
start_time DATETIME NOT NULL,
|
||||||
|
end_time DATETIME,
|
||||||
|
status VARCHAR(255) NOT NULL,
|
||||||
|
bytes_transferred INTEGER,
|
||||||
|
files_transferred INTEGER,
|
||||||
|
error_message TEXT,
|
||||||
|
FOREIGN KEY (job_id) REFERENCES jobs(id)
|
||||||
|
)`).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create FileMetadata table
|
||||||
|
if err := tx.Exec(`CREATE TABLE IF NOT EXISTS file_metadata (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
job_id INTEGER NOT NULL,
|
||||||
|
config_id INTEGER DEFAULT 0,
|
||||||
|
file_name VARCHAR(255) NOT NULL,
|
||||||
|
original_path TEXT NOT NULL,
|
||||||
|
file_size INTEGER NOT NULL,
|
||||||
|
file_hash VARCHAR(255),
|
||||||
|
creation_time DATETIME,
|
||||||
|
mod_time DATETIME,
|
||||||
|
processed_time DATETIME NOT NULL,
|
||||||
|
destination_path TEXT NOT NULL,
|
||||||
|
status VARCHAR(255) NOT NULL,
|
||||||
|
error_message TEXT,
|
||||||
|
created_at DATETIME,
|
||||||
|
updated_at DATETIME,
|
||||||
|
FOREIGN KEY (job_id) REFERENCES jobs(id)
|
||||||
|
)`).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-enable foreign key constraints and verify integrity
|
||||||
|
if err := tx.Exec("PRAGMA foreign_key_check").Error; err != nil {
|
||||||
|
return fmt.Errorf("foreign key integrity check failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Rollback: func(tx *gorm.DB) error {
|
||||||
|
// Drop tables in reverse order to handle foreign key constraints
|
||||||
|
tables := []string{
|
||||||
|
"file_metadata",
|
||||||
|
"job_history",
|
||||||
|
"jobs",
|
||||||
|
"transfer_configs",
|
||||||
|
"password_reset_tokens",
|
||||||
|
"password_history",
|
||||||
|
"users",
|
||||||
|
}
|
||||||
|
for _, table := range tables {
|
||||||
|
if err := tx.Exec(fmt.Sprintf("DROP TABLE IF EXISTS %s", table)).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-gormigrate/gormigrate/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UpdateBuiltinAuthFields updates the use_builtin_auth field to separate source and destination fields
|
||||||
|
func UpdateBuiltinAuthFields() *gormigrate.Migration {
|
||||||
|
return &gormigrate.Migration{
|
||||||
|
ID: "002_update_builtin_auth_fields",
|
||||||
|
Migrate: func(tx *gorm.DB) error {
|
||||||
|
// First, add the new columns
|
||||||
|
if err := tx.Exec(`ALTER TABLE transfer_configs ADD COLUMN use_builtin_auth_source BOOLEAN`).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.Exec(`ALTER TABLE transfer_configs ADD COLUMN use_builtin_auth_dest BOOLEAN`).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the old value to both new columns
|
||||||
|
if err := tx.Exec(`UPDATE transfer_configs SET
|
||||||
|
use_builtin_auth_source = use_builtin_auth,
|
||||||
|
use_builtin_auth_dest = use_builtin_auth`).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop the old column
|
||||||
|
return tx.Exec(`ALTER TABLE transfer_configs DROP COLUMN use_builtin_auth`).Error
|
||||||
|
},
|
||||||
|
Rollback: func(tx *gorm.DB) error {
|
||||||
|
// Add back the original column
|
||||||
|
if err := tx.Exec(`ALTER TABLE transfer_configs ADD COLUMN use_builtin_auth BOOLEAN`).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the source value back (could also use dest, they should be the same)
|
||||||
|
if err := tx.Exec(`UPDATE transfer_configs SET use_builtin_auth = use_builtin_auth_source`).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop the new columns
|
||||||
|
if err := tx.Exec(`ALTER TABLE transfer_configs DROP COLUMN use_builtin_auth_source`).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.Exec(`ALTER TABLE transfer_configs DROP COLUMN use_builtin_auth_dest`).Error
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-gormigrate/gormigrate/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UpdateGDriveType updates the source_type and destination_type from 'google_drive' to 'gdrive'
|
||||||
|
func UpdateGDriveType() *gormigrate.Migration {
|
||||||
|
return &gormigrate.Migration{
|
||||||
|
ID: "003_update_gdrive_type",
|
||||||
|
Migrate: func(tx *gorm.DB) error {
|
||||||
|
// Update source_type
|
||||||
|
if err := tx.Exec(`UPDATE transfer_configs SET source_type = 'gdrive' WHERE source_type = 'google_drive'`).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update destination_type
|
||||||
|
return tx.Exec(`UPDATE transfer_configs SET destination_type = 'gdrive' WHERE destination_type = 'google_drive'`).Error
|
||||||
|
},
|
||||||
|
Rollback: func(tx *gorm.DB) error {
|
||||||
|
// Revert source_type
|
||||||
|
if err := tx.Exec(`UPDATE transfer_configs SET source_type = 'google_drive' WHERE source_type = 'gdrive'`).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revert destination_type
|
||||||
|
return tx.Exec(`UPDATE transfer_configs SET destination_type = 'google_drive' WHERE destination_type = 'gdrive'`).Error
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
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
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package migrations
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/go-gormigrate/gormigrate/v2"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AddDeleteAfterTransferColumn adds the delete_after_transfer column to transfer_configs table
|
|
||||||
func AddDeleteAfterTransferColumn() *gormigrate.Migration {
|
|
||||||
return &gormigrate.Migration{
|
|
||||||
ID: "add_delete_after_transfer_column",
|
|
||||||
Migrate: func(tx *gorm.DB) error {
|
|
||||||
return tx.Exec("ALTER TABLE transfer_configs ADD COLUMN delete_after_transfer BOOLEAN NOT NULL DEFAULT false").Error
|
|
||||||
},
|
|
||||||
Rollback: func(tx *gorm.DB) error {
|
|
||||||
return tx.Exec("ALTER TABLE transfer_configs DROP COLUMN delete_after_transfer").Error
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package migrations
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/go-gormigrate/gormigrate/v2"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AddGoogleDriveAuthenticated adds the GoogleDriveAuthenticated field to the transfer_configs table
|
|
||||||
func AddGoogleDriveAuthenticated() *gormigrate.Migration {
|
|
||||||
return &gormigrate.Migration{
|
|
||||||
ID: "20240315_add_google_drive_authenticated",
|
|
||||||
Migrate: func(tx *gorm.DB) error {
|
|
||||||
// Add the GoogleDriveAuthenticated column with a default value of false
|
|
||||||
return tx.Exec("ALTER TABLE transfer_configs ADD COLUMN google_drive_authenticated BOOLEAN DEFAULT false").Error
|
|
||||||
},
|
|
||||||
Rollback: func(tx *gorm.DB) error {
|
|
||||||
// Remove the GoogleDriveAuthenticated column
|
|
||||||
return tx.Exec("ALTER TABLE transfer_configs DROP COLUMN google_drive_authenticated").Error
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
package migrations
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/go-gormigrate/gormigrate/v2"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AddGooglePhotosSupport adds Google Photos related fields to the transfer_configs table
|
|
||||||
func AddGooglePhotosSupport() *gormigrate.Migration {
|
|
||||||
return &gormigrate.Migration{
|
|
||||||
ID: "20240518_add_google_photos_support",
|
|
||||||
Migrate: func(tx *gorm.DB) error {
|
|
||||||
// Add Google Photos source fields
|
|
||||||
if err := tx.Exec("ALTER TABLE transfer_configs ADD COLUMN source_read_only BOOLEAN DEFAULT false").Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := tx.Exec("ALTER TABLE transfer_configs ADD COLUMN source_start_year INTEGER DEFAULT 0").Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := tx.Exec("ALTER TABLE transfer_configs ADD COLUMN source_include_archived BOOLEAN DEFAULT false").Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add Google Photos destination fields
|
|
||||||
if err := tx.Exec("ALTER TABLE transfer_configs ADD COLUMN dest_read_only BOOLEAN DEFAULT false").Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := tx.Exec("ALTER TABLE transfer_configs ADD COLUMN dest_start_year INTEGER DEFAULT 0").Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := tx.Exec("ALTER TABLE transfer_configs ADD COLUMN dest_include_archived BOOLEAN DEFAULT false").Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add OAuth field
|
|
||||||
return tx.Exec("ALTER TABLE transfer_configs ADD COLUMN use_builtin_auth BOOLEAN DEFAULT true").Error
|
|
||||||
},
|
|
||||||
Rollback: func(tx *gorm.DB) error {
|
|
||||||
// Remove all added columns in reverse order
|
|
||||||
if err := tx.Exec("ALTER TABLE transfer_configs DROP COLUMN use_builtin_auth").Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := tx.Exec("ALTER TABLE transfer_configs DROP COLUMN dest_include_archived").Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := tx.Exec("ALTER TABLE transfer_configs DROP COLUMN dest_start_year").Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := tx.Exec("ALTER TABLE transfer_configs DROP COLUMN dest_read_only").Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := tx.Exec("ALTER TABLE transfer_configs DROP COLUMN source_include_archived").Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := tx.Exec("ALTER TABLE transfer_configs DROP COLUMN source_start_year").Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return tx.Exec("ALTER TABLE transfer_configs DROP COLUMN source_read_only").Error
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package migrations
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/go-gormigrate/gormigrate/v2"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AddMaxConcurrentTransfersColumn adds the max_concurrent_transfers column to transfer_configs table
|
|
||||||
func AddMaxConcurrentTransfersColumn() *gormigrate.Migration {
|
|
||||||
return &gormigrate.Migration{
|
|
||||||
ID: "20250311_add_max_concurrent_transfers",
|
|
||||||
Migrate: func(tx *gorm.DB) error {
|
|
||||||
// Add max_concurrent_transfers column with default value of 4
|
|
||||||
return tx.Exec("ALTER TABLE transfer_configs ADD COLUMN max_concurrent_transfers INTEGER DEFAULT 4").Error
|
|
||||||
},
|
|
||||||
Rollback: func(tx *gorm.DB) error {
|
|
||||||
// Drop the column if needed
|
|
||||||
return tx.Exec("ALTER TABLE transfer_configs DROP COLUMN max_concurrent_transfers").Error
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package migrations
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/go-gormigrate/gormigrate/v2"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AddMultiConfigSupport adds support for multiple configurations per job
|
|
||||||
func AddMultiConfigSupport() *gormigrate.Migration {
|
|
||||||
return &gormigrate.Migration{
|
|
||||||
ID: "20250315_add_multi_config_support",
|
|
||||||
Migrate: func(tx *gorm.DB) error {
|
|
||||||
// Add config_ids column to jobs table
|
|
||||||
if err := tx.Exec("ALTER TABLE jobs ADD COLUMN config_ids TEXT").Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add config_id column to job_histories table
|
|
||||||
if err := tx.Exec("ALTER TABLE job_histories ADD COLUMN config_id INTEGER").Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add config_id column to file_metadata table
|
|
||||||
if err := tx.Exec("ALTER TABLE file_metadata ADD COLUMN config_id INTEGER").Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update existing jobs to set the config_ids field to match the current config_id
|
|
||||||
if err := tx.Exec("UPDATE jobs SET config_ids = config_id WHERE config_id > 0").Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
Rollback: func(tx *gorm.DB) error {
|
|
||||||
// Drop the config_id columns from job_histories and file_metadata
|
|
||||||
if err := tx.Exec("ALTER TABLE job_histories DROP COLUMN config_id").Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Exec("ALTER TABLE file_metadata DROP COLUMN config_id").Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Drop the config_ids column from jobs
|
|
||||||
return tx.Exec("ALTER TABLE jobs DROP COLUMN config_ids").Error
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package migrations
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/go-gormigrate/gormigrate/v2"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AddSkipProcessedFilesColumn adds the skip_processed_files column to transfer_configs table
|
|
||||||
func AddSkipProcessedFilesColumn() *gormigrate.Migration {
|
|
||||||
return &gormigrate.Migration{
|
|
||||||
ID: "20250310_add_skip_processed_files",
|
|
||||||
Migrate: func(tx *gorm.DB) error {
|
|
||||||
// Add skip_processed_files column with default value of true
|
|
||||||
return tx.Exec("ALTER TABLE transfer_configs ADD COLUMN skip_processed_files BOOLEAN DEFAULT true").Error
|
|
||||||
},
|
|
||||||
Rollback: func(tx *gorm.DB) error {
|
|
||||||
// Drop the column if needed
|
|
||||||
return tx.Exec("ALTER TABLE transfer_configs DROP COLUMN skip_processed_files").Error
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
package migrations
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/go-gormigrate/gormigrate/v2"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AddWebhookSupport adds webhook notification fields to the jobs table
|
|
||||||
func AddWebhookSupport() *gormigrate.Migration {
|
|
||||||
return &gormigrate.Migration{
|
|
||||||
ID: "20240618_add_webhook_support",
|
|
||||||
Migrate: func(tx *gorm.DB) error {
|
|
||||||
// Add webhook URL field
|
|
||||||
if err := tx.Exec("ALTER TABLE jobs ADD COLUMN webhook_enabled BOOLEAN DEFAULT false").Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := tx.Exec("ALTER TABLE jobs ADD COLUMN webhook_url VARCHAR(255)").Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := tx.Exec("ALTER TABLE jobs ADD COLUMN webhook_secret VARCHAR(255)").Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := tx.Exec("ALTER TABLE jobs ADD COLUMN webhook_headers TEXT").Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add notification settings
|
|
||||||
if err := tx.Exec("ALTER TABLE jobs ADD COLUMN notify_on_success BOOLEAN DEFAULT true").Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := tx.Exec("ALTER TABLE jobs ADD COLUMN notify_on_failure BOOLEAN DEFAULT true").Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
Rollback: func(tx *gorm.DB) error {
|
|
||||||
// Drop the webhook fields from jobs
|
|
||||||
if err := tx.Exec("ALTER TABLE jobs DROP COLUMN webhook_enabled").Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := tx.Exec("ALTER TABLE jobs DROP COLUMN webhook_url").Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := tx.Exec("ALTER TABLE jobs DROP COLUMN webhook_secret").Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := tx.Exec("ALTER TABLE jobs DROP COLUMN webhook_headers").Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := tx.Exec("ALTER TABLE jobs DROP COLUMN notify_on_success").Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := tx.Exec("ALTER TABLE jobs DROP COLUMN notify_on_failure").Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,16 +8,9 @@ import (
|
|||||||
// InitMigrations initializes the migrations
|
// InitMigrations initializes the migrations
|
||||||
func InitMigrations(db *gorm.DB) *gormigrate.Gormigrate {
|
func InitMigrations(db *gorm.DB) *gormigrate.Gormigrate {
|
||||||
migrations := []*gormigrate.Migration{
|
migrations := []*gormigrate.Migration{
|
||||||
// ... existing migrations
|
InitialSchema(),
|
||||||
AddDeleteAfterTransferColumn(),
|
UpdateBuiltinAuthFields(),
|
||||||
AddCloudStorageFields(),
|
UpdateGDriveType(),
|
||||||
AddSkipProcessedFilesColumn(),
|
|
||||||
AddMaxConcurrentTransfersColumn(),
|
|
||||||
AddMultiConfigSupport(),
|
|
||||||
UpdateSkipProcessedFilesToNullable(),
|
|
||||||
AddWebhookSupport(),
|
|
||||||
AddGoogleDriveAuthenticated(),
|
|
||||||
AddGooglePhotosSupport(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return gormigrate.New(db, gormigrate.DefaultOptions, migrations)
|
return gormigrate.New(db, gormigrate.DefaultOptions, migrations)
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
package migrations
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/go-gormigrate/gormigrate/v2"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// UpdateSkipProcessedFilesToNullable changes the skip_processed_files column to be nullable
|
|
||||||
func UpdateSkipProcessedFilesToNullable() *gormigrate.Migration {
|
|
||||||
return &gormigrate.Migration{
|
|
||||||
ID: "20250515_update_skip_processed_files_to_nullable",
|
|
||||||
Migrate: func(tx *gorm.DB) error {
|
|
||||||
// SQLite specific command - this would need to be adjusted for other databases
|
|
||||||
return tx.Exec("ALTER TABLE transfer_configs RENAME TO transfer_configs_old; " +
|
|
||||||
"CREATE TABLE transfer_configs (" +
|
|
||||||
"id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
|
||||||
"name VARCHAR(255) NOT NULL, " +
|
|
||||||
"source_type VARCHAR(255) NOT NULL, " +
|
|
||||||
"source_path VARCHAR(255) NOT NULL, " +
|
|
||||||
"source_host VARCHAR(255), " +
|
|
||||||
"source_port INTEGER DEFAULT 22, " +
|
|
||||||
"source_user VARCHAR(255), " +
|
|
||||||
"source_key_file VARCHAR(255), " +
|
|
||||||
"source_bucket VARCHAR(255), " +
|
|
||||||
"source_region VARCHAR(255), " +
|
|
||||||
"source_access_key VARCHAR(255), " +
|
|
||||||
"source_endpoint VARCHAR(255), " +
|
|
||||||
"source_share VARCHAR(255), " +
|
|
||||||
"source_domain VARCHAR(255), " +
|
|
||||||
"source_passive_mode BOOLEAN DEFAULT true, " +
|
|
||||||
"source_client_id VARCHAR(255), " +
|
|
||||||
"source_drive_id VARCHAR(255), " +
|
|
||||||
"source_team_drive VARCHAR(255), " +
|
|
||||||
"file_pattern VARCHAR(255) DEFAULT '*', " +
|
|
||||||
"output_pattern VARCHAR(255), " +
|
|
||||||
"destination_type VARCHAR(255) NOT NULL, " +
|
|
||||||
"destination_path VARCHAR(255) NOT NULL, " +
|
|
||||||
"dest_host VARCHAR(255), " +
|
|
||||||
"dest_port INTEGER DEFAULT 22, " +
|
|
||||||
"dest_user VARCHAR(255), " +
|
|
||||||
"dest_key_file VARCHAR(255), " +
|
|
||||||
"dest_bucket VARCHAR(255), " +
|
|
||||||
"dest_region VARCHAR(255), " +
|
|
||||||
"dest_access_key VARCHAR(255), " +
|
|
||||||
"dest_endpoint VARCHAR(255), " +
|
|
||||||
"dest_share VARCHAR(255), " +
|
|
||||||
"dest_domain VARCHAR(255), " +
|
|
||||||
"dest_passive_mode BOOLEAN DEFAULT true, " +
|
|
||||||
"dest_client_id VARCHAR(255), " +
|
|
||||||
"dest_drive_id VARCHAR(255), " +
|
|
||||||
"dest_team_drive VARCHAR(255), " +
|
|
||||||
"archive_path VARCHAR(255), " +
|
|
||||||
"archive_enabled BOOLEAN DEFAULT false, " +
|
|
||||||
"rclone_flags VARCHAR(255), " +
|
|
||||||
"delete_after_transfer BOOLEAN DEFAULT false, " +
|
|
||||||
"skip_processed_files BOOLEAN DEFAULT true, " + // Keep as BOOLEAN, but now it's nullable
|
|
||||||
"max_concurrent_transfers INTEGER DEFAULT 4, " +
|
|
||||||
"created_by INTEGER, " +
|
|
||||||
"created_at DATETIME, " +
|
|
||||||
"updated_at DATETIME" +
|
|
||||||
"); " +
|
|
||||||
"INSERT INTO transfer_configs SELECT * FROM transfer_configs_old; " +
|
|
||||||
"DROP TABLE transfer_configs_old;").Error
|
|
||||||
},
|
|
||||||
Rollback: func(tx *gorm.DB) error {
|
|
||||||
// No need to rollback as the data structure remains compatible
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -72,7 +72,7 @@ func TestGenerateRcloneConfigWithoutRclone(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Test configs for different source types
|
// Test configs for different source types
|
||||||
sourceTypes := []string{"sftp", "s3", "minio", "b2", "smb", "ftp", "webdav", "nextcloud", "onedrive", "google_drive"}
|
sourceTypes := []string{"sftp", "s3", "minio", "b2", "smb", "ftp", "webdav", "nextcloud", "onedrive", "gdrive"}
|
||||||
|
|
||||||
for _, sourceType := range sourceTypes {
|
for _, sourceType := range sourceTypes {
|
||||||
testConfig := &TransferConfig{
|
testConfig := &TransferConfig{
|
||||||
@@ -102,7 +102,7 @@ func TestGenerateRcloneConfigWithoutRclone(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Test configs for different destination types
|
// Test configs for different destination types
|
||||||
destTypes := []string{"sftp", "s3", "minio", "b2", "smb", "ftp", "webdav", "nextcloud", "onedrive", "google_drive"}
|
destTypes := []string{"sftp", "s3", "minio", "b2", "smb", "ftp", "webdav", "nextcloud", "onedrive", "gdrive"}
|
||||||
|
|
||||||
for _, destType := range destTypes {
|
for _, destType := range destTypes {
|
||||||
testConfig := &TransferConfig{
|
testConfig := &TransferConfig{
|
||||||
@@ -164,7 +164,7 @@ func TestGoogleDriveRcloneConfig(t *testing.T) {
|
|||||||
// Create Google Drive source config
|
// Create Google Drive source config
|
||||||
googleSourceConfig := &TransferConfig{
|
googleSourceConfig := &TransferConfig{
|
||||||
Name: "Google Drive Source Rclone Test",
|
Name: "Google Drive Source Rclone Test",
|
||||||
SourceType: "google_drive",
|
SourceType: "gdrive",
|
||||||
SourcePath: "/path/in/google/drive",
|
SourcePath: "/path/in/google/drive",
|
||||||
SourceClientID: "source_google_client_id",
|
SourceClientID: "source_google_client_id",
|
||||||
SourceClientSecret: "source_google_client_secret",
|
SourceClientSecret: "source_google_client_secret",
|
||||||
@@ -209,7 +209,7 @@ func TestGoogleDriveRcloneConfig(t *testing.T) {
|
|||||||
Name: "Google Drive Dest Rclone Test",
|
Name: "Google Drive Dest Rclone Test",
|
||||||
SourceType: "local",
|
SourceType: "local",
|
||||||
SourcePath: "/local/source/path",
|
SourcePath: "/local/source/path",
|
||||||
DestinationType: "google_drive",
|
DestinationType: "gdrive",
|
||||||
DestinationPath: "/dest/path/in/google/drive",
|
DestinationPath: "/dest/path/in/google/drive",
|
||||||
DestClientID: "dest_google_client_id",
|
DestClientID: "dest_google_client_id",
|
||||||
DestClientSecret: "dest_google_client_secret",
|
DestClientSecret: "dest_google_client_secret",
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ func (h *Handlers) HandleEditConfig(c *gin.Context) {
|
|||||||
// HandleCreateConfig handles the POST /configs route
|
// HandleCreateConfig handles the POST /configs route
|
||||||
func (h *Handlers) HandleCreateConfig(c *gin.Context) {
|
func (h *Handlers) HandleCreateConfig(c *gin.Context) {
|
||||||
var config db.TransferConfig
|
var config db.TransferConfig
|
||||||
|
|
||||||
if err := c.ShouldBind(&config); err != nil {
|
if err := c.ShouldBind(&config); err != nil {
|
||||||
log.Printf("Error binding config form: %v", err)
|
log.Printf("Error binding config form: %v", err)
|
||||||
c.String(http.StatusBadRequest, fmt.Sprintf("Invalid form data: %v", err))
|
c.String(http.StatusBadRequest, fmt.Sprintf("Invalid form data: %v", err))
|
||||||
@@ -118,9 +119,13 @@ func (h *Handlers) HandleCreateConfig(c *gin.Context) {
|
|||||||
sourceIncludeArchivedValue := sourceIncludeArchivedVal == "on" || sourceIncludeArchivedVal == "true"
|
sourceIncludeArchivedValue := sourceIncludeArchivedVal == "on" || sourceIncludeArchivedVal == "true"
|
||||||
config.SourceIncludeArchived = &sourceIncludeArchivedValue
|
config.SourceIncludeArchived = &sourceIncludeArchivedValue
|
||||||
|
|
||||||
useBuiltinAuthVal := c.Request.FormValue("use_builtin_auth")
|
useBuiltinAuthSourceVal := c.Request.FormValue("use_builtin_auth_source")
|
||||||
useBuiltinAuthValue := useBuiltinAuthVal == "on" || useBuiltinAuthVal == "true"
|
useBuiltinAuthSourceValue := useBuiltinAuthSourceVal == "on" || useBuiltinAuthSourceVal == "true"
|
||||||
config.UseBuiltinAuth = &useBuiltinAuthValue
|
config.UseBuiltinAuthSource = &useBuiltinAuthSourceValue
|
||||||
|
|
||||||
|
useBuiltinAuthDestVal := c.Request.FormValue("use_builtin_auth_dest")
|
||||||
|
useBuiltinAuthDestValue := useBuiltinAuthDestVal == "on" || useBuiltinAuthDestVal == "true"
|
||||||
|
config.UseBuiltinAuthDest = &useBuiltinAuthDestValue
|
||||||
|
|
||||||
if err := h.DB.Create(&config).Error; err != nil {
|
if err := h.DB.Create(&config).Error; err != nil {
|
||||||
log.Printf("Error creating config: %v", err)
|
log.Printf("Error creating config: %v", err)
|
||||||
@@ -209,9 +214,13 @@ func (h *Handlers) HandleUpdateConfig(c *gin.Context) {
|
|||||||
sourceIncludeArchivedValue := sourceIncludeArchivedVal == "on" || sourceIncludeArchivedVal == "true"
|
sourceIncludeArchivedValue := sourceIncludeArchivedVal == "on" || sourceIncludeArchivedVal == "true"
|
||||||
config.SourceIncludeArchived = &sourceIncludeArchivedValue
|
config.SourceIncludeArchived = &sourceIncludeArchivedValue
|
||||||
|
|
||||||
useBuiltinAuthVal := c.Request.FormValue("use_builtin_auth")
|
useBuiltinAuthSourceVal := c.Request.FormValue("use_builtin_auth_source")
|
||||||
useBuiltinAuthValue := useBuiltinAuthVal == "on" || useBuiltinAuthVal == "true"
|
useBuiltinAuthSourceValue := useBuiltinAuthSourceVal == "on" || useBuiltinAuthSourceVal == "true"
|
||||||
config.UseBuiltinAuth = &useBuiltinAuthValue
|
config.UseBuiltinAuthSource = &useBuiltinAuthSourceValue
|
||||||
|
|
||||||
|
useBuiltinAuthDestVal := c.Request.FormValue("use_builtin_auth_dest")
|
||||||
|
useBuiltinAuthDestValue := useBuiltinAuthDestVal == "on" || useBuiltinAuthDestVal == "true"
|
||||||
|
config.UseBuiltinAuthDest = &useBuiltinAuthDestValue
|
||||||
|
|
||||||
// Preserve fields that shouldn't be updated
|
// Preserve fields that shouldn't be updated
|
||||||
config.CreatedBy = oldConfig.CreatedBy
|
config.CreatedBy = oldConfig.CreatedBy
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ func (h *Handlers) HandleGDriveAuth(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ensure it's a Google Drive or Google Photos configuration
|
// Ensure it's a Google Drive or Google Photos configuration
|
||||||
if config.DestinationType != "gdrive" && config.DestinationType != "gphotos" {
|
if config.SourceType != "gdrive" && config.DestinationType != "gdrive" && config.SourceType != "gphotos" && config.DestinationType != "gphotos" {
|
||||||
RenderErrorPage(c, "Not a Google configuration", "The selected configuration is not set up for Google Drive or Google Photos")
|
RenderErrorPage(c, "Not a Google configuration", "The selected configuration is not set up for Google Drive or Google Photos")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -365,8 +365,8 @@ func (h *Handlers) HandleGDriveTokenProcess(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure it's a Google Drive configuration
|
// Ensure it's a Google Drive or Google Photos configuration
|
||||||
if config.DestinationType != "gdrive" {
|
if config.SourceType != "gdrive" && config.DestinationType != "gdrive" && config.SourceType != "gphotos" && config.DestinationType != "gphotos" {
|
||||||
RenderErrorPage(c, "Not a Google Drive configuration", "")
|
RenderErrorPage(c, "Not a Google Drive configuration", "")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,26 +64,26 @@ func (h *MockHandlers) HandleGDriveAuth(c *gin.Context) {
|
|||||||
// Get the config ID from the query parameter
|
// Get the config ID from the query parameter
|
||||||
configIDStr := c.Param("id")
|
configIDStr := c.Param("id")
|
||||||
if configIDStr == "" {
|
if configIDStr == "" {
|
||||||
RenderErrorPage(c, "Missing configuration ID", "")
|
RenderErrorPageTest(c, "Missing configuration ID", "")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
configID, err := strconv.ParseUint(configIDStr, 10, 64)
|
configID, err := strconv.ParseUint(configIDStr, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
RenderErrorPage(c, "Invalid configuration ID", err.Error())
|
RenderErrorPageTest(c, "Invalid configuration ID", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the configuration
|
// Get the configuration
|
||||||
config, err := h.DB.GetTransferConfig(uint(configID))
|
config, err := h.DB.GetTransferConfig(uint(configID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
RenderErrorPage(c, "Configuration not found", err.Error())
|
RenderErrorPageTest(c, "Configuration not found", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure it's a Google Drive or Google Photos configuration
|
// Ensure it's a Google Drive or Google Photos configuration
|
||||||
if config.DestinationType != "gdrive" && config.DestinationType != "gphotos" {
|
if config.DestinationType != "gdrive" && config.DestinationType != "gphotos" {
|
||||||
RenderErrorPage(c, "Not a Google configuration", "The selected configuration is not set up for Google Drive or Google Photos")
|
RenderErrorPageTest(c, "Not a Google configuration", "The selected configuration is not set up for Google Drive or Google Photos")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,14 +96,14 @@ func (h *MockHandlers) HandleGDriveAuth(c *gin.Context) {
|
|||||||
// Get Rclone Config Path
|
// Get Rclone Config Path
|
||||||
rcloneConfigPath := h.DB.GetConfigRclonePath(config)
|
rcloneConfigPath := h.DB.GetConfigRclonePath(config)
|
||||||
if rcloneConfigPath == "" {
|
if rcloneConfigPath == "" {
|
||||||
RenderErrorPage(c, "Rclone config not found", "The selected configuration does not have a valid rclone config")
|
RenderErrorPageTest(c, "Rclone config not found", "The selected configuration does not have a valid rclone config")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a temporary config file for authentication
|
// Create a temporary config file for authentication
|
||||||
tempConfigDir := filepath.Join(dataDir, "temp")
|
tempConfigDir := filepath.Join(dataDir, "temp")
|
||||||
if err := os.MkdirAll(tempConfigDir, 0755); err != nil {
|
if err := os.MkdirAll(tempConfigDir, 0755); err != nil {
|
||||||
RenderErrorPage(c, "Failed to create temporary directory", err.Error())
|
RenderErrorPageTest(c, "Failed to create temporary directory", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,7 +180,7 @@ func (h *MockHandlers) HandleGDriveAuthCallback(c *gin.Context) {
|
|||||||
// Get auth code from query parameters
|
// Get auth code from query parameters
|
||||||
authCode := c.Query("code")
|
authCode := c.Query("code")
|
||||||
if authCode == "" {
|
if authCode == "" {
|
||||||
RenderErrorPage(c, "Authentication failed", "No authorization code received from Google")
|
RenderErrorPageTest(c, "Authentication failed", "No authorization code received from Google")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,27 +188,27 @@ func (h *MockHandlers) HandleGDriveAuthCallback(c *gin.Context) {
|
|||||||
state := c.Query("state")
|
state := c.Query("state")
|
||||||
storedState, err := c.Cookie("gdrive_auth_state")
|
storedState, err := c.Cookie("gdrive_auth_state")
|
||||||
if err != nil || state != storedState {
|
if err != nil || state != storedState {
|
||||||
RenderErrorPage(c, "Authentication failed", "Invalid state parameter")
|
RenderErrorPageTest(c, "Authentication failed", "Invalid state parameter")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get config ID from cookie
|
// Get config ID from cookie
|
||||||
configIDStr, err := c.Cookie("gdrive_config_id")
|
configIDStr, err := c.Cookie("gdrive_config_id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
RenderErrorPage(c, "Authentication failed", "Unable to retrieve configuration ID")
|
RenderErrorPageTest(c, "Authentication failed", "Unable to retrieve configuration ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
configID, err := strconv.ParseUint(configIDStr, 10, 64)
|
configID, err := strconv.ParseUint(configIDStr, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
RenderErrorPage(c, "Invalid configuration ID", err.Error())
|
RenderErrorPageTest(c, "Invalid configuration ID", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the configuration
|
// Get the configuration
|
||||||
config, err := h.DB.GetTransferConfig(uint(configID))
|
config, err := h.DB.GetTransferConfig(uint(configID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
RenderErrorPage(c, "Failed to get configuration", err.Error())
|
RenderErrorPageTest(c, "Failed to get configuration", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +219,7 @@ func (h *MockHandlers) HandleGDriveAuthCallback(c *gin.Context) {
|
|||||||
// Update the config with the token
|
// Update the config with the token
|
||||||
err = h.DB.GenerateRcloneConfigWithToken(config, mockToken)
|
err = h.DB.GenerateRcloneConfigWithToken(config, mockToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
RenderErrorPage(c, "Failed to update configuration", err.Error())
|
RenderErrorPageTest(c, "Failed to update configuration", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,8 +420,8 @@ func TestHandleGDriveAuth_NonGoogleConfig(t *testing.T) {
|
|||||||
assert.Contains(t, w.Body.String(), "Not a Google configuration")
|
assert.Contains(t, w.Body.String(), "Not a Google configuration")
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderErrorPage renders an error page with the given message
|
// RenderErrorPageTest renders an error page with the given message
|
||||||
func RenderErrorPage(c *gin.Context, title string, details string) {
|
func RenderErrorPageTest(c *gin.Context, title string, details string) {
|
||||||
// Here we'd typically use a component for error display
|
// Here we'd typically use a component for error display
|
||||||
// For now, we'll just render a simple HTML error page for testing
|
// For now, we'll just render a simple HTML error page for testing
|
||||||
errorHTML := fmt.Sprintf("<html><body><h1>Error: %s</h1><p>%s</p></body></html>", title, details)
|
errorHTML := fmt.Sprintf("<html><body><h1>Error: %s</h1><p>%s</p></body></html>", title, details)
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HandleCheckPath validates if a given path exists and is accessible
|
||||||
|
func (h *Handlers) HandleCheckPath(c *gin.Context) {
|
||||||
|
path := c.Query("path")
|
||||||
|
if path == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"valid": false,
|
||||||
|
"error": "No path provided",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean and resolve the path
|
||||||
|
path = filepath.Clean(path)
|
||||||
|
absPath, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"valid": false,
|
||||||
|
"error": "Invalid path format",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if path exists
|
||||||
|
info, err := os.Stat(absPath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"valid": false,
|
||||||
|
"error": "Path does not exist",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"valid": false,
|
||||||
|
"error": "Error accessing path: " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a directory
|
||||||
|
if !info.IsDir() {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"valid": false,
|
||||||
|
"error": "Path exists but is not a directory",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have read access
|
||||||
|
testFile := filepath.Join(absPath, ".gomft_test")
|
||||||
|
f, err := os.OpenFile(testFile, os.O_CREATE|os.O_WRONLY, 0666)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"valid": false,
|
||||||
|
"error": "Directory exists but is not writable",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
os.Remove(testFile)
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"valid": true,
|
||||||
|
"error": "",
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -32,6 +32,9 @@ func (h *Handlers) RegisterRoutes(router *gin.Engine) {
|
|||||||
authorized.POST("/configs/:id", h.HandleUpdateConfig)
|
authorized.POST("/configs/:id", h.HandleUpdateConfig)
|
||||||
authorized.DELETE("/configs/:id", h.HandleDeleteConfig)
|
authorized.DELETE("/configs/:id", h.HandleDeleteConfig)
|
||||||
|
|
||||||
|
// Path validation endpoint
|
||||||
|
authorized.GET("/check-path", h.HandleCheckPath)
|
||||||
|
|
||||||
// Google Drive authentication routes
|
// Google Drive authentication routes
|
||||||
authorized.GET("/configs/:id/gdrive-auth", h.HandleGDriveAuth)
|
authorized.GET("/configs/:id/gdrive-auth", h.HandleGDriveAuth)
|
||||||
authorized.GET("/configs/gdrive-callback", h.HandleGDriveAuthCallback)
|
authorized.GET("/configs/gdrive-callback", h.HandleGDriveAuthCallback)
|
||||||
|
|||||||
Reference in new issue
Block a user