feat: initial goget release — modern IPv6-first download utility
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
//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 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, 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
|
||||
}
|
||||
Reference in New Issue
Block a user