feat: Implement command flag management for rclone configurations

- Added functionality to manage command flags and their values in the configuration form.
- Updated the database schema to include a new field for storing command flag values.
- Enhanced backend logic to process and store flag values for non-boolean flags during configuration creation and updates.
- Improved the user interface to dynamically require destination fields based on selected commands.
- Refactored related templates and handlers to support the new command flag management features.
This commit is contained in:
StarFleetCPTN
2025-03-23 20:30:02 -07:00
parent 0cebfcf1f5
commit 233f1779d8
20 changed files with 457 additions and 239 deletions
-87
View File
@@ -302,93 +302,6 @@ templ AdminAuditLogs(ctx context.Context, data AuditLogsData) {
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize Notyf toast notifications
const notyf = window.notyfInstance || new Notyf({
position: { x: 'right', y: 'top' },
duration: 3000,
ripple: true,
dismissible: true
});
// Check for URL parameters for success/error messages
const urlParams = new URLSearchParams(window.location.search);
const errorParam = urlParams.get('error');
const successParam = urlParams.get('success');
if (errorParam) {
notyf.error(decodeURIComponent(errorParam));
// Also show in the error box
const errorBox = document.getElementById('error-message');
const errorTitle = errorBox.querySelector('.error-title');
errorTitle.textContent = 'Error';
const errorDetails = errorBox.querySelector('.error-details');
errorDetails.textContent = decodeURIComponent(errorParam);
errorBox.classList.remove('hidden');
}
if (successParam) {
notyf.success(decodeURIComponent(successParam));
// Also show in the status box
const statusBox = document.getElementById('status-message');
statusBox.textContent = decodeURIComponent(successParam);
statusBox.classList.remove('hidden');
}
// Handle details modal
const detailsButtons = document.querySelectorAll('[data-modal-target="details-modal"]');
detailsButtons.forEach(button => {
button.addEventListener('click', function() {
const detailsData = this.getAttribute('data-details');
const detailsContent = document.getElementById('details-content');
detailsContent.textContent = detailsData;
// Format JSON if possible
try {
const jsonData = JSON.parse(detailsData);
detailsContent.textContent = JSON.stringify(jsonData, null, 2);
} catch (e) {
// Not valid JSON, leave as is
}
// Show the modal
const modal = document.getElementById('details-modal');
modal.classList.remove('hidden');
modal.classList.add('flex');
});
});
// Handle modal hide buttons
const hideButtons = document.querySelectorAll('[data-modal-hide]');
hideButtons.forEach(button => {
button.addEventListener('click', function() {
const modalId = this.getAttribute('data-modal-hide');
const modal = document.getElementById(modalId);
modal.classList.add('hidden');
modal.classList.remove('flex');
});
});
// Set dark background color if in dark mode
if (document.documentElement.classList.contains('dark')) {
document.getElementById('audit-logs-container').style.backgroundColor = '#111827';
}
// Handle theme changes
const themeToggle = document.getElementById('theme-toggle');
if (themeToggle) {
themeToggle.addEventListener('click', function() {
setTimeout(function() {
const isDark = document.documentElement.classList.contains('dark');
document.getElementById('audit-logs-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';
}, 50);
});
}
});
</script>
}
}
+80 -7
View File
@@ -249,6 +249,9 @@ func getInitialData(config *db.TransferConfig) string {
sourcePathError: '',
destPathValid: null,
destPathError: '',
// Command configuration
requiresDestination: true,
// Methods for path validation
checkPath(path, type) {
@@ -268,6 +271,68 @@ func getInitialData(config *db.TransferConfig) string {
this[type + 'PathValid'] = false;
this[type + 'PathError'] = 'Error checking path: ' + error.message;
});
},
// Method to check if destination is required based on command type
updateCommandRequirements() {
// List of commands that don't require destination
const listingCommands = ['ls', 'lsd', 'lsl', 'lsf', 'lsjson', 'listremotes'];
const infoCommands = ['md5sum', 'sha1sum', 'size', 'version'];
const dirCommands = ['mkdir', 'rmdir', 'rmdirs'];
const destructiveCommands = ['delete', 'purge'];
const specialSinglePathCommands = ['obscure'];
// Get the command name from the command ID
// This will need coordination with your backend to ensure the IDs match the commands
let commandName = '';
switch(parseInt(this.commandId)) {
// Map command IDs to command names - adjust these based on your actual command IDs
case 1: commandName = 'copy'; break;
case 2: commandName = 'move'; break;
case 3: commandName = 'sync'; break;
case 4: commandName = 'ls'; break;
case 5: commandName = 'lsd'; break;
case 6: commandName = 'lsl'; break;
case 7: commandName = 'lsf'; break;
case 8: commandName = 'lsjson'; break;
case 9: commandName = 'md5sum'; break;
case 10: commandName = 'sha1sum'; break;
case 11: commandName = 'size'; break;
case 12: commandName = 'delete'; break;
case 13: commandName = 'purge'; break;
case 14: commandName = 'mkdir'; break;
case 15: commandName = 'rmdir'; break;
case 16: commandName = 'rmdirs'; break;
case 17: commandName = 'check'; break;
case 18: commandName = 'cleanup'; break;
case 19: commandName = 'dedupe'; break;
case 20: commandName = 'version'; break;
case 21: commandName = 'listremotes'; break;
case 22: commandName = 'cryptcheck'; break;
case 23: commandName = 'bisync'; break;
case 24: commandName = 'copyto'; break;
case 25: commandName = 'moveto'; break;
default: commandName = 'copy'; // Default to copy
}
console.log('Command ID:', this.commandId, 'Command Name:', commandName);
// Check if the command requires a destination
if (
listingCommands.includes(commandName) ||
infoCommands.includes(commandName) ||
(dirCommands.includes(commandName) && !this.rcloneFlags.includes('--dst')) ||
destructiveCommands.includes(commandName) ||
specialSinglePathCommands.includes(commandName) ||
commandName === 'version' ||
commandName === 'listremotes'
) {
this.requiresDestination = false;
console.log('Destination not required for command:', commandName);
} else {
this.requiresDestination = true;
console.log('Destination required for command:', commandName);
}
}
}`,
name, sourceType, sourcePath, sourceHost, sourcePort, sourceUser, sourcePassword, sourceKeyFile, sourceAuthType,
@@ -323,6 +388,8 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
destinationType = destinationType || 'local';
sourcePort = sourcePort || 22;
destPort = destPort || 22;
// Initialize command requirements
updateCommandRequirements();
})"
>
@@ -352,6 +419,16 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Choose a descriptive name to identify this configuration.</p>
</div>
<!-- Rclone Command Configuration Section -->
<div class="p-5 bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700">
<h3 class="mb-4 text-xl font-bold text-gray-900 dark:text-white flex items-center">
<i class="fas fa-terminal mr-2 text-blue-500 dark:text-blue-400"></i>Command Configuration
</h3>
<!-- Use the RcloneFlags component from common package -->
@common.RcloneFlags()
</div>
<!-- Source Configuration Section -->
<div class="p-5 bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700">
<h3 class="mb-4 text-xl font-bold text-gray-900 dark:text-white flex items-center">
@@ -413,8 +490,8 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
@common.FilePatternFields()
</div>
<!-- Destination Configuration Section -->
<div class="p-5 bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700">
<!-- Destination Configuration Section (only shown if required) -->
<div x-show="requiresDestination" x-transition class="p-5 bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700">
<h3 class="mb-4 text-xl font-bold text-gray-900 dark:text-white flex items-center">
<i class="fas fa-download mr-2 text-blue-500 dark:text-blue-400"></i>Destination Configuration
</h3>
@@ -476,11 +553,7 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
@common.ArchiveOptions()
</div>
<!-- Rclone flags -->
<div>
<h4 class="text-lg font-medium text-gray-900 dark:text-white mb-4">Rclone Configuration</h4>
@common.RcloneFlags()
</div>
</div>
<!-- Form Actions -->
+8 -42
View File
@@ -3,6 +3,7 @@ package components
import (
"context"
"fmt"
"strings"
"github.com/gin-gonic/gin"
"time"
)
@@ -10,7 +11,7 @@ import (
// AppVersion will be set at build time using ldflags
// Example build command:
// go build -ldflags "-X github.com/starfleetcptn/gomft/components.AppVersion=1.2.3"
var AppVersion = "dev"
var AppVersion = "DEV"
// GetReleaseURL returns the URL to the specific GitHub release
func GetReleaseURL() templ.SafeURL {
@@ -62,42 +63,7 @@ templ LayoutWithContext(title string, ctx context.Context) {
<script src="/static/js/app.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"/>
<link rel="stylesheet" href="/static/css/app.css"/>
<!-- Notyf Toast Notifications -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/notyf@3/notyf.min.css"/>
<script src="https://cdn.jsdelivr.net/npm/notyf@3/notyf.min.js"></script>
<script>
// Initialize Notyf and make it available globally
document.addEventListener('DOMContentLoaded', function() {
window.notyfInstance = new Notyf({
duration: 3000,
position: {
x: 'right',
y: 'bottom',
},
types: [
{
type: 'success',
className: 'notyf__toast--success',
background: '#10B981',
icon: {
className: 'fas fa-check-circle',
tagName: 'i'
}
},
{
type: 'error',
className: 'notyf__toast--error',
background: '#EF4444',
icon: {
className: 'fas fa-exclamation-circle',
tagName: 'i'
}
}
]
});
});
</script>
<script>
tailwind.config = {
darkMode: 'class',
@@ -530,17 +496,17 @@ func isLoggedIn(ctx context.Context) bool {
func getUserInitial(ctx context.Context) string {
// Try as string first
if username, ok := ctx.Value("username").(string); ok && username != "" {
return string(username[0])
return strings.ToUpper(string(username[0]))
}
// Try as interface{} (from JWT claims)
if username, ok := ctx.Value("username").(interface{}); ok {
if strVal, ok := username.(string); ok && strVal != "" {
return string(strVal[0])
return strings.ToUpper(string(strVal[0]))
}
}
// Try email as fallback
if email, ok := ctx.Value("email").(string); ok && email != "" {
return string(email[0])
return strings.ToUpper(string(email[0]))
}
return "U"
}
@@ -549,12 +515,12 @@ func getUserInitial(ctx context.Context) string {
func getUserEmail(ctx context.Context) string {
// Try as string first
if email, ok := ctx.Value("email").(string); ok && email != "" {
return email
return strings.ToLower(email)
}
// Try as interface{} (from JWT claims)
if email, ok := ctx.Value("email").(interface{}); ok {
if strVal, ok := email.(string); ok && strVal != "" {
return strVal
return strings.ToLower(strVal)
}
}
return "user@example.com"
+179 -22
View File
@@ -160,6 +160,7 @@ templ RcloneFlags() {
<i class="fas fa-flag text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" name="rclone_flags" id="rclone_flags" x-model="rcloneFlags"
@change="updateCommandRequirements()"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="--transfers 4 --checkers 8" />
</div>
@@ -198,8 +199,8 @@ templ SourceSelection() {
<option value="smb">SMB</option>
<option value="nextcloud">NextCloud</option>
<option value="webdav">WebDAV</option>
<option value="gdrive">Google Drive</option>
<option value="gphotos">Google Photos</option>
<option value="gdrive">Google Drive (BETA)</option>
<option value="gphotos">Google Photos (BETA)</option>
</select>
</div>
</div>
@@ -222,8 +223,8 @@ templ DestinationSelection() {
<option value="smb">SMB</option>
<option value="nextcloud">NextCloud</option>
<option value="webdav">WebDAV</option>
<option value="gdrive">Google Drive</option>
<option value="gphotos">Google Photos</option>
<option value="gdrive">Google Drive (BETA)</option>
<option value="gphotos">Google Photos (BETA)</option>
</select>
</div>
</div>
@@ -236,6 +237,7 @@ templ RcloneCommandOptionsContent(categoryMap map[string][]db.RcloneCommand, cat
hx-target="#command-flags-container"
hx-trigger="change"
hx-include="[name='command_id']"
@change="updateCommandRequirements()"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
<option value="">Select command...</option>
for _, category := range categories {
@@ -266,33 +268,127 @@ templ RcloneCommandFlagsContent(command *db.RcloneCommand) {
<h5 class="font-medium text-gray-900 dark:text-white mb-2">Command Flags:</h5>
<div class="space-y-3">
for _, flag := range command.Flags {
<div class="flex items-start">
<input
type="checkbox"
id={ fmt.Sprintf("flag_%d", flag.ID) }
name="command_flags"
value={ fmt.Sprintf("%d", flag.ID) }
class="mt-0.5 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:focus:ring-blue-600"
/>
<div class="ml-3">
<label for={ fmt.Sprintf("flag_%d", flag.ID) } class="font-medium text-gray-900 dark:text-white">
{ flag.Name } - { flag.Description }
</label>
if flag.DefaultValue != "" {
<p class="text-xs text-gray-500 dark:text-gray-400">Default: { flag.DefaultValue }</p>
}
if flag.DataType == "bool" {
<!-- Boolean flag (simple checkbox) -->
<div class="flex items-start mb-4">
<input
type="checkbox"
id={ fmt.Sprintf("flag_%d", flag.ID) }
name="command_flags"
value={ fmt.Sprintf("%d", flag.ID) }
class="mt-0.5 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:focus:ring-blue-600"
/>
<div class="ml-3">
<label for={ fmt.Sprintf("flag_%d", flag.ID) } class="font-medium text-gray-900 dark:text-white">
{ flag.Name } - { flag.Description }
</label>
if flag.DefaultValue != "" {
<p class="text-xs text-gray-500 dark:text-gray-400">Default: { flag.DefaultValue }</p>
}
</div>
</div>
</div>
} else {
<!-- Non-boolean flag (requires value) -->
<div class="flex items-start mb-4 w-full">
<div class="w-full">
<div class="flex items-center mb-2">
<input
type="checkbox"
id={ fmt.Sprintf("flag_enable_%d", flag.ID) }
name={ fmt.Sprintf("flag_enable_%d", flag.ID) }
class="mr-2 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:focus:ring-blue-600"
data-input-id={ fmt.Sprintf("flag_value_%d", flag.ID) }
onclick="toggleFlagValue(this)"
/>
<label for={ fmt.Sprintf("flag_enable_%d", flag.ID) } class="font-medium text-gray-900 dark:text-white">
{ flag.Name } - { flag.Description }
</label>
</div>
<div class="w-full mt-2">
@renderFlagInput(flag)
if flag.DefaultValue != "" {
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Default: { flag.DefaultValue }</p>
}
</div>
</div>
</div>
}
}
</div>
</div>
<!-- JavaScript to handle enabling/disabling flag value inputs -->
<script>
function toggleFlagValue(checkbox) {
const inputId = checkbox.getAttribute('data-input-id');
const input = document.getElementById(inputId);
if (input) {
input.disabled = !checkbox.checked;
if (checkbox.checked) {
// Focus on the input when enabled
input.focus();
}
// Also enable/disable the hidden input for flag ID
const hiddenId = inputId.replace('flag_value_', 'flag_hidden_');
const hiddenInput = document.getElementById(hiddenId);
if (hiddenInput) {
hiddenInput.disabled = !checkbox.checked;
console.log('Toggle hidden input:', hiddenId, checkbox.checked);
}
// For debugging
console.log('Toggle flag value:', inputId, checkbox.checked);
}
}
// Initialize all flag inputs on page load
document.addEventListener('DOMContentLoaded', function() {
const checkboxes = document.querySelectorAll('input[id^="flag_enable_"]');
checkboxes.forEach(function(checkbox) {
const inputId = checkbox.getAttribute('data-input-id');
const input = document.getElementById(inputId);
if (input) {
input.disabled = !checkbox.checked;
// Also initialize the hidden input
const hiddenId = inputId.replace('flag_value_', 'flag_hidden_');
const hiddenInput = document.getElementById(hiddenId);
if (hiddenInput) {
hiddenInput.disabled = !checkbox.checked;
}
}
});
});
</script>
<div class="mt-4 p-3 bg-blue-50 text-blue-800 rounded-lg border border-blue-100 dark:bg-blue-900/20 dark:text-blue-300 dark:border-blue-900 text-sm">
<div class="flex items-center mb-1">
<i class="fas fa-info-circle mr-2"></i>
<span class="font-medium">Usage Example</span>
<span class="font-medium">Usage Examples</span>
</div>
<code class="block mt-1 font-mono text-xs overflow-x-auto">rclone { command.Name } [flags] source:path dest:path</code>
if command.Name == "ls" ||
command.Name == "lsd" ||
command.Name == "lsl" ||
command.Name == "lsjson" ||
command.Name == "lsf" ||
command.Name == "delete" ||
command.Name == "purge" ||
command.Name == "rmdirs" ||
command.Name == "mkdir" ||
command.Name == "touch" ||
command.Name == "md5sum" ||
command.Name == "sha1sum" ||
command.Name == "sha256sum" ||
command.Name == "size" ||
command.Name == "stat" ||
command.Name == "version" {
<code class="block mt-1 font-mono text-xs overflow-x-auto">rclone { command.Name } [flags] source:path</code>
} else {
<code class="block mt-1 font-mono text-xs overflow-x-auto">rclone { command.Name } [flags] source:path dest:path</code>
}
</div>
} else {
<div class="p-3 bg-gray-50 text-gray-600 rounded-lg border border-gray-200 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-700 text-sm">
@@ -303,4 +399,65 @@ templ RcloneCommandFlagsContent(command *db.RcloneCommand) {
</div>
}
</div>
}
// Helper function to render appropriate input based on flag data type
templ renderFlagInput(flag db.RcloneCommandFlag) {
if flag.DataType == "int" {
<input
type="number"
id={ fmt.Sprintf("flag_value_%d", flag.ID) }
name={ fmt.Sprintf("flag_value_%d", flag.ID) }
class="w-full bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder={ flag.DefaultValue }
disabled
/>
<!-- Hidden input to include this flag ID when checked -->
<input
type="hidden"
name="command_flags"
value={ fmt.Sprintf("%d", flag.ID) }
disabled
id={ fmt.Sprintf("flag_hidden_%d", flag.ID) }
data-enable-with={ fmt.Sprintf("flag_enable_%d", flag.ID) }
/>
} else if flag.DataType == "float" {
<input
type="number"
id={ fmt.Sprintf("flag_value_%d", flag.ID) }
name={ fmt.Sprintf("flag_value_%d", flag.ID) }
step="0.01"
class="w-full bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder={ flag.DefaultValue }
disabled
/>
<!-- Hidden input to include this flag ID when checked -->
<input
type="hidden"
name="command_flags"
value={ fmt.Sprintf("%d", flag.ID) }
disabled
id={ fmt.Sprintf("flag_hidden_%d", flag.ID) }
data-enable-with={ fmt.Sprintf("flag_enable_%d", flag.ID) }
/>
} else {
<!-- Default to text input for string and other types -->
<input
type="text"
id={ fmt.Sprintf("flag_value_%d", flag.ID) }
name={ fmt.Sprintf("flag_value_%d", flag.ID) }
class="w-full bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder={ flag.DefaultValue }
disabled
/>
<!-- Hidden input to include this flag ID when checked -->
<input
type="hidden"
name="command_flags"
value={ fmt.Sprintf("%d", flag.ID) }
disabled
id={ fmt.Sprintf("flag_hidden_%d", flag.ID) }
data-enable-with={ fmt.Sprintf("flag_enable_%d", flag.ID) }
/>
}
}
+4 -4
View File
@@ -8,7 +8,7 @@ templ FTPDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_host" name="dest_host" x-model="destinationHost" required
<input type="text" id="dest_host" name="dest_host" x-model="destinationHost" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="ftp.example.com" />
</div>
@@ -34,7 +34,7 @@ templ FTPDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-user text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_user" name="dest_user" x-model="destinationUser" required
<input type="text" id="dest_user" name="dest_user" x-model="destinationUser" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="username" />
</div>
@@ -47,7 +47,7 @@ templ FTPDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
</div>
<input type="password" id="dest_password" name="dest_password" x-model="destinationPassword" required
<input type="password" id="dest_password" name="dest_password" x-model="destinationPassword" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="Enter password" />
</div>
@@ -69,7 +69,7 @@ templ FTPDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-folder-open text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="destination_path" name="destination_path" x-model="destinationPath" required
<input type="text" id="destination_path" name="destination_path" x-model="destinationPath" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="/remote/path/to/files" />
</div>
+7 -7
View File
@@ -8,29 +8,29 @@ templ LocalDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-folder-open text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="destination_path" name="destination_path" x-model="destinationPath" required
:class="{ 'border-red-500 dark:border-red-700': destinationPathValid === false, 'border-green-500 dark:border-green-700': destinationPathValid === true }"
<input type="text" id="destination_path" name="destination_path" x-model="destinationPath" x-bind:required="requiresDestination"
:class="{ 'border-red-500 dark:border-red-700': destPathValid === false, 'border-green-500 dark:border-green-700': destPathValid === true }"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="/path/to/files" />
</div>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">Full path to the local directory containing your files</p>
<div x-show="destinationPathValid === false" class="mt-2 text-sm text-red-600 dark:text-red-400">
<div x-show="destPathValid === false" class="mt-2 text-sm text-red-600 dark:text-red-400">
<div class="flex items-center">
<i class="fas fa-exclamation-circle mr-2"></i>
<span x-text="destinationPathError"></span>
<span x-text="destPathError"></span>
</div>
</div>
<div x-show="destinationPathValid === true" class="mt-2 text-sm text-green-600 dark:text-green-400">
<div x-show="destPathValid === true" class="mt-2 text-sm text-green-600 dark:text-green-400">
<div class="flex items-center">
<i class="fas fa-check-circle mr-2"></i>
<span x-text="destinationPathError || 'Path is valid'"></span>
<span x-text="destPathError || 'Path is valid'"></span>
</div>
</div>
</div>
<button
type="button"
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
@click="checkPath(destinationPath, 'destination')"
@click="checkPath(destinationPath, 'dest')"
>
<svg class="w-4 h-4 mr-2 inline" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 10a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm-6.5 4.5A5.5 5.5 0 0 1 9 9h2a5.5 5.5 0 0 1 5.5 5.5V17a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1v-2.5Z"/>
+4 -4
View File
@@ -15,7 +15,7 @@ templ MinIODestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_endpoint" name="dest_endpoint" x-model="destinationEndpoint" required
<input type="text" id="dest_endpoint" name="dest_endpoint" x-model="destinationEndpoint" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="https://minio.example.com" />
</div>
@@ -30,7 +30,7 @@ templ MinIODestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-archive text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_bucket" name="dest_bucket" x-model="destinationBucket" required
<input type="text" id="dest_bucket" name="dest_bucket" x-model="destinationBucket" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="my-bucket" />
</div>
@@ -45,7 +45,7 @@ templ MinIODestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-key text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_access_key" name="dest_access_key" x-model="destinationAccessKey" required
<input type="text" id="dest_access_key" name="dest_access_key" x-model="destinationAccessKey" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="minioadmin" />
</div>
@@ -60,7 +60,7 @@ templ MinIODestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
</div>
<input type="password" id="dest_secret_key" name="dest_secret_key" x-model="destinationSecretKey" required
<input type="password" id="dest_secret_key" name="dest_secret_key" x-model="destinationSecretKey" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="minioadmin" />
</div>
@@ -15,7 +15,7 @@ templ NextCloudDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-cloud text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_endpoint" name="dest_endpoint" x-model="destinationEndpoint" required
<input type="text" id="dest_endpoint" name="dest_endpoint" x-model="destinationEndpoint" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="https://nextcloud.example.com" />
</div>
@@ -30,7 +30,7 @@ templ NextCloudDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-user text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_user" name="dest_user" x-model="destinationUser" required
<input type="text" id="dest_user" name="dest_user" x-model="destinationUser" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="nextcloud_username" />
</div>
@@ -45,7 +45,7 @@ templ NextCloudDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
</div>
<input type="password" id="dest_password" name="dest_password" x-model="destinationPassword" required
<input type="password" id="dest_password" name="dest_password" x-model="destinationPassword" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="NextCloud password" />
</div>
@@ -60,7 +60,7 @@ templ NextCloudDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-folder-open text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="destination_path" name="destination_path" x-model="destinationPath" required
<input type="text" id="destination_path" name="destination_path" x-model="destinationPath" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="remote.php/dav/files/username/path/to/files" />
</div>
+4 -4
View File
@@ -15,7 +15,7 @@ templ S3DestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-globe text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_region" name="dest_region" x-model="destinationRegion" required
<input type="text" id="dest_region" name="dest_region" x-model="destinationRegion" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="us-east-1" />
</div>
@@ -30,7 +30,7 @@ templ S3DestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-archive text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_bucket" name="dest_bucket" x-model="destinationBucket" required
<input type="text" id="dest_bucket" name="dest_bucket" x-model="destinationBucket" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="my-bucket" />
</div>
@@ -45,7 +45,7 @@ templ S3DestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-key text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_access_key" name="dest_access_key" x-model="destinationAccessKey" required
<input type="text" id="dest_access_key" name="dest_access_key" x-model="destinationAccessKey" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="AKIAIOSFODNN7EXAMPLE" />
</div>
@@ -60,7 +60,7 @@ templ S3DestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
</div>
<input type="password" id="dest_secret_key" name="dest_secret_key" x-model="destinationSecretKey" required
<input type="password" id="dest_secret_key" name="dest_secret_key" x-model="destinationSecretKey" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" />
</div>
+3 -3
View File
@@ -8,7 +8,7 @@ templ SFTPDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_host" name="dest_host" x-model="destinationHost" required
<input type="text" id="dest_host" name="dest_host" x-model="destinationHost" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="sftp.example.com" />
</div>
@@ -34,7 +34,7 @@ templ SFTPDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-user text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_user" name="dest_user" x-model="destinationUser" required
<input type="text" id="dest_user" name="dest_user" x-model="destinationUser" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="username" />
</div>
@@ -87,7 +87,7 @@ templ SFTPDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-folder-open text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="destination_path" name="destination_path" x-model="destinationPath" required
<input type="text" id="destination_path" name="destination_path" x-model="destinationPath" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="/remote/path/to/files" />
</div>
+4 -4
View File
@@ -15,7 +15,7 @@ templ SMBDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_host" name="dest_host" x-model="destinationHost" required
<input type="text" id="dest_host" name="dest_host" x-model="destinationHost" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="192.168.1.100 or server.example.com" />
</div>
@@ -30,7 +30,7 @@ templ SMBDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-share-alt text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_share" name="dest_share" x-model="destinationShare" required
<input type="text" id="dest_share" name="dest_share" x-model="destinationShare" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="Documents" />
</div>
@@ -60,7 +60,7 @@ templ SMBDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-user text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_user" name="dest_user" x-model="destinationUser" required
<input type="text" id="dest_user" name="dest_user" x-model="destinationUser" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="smb_username" />
</div>
@@ -75,7 +75,7 @@ templ SMBDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
</div>
<input type="password" id="dest_password" name="dest_password" x-model="destinationPassword" required
<input type="password" id="dest_password" name="dest_password" x-model="destinationPassword" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="SMB password" />
</div>
@@ -15,7 +15,7 @@ templ WebDAVDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-globe text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_endpoint" name="dest_endpoint" x-model="destinationEndpoint" required
<input type="text" id="dest_endpoint" name="dest_endpoint" x-model="destinationEndpoint" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="https://webdav.example.com" />
</div>
@@ -30,7 +30,7 @@ templ WebDAVDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-user text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_user" name="dest_user" x-model="destinationUser" required
<input type="text" id="dest_user" name="dest_user" x-model="destinationUser" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="webdav_username" />
</div>
@@ -45,7 +45,7 @@ templ WebDAVDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
</div>
<input type="password" id="dest_password" name="dest_password" x-model="destinationPassword" required
<input type="password" id="dest_password" name="dest_password" x-model="destinationPassword" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="WebDAV password" />
</div>
+2 -2
View File
@@ -10,13 +10,13 @@ templ TwoFactorVerify(ctx context.Context, data TwoFactorVerifyData) {
@LayoutWithContext("Two-Factor Authentication", ctx) {
<div class="p-4 md:p-6 2xl:p-10">
<!-- Page Header -->
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div class="mb-6 flex flex-col items-center justify-center text-center">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
<i class="fas fa-shield-alt w-6 h-6 mr-2 text-blue-500"></i>
Two-Factor Authentication
</h1>
</div>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-6">
<p class="text-sm text-gray-500 dark:text-gray-400 mb-6 text-center">
Enter the verification code from your authenticator app to continue
</p>
-41
View File
@@ -198,44 +198,3 @@ templ UserEdit(ctx context.Context, data UserEditData) {
</div>
}
}
// JavaScript to handle URL parameters for displaying messages
script handleUrlParams() {
document.addEventListener('DOMContentLoaded', function() {
const params = new URLSearchParams(window.location.search);
const error = params.get('error');
const details = params.get('details');
const status = params.get('status');
if (error) {
const errorMsgEl = document.getElementById('error-message');
const errorTitleEl = errorMsgEl.querySelector('.error-title');
const errorDetailsEl = errorMsgEl.querySelector('.error-details');
errorTitleEl.textContent = error;
if (details) {
errorDetailsEl.textContent = details;
} else {
errorDetailsEl.textContent = '';
}
errorMsgEl.classList.remove('hidden');
// If notyf is available, use it
if (window.notyf) {
window.notyf.error(error);
}
}
if (status) {
const statusMsgEl = document.getElementById('status-message');
statusMsgEl.textContent = status;
statusMsgEl.classList.remove('hidden');
// If notyf is available, use it
if (window.notyf) {
window.notyf.success(status);
}
}
});
}
+1
View File
@@ -126,6 +126,7 @@ type TransferConfig struct {
// Rclone command fields
CommandID uint `gorm:"default:1" form:"command_id"` // Default to 'copy' command ID (1)
CommandFlags string `form:"command_flags"` // JSON string of selected flags
CommandFlagValues string `form:"command_flag_values"` // JSON string of flag values by ID
DeleteAfterTransfer *bool `gorm:"default:false" form:"delete_after_transfer"`
SkipProcessedFiles *bool `gorm:"default:true" form:"skip_processed_files"`
MaxConcurrentTransfers int `gorm:"default:4" form:"max_concurrent_transfers"` // Number of concurrent file transfers
@@ -78,6 +78,10 @@ func AddRcloneCommandToConfig() *gormigrate.Migration {
return err
}
if err := tx.Exec(`ALTER TABLE transfer_configs ADD COLUMN command_flag_values TEXT DEFAULT NULL`).Error; err != nil {
return err
}
return nil
},
Rollback: func(tx *gorm.DB) error {
@@ -90,6 +94,10 @@ func AddRcloneCommandToConfig() *gormigrate.Migration {
return err
}
if err := tx.Exec(`ALTER TABLE transfer_configs DROP COLUMN command_flag_values`).Error; err != nil {
return err
}
return nil
},
}
+73 -1
View File
@@ -1929,7 +1929,7 @@ func determineCommandType(commandName string) string {
return "transfer"
}
// executeSimpleCommand executes rclone commands that don't require file-by-file processing
// executeSimpleCommand executes a simple command (non file-by-file transfer)
func (s *Scheduler) executeSimpleCommand(cmdName string, cmdType string, job db.Job, config db.TransferConfig, history *db.JobHistory, configPath string) {
s.log.LogInfo("Executing simple command '%s' of type '%s' for job %d, config %d", cmdName, cmdType, job.ID, config.ID)
@@ -2139,3 +2139,75 @@ func (s *Scheduler) executeSimpleCommand(cmdName string, cmdType string, job db.
// Send webhook notification
s.sendWebhookNotification(&job, history, &config)
}
// prepareBaseArguments prepares the base arguments for a command
func (s *Scheduler) prepareBaseArguments(command string, config *db.TransferConfig, progressCallback func(string)) []string {
args := []string{command}
// Add rclone flags from the config
if config.CommandFlags != "" {
var flagIDs []uint
if err := json.Unmarshal([]byte(config.CommandFlags), &flagIDs); err != nil {
s.log.LogError("Error parsing command flags: %v", err)
} else {
// Get all available flags for this command and their values
flagsMap, err := s.db.GetRcloneCommandFlagsMap(config.CommandID)
if err != nil {
s.log.LogError("Error getting flags map: %v", err)
} else {
// Parse flag values if available
var flagValues map[uint]string
if config.CommandFlagValues != "" {
if err := json.Unmarshal([]byte(config.CommandFlagValues), &flagValues); err != nil {
s.log.LogError("Error parsing flag values: %v", err)
}
}
// Add each selected flag
for _, flagID := range flagIDs {
if flag, ok := flagsMap[flagID]; ok {
if flag.DataType == "bool" {
// Boolean flags don't have values
args = append(args, flag.Name)
} else if flagValues != nil {
// Check if we have a value for this flag
if value, ok := flagValues[flagID]; ok && value != "" {
args = append(args, flag.Name, value)
} else {
// If there's a default value, use it
if flag.DefaultValue != "" {
args = append(args, flag.Name, flag.DefaultValue)
} else {
// Skip flags without values
s.log.LogError("Skipping flag %s: no value provided", flag.Name)
}
}
}
}
}
}
}
}
// Add any additional rclone flags specified by the user
if config.RcloneFlags != "" {
additionalFlags := strings.Fields(config.RcloneFlags)
args = append(args, additionalFlags...)
}
// Add common rclone options
args = append(args, "--progress")
args = append(args, "--stats", "1s")
// Add config file location
configPath := s.db.GetConfigRclonePath(config)
args = append(args, "--config", configPath)
// Add progress callback
args = append(args, "--stats-one-line")
// Set JSON output for easier parsing
args = append(args, "--json")
return args
}
+63
View File
@@ -6,6 +6,7 @@ import (
"log"
"net/http"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
@@ -156,6 +157,37 @@ func (h *Handlers) HandleCreateConfig(c *gin.Context) {
}
}
// Process flag values for non-boolean flags
flagValues := make(map[uint]string)
for key, values := range c.Request.PostForm {
// Check if key is a flag value field (format: flag_value_ID)
if strings.HasPrefix(key, "flag_value_") {
flagIDStr := strings.TrimPrefix(key, "flag_value_")
flagID, err := strconv.ParseUint(flagIDStr, 10, 64)
if err != nil {
log.Printf("Error parsing flag value ID: %v", err)
continue
}
// Only process if the corresponding enable checkbox is checked
enableKey := fmt.Sprintf("flag_enable_%s", flagIDStr)
enableValue := c.Request.PostForm.Get(enableKey)
if enableValue == "on" && len(values) > 0 && values[0] != "" {
flagValues[uint(flagID)] = values[0]
}
}
}
// Store flag values as JSON if any exist
if len(flagValues) > 0 {
flagValuesJSON, err := json.Marshal(flagValues)
if err != nil {
log.Printf("Error marshaling flag values: %v", err)
} else {
config.CommandFlagValues = string(flagValuesJSON)
}
}
// Process builtin auth settings
useBuiltinAuthSourceVal := c.Request.FormValue("use_builtin_auth_source")
useBuiltinAuthSourceValue := useBuiltinAuthSourceVal == "on" || useBuiltinAuthSourceVal == "true"
@@ -343,6 +375,37 @@ func (h *Handlers) HandleUpdateConfig(c *gin.Context) {
}
}
// Process flag values for non-boolean flags
flagValues := make(map[uint]string)
for key, values := range c.Request.PostForm {
// Check if key is a flag value field (format: flag_value_ID)
if strings.HasPrefix(key, "flag_value_") {
flagIDStr := strings.TrimPrefix(key, "flag_value_")
flagID, err := strconv.ParseUint(flagIDStr, 10, 64)
if err != nil {
log.Printf("Error parsing flag value ID: %v", err)
continue
}
// Only process if the corresponding enable checkbox is checked
enableKey := fmt.Sprintf("flag_enable_%s", flagIDStr)
enableValue := c.Request.PostForm.Get(enableKey)
if enableValue == "on" && len(values) > 0 && values[0] != "" {
flagValues[uint(flagID)] = values[0]
}
}
}
// Store flag values as JSON if any exist
if len(flagValues) > 0 {
flagValuesJSON, err := json.Marshal(flagValues)
if err != nil {
log.Printf("Error marshaling flag values: %v", err)
} else {
config.CommandFlagValues = string(flagValuesJSON)
}
}
// Process builtin auth settings
useBuiltinAuthSourceVal := c.Request.FormValue("use_builtin_auth_source")
useBuiltinAuthSourceValue := useBuiltinAuthSourceVal == "on" || useBuiltinAuthSourceVal == "true"
+4 -4
View File
@@ -675,7 +675,7 @@ func (h *Handlers) HandleRunJob(c *gin.Context) {
var job db.Job
if err := h.DB.First(&job, id).Error; err != nil {
c.Header("Content-Type", "text/html")
c.String(http.StatusNotFound, "<script>window.notyfInstance.error('Job not found')</script>")
c.String(http.StatusNotFound, "Job not found")
return
}
@@ -685,7 +685,7 @@ func (h *Handlers) HandleRunJob(c *gin.Context) {
isAdmin, exists := c.Get("isAdmin")
if !exists || isAdmin != true {
c.Header("Content-Type", "text/html")
c.String(http.StatusForbidden, "<script>window.notyfInstance.error('You do not have permission to run this job')</script>")
c.String(http.StatusForbidden, "You do not have permission to run this job")
return
}
}
@@ -720,7 +720,7 @@ func (h *Handlers) HandleRunJob(c *gin.Context) {
// Run the job immediately using the scheduler
if err := h.Scheduler.RunJobNow(job.ID); err != nil {
c.Header("Content-Type", "text/html")
errorMsg := fmt.Sprintf("<script>window.notyfInstance.error('Failed to run job: %s')</script>", err.Error())
errorMsg := fmt.Sprintf("%s", err.Error())
c.String(http.StatusInternalServerError, errorMsg)
return
}
@@ -730,7 +730,7 @@ func (h *Handlers) HandleRunJob(c *gin.Context) {
c.Header("Content-Type", "text/html")
// Return HTML with JavaScript to trigger the notification
successScript := fmt.Sprintf("<script>window.notyfInstance.success('Job \"%s\" has been started successfully')</script>", jobName)
successScript := fmt.Sprintf("Job \"%s\" has been started successfully", jobName)
c.String(http.StatusOK, successScript)
}
+6
View File
@@ -4,6 +4,7 @@ import (
"html/template"
"log"
"net/http"
"sort"
"strconv"
"github.com/gin-gonic/gin"
@@ -76,6 +77,11 @@ func (h *RcloneHandler) RcloneCommandFlags(c *gin.Context) {
return
}
// Sort the flags alphabetically by name
sort.Slice(command.Flags, func(i, j int) bool {
return command.Flags[i].Name < command.Flags[j].Name
})
_ = common.RcloneCommandFlagsContent(command).Render(c.Request.Context(), c.Writer)
}