tagliatelle

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

include-admin.go (8571B)


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