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 }