Files
goget/internal/protocol/ftp/protocol.go
T

224 lines
5.9 KiB
Go

//go:build linux || freebsd
// +build linux freebsd
package ftp
import (
"context"
"fmt"
"io"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"codeberg.org/petrbalvin/goget/internal/core"
"codeberg.org/petrbalvin/goget/internal/output"
)
// Protocol implements the core.Protocol interface for FTP/FTPS
type Protocol struct{}
// NewProtocol creates a new instance of FTP protocol
func NewProtocol() *Protocol {
return &Protocol{}
}
// Scheme returns the protocol scheme
func (p *Protocol) Scheme() string {
return "ftp"
}
// CanHandle checks whether the protocol can handle the given URL
func (p *Protocol) CanHandle(u *url.URL) bool {
if u == nil {
return false
}
scheme := strings.ToLower(u.Scheme)
return scheme == "ftp" || scheme == "ftps"
}
// Capabilities returns the list of supported features
func (p *Protocol) Capabilities() []string {
return []string{
"download",
"resume",
"recursive",
"tls",
}
}
// SupportsResume returns whether the protocol supports resume
func (p *Protocol) SupportsResume() bool {
return true
}
// SupportsCompression returns whether the protocol supports compression
func (p *Protocol) SupportsCompression() bool {
return false
}
// SupportsParallel returns whether the protocol supports parallel downloading
func (p *Protocol) SupportsParallel() bool {
return false
}
// SupportsRecursive returns whether the protocol supports recursive downloading
func (p *Protocol) SupportsRecursive() bool {
return true
}
// Download downloads a file or directory via FTP/FTPS
func (p *Protocol) Download(ctx context.Context, req *core.DownloadRequest) (*core.DownloadResult, error) {
timeout := req.Timeout
if timeout <= 0 {
timeout = 30 * time.Second
}
client, err := NewClient(req.URL.String(), timeout)
if err != nil {
return nil, core.NewProtocolError(fmt.Sprintf("failed to create FTP client: %v", err), nil, core.SafeURL(req.URL))
}
if req.PinnedCertHash != "" {
client.SetPinnedCert(req.PinnedCertHash)
}
if err := client.Connect(); err != nil {
return nil, core.NewProtocolError(fmt.Sprintf("failed to connect: %v", err), nil, core.SafeURL(req.URL))
}
defer client.Close()
startTime := time.Now()
var bytesDownloaded int64 = 0
var chunksCount int
// Determine whether it is a file or directory
remotePath := req.URL.Path
if remotePath == "" {
remotePath = "/"
}
// Try to get file size - if it fails, it's probably a directory
fileSize, err := client.getFileSize(remotePath)
isFile := (err == nil)
if isFile {
// Downloading file
var outputFile string
if req.Output != "" {
outputFile = req.Output
} else {
outputFile = filepath.Base(remotePath)
if outputFile == "" || outputFile == "/" {
outputFile = "index.html"
}
}
// Kontrola resume
var resumeOffset int64 = 0
if req.Resume && req.Output != "" {
if canResume, meta, _ := output.CanResume(req.Output, req.URL.String()); canResume && meta != nil {
resumeOffset = meta.Downloaded
if req.Verbose {
fmt.Fprintf(os.Stderr, "Resuming from offset %d\n", resumeOffset)
}
}
}
// Progress callback
var progressCallback func(current, total int64)
if req.ProgressCallback != nil && fileSize > 0 {
totalSize := fileSize
startOffset := resumeOffset
progressCallback = func(current, _ int64) {
req.ProgressCallback(startOffset+current, totalSize, 0)
}
}
if err := client.DownloadFile(ctx, remotePath, outputFile, resumeOffset, progressCallback); err != nil {
return nil, core.NewProtocolError(fmt.Sprintf("download failed: %v", err), nil, core.SafeURL(req.URL))
}
// Get the actual size of the downloaded file
if info, err := os.Stat(outputFile); err == nil {
bytesDownloaded = info.Size()
}
chunksCount = 1
} else {
// Recursive download of directory
if !req.Recursive {
return nil, core.NewProtocolError("remote path is a directory, use --recursive to download", nil, core.SafeURL(req.URL))
}
outputDir := req.Output
if outputDir == "" {
outputDir = filepath.Base(remotePath)
if outputDir == "" || outputDir == "/" {
outputDir = "download"
}
}
var progressCallback func(file string, current, total int64)
if req.Verbose {
progressCallback = func(file string, current, total int64) {
fmt.Fprintf(os.Stderr, "Downloading: %s\n", file)
}
}
// Dispatch to the parallel implementation when the user
// requested worker-pool concurrency, otherwise fall back to the
// single-connection sequential implementation. The parallel path
// opens N control connections to avoid stepping on CWD state and
// to keep the existing client-side protocol serialisation.
if req.RecursiveParallel > 1 {
if err := DownloadDirectoryConcurrent(ctx, req.URL.String(), timeout, remotePath, outputDir, req.RecursiveParallel, req.PinnedCertHash, progressCallback); err != nil {
return nil, core.NewProtocolError(fmt.Sprintf("directory download failed: %v", err), nil, core.SafeURL(req.URL))
}
} else {
if err := client.DownloadDirectory(ctx, remotePath, outputDir, progressCallback); err != nil {
return nil, core.NewProtocolError(fmt.Sprintf("directory download failed: %v", err), nil, core.SafeURL(req.URL))
}
}
// Calculate total size and file count
filepath.Walk(outputDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
bytesDownloaded += info.Size()
chunksCount++
}
return nil
})
}
duration := time.Since(startTime)
return &core.DownloadResult{
BytesDownloaded: bytesDownloaded,
Duration: duration,
ChunksCount: chunksCount,
}, nil
}
// Helper function for reading from io.Reader
func readAll(reader io.Reader) ([]byte, error) {
var data []byte
buf := make([]byte, 32*1024)
for {
n, err := reader.Read(buf)
if n > 0 {
data = append(data, buf[:n]...)
}
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
}
return data, nil
}