229 lines
6.4 KiB
Go
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, ×tamp); 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
|
|
}
|