183 lines
4.4 KiB
Go
183 lines
4.4 KiB
Go
package archiver
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.nakama.town/fmartingr/hako/internal/model"
|
|
"git.nakama.town/fmartingr/hako/internal/storage"
|
|
)
|
|
|
|
// ThumbnailArchiver implements thumbnail extraction from URLs
|
|
type ThumbnailArchiver struct {
|
|
httpClient *http.Client
|
|
resize bool
|
|
}
|
|
|
|
// NewThumbnailExtractor creates a new ThumbnailArchiver
|
|
func NewThumbnailExtractor() *ThumbnailArchiver {
|
|
return &ThumbnailArchiver{
|
|
httpClient: &http.Client{
|
|
Timeout: 5 * time.Minute,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Key returns the unique identifier for this archiver
|
|
func (e *ThumbnailArchiver) Key() string {
|
|
return "thumbnail"
|
|
}
|
|
|
|
// Name returns the human-readable name for this archiver
|
|
func (e *ThumbnailArchiver) Name() string {
|
|
return "Thumbnail"
|
|
}
|
|
|
|
// IsEnabled checks if this archiver is available
|
|
func (e *ThumbnailArchiver) IsEnabled(ctx context.Context) bool {
|
|
// Thumbnail archiver is always enabled
|
|
return true
|
|
}
|
|
|
|
// Init initializes the archiver
|
|
func (e *ThumbnailArchiver) Init() error {
|
|
// No initialization needed
|
|
return nil
|
|
}
|
|
|
|
// GetDefaultConfig returns the default configuration
|
|
func (e *ThumbnailArchiver) GetDefaultConfig() any {
|
|
return map[string]any{
|
|
"timeout": "5m",
|
|
"resize": false,
|
|
}
|
|
}
|
|
|
|
// ApplyConfig applies the provided configuration to the archiver
|
|
func (e *ThumbnailArchiver) 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)
|
|
}
|
|
e.httpClient.Timeout = timeout
|
|
}
|
|
|
|
// Parse resize setting
|
|
if resizeVal, ok := config["resize"]; ok {
|
|
if resize, ok := resizeVal.(bool); ok {
|
|
e.resize = resize
|
|
} else {
|
|
return fmt.Errorf("invalid resize format: expected boolean")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Archive performs the thumbnail extraction
|
|
func (e *ThumbnailArchiver) Archive(ctx context.Context, link *model.Link, stor storage.Storage) (*ArchiveResult, error) {
|
|
// Fetch the URL to get HTML
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, link.URL, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("User-Agent", "Hako/1.0")
|
|
|
|
resp, err := e.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch URL: %w", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
// Check if it's HTML content
|
|
contentType := resp.Header.Get("Content-Type")
|
|
if !strings.HasPrefix(contentType, "text/html") {
|
|
// Not HTML, skip thumbnail extraction
|
|
return &ArchiveResult{
|
|
Title: "",
|
|
Files: []FileInfo{},
|
|
}, nil
|
|
}
|
|
|
|
// Read HTML content
|
|
htmlBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read HTML: %w", err)
|
|
}
|
|
|
|
// Extract thumbnail from HTML
|
|
thumbnailResult, err := extractThumbnailFromHTML(ctx, htmlBytes, link.URL, e.resize)
|
|
if err != nil || thumbnailResult == nil {
|
|
// No thumbnail found or error - return empty result (not an error)
|
|
return &ArchiveResult{
|
|
Title: "",
|
|
Files: []FileInfo{},
|
|
}, nil
|
|
}
|
|
|
|
// 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() }()
|
|
reader := bytes.NewReader(thumbnailResult.ImageBytes)
|
|
if _, err := io.Copy(multiWriter, reader); err != nil {
|
|
errChan <- fmt.Errorf("failed to copy data: %w", err)
|
|
return
|
|
}
|
|
errChan <- nil
|
|
}()
|
|
|
|
// Save to storage
|
|
path, err = stor.Save(ctx, link.ID, thumbnailResult.Filename, pipeReader)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to save thumbnail: %w", err)
|
|
}
|
|
|
|
// Check for copy errors
|
|
if copyErr := <-errChan; copyErr != nil {
|
|
return nil, copyErr
|
|
}
|
|
|
|
// Get file size
|
|
fileSize = int64(len(thumbnailResult.ImageBytes))
|
|
|
|
// Calculate hash
|
|
hashStr := hex.EncodeToString(hash.Sum(nil))
|
|
|
|
// Create MIME type with +thumbnail suffix
|
|
mimeType := thumbnailResult.MimeType + "+thumbnail"
|
|
|
|
result := &ArchiveResult{
|
|
Title: "",
|
|
Files: []FileInfo{
|
|
{
|
|
Filename: thumbnailResult.Filename,
|
|
MimeType: mimeType,
|
|
FileSize: fileSize,
|
|
HashSha256: hashStr,
|
|
Path: path,
|
|
},
|
|
},
|
|
}
|
|
|
|
return result, nil
|
|
}
|