mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-09-08 15:41:20 +02:00
feat: Add connection testing functionality for Rclone providers
- Introduced a new endpoint to test connections for source and destination providers. - Implemented a TestResult component to display success or error messages based on connection outcomes. - Enhanced the configuration form with buttons to initiate connection tests, improving user experience. - Updated backend logic to handle connection testing and return appropriate feedback to the frontend. - Refactored existing handlers and routes to accommodate the new testing functionality.
This commit is contained in:
@@ -459,6 +459,9 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
|||||||
<i class="fas fa-terminal mr-2 text-blue-500 dark:text-blue-400"></i>Command Configuration
|
<i class="fas fa-terminal mr-2 text-blue-500 dark:text-blue-400"></i>Command Configuration
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
|
<!-- Additonal Rclone Flags -->
|
||||||
|
@common.RcloneFlags(data.Config.CommandID) // Pass current command ID
|
||||||
|
|
||||||
<!-- Container for flags, pre-rendered on edit, loaded via HTMX on new/change -->
|
<!-- Container for flags, pre-rendered on edit, loaded via HTMX on new/change -->
|
||||||
<div id="command-flags-container" class="mt-4">
|
<div id="command-flags-container" class="mt-4">
|
||||||
if !data.IsNew && data.InitialCommand != nil {
|
if !data.IsNew && data.InitialCommand != nil {
|
||||||
@@ -467,9 +470,7 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Additonal Rclone Flags -->
|
|
||||||
@common.RcloneFlags(data.Config.CommandID) // Pass current command ID
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Source Configuration Section -->
|
<!-- Source Configuration Section -->
|
||||||
@@ -481,6 +482,20 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
|||||||
<!-- Source selection -->
|
<!-- Source selection -->
|
||||||
@common.SourceSelection()
|
@common.SourceSelection()
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<button type="button"
|
||||||
|
class="text-white bg-green-600 hover:bg-green-700 focus:ring-4 focus:ring-green-300 font-medium rounded-lg text-sm px-4 py-2 text-center dark:bg-green-500 dark:hover:bg-green-600 dark:focus:ring-green-800"
|
||||||
|
hx-post="/configs/test-connection"
|
||||||
|
hx-include="closest form"
|
||||||
|
hx-vals='{"providerType": "source"}'
|
||||||
|
hx-swap="none"
|
||||||
|
hx-indicator="#source-test-spinner">
|
||||||
|
<i class="fas fa-plug mr-1"></i> Test Source
|
||||||
|
<span id="source-test-spinner" class="htmx-indicator ml-2"><i class="fas fa-spinner fa-spin"></i></span>
|
||||||
|
</button>
|
||||||
|
<!-- Removed target div, result shown via toast -->
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Source type specific forms -->
|
<!-- Source type specific forms -->
|
||||||
<template x-if="sourceType === 'local'">
|
<template x-if="sourceType === 'local'">
|
||||||
@source.LocalSourceForm()
|
@source.LocalSourceForm()
|
||||||
@@ -542,6 +557,20 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
|
|||||||
<!-- Destination selection -->
|
<!-- Destination selection -->
|
||||||
@common.DestinationSelection()
|
@common.DestinationSelection()
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<button type="button"
|
||||||
|
class="text-white bg-green-600 hover:bg-green-700 focus:ring-4 focus:ring-green-300 font-medium rounded-lg text-sm px-4 py-2 text-center dark:bg-green-500 dark:hover:bg-green-600 dark:focus:ring-green-800"
|
||||||
|
hx-post="/configs/test-connection"
|
||||||
|
hx-include="closest form"
|
||||||
|
hx-vals='{"providerType": "destination"}'
|
||||||
|
hx-swap="none"
|
||||||
|
hx-indicator="#dest-test-spinner">
|
||||||
|
<i class="fas fa-plug mr-1"></i> Test Destination
|
||||||
|
<span id="dest-test-spinner" class="htmx-indicator ml-2"><i class="fas fa-spinner fa-spin"></i></span>
|
||||||
|
</button>
|
||||||
|
<!-- Removed target div, result shown via toast -->
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Destination type specific forms -->
|
<!-- Destination type specific forms -->
|
||||||
<template x-if="destinationType === 'local'">
|
<template x-if="destinationType === 'local'">
|
||||||
@destination.LocalDestinationForm()
|
@destination.LocalDestinationForm()
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ templ RcloneCommandOptionsContent(categoryMap map[string][]db.RcloneCommand, cat
|
|||||||
<select id="command_id" name="command_id" x-model="commandId"
|
<select id="command_id" name="command_id" x-model="commandId"
|
||||||
hx-get="/api/rclone/command-flags"
|
hx-get="/api/rclone/command-flags"
|
||||||
hx-target="#command-flags-container"
|
hx-target="#command-flags-container"
|
||||||
hx-trigger="load, change"
|
hx-trigger="change"
|
||||||
hx-include="[name='command_id']"
|
hx-include="[name='command_id']"
|
||||||
hx-vals={ fmt.Sprintf(`{"commandFlags": %s, "commandFlagValues": %s}`, commandFlagsJSON, commandFlagValuesJSON) }
|
hx-vals={ fmt.Sprintf(`{"commandFlags": %s, "commandFlagValues": %s}`, commandFlagsJSON, commandFlagValuesJSON) }
|
||||||
@change="updateCommandRequirements()"
|
@change="updateCommandRequirements()"
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
templ TestResult(success bool, message string) {
|
||||||
|
if success {
|
||||||
|
<div class="text-green-600 dark:text-green-400 flex items-center">
|
||||||
|
<i class="fas fa-check-circle mr-2"></i>
|
||||||
|
<span>{ message }</span>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<div class="text-red-600 dark:text-red-400 flex items-center">
|
||||||
|
<i class="fas fa-exclamation-triangle mr-2"></i>
|
||||||
|
<span>{ message }</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
package rclone_service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/starfleetcptn/gomft/internal/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestRcloneConnection attempts to connect to a provider using temporary config created via `rclone config create`.
|
||||||
|
// It returns success (bool), a message (string), and an error.
|
||||||
|
func TestRcloneConnection(config db.TransferConfig, providerType string, dbInstance *db.DB) (bool, string, error) {
|
||||||
|
var remoteName string
|
||||||
|
var remotePath string
|
||||||
|
var provider string
|
||||||
|
// Removed bucket, share as they are not used in current rclone config create args
|
||||||
|
var host, user, pass, keyFile, region, accessKey, secretKey, endpoint, domain, clientID, clientSecret, driveID, teamDrive string
|
||||||
|
var port int
|
||||||
|
// Add other necessary fields like passiveMode, readOnly etc. if needed by rclone config create for specific types
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Create a temporary directory for the config file
|
||||||
|
tempDir, err := os.MkdirTemp("", "gomft-rclone-test-")
|
||||||
|
if err != nil {
|
||||||
|
return false, "Failed to create temp directory for rclone config", err
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir) // Clean up the temp directory
|
||||||
|
tempConfigPath := filepath.Join(tempDir, "rclone_test.conf")
|
||||||
|
|
||||||
|
// Extract parameters based on providerType
|
||||||
|
if providerType == "source" {
|
||||||
|
remoteName = "testSource"
|
||||||
|
remotePath = config.SourcePath
|
||||||
|
provider = config.SourceType
|
||||||
|
host = config.SourceHost
|
||||||
|
port = config.SourcePort
|
||||||
|
user = config.SourceUser
|
||||||
|
pass = config.SourcePassword
|
||||||
|
keyFile = config.SourceKeyFile
|
||||||
|
// bucket = config.SourceBucket // Removed - Not used in create args yet
|
||||||
|
region = config.SourceRegion
|
||||||
|
accessKey = config.SourceAccessKey
|
||||||
|
secretKey = config.SourceSecretKey
|
||||||
|
endpoint = config.SourceEndpoint
|
||||||
|
// share = config.SourceShare // Removed - Not used in create args yet
|
||||||
|
domain = config.SourceDomain
|
||||||
|
clientID = config.SourceClientID
|
||||||
|
clientSecret = config.SourceClientSecret
|
||||||
|
driveID = config.SourceDriveID
|
||||||
|
teamDrive = config.SourceTeamDrive
|
||||||
|
// Extract other source fields as needed (e.g., passiveMode, readOnly)
|
||||||
|
} else if providerType == "destination" {
|
||||||
|
remoteName = "testDest"
|
||||||
|
remotePath = config.DestinationPath
|
||||||
|
provider = config.DestinationType
|
||||||
|
host = config.DestHost
|
||||||
|
port = config.DestPort
|
||||||
|
user = config.DestUser
|
||||||
|
pass = config.DestPassword
|
||||||
|
keyFile = config.DestKeyFile
|
||||||
|
// bucket = config.DestBucket // Removed - Not used in create args yet
|
||||||
|
region = config.DestRegion
|
||||||
|
accessKey = config.DestAccessKey
|
||||||
|
secretKey = config.DestSecretKey
|
||||||
|
endpoint = config.DestEndpoint
|
||||||
|
// share = config.DestShare // Removed - Not used in create args yet
|
||||||
|
domain = config.DestDomain
|
||||||
|
clientID = config.DestClientID
|
||||||
|
clientSecret = config.DestClientSecret
|
||||||
|
driveID = config.DestDriveID
|
||||||
|
teamDrive = config.DestTeamDrive
|
||||||
|
// Extract other dest fields as needed (e.g., passiveMode, readOnly)
|
||||||
|
} else {
|
||||||
|
return false, "Invalid provider type specified", fmt.Errorf("unknown provider type: %s", providerType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the rclone path from the environment variable or use the default path
|
||||||
|
rclonePath := os.Getenv("RCLONE_PATH")
|
||||||
|
if rclonePath == "" {
|
||||||
|
rclonePath = "rclone"
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Use `rclone config create` to generate the temporary config section ---
|
||||||
|
createArgs := []string{
|
||||||
|
"config", "create", remoteName, provider,
|
||||||
|
"--config", tempConfigPath,
|
||||||
|
"--non-interactive",
|
||||||
|
"--log-level", "DEBUG", // Use DEBUG for create to see details if it fails
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Declare variables for lsd command *before* the switch/goto ---
|
||||||
|
var ctx context.Context
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
var lsdArgs []string
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
var lsdCmd *exec.Cmd
|
||||||
|
var createCmd *exec.Cmd // Declare createCmd here as well
|
||||||
|
|
||||||
|
// Add provider-specific arguments
|
||||||
|
// Mirroring logic from db.GenerateRcloneConfig but passing args to CLI
|
||||||
|
switch provider {
|
||||||
|
case "sftp":
|
||||||
|
createArgs = append(createArgs, "host", host, "user", user)
|
||||||
|
if port != 0 {
|
||||||
|
createArgs = append(createArgs, "port", fmt.Sprintf("%d", port))
|
||||||
|
}
|
||||||
|
if pass != "" {
|
||||||
|
createArgs = append(createArgs, "pass", pass) // Pass directly
|
||||||
|
}
|
||||||
|
if keyFile != "" {
|
||||||
|
createArgs = append(createArgs, "key_file", keyFile)
|
||||||
|
// Note: Passphrase for keyfile might need 'pass' too, rclone handles this context.
|
||||||
|
}
|
||||||
|
// Add other SFTP args like ssh_agent, use_insecure_cipher etc. if needed
|
||||||
|
case "s3":
|
||||||
|
createArgs = append(createArgs, "provider", "AWS", "env_auth", "false") // Assume AWS for generic S3
|
||||||
|
if accessKey != "" {
|
||||||
|
createArgs = append(createArgs, "access_key_id", accessKey)
|
||||||
|
}
|
||||||
|
if secretKey != "" {
|
||||||
|
createArgs = append(createArgs, "secret_access_key", secretKey) // Pass directly
|
||||||
|
}
|
||||||
|
if region != "" {
|
||||||
|
createArgs = append(createArgs, "region", region)
|
||||||
|
}
|
||||||
|
if endpoint != "" {
|
||||||
|
createArgs = append(createArgs, "endpoint", endpoint)
|
||||||
|
}
|
||||||
|
// Add other S3 args like acl, storage_class if needed
|
||||||
|
case "minio":
|
||||||
|
createArgs = append(createArgs, "provider", "Minio", "env_auth", "false")
|
||||||
|
if accessKey != "" {
|
||||||
|
createArgs = append(createArgs, "access_key_id", accessKey)
|
||||||
|
}
|
||||||
|
if secretKey != "" {
|
||||||
|
createArgs = append(createArgs, "secret_access_key", secretKey) // Pass directly
|
||||||
|
}
|
||||||
|
if endpoint != "" {
|
||||||
|
createArgs = append(createArgs, "endpoint", endpoint)
|
||||||
|
}
|
||||||
|
if region != "" { // Minio might ignore region, but add if present
|
||||||
|
createArgs = append(createArgs, "region", region)
|
||||||
|
}
|
||||||
|
case "ftp":
|
||||||
|
createArgs = append(createArgs, "host", host, "user", user)
|
||||||
|
if port != 0 {
|
||||||
|
createArgs = append(createArgs, "port", fmt.Sprintf("%d", port))
|
||||||
|
}
|
||||||
|
if pass != "" {
|
||||||
|
createArgs = append(createArgs, "pass", pass)
|
||||||
|
}
|
||||||
|
// Add explicit_tls, passive_mode based on config boolean pointers
|
||||||
|
if config.GetSourcePassiveMode() || config.GetDestPassiveMode() { // Check based on providerType
|
||||||
|
createArgs = append(createArgs, "passive_mode", "true")
|
||||||
|
} else {
|
||||||
|
createArgs = append(createArgs, "passive_mode", "false")
|
||||||
|
}
|
||||||
|
createArgs = append(createArgs, "explicit_tls", "true") // Defaulting to true
|
||||||
|
case "smb":
|
||||||
|
createArgs = append(createArgs, "host", host, "user", user)
|
||||||
|
if port != 0 {
|
||||||
|
createArgs = append(createArgs, "port", fmt.Sprintf("%d", port))
|
||||||
|
}
|
||||||
|
if pass != "" {
|
||||||
|
createArgs = append(createArgs, "pass", pass)
|
||||||
|
}
|
||||||
|
if domain != "" {
|
||||||
|
createArgs = append(createArgs, "domain", domain)
|
||||||
|
}
|
||||||
|
case "webdav":
|
||||||
|
createArgs = append(createArgs, "url", endpoint, "vendor", "other", "user", user) // Default vendor
|
||||||
|
if pass != "" {
|
||||||
|
createArgs = append(createArgs, "pass", pass)
|
||||||
|
}
|
||||||
|
case "nextcloud":
|
||||||
|
createArgs = append(createArgs, "url", endpoint, "vendor", "nextcloud", "user", user)
|
||||||
|
if pass != "" {
|
||||||
|
createArgs = append(createArgs, "pass", pass)
|
||||||
|
}
|
||||||
|
// Add cases for gdrive, gphotos - these are complex due to token handling
|
||||||
|
// For testing, they might require pre-existing tokens or manual auth flow outside this scope.
|
||||||
|
// Passing client_id/secret might work for initial setup but not subsequent tests without a token.
|
||||||
|
case "gdrive":
|
||||||
|
createArgs = append(createArgs, "scope", "drive") // Default scope
|
||||||
|
if clientID != "" {
|
||||||
|
createArgs = append(createArgs, "client_id", clientID)
|
||||||
|
}
|
||||||
|
if clientSecret != "" {
|
||||||
|
createArgs = append(createArgs, "client_secret", clientSecret)
|
||||||
|
}
|
||||||
|
if driveID != "" { // Use root_folder_id for specific drive/folder
|
||||||
|
createArgs = append(createArgs, "root_folder_id", driveID)
|
||||||
|
}
|
||||||
|
if teamDrive != "" {
|
||||||
|
createArgs = append(createArgs, "team_drive", teamDrive)
|
||||||
|
}
|
||||||
|
// Cannot pass token directly via 'config create' easily for testing non-interactive flow.
|
||||||
|
log.Println("Warning: Google Drive test may require pre-existing token or manual auth.")
|
||||||
|
|
||||||
|
case "gphotos":
|
||||||
|
// Similar complexity to gdrive regarding tokens
|
||||||
|
if clientID != "" {
|
||||||
|
createArgs = append(createArgs, "client_id", clientID)
|
||||||
|
}
|
||||||
|
if clientSecret != "" {
|
||||||
|
createArgs = append(createArgs, "client_secret", clientSecret)
|
||||||
|
}
|
||||||
|
// Add read_only, start_year, include_archived based on config
|
||||||
|
// Cannot pass token directly via 'config create' easily for testing non-interactive flow.
|
||||||
|
log.Println("Warning: Google Photos test may require pre-existing token or manual auth.")
|
||||||
|
|
||||||
|
case "local":
|
||||||
|
// 'rclone config create' might not be needed or work well for 'local' type.
|
||||||
|
// Write a minimal config manually for local.
|
||||||
|
localConfigContent := fmt.Sprintf("[%s]\ntype = local\nnounc = true\n", remoteName)
|
||||||
|
if err := os.WriteFile(tempConfigPath, []byte(localConfigContent), 0600); err != nil {
|
||||||
|
return false, fmt.Sprintf("Failed to write temporary local config: %v", err), err
|
||||||
|
}
|
||||||
|
goto RunLsd // Skip rclone config create for local
|
||||||
|
default:
|
||||||
|
return false, fmt.Sprintf("Provider type '%s' not yet supported for testing via 'rclone config create'", provider), fmt.Errorf("unsupported provider")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Executing rclone config create command: %s %s", rclonePath, strings.Join(createArgs, " "))
|
||||||
|
createCmd = exec.Command(rclonePath, createArgs...) // Assign value using =
|
||||||
|
if output, err := createCmd.CombinedOutput(); err != nil {
|
||||||
|
// Log the config file content on error for debugging
|
||||||
|
configContentBytes, _ := os.ReadFile(tempConfigPath)
|
||||||
|
log.Printf("Temp config content on create error:\n---\n%s\n---", string(configContentBytes))
|
||||||
|
return false, fmt.Sprintf("Failed to create temp config section: %v\nOutput: %s", err, string(output)), err
|
||||||
|
}
|
||||||
|
log.Printf("Successfully created temp config section for %s", remoteName)
|
||||||
|
|
||||||
|
// Declarations moved before the switch statement
|
||||||
|
|
||||||
|
RunLsd: // Label to jump to for local type
|
||||||
|
|
||||||
|
// --- Execute `rclone lsd` using the temporary config ---
|
||||||
|
ctx, cancel = context.WithTimeout(context.Background(), 30*time.Second) // Assign values
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
lsdArgs = []string{ // Assign value
|
||||||
|
"--config", tempConfigPath,
|
||||||
|
"lsd",
|
||||||
|
fmt.Sprintf("%s:%s", remoteName, remotePath),
|
||||||
|
"--low-level-retries", "1",
|
||||||
|
"--retries", "1",
|
||||||
|
// Add -vv for verbose logging during test if needed
|
||||||
|
// "-vv",
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Executing rclone lsd command: %s %s", rclonePath, strings.Join(lsdArgs, " "))
|
||||||
|
lsdCmd = exec.CommandContext(ctx, rclonePath, lsdArgs...) // Assign value
|
||||||
|
|
||||||
|
// var stdout, stderr bytes.Buffer // Moved declaration up
|
||||||
|
lsdCmd.Stdout = &stdout // Assign buffer
|
||||||
|
lsdCmd.Stderr = &stderr // Assign buffer
|
||||||
|
|
||||||
|
err = lsdCmd.Run() // Assign error
|
||||||
|
|
||||||
|
stdoutStr := stdout.String()
|
||||||
|
stderrStr := stderr.String()
|
||||||
|
|
||||||
|
log.Printf("Rclone lsd stdout:\n%s", stdoutStr)
|
||||||
|
log.Printf("Rclone lsd stderr:\n%s", stderrStr)
|
||||||
|
|
||||||
|
if ctx.Err() == context.DeadlineExceeded {
|
||||||
|
return false, "Connection test timed out after 30 seconds.", ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// Try to provide a more specific error message based on lsd output
|
||||||
|
errMsg := fmt.Sprintf("Connection test failed: %v. Stderr: %s", err, stderrStr)
|
||||||
|
// Add specific error checks based on stderrStr if needed
|
||||||
|
if strings.Contains(stderrStr, "connect: connection refused") {
|
||||||
|
errMsg = "Connection test failed: Connection refused by host."
|
||||||
|
} else if strings.Contains(stderrStr, "no such host") || strings.Contains(stderrStr, "name resolution error") {
|
||||||
|
errMsg = "Connection test failed: Hostname not found or DNS resolution error."
|
||||||
|
} else if strings.Contains(stderrStr, "authentication failed") || strings.Contains(stderrStr, "login incorrect") || strings.Contains(stderrStr, "permission denied") {
|
||||||
|
errMsg = "Connection test failed: Authentication failed (check credentials/permissions)."
|
||||||
|
} else if strings.Contains(stderrStr, "directory not found") {
|
||||||
|
errMsg = "Connection test failed: Directory/Path not found (check path)."
|
||||||
|
} else if strings.Contains(stderrStr, "Couldn't find section") { // Error from rclone config create
|
||||||
|
errMsg = "Connection test failed: Invalid parameters provided for provider type."
|
||||||
|
}
|
||||||
|
return false, errMsg, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If lsd runs without error, the connection is likely okay
|
||||||
|
return true, "Connection test successful!", nil
|
||||||
|
}
|
||||||
@@ -11,7 +11,9 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/starfleetcptn/gomft/components"
|
"github.com/starfleetcptn/gomft/components"
|
||||||
|
|
||||||
"github.com/starfleetcptn/gomft/internal/db"
|
"github.com/starfleetcptn/gomft/internal/db"
|
||||||
|
"github.com/starfleetcptn/gomft/internal/rclone_service" // Assuming we create this package
|
||||||
)
|
)
|
||||||
|
|
||||||
// HandleConfigs handles the GET /configs route
|
// HandleConfigs handles the GET /configs route
|
||||||
@@ -686,3 +688,89 @@ func (h *Handlers) HandleDuplicateConfig(c *gin.Context) {
|
|||||||
c.Header("HX-Refresh", "true")
|
c.Header("HX-Refresh", "true")
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "Config duplicated successfully"})
|
c.JSON(http.StatusOK, gin.H{"message": "Config duplicated successfully"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandleTestProviderConnection handles the POST /configs/test-connection route
|
||||||
|
func (h *Handlers) HandleTestProviderConnection(c *gin.Context) {
|
||||||
|
var config db.TransferConfig
|
||||||
|
providerType := c.PostForm("providerType") // "source" or "destination"
|
||||||
|
|
||||||
|
// Bind all form data into a temporary config struct
|
||||||
|
// We don't save this, just use it to gather the necessary fields
|
||||||
|
if err := c.ShouldBind(&config); err != nil {
|
||||||
|
log.Printf("Error binding test connection form: %v", err)
|
||||||
|
// Render error using the TestResult component
|
||||||
|
components.TestResult(false, fmt.Sprintf("Invalid form data: %v", err)).Render(c, c.Writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process boolean fields manually as ShouldBind might not handle 'on' correctly for pointers
|
||||||
|
if providerType == "source" {
|
||||||
|
sourcePassiveModeVal := c.Request.FormValue("source_passive_mode")
|
||||||
|
sourcePassiveModeValue := sourcePassiveModeVal == "on" || sourcePassiveModeVal == "true"
|
||||||
|
config.SourcePassiveMode = &sourcePassiveModeValue
|
||||||
|
|
||||||
|
sourceReadOnlyVal := c.Request.FormValue("source_read_only")
|
||||||
|
sourceReadOnlyValue := sourceReadOnlyVal == "on" || sourceReadOnlyVal == "true"
|
||||||
|
config.SourceReadOnly = &sourceReadOnlyValue
|
||||||
|
|
||||||
|
sourceIncludeArchivedVal := c.Request.FormValue("source_include_archived")
|
||||||
|
sourceIncludeArchivedValue := sourceIncludeArchivedVal == "on" || sourceIncludeArchivedVal == "true"
|
||||||
|
config.SourceIncludeArchived = &sourceIncludeArchivedValue
|
||||||
|
|
||||||
|
useBuiltinAuthSourceVal := c.Request.FormValue("use_builtin_auth_source")
|
||||||
|
useBuiltinAuthSourceValue := useBuiltinAuthSourceVal == "on" || useBuiltinAuthSourceVal == "true"
|
||||||
|
config.UseBuiltinAuthSource = &useBuiltinAuthSourceValue
|
||||||
|
} else if providerType == "destination" {
|
||||||
|
destPassiveModeVal := c.Request.FormValue("dest_passive_mode")
|
||||||
|
destPassiveModeValue := destPassiveModeVal == "on" || destPassiveModeVal == "true"
|
||||||
|
config.DestPassiveMode = &destPassiveModeValue
|
||||||
|
|
||||||
|
destReadOnlyVal := c.Request.FormValue("dest_read_only")
|
||||||
|
destReadOnlyValue := destReadOnlyVal == "on" || destReadOnlyVal == "true"
|
||||||
|
config.DestReadOnly = &destReadOnlyValue
|
||||||
|
|
||||||
|
destIncludeArchivedVal := c.Request.FormValue("dest_include_archived")
|
||||||
|
destIncludeArchivedValue := destIncludeArchivedVal == "on" || destIncludeArchivedVal == "true"
|
||||||
|
config.DestIncludeArchived = &destIncludeArchivedValue
|
||||||
|
|
||||||
|
useBuiltinAuthDestVal := c.Request.FormValue("use_builtin_auth_dest")
|
||||||
|
useBuiltinAuthDestValue := useBuiltinAuthDestVal == "on" || useBuiltinAuthDestVal == "true"
|
||||||
|
config.UseBuiltinAuthDest = &useBuiltinAuthDestValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the rclone test function (to be implemented)
|
||||||
|
success, message, err := rclone_service.TestRcloneConnection(config, providerType, h.DB) // Pass DB if needed for built-in auth
|
||||||
|
toastType := "info" // Default type
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error testing rclone connection: %v. Message: %s", err, message) // Log both err and message
|
||||||
|
toastType = "error"
|
||||||
|
// Use the message from TestRcloneConnection for the toast
|
||||||
|
} else if success {
|
||||||
|
toastType = "success"
|
||||||
|
} else {
|
||||||
|
// If no error but not success, treat as error/warning
|
||||||
|
toastType = "error"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare data for HX-Trigger
|
||||||
|
toastData := map[string]interface{}{
|
||||||
|
"showToast": map[string]string{
|
||||||
|
"message": message,
|
||||||
|
"type": toastType,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal data to JSON for the header
|
||||||
|
jsonData, err := json.Marshal(toastData)
|
||||||
|
if err != nil {
|
||||||
|
// Log the error, but maybe still try to send a basic trigger? Or just fail?
|
||||||
|
log.Printf("Error marshaling toast data for HX-Trigger: %v", err)
|
||||||
|
// Fallback or error handling - for now, just proceed without trigger maybe?
|
||||||
|
c.Status(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger toast notification on the frontend via HX-Trigger header
|
||||||
|
c.Header("HX-Trigger", string(jsonData))
|
||||||
|
c.Status(http.StatusOK) // Return 200 OK, but with no body swap intended
|
||||||
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ func (h *Handlers) RegisterRoutes(router *gin.Engine) {
|
|||||||
authorized.POST("/configs/:id", h.HandleUpdateConfig)
|
authorized.POST("/configs/:id", h.HandleUpdateConfig)
|
||||||
authorized.DELETE("/configs/:id", h.HandleDeleteConfig)
|
authorized.DELETE("/configs/:id", h.HandleDeleteConfig)
|
||||||
authorized.POST("/configs/:id/duplicate", h.HandleDuplicateConfig)
|
authorized.POST("/configs/:id/duplicate", h.HandleDuplicateConfig)
|
||||||
|
authorized.POST("/configs/test-connection", h.HandleTestProviderConnection)
|
||||||
|
|
||||||
// Path validation endpoint
|
// Path validation endpoint
|
||||||
authorized.GET("/check-path", h.HandleCheckPath)
|
authorized.GET("/check-path", h.HandleCheckPath)
|
||||||
|
|||||||
@@ -446,3 +446,20 @@ function enhanceMobileForms() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Listen for custom 'showToast' event triggered by HX-Trigger
|
||||||
|
document.body.addEventListener('showToast', function(event) {
|
||||||
|
// Debug logs removed
|
||||||
|
if (event.detail && event.detail.message && event.detail.type) {
|
||||||
|
// Call the globally defined showToast function from toast_js.templ
|
||||||
|
if (typeof showToast === 'function') {
|
||||||
|
showToast(event.detail.message, event.detail.type);
|
||||||
|
} else {
|
||||||
|
console.error('showToast function not found. Ensure toast_js.templ is loaded.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('Received showToast event without expected detail:', event.detail);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user