feat: initial goget release — modern IPv6-first download utility
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
//go:build linux || freebsd
|
||||
// +build linux freebsd
|
||||
|
||||
package dataurl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"codeberg.org/petrbalvin/goget/internal/core"
|
||||
"codeberg.org/petrbalvin/goget/internal/protocol"
|
||||
)
|
||||
|
||||
// Protocol handles data: URLs (RFC 2397).
|
||||
type Protocol struct {
|
||||
*protocol.BaseProtocol
|
||||
}
|
||||
|
||||
// NewProtocol creates a new data: URL protocol handler.
|
||||
func NewProtocol() *Protocol {
|
||||
return &Protocol{
|
||||
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
|
||||
Name: "DATA",
|
||||
Scheme: "data",
|
||||
DefaultPort: 0,
|
||||
Features: []string{},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Protocol) Download(ctx context.Context, req *core.DownloadRequest) (*core.DownloadResult, error) {
|
||||
startTime := time.Now()
|
||||
|
||||
// data:[<mediatype>][;base64],<data>
|
||||
data, err := Decode(req.URL.String())
|
||||
if err != nil {
|
||||
return nil, core.NewProtocolError("failed to decode data URL", err, core.SafeURL(req.URL))
|
||||
}
|
||||
|
||||
var writer io.Writer
|
||||
outputPath := req.Output
|
||||
|
||||
if req.Writer != nil {
|
||||
writer = req.Writer
|
||||
} else if outputPath == "" || outputPath == "-" {
|
||||
writer = os.Stdout
|
||||
outputPath = "-"
|
||||
} else {
|
||||
f, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return nil, core.NewFileError("failed to create output file", err)
|
||||
}
|
||||
defer f.Close()
|
||||
writer = f
|
||||
}
|
||||
|
||||
n, err := writer.Write(data)
|
||||
if err != nil {
|
||||
return nil, core.NewFileError("failed to write data URL content", err)
|
||||
}
|
||||
|
||||
duration := time.Since(startTime)
|
||||
|
||||
return &core.DownloadResult{
|
||||
BytesDownloaded: int64(n),
|
||||
TotalSize: int64(len(data)),
|
||||
Duration: duration,
|
||||
Protocol: "DATA",
|
||||
IPVersion: 0,
|
||||
Speed: float64(n) / duration.Seconds(),
|
||||
OutputPath: outputPath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Decode parses and decodes a data: URL.
|
||||
func Decode(rawURL string) ([]byte, error) {
|
||||
if !strings.HasPrefix(rawURL, "data:") {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
content := strings.TrimPrefix(rawURL, "data:")
|
||||
isBase64 := false
|
||||
|
||||
if idx := strings.Index(content, ";base64,"); idx >= 0 {
|
||||
isBase64 = true
|
||||
content = content[idx+8:]
|
||||
} else if idx := strings.Index(content, ","); idx >= 0 {
|
||||
content = content[idx+1:]
|
||||
}
|
||||
|
||||
decoded, err := url.PathUnescape(content)
|
||||
if err != nil {
|
||||
decoded = content
|
||||
}
|
||||
|
||||
if isBase64 {
|
||||
return base64.StdEncoding.DecodeString(decoded)
|
||||
}
|
||||
return []byte(decoded), nil
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
//go:build linux || freebsd
|
||||
// +build linux freebsd
|
||||
|
||||
package dataurl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"codeberg.org/petrbalvin/goget/internal/core"
|
||||
)
|
||||
|
||||
func TestDecodePlainText(t *testing.T) {
|
||||
data, err := Decode("data:text/plain,hello%20world")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(data) != "hello world" {
|
||||
t.Errorf("got %q, want %q", data, "hello world")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeBase64(t *testing.T) {
|
||||
data, err := Decode("data:text/plain;base64,SGVsbG8=")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(data) != "Hello" {
|
||||
t.Errorf("got %q, want %q", data, "Hello")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeEmpty(t *testing.T) {
|
||||
data, err := Decode("data:,")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(data) != 0 {
|
||||
t.Errorf("expected empty, got %d bytes", len(data))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeNotDataURL(t *testing.T) {
|
||||
data, err := Decode("https://example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if data != nil {
|
||||
t.Error("expected nil for non-data URL")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadPlainText(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
proto := NewProtocol()
|
||||
u, _ := url.Parse("data:text/plain,test123")
|
||||
result, err := proto.Download(context.Background(), &core.DownloadRequest{
|
||||
URL: u,
|
||||
Writer: &buf,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.BytesDownloaded != 7 {
|
||||
t.Errorf("bytes = %d, want 7", result.BytesDownloaded)
|
||||
}
|
||||
if buf.String() != "test123" {
|
||||
t.Errorf("got %q, want %q", buf.String(), "test123")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadToFile(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
dst := filepath.Join(tmp, "out.txt")
|
||||
|
||||
proto := NewProtocol()
|
||||
u, _ := url.Parse("data:text/plain,hello")
|
||||
_, err := proto.Download(context.Background(), &core.DownloadRequest{
|
||||
URL: u,
|
||||
Output: dst,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, _ := os.ReadFile(dst)
|
||||
if string(data) != "hello" {
|
||||
t.Errorf("got %q, want %q", data, "hello")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user