Files
insertr/internal/content/site_manager.go
Joakim 8d92c6477b feat: implement server-hosted static site enhancement with real-time content updates
- Add SiteManager for registering and managing static sites with file-based enhancement
- Implement EnhanceInPlace method for in-place file modification using database content
- Integrate automatic file enhancement triggers in UpdateContent API handler
- Add comprehensive site configuration support in insertr.yaml with auto-enhancement
- Extend serve command to automatically register and manage configured sites
- Add backup system for original files before enhancement
- Support multi-site hosting with individual auto-enhancement settings
- Update documentation for server-hosted enhancement workflow

This enables real-time content deployment where database content changes
immediately update static files without requiring rebuilds or redeployment.
The database remains the single source of truth while maintaining static
file performance benefits.
2025-09-10 23:05:09 +02:00

301 lines
7.4 KiB
Go

package content
import (
"fmt"
"log"
"os"
"path/filepath"
"sync"
"time"
)
// SiteConfig represents configuration for a registered site
type SiteConfig struct {
SiteID string `yaml:"site_id"`
Path string `yaml:"path"`
Domain string `yaml:"domain,omitempty"`
AutoEnhance bool `yaml:"auto_enhance"`
BackupOriginals bool `yaml:"backup_originals"`
}
// SiteManager handles registration and enhancement of static sites
type SiteManager struct {
sites map[string]*SiteConfig
enhancer *Enhancer
mutex sync.RWMutex
backupDir string
}
// NewSiteManager creates a new site manager
func NewSiteManager(contentClient ContentClient, backupDir string) *SiteManager {
if backupDir == "" {
backupDir = "./insertr-backups"
}
return &SiteManager{
sites: make(map[string]*SiteConfig),
enhancer: NewEnhancer(contentClient, ""), // siteID will be set per operation
backupDir: backupDir,
}
}
// RegisterSite adds a site to the manager
func (sm *SiteManager) RegisterSite(config *SiteConfig) error {
sm.mutex.Lock()
defer sm.mutex.Unlock()
// Validate site configuration
if config.SiteID == "" {
return fmt.Errorf("site_id is required")
}
if config.Path == "" {
return fmt.Errorf("path is required for site %s", config.SiteID)
}
// Check if path exists
if _, err := os.Stat(config.Path); os.IsNotExist(err) {
return fmt.Errorf("site path does not exist: %s", config.Path)
}
// Convert to absolute path
absPath, err := filepath.Abs(config.Path)
if err != nil {
return fmt.Errorf("failed to resolve absolute path for %s: %w", config.Path, err)
}
config.Path = absPath
sm.sites[config.SiteID] = config
log.Printf("📁 Registered site %s at %s", config.SiteID, config.Path)
return nil
}
// RegisterSites bulk registers multiple sites from configuration
func (sm *SiteManager) RegisterSites(configs []*SiteConfig) error {
for _, config := range configs {
if err := sm.RegisterSite(config); err != nil {
return fmt.Errorf("failed to register site %s: %w", config.SiteID, err)
}
}
return nil
}
// GetSite returns a registered site configuration
func (sm *SiteManager) GetSite(siteID string) (*SiteConfig, bool) {
sm.mutex.RLock()
defer sm.mutex.RUnlock()
site, exists := sm.sites[siteID]
return site, exists
}
// GetAllSites returns all registered sites
func (sm *SiteManager) GetAllSites() map[string]*SiteConfig {
sm.mutex.RLock()
defer sm.mutex.RUnlock()
// Return a copy to prevent external modification
result := make(map[string]*SiteConfig)
for id, site := range sm.sites {
result[id] = site
}
return result
}
// IsAutoEnhanceEnabled checks if a site has auto-enhancement enabled
func (sm *SiteManager) IsAutoEnhanceEnabled(siteID string) bool {
sm.mutex.RLock()
defer sm.mutex.RUnlock()
site, exists := sm.sites[siteID]
return exists && site.AutoEnhance
}
// EnhanceSite performs in-place enhancement of a registered site
func (sm *SiteManager) EnhanceSite(siteID string) error {
sm.mutex.RLock()
site, exists := sm.sites[siteID]
sm.mutex.RUnlock()
if !exists {
return fmt.Errorf("site %s is not registered", siteID)
}
log.Printf("🔄 Enhancing site %s at %s", siteID, site.Path)
// Create backup if enabled
if site.BackupOriginals {
if err := sm.createBackup(siteID, site.Path); err != nil {
log.Printf("⚠️ Failed to create backup for site %s: %v", siteID, err)
// Continue with enhancement even if backup fails
}
}
// Perform in-place enhancement
if err := sm.enhancer.EnhanceInPlace(site.Path, siteID); err != nil {
return fmt.Errorf("failed to enhance site %s: %w", siteID, err)
}
log.Printf("✅ Successfully enhanced site %s", siteID)
return nil
}
// EnhanceAllSites enhances all registered sites that have auto-enhancement enabled
func (sm *SiteManager) EnhanceAllSites() error {
sm.mutex.RLock()
sites := make([]*SiteConfig, 0, len(sm.sites))
for _, site := range sm.sites {
if site.AutoEnhance {
sites = append(sites, site)
}
}
sm.mutex.RUnlock()
var errors []error
for _, site := range sites {
if err := sm.EnhanceSite(site.SiteID); err != nil {
errors = append(errors, err)
}
}
if len(errors) > 0 {
return fmt.Errorf("enhancement failed for some sites: %v", errors)
}
return nil
}
// createBackup creates a timestamped backup of the site
func (sm *SiteManager) createBackup(siteID, sitePath string) error {
// Create backup directory structure
timestamp := time.Now().Format("20060102-150405")
backupPath := filepath.Join(sm.backupDir, siteID, timestamp)
if err := os.MkdirAll(backupPath, 0755); err != nil {
return fmt.Errorf("failed to create backup directory: %w", err)
}
// Copy HTML files to backup
return filepath.Walk(sitePath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Only backup HTML files
if !info.IsDir() && filepath.Ext(path) == ".html" {
relPath, err := filepath.Rel(sitePath, path)
if err != nil {
return err
}
backupFilePath := filepath.Join(backupPath, relPath)
// Create directory structure in backup
if err := os.MkdirAll(filepath.Dir(backupFilePath), 0755); err != nil {
return err
}
// Copy file
content, err := os.ReadFile(path)
if err != nil {
return err
}
return os.WriteFile(backupFilePath, content, info.Mode())
}
return nil
})
}
// RestoreFromBackup restores a site from a specific backup
func (sm *SiteManager) RestoreFromBackup(siteID, timestamp string) error {
sm.mutex.RLock()
site, exists := sm.sites[siteID]
sm.mutex.RUnlock()
if !exists {
return fmt.Errorf("site %s is not registered", siteID)
}
backupPath := filepath.Join(sm.backupDir, siteID, timestamp)
if _, err := os.Stat(backupPath); os.IsNotExist(err) {
return fmt.Errorf("backup not found: %s", backupPath)
}
log.Printf("🔄 Restoring site %s from backup %s", siteID, timestamp)
// Copy backup files back to site directory
return filepath.Walk(backupPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
relPath, err := filepath.Rel(backupPath, path)
if err != nil {
return err
}
targetPath := filepath.Join(site.Path, relPath)
// Create directory structure
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
return err
}
// Copy file
content, err := os.ReadFile(path)
if err != nil {
return err
}
return os.WriteFile(targetPath, content, info.Mode())
}
return nil
})
}
// ListBackups returns available backups for a site
func (sm *SiteManager) ListBackups(siteID string) ([]string, error) {
backupSitePath := filepath.Join(sm.backupDir, siteID)
if _, err := os.Stat(backupSitePath); os.IsNotExist(err) {
return []string{}, nil // No backups exist
}
entries, err := os.ReadDir(backupSitePath)
if err != nil {
return nil, fmt.Errorf("failed to read backup directory: %w", err)
}
var backups []string
for _, entry := range entries {
if entry.IsDir() {
backups = append(backups, entry.Name())
}
}
return backups, nil
}
// GetStats returns statistics about registered sites
func (sm *SiteManager) GetStats() map[string]interface{} {
sm.mutex.RLock()
defer sm.mutex.RUnlock()
autoEnhanceCount := 0
for _, site := range sm.sites {
if site.AutoEnhance {
autoEnhanceCount++
}
}
return map[string]interface{}{
"total_sites": len(sm.sites),
"auto_enhance_sites": autoEnhanceCount,
"backup_directory": sm.backupDir,
}
}