mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-09-08 15:41:20 +02:00
+43
-9
@@ -16,15 +16,49 @@ if [ -n "${PUID}" ] && [ -n "${PGID}" ]; then
|
|||||||
echo "Detected Alpine Linux, using busybox usermod/groupmod..."
|
echo "Detected Alpine Linux, using busybox usermod/groupmod..."
|
||||||
|
|
||||||
# Update group ID first
|
# Update group ID first
|
||||||
if [ "$(getent group ${USERNAME} | cut -d: -f3)" != "${PGID}" ]; then
|
CURRENT_GID=$(getent group ${USERNAME} | cut -d: -f3)
|
||||||
echo "Updating GID to ${PGID}..."
|
CURRENT_UID=$(id -u ${USERNAME})
|
||||||
groupmod -g ${PGID} ${USERNAME} || echo "⚠️ Failed to change GID"
|
|
||||||
fi
|
if [ "${CURRENT_GID}" != "${PGID}" ] || [ "${CURRENT_UID}" != "${PUID}" ]; then
|
||||||
|
echo "Attempting to update UID/GID to ${PUID}:${PGID} using delete/recreate..."
|
||||||
# Update user ID
|
|
||||||
if [ "$(id -u ${USERNAME})" != "${PUID}" ]; then
|
# Delete existing user and group, ignoring errors
|
||||||
echo "Updating UID to ${PUID}..."
|
deluser ${USERNAME} > /dev/null 2>&1 || true
|
||||||
usermod -u ${PUID} ${USERNAME} || echo "⚠️ Failed to change UID"
|
delgroup ${USERNAME} > /dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# Add group with the specified GID
|
||||||
|
echo "Adding group ${USERNAME} with GID ${PGID}"
|
||||||
|
if ! addgroup -g ${PGID} ${USERNAME}; then
|
||||||
|
echo "⚠️ Failed to add group ${USERNAME} with GID ${PGID}."
|
||||||
|
# Exiting because user creation will likely fail
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add user with the specified UID and GID
|
||||||
|
# Use -G for primary group with adduser in BusyBox
|
||||||
|
# Use -h /app for home directory (consistent with expectations)
|
||||||
|
# Use -s /bin/sh for shell
|
||||||
|
# Use -D for no password (system user)
|
||||||
|
echo "Adding user ${USERNAME} with UID ${PUID}"
|
||||||
|
if ! adduser -u ${PUID} -G ${USERNAME} -h /app -s /bin/sh -D ${USERNAME}; then
|
||||||
|
echo "⚠️ Failed to add user ${USERNAME} with UID ${PUID} and group ${USERNAME}."
|
||||||
|
# Exiting because the application cannot run as the correct user
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify the change
|
||||||
|
FINAL_UID=$(id -u ${USERNAME} 2>/dev/null || echo "error")
|
||||||
|
FINAL_GID=$(getent group ${USERNAME} | cut -d: -f3 2>/dev/null || echo "error")
|
||||||
|
|
||||||
|
if [ "${FINAL_UID}" = "${PUID}" ] && [ "${FINAL_GID}" = "${PGID}" ]; then
|
||||||
|
echo "✅ Successfully updated UID/GID to ${PUID}:${PGID}"
|
||||||
|
else
|
||||||
|
echo "⚠️ Verification failed after update. Target: ${PUID}:${PGID}, Actual: ${FINAL_UID}:${FINAL_GID}"
|
||||||
|
# Exiting because the UID/GID is not correct
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "UID/GID ${PUID}:${PGID} already set."
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo "Non-Alpine system, using standard user management..."
|
echo "Non-Alpine system, using standard user management..."
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/go-gormigrate/gormigrate/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RecoverTransferConfigsRename checks for and corrects a specific inconsistent state
|
||||||
|
// left by a potentially failed run of migration 012, where the transfer_configs
|
||||||
|
// table might have been left renamed as _transfer_configs_old.
|
||||||
|
func RecoverTransferConfigsRename() *gormigrate.Migration {
|
||||||
|
return &gormigrate.Migration{
|
||||||
|
ID: "011a_recover_transfer_configs_rename",
|
||||||
|
Migrate: func(tx *gorm.DB) error {
|
||||||
|
fmt.Println("Running migration 011a: Checking for transfer_configs rename recovery...")
|
||||||
|
|
||||||
|
var oldTableExists int
|
||||||
|
tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='_transfer_configs_old'").Scan(&oldTableExists)
|
||||||
|
|
||||||
|
var newTableExists int
|
||||||
|
tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='transfer_configs'").Scan(&newTableExists)
|
||||||
|
|
||||||
|
if oldTableExists > 0 && newTableExists == 0 {
|
||||||
|
fmt.Println("Found _transfer_configs_old table but not transfer_configs. Attempting recovery rename...")
|
||||||
|
if err := tx.Exec("ALTER TABLE _transfer_configs_old RENAME TO transfer_configs").Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to rename _transfer_configs_old back to transfer_configs: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Successfully renamed _transfer_configs_old to transfer_configs.")
|
||||||
|
} else if oldTableExists > 0 && newTableExists > 0 {
|
||||||
|
// This state shouldn't ideally happen if migration 012 followed its logic,
|
||||||
|
// but indicates a potential issue. Maybe drop the old one? For now, just log.
|
||||||
|
fmt.Println("Warning: Both transfer_configs and _transfer_configs_old tables exist. Manual inspection might be needed.")
|
||||||
|
} else {
|
||||||
|
fmt.Println("No recovery needed for transfer_configs rename.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Rollback: func(tx *gorm.DB) error {
|
||||||
|
// Rollback doesn't make sense for a recovery step.
|
||||||
|
fmt.Println("Rollback for migration 011a_recover_transfer_configs_rename is not applicable.")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -178,6 +178,12 @@ func AlterBooleanDefaults() *gormigrate.Migration {
|
|||||||
fmt.Printf("Recreating table %s...\n", tableName)
|
fmt.Printf("Recreating table %s...\n", tableName)
|
||||||
oldTableName := fmt.Sprintf("_%s_old", tableName)
|
oldTableName := fmt.Sprintf("_%s_old", tableName)
|
||||||
|
|
||||||
|
// Drop the old temp table if it exists from a previous failed run
|
||||||
|
if err := tx.Exec(fmt.Sprintf("DROP TABLE IF EXISTS %s", oldTableName)).Error; err != nil {
|
||||||
|
// Log the error but proceed, as the rename might still work or fail for the intended reason
|
||||||
|
fmt.Printf("Warning: failed to drop potential leftover table %s: %v\n", oldTableName, err)
|
||||||
|
}
|
||||||
|
|
||||||
// Rename old table
|
// Rename old table
|
||||||
if err := tx.Exec(fmt.Sprintf("ALTER TABLE %s RENAME TO %s", tableName, oldTableName)).Error; err == nil {
|
if err := tx.Exec(fmt.Sprintf("ALTER TABLE %s RENAME TO %s", tableName, oldTableName)).Error; err == nil {
|
||||||
fmt.Printf("Renamed %s to %s.\n", tableName, oldTableName)
|
fmt.Printf("Renamed %s to %s.\n", tableName, oldTableName)
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/go-gormigrate/gormigrate/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CleanupInvalidBooleans updates boolean columns represented as integers
|
||||||
|
// to ensure they only contain valid values (0, 1, or NULL).
|
||||||
|
func CleanupInvalidBooleans() *gormigrate.Migration {
|
||||||
|
return &gormigrate.Migration{
|
||||||
|
ID: "013_cleanup_invalid_booleans",
|
||||||
|
Migrate: func(tx *gorm.DB) error {
|
||||||
|
fmt.Println("Running migration 013: Cleaning up invalid boolean values...")
|
||||||
|
|
||||||
|
// Target: transfer_configs.delete_after_transfer
|
||||||
|
// Set any non-NULL value that is not 0 or 1 to 0 (false)
|
||||||
|
sql := `UPDATE transfer_configs
|
||||||
|
SET delete_after_transfer = 0
|
||||||
|
WHERE delete_after_transfer IS NOT NULL AND delete_after_transfer NOT IN (0, 1);`
|
||||||
|
|
||||||
|
if err := tx.Exec(sql).Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to cleanup delete_after_transfer in transfer_configs: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Cleaned up invalid values in transfer_configs.delete_after_transfer.")
|
||||||
|
|
||||||
|
// Target: transfer_configs.archive_enabled
|
||||||
|
sql = `UPDATE transfer_configs
|
||||||
|
SET archive_enabled = 0
|
||||||
|
WHERE archive_enabled IS NOT NULL AND archive_enabled NOT IN (0, 1);`
|
||||||
|
if err := tx.Exec(sql).Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to cleanup archive_enabled in transfer_configs: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Cleaned up invalid values in transfer_configs.archive_enabled.")
|
||||||
|
|
||||||
|
// Target: transfer_configs.skip_processed_files
|
||||||
|
sql = `UPDATE transfer_configs
|
||||||
|
SET skip_processed_files = 0
|
||||||
|
WHERE skip_processed_files IS NOT NULL AND skip_processed_files NOT IN (0, 1);`
|
||||||
|
if err := tx.Exec(sql).Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to cleanup skip_processed_files in transfer_configs: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Cleaned up invalid values in transfer_configs.skip_processed_files.")
|
||||||
|
|
||||||
|
// Target: notification_services.is_enabled
|
||||||
|
sql = `UPDATE notification_services
|
||||||
|
SET is_enabled = 0
|
||||||
|
WHERE is_enabled IS NOT NULL AND is_enabled NOT IN (0, 1);`
|
||||||
|
if err := tx.Exec(sql).Error; err != nil {
|
||||||
|
// Check if the table exists before failing hard
|
||||||
|
var tableExists int
|
||||||
|
tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='notification_services'").Scan(&tableExists)
|
||||||
|
if tableExists == 0 {
|
||||||
|
fmt.Println("Skipping cleanup for notification_services.is_enabled: table does not exist.")
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("failed to cleanup is_enabled in notification_services: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println("Cleaned up invalid values in notification_services.is_enabled.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Target: auth_providers.enabled
|
||||||
|
sql = `UPDATE auth_providers
|
||||||
|
SET enabled = 0
|
||||||
|
WHERE enabled IS NOT NULL AND enabled NOT IN (0, 1);`
|
||||||
|
if err := tx.Exec(sql).Error; err != nil {
|
||||||
|
// Check if the table exists before failing hard
|
||||||
|
var tableExists int
|
||||||
|
tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='auth_providers'").Scan(&tableExists)
|
||||||
|
if tableExists == 0 {
|
||||||
|
fmt.Println("Skipping cleanup for auth_providers.enabled: table does not exist.")
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("failed to cleanup enabled in auth_providers: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println("Cleaned up invalid values in auth_providers.enabled.")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Migration 013 completed successfully.")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Rollback: func(tx *gorm.DB) error {
|
||||||
|
// This migration cleans up data. Rolling back doesn't make sense
|
||||||
|
// as we don't know the original invalid values.
|
||||||
|
fmt.Println("Rollback for migration 013_cleanup_invalid_booleans is not applicable.")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,18 +11,20 @@ var migrations []*gormigrate.Migration
|
|||||||
func GetMigrations(db *gorm.DB) *gormigrate.Gormigrate {
|
func GetMigrations(db *gorm.DB) *gormigrate.Gormigrate {
|
||||||
// Add all migrations in order
|
// Add all migrations in order
|
||||||
migrations = append(migrations,
|
migrations = append(migrations,
|
||||||
InitialSchema(), // 001
|
InitialSchema(), // 001
|
||||||
UpdateGDriveType(), // 002
|
UpdateGDriveType(), // 002
|
||||||
Add2FA(), // 003
|
Add2FA(), // 003
|
||||||
AddAuditLogs(), // 004
|
AddAuditLogs(), // 004
|
||||||
AddDefaultRoles(), // 005
|
AddDefaultRoles(), // 005
|
||||||
AddTimestampsToJobHistories(), // 006
|
AddTimestampsToJobHistories(), // 006
|
||||||
AddNotificationServices(), // 007
|
AddNotificationServices(), // 007
|
||||||
AddUserNotifications(), // 008
|
AddUserNotifications(), // 008
|
||||||
AddRcloneTables(), // 009
|
AddRcloneTables(), // 009
|
||||||
AddRcloneCommandToConfig(), // 010
|
AddRcloneCommandToConfig(), // 010
|
||||||
AddAuthProviders(), // 011
|
AddAuthProviders(), // 011
|
||||||
AlterBooleanDefaults(), // 012
|
RecoverTransferConfigsRename(), // 011a
|
||||||
|
AlterBooleanDefaults(), // 012
|
||||||
|
CleanupInvalidBooleans(), // 013
|
||||||
)
|
)
|
||||||
|
|
||||||
return gormigrate.New(db, gormigrate.DefaultOptions, migrations)
|
return gormigrate.New(db, gormigrate.DefaultOptions, migrations)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package scheduler
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
// Needed for Job.NextRun update
|
// Needed for Job.NextRun update
|
||||||
@@ -162,38 +161,18 @@ func (s *Scheduler) ScheduleJob(job *db.Job) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use a 6-field parser for validation and determine the schedule string to use.
|
// Rely on the cron instance's AddFunc for validation based on its configuration (5 or 6 fields)
|
||||||
scheduleToUse := job.Schedule
|
scheduleToUse := job.Schedule // Use the original schedule string
|
||||||
parser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
|
s.logger.LogDebug("Using schedule '%s' for job %d", scheduleToUse, jobID)
|
||||||
_, err := parser.Parse(scheduleToUse)
|
// Schedule the job using the original schedule string. AddFunc will validate it.
|
||||||
|
|
||||||
// If initial parse fails AND it was a 5-field schedule, try prepending seconds
|
|
||||||
if err != nil && len(strings.Fields(job.Schedule)) == 5 {
|
|
||||||
scheduleWithSeconds := "0 " + job.Schedule
|
|
||||||
_, errSeconds := parser.Parse(scheduleWithSeconds)
|
|
||||||
if errSeconds == nil {
|
|
||||||
scheduleToUse = scheduleWithSeconds // Use the 6-field version
|
|
||||||
err = nil // Clear the original error
|
|
||||||
s.logger.LogDebug("Converted 5-field schedule '%s' to 6-field '%s'", job.Schedule, scheduleToUse)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If error still exists after trying conversion, return it
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid cron expression '%s': %w", job.Schedule, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.logger.LogDebug("Validated cron expression '%s' for job %d", scheduleToUse, jobID)
|
|
||||||
|
|
||||||
// Schedule the job using the validated scheduleToUse
|
|
||||||
entryID, err := s.cron.AddFunc(scheduleToUse, func() { // Calls interface method
|
entryID, err := s.cron.AddFunc(scheduleToUse, func() { // Calls interface method
|
||||||
s.executor.executeJob(jobID) // Calls interface method
|
s.executor.executeJob(jobID) // Calls interface method
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.LogError("Error scheduling job %d: %v", jobID, err)
|
// Log and return a more informative error if AddFunc fails validation
|
||||||
return err
|
s.logger.LogError("Error scheduling job %d with schedule '%s': %v", jobID, scheduleToUse, err)
|
||||||
} // <-- Added missing closing brace
|
return fmt.Errorf("invalid cron expression '%s' for the configured scheduler: %w", scheduleToUse, err)
|
||||||
|
}
|
||||||
s.logger.LogDebug("Scheduled job %d with cron entry ID %d", jobID, entryID)
|
s.logger.LogDebug("Scheduled job %d with cron entry ID %d", jobID, entryID)
|
||||||
|
|
||||||
// Store mapping of job ID to cron entry ID
|
// Store mapping of job ID to cron entry ID
|
||||||
|
|||||||
@@ -1181,15 +1181,18 @@ func (h *Handlers) HandleDeleteUser(c *gin.Context) {
|
|||||||
|
|
||||||
// Check if this is an admin user
|
// Check if this is an admin user
|
||||||
if user.GetIsAdmin() {
|
if user.GetIsAdmin() {
|
||||||
// Count how many admins there are
|
// Check if other administrators exist
|
||||||
var adminCount int64
|
var otherAdminCount int64
|
||||||
if err := h.DB.Model(&db.User{}).Where("metadata->>'is_admin' = 'true'").Count(&adminCount).Error; err != nil {
|
// Use the actual 'is_admin' column, comparing against true
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check admin count"})
|
if err := h.DB.Model(&db.User{}).
|
||||||
|
Where("is_admin = ? AND id != ?", true, user.ID).
|
||||||
|
Count(&otherAdminCount).Error; err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check for other administrators"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this is the last admin, prevent deletion
|
// If no other administrators exist, prevent deletion
|
||||||
if adminCount <= 1 {
|
if otherAdminCount == 0 {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot delete the last administrator"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot delete the last administrator"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user