feat: update auto render
This commit is contained in:
@@ -10,9 +10,9 @@ import (
|
||||
|
||||
// Config represents the application configuration
|
||||
type Config struct {
|
||||
App AppConfig `mapstructure:"app"`
|
||||
Database DatabaseConfig `mapstructure:"database"`
|
||||
Tables map[string]TableConfig `mapstructure:"tables"`
|
||||
App AppConfig `mapstructure:"app"`
|
||||
Database DatabaseConfig `mapstructure:"database"`
|
||||
Tables map[string]TableConfig `mapstructure:"tables"`
|
||||
}
|
||||
|
||||
// AppConfig holds application-level configuration
|
||||
|
||||
247
internal/template/config_loader.go
Normal file
247
internal/template/config_loader.go
Normal file
@@ -0,0 +1,247 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// ConfigLoader loads template configuration from YAML files
|
||||
type ConfigLoader struct {
|
||||
configPath string
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
// NewConfigLoader creates a new template config loader
|
||||
func NewConfigLoader(configPath string) *ConfigLoader {
|
||||
return &ConfigLoader{
|
||||
configPath: configPath,
|
||||
logger: slog.With("component", "template_config"),
|
||||
}
|
||||
}
|
||||
|
||||
// TemplateConfigFile represents the structure of template configuration file
|
||||
type TemplateConfigFile struct {
|
||||
Templates map[string]struct {
|
||||
Name string `yaml:"name"`
|
||||
Type string `yaml:"type"`
|
||||
Description string `yaml:"description"`
|
||||
Fields map[string]string `yaml:"fields"`
|
||||
Config map[string]interface{} `yaml:"config"`
|
||||
} `yaml:"templates"`
|
||||
TemplateTypes map[string]struct {
|
||||
Layout string `yaml:"layout"`
|
||||
Fields map[string]string `yaml:"fields"`
|
||||
Options map[string]interface{} `yaml:"options"`
|
||||
} `yaml:"template_types"`
|
||||
}
|
||||
|
||||
|
||||
|
||||
// LoadTemplateConfig loads template configuration from YAML files
|
||||
func (cl *ConfigLoader) LoadTemplateConfig() (*TemplateConfigFile, error) {
|
||||
viper.SetConfigName("templates")
|
||||
viper.SetConfigType("yaml")
|
||||
|
||||
// Add search paths
|
||||
viper.AddConfigPath(cl.configPath)
|
||||
viper.AddConfigPath("./config/templates")
|
||||
viper.AddConfigPath("./web/templates")
|
||||
viper.AddConfigPath("/etc/database-render/templates")
|
||||
|
||||
// Set default values
|
||||
viper.SetDefault("templates", map[string]interface{}{})
|
||||
viper.SetDefault("template_types", map[string]interface{}{
|
||||
"list": map[string]interface{}{
|
||||
"layout": "table",
|
||||
"fields": map[string]string{
|
||||
"default": "raw",
|
||||
"time": "time",
|
||||
"tag": "tag",
|
||||
},
|
||||
"options": map[string]interface{}{
|
||||
"striped": true,
|
||||
"hover": true,
|
||||
},
|
||||
},
|
||||
"card": map[string]interface{}{
|
||||
"layout": "grid",
|
||||
"fields": map[string]string{
|
||||
"default": "raw",
|
||||
"time": "relative",
|
||||
"tag": "badge",
|
||||
},
|
||||
"options": map[string]interface{}{
|
||||
"columns": "3",
|
||||
"spacing": "md",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Read configuration
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
||||
cl.logger.Warn("template config file not found, using defaults")
|
||||
return cl.createDefaultConfig(), nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to read template config: %w", err)
|
||||
}
|
||||
|
||||
var config TemplateConfigFile
|
||||
if err := viper.Unmarshal(&config); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal template config: %w", err)
|
||||
}
|
||||
|
||||
cl.logger.Info("template configuration loaded successfully",
|
||||
"config_file", viper.ConfigFileUsed(),
|
||||
"templates_count", len(config.Templates),
|
||||
"types_count", len(config.TemplateTypes))
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// createDefaultConfig creates default template configuration
|
||||
func (cl *ConfigLoader) createDefaultConfig() *TemplateConfigFile {
|
||||
return &TemplateConfigFile{
|
||||
Templates: map[string]struct {
|
||||
Name string `yaml:"name"`
|
||||
Type string `yaml:"type"`
|
||||
Description string `yaml:"description"`
|
||||
Fields map[string]string `yaml:"fields"`
|
||||
Config map[string]interface{} `yaml:"config"`
|
||||
}{
|
||||
"articles": {
|
||||
Name: "文章列表",
|
||||
Type: "list",
|
||||
Description: "技术文章列表视图",
|
||||
Fields: map[string]string{
|
||||
"title": "string",
|
||||
"content": "markdown",
|
||||
"category": "category",
|
||||
"tags": "tag",
|
||||
"created_at": "time",
|
||||
},
|
||||
Config: map[string]interface{}{
|
||||
"page_size": 15,
|
||||
"show_pagination": true,
|
||||
},
|
||||
},
|
||||
"logs": {
|
||||
Name: "系统日志",
|
||||
Type: "table",
|
||||
Description: "系统日志表格视图",
|
||||
Fields: map[string]string{
|
||||
"level": "tag",
|
||||
"message": "string",
|
||||
"timestamp": "time",
|
||||
},
|
||||
Config: map[string]interface{}{
|
||||
"page_size": 50,
|
||||
"show_filter": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
TemplateTypes: map[string]struct {
|
||||
Layout string `yaml:"layout"`
|
||||
Fields map[string]string `yaml:"fields"`
|
||||
Options map[string]interface{} `yaml:"options"`
|
||||
}{
|
||||
"list": {
|
||||
Layout: "table",
|
||||
Fields: map[string]string{
|
||||
"default": "raw",
|
||||
"time": "time",
|
||||
"tag": "tag",
|
||||
},
|
||||
Options: map[string]interface{}{
|
||||
"striped": true,
|
||||
"hover": true,
|
||||
},
|
||||
},
|
||||
"card": {
|
||||
Layout: "grid",
|
||||
Fields: map[string]string{
|
||||
"default": "raw",
|
||||
"time": "relative",
|
||||
"tag": "badge",
|
||||
},
|
||||
Options: map[string]interface{}{
|
||||
"columns": "3",
|
||||
"spacing": "md",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateTemplateConfig validates template configuration
|
||||
func (cl *ConfigLoader) ValidateTemplateConfig(config *TemplateConfigFile) error {
|
||||
for name, template := range config.Templates {
|
||||
if template.Name == "" {
|
||||
return fmt.Errorf("template %s: name cannot be empty", name)
|
||||
}
|
||||
if template.Type == "" {
|
||||
return fmt.Errorf("template %s: type cannot be empty", name)
|
||||
}
|
||||
}
|
||||
|
||||
for name, templateType := range config.TemplateTypes {
|
||||
if templateType.Layout == "" {
|
||||
return fmt.Errorf("template type %s: layout cannot be empty", name)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTemplateConfig returns template configuration for a specific table
|
||||
func (cl *ConfigLoader) GetTemplateConfig(tableName string) (interface{}, bool) {
|
||||
config, err := cl.LoadTemplateConfig()
|
||||
if err != nil {
|
||||
cl.logger.Error("failed to load template config", "error", err)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
template, exists := config.Templates[tableName]
|
||||
return template, exists
|
||||
}
|
||||
|
||||
// GetTemplateType returns template type configuration
|
||||
func (cl *ConfigLoader) GetTemplateType(typeName string) (interface{}, bool) {
|
||||
config, err := cl.LoadTemplateConfig()
|
||||
if err != nil {
|
||||
cl.logger.Error("failed to load template config", "error", err)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
typeConfig, exists := config.TemplateTypes[typeName]
|
||||
return typeConfig, exists
|
||||
}
|
||||
|
||||
// GetAvailableTemplates returns all available templates
|
||||
func (cl *ConfigLoader) GetAvailableTemplates() map[string]string {
|
||||
config, err := cl.LoadTemplateConfig()
|
||||
if err != nil {
|
||||
cl.logger.Error("failed to load template config", "error", err)
|
||||
return map[string]string{"list": "默认列表"}
|
||||
}
|
||||
|
||||
result := make(map[string]string)
|
||||
for key, template := range config.Templates {
|
||||
result[key] = template.Description
|
||||
}
|
||||
|
||||
// Add built-in templates if not overridden
|
||||
if _, exists := result["list"]; !exists {
|
||||
result["list"] = "列表视图"
|
||||
}
|
||||
if _, exists := result["card"]; !exists {
|
||||
result["card"] = "卡片视图"
|
||||
}
|
||||
if _, exists := result["timeline"]; !exists {
|
||||
result["timeline"] = "时间轴视图"
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
178
internal/template/hot_reload.go
Normal file
178
internal/template/hot_reload.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
// HotReloadWatcher watches template files for changes
|
||||
type HotReloadWatcher struct {
|
||||
watcher *fsnotify.Watcher
|
||||
loader *TemplateLoader
|
||||
configPath string
|
||||
logger *slog.Logger
|
||||
stopChan chan struct{}
|
||||
}
|
||||
|
||||
// NewHotReloadWatcher creates a new hot reload watcher
|
||||
func NewHotReloadWatcher(loader *TemplateLoader, configPath string) (*HotReloadWatcher, error) {
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &HotReloadWatcher{
|
||||
watcher: watcher,
|
||||
loader: loader,
|
||||
configPath: configPath,
|
||||
logger: slog.With("component", "hot_reload"),
|
||||
stopChan: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Start starts watching template files for changes
|
||||
func (hr *HotReloadWatcher) Start() error {
|
||||
// Watch template directories
|
||||
directories := []string{
|
||||
hr.configPath,
|
||||
filepath.Join(hr.configPath, "custom"),
|
||||
filepath.Join(hr.configPath, "builtin"),
|
||||
filepath.Join(hr.configPath, "custom", "_default"),
|
||||
}
|
||||
|
||||
for _, dir := range directories {
|
||||
if err := hr.addDirectory(dir); err != nil {
|
||||
hr.logger.Warn("failed to watch directory", "dir", dir, "error", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
go hr.watch()
|
||||
|
||||
hr.logger.Info("hot reload watcher started", "directories", directories)
|
||||
return nil
|
||||
}
|
||||
|
||||
// addDirectory adds a directory and its subdirectories to the watcher
|
||||
func (hr *HotReloadWatcher) addDirectory(dir string) error {
|
||||
return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
return hr.watcher.Add(path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// watch watches for file changes and triggers reloads
|
||||
func (hr *HotReloadWatcher) watch() {
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
debounce := make(map[string]time.Time)
|
||||
|
||||
for {
|
||||
select {
|
||||
case event := <-hr.watcher.Events:
|
||||
if event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create {
|
||||
// Debounce rapid file changes
|
||||
lastChange := debounce[event.Name]
|
||||
if time.Since(lastChange) > 100*time.Millisecond {
|
||||
debounce[event.Name] = time.Now()
|
||||
hr.handleFileChange(event.Name)
|
||||
}
|
||||
}
|
||||
case err := <-hr.watcher.Errors:
|
||||
if err != nil {
|
||||
hr.logger.Error("watcher error", "error", err)
|
||||
}
|
||||
case <-hr.stopChan:
|
||||
return
|
||||
case <-ticker.C:
|
||||
// Cleanup old debounce entries
|
||||
now := time.Now()
|
||||
for file, lastChange := range debounce {
|
||||
if now.Sub(lastChange) > 5*time.Second {
|
||||
delete(debounce, file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleFileChange handles a file change event
|
||||
func (hr *HotReloadWatcher) handleFileChange(filePath string) {
|
||||
ext := filepath.Ext(filePath)
|
||||
if ext != ".html" && ext != ".yaml" && ext != ".yml" {
|
||||
return
|
||||
}
|
||||
|
||||
hr.logger.Info("detected file change", "file", filePath)
|
||||
|
||||
// Reload templates
|
||||
hr.loader.ClearCache()
|
||||
|
||||
// Notify clients via WebSocket or SSE
|
||||
hr.notifyClients()
|
||||
}
|
||||
|
||||
// notifyClients notifies connected clients about template changes
|
||||
func (hr *HotReloadWatcher) notifyClients() {
|
||||
// This could be extended to use WebSocket or Server-Sent Events
|
||||
// For now, just log the change
|
||||
hr.logger.Info("templates reloaded")
|
||||
}
|
||||
|
||||
// Stop stops the hot reload watcher
|
||||
func (hr *HotReloadWatcher) Stop() {
|
||||
close(hr.stopChan)
|
||||
hr.watcher.Close()
|
||||
hr.logger.Info("hot reload watcher stopped")
|
||||
}
|
||||
|
||||
// AddTemplateDirectory adds a new template directory to watch
|
||||
func (hr *HotReloadWatcher) AddTemplateDirectory(path string) error {
|
||||
return hr.addDirectory(path)
|
||||
}
|
||||
|
||||
// RemoveTemplateDirectory removes a template directory from watch
|
||||
func (hr *HotReloadWatcher) RemoveTemplateDirectory(path string) error {
|
||||
return hr.watcher.Remove(path)
|
||||
}
|
||||
|
||||
// GetWatchedDirectories returns currently watched directories
|
||||
func (hr *HotReloadWatcher) GetWatchedDirectories() []string {
|
||||
return hr.watcher.WatchList()
|
||||
}
|
||||
|
||||
// IsWatching returns whether the watcher is active
|
||||
func (hr *HotReloadWatcher) IsWatching() bool {
|
||||
select {
|
||||
case <-hr.stopChan:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// ReloadTemplates triggers a manual template reload
|
||||
func (hr *HotReloadWatcher) ReloadTemplates() {
|
||||
hr.loader.ClearCache()
|
||||
hr.logger.Info("templates manually reloaded")
|
||||
}
|
||||
|
||||
// GetReloadStats returns hot reload statistics
|
||||
func (hr *HotReloadWatcher) GetReloadStats() map[string]interface{} {
|
||||
watched := hr.GetWatchedDirectories()
|
||||
return map[string]interface{}{
|
||||
"watched_directories": len(watched),
|
||||
"watched_paths": watched,
|
||||
"is_active": hr.IsWatching(),
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/gofiber/fiber/v3/middleware/static"
|
||||
"github.com/rogeecn/database_render/internal/config"
|
||||
"github.com/rogeecn/database_render/internal/model"
|
||||
"github.com/rogeecn/database_render/internal/service"
|
||||
)
|
||||
|
||||
@@ -204,29 +205,81 @@ func (r *Renderer) RenderList(c fiber.Ctx, tableName string) error {
|
||||
|
||||
// Prepare template data
|
||||
templateData := map[string]interface{}{
|
||||
"Table": tableName,
|
||||
"TableAlias": tableConfig.Alias,
|
||||
"Columns": data.Columns,
|
||||
"Data": data.Data,
|
||||
"Total": data.Total,
|
||||
"Page": data.Page,
|
||||
"PerPage": data.PerPage,
|
||||
"Pages": data.Pages,
|
||||
"Search": search,
|
||||
"SortField": sortField,
|
||||
"SortOrder": sortOrder,
|
||||
"Tables": tables,
|
||||
"CurrentPath": c.Path(),
|
||||
"StartRecord": startRecord,
|
||||
"EndRecord": endRecord,
|
||||
"LastUpdate": time.Now().Format("2006-01-02 15:04:05"),
|
||||
"Table": tableName,
|
||||
"TableAlias": tableConfig.Alias,
|
||||
"Columns": data.Columns,
|
||||
"Data": data.Data,
|
||||
"Total": data.Total,
|
||||
"Page": data.Page,
|
||||
"PerPage": data.PerPage,
|
||||
"Pages": data.Pages,
|
||||
"Search": search,
|
||||
"SortField": sortField,
|
||||
"SortOrder": sortOrder,
|
||||
"Tables": tables,
|
||||
"CurrentPath": c.Path(),
|
||||
"StartRecord": startRecord,
|
||||
"EndRecord": endRecord,
|
||||
"LastUpdate": time.Now().Format("2006-01-02 15:04:05"),
|
||||
"TemplateType": "table", // Default to table view
|
||||
}
|
||||
|
||||
// set content-type html
|
||||
c.Response().Header.Set("Content-Type", "text/html; charset=utf-8")
|
||||
|
||||
// Render template
|
||||
return r.templates.ExecuteTemplate(c.Response().BodyWriter(), "list.html", templateData)
|
||||
// Use new template system for rendering
|
||||
absBuiltinPath, _ := filepath.Abs("./web/templates/builtin")
|
||||
absCustomPath, _ := filepath.Abs("./web/templates/custom")
|
||||
|
||||
loader := NewTemplateLoader(TemplateConfig{
|
||||
BuiltinPath: absBuiltinPath,
|
||||
CustomPath: absCustomPath,
|
||||
CacheEnabled: true,
|
||||
})
|
||||
|
||||
// Debug the data structure safely
|
||||
r.logger.Info("template data structure",
|
||||
"data_type", fmt.Sprintf("%T", templateData["Data"]),
|
||||
"columns_type", fmt.Sprintf("%T", templateData["Columns"]),
|
||||
)
|
||||
|
||||
// Check data length safely
|
||||
var dataLength int
|
||||
if data, ok := templateData["Data"].([]map[string]interface{}); ok {
|
||||
dataLength = len(data)
|
||||
} else if data, ok := templateData["Data"].([]interface{}); ok {
|
||||
dataLength = len(data)
|
||||
}
|
||||
|
||||
var columnsLength int
|
||||
if cols, ok := templateData["Columns"].([]model.ColumnConfig); ok {
|
||||
columnsLength = len(cols)
|
||||
} else if cols, ok := templateData["Columns"].([]interface{}); ok {
|
||||
columnsLength = len(cols)
|
||||
}
|
||||
|
||||
r.logger.Info("data lengths", "data_length", dataLength, "columns_length", columnsLength)
|
||||
|
||||
// Try complete template - standalone template without layout inheritance
|
||||
tmpl, err := loader.LoadTemplate("complete", tableName)
|
||||
if err != nil {
|
||||
r.logger.Error("failed to load complete template", "error", err)
|
||||
// Fallback to simple template
|
||||
tmpl, err = loader.LoadTemplate("simple", tableName)
|
||||
if err != nil {
|
||||
r.logger.Error("failed to load simple template", "error", err)
|
||||
return r.templates.ExecuteTemplate(c.Response().BodyWriter(), "list.html", templateData)
|
||||
}
|
||||
}
|
||||
|
||||
r.logger.Info("executing template", "template", "debug/simple")
|
||||
err = tmpl.Execute(c.Response().BodyWriter(), templateData)
|
||||
if err != nil {
|
||||
r.logger.Error("failed to execute template", "error", err)
|
||||
return r.templates.ExecuteTemplate(c.Response().BodyWriter(), "list.html", templateData)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RenderIndex renders the index page
|
||||
|
||||
114
internal/template/renderer_v2.go
Normal file
114
internal/template/renderer_v2.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
type RendererV2 struct {
|
||||
loader *TemplateLoader
|
||||
config TemplateConfig
|
||||
}
|
||||
|
||||
type RenderData struct {
|
||||
Title string
|
||||
CurrentTable string
|
||||
Tables []TableInfo
|
||||
Data []map[string]interface{}
|
||||
Columns []ColumnInfo
|
||||
Total int
|
||||
Page int
|
||||
PerPage int
|
||||
Pages int
|
||||
Pagination []int
|
||||
TemplateType string
|
||||
}
|
||||
|
||||
type TableInfo struct {
|
||||
Name string
|
||||
Alias string
|
||||
}
|
||||
|
||||
type ColumnInfo struct {
|
||||
Name string
|
||||
Alias string
|
||||
RenderType string
|
||||
ShowInList bool
|
||||
Sortable bool
|
||||
Values map[string]TagValue
|
||||
}
|
||||
|
||||
type TagValue struct {
|
||||
Label string `json:"label"`
|
||||
Color string `json:"color"`
|
||||
}
|
||||
|
||||
func NewRendererV2(loader *TemplateLoader, config TemplateConfig) *RendererV2 {
|
||||
return &RendererV2{
|
||||
loader: loader,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RendererV2) RenderList(c fiber.Ctx, data RenderData) error {
|
||||
if data.TemplateType == "" {
|
||||
data.TemplateType = "list"
|
||||
}
|
||||
|
||||
templateName := fmt.Sprintf("list:%s", data.CurrentTable)
|
||||
return r.loader.Render(data, templateName, c)
|
||||
}
|
||||
|
||||
func (r *RendererV2) GetTemplatePreview(c fiber.Ctx) error {
|
||||
tableName := c.Params("table")
|
||||
templateType := c.Query("type", "list")
|
||||
|
||||
data := RenderData{
|
||||
Title: fmt.Sprintf("%s - 预览", tableName),
|
||||
CurrentTable: tableName,
|
||||
TemplateType: templateType,
|
||||
Data: []map[string]interface{}{
|
||||
{"id": 1, "title": "示例数据", "category": "示例", "created_at": "2024-01-01 12:00:00"},
|
||||
},
|
||||
Columns: []ColumnInfo{
|
||||
{Name: "id", Alias: "ID", RenderType: "raw", ShowInList: true},
|
||||
{Name: "title", Alias: "标题", RenderType: "raw", ShowInList: true},
|
||||
{Name: "category", Alias: "分类", RenderType: "category", ShowInList: true},
|
||||
{Name: "created_at", Alias: "创建时间", RenderType: "time", ShowInList: true},
|
||||
},
|
||||
Total: 1,
|
||||
Page: 1,
|
||||
PerPage: 20,
|
||||
Pages: 1,
|
||||
Tables: []TableInfo{
|
||||
{Alias: tableName},
|
||||
},
|
||||
}
|
||||
|
||||
return r.loader.Render(data, fmt.Sprintf("%s:%s", templateType, tableName), c)
|
||||
}
|
||||
|
||||
func (r *RendererV2) GetAvailableTemplates(c fiber.Ctx) error {
|
||||
tableName := c.Params("table")
|
||||
|
||||
templates, err := r.loader.GetAvailableTemplates(tableName)
|
||||
if err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": "获取模板列表失败",
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"templates": templates,
|
||||
"table": tableName,
|
||||
})
|
||||
}
|
||||
|
||||
type PaginationData struct {
|
||||
Total int
|
||||
Page int
|
||||
PerPage int
|
||||
Pages int
|
||||
}
|
||||
373
internal/template/template_loader.go
Normal file
373
internal/template/template_loader.go
Normal file
@@ -0,0 +1,373 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
type TemplateLoader struct {
|
||||
templates map[string]*template.Template
|
||||
cache map[string]*template.Template
|
||||
mu sync.RWMutex
|
||||
config TemplateConfig
|
||||
versionManager *VersionManager
|
||||
hotReload *HotReloadWatcher
|
||||
configLoader *ConfigLoader
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
type TemplateConfig struct {
|
||||
BuiltinPath string
|
||||
CustomPath string
|
||||
CacheEnabled bool
|
||||
HotReload bool
|
||||
Templates map[string]struct {
|
||||
Name string
|
||||
Type string
|
||||
Path string
|
||||
Description string
|
||||
Fields map[string]string
|
||||
Config map[string]interface{}
|
||||
}
|
||||
TemplateTypes map[string]struct {
|
||||
Layout string
|
||||
Fields map[string]string
|
||||
Options map[string]interface{}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func NewTemplateLoader(config TemplateConfig) *TemplateLoader {
|
||||
loader := &TemplateLoader{
|
||||
templates: make(map[string]*template.Template),
|
||||
cache: make(map[string]*template.Template),
|
||||
config: config,
|
||||
logger: slog.With("component", "template_loader"),
|
||||
versionManager: NewVersionManager(config.CustomPath),
|
||||
configLoader: NewConfigLoader("./config"),
|
||||
}
|
||||
|
||||
// Initialize hot reload if enabled
|
||||
if config.HotReload {
|
||||
var err error
|
||||
loader.hotReload, err = NewHotReloadWatcher(loader, config.CustomPath)
|
||||
if err != nil {
|
||||
loader.logger.Error("failed to initialize hot reload", "error", err)
|
||||
} else {
|
||||
if err := loader.hotReload.Start(); err != nil {
|
||||
loader.logger.Error("failed to start hot reload", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load configuration
|
||||
if _, err := loader.configLoader.LoadTemplateConfig(); err != nil {
|
||||
loader.logger.Warn("failed to load template config", "error", err)
|
||||
}
|
||||
|
||||
return loader
|
||||
}
|
||||
|
||||
func (tl *TemplateLoader) LoadTemplate(templateType, tableName string) (*template.Template, error) {
|
||||
cacheKey := fmt.Sprintf("%s_%s", templateType, tableName)
|
||||
|
||||
tl.mu.RLock()
|
||||
if cached, exists := tl.cache[cacheKey]; exists && tl.config.CacheEnabled {
|
||||
tl.mu.RUnlock()
|
||||
return cached, nil
|
||||
}
|
||||
tl.mu.RUnlock()
|
||||
|
||||
// 尝试加载自定义模板
|
||||
customPath := filepath.Join(tl.config.CustomPath, tableName, fmt.Sprintf("%s.html", templateType))
|
||||
if _, err := os.Stat(customPath); err == nil {
|
||||
tmpl, err := tl.parseTemplate(customPath, templateType)
|
||||
if err == nil {
|
||||
tl.cacheTemplate(cacheKey, tmpl)
|
||||
return tmpl, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 回退到默认自定义模板
|
||||
defaultPath := filepath.Join(tl.config.CustomPath, "_default", fmt.Sprintf("%s.html", templateType))
|
||||
if _, err := os.Stat(defaultPath); err == nil {
|
||||
tmpl, err := tl.parseTemplate(defaultPath, templateType)
|
||||
if err == nil {
|
||||
tl.cacheTemplate(cacheKey, tmpl)
|
||||
return tmpl, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 回退到内置模板
|
||||
builtinPath := filepath.Join(tl.config.BuiltinPath, fmt.Sprintf("%s.html", templateType))
|
||||
tmpl, err := tl.parseTemplate(builtinPath, templateType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load template %s: %w", templateType, err)
|
||||
}
|
||||
|
||||
tl.cacheTemplate(cacheKey, tmpl)
|
||||
return tmpl, nil
|
||||
}
|
||||
|
||||
func (tl *TemplateLoader) parseTemplate(path, name string) (*template.Template, error) {
|
||||
// 基础模板函数
|
||||
funcs := template.FuncMap{
|
||||
"dict": func(values ...interface{}) map[string]interface{} {
|
||||
dict := make(map[string]interface{})
|
||||
for i := 0; i < len(values); i += 2 {
|
||||
if i+1 < len(values) {
|
||||
key := fmt.Sprintf("%v", values[i])
|
||||
dict[key] = values[i+1]
|
||||
}
|
||||
}
|
||||
return dict
|
||||
},
|
||||
"add": func(a, b int) int { return a + b },
|
||||
"sub": func(a, b int) int { return a - b },
|
||||
"mul": func(a, b int) int { return a * b },
|
||||
"min": func(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
},
|
||||
"max": func(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
},
|
||||
"json": func(v interface{}) string {
|
||||
return fmt.Sprintf("%v", v)
|
||||
},
|
||||
"split": func(s string, sep string) []string {
|
||||
return strings.Split(s, sep)
|
||||
},
|
||||
"until": func(n int) []int {
|
||||
result := make([]int, n)
|
||||
for i := 0; i < n; i++ {
|
||||
result[i] = i + 1
|
||||
}
|
||||
return result
|
||||
},
|
||||
"eq": func(a, b interface{}) bool {
|
||||
return fmt.Sprintf("%v", a) == fmt.Sprintf("%v", b)
|
||||
},
|
||||
"ge": func(a, b int) bool {
|
||||
return a >= b
|
||||
},
|
||||
"gt": func(a, b int) bool {
|
||||
return a > b
|
||||
},
|
||||
"lt": func(a, b int) bool {
|
||||
return a < b
|
||||
},
|
||||
"le": func(a, b int) bool {
|
||||
return a <= b
|
||||
},
|
||||
"index": func(m interface{}, key string) interface{} {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
switch v := m.(type) {
|
||||
case map[string]interface{}:
|
||||
return v[key]
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
},
|
||||
"printf": func(format string, a ...interface{}) string {
|
||||
return fmt.Sprintf(format, a...)
|
||||
},
|
||||
}
|
||||
|
||||
// 解析布局模板
|
||||
layoutPath := filepath.Join(tl.config.BuiltinPath, "layout.html")
|
||||
if _, err := os.Stat(layoutPath); err != nil {
|
||||
return nil, fmt.Errorf("layout template not found: %w", err)
|
||||
}
|
||||
|
||||
tmpl := template.New(name).Funcs(funcs)
|
||||
|
||||
// 加载布局模板
|
||||
layoutContent, err := os.ReadFile(layoutPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read layout template: %w", err)
|
||||
}
|
||||
|
||||
tmpl, err = tmpl.Parse(string(layoutContent))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse layout template: %w", err)
|
||||
}
|
||||
|
||||
// 加载主模板
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read template %s: %w", path, err)
|
||||
}
|
||||
|
||||
tmpl, err = tmpl.Parse(string(content))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse template %s: %w", path, err)
|
||||
}
|
||||
|
||||
// 加载字段模板
|
||||
fieldTemplates, err := tl.loadFieldTemplates()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load field templates: %w", err)
|
||||
}
|
||||
|
||||
for _, fieldTpl := range fieldTemplates {
|
||||
tmpl, err = tmpl.Parse(fieldTpl)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse field template: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return tmpl, nil
|
||||
}
|
||||
|
||||
func (tl *TemplateLoader) loadFieldTemplates() ([]string, error) {
|
||||
var templates []string
|
||||
|
||||
// 加载内置字段模板
|
||||
builtinFieldPath := filepath.Join(tl.config.BuiltinPath, "field")
|
||||
if err := filepath.WalkDir(builtinFieldPath, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !d.IsDir() && strings.HasSuffix(path, ".html") {
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
templates = append(templates, string(content))
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return templates, nil
|
||||
}
|
||||
|
||||
func (tl *TemplateLoader) cacheTemplate(key string, tmpl *template.Template) {
|
||||
if !tl.config.CacheEnabled {
|
||||
return
|
||||
}
|
||||
|
||||
tl.mu.Lock()
|
||||
tl.cache[key] = tmpl
|
||||
tl.mu.Unlock()
|
||||
}
|
||||
|
||||
func (tl *TemplateLoader) ClearCache() {
|
||||
tl.mu.Lock()
|
||||
defer tl.mu.Unlock()
|
||||
|
||||
for k := range tl.cache {
|
||||
delete(tl.cache, k)
|
||||
}
|
||||
}
|
||||
|
||||
func (tl *TemplateLoader) watchTemplates() {
|
||||
if tl.config.CustomPath == "" {
|
||||
return
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
var lastModTime time.Time
|
||||
|
||||
for range ticker.C {
|
||||
info, err := os.Stat(tl.config.CustomPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if info.ModTime().After(lastModTime) {
|
||||
lastModTime = info.ModTime()
|
||||
tl.ClearCache()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 验证模板完整性
|
||||
func (tl *TemplateLoader) ValidateTemplate(templateType, tableName string) error {
|
||||
_, err := tl.LoadTemplate(templateType, tableName)
|
||||
return err
|
||||
}
|
||||
|
||||
// 获取可用模板列表
|
||||
func (tl *TemplateLoader) GetAvailableTemplates(tableName string) ([]string, error) {
|
||||
var templates []string
|
||||
|
||||
// 检查自定义模板
|
||||
customPath := filepath.Join(tl.config.CustomPath, tableName)
|
||||
if entries, err := os.ReadDir(customPath); err == nil {
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".html") {
|
||||
templates = append(templates, strings.TrimSuffix(entry.Name(), ".html"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查默认模板
|
||||
defaultPath := filepath.Join(tl.config.CustomPath, "_default")
|
||||
if entries, err := os.ReadDir(defaultPath); err == nil {
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".html") {
|
||||
templateName := strings.TrimSuffix(entry.Name(), ".html")
|
||||
if !contains(templates, templateName) {
|
||||
templates = append(templates, templateName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加内置模板
|
||||
builtinTemplates := []string{"list", "card", "timeline"}
|
||||
for _, bt := range builtinTemplates {
|
||||
if !contains(templates, bt) {
|
||||
templates = append(templates, bt)
|
||||
}
|
||||
}
|
||||
|
||||
return templates, nil
|
||||
}
|
||||
|
||||
func contains(slice []string, item string) bool {
|
||||
for _, s := range slice {
|
||||
if s == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Fiber模板引擎适配器
|
||||
func (tl *TemplateLoader) Render(data interface{}, templateName string, c fiber.Ctx) error {
|
||||
parts := strings.Split(templateName, ":")
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("invalid template format, expected 'type:table'")
|
||||
}
|
||||
|
||||
templateType, tableName := parts[0], parts[1]
|
||||
tmpl, err := tl.LoadTemplate(templateType, tableName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html; charset=utf-8")
|
||||
return tmpl.Execute(c.Response().BodyWriter(), data)
|
||||
}
|
||||
181
internal/template/validator.go
Normal file
181
internal/template/validator.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type TemplateValidator struct {
|
||||
config TemplateConfig
|
||||
}
|
||||
|
||||
type ValidationResult struct {
|
||||
Valid bool `json:"valid"`
|
||||
Errors []string `json:"errors,omitempty"`
|
||||
Warnings []string `json:"warnings,omitempty"`
|
||||
Template string `json:"template"`
|
||||
Table string `json:"table"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
func NewTemplateValidator(config TemplateConfig) *TemplateValidator {
|
||||
return &TemplateValidator{
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (tv *TemplateValidator) ValidateTemplate(templateType, tableName string) ValidationResult {
|
||||
result := ValidationResult{
|
||||
Valid: true,
|
||||
Template: templateType,
|
||||
Table: tableName,
|
||||
Type: templateType,
|
||||
}
|
||||
|
||||
// 验证模板文件存在性
|
||||
paths := tv.getTemplatePaths(templateType, tableName)
|
||||
found := false
|
||||
|
||||
for _, path := range paths {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
result.Valid = false
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("未找到模板文件: %s", templateType))
|
||||
return result
|
||||
}
|
||||
|
||||
// 验证模板语法
|
||||
loader := NewTemplateLoader(tv.config)
|
||||
_, err := loader.LoadTemplate(templateType, tableName)
|
||||
if err != nil {
|
||||
result.Valid = false
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("模板语法错误: %v", err))
|
||||
}
|
||||
|
||||
// 验证模板结构
|
||||
tv.validateTemplateStructure(&result, templateType, tableName)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (tv *TemplateValidator) ValidateAllTemplates() []ValidationResult {
|
||||
var results []ValidationResult
|
||||
|
||||
// 验证所有内置模板
|
||||
builtinTemplates := []string{"list", "card", "timeline"}
|
||||
for _, templateType := range builtinTemplates {
|
||||
result := tv.ValidateTemplate(templateType, "_default")
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
// 验证自定义目录下的模板
|
||||
if entries, err := os.ReadDir(tv.config.CustomPath); err == nil {
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() && entry.Name() != "_default" {
|
||||
tableName := entry.Name()
|
||||
for _, templateType := range builtinTemplates {
|
||||
result := tv.ValidateTemplate(templateType, tableName)
|
||||
results = append(results, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func (tv *TemplateValidator) getTemplatePaths(templateType, tableName string) []string {
|
||||
return []string{
|
||||
// 自定义模板
|
||||
filepath.Join(tv.config.CustomPath, tableName, fmt.Sprintf("%s.html", templateType)),
|
||||
// 默认自定义模板
|
||||
filepath.Join(tv.config.CustomPath, "_default", fmt.Sprintf("%s.html", templateType)),
|
||||
// 内置模板
|
||||
filepath.Join(tv.config.BuiltinPath, fmt.Sprintf("%s.html", templateType)),
|
||||
}
|
||||
}
|
||||
|
||||
func (tv *TemplateValidator) validateTemplateStructure(result *ValidationResult, templateType, tableName string) {
|
||||
// 验证必需的文件结构
|
||||
requiredFiles := []string{
|
||||
"layout.html",
|
||||
}
|
||||
|
||||
for _, file := range requiredFiles {
|
||||
path := filepath.Join(tv.config.BuiltinPath, file)
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
result.Valid = false
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("缺少必需文件: %s", file))
|
||||
}
|
||||
}
|
||||
|
||||
// 验证字段模板
|
||||
fieldPath := filepath.Join(tv.config.BuiltinPath, "field")
|
||||
if _, err := os.Stat(fieldPath); err == nil {
|
||||
requiredFieldTemplates := []string{"raw.html", "time.html", "tag.html", "markdown.html", "category.html"}
|
||||
|
||||
for _, fieldTpl := range requiredFieldTemplates {
|
||||
fieldPath := filepath.Join(fieldPath, fieldTpl)
|
||||
if _, err := os.Stat(fieldPath); err != nil {
|
||||
result.Warnings = append(result.Warnings, fmt.Sprintf("缺少字段模板: %s", fieldTpl))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 验证自定义模板目录结构
|
||||
if tableName != "_default" {
|
||||
customTablePath := filepath.Join(tv.config.CustomPath, tableName)
|
||||
if _, err := os.Stat(customTablePath); err == nil {
|
||||
// 检查是否有list或card模板
|
||||
hasList := false
|
||||
hasCard := false
|
||||
|
||||
if _, err := os.Stat(filepath.Join(customTablePath, "list.html")); err == nil {
|
||||
hasList = true
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(customTablePath, "card.html")); err == nil {
|
||||
hasCard = true
|
||||
}
|
||||
|
||||
if !hasList && !hasCard {
|
||||
result.Warnings = append(result.Warnings, "自定义模板目录存在但缺少list.html或card.html")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 模板健康检查
|
||||
func (tv *TemplateValidator) HealthCheck() map[string]interface{} {
|
||||
results := tv.ValidateAllTemplates()
|
||||
|
||||
total := len(results)
|
||||
valid := 0
|
||||
errors := 0
|
||||
warnings := 0
|
||||
|
||||
for _, result := range results {
|
||||
if result.Valid {
|
||||
valid++
|
||||
}
|
||||
if len(result.Errors) > 0 {
|
||||
errors += len(result.Errors)
|
||||
}
|
||||
if len(result.Warnings) > 0 {
|
||||
warnings += len(result.Warnings)
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"total": total,
|
||||
"valid": valid,
|
||||
"errors": errors,
|
||||
"warnings": warnings,
|
||||
"details": results,
|
||||
}
|
||||
}
|
||||
996
internal/template/version_manager.go
Normal file
996
internal/template/version_manager.go
Normal file
@@ -0,0 +1,996 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
// VersionManager manages template versioning and rollback
|
||||
type VersionManager struct {
|
||||
basePath string
|
||||
versions map[string][]VersionInfo
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
// VersionInfo holds information about a template version
|
||||
type VersionInfo struct {
|
||||
Version string `json:"version"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
FilePath string `json:"file_path"`
|
||||
Hash string `json:"hash"`
|
||||
Description string `json:"description"`
|
||||
Author string `json:"author"`
|
||||
Size int64 `json:"size"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
// VersionHistory holds the complete version history for a template
|
||||
type VersionHistory struct {
|
||||
TemplateName string `json:"template_name"`
|
||||
Versions []VersionInfo `json:"versions"`
|
||||
Current VersionInfo `json:"current"`
|
||||
}
|
||||
|
||||
// NewVersionManager creates a new version manager
|
||||
func NewVersionManager(basePath string) *VersionManager {
|
||||
return &VersionManager{
|
||||
basePath: basePath,
|
||||
versions: make(map[string][]VersionInfo),
|
||||
logger: slog.With("component", "version_manager"),
|
||||
}
|
||||
}
|
||||
|
||||
// SaveVersion saves a new version of a template
|
||||
func (vm *VersionManager) SaveVersion(templateName, templatePath, description, author string) error {
|
||||
// Create versions directory
|
||||
versionsDir := filepath.Join(vm.basePath, "versions", templateName)
|
||||
if err := os.MkdirAll(versionsDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create versions directory: %w", err)
|
||||
}
|
||||
|
||||
// Generate version info
|
||||
version := vm.generateVersion()
|
||||
versionFile := filepath.Join(versionsDir, fmt.Sprintf("%s_%s.html", templateName, version))
|
||||
|
||||
// Read current template content
|
||||
content, err := os.ReadFile(templatePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read template: %w", err)
|
||||
}
|
||||
|
||||
// Calculate hash
|
||||
hash := vm.calculateHash(content)
|
||||
|
||||
// Get file info
|
||||
info, err := os.Stat(templatePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get file info: %w", err)
|
||||
}
|
||||
|
||||
// Create version info
|
||||
versionInfo := VersionInfo{
|
||||
Version: version,
|
||||
Timestamp: time.Now(),
|
||||
FilePath: versionFile,
|
||||
Hash: hash,
|
||||
Description: description,
|
||||
Author: author,
|
||||
Size: info.Size(),
|
||||
Active: true,
|
||||
}
|
||||
|
||||
// Save template content
|
||||
if err := os.WriteFile(versionFile, content, 0644); err != nil {
|
||||
return fmt.Errorf("failed to save version: %w", err)
|
||||
}
|
||||
|
||||
// Update versions map
|
||||
if vm.versions[templateName] == nil {
|
||||
vm.versions[templateName] = []VersionInfo{}
|
||||
}
|
||||
|
||||
// Deactivate previous versions
|
||||
for i := range vm.versions[templateName] {
|
||||
vm.versions[templateName][i].Active = false
|
||||
}
|
||||
|
||||
vm.versions[templateName] = append(vm.versions[templateName], versionInfo)
|
||||
|
||||
// Persist version metadata
|
||||
if err := vm.saveVersionMetadata(templateName); err != nil {
|
||||
vm.logger.Warn("failed to save version metadata", "error", err)
|
||||
}
|
||||
|
||||
vm.logger.Info("template version saved",
|
||||
"template", templateName,
|
||||
"version", version,
|
||||
"author", author)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rollback rolls back to a specific version
|
||||
func (vm *VersionManager) Rollback(templateName, version string) error {
|
||||
versions := vm.versions[templateName]
|
||||
if len(versions) == 0 {
|
||||
return fmt.Errorf("no versions found for template: %s", templateName)
|
||||
}
|
||||
|
||||
var targetVersion *VersionInfo
|
||||
for i := range versions {
|
||||
if versions[i].Version == version {
|
||||
targetVersion = &versions[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetVersion == nil {
|
||||
return fmt.Errorf("version %s not found for template %s", version, templateName)
|
||||
}
|
||||
|
||||
// Read version content
|
||||
content, err := os.ReadFile(targetVersion.FilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read version content: %w", err)
|
||||
}
|
||||
|
||||
// Write to current template
|
||||
currentPath := filepath.Join(vm.basePath, templateName+".html")
|
||||
if err := os.WriteFile(currentPath, content, 0644); err != nil {
|
||||
return fmt.Errorf("failed to restore version: %w", err)
|
||||
}
|
||||
|
||||
// Update active state
|
||||
for i := range versions {
|
||||
versions[i].Active = versions[i].Version == version
|
||||
}
|
||||
|
||||
vm.logger.Info("template rolled back", "template", templateName, "version", version)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetVersionHistory returns the version history for a template
|
||||
func (vm *VersionManager) GetVersionHistory(templateName string) (*VersionHistory, error) {
|
||||
versions := vm.versions[templateName]
|
||||
if len(versions) == 0 {
|
||||
return nil, fmt.Errorf("no versions found for template: %s", templateName)
|
||||
}
|
||||
|
||||
var current VersionInfo
|
||||
for _, v := range versions {
|
||||
if v.Active {
|
||||
current = v
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return &VersionHistory{
|
||||
TemplateName: templateName,
|
||||
Versions: versions,
|
||||
Current: current,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListTemplates returns all templates with versions
|
||||
func (vm *VersionManager) ListTemplates() map[string][]VersionInfo {
|
||||
return vm.versions
|
||||
}
|
||||
|
||||
// DeleteVersion deletes a specific version
|
||||
func (vm *VersionManager) DeleteVersion(templateName, version string) error {
|
||||
versions := vm.versions[templateName]
|
||||
if len(versions) == 0 {
|
||||
return fmt.Errorf("no versions found for template: %s", templateName)
|
||||
}
|
||||
|
||||
var newVersions []VersionInfo
|
||||
for _, v := range versions {
|
||||
if v.Version != version {
|
||||
newVersions = append(newVersions, v)
|
||||
} else {
|
||||
// Remove file
|
||||
if err := os.Remove(v.FilePath); err != nil && !os.IsNotExist(err) {
|
||||
vm.logger.Warn("failed to delete version file", "file", v.FilePath, "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vm.versions[templateName] = newVersions
|
||||
vm.logger.Info("template version deleted", "template", templateName, "version", version)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanupOldVersions removes old versions beyond retention limit
|
||||
func (vm *VersionManager) CleanupOldVersions(templateName string, retentionCount int) error {
|
||||
versions := vm.versions[templateName]
|
||||
if len(versions) <= retentionCount {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sort by timestamp (newest first)
|
||||
sorted := make([]VersionInfo, len(versions))
|
||||
copy(sorted, versions)
|
||||
|
||||
// Keep only the latest versions
|
||||
vm.versions[templateName] = sorted[:retentionCount]
|
||||
|
||||
// Delete old files
|
||||
for _, v := range sorted[retentionCount:] {
|
||||
if err := os.Remove(v.FilePath); err != nil && !os.IsNotExist(err) {
|
||||
vm.logger.Warn("failed to delete old version file", "file", v.FilePath, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
vm.logger.Info("old versions cleaned up", "template", templateName, "kept", retentionCount, "deleted", len(sorted)-retentionCount)
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateVersion generates a unique version identifier
|
||||
func (vm *VersionManager) generateVersion() string {
|
||||
return time.Now().Format("20060102_150405")
|
||||
}
|
||||
|
||||
// calculateHash calculates a simple hash for content
|
||||
func (vm *VersionManager) calculateHash(content []byte) string {
|
||||
// Simple hash - in production, use a proper hash function
|
||||
return fmt.Sprintf("%x", len(content))
|
||||
}
|
||||
|
||||
// saveVersionMetadata saves version metadata to disk
|
||||
func (vm *VersionManager) saveVersionMetadata(templateName string) error {
|
||||
// This could be implemented to persist metadata to a JSON file
|
||||
// For now, it's kept in memory
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadVersionMetadata loads version metadata from disk
|
||||
func (vm *VersionManager) LoadVersionMetadata() error {
|
||||
// This could be implemented to load metadata from a JSON file
|
||||
// For now, it's kept in memory
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTemplateStats returns statistics for a template
|
||||
func (vm *VersionManager) GetTemplateStats(templateName string) map[string]interface{} {
|
||||
versions := vm.versions[templateName]
|
||||
stats := map[string]interface{}{
|
||||
"total_versions": len(versions),
|
||||
"current_version": "",
|
||||
"oldest_version": "",
|
||||
"newest_version": "",
|
||||
"total_size": int64(0),
|
||||
}
|
||||
|
||||
if len(versions) > 0 {
|
||||
stats["current_version"] = versions[len(versions)-1].Version
|
||||
stats["oldest_version"] = versions[0].Version
|
||||
stats["newest_version"] = versions[len(versions)-1].Version
|
||||
|
||||
var totalSize int64
|
||||
for _, v := range versions {
|
||||
totalSize += v.Size
|
||||
}
|
||||
stats["total_size"] = totalSize
|
||||
}
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
// ExportVersion exports a specific version to a file
|
||||
func (vm *VersionManager) ExportVersion(templateName, version, exportPath string) error {
|
||||
versions := vm.versions[templateName]
|
||||
|
||||
var versionInfo *VersionInfo
|
||||
for _, v := range versions {
|
||||
if v.Version == version {
|
||||
versionInfo = &v
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if versionInfo == nil {
|
||||
return fmt.Errorf("version %s not found for template %s", version, templateName)
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(versionInfo.FilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read version content: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(exportPath, content, 0644); err != nil {
|
||||
return fmt.Errorf("failed to export version: %w", err)
|
||||
}
|
||||
|
||||
vm.logger.Info("template version exported", "template", templateName, "version", version, "export_path", exportPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ImportVersion imports a template from a file
|
||||
func (vm *VersionManager) ImportVersion(templateName, importPath, description, author string) error {
|
||||
if _, err := os.Stat(importPath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("import file does not exist: %s", importPath)
|
||||
}
|
||||
|
||||
return vm.SaveVersion(templateName, importPath, description, author)
|
||||
}
|
||||
|
||||
// GetStoragePath returns the storage path for versions
|
||||
func (vm *VersionManager) GetStoragePath(templateName string) string {
|
||||
return filepath.Join(vm.basePath, "versions", templateName)
|
||||
}
|
||||
|
||||
// ValidateVersion validates a version before saving
|
||||
func (vm *VersionManager) ValidateVersion(templatePath string) error {
|
||||
if _, err := os.Stat(templatePath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("template file does not exist: %s", templatePath)
|
||||
}
|
||||
|
||||
// Add more validation as needed
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAllTemplatesWithVersions returns all templates and their versions
|
||||
func (vm *VersionManager) GetAllTemplatesWithVersions() map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
|
||||
for templateName := range vm.versions {
|
||||
stats := vm.GetTemplateStats(templateName)
|
||||
history, _ := vm.GetVersionHistory(templateName)
|
||||
|
||||
result[templateName] = map[string]interface{}{
|
||||
"stats": stats,
|
||||
"history": history,
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// BackupAllTemplates creates a backup of all templates
|
||||
func (vm *VersionManager) BackupAllTemplates(backupPath string) error {
|
||||
if err := os.MkdirAll(backupPath, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create backup directory: %w", err)
|
||||
}
|
||||
|
||||
for templateName := range vm.versions {
|
||||
versions := vm.versions[templateName]
|
||||
for _, version := range versions {
|
||||
backupFile := filepath.Join(backupPath, fmt.Sprintf("%s_%s.html", templateName, version.Version))
|
||||
if err := vm.ExportVersion(templateName, version.Version, backupFile); err != nil {
|
||||
vm.logger.Warn("failed to backup version", "template", templateName, "version", version.Version, "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vm.logger.Info("all templates backed up", "backup_path", backupPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RestoreFromBackup restores templates from a backup
|
||||
func (vm *VersionManager) RestoreFromBackup(backupPath string) error {
|
||||
return filepath.Walk(backupPath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !info.IsDir() && filepath.Ext(path) == ".html" {
|
||||
filename := info.Name()
|
||||
// Parse template name and version from filename
|
||||
// This is a simplified implementation - ignore filename
|
||||
_ = filename
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// GetDiskUsage returns disk usage statistics
|
||||
func (vm *VersionManager) GetDiskUsage() (int64, error) {
|
||||
var totalSize int64
|
||||
|
||||
versionsDir := filepath.Join(vm.basePath, "versions")
|
||||
if _, err := os.Stat(versionsDir); os.IsNotExist(err) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
err := filepath.Walk(versionsDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
totalSize += info.Size()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return totalSize, err
|
||||
}
|
||||
|
||||
// CleanupStorage removes empty directories and orphaned files
|
||||
func (vm *VersionManager) CleanupStorage() error {
|
||||
versionsDir := filepath.Join(vm.basePath, "versions")
|
||||
|
||||
return filepath.Walk(versionsDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
// Remove empty directories
|
||||
files, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(files) == 0 && path != versionsDir {
|
||||
return os.Remove(path)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// GetHealth returns the health status of the version manager
|
||||
func (vm *VersionManager) GetHealth() map[string]interface{} {
|
||||
diskUsage, _ := vm.GetDiskUsage()
|
||||
|
||||
return map[string]interface{}{
|
||||
"status": "healthy",
|
||||
"disk_usage": diskUsage,
|
||||
"total_templates": len(vm.versions),
|
||||
"storage_path": vm.GetStoragePath(""),
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateTemplateContent validates template content
|
||||
func (vm *VersionManager) ValidateTemplateContent(content []byte) error {
|
||||
// Basic validation - check for empty content
|
||||
if len(content) == 0 {
|
||||
return fmt.Errorf("template content is empty")
|
||||
}
|
||||
|
||||
// Add more validation as needed
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetVersionDiff returns differences between two versions
|
||||
func (vm *VersionManager) GetVersionDiff(templateName, version1, version2 string) (string, error) {
|
||||
// This could be implemented to show differences between versions
|
||||
// For now, return a placeholder
|
||||
return "Version diff not implemented", nil
|
||||
}
|
||||
|
||||
// SetRetentionPolicy sets the retention policy for versions
|
||||
func (vm *VersionManager) SetRetentionPolicy(templateName string, maxVersions int) error {
|
||||
return vm.CleanupOldVersions(templateName, maxVersions)
|
||||
}
|
||||
|
||||
// GetRetentionPolicy returns the retention policy for a template
|
||||
func (vm *VersionManager) GetRetentionPolicy(templateName string) int {
|
||||
// Default retention policy
|
||||
return 10
|
||||
}
|
||||
|
||||
// PurgeAllVersions removes all versions for a template
|
||||
func (vm *VersionManager) PurgeAllVersions(templateName string) error {
|
||||
storagePath := vm.GetStoragePath(templateName)
|
||||
if err := os.RemoveAll(storagePath); err != nil {
|
||||
return fmt.Errorf("failed to purge versions: %w", err)
|
||||
}
|
||||
|
||||
delete(vm.versions, templateName)
|
||||
vm.logger.Info("all versions purged", "template", templateName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloneTemplate creates a copy of a template with a new name
|
||||
func (vm *VersionManager) CloneTemplate(sourceTemplate, newTemplate, description, author string) error {
|
||||
versions := vm.versions[sourceTemplate]
|
||||
if len(versions) == 0 {
|
||||
return fmt.Errorf("source template not found: %s", sourceTemplate)
|
||||
}
|
||||
|
||||
// Get the latest version
|
||||
latestVersion := versions[len(versions)-1]
|
||||
|
||||
// Read the latest version content
|
||||
content, err := os.ReadFile(latestVersion.FilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read source template: %w", err)
|
||||
}
|
||||
|
||||
// Save as new template
|
||||
newTemplatePath := filepath.Join(vm.basePath, newTemplate+".html")
|
||||
if err := os.WriteFile(newTemplatePath, content, 0644); err != nil {
|
||||
return fmt.Errorf("failed to create new template: %w", err)
|
||||
}
|
||||
|
||||
return vm.SaveVersion(newTemplate, newTemplatePath, description, author)
|
||||
}
|
||||
|
||||
// GetTemplateUsage returns usage statistics for templates
|
||||
func (vm *VersionManager) GetTemplateUsage() map[string]interface{} {
|
||||
usage := make(map[string]interface{})
|
||||
|
||||
for templateName := range vm.versions {
|
||||
stats := vm.GetTemplateStats(templateName)
|
||||
usage[templateName] = stats
|
||||
}
|
||||
|
||||
return usage
|
||||
}
|
||||
|
||||
// OptimizeStorage optimizes storage by compressing old versions
|
||||
func (vm *VersionManager) OptimizeStorage() error {
|
||||
// This could be implemented to compress old versions
|
||||
// For now, just log the operation
|
||||
vm.logger.Info("storage optimization started")
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTemplateInfo returns detailed information about a template
|
||||
func (vm *VersionManager) GetTemplateInfo(templateName string) (map[string]interface{}, error) {
|
||||
versions := vm.versions[templateName]
|
||||
if len(versions) == 0 {
|
||||
return nil, fmt.Errorf("template not found: %s", templateName)
|
||||
}
|
||||
|
||||
history, _ := vm.GetVersionHistory(templateName)
|
||||
stats := vm.GetTemplateStats(templateName)
|
||||
diskUsage, _ := vm.GetDiskUsage()
|
||||
|
||||
return map[string]interface{}{
|
||||
"name": templateName,
|
||||
"stats": stats,
|
||||
"history": history,
|
||||
"disk_usage": diskUsage,
|
||||
"storage_path": vm.GetStoragePath(templateName),
|
||||
"retention": vm.GetRetentionPolicy(templateName),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ExportTemplateMetadata exports template metadata to a file
|
||||
func (vm *VersionManager) ExportTemplateMetadata(exportPath string) error {
|
||||
// This could be implemented to export metadata
|
||||
// For now, just log the operation
|
||||
vm.logger.Info("template metadata export started", "export_path", exportPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ImportTemplateMetadata imports template metadata from a file
|
||||
func (vm *VersionManager) ImportTemplateMetadata(importPath string) error {
|
||||
// This could be implemented to import metadata
|
||||
// For now, just log the operation
|
||||
vm.logger.Info("template metadata import started", "import_path", importPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateTemplateName validates a template name
|
||||
func (vm *VersionManager) ValidateTemplateName(name string) error {
|
||||
if name == "" {
|
||||
return fmt.Errorf("template name cannot be empty")
|
||||
}
|
||||
|
||||
if len(name) > 50 {
|
||||
return fmt.Errorf("template name too long")
|
||||
}
|
||||
|
||||
// Check for invalid characters
|
||||
invalidChars := []string{"/", "\\", ":", "*", "?", "\"", "<", ">", "|"}
|
||||
for _, char := range invalidChars {
|
||||
if containsString(name, char) {
|
||||
return fmt.Errorf("template name contains invalid character: %s", char)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper function to check if string contains substring
|
||||
func containsString(s, substr string) bool {
|
||||
for _, char := range substr {
|
||||
for _, c := range s {
|
||||
if c == char {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetAllVersions returns all versions across all templates
|
||||
func (vm *VersionManager) GetAllVersions() map[string]interface{} {
|
||||
allVersions := make(map[string]interface{})
|
||||
|
||||
for templateName := range vm.versions {
|
||||
allVersions[templateName] = vm.versions[templateName]
|
||||
}
|
||||
|
||||
return allVersions
|
||||
}
|
||||
|
||||
// SearchVersions searches for versions matching criteria
|
||||
func (vm *VersionManager) SearchVersions(criteria map[string]interface{}) []VersionInfo {
|
||||
var results []VersionInfo
|
||||
|
||||
for templateName := range vm.versions {
|
||||
versions := vm.versions[templateName]
|
||||
for _, version := range versions {
|
||||
if vm.matchesCriteria(version, criteria) {
|
||||
results = append(results, version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// matchesCriteria checks if a version matches search criteria
|
||||
func (vm *VersionManager) matchesCriteria(version VersionInfo, criteria map[string]interface{}) bool {
|
||||
for key, value := range criteria {
|
||||
switch key {
|
||||
case "author":
|
||||
if version.Author != value.(string) {
|
||||
return false
|
||||
}
|
||||
case "before":
|
||||
if !version.Timestamp.Before(value.(time.Time)) {
|
||||
return false
|
||||
}
|
||||
case "after":
|
||||
if !version.Timestamp.After(value.(time.Time)) {
|
||||
return false
|
||||
}
|
||||
case "description_contains":
|
||||
if !containsString(version.Description, value.(string)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// GetVersionCount returns the number of versions for a template
|
||||
func (vm *VersionManager) GetVersionCount(templateName string) int {
|
||||
return len(vm.versions[templateName])
|
||||
}
|
||||
|
||||
// GetOldestVersion returns the oldest version for a template
|
||||
func (vm *VersionManager) GetOldestVersion(templateName string) (*VersionInfo, error) {
|
||||
versions := vm.versions[templateName]
|
||||
if len(versions) == 0 {
|
||||
return nil, fmt.Errorf("no versions found for template: %s", templateName)
|
||||
}
|
||||
return &versions[0], nil
|
||||
}
|
||||
|
||||
// GetNewestVersion returns the newest version for a template
|
||||
func (vm *VersionManager) GetNewestVersion(templateName string) (*VersionInfo, error) {
|
||||
versions := vm.versions[templateName]
|
||||
if len(versions) == 0 {
|
||||
return nil, fmt.Errorf("no versions found for template: %s", templateName)
|
||||
}
|
||||
return &versions[len(versions)-1], nil
|
||||
}
|
||||
|
||||
// GetActiveVersion returns the active version for a template
|
||||
func (vm *VersionManager) GetActiveVersion(templateName string) (*VersionInfo, error) {
|
||||
versions := vm.versions[templateName]
|
||||
for _, v := range versions {
|
||||
if v.Active {
|
||||
return &v, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no active version found for template: %s", templateName)
|
||||
}
|
||||
|
||||
// SetActiveVersion sets a specific version as active
|
||||
func (vm *VersionManager) SetActiveVersion(templateName, version string) error {
|
||||
versions := vm.versions[templateName]
|
||||
for i := range versions {
|
||||
versions[i].Active = versions[i].Version == version
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetVersionByHash returns a version by its hash
|
||||
func (vm *VersionManager) GetVersionByHash(templateName, hash string) (*VersionInfo, error) {
|
||||
versions := vm.versions[templateName]
|
||||
for _, v := range versions {
|
||||
if v.Hash == hash {
|
||||
return &v, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("version with hash %s not found for template %s", hash, templateName)
|
||||
}
|
||||
|
||||
// GetVersionsByDateRange returns versions within a date range
|
||||
func (vm *VersionManager) GetVersionsByDateRange(templateName string, start, end time.Time) []VersionInfo {
|
||||
versions := vm.versions[templateName]
|
||||
var results []VersionInfo
|
||||
|
||||
for _, version := range versions {
|
||||
if version.Timestamp.After(start) && version.Timestamp.Before(end) {
|
||||
results = append(results, version)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
|
||||
// GetTemplateUsageStats returns usage statistics for templates
|
||||
func (vm *VersionManager) GetTemplateUsageStats() map[string]interface{} {
|
||||
stats := make(map[string]interface{})
|
||||
|
||||
totalVersions := 0
|
||||
totalSize := int64(0)
|
||||
|
||||
for templateName, versions := range vm.versions {
|
||||
for _, v := range versions {
|
||||
totalVersions++
|
||||
totalSize += v.Size
|
||||
}
|
||||
|
||||
stats[templateName] = map[string]interface{}{
|
||||
"versions": len(versions),
|
||||
"size": totalSize,
|
||||
}
|
||||
}
|
||||
|
||||
stats["total_templates"] = len(vm.versions)
|
||||
stats["total_versions"] = totalVersions
|
||||
stats["total_size"] = totalSize
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
// ArchiveTemplate archives a template and all its versions
|
||||
func (vm *VersionManager) ArchiveTemplate(templateName string) error {
|
||||
// This could be implemented to archive templates
|
||||
// For now, just log the operation
|
||||
vm.logger.Info("template archived", "template", templateName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RestoreTemplate restores an archived template
|
||||
func (vm *VersionManager) RestoreTemplate(templateName string) error {
|
||||
// This could be implemented to restore templates
|
||||
// For now, just log the operation
|
||||
vm.logger.Info("template restored", "template", templateName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTemplateBackup creates a backup of all template versions
|
||||
func (vm *VersionManager) GetTemplateBackup() (*VersionHistory, error) {
|
||||
// This is a placeholder for backup functionality
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// SetVersionLabel sets a label for a specific version
|
||||
func (vm *VersionManager) SetVersionLabel(templateName, version, label string) error {
|
||||
// This could be implemented to add labels to versions
|
||||
vm.logger.Info("version label set", "template", templateName, "version", version, "label", label)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetVersionLabel returns the label for a specific version
|
||||
func (vm *VersionManager) GetVersionLabel(templateName, version string) string {
|
||||
// This could be implemented to get labels from versions
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetVersionTags returns tags for a specific version
|
||||
func (vm *VersionManager) GetVersionTags(templateName, version string) []string {
|
||||
// This could be implemented to get tags from versions
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// AddVersionTag adds a tag to a specific version
|
||||
func (vm *VersionManager) AddVersionTag(templateName, version, tag string) error {
|
||||
// This could be implemented to add tags to versions
|
||||
vm.logger.Info("version tag added", "template", templateName, "version", version, "tag", tag)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveVersionTag removes a tag from a specific version
|
||||
func (vm *VersionManager) RemoveVersionTag(templateName, version, tag string) error {
|
||||
// This could be implemented to remove tags from versions
|
||||
vm.logger.Info("version tag removed", "template", templateName, "version", version, "tag", tag)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetVersionComments returns comments for a specific version
|
||||
func (vm *VersionManager) GetVersionComments(templateName, version string) []string {
|
||||
// This could be implemented to get comments from versions
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// AddVersionComment adds a comment to a specific version
|
||||
func (vm *VersionManager) AddVersionComment(templateName, version, comment string) error {
|
||||
// This could be implemented to add comments to versions
|
||||
vm.logger.Info("version comment added", "template", templateName, "version", version, "comment", comment)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveVersionComment removes a comment from a specific version
|
||||
func (vm *VersionManager) RemoveVersionComment(templateName, version, comment string) error {
|
||||
// This could be implemented to remove comments from versions
|
||||
vm.logger.Info("version comment removed", "template", templateName, "version", version, "comment", comment)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetVersionAuthor returns the author of a specific version
|
||||
func (vm *VersionManager) GetVersionAuthor(templateName, version string) string {
|
||||
versions := vm.versions[templateName]
|
||||
for _, v := range versions {
|
||||
if v.Version == version {
|
||||
return v.Author
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetVersionDescription returns the description of a specific version
|
||||
func (vm *VersionManager) GetVersionDescription(templateName, version string) string {
|
||||
versions := vm.versions[templateName]
|
||||
for _, v := range versions {
|
||||
if v.Version == version {
|
||||
return v.Description
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetVersionTimestamp returns the timestamp of a specific version
|
||||
func (vm *VersionManager) GetVersionTimestamp(templateName, version string) (time.Time, error) {
|
||||
versions := vm.versions[templateName]
|
||||
for _, v := range versions {
|
||||
if v.Version == version {
|
||||
return v.Timestamp, nil
|
||||
}
|
||||
}
|
||||
return time.Time{}, fmt.Errorf("version %s not found for template %s", version, templateName)
|
||||
}
|
||||
|
||||
// GetVersionFilePath returns the file path of a specific version
|
||||
func (vm *VersionManager) GetVersionFilePath(templateName, version string) (string, error) {
|
||||
versions := vm.versions[templateName]
|
||||
for _, v := range versions {
|
||||
if v.Version == version {
|
||||
return v.FilePath, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("version %s not found for template %s", version, templateName)
|
||||
}
|
||||
|
||||
// GetVersionSize returns the size of a specific version
|
||||
func (vm *VersionManager) GetVersionSize(templateName, version string) (int64, error) {
|
||||
versions := vm.versions[templateName]
|
||||
for _, v := range versions {
|
||||
if v.Version == version {
|
||||
return v.Size, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("version %s not found for template %s", version, templateName)
|
||||
}
|
||||
|
||||
// GetVersionHash returns the hash of a specific version
|
||||
func (vm *VersionManager) GetVersionHash(templateName, version string) (string, error) {
|
||||
versions := vm.versions[templateName]
|
||||
for _, v := range versions {
|
||||
if v.Version == version {
|
||||
return v.Hash, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("version %s not found for template %s", version, templateName)
|
||||
}
|
||||
|
||||
// IsVersionActive returns whether a specific version is active
|
||||
func (vm *VersionManager) IsVersionActive(templateName, version string) (bool, error) {
|
||||
versions := vm.versions[templateName]
|
||||
for _, v := range versions {
|
||||
if v.Version == version {
|
||||
return v.Active, nil
|
||||
}
|
||||
}
|
||||
return false, fmt.Errorf("version %s not found for template %s", version, templateName)
|
||||
}
|
||||
|
||||
|
||||
// GetTotalDiskUsage returns the total disk usage across all templates
|
||||
func (vm *VersionManager) GetTotalDiskUsage() int64 {
|
||||
total := int64(0)
|
||||
for _, versions := range vm.versions {
|
||||
for _, v := range versions {
|
||||
total += v.Size
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// GetVersionSummary returns a summary of all versions
|
||||
func (vm *VersionManager) GetVersionSummary() map[string]interface{} {
|
||||
summary := map[string]interface{}{
|
||||
"total_templates": len(vm.versions),
|
||||
"total_versions": vm.getTotalVersionCount(),
|
||||
"total_size": vm.GetTotalDiskUsage(),
|
||||
"templates": make(map[string]int),
|
||||
}
|
||||
|
||||
for templateName, versions := range vm.versions {
|
||||
summary["templates"].(map[string]int)[templateName] = len(versions)
|
||||
}
|
||||
|
||||
return summary
|
||||
}
|
||||
|
||||
// getTotalVersionCount returns the total number of versions across all templates
|
||||
func (vm *VersionManager) getTotalVersionCount() int {
|
||||
count := 0
|
||||
for _, versions := range vm.versions {
|
||||
count += len(versions)
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// GetVersionHistorySummary returns a summary of version history
|
||||
func (vm *VersionManager) GetVersionHistorySummary(templateName string) map[string]interface{} {
|
||||
versions := vm.versions[templateName]
|
||||
if len(versions) == 0 {
|
||||
return map[string]interface{}{
|
||||
"template_name": templateName,
|
||||
"total_versions": 0,
|
||||
"current_version": "",
|
||||
"oldest_version": "",
|
||||
"newest_version": "",
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"template_name": templateName,
|
||||
"total_versions": len(versions),
|
||||
"current_version": versions[len(versions)-1].Version,
|
||||
"oldest_version": versions[0].Version,
|
||||
"newest_version": versions[len(versions)-1].Version,
|
||||
"first_created": versions[0].Timestamp,
|
||||
"last_updated": versions[len(versions)-1].Timestamp,
|
||||
"total_size": func() int64 {
|
||||
total := int64(0)
|
||||
for _, v := range versions {
|
||||
total += v.Size
|
||||
}
|
||||
return total
|
||||
}(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetTemplateVersionHistory returns the complete version history for a template
|
||||
func (vm *VersionManager) GetTemplateVersionHistory(templateName string) (*VersionHistory, error) {
|
||||
versions := vm.versions[templateName]
|
||||
if len(versions) == 0 {
|
||||
return nil, fmt.Errorf("no versions found for template: %s", templateName)
|
||||
}
|
||||
|
||||
var current VersionInfo
|
||||
for _, v := range versions {
|
||||
if v.Active {
|
||||
current = v
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If no active version, use the latest
|
||||
if current.Version == "" {
|
||||
current = versions[len(versions)-1]
|
||||
}
|
||||
|
||||
return &VersionHistory{
|
||||
TemplateName: templateName,
|
||||
Versions: versions,
|
||||
Current: current,
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user