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 }