mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-09-08 15:41:20 +02:00
refactor: database files
This commit is contained in:
-2050
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// FileMetadata stores information about processed files
|
||||
type FileMetadata struct {
|
||||
ID uint `gorm:"primarykey"`
|
||||
JobID uint `gorm:"not null;index"`
|
||||
Job Job `gorm:"foreignkey:JobID"`
|
||||
ConfigID uint `gorm:"default:0"` // The specific config ID this file was processed with
|
||||
FileName string `gorm:"not null"`
|
||||
OriginalPath string `gorm:"not null"`
|
||||
FileSize int64 `gorm:"not null"`
|
||||
FileHash string `gorm:"index"` // MD5 or other hash for file identity
|
||||
CreationTime time.Time
|
||||
ModTime time.Time
|
||||
ProcessedTime time.Time `gorm:"not null"`
|
||||
DestinationPath string `gorm:"not null"`
|
||||
Status string `gorm:"not null"` // processed, archived, deleted, etc.
|
||||
ErrorMessage string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package db
|
||||
|
||||
// --- FileMetadata Store Methods ---
|
||||
|
||||
// CreateFileMetadata creates a new file metadata record
|
||||
func (db *DB) CreateFileMetadata(metadata *FileMetadata) error {
|
||||
return db.Create(metadata).Error
|
||||
}
|
||||
|
||||
// GetFileMetadataByJobAndName retrieves file metadata by job ID and filename
|
||||
func (db *DB) GetFileMetadataByJobAndName(jobID uint, fileName string) (*FileMetadata, error) {
|
||||
var metadata FileMetadata
|
||||
err := db.Where("job_id = ? AND file_name = ?", jobID, fileName).First(&metadata).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &metadata, nil
|
||||
}
|
||||
|
||||
// GetFileMetadataByHash retrieves file metadata by file hash
|
||||
func (db *DB) GetFileMetadataByHash(fileHash string) (*FileMetadata, error) {
|
||||
var metadata FileMetadata
|
||||
err := db.Where("file_hash = ?", fileHash).First(&metadata).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &metadata, nil
|
||||
}
|
||||
|
||||
// DeleteFileMetadata deletes file metadata by ID
|
||||
func (db *DB) DeleteFileMetadata(id uint) error {
|
||||
return db.Delete(&FileMetadata{}, id).Error
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Job represents a scheduled transfer task
|
||||
type Job struct {
|
||||
ID uint `gorm:"primarykey"`
|
||||
Name string `form:"name"`
|
||||
ConfigID uint `gorm:"not null" form:"config_id"`
|
||||
Config TransferConfig `gorm:"foreignkey:ConfigID"`
|
||||
ConfigIDs string `gorm:"column:config_ids"` // Comma-separated list of config IDs
|
||||
Schedule string `gorm:"not null" form:"schedule"`
|
||||
Enabled *bool `gorm:"default:true" form:"enabled"`
|
||||
LastRun *time.Time
|
||||
NextRun *time.Time
|
||||
// Webhook notification fields
|
||||
WebhookEnabled *bool `gorm:"default:false" form:"webhook_enabled"`
|
||||
WebhookURL string `form:"webhook_url"`
|
||||
WebhookSecret string `form:"webhook_secret"`
|
||||
WebhookHeaders string `form:"webhook_headers"` // JSON-encoded headers
|
||||
NotifyOnSuccess *bool `gorm:"default:true" form:"notify_on_success"`
|
||||
NotifyOnFailure *bool `gorm:"default:true" form:"notify_on_failure"`
|
||||
CreatedBy uint
|
||||
User User `gorm:"foreignkey:CreatedBy"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// JobHistory records the execution history of a job
|
||||
type JobHistory struct {
|
||||
ID uint `gorm:"primarykey"`
|
||||
JobID uint `gorm:"not null"`
|
||||
Job Job `gorm:"foreignkey:JobID"`
|
||||
ConfigID uint `gorm:"default:0"` // The specific config ID this history entry is for
|
||||
StartTime time.Time `gorm:"not null"`
|
||||
EndTime *time.Time
|
||||
Status string `gorm:"not null"`
|
||||
BytesTransferred int64
|
||||
FilesTransferred int
|
||||
ErrorMessage string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// --- Job Helper Methods ---
|
||||
|
||||
// GetConfigIDsList returns the list of config IDs as integers
|
||||
func (j *Job) GetConfigIDsList() []uint {
|
||||
if j.ConfigIDs == "" {
|
||||
// If ConfigIDs is empty but ConfigID is set, return that as the only ID
|
||||
if j.ConfigID > 0 {
|
||||
return []uint{j.ConfigID}
|
||||
}
|
||||
return []uint{}
|
||||
}
|
||||
|
||||
// Split the comma-separated string
|
||||
strIDs := strings.Split(j.ConfigIDs, ",")
|
||||
ids := make([]uint, 0, len(strIDs))
|
||||
|
||||
// Convert each string to uint
|
||||
for _, strID := range strIDs {
|
||||
if id, err := strconv.ParseUint(strings.TrimSpace(strID), 10, 32); err == nil {
|
||||
ids = append(ids, uint(id))
|
||||
}
|
||||
}
|
||||
|
||||
return ids
|
||||
}
|
||||
|
||||
// SetConfigIDsList sets the config IDs from a slice of uint
|
||||
func (j *Job) SetConfigIDsList(ids []uint) {
|
||||
// Convert to strings
|
||||
strIDs := make([]string, len(ids))
|
||||
for i, id := range ids {
|
||||
strIDs[i] = strconv.FormatUint(uint64(id), 10)
|
||||
}
|
||||
|
||||
// Join with commas
|
||||
j.ConfigIDs = strings.Join(strIDs, ",")
|
||||
|
||||
// Debug log the final ConfigIDs string
|
||||
log.Printf("SetConfigIDsList: Setting ConfigIDs to: %s (from %v)", j.ConfigIDs, ids)
|
||||
|
||||
// If there's at least one ID, set ConfigID to the first one for backward compatibility
|
||||
if len(ids) > 0 {
|
||||
j.ConfigID = ids[0]
|
||||
} else {
|
||||
j.ConfigID = 0 // Ensure ConfigID is cleared if the list is empty
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfigIDsAsStrings returns the list of config IDs as strings for template rendering
|
||||
func (j *Job) GetConfigIDsAsStrings() []string {
|
||||
ids := j.GetConfigIDsList()
|
||||
strIDs := make([]string, len(ids))
|
||||
|
||||
for i, id := range ids {
|
||||
strIDs[i] = fmt.Sprintf("'%d'", id)
|
||||
}
|
||||
|
||||
return strIDs
|
||||
}
|
||||
|
||||
// GetEnabled returns the value of Enabled with a default if nil
|
||||
func (j *Job) GetEnabled() bool {
|
||||
if j.Enabled == nil {
|
||||
return true // Default to true if not set
|
||||
}
|
||||
return *j.Enabled
|
||||
}
|
||||
|
||||
// SetEnabled sets the Enabled field
|
||||
func (j *Job) SetEnabled(value bool) {
|
||||
j.Enabled = &value
|
||||
}
|
||||
|
||||
// GetWebhookEnabled returns the value of WebhookEnabled with a default if nil
|
||||
func (j *Job) GetWebhookEnabled() bool {
|
||||
if j.WebhookEnabled == nil {
|
||||
return false // Default to false if not set
|
||||
}
|
||||
return *j.WebhookEnabled
|
||||
}
|
||||
|
||||
// SetWebhookEnabled sets the WebhookEnabled field
|
||||
func (j *Job) SetWebhookEnabled(value bool) {
|
||||
j.WebhookEnabled = &value
|
||||
}
|
||||
|
||||
// GetNotifyOnSuccess returns the value of NotifyOnSuccess with a default if nil
|
||||
func (j *Job) GetNotifyOnSuccess() bool {
|
||||
if j.NotifyOnSuccess == nil {
|
||||
return true // Default to true if not set
|
||||
}
|
||||
return *j.NotifyOnSuccess
|
||||
}
|
||||
|
||||
// SetNotifyOnSuccess sets the NotifyOnSuccess field
|
||||
func (j *Job) SetNotifyOnSuccess(value bool) {
|
||||
j.NotifyOnSuccess = &value
|
||||
}
|
||||
|
||||
// GetNotifyOnFailure returns the value of NotifyOnFailure with a default if nil
|
||||
func (j *Job) GetNotifyOnFailure() bool {
|
||||
if j.NotifyOnFailure == nil {
|
||||
return true // Default to true if not set
|
||||
}
|
||||
return *j.NotifyOnFailure
|
||||
}
|
||||
|
||||
// SetNotifyOnFailure sets the NotifyOnFailure field
|
||||
func (j *Job) SetNotifyOnFailure(value bool) {
|
||||
j.NotifyOnFailure = &value
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
// --- Job Store Methods ---
|
||||
|
||||
// CreateJob creates a new job record
|
||||
func (db *DB) CreateJob(job *Job) error {
|
||||
// Use Omit to prevent GORM from creating a new config
|
||||
return db.Omit("Config").Create(job).Error
|
||||
}
|
||||
|
||||
// GetJobs retrieves all jobs for a user, preloading the associated config
|
||||
func (db *DB) GetJobs(userID uint) ([]Job, error) {
|
||||
var jobs []Job
|
||||
err := db.Preload("Config").Where("created_by = ?", userID).Find(&jobs).Error
|
||||
return jobs, err
|
||||
}
|
||||
|
||||
// GetJob retrieves a single job by ID, preloading the associated config
|
||||
func (db *DB) GetJob(id uint) (*Job, error) {
|
||||
var job Job
|
||||
err := db.Preload("Config").First(&job, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &job, nil
|
||||
}
|
||||
|
||||
// UpdateJob updates an existing job record
|
||||
func (db *DB) UpdateJob(job *Job) error {
|
||||
log.Printf("UpdateJob: Updating job ID: %d, ConfigIDs: %s", job.ID, job.ConfigIDs)
|
||||
|
||||
// Use Omit to prevent GORM from updating or creating a new config
|
||||
// Explicitly update fields that can be changed
|
||||
return db.Model(&Job{}).
|
||||
Where("id = ?", job.ID).
|
||||
Omit("Config"). // Omit the nested Config struct
|
||||
Updates(map[string]interface{}{
|
||||
"name": job.Name,
|
||||
"config_id": job.ConfigID, // Update the foreign key if needed
|
||||
"config_ids": job.ConfigIDs, // Explicitly update config_ids string
|
||||
"schedule": job.Schedule,
|
||||
"enabled": job.Enabled,
|
||||
"webhook_enabled": job.WebhookEnabled,
|
||||
"webhook_url": job.WebhookURL,
|
||||
"webhook_secret": job.WebhookSecret,
|
||||
"webhook_headers": job.WebhookHeaders,
|
||||
"notify_on_success": job.NotifyOnSuccess,
|
||||
"notify_on_failure": job.NotifyOnFailure,
|
||||
// Do not update LastRun, NextRun, CreatedBy, CreatedAt, UpdatedAt here
|
||||
// GORM handles UpdatedAt automatically
|
||||
}).Error
|
||||
}
|
||||
|
||||
// DeleteJob deletes a job and its associated history records
|
||||
func (db *DB) DeleteJob(id uint) error {
|
||||
// Start transaction
|
||||
tx := db.Begin()
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
// Delete associated job history records first
|
||||
if err := tx.Where("job_id = ?", id).Delete(&JobHistory{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("failed to delete job history: %v", err)
|
||||
}
|
||||
|
||||
// Delete the job
|
||||
if err := tx.Delete(&Job{}, id).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("failed to delete job: %v", err)
|
||||
}
|
||||
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
// UpdateJobStatus updates the LastRun and NextRun fields of a job
|
||||
func (db *DB) UpdateJobStatus(job *Job) error {
|
||||
// Only update specific fields related to run status
|
||||
return db.Model(job).Updates(map[string]interface{}{
|
||||
"last_run": job.LastRun,
|
||||
"next_run": job.NextRun,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// GetActiveJobs returns all active (enabled) jobs
|
||||
func (db *DB) GetActiveJobs() ([]Job, error) {
|
||||
if db.DB == nil {
|
||||
return nil, fmt.Errorf("database connection is nil")
|
||||
}
|
||||
var jobs []Job
|
||||
// For boolean pointer fields, need to check either NULL (for default) or true value
|
||||
err := db.Preload("Config").Where("enabled IS NULL OR enabled = ?", true).Find(&jobs).Error
|
||||
return jobs, err
|
||||
}
|
||||
|
||||
// GetConfigsForJob returns all transfer configurations associated with a job, in the order specified by ConfigIDs
|
||||
func (db *DB) GetConfigsForJob(jobID uint) ([]TransferConfig, error) {
|
||||
var job Job
|
||||
if err := db.First(&job, jobID).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get job %d: %w", jobID, err)
|
||||
}
|
||||
|
||||
configIDs := job.GetConfigIDsList()
|
||||
if len(configIDs) == 0 {
|
||||
return []TransferConfig{}, nil // No configs associated
|
||||
}
|
||||
|
||||
var configs []TransferConfig
|
||||
if err := db.Where("id IN ?", configIDs).Find(&configs).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get configs for job %d: %w", jobID, err)
|
||||
}
|
||||
|
||||
// Order the fetched configs according to the job.ConfigIDs list
|
||||
configMap := make(map[uint]TransferConfig, len(configs))
|
||||
for _, cfg := range configs {
|
||||
configMap[cfg.ID] = cfg
|
||||
}
|
||||
|
||||
orderedConfigs := make([]TransferConfig, 0, len(configIDs))
|
||||
for _, id := range configIDs {
|
||||
if cfg, ok := configMap[id]; ok {
|
||||
orderedConfigs = append(orderedConfigs, cfg)
|
||||
} else {
|
||||
log.Printf("Warning: Config ID %d listed in job %d not found in database", id, jobID)
|
||||
}
|
||||
}
|
||||
|
||||
return orderedConfigs, nil
|
||||
}
|
||||
|
||||
// --- JobHistory Store Methods ---
|
||||
|
||||
// CreateJobHistory creates a new job history record
|
||||
func (db *DB) CreateJobHistory(history *JobHistory) error {
|
||||
return db.Create(history).Error
|
||||
}
|
||||
|
||||
// UpdateJobHistory updates an existing job history record
|
||||
func (db *DB) UpdateJobHistory(history *JobHistory) error {
|
||||
return db.Save(history).Error
|
||||
}
|
||||
|
||||
// GetJobHistory retrieves all history records for a specific job, ordered by start time descending
|
||||
func (db *DB) GetJobHistory(jobID uint) ([]JobHistory, error) {
|
||||
var histories []JobHistory
|
||||
err := db.Where("job_id = ?", jobID).Order("start_time desc").Find(&histories).Error
|
||||
return histories, err
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RcloneCommand represents a command available in rclone
|
||||
type RcloneCommand struct {
|
||||
ID uint `gorm:"primarykey"`
|
||||
Name string `gorm:"not null;uniqueIndex"`
|
||||
Description string `gorm:"not null"`
|
||||
Category string `gorm:"not null;index"`
|
||||
IsAdvanced bool `gorm:"not null;default:false"`
|
||||
Flags []RcloneCommandFlag `gorm:"foreignKey:CommandID;constraint:OnDelete:CASCADE"`
|
||||
CreatedAt time.Time `gorm:"not null"`
|
||||
}
|
||||
|
||||
// RcloneCommandFlag represents a flag that can be used with an rclone command
|
||||
type RcloneCommandFlag struct {
|
||||
ID uint `gorm:"primarykey"`
|
||||
CommandID uint `gorm:"not null;index"`
|
||||
Command RcloneCommand `gorm:"foreignKey:CommandID"`
|
||||
Name string `gorm:"not null;index"`
|
||||
ShortName string
|
||||
Description string `gorm:"not null"`
|
||||
DataType string `gorm:"not null"` // string, int, bool, etc.
|
||||
IsRequired bool `gorm:"not null;default:false"`
|
||||
DefaultValue string
|
||||
CreatedAt time.Time `gorm:"not null"`
|
||||
}
|
||||
|
||||
// --- Rclone Helper Methods ---
|
||||
|
||||
// GetUsageExample returns a human-readable usage example for a flag
|
||||
func (flag *RcloneCommandFlag) GetUsageExample() string {
|
||||
switch flag.DataType {
|
||||
case "bool":
|
||||
return flag.Name
|
||||
case "int":
|
||||
return fmt.Sprintf("%s=<number>", flag.Name)
|
||||
case "float":
|
||||
return fmt.Sprintf("%s=<decimal>", flag.Name)
|
||||
case "string":
|
||||
return fmt.Sprintf("%s=<text>", flag.Name)
|
||||
default:
|
||||
return fmt.Sprintf("%s=<value>", flag.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// ParseRcloneFlags parses a string of rclone flags into a map
|
||||
// Note: This is a general utility function, not tied to a specific struct instance.
|
||||
// It might be better placed in a more general utility package if one exists,
|
||||
// but keeping it here for now as per the original file structure.
|
||||
func ParseRcloneFlags(flagsStr string) map[string]string {
|
||||
result := make(map[string]string)
|
||||
if flagsStr == "" {
|
||||
return result
|
||||
}
|
||||
|
||||
// Split the flags string by spaces
|
||||
parts := strings.Fields(flagsStr)
|
||||
|
||||
for i := 0; i < len(parts); i++ {
|
||||
part := parts[i]
|
||||
|
||||
// Check if it's a flag (starts with --)
|
||||
if strings.HasPrefix(part, "--") {
|
||||
// Remove the -- prefix
|
||||
flagName := part // Keep the '--' prefix in the map key for consistency? Or remove? Plan used remove.
|
||||
// flagName := strings.TrimPrefix(part, "--") // Alternative: remove prefix
|
||||
|
||||
// Check if the flag has a value
|
||||
if i+1 < len(parts) && !strings.HasPrefix(parts[i+1], "--") {
|
||||
// Next part is a value
|
||||
result[flagName] = parts[i+1]
|
||||
i++ // Skip the value in the next iteration
|
||||
} else {
|
||||
// Flag without value, treat as boolean true
|
||||
result[flagName] = "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// --- Rclone Store Methods ---
|
||||
|
||||
// GetRcloneCommands returns all rclone commands
|
||||
func (db *DB) GetRcloneCommands() ([]RcloneCommand, error) {
|
||||
var commands []RcloneCommand
|
||||
err := db.Find(&commands).Error
|
||||
return commands, err
|
||||
}
|
||||
|
||||
// GetRcloneCommand returns a specific rclone command by ID
|
||||
func (db *DB) GetRcloneCommand(id uint) (*RcloneCommand, error) {
|
||||
var command RcloneCommand
|
||||
err := db.First(&command, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &command, nil
|
||||
}
|
||||
|
||||
// GetRcloneCommandByName returns a specific rclone command by name
|
||||
func (db *DB) GetRcloneCommandByName(name string) (*RcloneCommand, error) {
|
||||
var command RcloneCommand
|
||||
err := db.Where("name = ?", name).First(&command).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &command, nil
|
||||
}
|
||||
|
||||
// GetRcloneCommandsInCategory returns all commands in a specific category
|
||||
func (db *DB) GetRcloneCommandsInCategory(category string) ([]RcloneCommand, error) {
|
||||
var commands []RcloneCommand
|
||||
err := db.Where("category = ?", category).Find(&commands).Error
|
||||
return commands, err
|
||||
}
|
||||
|
||||
// GetRcloneCommandFlag returns a specific flag by ID
|
||||
func (db *DB) GetRcloneCommandFlag(id uint) (*RcloneCommandFlag, error) {
|
||||
var flag RcloneCommandFlag
|
||||
err := db.First(&flag, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &flag, nil
|
||||
}
|
||||
|
||||
// GetRcloneCommandFlagByName returns a specific flag by name for a command
|
||||
func (db *DB) GetRcloneCommandFlagByName(commandID uint, name string) (*RcloneCommandFlag, error) {
|
||||
var flag RcloneCommandFlag
|
||||
err := db.Where("command_id = ? AND name = ?", commandID, name).First(&flag).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &flag, nil
|
||||
}
|
||||
|
||||
// GetRcloneCommandFlags returns all flags for a specific command
|
||||
func (db *DB) GetRcloneCommandFlags(commandID uint) ([]RcloneCommandFlag, error) {
|
||||
var flags []RcloneCommandFlag
|
||||
err := db.Where("command_id = ?", commandID).Find(&flags).Error
|
||||
return flags, err
|
||||
}
|
||||
|
||||
// GetRcloneCommandWithFlags returns a command with all its flags
|
||||
func (db *DB) GetRcloneCommandWithFlags(commandID uint) (*RcloneCommand, error) {
|
||||
var command RcloneCommand
|
||||
err := db.Preload("Flags").First(&command, commandID).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &command, nil
|
||||
}
|
||||
|
||||
// BuildRcloneCommand builds an rclone command string with the specified command and flags
|
||||
func (db *DB) BuildRcloneCommand(commandName string, flags map[string]string) (string, error) {
|
||||
// Get the command details
|
||||
command, err := db.GetRcloneCommandByName(commandName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("command not found: %s", commandName)
|
||||
}
|
||||
|
||||
// Start building the command string
|
||||
cmdStr := "rclone " + command.Name
|
||||
|
||||
// Get all flags for this command
|
||||
allFlags, err := db.GetRcloneCommandFlags(command.ID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get flags for command: %v", err)
|
||||
}
|
||||
|
||||
// Create a map of flag details for easy lookup
|
||||
flagDetails := make(map[string]RcloneCommandFlag)
|
||||
for _, f := range allFlags {
|
||||
flagDetails[f.Name] = f
|
||||
}
|
||||
|
||||
// Add the flags to the command
|
||||
for name, value := range flags {
|
||||
// Check if the flag exists for this command
|
||||
flag, exists := flagDetails[name]
|
||||
if !exists {
|
||||
// Allow passing flags not explicitly defined in the DB (e.g., global flags)
|
||||
// Consider adding validation or logging for unknown flags if stricter control is needed
|
||||
cmdStr += " " + name
|
||||
if value != "true" { // Assume boolean flags are passed as "true" if value is needed
|
||||
cmdStr += " " + value
|
||||
}
|
||||
continue
|
||||
// return "", fmt.Errorf("invalid flag for command %s: %s", commandName, name)
|
||||
}
|
||||
|
||||
// Handle different flag types
|
||||
switch flag.DataType {
|
||||
case "bool":
|
||||
if value == "true" {
|
||||
cmdStr += " " + flag.Name // Use flag.Name which includes '--'
|
||||
}
|
||||
default:
|
||||
cmdStr += " " + flag.Name + " " + value // Use flag.Name which includes '--'
|
||||
}
|
||||
}
|
||||
|
||||
return cmdStr, nil
|
||||
}
|
||||
|
||||
// ValidateRcloneFlags validates if the provided flags are valid for the command
|
||||
func (db *DB) ValidateRcloneFlags(commandName string, flags map[string]string) (bool, map[string]string) {
|
||||
// Initialize errors map
|
||||
errorsMap := make(map[string]string)
|
||||
|
||||
// Get the command details
|
||||
command, err := db.GetRcloneCommandByName(commandName)
|
||||
if err != nil {
|
||||
errorsMap["command"] = "Command not found: " + commandName
|
||||
return false, errorsMap
|
||||
}
|
||||
|
||||
// Get all flags for this command
|
||||
allFlags, err := db.GetRcloneCommandFlags(command.ID)
|
||||
if err != nil {
|
||||
errorsMap["command"] = "Failed to get flags for command"
|
||||
return false, errorsMap
|
||||
}
|
||||
|
||||
// Create a map of flag details for easy lookup
|
||||
flagDetails := make(map[string]RcloneCommandFlag)
|
||||
for _, f := range allFlags {
|
||||
flagDetails[f.Name] = f // Assuming Name includes '--' prefix
|
||||
}
|
||||
|
||||
// Check each provided flag
|
||||
for name, value := range flags {
|
||||
// Check if the flag exists for this command
|
||||
flag, exists := flagDetails[name]
|
||||
if !exists {
|
||||
// Allow unknown flags for now, but could add an error here if needed
|
||||
// errorsMap[name] = "Invalid flag for command " + commandName
|
||||
continue
|
||||
}
|
||||
|
||||
// Validate the flag value based on data type
|
||||
switch flag.DataType {
|
||||
case "int":
|
||||
if _, err := strconv.Atoi(value); err != nil {
|
||||
errorsMap[name] = "Value must be an integer"
|
||||
}
|
||||
case "float":
|
||||
if _, err := strconv.ParseFloat(value, 64); err != nil {
|
||||
errorsMap[name] = "Value must be a number"
|
||||
}
|
||||
case "bool":
|
||||
// For boolean flags passed in the map, the value should ideally be "true" or omitted
|
||||
// If present and not "true", it's likely an error or misuse.
|
||||
// Rclone CLI typically handles bool flags by presence/absence.
|
||||
// This validation might need refinement based on how flags are constructed before calling this.
|
||||
if value != "true" {
|
||||
// errorsMap[name] = "Boolean flag should have value 'true' or be omitted"
|
||||
}
|
||||
case "string":
|
||||
// Basic check: ensure value is not empty if flag requires a value
|
||||
// More complex validation (regex, length) could be added here
|
||||
if value == "" && flag.IsRequired { // Check if required string flags have values
|
||||
errorsMap[name] = "Value cannot be empty for required string flag"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for missing required flags
|
||||
for _, flag := range allFlags {
|
||||
if flag.IsRequired {
|
||||
if _, provided := flags[flag.Name]; !provided {
|
||||
// Check if the short name was provided instead
|
||||
shortNameProvided := false
|
||||
if flag.ShortName != "" {
|
||||
_, shortNameProvided = flags[flag.ShortName]
|
||||
}
|
||||
if !shortNameProvided {
|
||||
errorsMap[flag.Name] = "This flag is required"
|
||||
}
|
||||
} else if flag.DataType != "bool" && flags[flag.Name] == "" {
|
||||
// Required non-bool flags must have a value
|
||||
errorsMap[flag.Name] = "Value cannot be empty for required flag"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return len(errorsMap) == 0, errorsMap
|
||||
}
|
||||
|
||||
// GetRcloneCategories returns all unique categories of rclone commands
|
||||
func (db *DB) GetRcloneCategories() ([]string, error) {
|
||||
var categories []string
|
||||
err := db.Model(&RcloneCommand{}).Distinct("category").Pluck("category", &categories).Error
|
||||
return categories, err
|
||||
}
|
||||
|
||||
// GetRcloneCommandsByAdvanced returns commands filtered by their advanced status
|
||||
func (db *DB) GetRcloneCommandsByAdvanced(isAdvanced bool) ([]RcloneCommand, error) {
|
||||
var commands []RcloneCommand
|
||||
err := db.Where("is_advanced = ?", isAdvanced).Find(&commands).Error
|
||||
return commands, err
|
||||
}
|
||||
|
||||
// SearchRcloneCommands searches for commands by name or description
|
||||
func (db *DB) SearchRcloneCommands(query string) ([]RcloneCommand, error) {
|
||||
var commands []RcloneCommand
|
||||
searchQuery := "%" + query + "%"
|
||||
err := db.Where("name LIKE ? OR description LIKE ?", searchQuery, searchQuery).Find(&commands).Error
|
||||
return commands, err
|
||||
}
|
||||
|
||||
// GetRcloneCommandUsage returns a basic usage example for a command with its required flags
|
||||
func (db *DB) GetRcloneCommandUsage(commandID uint) (string, error) {
|
||||
command, err := db.GetRcloneCommandWithFlags(commandID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
usage := fmt.Sprintf("rclone %s [flags] <source> <dest>", command.Name)
|
||||
|
||||
// Add basic usage examples for required flags
|
||||
requiredFlags := []string{}
|
||||
for _, flag := range command.Flags {
|
||||
if flag.IsRequired {
|
||||
// Assuming GetUsageExample is defined on RcloneCommandFlag in rclone.go
|
||||
requiredFlags = append(requiredFlags, flag.GetUsageExample())
|
||||
}
|
||||
}
|
||||
|
||||
if len(requiredFlags) > 0 {
|
||||
usage += "\n\nRequired flags:\n " + strings.Join(requiredFlags, "\n ")
|
||||
}
|
||||
|
||||
return usage, nil
|
||||
}
|
||||
|
||||
// RenderRcloneCommandHelp generates a help text for a command with its flags
|
||||
func (db *DB) RenderRcloneCommandHelp(commandID uint) (string, error) {
|
||||
command, err := db.GetRcloneCommandWithFlags(commandID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Build the help text
|
||||
help := fmt.Sprintf("COMMAND: %s\n", command.Name)
|
||||
help += fmt.Sprintf("DESCRIPTION: %s\n\n", command.Description)
|
||||
help += "FLAGS:\n"
|
||||
|
||||
// Group flags by required status
|
||||
var requiredFlags, optionalFlags []RcloneCommandFlag
|
||||
for _, flag := range command.Flags {
|
||||
if flag.IsRequired {
|
||||
requiredFlags = append(requiredFlags, flag)
|
||||
} else {
|
||||
optionalFlags = append(optionalFlags, flag)
|
||||
}
|
||||
}
|
||||
|
||||
// Add required flags
|
||||
if len(requiredFlags) > 0 {
|
||||
help += " Required:\n"
|
||||
for _, flag := range requiredFlags {
|
||||
shortName := ""
|
||||
if flag.ShortName != "" {
|
||||
shortName = fmt.Sprintf(" (-%s)", flag.ShortName)
|
||||
}
|
||||
help += fmt.Sprintf(" %s%s - %s\n", flag.Name, shortName, flag.Description)
|
||||
if flag.DataType != "bool" && flag.DefaultValue != "" {
|
||||
help += fmt.Sprintf(" Default: %s\n", flag.DefaultValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add optional flags
|
||||
if len(optionalFlags) > 0 {
|
||||
help += "\n Optional:\n"
|
||||
for _, flag := range optionalFlags {
|
||||
shortName := ""
|
||||
if flag.ShortName != "" {
|
||||
shortName = fmt.Sprintf(" (-%s)", flag.ShortName)
|
||||
}
|
||||
help += fmt.Sprintf(" %s%s - %s\n", flag.Name, shortName, flag.Description)
|
||||
if flag.DataType != "bool" && flag.DefaultValue != "" {
|
||||
help += fmt.Sprintf(" Default: %s\n", flag.DefaultValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return help, nil
|
||||
}
|
||||
|
||||
// GetRcloneCommandFlagsMap returns all flags for a specific command as a map keyed by flag ID
|
||||
func (db *DB) GetRcloneCommandFlagsMap(commandID uint) (map[uint]RcloneCommandFlag, error) {
|
||||
flags, err := db.GetRcloneCommandFlags(commandID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
flagsMap := make(map[uint]RcloneCommandFlag)
|
||||
for _, flag := range flags {
|
||||
flagsMap[flag.ID] = flag
|
||||
}
|
||||
|
||||
return flagsMap, nil
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// --- Role Store Methods ---
|
||||
|
||||
// CreateRole creates a new role record
|
||||
func (db *DB) CreateRole(role *Role) error {
|
||||
return db.Create(role).Error
|
||||
}
|
||||
|
||||
// GetRole retrieves a role by ID, preloading permissions
|
||||
func (db *DB) GetRole(id uint) (*Role, error) {
|
||||
var role Role
|
||||
// Assuming Permissions are handled correctly by GORM or custom type
|
||||
err := db.First(&role, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &role, nil
|
||||
}
|
||||
|
||||
// GetRoleByName retrieves a role by name, preloading permissions
|
||||
func (db *DB) GetRoleByName(name string) (*Role, error) {
|
||||
var role Role
|
||||
err := db.Where("name = ?", name).First(&role).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &role, nil
|
||||
}
|
||||
|
||||
// UpdateRole updates an existing role record
|
||||
func (db *DB) UpdateRole(role *Role) error {
|
||||
// Use Omit Users to prevent GORM from trying to update the many2many relationship directly here
|
||||
return db.Omit("Users").Save(role).Error
|
||||
}
|
||||
|
||||
// DeleteRole deletes a role after checking dependencies and removing assignments
|
||||
func (db *DB) DeleteRole(id uint) error {
|
||||
var role Role
|
||||
if err := db.First(&role, id).Error; err != nil {
|
||||
return fmt.Errorf("role not found: %w", err)
|
||||
}
|
||||
|
||||
if role.IsSystemRole() {
|
||||
return errors.New("cannot delete system role")
|
||||
}
|
||||
|
||||
// Start transaction
|
||||
tx := db.Begin()
|
||||
if err := tx.Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Manually delete role assignments from the join table
|
||||
if err := tx.Exec("DELETE FROM user_roles WHERE role_id = ?", id).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("failed to delete role assignments: %w", err)
|
||||
}
|
||||
|
||||
// Delete the role itself
|
||||
if err := tx.Delete(&role).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("failed to delete role: %w", err)
|
||||
}
|
||||
|
||||
// Commit transaction
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
// ListRoles retrieves all roles
|
||||
func (db *DB) ListRoles() ([]Role, error) {
|
||||
var roles []Role
|
||||
err := db.Find(&roles).Error
|
||||
return roles, err
|
||||
}
|
||||
|
||||
// GetUserRoles retrieves all roles assigned to a specific user ID
|
||||
func (db *DB) GetUserRoles(userID uint) ([]Role, error) {
|
||||
var user User
|
||||
// Preload the Roles association
|
||||
if err := db.Preload("Roles").First(&user, userID).Error; err != nil {
|
||||
// Handle case where user might not be found vs. other errors
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fmt.Errorf("user with ID %d not found", userID)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get user %d roles: %w", userID, err)
|
||||
}
|
||||
return user.Roles, nil
|
||||
}
|
||||
|
||||
// AssignRoleToUser assigns a role to a user, handling the join table
|
||||
func (db *DB) AssignRoleToUser(roleID, userID, assignedByID uint) error {
|
||||
var role Role
|
||||
if err := db.First(&role, roleID).Error; err != nil {
|
||||
return fmt.Errorf("role with ID %d not found: %w", roleID, err)
|
||||
}
|
||||
var user User
|
||||
if err := db.First(&user, userID).Error; err != nil {
|
||||
return fmt.Errorf("user with ID %d not found: %w", userID, err)
|
||||
}
|
||||
|
||||
// Use GORM's Association API for many2many
|
||||
err := db.Model(&user).Association("Roles").Append(&role)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to assign role %d to user %d: %w", roleID, userID, err)
|
||||
}
|
||||
|
||||
// Optionally, log the assignment (consider moving audit logging to a dedicated service/hook)
|
||||
// db.Create(&AuditLog{...})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnassignRoleFromUser removes a role from a user, handling the join table
|
||||
func (db *DB) UnassignRoleFromUser(roleID, userID, unassignedByID uint) error {
|
||||
var role Role
|
||||
if err := db.First(&role, roleID).Error; err != nil {
|
||||
return fmt.Errorf("role with ID %d not found: %w", roleID, err)
|
||||
}
|
||||
var user User
|
||||
// Need to preload roles to check if the association exists before deleting
|
||||
if err := db.Preload("Roles").First(&user, userID).Error; err != nil {
|
||||
return fmt.Errorf("user with ID %d not found: %w", userID, err)
|
||||
}
|
||||
|
||||
// Use GORM's Association API for many2many deletion
|
||||
err := db.Model(&user).Association("Roles").Delete(&role)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unassign role %d from user %d: %w", roleID, userID, err)
|
||||
}
|
||||
|
||||
// Optionally, log the unassignment
|
||||
// db.Create(&AuditLog{...})
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// TransferConfig holds the configuration for a data transfer operation
|
||||
type TransferConfig struct {
|
||||
ID uint `gorm:"primarykey"`
|
||||
Name string `gorm:"not null" form:"name"`
|
||||
SourceType string `gorm:"not null" form:"source_type"`
|
||||
SourcePath string `gorm:"not null" form:"source_path"`
|
||||
SourceHost string `form:"source_host"`
|
||||
SourcePort int `gorm:"default:22" form:"source_port"`
|
||||
SourceUser string `form:"source_user"`
|
||||
SourcePassword string `form:"source_password" gorm:"-"` // Not stored in DB, only used for form
|
||||
SourceKeyFile string `form:"source_key_file"`
|
||||
// S3 source fields
|
||||
SourceBucket string `form:"source_bucket"`
|
||||
SourceRegion string `form:"source_region"`
|
||||
SourceAccessKey string `form:"source_access_key"`
|
||||
SourceSecretKey string `form:"source_secret_key" gorm:"-"` // Not stored in DB, only used for form
|
||||
SourceEndpoint string `form:"source_endpoint"`
|
||||
// SMB source fields
|
||||
SourceShare string `form:"source_share"`
|
||||
SourceDomain string `form:"source_domain"`
|
||||
// FTP source fields
|
||||
SourcePassiveMode *bool `gorm:"default:true" form:"source_passive_mode"`
|
||||
// OneDrive and Google Drive source fields
|
||||
SourceClientID string `form:"source_client_id"`
|
||||
SourceClientSecret string `form:"source_client_secret" gorm:"-"` // Not stored in DB, only used for form
|
||||
SourceDriveID string `form:"source_drive_id"` // For OneDrive
|
||||
SourceTeamDrive string `form:"source_team_drive"` // For Google Drive
|
||||
// Google Photos source fields
|
||||
SourceReadOnly *bool `form:"source_read_only"` // For Google Photos
|
||||
SourceStartYear int `form:"source_start_year"` // For Google Photos
|
||||
SourceIncludeArchived *bool `form:"source_include_archived"` // For Google Photos
|
||||
// General fields
|
||||
FilePattern string `gorm:"default:'*'" form:"file_pattern"`
|
||||
OutputPattern string `form:"output_pattern"` // Pattern for output filenames with date variables
|
||||
DestinationType string `gorm:"not null" form:"destination_type"`
|
||||
DestinationPath string `gorm:"not null" form:"destination_path"`
|
||||
DestHost string `form:"dest_host"`
|
||||
DestPort int `gorm:"default:22" form:"dest_port"`
|
||||
DestUser string `form:"dest_user"`
|
||||
DestPassword string `form:"dest_password" gorm:"-"` // Not stored in DB, only used for form
|
||||
DestKeyFile string `form:"dest_key_file"`
|
||||
// S3 destination fields
|
||||
DestBucket string `form:"dest_bucket"`
|
||||
DestRegion string `form:"dest_region"`
|
||||
DestAccessKey string `form:"dest_access_key"`
|
||||
DestSecretKey string `form:"dest_secret_key" gorm:"-"` // Not stored in DB, only used for form
|
||||
DestEndpoint string `form:"dest_endpoint"`
|
||||
// SMB destination fields
|
||||
DestShare string `form:"dest_share"`
|
||||
DestDomain string `form:"dest_domain"`
|
||||
// FTP destination fields
|
||||
DestPassiveMode *bool `gorm:"default:true" form:"dest_passive_mode"`
|
||||
// OneDrive and Google Drive destination fields
|
||||
DestClientID string `form:"dest_client_id"`
|
||||
DestClientSecret string `form:"dest_client_secret" gorm:"-"` // Not stored in DB, only used for form
|
||||
DestDriveID string `form:"dest_drive_id"` // For OneDrive
|
||||
DestTeamDrive string `form:"dest_team_drive"` // For Google Drive
|
||||
// Google Photos destination fields
|
||||
DestReadOnly *bool `form:"dest_read_only"` // For Google Photos
|
||||
DestStartYear int `form:"dest_start_year"` // For Google Photos
|
||||
DestIncludeArchived *bool `form:"dest_include_archived"` // For Google Photos
|
||||
// Security fields
|
||||
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
|
||||
// General fields
|
||||
ArchivePath string `form:"archive_path"`
|
||||
ArchiveEnabled *bool `gorm:"default:false" form:"archive_enabled"`
|
||||
RcloneFlags string `form:"rclone_flags"`
|
||||
// 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
|
||||
CreatedBy uint
|
||||
User User `gorm:"foreignkey:CreatedBy"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// --- TransferConfig Helper Methods ---
|
||||
|
||||
// GetSourcePassiveMode returns the value of SourcePassiveMode with a default if nil
|
||||
func (tc *TransferConfig) GetSourcePassiveMode() bool {
|
||||
if tc.SourcePassiveMode == nil {
|
||||
return true // Default to true if not set
|
||||
}
|
||||
return *tc.SourcePassiveMode
|
||||
}
|
||||
|
||||
// SetSourcePassiveMode sets the SourcePassiveMode field
|
||||
func (tc *TransferConfig) SetSourcePassiveMode(value bool) {
|
||||
tc.SourcePassiveMode = &value
|
||||
}
|
||||
|
||||
// GetDestPassiveMode returns the value of DestPassiveMode with a default if nil
|
||||
func (tc *TransferConfig) GetDestPassiveMode() bool {
|
||||
if tc.DestPassiveMode == nil {
|
||||
return true // Default to true if not set
|
||||
}
|
||||
return *tc.DestPassiveMode
|
||||
}
|
||||
|
||||
// SetDestPassiveMode sets the DestPassiveMode field
|
||||
func (tc *TransferConfig) SetDestPassiveMode(value bool) {
|
||||
tc.DestPassiveMode = &value
|
||||
}
|
||||
|
||||
// GetGoogleDriveAuthenticated returns whether the transfer config has been authenticated with Google Drive
|
||||
func (tc *TransferConfig) GetGoogleDriveAuthenticated() bool {
|
||||
return tc.GoogleDriveAuthenticated != nil && *tc.GoogleDriveAuthenticated
|
||||
}
|
||||
|
||||
// SetGoogleDriveAuthenticated sets the Google Drive authentication status
|
||||
func (tc *TransferConfig) SetGoogleDriveAuthenticated(value bool) {
|
||||
tc.GoogleDriveAuthenticated = &value
|
||||
}
|
||||
|
||||
// GetGoogleAuthenticated is an alias for GetGoogleDriveAuthenticated for better semantics when working with Google Photos
|
||||
func (tc *TransferConfig) GetGoogleAuthenticated() bool {
|
||||
return tc.GetGoogleDriveAuthenticated()
|
||||
}
|
||||
|
||||
// SetGoogleAuthenticated is an alias for SetGoogleDriveAuthenticated for better semantics when working with Google Photos
|
||||
func (tc *TransferConfig) SetGoogleAuthenticated(value bool) {
|
||||
tc.SetGoogleDriveAuthenticated(value)
|
||||
}
|
||||
|
||||
// GetArchiveEnabled returns the value of ArchiveEnabled with a default if nil
|
||||
func (tc *TransferConfig) GetArchiveEnabled() bool {
|
||||
if tc.ArchiveEnabled == nil {
|
||||
return false // Default to false if not set
|
||||
}
|
||||
return *tc.ArchiveEnabled
|
||||
}
|
||||
|
||||
// SetArchiveEnabled sets the ArchiveEnabled field
|
||||
func (tc *TransferConfig) SetArchiveEnabled(value bool) {
|
||||
tc.ArchiveEnabled = &value
|
||||
}
|
||||
|
||||
// GetDeleteAfterTransfer returns the value of DeleteAfterTransfer with a default if nil
|
||||
func (tc *TransferConfig) GetDeleteAfterTransfer() bool {
|
||||
if tc.DeleteAfterTransfer == nil {
|
||||
return false // Default to false if not set
|
||||
}
|
||||
return *tc.DeleteAfterTransfer
|
||||
}
|
||||
|
||||
// SetDeleteAfterTransfer sets the DeleteAfterTransfer field
|
||||
func (tc *TransferConfig) SetDeleteAfterTransfer(value bool) {
|
||||
tc.DeleteAfterTransfer = &value
|
||||
}
|
||||
|
||||
// GetSkipProcessedFiles returns the value of SkipProcessedFiles with a default if nil
|
||||
func (tc *TransferConfig) GetSkipProcessedFiles() bool {
|
||||
if tc.SkipProcessedFiles == nil {
|
||||
return true // Default to true if not set
|
||||
}
|
||||
return *tc.SkipProcessedFiles
|
||||
}
|
||||
|
||||
// SetSkipProcessedFiles sets the SkipProcessedFiles field
|
||||
func (tc *TransferConfig) SetSkipProcessedFiles(value bool) {
|
||||
tc.SkipProcessedFiles = &value
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,497 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// --- TransferConfig Store Methods ---
|
||||
|
||||
// CreateTransferConfig creates a new transfer config record
|
||||
func (db *DB) CreateTransferConfig(config *TransferConfig) error {
|
||||
return db.Create(config).Error
|
||||
}
|
||||
|
||||
// GetTransferConfigs retrieves all transfer configs for a user
|
||||
func (db *DB) GetTransferConfigs(userID uint) ([]TransferConfig, error) {
|
||||
var configs []TransferConfig
|
||||
err := db.Where("created_by = ?", userID).Find(&configs).Error
|
||||
return configs, err
|
||||
}
|
||||
|
||||
// GetTransferConfig retrieves a single transfer config by ID
|
||||
func (db *DB) GetTransferConfig(id uint) (*TransferConfig, error) {
|
||||
var config TransferConfig
|
||||
err := db.First(&config, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// UpdateTransferConfig updates an existing transfer config record
|
||||
func (db *DB) UpdateTransferConfig(config *TransferConfig) error {
|
||||
return db.Save(config).Error
|
||||
}
|
||||
|
||||
// DeleteTransferConfig deletes a transfer config record after checking dependencies
|
||||
func (db *DB) DeleteTransferConfig(id uint) error {
|
||||
// First check if any jobs are using this config
|
||||
var count int64
|
||||
// Need to check both ConfigID and ConfigIDs list
|
||||
// This check might need refinement depending on how ConfigIDs is used reliably
|
||||
if err := db.Model(&Job{}).Where("config_id = ? OR config_ids LIKE ?", id, "%"+strconv.FormatUint(uint64(id), 10)+"%").Count(&count).Error; err != nil {
|
||||
return fmt.Errorf("failed to check for dependent jobs: %v", err)
|
||||
}
|
||||
if count > 0 {
|
||||
return fmt.Errorf("cannot delete config: %d jobs are using this configuration", count)
|
||||
}
|
||||
|
||||
// Delete the config
|
||||
return db.Delete(&TransferConfig{}, id).Error
|
||||
}
|
||||
|
||||
// GetConfigRclonePath returns the path to the rclone config file for a given transfer config
|
||||
func (db *DB) GetConfigRclonePath(config *TransferConfig) string {
|
||||
// Get data directory from environment or use default
|
||||
dataDir := os.Getenv("DATA_DIR")
|
||||
if dataDir == "" {
|
||||
dataDir = "./data"
|
||||
}
|
||||
|
||||
// Store configs in the data directory
|
||||
return filepath.Join(dataDir, "configs", fmt.Sprintf("config_%d.conf", config.ID))
|
||||
}
|
||||
|
||||
// GenerateRcloneConfig generates the rclone config file content based on TransferConfig
|
||||
// This function now primarily focuses on generating the content string or calling rclone config create
|
||||
func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
|
||||
configPath := db.GetConfigRclonePath(config)
|
||||
|
||||
// Get the directory part of the path
|
||||
configDir := filepath.Dir(configPath)
|
||||
|
||||
// Ensure configs directory exists
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create configs directory: %v", err)
|
||||
}
|
||||
|
||||
// Get the rclone path from the environment variable or use the default path
|
||||
rclonePath := os.Getenv("RCLONE_PATH")
|
||||
if rclonePath == "" {
|
||||
rclonePath = "rclone"
|
||||
}
|
||||
|
||||
sourceName := fmt.Sprintf("source_%d", config.ID)
|
||||
// Generate rclone config using rclone CLI for source
|
||||
switch config.SourceType {
|
||||
case "sftp":
|
||||
args := []string{
|
||||
"config", "create", sourceName, "sftp",
|
||||
"host", config.SourceHost,
|
||||
"user", config.SourceUser,
|
||||
"port", fmt.Sprintf("%d", config.SourcePort),
|
||||
"--non-interactive",
|
||||
"--config", configPath,
|
||||
"--log-level", "ERROR",
|
||||
}
|
||||
if config.SourcePassword != "" {
|
||||
args = append(args, "pass", config.SourcePassword)
|
||||
}
|
||||
if config.SourceKeyFile != "" {
|
||||
args = append(args, "key_file", config.SourceKeyFile)
|
||||
}
|
||||
cmd := exec.Command(rclonePath, args...)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to create source config (sftp): %v\nOutput: %s", err, output)
|
||||
}
|
||||
case "s3":
|
||||
args := []string{
|
||||
"config", "create", sourceName, "s3",
|
||||
"provider", "AWS", // Assuming AWS provider, adjust if needed
|
||||
"env_auth", "false",
|
||||
"access_key_id", config.SourceAccessKey,
|
||||
"secret_access_key", config.SourceSecretKey,
|
||||
"region", config.SourceRegion,
|
||||
"--non-interactive",
|
||||
"--config", configPath,
|
||||
"--log-level", "ERROR",
|
||||
}
|
||||
if config.SourceEndpoint != "" {
|
||||
args = append(args, "endpoint", config.SourceEndpoint)
|
||||
}
|
||||
cmd := exec.Command(rclonePath, args...)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to create source config (s3): %v\nOutput: %s", err, output)
|
||||
}
|
||||
case "minio":
|
||||
args := []string{
|
||||
"config", "create", sourceName, "s3",
|
||||
"provider", "Minio",
|
||||
"env_auth", "false",
|
||||
"access_key_id", config.SourceAccessKey,
|
||||
"secret_access_key", config.SourceSecretKey,
|
||||
"endpoint", config.SourceEndpoint,
|
||||
"--non-interactive",
|
||||
"--config", configPath,
|
||||
"--log-level", "ERROR",
|
||||
}
|
||||
cmd := exec.Command(rclonePath, args...)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to create source config (minio): %v\nOutput: %s", err, output)
|
||||
}
|
||||
// ... (Add cases for other source types: b2, smb, ftp, webdav, nextcloud, onedrive, gdrive, gphotos) ...
|
||||
case "local":
|
||||
// For local source, ensure the section exists but might not need specific rclone config create
|
||||
content := fmt.Sprintf("[%s]\ntype = local\n\n", sourceName)
|
||||
if err := os.WriteFile(configPath, []byte(content), 0600); err != nil {
|
||||
return fmt.Errorf("failed to write source config (local): %v", err)
|
||||
}
|
||||
default:
|
||||
// Handle unknown or unsupported source types if necessary
|
||||
return fmt.Errorf("unsupported source type for rclone config generation: %s", config.SourceType)
|
||||
|
||||
}
|
||||
|
||||
destName := fmt.Sprintf("dest_%d", config.ID)
|
||||
// Generate rclone config using rclone CLI for destination
|
||||
switch config.DestinationType {
|
||||
case "sftp":
|
||||
args := []string{
|
||||
"config", "create", destName, "sftp",
|
||||
"host", config.DestHost,
|
||||
"user", config.DestUser,
|
||||
"port", fmt.Sprintf("%d", config.DestPort),
|
||||
"--non-interactive",
|
||||
"--config", configPath,
|
||||
"--log-level", "ERROR",
|
||||
}
|
||||
if config.DestPassword != "" {
|
||||
args = append(args, "pass", config.DestPassword)
|
||||
}
|
||||
if config.DestKeyFile != "" {
|
||||
args = append(args, "key_file", config.DestKeyFile)
|
||||
}
|
||||
cmd := exec.Command(rclonePath, args...)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to create destination config (sftp): %v\nOutput: %s", err, output)
|
||||
}
|
||||
case "s3":
|
||||
args := []string{
|
||||
"config", "create", destName, "s3",
|
||||
"provider", "AWS", // Assuming AWS provider
|
||||
"env_auth", "false",
|
||||
"access_key_id", config.DestAccessKey,
|
||||
"secret_access_key", config.DestSecretKey,
|
||||
"region", config.DestRegion,
|
||||
"--non-interactive",
|
||||
"--config", configPath,
|
||||
"--log-level", "ERROR",
|
||||
}
|
||||
if config.DestEndpoint != "" {
|
||||
args = append(args, "endpoint", config.DestEndpoint)
|
||||
}
|
||||
cmd := exec.Command(rclonePath, args...)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to create destination config (s3): %v\nOutput: %s", err, output)
|
||||
}
|
||||
case "minio":
|
||||
args := []string{
|
||||
"config", "create", destName, "s3",
|
||||
"provider", "Minio",
|
||||
"env_auth", "false",
|
||||
"access_key_id", config.DestAccessKey,
|
||||
"secret_access_key", config.DestSecretKey,
|
||||
"endpoint", config.DestEndpoint,
|
||||
"--non-interactive",
|
||||
"--config", configPath,
|
||||
"--log-level", "ERROR",
|
||||
}
|
||||
cmd := exec.Command(rclonePath, args...)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to create destination config (minio): %v\nOutput: %s", err, output)
|
||||
}
|
||||
// ... (Add cases for other destination types: b2, smb, ftp, webdav, nextcloud, onedrive, gdrive, gphotos) ...
|
||||
case "local":
|
||||
// Append local config section
|
||||
content := fmt.Sprintf("\n[%s]\ntype = local\n", destName)
|
||||
f, err := os.OpenFile(configPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open config file for appending (local dest): %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
if _, err := f.WriteString(content); err != nil {
|
||||
return fmt.Errorf("failed to write destination config (local): %v", err)
|
||||
}
|
||||
default:
|
||||
// Handle unknown or unsupported destination types if necessary
|
||||
return fmt.Errorf("unsupported destination type for rclone config generation: %s", config.DestinationType)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StoreGoogleDriveToken stores the Google Drive auth token for a config
|
||||
func (db *DB) StoreGoogleDriveToken(configIDStr string, token string) error {
|
||||
configID, err := strconv.ParseUint(configIDStr, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid config ID: %v", err)
|
||||
}
|
||||
|
||||
config, err := db.GetTransferConfig(uint(configID))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get config: %v", err)
|
||||
}
|
||||
|
||||
authenticated := true
|
||||
config.GoogleDriveAuthenticated = &authenticated
|
||||
|
||||
if err := db.UpdateTransferConfig(config); err != nil {
|
||||
return fmt.Errorf("failed to update config: %v", err)
|
||||
}
|
||||
|
||||
configPath := db.GetConfigRclonePath(config)
|
||||
existingConfig := ""
|
||||
if _, err := os.Stat(configPath); err == nil {
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read existing config: %v", err)
|
||||
}
|
||||
existingConfig = string(data)
|
||||
}
|
||||
|
||||
configDir := filepath.Dir(configPath)
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create config directory: %v", err)
|
||||
}
|
||||
|
||||
destName := fmt.Sprintf("dest_%d", config.ID)
|
||||
newConfig := fmt.Sprintf("[%s]\ntype = drive\ntoken = %s\n", destName, token)
|
||||
|
||||
if config.DestClientID != "" && config.DestClientSecret != "" {
|
||||
newConfig += fmt.Sprintf("client_id = %s\nclient_secret = %s\n", config.DestClientID, config.DestClientSecret)
|
||||
}
|
||||
if config.DestDriveID != "" {
|
||||
newConfig += fmt.Sprintf("root_folder_id = %s\n", config.DestDriveID)
|
||||
}
|
||||
if config.DestTeamDrive != "" {
|
||||
newConfig += fmt.Sprintf("team_drive = %s\n", config.DestTeamDrive)
|
||||
}
|
||||
|
||||
var content string
|
||||
sectionHeader := fmt.Sprintf("[%s]", destName)
|
||||
if strings.Contains(existingConfig, sectionHeader) {
|
||||
parts := strings.SplitN(existingConfig, sectionHeader, 2)
|
||||
nextSectionIdx := strings.Index(parts[1], "[")
|
||||
if nextSectionIdx != -1 {
|
||||
content = parts[0] + newConfig + parts[1][nextSectionIdx:]
|
||||
} else {
|
||||
content = parts[0] + newConfig
|
||||
}
|
||||
} else {
|
||||
content = existingConfig + "\n" + newConfig
|
||||
}
|
||||
|
||||
if err := os.WriteFile(configPath, []byte(content), 0600); err != nil {
|
||||
return fmt.Errorf("failed to write config: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateRcloneConfigWithToken generates a rclone config file for a transfer config with a provided token
|
||||
// Note: This seems partially redundant with StoreGoogleDriveToken and GenerateRcloneConfig. Consolidate if possible.
|
||||
func (db *DB) GenerateRcloneConfigWithToken(config *TransferConfig, token string) error {
|
||||
configPath := db.GetConfigRclonePath(config)
|
||||
if configPath == "" {
|
||||
return fmt.Errorf("failed to get config path")
|
||||
}
|
||||
|
||||
token = strings.TrimSpace(token)
|
||||
token = strings.ReplaceAll(token, "\n", "")
|
||||
token = strings.ReplaceAll(token, "\r", "")
|
||||
|
||||
var configType, section, clientID, clientSecret string
|
||||
var readOnly, includeArchived *bool
|
||||
var startYear int
|
||||
|
||||
// Determine if source or destination needs token update
|
||||
if config.DestinationType == "gdrive" || config.DestinationType == "gphotos" {
|
||||
configType = config.DestinationType
|
||||
section = "dest"
|
||||
clientID = config.DestClientID
|
||||
clientSecret = config.DestClientSecret
|
||||
readOnly = config.DestReadOnly
|
||||
startYear = config.DestStartYear
|
||||
includeArchived = config.DestIncludeArchived
|
||||
} else if config.SourceType == "gdrive" || config.SourceType == "gphotos" {
|
||||
configType = config.SourceType
|
||||
section = "source"
|
||||
clientID = config.SourceClientID
|
||||
clientSecret = config.SourceClientSecret
|
||||
readOnly = config.SourceReadOnly
|
||||
startYear = config.SourceStartYear
|
||||
includeArchived = config.SourceIncludeArchived
|
||||
} else {
|
||||
return fmt.Errorf("config is not for Google Drive or Google Photos")
|
||||
}
|
||||
|
||||
contentBytes, err := os.ReadFile(configPath)
|
||||
if err != nil && !os.IsNotExist(err) { // Allow file not existing yet
|
||||
return fmt.Errorf("failed to read config file: %v", err)
|
||||
}
|
||||
content := string(contentBytes)
|
||||
|
||||
var sectionContent string
|
||||
sectionHeader := fmt.Sprintf("[%s_%d]", section, config.ID)
|
||||
|
||||
if configType == "gdrive" {
|
||||
sectionContent = sectionHeader + "\ntype = drive\n"
|
||||
if clientID != "" {
|
||||
sectionContent += fmt.Sprintf("client_id = %s\n", clientID)
|
||||
}
|
||||
if clientSecret != "" {
|
||||
sectionContent += fmt.Sprintf("client_secret = %s\n", clientSecret)
|
||||
}
|
||||
sectionContent += fmt.Sprintf("token = %s\n", token)
|
||||
if section == "source" && config.SourceTeamDrive != "" {
|
||||
sectionContent += fmt.Sprintf("team_drive = %s\n", config.SourceTeamDrive)
|
||||
}
|
||||
if section == "dest" && config.DestTeamDrive != "" {
|
||||
sectionContent += fmt.Sprintf("team_drive = %s\n", config.DestTeamDrive)
|
||||
}
|
||||
if section == "dest" && config.DestDriveID != "" {
|
||||
sectionContent += fmt.Sprintf("root_folder_id = %s\n", config.DestDriveID)
|
||||
} // Use DestDriveID for root_folder_id
|
||||
} else if configType == "gphotos" {
|
||||
sectionContent = sectionHeader + "\ntype = google photos\n"
|
||||
if clientID != "" {
|
||||
sectionContent += fmt.Sprintf("client_id = %s\n", clientID)
|
||||
}
|
||||
if clientSecret != "" {
|
||||
sectionContent += fmt.Sprintf("client_secret = %s\n", clientSecret)
|
||||
}
|
||||
sectionContent += fmt.Sprintf("token = %s\n", token)
|
||||
if readOnly != nil && *readOnly {
|
||||
sectionContent += "read_only = true\n"
|
||||
}
|
||||
if startYear > 0 {
|
||||
sectionContent += fmt.Sprintf("start_year = %d\n", startYear)
|
||||
}
|
||||
if includeArchived != nil && *includeArchived {
|
||||
sectionContent += "include_archived = true\n"
|
||||
}
|
||||
}
|
||||
|
||||
// Replace or append logic
|
||||
sectionPattern := regexp.MustCompile(fmt.Sprintf(`(?m)^%s[^\[]*`, regexp.QuoteMeta(sectionHeader))) // Match section start to next section or EOF
|
||||
if sectionPattern.MatchString(content) {
|
||||
content = sectionPattern.ReplaceAllString(content, sectionContent)
|
||||
} else {
|
||||
if content != "" && !strings.HasSuffix(content, "\n\n") { // Ensure separation
|
||||
if !strings.HasSuffix(content, "\n") {
|
||||
content += "\n"
|
||||
}
|
||||
content += "\n"
|
||||
}
|
||||
content += sectionContent
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
configDir := filepath.Dir(configPath)
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create config directory: %v", err)
|
||||
}
|
||||
|
||||
// Write the updated config file
|
||||
if err := os.WriteFile(configPath, []byte(content), 0600); err != nil { // Use 0600 for sensitive files
|
||||
return fmt.Errorf("failed to write updated config file: %v", err)
|
||||
}
|
||||
|
||||
// Update the authentication status in DB
|
||||
authenticated := true
|
||||
if config.DestinationType == "gdrive" || config.DestinationType == "gphotos" {
|
||||
config.SetGoogleAuthenticated(authenticated)
|
||||
} else if config.SourceType == "gdrive" || config.SourceType == "gphotos" {
|
||||
config.SetGoogleAuthenticated(authenticated)
|
||||
}
|
||||
// Persist the change (assuming UpdateTransferConfig saves the whole object)
|
||||
if err := db.UpdateTransferConfig(config); err != nil {
|
||||
return fmt.Errorf("failed to update config authentication status: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetGDriveCredentialsFromConfig extracts Google Drive client ID and secret from an existing rclone config file
|
||||
func (db *DB) GetGDriveCredentialsFromConfig(config *TransferConfig) (string, string) {
|
||||
configPath := db.GetConfigRclonePath(config)
|
||||
if configPath == "" {
|
||||
return "", ""
|
||||
}
|
||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
lines := strings.Split(string(content), "\n")
|
||||
sourceSectionName := fmt.Sprintf("[source_%d]", config.ID)
|
||||
destSectionName := fmt.Sprintf("[dest_%d]", config.ID)
|
||||
var inSourceSection, inDestSection bool
|
||||
var sourceClientID, sourceClientSecret, destClientID, destClientSecret string
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||
inSourceSection = line == sourceSectionName
|
||||
inDestSection = line == destSectionName
|
||||
continue
|
||||
}
|
||||
if inSourceSection {
|
||||
if strings.HasPrefix(line, "client_id") {
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
sourceClientID = strings.TrimSpace(parts[1])
|
||||
}
|
||||
} else if strings.HasPrefix(line, "client_secret") {
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
sourceClientSecret = strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
if inDestSection {
|
||||
if strings.HasPrefix(line, "client_id") {
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
destClientID = strings.TrimSpace(parts[1])
|
||||
}
|
||||
} else if strings.HasPrefix(line, "client_secret") {
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
destClientSecret = strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
if sourceClientID != "" && sourceClientSecret != "" && destClientID != "" && destClientSecret != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if destClientID != "" && destClientSecret != "" {
|
||||
return destClientID, destClientSecret
|
||||
}
|
||||
if sourceClientID != "" && sourceClientSecret != "" {
|
||||
return sourceClientID, sourceClientSecret
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// User represents a user account in the system
|
||||
type User struct {
|
||||
ID uint `gorm:"primarykey"`
|
||||
Email string `gorm:"unique;not null"`
|
||||
PasswordHash string `gorm:"not null"`
|
||||
IsAdmin *bool `gorm:"default:false"`
|
||||
LastPasswordChange time.Time
|
||||
FailedLoginAttempts int `gorm:"default:0"`
|
||||
AccountLocked *bool `gorm:"default:false"`
|
||||
LockoutUntil *time.Time
|
||||
Theme string `gorm:"default:'light'"`
|
||||
TwoFactorSecret string `gorm:"type:varchar(32)"`
|
||||
TwoFactorEnabled bool `gorm:"default:false"`
|
||||
BackupCodes string `gorm:"type:text"` // Comma-separated backup codes
|
||||
Roles []Role `gorm:"many2many:user_roles"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// PasswordHistory stores previous passwords for a user
|
||||
type PasswordHistory struct {
|
||||
ID uint `gorm:"primarykey"`
|
||||
UserID uint `gorm:"not null"`
|
||||
User User `gorm:"foreignkey:UserID"`
|
||||
PasswordHash string `gorm:"not null"`
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// PasswordResetToken stores tokens for password reset requests
|
||||
type PasswordResetToken struct {
|
||||
ID uint `gorm:"primarykey"`
|
||||
UserID uint `gorm:"not null"`
|
||||
User User `gorm:"foreignkey:UserID"`
|
||||
Token string `gorm:"not null"`
|
||||
ExpiresAt time.Time `gorm:"not null"`
|
||||
Used *bool `gorm:"default:false"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// --- User Helper Methods ---
|
||||
|
||||
// GetIsAdmin returns the value of IsAdmin with a default if nil
|
||||
func (u *User) GetIsAdmin() bool {
|
||||
if u.IsAdmin == nil {
|
||||
return false // Default to false if not set
|
||||
}
|
||||
return *u.IsAdmin
|
||||
}
|
||||
|
||||
// SetIsAdmin sets the IsAdmin field
|
||||
func (u *User) SetIsAdmin(value bool) {
|
||||
u.IsAdmin = &value
|
||||
}
|
||||
|
||||
// GetAccountLocked returns the value of AccountLocked with a default if nil
|
||||
func (u *User) GetAccountLocked() bool {
|
||||
if u.AccountLocked == nil {
|
||||
return false // Default to false if not set
|
||||
}
|
||||
return *u.AccountLocked
|
||||
}
|
||||
|
||||
// SetAccountLocked sets the AccountLocked field
|
||||
func (u *User) SetAccountLocked(value bool) {
|
||||
u.AccountLocked = &value
|
||||
}
|
||||
|
||||
// HasRole checks if the user has a specific role
|
||||
func (u *User) HasRole(roleName string) bool {
|
||||
for _, role := range u.Roles {
|
||||
if role.Name == roleName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasPermission checks if the user has a specific permission through any of their roles
|
||||
func (u *User) HasPermission(permission string) bool {
|
||||
for _, role := range u.Roles {
|
||||
if role.HasPermission(permission) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetRoles returns all roles assigned to the user
|
||||
// Note: This requires preloading Roles when fetching the user
|
||||
func (u *User) GetRoles(tx *gorm.DB) ([]Role, error) {
|
||||
var roles []Role
|
||||
err := tx.Model(u).Association("Roles").Find(&roles)
|
||||
return roles, err
|
||||
}
|
||||
|
||||
// AssignRole assigns a role to the user
|
||||
func (u *User) AssignRole(tx *gorm.DB, roleID uint, assignedByID uint) error {
|
||||
var role Role
|
||||
if err := tx.First(&role, roleID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// Assuming Role struct has AssignToUser method (from role.go)
|
||||
return role.AssignToUser(tx, u.ID, assignedByID)
|
||||
}
|
||||
|
||||
// UnassignRole removes a role from the user
|
||||
func (u *User) UnassignRole(tx *gorm.DB, roleID uint, unassignedByID uint) error {
|
||||
var role Role
|
||||
if err := tx.First(&role, roleID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// Assuming Role struct has UnassignFromUser method (from role.go)
|
||||
return role.UnassignFromUser(tx, u.ID, unassignedByID)
|
||||
}
|
||||
|
||||
// SetPassword sets the user's password with secure hashing
|
||||
func (u *User) SetPassword(password string) error {
|
||||
// Validate password length
|
||||
if len(password) < 8 {
|
||||
return fmt.Errorf("password must be at least 8 characters long")
|
||||
}
|
||||
|
||||
// Hash the password using bcrypt
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to hash password: %w", err)
|
||||
}
|
||||
|
||||
// Store the hashed password
|
||||
u.PasswordHash = string(hashedPassword)
|
||||
u.LastPasswordChange = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckPassword verifies if the provided password matches the stored hash
|
||||
func (u *User) CheckPassword(password string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// --- PasswordResetToken Helper Methods ---
|
||||
|
||||
// GetUsed returns the value of Used with a default if nil
|
||||
func (t *PasswordResetToken) GetUsed() bool {
|
||||
if t.Used == nil {
|
||||
return false // Default to false if not set
|
||||
}
|
||||
return *t.Used
|
||||
}
|
||||
|
||||
// SetUsed sets the Used field
|
||||
func (t *PasswordResetToken) SetUsed(value bool) {
|
||||
t.Used = &value
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// --- User Store Methods ---
|
||||
|
||||
// CreateUser creates a new user record
|
||||
func (db *DB) CreateUser(user *User) error {
|
||||
return db.Create(user).Error
|
||||
}
|
||||
|
||||
// GetUserByEmail retrieves a user by their email address
|
||||
func (db *DB) GetUserByEmail(email string) (*User, error) {
|
||||
var user User
|
||||
// Preload Roles to ensure they are available for permission checks
|
||||
err := db.Preload("Roles").Where("email = ?", email).First(&user).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GetUserByID retrieves a user by their ID
|
||||
func (db *DB) GetUserByID(id uint) (*User, error) {
|
||||
var user User
|
||||
// Preload Roles
|
||||
err := db.Preload("Roles").First(&user, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// UpdateUser updates an existing user record
|
||||
func (db *DB) UpdateUser(user *User) error {
|
||||
// Use Omit to prevent accidentally changing Roles association directly
|
||||
// Role assignments should use AssignRole/UnassignRole methods
|
||||
return db.Omit("Roles").Save(user).Error
|
||||
}
|
||||
|
||||
// --- PasswordResetToken Store Methods ---
|
||||
|
||||
// CreatePasswordResetToken creates a new password reset token record
|
||||
func (db *DB) CreatePasswordResetToken(token *PasswordResetToken) error {
|
||||
return db.Create(token).Error
|
||||
}
|
||||
|
||||
// GetPasswordResetToken retrieves a valid, unused password reset token
|
||||
func (db *DB) GetPasswordResetToken(token string) (*PasswordResetToken, error) {
|
||||
var resetToken PasswordResetToken
|
||||
err := db.Where("token = ? AND used = ? AND expires_at > ?", token, false, time.Now()).First(&resetToken).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resetToken, nil
|
||||
}
|
||||
|
||||
// MarkPasswordResetTokenAsUsed marks a password reset token as used
|
||||
func (db *DB) MarkPasswordResetTokenAsUsed(tokenID uint) error {
|
||||
return db.Model(&PasswordResetToken{}).Where("id = ?", tokenID).Update("used", true).Error
|
||||
}
|
||||
Reference in New Issue
Block a user