tagliatelle

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

admin.go (8633B)


      1 package app
      2 
      3 import (
      4 	"fmt"
      5 	"io"
      6 	"log"
      7 	"net/http"
      8 	"os"
      9 	"path/filepath"
     10 	"strconv"
     11 	"strings"
     12 	"time"
     13 )
     14 
     15 type AdminPageData struct {
     16 	Config            Config
     17 	Error             string
     18 	Success           string
     19 	OrphanData        OrphanData
     20 	ActiveTab         string
     21 	MissingThumbnails []VideoFile
     22 	RecentFiles       []File
     23 }
     24 
     25 type DBFile struct {
     26 	ID       int
     27 	Filename string
     28 }
     29 
     30 type SedRule struct {
     31 	Name        string `json:"name"`
     32 	Description string `json:"description"`
     33 	Command     string `json:"command"`
     34 }
     35 
     36 func renderAdminPage(w http.ResponseWriter, r *http.Request, data AdminPageData) {
     37 	if data.ActiveTab == "" {
     38 		data.ActiveTab = r.FormValue("active_tab")
     39 	}
     40 	pageData := buildPageData("Admin", data)
     41 	renderTemplate(w, "admin.html", pageData)
     42 }
     43 
     44 func getRecentFiles() []File {
     45 	rows, err := DB.Query("SELECT id, filename FROM files ORDER BY id DESC LIMIT 20")
     46 	if err != nil {
     47 		log.Printf("Warning: getRecentFiles: failed to query recent files: %v", err)
     48 		return nil
     49 	}
     50 	defer rows.Close()
     51 	var files []File
     52 	for rows.Next() {
     53 		var f File
     54 		rows.Scan(&f.ID, &f.Filename)
     55 		files = append(files, f)
     56 	}
     57 	return files
     58 }
     59 
     60 func currentAdminState(r *http.Request, orphanData OrphanData, missingThumbnails []VideoFile) AdminPageData {
     61 	return AdminPageData{
     62 		Config:            Cfg,
     63 		OrphanData:        orphanData,
     64 		ActiveTab:         r.FormValue("active_tab"),
     65 		MissingThumbnails: missingThumbnails,
     66 		RecentFiles:       getRecentFiles(),
     67 	}
     68 }
     69 
     70 func validateConfig(newConfig Config) error {
     71 	if newConfig.GallerySize == "" {
     72 		return fmt.Errorf("gallery size cannot be empty")
     73 	}
     74 	if newConfig.ItemsPerPage == "" {
     75 		return fmt.Errorf("items per page cannot be empty")
     76 	}
     77 	return nil
     78 }
     79 
     80 func adminHandler(w http.ResponseWriter, r *http.Request) {
     81 	// Get orphaned files
     82 	orphanData, err := getOrphanedFiles(Cfg.UploadDir)
     83 	if err != nil {
     84 		log.Printf("Warning: adminHandler: failed to get orphaned files: %v", err)
     85 	}
     86 
     87 	// Get video files for thumbnails
     88 	missingThumbnails, err := getMissingThumbnailVideos()
     89 	if err != nil {
     90 		log.Printf("Warning: adminHandler: failed to get missing thumbnails: %v", err)
     91 	}
     92 
     93 	switch r.Method {
     94 	case http.MethodPost:
     95 		switch r.FormValue("action") {
     96 		case "save", "":
     97 			handleSaveSettings(w, r, orphanData, missingThumbnails)
     98 
     99 		case "backup":
    100 			err := backupDatabase(Cfg.DatabasePath)
    101 			if err != nil {
    102 				log.Printf("Error: adminHandler: database backup failed: %v", err)
    103 			}
    104 			data := currentAdminState(r, orphanData, missingThumbnails)
    105 			data.Error = errorString(err)
    106 			data.Success = successString(err, "Database backup created successfully!")
    107 			renderAdminPage(w, r, data)
    108 
    109 		case "vacuum":
    110 			err := vacuumDatabase()
    111 			if err != nil {
    112 				log.Printf("Error: adminHandler: database vacuum failed: %v", err)
    113 			}
    114 			data := currentAdminState(r, orphanData, missingThumbnails)
    115 			data.Error = errorString(err)
    116 			data.Success = successString(err, "Database vacuum completed successfully!")
    117 			renderAdminPage(w, r, data)
    118 
    119 		case "save_aliases":
    120 			handleSaveAliases(w, r, orphanData, missingThumbnails)
    121 
    122 		case "save_sed_rules":
    123 			handleSaveSedRules(w, r, orphanData, missingThumbnails)
    124 
    125 		case "compute_properties":
    126 			handleComputeProperties(w, r, orphanData, missingThumbnails)
    127 		}
    128 
    129 	default:
    130 		renderAdminPage(w, r, currentAdminState(r, orphanData, missingThumbnails))
    131 	}
    132 }
    133 
    134 func parseAliasesFromForm(r *http.Request) []TagAliasGroup {
    135 	var groups []TagAliasGroup
    136 	for i := 0; ; i++ {
    137 		category := strings.TrimSpace(r.FormValue(fmt.Sprintf("aliases[%d][category]", i)))
    138 		if category == "" {
    139 			break
    140 		}
    141 		var aliases []string
    142 		for j := 0; ; j++ {
    143 			v := strings.TrimSpace(r.FormValue(fmt.Sprintf("aliases[%d][aliases][%d]", i, j)))
    144 			if v == "" {
    145 				break
    146 			}
    147 			aliases = append(aliases, v)
    148 		}
    149 		if len(aliases) >= 2 {
    150 			groups = append(groups, TagAliasGroup{Category: category, Aliases: aliases})
    151 		} else {
    152 			log.Printf("Warning: parseAliasesFromForm: alias group for category %q has fewer than 2 aliases (%d) and was discarded", category, len(aliases))
    153 		}
    154 	}
    155 	return groups
    156 }
    157 
    158 func parseSedRulesFromForm(r *http.Request) ([]SedRule, error) {
    159 	var rules []SedRule
    160 	for i := 0; ; i++ {
    161 		name := strings.TrimSpace(r.FormValue(fmt.Sprintf("sed_rules[%d][name]", i)))
    162 		if name == "" {
    163 			break
    164 		}
    165 		command := strings.TrimSpace(r.FormValue(fmt.Sprintf("sed_rules[%d][command]", i)))
    166 		if command == "" {
    167 			return nil, fmt.Errorf("rule %s is missing a command", strconv.Itoa(i+1))
    168 		}
    169 		rules = append(rules, SedRule{
    170 			Name:        name,
    171 			Description: strings.TrimSpace(r.FormValue(fmt.Sprintf("sed_rules[%d][description]", i))),
    172 			Command:     command,
    173 		})
    174 	}
    175 	return rules, nil
    176 }
    177 
    178 func handleSaveAliases(w http.ResponseWriter, r *http.Request, orphanData OrphanData, missingThumbnails []VideoFile) {
    179 	Cfg.TagAliases = parseAliasesFromForm(r)
    180 
    181 	if err := SaveConfig(DB, Cfg); err != nil {
    182 		log.Printf("Error: handleSaveAliases: failed to save configuration: %v", err)
    183 		data := currentAdminState(r, orphanData, missingThumbnails)
    184 		data.Error = "Failed to save configuration: " + err.Error()
    185 		renderAdminPage(w, r, data)
    186 		return
    187 	}
    188 
    189 	data := currentAdminState(r, orphanData, missingThumbnails)
    190 	data.Success = "Tag aliases saved successfully!"
    191 	renderAdminPage(w, r, data)
    192 }
    193 
    194 func handleSaveSettings(w http.ResponseWriter, r *http.Request, orphanData OrphanData, missingThumbnails []VideoFile) {
    195 	newConfig := Cfg // preserve runtime fields
    196 	newConfig.GallerySize = strings.TrimSpace(r.FormValue("gallery_size"))
    197 	newConfig.ItemsPerPage = strings.TrimSpace(r.FormValue("items_per_page"))
    198 
    199 	if err := validateConfig(newConfig); err != nil {
    200 		data := currentAdminState(r, orphanData, missingThumbnails)
    201 		data.Error = err.Error()
    202 		renderAdminPage(w, r, data)
    203 		return
    204 	}
    205 
    206 	Cfg = newConfig
    207 
    208 	if err := SaveConfig(DB, Cfg); err != nil {
    209 		log.Printf("Error: handleSaveSettings: failed to save configuration: %v", err)
    210 		data := currentAdminState(r, orphanData, missingThumbnails)
    211 		data.Error = "Failed to save configuration: " + err.Error()
    212 		renderAdminPage(w, r, data)
    213 		return
    214 	}
    215 
    216 	data := currentAdminState(r, orphanData, missingThumbnails)
    217 	data.Success = "Settings saved successfully!"
    218 	renderAdminPage(w, r, data)
    219 }
    220 
    221 func handleSaveSedRules(w http.ResponseWriter, r *http.Request, orphanData OrphanData, missingThumbnails []VideoFile) {
    222 	rules, err := parseSedRulesFromForm(r)
    223 	if err != nil {
    224 		data := currentAdminState(r, orphanData, missingThumbnails)
    225 		data.Error = err.Error()
    226 		renderAdminPage(w, r, data)
    227 		return
    228 	}
    229 
    230 	Cfg.SedRules = rules
    231 
    232 	if err := SaveConfig(DB, Cfg); err != nil {
    233 		log.Printf("Error: handleSaveSedRules: failed to save configuration: %v", err)
    234 		data := currentAdminState(r, orphanData, missingThumbnails)
    235 		data.Error = "Failed to save configuration: " + err.Error()
    236 		renderAdminPage(w, r, data)
    237 		return
    238 	}
    239 
    240 	data := currentAdminState(r, orphanData, missingThumbnails)
    241 	data.Success = "Sed rules saved successfully!"
    242 	renderAdminPage(w, r, data)
    243 }
    244 
    245 func backupDatabase(dbPath string) error {
    246 	if dbPath == "" {
    247 		return fmt.Errorf("database path not configured")
    248 	}
    249 
    250 	backupDir := filepath.Join(filepath.Dir(dbPath), "backups")
    251 	if err := os.MkdirAll(backupDir, 0755); err != nil {
    252 		return fmt.Errorf("failed to create backups directory: %w", err)
    253 	}
    254 
    255 	dbName := strings.TrimSuffix(filepath.Base(dbPath), filepath.Ext(dbPath))
    256 	timestamp := time.Now().Format("20060102_150405")
    257 	backupPath := filepath.Join(backupDir, fmt.Sprintf("%s_backup_%s.DB", dbName, timestamp))
    258 
    259 	input, err := os.Open(dbPath)
    260 	if err != nil {
    261 		return fmt.Errorf("failed to open database: %w", err)
    262 	}
    263 	defer input.Close()
    264 
    265 	output, err := os.Create(backupPath)
    266 	if err != nil {
    267 		return fmt.Errorf("failed to create backup file: %w", err)
    268 	}
    269 	defer output.Close()
    270 
    271 	if _, err := io.Copy(output, input); err != nil {
    272 		return fmt.Errorf("failed to copy database: %w", err)
    273 	}
    274 
    275 	log.Printf("Info: backupDatabase: backup written to %s", backupPath)
    276 	return nil
    277 }
    278 
    279 func vacuumDatabase() error {
    280 	if _, err := DB.Exec("VACUUM;"); err != nil {
    281 		return fmt.Errorf("VACUUM failed: %w", err)
    282 	}
    283 
    284 	log.Printf("Info: vacuumDatabase: VACUUM completed successfully")
    285 	return nil
    286 }
    287 
    288 func getFilesInDB() (map[string]DBFile, error) {
    289 	rows, err := DB.Query("SELECT id, filename FROM files")
    290 	if err != nil {
    291 		return nil, err
    292 	}
    293 	defer rows.Close()
    294 
    295 	fileMap := make(map[string]DBFile)
    296 	for rows.Next() {
    297 		var file DBFile
    298 
    299 		if err := rows.Scan(&file.ID, &file.Filename); err != nil {
    300 			return nil, err
    301 		}
    302 
    303 		fileMap[file.Filename] = file
    304 	}
    305 	if err := rows.Err(); err != nil {
    306 		return nil, err
    307 	}
    308 	return fileMap, nil
    309 }