mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-09-08 15:41:20 +02:00
- Introduced new source and destination forms for Backblaze B2 and Wasabi, allowing users to configure their cloud storage settings. - Updated the configuration generation logic to handle B2 and Wasabi, including necessary parameters such as access keys, bucket names, and endpoints. - Enhanced the user interface to provide clear instructions and security notes for both storage options, improving user experience and security awareness.
663 lines
22 KiB
Go
663 lines
22 KiB
Go
package db
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"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 "wasabi":
|
|
args := []string{
|
|
"config", "create", sourceName, "s3",
|
|
"provider", "Wasabi",
|
|
"env_auth", "false",
|
|
"access_key_id", config.SourceAccessKey,
|
|
"secret_access_key", config.SourceSecretKey,
|
|
"region", config.SourceRegion,
|
|
"--non-interactive",
|
|
"--config", configPath,
|
|
"--log-level", "ERROR",
|
|
}
|
|
endpoint := config.SourceEndpoint
|
|
if endpoint == "" {
|
|
endpoint = "s3.wasabisys.com"
|
|
}
|
|
args = append(args, "endpoint", endpoint)
|
|
cmd := exec.Command(rclonePath, args...)
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to create source config (wasabi): %v\nOutput: %s", err, output)
|
|
}
|
|
case "b2":
|
|
args := []string{
|
|
"config", "create", sourceName, "b2",
|
|
"account", config.SourceAccessKey, // B2 Account ID
|
|
"key", config.SourceSecretKey, // B2 Application Key
|
|
"--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 (b2): %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",
|
|
}
|
|
// Add region if specified
|
|
if config.SourceRegion != "" {
|
|
args = append(args, "region", config.SourceRegion)
|
|
}
|
|
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)
|
|
}
|
|
case "webdav", "nextcloud": // Handle both webdav and nextcloud similarly
|
|
// Construct the WebDAV URL
|
|
// Parse the provided source URL, assuming it includes the scheme
|
|
inputURL := config.SourceHost
|
|
parsedURL, err := url.Parse(inputURL)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse source URL '%s': %v", inputURL, err)
|
|
}
|
|
// Validate that both scheme and host are present
|
|
if parsedURL.Scheme == "" || parsedURL.Host == "" {
|
|
return fmt.Errorf("invalid source URL '%s': must include scheme (http/https) and host", inputURL)
|
|
}
|
|
// Use the scheme and host from the parsed URL
|
|
webdavURL := fmt.Sprintf("%s://%s", parsedURL.Scheme, parsedURL.Host)
|
|
|
|
// Determine vendor based on type
|
|
vendor := "other" // Default vendor
|
|
if config.SourceType == "nextcloud" {
|
|
vendor = "nextcloud"
|
|
|
|
// Construct the full Nextcloud path using the parsed base URL
|
|
webdavURL = fmt.Sprintf("%s/remote.php/dav/files/%s/", webdavURL, config.SourceUser)
|
|
}
|
|
|
|
args := []string{
|
|
"config", "create", sourceName, "webdav",
|
|
"url", webdavURL,
|
|
"vendor", vendor,
|
|
"user", config.SourceUser,
|
|
"pass", config.SourcePassword, // rclone obscures this
|
|
"--non-interactive",
|
|
"--config", configPath,
|
|
"--log-level", "ERROR",
|
|
}
|
|
cmd := exec.Command(rclonePath, args...)
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
errorMsg := fmt.Sprintf("failed to create source config (%s): %v", config.SourceType, err)
|
|
// Check if output contains useful info, especially for auth errors
|
|
if len(output) > 0 {
|
|
errorMsg += fmt.Sprintf("\nOutput: %s", output)
|
|
}
|
|
return fmt.Errorf(errorMsg)
|
|
}
|
|
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 "wasabi":
|
|
args := []string{
|
|
"config", "create", destName, "s3",
|
|
"provider", "Wasabi",
|
|
"env_auth", "false",
|
|
"access_key_id", config.DestAccessKey,
|
|
"secret_access_key", config.DestSecretKey,
|
|
"region", config.DestRegion,
|
|
"--non-interactive",
|
|
"--config", configPath,
|
|
"--log-level", "ERROR",
|
|
}
|
|
endpoint := config.DestEndpoint
|
|
if endpoint == "" {
|
|
endpoint = "s3.wasabisys.com"
|
|
}
|
|
args = append(args, "endpoint", endpoint)
|
|
cmd := exec.Command(rclonePath, args...)
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to create destination config (wasabi): %v\nOutput: %s", err, output)
|
|
}
|
|
case "b2":
|
|
args := []string{
|
|
"config", "create", destName, "b2",
|
|
"account", config.DestAccessKey, // B2 Account ID
|
|
"key", config.DestSecretKey, // B2 Application Key
|
|
"--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 (b2): %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",
|
|
}
|
|
// Add region if specified
|
|
if config.DestRegion != "" {
|
|
args = append(args, "region", config.DestRegion)
|
|
}
|
|
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)
|
|
}
|
|
case "webdav", "nextcloud": // Combined case for WebDAV and Nextcloud
|
|
// Parse and reconstruct the WebDAV URL robustly
|
|
// Parse the provided destination URL, assuming it includes the scheme
|
|
inputURL := config.DestHost
|
|
parsedURL, err := url.Parse(inputURL)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse destination URL '%s': %v", inputURL, err)
|
|
}
|
|
// Validate that both scheme and host are present
|
|
if parsedURL.Scheme == "" || parsedURL.Host == "" {
|
|
return fmt.Errorf("invalid destination URL '%s': must include scheme (http/https) and host", inputURL)
|
|
}
|
|
// Use the scheme and host from the parsed URL
|
|
webdavURL := fmt.Sprintf("%s://%s", parsedURL.Scheme, parsedURL.Host)
|
|
|
|
// Determine vendor based on type
|
|
vendor := "other" // Default vendor
|
|
if config.DestinationType == "nextcloud" {
|
|
vendor = "nextcloud"
|
|
|
|
webdavURL = fmt.Sprintf("%s/remote.php/dav/files/%s/", webdavURL, config.DestUser) // Corrected variable
|
|
}
|
|
|
|
args := []string{
|
|
"config", "create", destName, "webdav",
|
|
"url", webdavURL, // Use the parsed and reconstructed URL
|
|
"vendor", vendor,
|
|
"user", config.DestUser,
|
|
"pass", config.DestPassword, // rclone obscures this
|
|
"--non-interactive",
|
|
"--config", configPath,
|
|
"--log-level", "ERROR",
|
|
}
|
|
cmd := exec.Command(rclonePath, args...)
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
errorMsg := fmt.Sprintf("failed to create destination config (%s): %v", config.DestinationType, err)
|
|
if len(output) > 0 {
|
|
errorMsg += fmt.Sprintf("\nOutput: %s", output)
|
|
}
|
|
return fmt.Errorf(errorMsg)
|
|
}
|
|
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 "", ""
|
|
}
|