Files

212 lines
4.7 KiB
Go
Raw Permalink Normal View History

//go:build linux || freebsd
// +build linux freebsd
package file
import (
"context"
"io"
"os"
"path/filepath"
"time"
"codeberg.org/petrbalvin/goget/internal/core"
"codeberg.org/petrbalvin/goget/internal/protocol"
)
// Protocol implements file:// downloads.
type Protocol struct {
*protocol.BaseProtocol
}
// NewProtocol creates a new file protocol handler.
func NewProtocol() *Protocol {
return &Protocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "FILE",
Scheme: "file",
DefaultPort: 0,
Features: []string{"resume", "recursive"},
}),
}
}
func (p *Protocol) Download(ctx context.Context, req *core.DownloadRequest) (*core.DownloadResult, error) {
startTime := time.Now()
filePath := req.URL.Path
if req.URL.Host != "" {
filePath = filepath.Join("/", req.URL.Host, filePath)
}
filePath = filepath.Clean(filePath)
info, err := os.Stat(filePath)
if err != nil {
return nil, core.NewFileError("failed to stat source path", err)
}
if info.IsDir() {
return p.downloadDirectory(ctx, req, filePath, startTime)
}
src, err := os.Open(filePath)
if err != nil {
return nil, core.NewFileError("failed to open source file", err)
}
defer src.Close()
totalSize := info.Size()
var writer io.Writer
var outputPath string
if req.Writer != nil {
writer = req.Writer
outputPath = req.Output
} else if req.Output == "" || req.Output == "-" {
writer = os.Stdout
outputPath = "-"
} else {
dst, err := os.Create(req.Output)
if err != nil {
return nil, core.NewFileError("failed to create output file", err)
}
defer dst.Close()
writer = dst
outputPath = req.Output
}
var written int64
if req.Resume && outputPath != "" && outputPath != "-" {
if fi, err := os.Stat(outputPath); err == nil {
written = fi.Size()
if written >= totalSize {
return &core.DownloadResult{
BytesDownloaded: totalSize,
TotalSize: totalSize,
Duration: time.Since(startTime),
Protocol: "FILE",
OutputPath: outputPath,
Resumed: true,
}, nil
}
if _, err := src.Seek(written, io.SeekStart); err != nil {
return nil, core.NewFileError("failed to seek for resume", err)
}
}
}
buf := make([]byte, 32*1024)
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
n, err := src.Read(buf)
if n > 0 {
if _, werr := writer.Write(buf[:n]); werr != nil {
return nil, core.NewFileError("failed to write data", werr)
}
written += int64(n)
if req.ProgressCallback != nil {
speed := float64(written) / time.Since(startTime).Seconds()
req.ProgressCallback(written, totalSize, speed)
}
}
if err == io.EOF {
break
}
if err != nil {
return nil, core.NewFileError("failed to read source file", err)
}
}
duration := time.Since(startTime)
speed := float64(written) / duration.Seconds()
return &core.DownloadResult{
BytesDownloaded: written,
TotalSize: totalSize,
Duration: duration,
Protocol: "FILE",
Speed: speed,
OutputPath: outputPath,
Resumed: written > 0 && req.Resume,
}, nil
}
// downloadDirectory recursively copies a local directory.
func (p *Protocol) downloadDirectory(ctx context.Context, req *core.DownloadRequest, dirPath string, startTime time.Time) (*core.DownloadResult, error) {
outputBase := req.Output
if outputBase == "" {
outputBase = filepath.Base(dirPath)
}
var totalBytes int64
var fileCount int
err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
if err != nil {
return err
}
if info.IsDir() {
return nil
}
// Calculate relative path
rel, err := filepath.Rel(dirPath, path)
if err != nil {
return err
}
outputPath := filepath.Join(outputBase, rel)
if err := os.MkdirAll(filepath.Dir(outputPath), 0755); err != nil {
return err
}
src, err := os.Open(path)
if err != nil {
return err
}
defer src.Close()
dst, err := os.Create(outputPath)
if err != nil {
return err
}
defer dst.Close()
n, err := io.Copy(dst, src)
if err != nil {
return err
}
totalBytes += n
fileCount++
return nil
})
if err != nil {
return nil, core.NewFileError("failed to copy directory recursively", err)
}
duration := time.Since(startTime)
speed := float64(0)
if duration.Seconds() > 0 {
speed = float64(totalBytes) / duration.Seconds()
}
return &core.DownloadResult{
BytesDownloaded: totalBytes,
TotalSize: totalBytes,
Duration: duration,
Protocol: "FILE",
Speed: speed,
OutputPath: outputBase,
ChunksCount: fileCount,
}, nil
}