Files
WPSidekick/internal/storage/optimization_storage.go
T
2026-01-25 08:57:55 +01:00

229 lines
6.4 KiB
Go

package storage
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"time"
"seo-optimizer/internal/logger"
"seo-optimizer/pkg/models"
)
// OptimizationRecord represents a stored optimization result
type OptimizationRecord struct {
PostID int `json:"post_id"`
OriginalPostID int `json:"original_post_id"` // For draft posts
DraftPostID int `json:"draft_post_id"`
PostTitle string `json:"post_title"`
Language string `json:"language"`
OptimizedAt time.Time `json:"optimized_at"`
Status string `json:"status"` // "pending", "applied", "rejected", "skipped"
Optimization *models.OptimizationResponse `json:"optimization"`
SkipReason string `json:"skip_reason,omitempty"`
AppliedAt *time.Time `json:"applied_at,omitempty"`
}
// OptimizationStorage manages file-based storage of optimization results
type OptimizationStorage struct {
basePath string
logger *logger.Logger
}
// NewOptimizationStorage creates a new optimization storage instance
func NewOptimizationStorage(basePath string, log *logger.Logger) *OptimizationStorage {
if log == nil {
log = logger.NewDefaultLogger()
}
return &OptimizationStorage{
basePath: basePath,
logger: log.WithComponent("storage"),
}
}
// Initialize creates the storage directory structure
func (s *OptimizationStorage) Initialize() error {
dirs := []string{
s.basePath,
filepath.Join(s.basePath, "pending"),
filepath.Join(s.basePath, "applied"),
filepath.Join(s.basePath, "rejected"),
filepath.Join(s.basePath, "skipped"),
}
for _, dir := range dirs {
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create directory %s: %w", dir, err)
}
}
s.logger.Info("Storage initialized", "base_path", s.basePath)
return nil
}
// Save stores an optimization record
func (s *OptimizationStorage) Save(record *OptimizationRecord) error {
if record.Status == "" {
record.Status = "pending"
}
// Determine the directory based on status
dir := filepath.Join(s.basePath, record.Status)
filename := fmt.Sprintf("%d_%d.json", record.OriginalPostID, record.OptimizedAt.Unix())
filePath := filepath.Join(dir, filename)
data, err := json.MarshalIndent(record, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal record: %w", err)
}
if err := os.WriteFile(filePath, data, 0644); err != nil {
return fmt.Errorf("failed to write file: %w", err)
}
s.logger.Debug("Optimization record saved",
"post_id", record.OriginalPostID,
"status", record.Status,
"file", filePath,
)
return nil
}
// Get retrieves an optimization record by original post ID
func (s *OptimizationStorage) Get(originalPostID int) (*OptimizationRecord, error) {
// Search in all status directories
statuses := []string{"pending", "applied", "rejected", "skipped"}
for _, status := range statuses {
dir := filepath.Join(s.basePath, status)
files, err := os.ReadDir(dir)
if err != nil {
continue
}
// Find the most recent file for this post ID
for i := len(files) - 1; i >= 0; i-- {
file := files[i]
if file.IsDir() {
continue
}
// Check if filename starts with the post ID
var filePostID int
var timestamp int64
if _, err := fmt.Sscanf(file.Name(), "%d_%d.json", &filePostID, &timestamp); err != nil {
continue
}
if filePostID == originalPostID {
filePath := filepath.Join(dir, file.Name())
return s.readRecord(filePath)
}
}
}
return nil, fmt.Errorf("optimization record not found for post %d", originalPostID)
}
// ListPending returns all pending optimization records
func (s *OptimizationStorage) ListPending() ([]*OptimizationRecord, error) {
return s.listByStatus("pending")
}
// ListApplied returns all applied optimization records
func (s *OptimizationStorage) ListApplied() ([]*OptimizationRecord, error) {
return s.listByStatus("applied")
}
// listByStatus returns all records with the specified status
func (s *OptimizationStorage) listByStatus(status string) ([]*OptimizationRecord, error) {
dir := filepath.Join(s.basePath, status)
files, err := os.ReadDir(dir)
if err != nil {
if os.IsNotExist(err) {
return []*OptimizationRecord{}, nil
}
return nil, fmt.Errorf("failed to read directory: %w", err)
}
var records []*OptimizationRecord
for _, file := range files {
if file.IsDir() || filepath.Ext(file.Name()) != ".json" {
continue
}
filePath := filepath.Join(dir, file.Name())
record, err := s.readRecord(filePath)
if err != nil {
s.logger.Warn("Failed to read record", "file", filePath, "error", err)
continue
}
records = append(records, record)
}
// Sort by optimization date (newest first)
sort.Slice(records, func(i, j int) bool {
return records[i].OptimizedAt.After(records[j].OptimizedAt)
})
return records, nil
}
// UpdateStatus moves a record from one status to another
func (s *OptimizationStorage) UpdateStatus(originalPostID int, newStatus string) error {
// Get the current record
record, err := s.Get(originalPostID)
if err != nil {
return err
}
// Delete old file
oldDir := filepath.Join(s.basePath, record.Status)
oldFilename := fmt.Sprintf("%d_%d.json", record.OriginalPostID, record.OptimizedAt.Unix())
oldPath := filepath.Join(oldDir, oldFilename)
if err := os.Remove(oldPath); err != nil && !os.IsNotExist(err) {
s.logger.Warn("Failed to remove old record", "path", oldPath, "error", err)
}
// Update status
record.Status = newStatus
if newStatus == "applied" {
now := time.Now()
record.AppliedAt = &now
}
// Save to new location
return s.Save(record)
}
// GetLastOptimizationDate returns the last optimization date for a post
func (s *OptimizationStorage) GetLastOptimizationDate(postID int) (*time.Time, error) {
record, err := s.Get(postID)
if err != nil {
return nil, err
}
return &record.OptimizedAt, nil
}
// readRecord reads and unmarshals a record from a file
func (s *OptimizationStorage) readRecord(filePath string) (*OptimizationRecord, error) {
data, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("failed to read file: %w", err)
}
var record OptimizationRecord
if err := json.Unmarshal(data, &record); err != nil {
return nil, fmt.Errorf("failed to unmarshal record: %w", err)
}
return &record, nil
}