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
+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)
}