|
5 | 5 | "os" |
6 | 6 | "os/exec" |
7 | 7 | "strings" |
| 8 | + "time" |
8 | 9 |
|
9 | 10 | "github.com/nullable-eth/syncarr/internal/config" |
10 | 11 | "github.com/nullable-eth/syncarr/internal/logger" |
@@ -32,16 +33,134 @@ type FileTransferrer interface { |
32 | 33 | MapLocalPathToDest(localPath string) (string, error) |
33 | 34 | } |
34 | 35 |
|
35 | | -// NewTransferrer creates a new file transferrer based on the specified method |
| 36 | +// internalTransferer defines the interface for internal transfer implementations (rsync/scp) |
| 37 | +// These only handle the actual transfer without common logic like file checks and logging |
| 38 | +type internalTransferer interface { |
| 39 | + doTransferFile(sourcePath, destPath string) error |
| 40 | + doTransferFiles(files []types.FileTransfer) error |
| 41 | + close() error |
| 42 | + fileExists(path string) (bool, error) |
| 43 | + getFileSize(path string) (int64, error) |
| 44 | + deleteFile(path string) error |
| 45 | + listDirectoryContents(rootPath string) ([]string, error) |
| 46 | + mapSourcePathToLocal(sourcePath string) (string, error) |
| 47 | + mapLocalPathToDest(localPath string) (string, error) |
| 48 | +} |
| 49 | + |
| 50 | +// TransferClient is the unified client that handles common logic and delegates to internal implementations |
| 51 | +type TransferClient struct { |
| 52 | + method TransferMethod |
| 53 | + internal internalTransferer |
| 54 | + logger *logger.Logger |
| 55 | + sshConfig *config.SSHConfig |
| 56 | + serverConfig *config.PlexServerConfig |
| 57 | +} |
| 58 | + |
| 59 | +// NewTransferrer creates a new unified file transferrer that automatically chooses the best method |
36 | 60 | func NewTransferrer(method TransferMethod, cfg *config.Config, log *logger.Logger) (FileTransferrer, error) { |
| 61 | + var internal internalTransferer |
| 62 | + var err error |
| 63 | + |
37 | 64 | switch method { |
38 | 65 | case TransferMethodSFTP: |
39 | | - return NewSCPTransfer(cfg, log) |
| 66 | + internal, err = newSCPTransfer(cfg, log) |
40 | 67 | case TransferMethodRsync: |
41 | | - return NewRsyncTransfer(cfg, log) |
| 68 | + internal, err = newRsyncTransfer(cfg, log) |
42 | 69 | default: |
43 | 70 | return nil, fmt.Errorf("unsupported transfer method: %s", method) |
44 | 71 | } |
| 72 | + |
| 73 | + if err != nil { |
| 74 | + return nil, fmt.Errorf("failed to create %s transferrer: %w", method, err) |
| 75 | + } |
| 76 | + |
| 77 | + return &TransferClient{ |
| 78 | + method: method, |
| 79 | + internal: internal, |
| 80 | + logger: log, |
| 81 | + sshConfig: &cfg.SSH, |
| 82 | + serverConfig: &cfg.Destination, |
| 83 | + }, nil |
| 84 | +} |
| 85 | + |
| 86 | +// TransferFile handles file transfer with unified logic - checks file existence, size, and delegates to internal implementation |
| 87 | +func (t *TransferClient) TransferFile(sourcePath, destPath string) error { |
| 88 | + // Get source file info |
| 89 | + fileInfo, err := os.Stat(sourcePath) |
| 90 | + if err != nil { |
| 91 | + return fmt.Errorf("failed to stat source file: %w", err) |
| 92 | + } |
| 93 | + |
| 94 | + // Check if destination file already exists with same size (unified logic) |
| 95 | + destExists, err := t.internal.fileExists(destPath) |
| 96 | + if err != nil { |
| 97 | + t.logger.WithError(err).WithField("dest_path", destPath).Debug("Failed to check if destination file exists, proceeding with transfer") |
| 98 | + } else if destExists { |
| 99 | + // Check if sizes match - if so, skip transfer entirely |
| 100 | + destSize, err := t.internal.getFileSize(destPath) |
| 101 | + if err != nil { |
| 102 | + t.logger.WithError(err).WithField("dest_path", destPath).Debug("Failed to get destination file size, proceeding with transfer") |
| 103 | + } else if destSize == fileInfo.Size() { |
| 104 | + // Files are the same size, log skip and return early |
| 105 | + t.logger.LogTransferSkipped(sourcePath, destPath, fileInfo.Size(), "identical_size") |
| 106 | + return nil |
| 107 | + } |
| 108 | + } |
| 109 | + |
| 110 | + // If we get here, we're actually going to transfer the file |
| 111 | + startTime := time.Now() |
| 112 | + t.logger.LogTransferStarted(sourcePath, destPath, fileInfo.Size()) |
| 113 | + |
| 114 | + // Delegate to internal implementation for actual transfer |
| 115 | + if err := t.internal.doTransferFile(sourcePath, destPath); err != nil { |
| 116 | + return fmt.Errorf("transfer failed using %s: %w", t.method, err) |
| 117 | + } |
| 118 | + |
| 119 | + // Log successful completion |
| 120 | + duration := time.Since(startTime) |
| 121 | + t.logger.LogTransferCompleted(sourcePath, destPath, fileInfo.Size(), duration) |
| 122 | + |
| 123 | + return nil |
| 124 | +} |
| 125 | + |
| 126 | +// TransferFiles transfers multiple files (delegates to internal implementation) |
| 127 | +func (t *TransferClient) TransferFiles(files []types.FileTransfer) error { |
| 128 | + return t.internal.doTransferFiles(files) |
| 129 | +} |
| 130 | + |
| 131 | +// Close closes the transfer client |
| 132 | +func (t *TransferClient) Close() error { |
| 133 | + return t.internal.close() |
| 134 | +} |
| 135 | + |
| 136 | +// FileExists checks if a file exists on the destination |
| 137 | +func (t *TransferClient) FileExists(path string) (bool, error) { |
| 138 | + return t.internal.fileExists(path) |
| 139 | +} |
| 140 | + |
| 141 | +// GetFileSize gets the size of a file on the destination |
| 142 | +func (t *TransferClient) GetFileSize(path string) (int64, error) { |
| 143 | + return t.internal.getFileSize(path) |
| 144 | +} |
| 145 | + |
| 146 | +// DeleteFile deletes a file on the destination |
| 147 | +func (t *TransferClient) DeleteFile(path string) error { |
| 148 | + return t.internal.deleteFile(path) |
| 149 | +} |
| 150 | + |
| 151 | +// ListDirectoryContents lists directory contents on the destination |
| 152 | +func (t *TransferClient) ListDirectoryContents(rootPath string) ([]string, error) { |
| 153 | + return t.internal.listDirectoryContents(rootPath) |
| 154 | +} |
| 155 | + |
| 156 | +// MapSourcePathToLocal maps source path to local path |
| 157 | +func (t *TransferClient) MapSourcePathToLocal(sourcePath string) (string, error) { |
| 158 | + return t.internal.mapSourcePathToLocal(sourcePath) |
| 159 | +} |
| 160 | + |
| 161 | +// MapLocalPathToDest maps local path to destination path |
| 162 | +func (t *TransferClient) MapLocalPathToDest(localPath string) (string, error) { |
| 163 | + return t.internal.mapLocalPathToDest(localPath) |
45 | 164 | } |
46 | 165 |
|
47 | 166 | // GetOptimalTransferMethod returns the recommended transfer method based on system capabilities |
|
0 commit comments