diff --git a/components/admin_audit.templ b/components/admin_audit.templ
index 9ad2913..8fc4e21 100644
--- a/components/admin_audit.templ
+++ b/components/admin_audit.templ
@@ -302,93 +302,6 @@ templ AdminAuditLogs(ctx context.Context, data AuditLogsData) {
-
}
}
diff --git a/components/config_form.templ b/components/config_form.templ
index f480858..a3dad3d 100644
--- a/components/config_form.templ
+++ b/components/config_form.templ
@@ -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) {
Choose a descriptive name to identify this configuration.
+
+
+
+ Command Configuration
+
+
+
+ @common.RcloneFlags()
+
+
@@ -413,8 +490,8 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
@common.FilePatternFields()
-
-
+
+
Destination Configuration
@@ -476,11 +553,7 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
@common.ArchiveOptions()
-
-
-
Rclone Configuration
- @common.RcloneFlags()
-
+
diff --git a/components/layout.templ b/components/layout.templ
index 4eb6cc4..f090a35 100644
--- a/components/layout.templ
+++ b/components/layout.templ
@@ -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) {
-
-
-
-
+
+
- Usage Example
+ Usage Examples
-
rclone { command.Name } [flags] source:path dest:path
+ 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" {
+
rclone { command.Name } [flags] source:path
+ } else {
+
rclone { command.Name } [flags] source:path dest:path
+ }
} else {
@@ -303,4 +399,65 @@ templ RcloneCommandFlagsContent(command *db.RcloneCommand) {
}
+}
+
+// Helper function to render appropriate input based on flag data type
+templ renderFlagInput(flag db.RcloneCommandFlag) {
+ if flag.DataType == "int" {
+
+
+
+ } else if flag.DataType == "float" {
+
+
+
+ } else {
+
+
+
+
+ }
}
\ No newline at end of file
diff --git a/components/providers/destination/ftp.templ b/components/providers/destination/ftp.templ
index 0ba5de7..60590c0 100644
--- a/components/providers/destination/ftp.templ
+++ b/components/providers/destination/ftp.templ
@@ -8,7 +8,7 @@ templ FTPDestinationForm() {
-
@@ -34,7 +34,7 @@ templ FTPDestinationForm() {
-
@@ -47,7 +47,7 @@ templ FTPDestinationForm() {
-
@@ -69,7 +69,7 @@ templ FTPDestinationForm() {
-
diff --git a/components/providers/destination/local.templ b/components/providers/destination/local.templ
index 9e80a0e..dc7c080 100644
--- a/components/providers/destination/local.templ
+++ b/components/providers/destination/local.templ
@@ -8,29 +8,29 @@ templ LocalDestinationForm() {
-
Full path to the local directory containing your files
-
+
-
@@ -30,7 +30,7 @@ templ MinIODestinationForm() {
-
@@ -45,7 +45,7 @@ templ MinIODestinationForm() {
-
@@ -60,7 +60,7 @@ templ MinIODestinationForm() {
-
diff --git a/components/providers/destination/nextcloud.templ b/components/providers/destination/nextcloud.templ
index bede4d7..d093a37 100644
--- a/components/providers/destination/nextcloud.templ
+++ b/components/providers/destination/nextcloud.templ
@@ -15,7 +15,7 @@ templ NextCloudDestinationForm() {
-
@@ -30,7 +30,7 @@ templ NextCloudDestinationForm() {
-
@@ -45,7 +45,7 @@ templ NextCloudDestinationForm() {
-
@@ -60,7 +60,7 @@ templ NextCloudDestinationForm() {
-
diff --git a/components/providers/destination/s3.templ b/components/providers/destination/s3.templ
index 50b630f..1e08420 100644
--- a/components/providers/destination/s3.templ
+++ b/components/providers/destination/s3.templ
@@ -15,7 +15,7 @@ templ S3DestinationForm() {
-
@@ -30,7 +30,7 @@ templ S3DestinationForm() {
-
@@ -45,7 +45,7 @@ templ S3DestinationForm() {
-
@@ -60,7 +60,7 @@ templ S3DestinationForm() {
-
diff --git a/components/providers/destination/sftp.templ b/components/providers/destination/sftp.templ
index 1bfd02a..5b865c0 100644
--- a/components/providers/destination/sftp.templ
+++ b/components/providers/destination/sftp.templ
@@ -8,7 +8,7 @@ templ SFTPDestinationForm() {
-
@@ -34,7 +34,7 @@ templ SFTPDestinationForm() {
-
@@ -87,7 +87,7 @@ templ SFTPDestinationForm() {
-
diff --git a/components/providers/destination/smb.templ b/components/providers/destination/smb.templ
index 597b273..06eff84 100644
--- a/components/providers/destination/smb.templ
+++ b/components/providers/destination/smb.templ
@@ -15,7 +15,7 @@ templ SMBDestinationForm() {
-
@@ -30,7 +30,7 @@ templ SMBDestinationForm() {
-
@@ -60,7 +60,7 @@ templ SMBDestinationForm() {
-
@@ -75,7 +75,7 @@ templ SMBDestinationForm() {
-
diff --git a/components/providers/destination/webdav.templ b/components/providers/destination/webdav.templ
index f50d0ab..9fd1e2e 100644
--- a/components/providers/destination/webdav.templ
+++ b/components/providers/destination/webdav.templ
@@ -15,7 +15,7 @@ templ WebDAVDestinationForm() {
-
@@ -30,7 +30,7 @@ templ WebDAVDestinationForm() {
-
@@ -45,7 +45,7 @@ templ WebDAVDestinationForm() {
-
diff --git a/components/two_factor_verify.templ b/components/two_factor_verify.templ
index 49584d9..cf8d873 100644
--- a/components/two_factor_verify.templ
+++ b/components/two_factor_verify.templ
@@ -10,13 +10,13 @@ templ TwoFactorVerify(ctx context.Context, data TwoFactorVerifyData) {
@LayoutWithContext("Two-Factor Authentication", ctx) {
-
+
Two-Factor Authentication
-
+
Enter the verification code from your authenticator app to continue
diff --git a/components/user_edit.templ b/components/user_edit.templ
index 3870133..37d0a3f 100644
--- a/components/user_edit.templ
+++ b/components/user_edit.templ
@@ -198,44 +198,3 @@ templ UserEdit(ctx context.Context, data UserEditData) {
}
}
-
-// 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);
- }
- }
- });
-}
\ No newline at end of file
diff --git a/internal/db/db.go b/internal/db/db.go
index 624f867..abc92f0 100644
--- a/internal/db/db.go
+++ b/internal/db/db.go
@@ -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
diff --git a/internal/db/migrations/010_add_rclone_command_to_config.go b/internal/db/migrations/010_add_rclone_command_to_config.go
index b1a3c76..7937f92 100644
--- a/internal/db/migrations/010_add_rclone_command_to_config.go
+++ b/internal/db/migrations/010_add_rclone_command_to_config.go
@@ -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
},
}
diff --git a/internal/scheduler/scheduler.go b/internal/scheduler/scheduler.go
index f58ca66..c94dcc7 100644
--- a/internal/scheduler/scheduler.go
+++ b/internal/scheduler/scheduler.go
@@ -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
+}
diff --git a/internal/web/handlers/config_handlers.go b/internal/web/handlers/config_handlers.go
index 17180eb..8350ac1 100644
--- a/internal/web/handlers/config_handlers.go
+++ b/internal/web/handlers/config_handlers.go
@@ -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"
diff --git a/internal/web/handlers/job_handlers.go b/internal/web/handlers/job_handlers.go
index 6ac8cfd..718d306 100644
--- a/internal/web/handlers/job_handlers.go
+++ b/internal/web/handlers/job_handlers.go
@@ -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, "")
+ 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, "")
+ 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("", 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("", jobName)
+ successScript := fmt.Sprintf("Job \"%s\" has been started successfully", jobName)
c.String(http.StatusOK, successScript)
}
diff --git a/internal/web/handlers/rclone_handlers.go b/internal/web/handlers/rclone_handlers.go
index e1c3afe..6c55e3f 100644
--- a/internal/web/handlers/rclone_handlers.go
+++ b/internal/web/handlers/rclone_handlers.go
@@ -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)
}