//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 }