203 lines
5 KiB
Go
203 lines
5 KiB
Go
package archiver
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"git.nakama.town/fmartingr/hako/internal/model"
|
|
"git.nakama.town/fmartingr/hako/internal/storage"
|
|
)
|
|
|
|
// DirectDownloadArchiver implements direct HTTP download archival
|
|
type DirectDownloadArchiver struct {
|
|
httpClient *http.Client
|
|
}
|
|
|
|
// NewDirectDownloadExtractor creates a new DirectDownloadArchiver
|
|
func NewDirectDownloadExtractor() *DirectDownloadArchiver {
|
|
return &DirectDownloadArchiver{
|
|
httpClient: &http.Client{
|
|
Timeout: 5 * time.Minute,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Key returns the unique identifier for this archiver
|
|
func (a *DirectDownloadArchiver) Key() string {
|
|
return "direct_download"
|
|
}
|
|
|
|
// Name returns the human-readable name for this archiver
|
|
func (a *DirectDownloadArchiver) Name() string {
|
|
return "Direct Download"
|
|
}
|
|
|
|
// IsEnabled checks if this archiver is available
|
|
func (a *DirectDownloadArchiver) IsEnabled(ctx context.Context) bool {
|
|
// Direct download is always enabled
|
|
return true
|
|
}
|
|
|
|
// Init initializes the archiver
|
|
func (a *DirectDownloadArchiver) Init() error {
|
|
// No initialization needed
|
|
return nil
|
|
}
|
|
|
|
// GetDefaultConfig returns the default configuration
|
|
func (a *DirectDownloadArchiver) GetDefaultConfig() any {
|
|
return map[string]any{
|
|
"timeout": "5m",
|
|
}
|
|
}
|
|
|
|
// ApplyConfig applies the provided configuration to the archiver
|
|
func (a *DirectDownloadArchiver) ApplyConfig(config map[string]any) error {
|
|
// Parse timeout duration
|
|
if timeoutStr, ok := config["timeout"].(string); ok {
|
|
timeout, err := time.ParseDuration(timeoutStr)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid timeout format: %w", err)
|
|
}
|
|
a.httpClient.Timeout = timeout
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Archive performs the direct download archival
|
|
func (a *DirectDownloadArchiver) Archive(ctx context.Context, link *model.Link, stor storage.Storage) (*ArchiveResult, error) {
|
|
// Create HTTP request
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, link.URL, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
// Set a user agent
|
|
req.Header.Set("User-Agent", "Hako/1.0")
|
|
|
|
// Perform the download
|
|
resp, err := a.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to download: %w", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
// Check status code
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
|
}
|
|
|
|
// Detect content type
|
|
contentType := resp.Header.Get("Content-Type")
|
|
if contentType == "" {
|
|
contentType = "application/octet-stream"
|
|
}
|
|
|
|
// Generate filename based on URL
|
|
filename := filepath.Base(link.URL)
|
|
if filename == "." || filename == "/" || filename == "" {
|
|
filename = "download"
|
|
}
|
|
|
|
// Add extension based on content type if not present
|
|
if filepath.Ext(filename) == "" {
|
|
ext := getExtensionFromContentType(contentType)
|
|
if ext != "" {
|
|
filename += ext
|
|
}
|
|
}
|
|
|
|
// Create a pipe to calculate hash while saving
|
|
pipeReader, pipeWriter := io.Pipe()
|
|
hash := sha256.New()
|
|
multiWriter := io.MultiWriter(pipeWriter, hash)
|
|
|
|
// Channel for errors from the goroutine
|
|
errChan := make(chan error, 1)
|
|
var path string
|
|
var fileSize int64
|
|
|
|
// Copy to storage in goroutine
|
|
go func() {
|
|
defer func() { _ = pipeWriter.Close() }()
|
|
if _, err := io.Copy(multiWriter, resp.Body); err != nil {
|
|
errChan <- fmt.Errorf("failed to copy data: %w", err)
|
|
return
|
|
}
|
|
errChan <- nil
|
|
}()
|
|
|
|
// Save to storage
|
|
path, err = stor.Save(ctx, link.ID, filename, pipeReader)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to save file: %w", err)
|
|
}
|
|
|
|
// Check for copy errors
|
|
if copyErr := <-errChan; copyErr != nil {
|
|
return nil, copyErr
|
|
}
|
|
|
|
// Get file size
|
|
if resp.ContentLength > 0 {
|
|
fileSize = resp.ContentLength
|
|
}
|
|
|
|
// Calculate hash
|
|
hashStr := hex.EncodeToString(hash.Sum(nil))
|
|
|
|
// Use filename as title (without extension for cleaner display)
|
|
title := filename
|
|
if ext := filepath.Ext(filename); ext != "" {
|
|
title = filename[:len(filename)-len(ext)]
|
|
}
|
|
// Fallback to URL if filename is not meaningful
|
|
if title == "" || title == "." || title == "/" || title == "download" {
|
|
title = link.URL
|
|
}
|
|
|
|
result := &ArchiveResult{
|
|
Title: title,
|
|
Files: []FileInfo{
|
|
{
|
|
Filename: filename,
|
|
MimeType: contentType,
|
|
FileSize: fileSize,
|
|
HashSha256: hashStr,
|
|
Path: path,
|
|
},
|
|
},
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// getExtensionFromContentType returns a file extension based on content type
|
|
func getExtensionFromContentType(contentType string) string {
|
|
// Simple mapping of common content types to extensions
|
|
types := map[string]string{
|
|
"text/html": ".html",
|
|
"text/plain": ".txt",
|
|
"application/pdf": ".pdf",
|
|
"image/jpeg": ".jpg",
|
|
"image/png": ".png",
|
|
"image/gif": ".gif",
|
|
"image/webp": ".webp",
|
|
"application/json": ".json",
|
|
"application/xml": ".xml",
|
|
"application/zip": ".zip",
|
|
"application/octet-stream": ".bin",
|
|
}
|
|
|
|
if ext, ok := types[contentType]; ok {
|
|
return ext
|
|
}
|
|
|
|
return ""
|
|
}
|