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.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -102,3 +102,4 @@ tmp/
|
|||||||
.air/
|
.air/
|
||||||
|
|
||||||
# Node.js root dependencies
|
# Node.js root dependencies
|
||||||
|
insertr-backups/
|
||||||
|
|||||||
15
COMMANDS.md
15
COMMANDS.md
@@ -65,7 +65,7 @@ insertr enhance [input-dir] [flags]
|
|||||||
|
|
||||||
## 🔌 serve Command
|
## 🔌 serve Command
|
||||||
|
|
||||||
Runtime API server - provides HTTP endpoints for content management.
|
Runtime API server - provides HTTP endpoints for content management and server-hosted static site enhancement.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
insertr serve [flags]
|
insertr serve [flags]
|
||||||
@@ -96,6 +96,19 @@ insertr serve [flags]
|
|||||||
./insertr --config production.yaml serve
|
./insertr --config production.yaml serve
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Server-Hosted Static Sites
|
||||||
|
|
||||||
|
When running `insertr serve`, the server automatically:
|
||||||
|
- **Registers sites** from `insertr.yaml` configuration
|
||||||
|
- **Enhances static files** with latest database content
|
||||||
|
- **Auto-updates files** when content changes via API
|
||||||
|
- **Creates backups** of original files (if enabled)
|
||||||
|
|
||||||
|
**Live Enhancement Process:**
|
||||||
|
1. Content updated via API → Database updated
|
||||||
|
2. If site has `auto_enhance: true` → File enhancement triggered
|
||||||
|
3. Static files updated in-place → Changes immediately live
|
||||||
|
|
||||||
### API Endpoints
|
### API Endpoints
|
||||||
|
|
||||||
When running `insertr serve`, these endpoints are available:
|
When running `insertr serve`, these endpoints are available:
|
||||||
|
|||||||
76
README.md
76
README.md
@@ -395,6 +395,57 @@ mock_content: false # Use mock content instead of real data
|
|||||||
- Works with any framework or generator
|
- Works with any framework or generator
|
||||||
- Developer experience focused
|
- Developer experience focused
|
||||||
|
|
||||||
|
## 🏗️ **Server-Hosted Static Sites** (NEW)
|
||||||
|
|
||||||
|
**Real-time content updates for server-hosted static sites with immediate deployment.**
|
||||||
|
|
||||||
|
### **Overview**
|
||||||
|
Insertr now supports hosting and enhancing static sites directly on the server. When content changes through the editor, static files are automatically updated in-place without requiring rebuilds or redeployment.
|
||||||
|
|
||||||
|
### **Key Benefits**
|
||||||
|
- ✅ **Immediate Updates**: Content changes are live instantly
|
||||||
|
- ✅ **Database Source of Truth**: Content stored in database, not files
|
||||||
|
- ✅ **No Rebuilds Required**: Only content updates, templates stay the same
|
||||||
|
- ✅ **Multi-Site Support**: Host multiple enhanced sites on one server
|
||||||
|
- ✅ **Automatic Backups**: Original files backed up before enhancement
|
||||||
|
|
||||||
|
### **Workflow**
|
||||||
|
```
|
||||||
|
1. Developer deploys static site → Server directory (e.g., /var/www/mysite)
|
||||||
|
2. Insertr enhances files → Adds editing capabilities + injects content
|
||||||
|
3. Editor makes changes → API updates database + triggers file enhancement
|
||||||
|
4. Changes immediately live → Visitors see updated content instantly
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Configuration Example**
|
||||||
|
```yaml
|
||||||
|
# insertr.yaml
|
||||||
|
server:
|
||||||
|
port: 8080
|
||||||
|
sites:
|
||||||
|
- site_id: "mysite"
|
||||||
|
path: "/var/www/mysite"
|
||||||
|
domain: "mysite.example.com"
|
||||||
|
auto_enhance: true
|
||||||
|
backup_originals: true
|
||||||
|
|
||||||
|
- site_id: "blog"
|
||||||
|
path: "/var/www/blog"
|
||||||
|
domain: "blog.example.com"
|
||||||
|
auto_enhance: true
|
||||||
|
backup_originals: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Quick Start**
|
||||||
|
```bash
|
||||||
|
# 1. Configure sites in insertr.yaml
|
||||||
|
# 2. Start the server
|
||||||
|
./insertr serve --dev-mode
|
||||||
|
|
||||||
|
# 3. Your sites are automatically registered and enhanced
|
||||||
|
# 4. Content changes via editor immediately update static files
|
||||||
|
```
|
||||||
|
|
||||||
## ⚙️ Configuration
|
## ⚙️ Configuration
|
||||||
|
|
||||||
### **YAML Configuration (insertr.yaml)**
|
### **YAML Configuration (insertr.yaml)**
|
||||||
@@ -403,21 +454,34 @@ mock_content: false # Use mock content instead of real data
|
|||||||
database:
|
database:
|
||||||
path: "./insertr.db" # SQLite file path or PostgreSQL connection string
|
path: "./insertr.db" # SQLite file path or PostgreSQL connection string
|
||||||
|
|
||||||
|
# Server configuration (with site hosting)
|
||||||
|
server:
|
||||||
|
port: 8080 # HTTP server port
|
||||||
|
sites: # Server-hosted static sites
|
||||||
|
- site_id: "demo"
|
||||||
|
path: "./demo-site"
|
||||||
|
domain: "localhost:3000"
|
||||||
|
auto_enhance: true
|
||||||
|
backup_originals: true
|
||||||
|
|
||||||
# API configuration (for remote content API)
|
# API configuration (for remote content API)
|
||||||
api:
|
api:
|
||||||
url: "" # Content API URL (leave empty to use local database)
|
url: "" # Content API URL (leave empty to use local database)
|
||||||
key: "" # API authentication key
|
key: "" # API authentication key
|
||||||
|
|
||||||
# Server configuration
|
# Build configuration (for CLI enhancement)
|
||||||
server:
|
|
||||||
port: 8080 # HTTP server port
|
|
||||||
dev_mode: false # Enable development mode features
|
|
||||||
|
|
||||||
# Build configuration
|
|
||||||
build:
|
build:
|
||||||
input: "./src" # Default input directory for enhancement
|
input: "./src" # Default input directory for enhancement
|
||||||
output: "./dist" # Default output directory for enhanced files
|
output: "./dist" # Default output directory for enhanced files
|
||||||
|
|
||||||
|
# Authentication configuration
|
||||||
|
auth:
|
||||||
|
provider: "mock" # "mock", "jwt", "authentik"
|
||||||
|
jwt_secret: "" # JWT signing secret
|
||||||
|
oidc:
|
||||||
|
endpoint: "" # https://auth.example.com/application/o/insertr/
|
||||||
|
client_id: "" # OAuth2 client ID
|
||||||
|
|
||||||
# Global settings
|
# Global settings
|
||||||
site_id: "demo" # Default site ID for content lookup
|
site_id: "demo" # Default site ID for content lookup
|
||||||
mock_content: false # Use mock content instead of real data
|
mock_content: false # Use mock content instead of real data
|
||||||
|
|||||||
41
cmd/serve.go
41
cmd/serve.go
@@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
"github.com/insertr/insertr/internal/api"
|
"github.com/insertr/insertr/internal/api"
|
||||||
"github.com/insertr/insertr/internal/auth"
|
"github.com/insertr/insertr/internal/auth"
|
||||||
|
"github.com/insertr/insertr/internal/content"
|
||||||
"github.com/insertr/insertr/internal/db"
|
"github.com/insertr/insertr/internal/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -68,8 +69,48 @@ func runServe(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
authService := auth.NewAuthService(authConfig)
|
authService := auth.NewAuthService(authConfig)
|
||||||
|
|
||||||
|
// Initialize content client for site manager
|
||||||
|
contentClient := content.NewDatabaseClient(database)
|
||||||
|
|
||||||
|
// Initialize site manager
|
||||||
|
siteManager := content.NewSiteManager(contentClient, "./insertr-backups")
|
||||||
|
|
||||||
|
// Load sites from configuration
|
||||||
|
if siteConfigs := viper.Get("server.sites"); siteConfigs != nil {
|
||||||
|
if configs, ok := siteConfigs.([]interface{}); ok {
|
||||||
|
var sites []*content.SiteConfig
|
||||||
|
for _, configInterface := range configs {
|
||||||
|
if configMap, ok := configInterface.(map[string]interface{}); ok {
|
||||||
|
site := &content.SiteConfig{}
|
||||||
|
if siteID, ok := configMap["site_id"].(string); ok {
|
||||||
|
site.SiteID = siteID
|
||||||
|
}
|
||||||
|
if path, ok := configMap["path"].(string); ok {
|
||||||
|
site.Path = path
|
||||||
|
}
|
||||||
|
if domain, ok := configMap["domain"].(string); ok {
|
||||||
|
site.Domain = domain
|
||||||
|
}
|
||||||
|
if autoEnhance, ok := configMap["auto_enhance"].(bool); ok {
|
||||||
|
site.AutoEnhance = autoEnhance
|
||||||
|
}
|
||||||
|
if backupOriginals, ok := configMap["backup_originals"].(bool); ok {
|
||||||
|
site.BackupOriginals = backupOriginals
|
||||||
|
}
|
||||||
|
if site.SiteID != "" && site.Path != "" {
|
||||||
|
sites = append(sites, site)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := siteManager.RegisterSites(sites); err != nil {
|
||||||
|
log.Printf("⚠️ Failed to register some sites: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize handlers
|
// Initialize handlers
|
||||||
contentHandler := api.NewContentHandler(database, authService)
|
contentHandler := api.NewContentHandler(database, authService)
|
||||||
|
contentHandler.SetSiteManager(siteManager)
|
||||||
|
|
||||||
// Setup router
|
// Setup router
|
||||||
router := mux.NewRouter()
|
router := mux.NewRouter()
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -3,6 +3,7 @@ module github.com/insertr/insertr
|
|||||||
go 1.24.6
|
go 1.24.6
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
github.com/gorilla/mux v1.8.1
|
github.com/gorilla/mux v1.8.1
|
||||||
github.com/lib/pq v1.10.9
|
github.com/lib/pq v1.10.9
|
||||||
github.com/mattn/go-sqlite3 v1.14.32
|
github.com/mattn/go-sqlite3 v1.14.32
|
||||||
@@ -13,7 +14,6 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/magiconair/properties v1.8.7 // indirect
|
github.com/magiconair/properties v1.8.7 // indirect
|
||||||
|
|||||||
24
insertr.yaml
24
insertr.yaml
@@ -11,6 +11,18 @@ database:
|
|||||||
# Server configuration (multi-site ready)
|
# Server configuration (multi-site ready)
|
||||||
server:
|
server:
|
||||||
port: 8080 # HTTP API server port
|
port: 8080 # HTTP API server port
|
||||||
|
sites: # Registered sites for file-based enhancement
|
||||||
|
- site_id: "demo"
|
||||||
|
path: "./demo-site"
|
||||||
|
domain: "localhost:3000"
|
||||||
|
auto_enhance: true
|
||||||
|
backup_originals: true
|
||||||
|
# Example additional site configuration:
|
||||||
|
# - site_id: "mysite"
|
||||||
|
# path: "/var/www/mysite"
|
||||||
|
# domain: "mysite.example.com"
|
||||||
|
# auto_enhance: true
|
||||||
|
# backup_originals: true
|
||||||
|
|
||||||
# CLI enhancement configuration
|
# CLI enhancement configuration
|
||||||
cli:
|
cli:
|
||||||
@@ -20,4 +32,14 @@ cli:
|
|||||||
# API client configuration (for CLI remote mode)
|
# API client configuration (for CLI remote mode)
|
||||||
api:
|
api:
|
||||||
url: "" # Content API URL (empty = use local database)
|
url: "" # Content API URL (empty = use local database)
|
||||||
key: "" # API authentication key
|
key: "" # API authentication key
|
||||||
|
|
||||||
|
# Authentication configuration
|
||||||
|
auth:
|
||||||
|
provider: "mock" # "mock", "jwt", "authentik"
|
||||||
|
jwt_secret: "" # JWT signing secret (auto-generated in dev mode)
|
||||||
|
# Authentik OIDC configuration (for production)
|
||||||
|
oidc:
|
||||||
|
endpoint: "" # https://auth.example.com/application/o/insertr/
|
||||||
|
client_id: "" # insertr-client
|
||||||
|
client_secret: "" # OAuth2 client secret
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -12,6 +13,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/insertr/insertr/internal/auth"
|
"github.com/insertr/insertr/internal/auth"
|
||||||
|
"github.com/insertr/insertr/internal/content"
|
||||||
"github.com/insertr/insertr/internal/db"
|
"github.com/insertr/insertr/internal/db"
|
||||||
"github.com/insertr/insertr/internal/db/postgresql"
|
"github.com/insertr/insertr/internal/db/postgresql"
|
||||||
"github.com/insertr/insertr/internal/db/sqlite"
|
"github.com/insertr/insertr/internal/db/sqlite"
|
||||||
@@ -21,6 +23,7 @@ import (
|
|||||||
type ContentHandler struct {
|
type ContentHandler struct {
|
||||||
database *db.Database
|
database *db.Database
|
||||||
authService *auth.AuthService
|
authService *auth.AuthService
|
||||||
|
siteManager *content.SiteManager
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewContentHandler creates a new content handler
|
// NewContentHandler creates a new content handler
|
||||||
@@ -28,9 +31,15 @@ func NewContentHandler(database *db.Database, authService *auth.AuthService) *Co
|
|||||||
return &ContentHandler{
|
return &ContentHandler{
|
||||||
database: database,
|
database: database,
|
||||||
authService: authService,
|
authService: authService,
|
||||||
|
siteManager: nil, // Will be set via SetSiteManager
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetSiteManager sets the site manager for file enhancement
|
||||||
|
func (h *ContentHandler) SetSiteManager(siteManager *content.SiteManager) {
|
||||||
|
h.siteManager = siteManager
|
||||||
|
}
|
||||||
|
|
||||||
// GetContent handles GET /api/content/{id}
|
// GetContent handles GET /api/content/{id}
|
||||||
func (h *ContentHandler) GetContent(w http.ResponseWriter, r *http.Request) {
|
func (h *ContentHandler) GetContent(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
@@ -315,6 +324,17 @@ func (h *ContentHandler) UpdateContent(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
item := h.convertToAPIContent(upsertedContent)
|
item := h.convertToAPIContent(upsertedContent)
|
||||||
|
|
||||||
|
// Trigger file enhancement if site is registered for auto-enhancement
|
||||||
|
if h.siteManager != nil && h.siteManager.IsAutoEnhanceEnabled(siteID) {
|
||||||
|
go func() {
|
||||||
|
if err := h.siteManager.EnhanceSite(siteID); err != nil {
|
||||||
|
log.Printf("⚠️ Failed to enhance site %s: %v", siteID, err)
|
||||||
|
} else {
|
||||||
|
log.Printf("✅ Enhanced files for site %s", siteID)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(item)
|
json.NewEncoder(w).Encode(item)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -214,3 +214,109 @@ func (e *Enhancer) writeHTML(doc *html.Node, outputPath string) error {
|
|||||||
// Write HTML
|
// Write HTML
|
||||||
return html.Render(file, doc)
|
return html.Render(file, doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnhanceInPlace performs in-place enhancement of static site files
|
||||||
|
func (e *Enhancer) EnhanceInPlace(sitePath string, siteID string) error {
|
||||||
|
// Update the injector with the correct siteID
|
||||||
|
e.injector.siteID = siteID
|
||||||
|
|
||||||
|
// Use existing parser logic to discover elements
|
||||||
|
result, err := e.parser.ParseDirectory(sitePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parsing directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.Elements) == 0 {
|
||||||
|
fmt.Printf("📄 No insertr elements found in %s\n", sitePath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group elements by file for efficient processing
|
||||||
|
fileElements := make(map[string][]parser.Element)
|
||||||
|
for _, elem := range result.Elements {
|
||||||
|
fileElements[elem.FilePath] = append(fileElements[elem.FilePath], elem)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process each file in-place
|
||||||
|
enhancedCount := 0
|
||||||
|
for filePath, elements := range fileElements {
|
||||||
|
if err := e.enhanceFileInPlace(filePath, elements); err != nil {
|
||||||
|
fmt.Printf("⚠️ Failed to enhance %s: %v\n", filepath.Base(filePath), err)
|
||||||
|
} else {
|
||||||
|
enhancedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("✅ Enhanced %d files with %d elements in site %s\n",
|
||||||
|
enhancedCount, len(result.Elements), siteID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// enhanceFileInPlace modifies an HTML file in-place with database content
|
||||||
|
func (e *Enhancer) enhanceFileInPlace(filePath string, elements []parser.Element) error {
|
||||||
|
// Read original file
|
||||||
|
htmlContent, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse HTML
|
||||||
|
doc, err := html.Parse(strings.NewReader(string(htmlContent)))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parsing HTML: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert parser elements to injector format with content IDs
|
||||||
|
elementIDs := make([]ElementWithID, 0, len(elements))
|
||||||
|
for _, elem := range elements {
|
||||||
|
// Find the corresponding node in the parsed document
|
||||||
|
node := e.findNodeInDocument(doc, elem)
|
||||||
|
if node != nil {
|
||||||
|
elementIDs = append(elementIDs, ElementWithID{
|
||||||
|
Element: &Element{
|
||||||
|
Node: node,
|
||||||
|
Type: string(elem.Type),
|
||||||
|
Tag: elem.Tag,
|
||||||
|
},
|
||||||
|
ContentID: elem.ContentID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use existing bulk injection logic for efficiency
|
||||||
|
if len(elementIDs) > 0 {
|
||||||
|
if err := e.injector.InjectBulkContent(elementIDs); err != nil {
|
||||||
|
return fmt.Errorf("injecting content: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write enhanced HTML back to the same file (in-place update)
|
||||||
|
return e.writeHTML(doc, filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// findNodeInDocument finds a specific node in the HTML document tree
|
||||||
|
func (e *Enhancer) findNodeInDocument(doc *html.Node, elem parser.Element) *html.Node {
|
||||||
|
// This is a simplified approach - in a production system we might need
|
||||||
|
// more sophisticated node matching based on attributes, position, etc.
|
||||||
|
return e.findNodeByTagAndClass(doc, elem.Tag, "insertr")
|
||||||
|
}
|
||||||
|
|
||||||
|
// findNodeByTagAndClass recursively searches for a node with specific tag and class
|
||||||
|
func (e *Enhancer) findNodeByTagAndClass(node *html.Node, targetTag, targetClass string) *html.Node {
|
||||||
|
if node.Type == html.ElementNode && node.Data == targetTag {
|
||||||
|
classes := getClasses(node)
|
||||||
|
if containsClass(classes, targetClass) {
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search children
|
||||||
|
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||||
|
if result := e.findNodeByTagAndClass(child, targetTag, targetClass); result != nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
300
internal/content/site_manager.go
Normal file
300
internal/content/site_manager.go
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user