hako/internal/jobs/worker.go

214 lines
6.6 KiB
Go

package jobs
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"path/filepath"
"time"
"git.nakama.town/fmartingr/hako/internal/archival/archiver"
"git.nakama.town/fmartingr/hako/internal/extractors"
"git.nakama.town/fmartingr/hako/internal/model"
)
// ArchiveService defines the interface for archive operations
type ArchiveService interface {
ProcessArchive(ctx context.Context, archive *model.Archive, link *model.Link, archiverKeys []string) error
}
// ArchiveStore defines the interface for archive operations
type ArchiveStore interface {
GetByID(ctx context.Context, id string) (*model.Archive, error)
UpdateStatus(ctx context.Context, id string, status model.ArchiveStatus, errorMessage string) error
}
// LinkStore defines the interface for link operations
type LinkStore interface {
GetByID(ctx context.Context, id string) (*model.Link, error)
}
// ArchiveFileStore defines the interface for archive file operations
type ArchiveFileStore interface {
GetByID(ctx context.Context, id string) (*model.ArchiveFile, error)
UpdateContent(ctx context.Context, fileID string, content string, contentMimeType string) error
Create(ctx context.Context, file *model.ArchiveFile) error
}
// Worker processes jobs from the queue
type Worker struct {
queue model.Queue
archiverMgr *archiver.Manager
extractorMgr *extractors.Manager
archiveService ArchiveService
archiveStore ArchiveStore
archiveFileStore ArchiveFileStore
linkStore LinkStore
storageBasePath string
logger *slog.Logger
}
// NewWorker creates a new worker
func NewWorker(queue model.Queue, archiverMgr *archiver.Manager, extractorMgr *extractors.Manager, archiveService ArchiveService, archiveStore ArchiveStore, archiveFileStore ArchiveFileStore, linkStore LinkStore, storageBasePath string, logger *slog.Logger) *Worker {
return &Worker{
queue: queue,
archiverMgr: archiverMgr,
extractorMgr: extractorMgr,
archiveService: archiveService,
archiveStore: archiveStore,
archiveFileStore: archiveFileStore,
linkStore: linkStore,
storageBasePath: storageBasePath,
logger: logger,
}
}
// Start starts the worker
func (w *Worker) Start(ctx context.Context) error {
w.logger.Info("Starting job worker")
for {
select {
case <-ctx.Done():
w.logger.Info("Job worker stopping")
return nil
default:
job, err := w.queue.Dequeue(ctx)
if err != nil {
w.logger.Error("Failed to dequeue job", "error", err)
time.Sleep(1 * time.Second)
continue
}
if job == nil {
time.Sleep(1 * time.Second)
continue
}
w.processJob(ctx, job)
}
}
}
// processJob processes a single job
func (w *Worker) processJob(ctx context.Context, job *model.Job) {
w.logger.Info("Processing job", "id", job.ID, "type", job.Type)
var err error
switch job.Type {
case model.JobTypeArchiveLink:
err = w.processArchiveLink(ctx, job)
case model.JobTypeExtractContent:
err = w.processExtractContent(ctx, job)
default:
err = fmt.Errorf("unknown job type: %s", job.Type)
}
if err != nil {
w.logger.Error("Job failed", "id", job.ID, "error", err)
if err := w.queue.Fail(ctx, job.ID, err.Error()); err != nil {
w.logger.Error("Failed to mark job as failed", "id", job.ID, "error", err)
}
return
}
if err := w.queue.Complete(ctx, job.ID); err != nil {
w.logger.Error("Failed to mark job as complete", "id", job.ID, "error", err)
}
w.logger.Info("Job completed", "id", job.ID)
}
// processArchiveLink processes an archive link job
func (w *Worker) processArchiveLink(ctx context.Context, job *model.Job) error {
var payload model.ArchiveLinkPayload
if err := json.Unmarshal([]byte(job.Payload), &payload); err != nil {
return fmt.Errorf("failed to unmarshal payload: %w", err)
}
// Get the archive
storeArchive, err := w.archiveStore.GetByID(ctx, payload.ArchiveID)
if err != nil {
return fmt.Errorf("failed to get archive: %w", err)
}
// Get the link
storeLink, err := w.linkStore.GetByID(ctx, payload.LinkID)
if err != nil {
// Update archive status to failed
_ = w.archiveStore.UpdateStatus(ctx, payload.ArchiveID, model.ArchiveStatusFailed, fmt.Sprintf("failed to get link: %v", err))
return fmt.Errorf("failed to get link: %w", err)
}
// Stores now return model types directly
archive := storeArchive
link := storeLink
// Process the archive with archiver keys from payload (if provided)
if err := w.archiveService.ProcessArchive(ctx, archive, link, payload.ArchiverKeys); err != nil {
// Update archive status to failed
_ = w.archiveStore.UpdateStatus(ctx, payload.ArchiveID, model.ArchiveStatusFailed, err.Error())
return fmt.Errorf("failed to process archive: %w", err)
}
return nil
}
// ExtractContentPayload is the payload for content extraction jobs
type ExtractContentPayload struct {
FileID string `json:"file_id"`
}
// processExtractContent processes a content extraction job
func (w *Worker) processExtractContent(ctx context.Context, job *model.Job) error {
var payload ExtractContentPayload
if err := json.Unmarshal([]byte(job.Payload), &payload); err != nil {
return fmt.Errorf("failed to unmarshal payload: %w", err)
}
// Get the archive file
file, err := w.archiveFileStore.GetByID(ctx, payload.FileID)
if err != nil {
return fmt.Errorf("failed to get archive file: %w", err)
}
// Skip if no MIME type
if file.MimeType == "" {
w.logger.Debug("Skipping content extraction - no MIME type", "fileID", payload.FileID)
return nil
}
// Find extractor for this MIME type
extractor, found := w.extractorMgr.GetExtractorForMimeType(file.MimeType)
if !found {
w.logger.Debug("No extractor found for MIME type", "fileID", payload.FileID, "mimeType", file.MimeType)
return nil // Not an error - just no extractor available
}
// Construct full file path
fullPath := filepath.Join(w.storageBasePath, file.StoragePath)
// Extract content
content, err := extractor.Extract(ctx, fullPath)
if err != nil {
w.logger.Error("Failed to extract content", "fileID", payload.FileID, "error", err)
return fmt.Errorf("failed to extract content: %w", err)
}
// Determine content MIME type based on extractor
contentMimeType := ""
if extractor.Key() == "readable" {
contentMimeType = "text/markdown"
} else if extractor.Key() == "pdf" {
contentMimeType = "text/plain"
}
// Update archive file with extracted content and content MIME type
if err := w.archiveFileStore.UpdateContent(ctx, payload.FileID, content, contentMimeType); err != nil {
return fmt.Errorf("failed to update archive file content: %w", err)
}
w.logger.Info("Content extracted successfully", "fileID", payload.FileID, "contentLength", len(content), "contentMimeType", contentMimeType)
return nil
}