include-viewer.go (11709B)
1 package main 2 3 import ( 4 "database/sql" 5 "fmt" 6 "html" 7 "log" 8 "net" 9 "net/http" 10 "net/url" 11 "strconv" 12 "strings" 13 ) 14 15 func getPreviousTagValue(category string, excludeFileID int) (string, error) { 16 var value string 17 err := db.QueryRow(` 18 SELECT t.value 19 FROM tags t 20 JOIN categories c ON c.id = t.category_id 21 JOIN file_tags ft ON ft.tag_id = t.id 22 JOIN files f ON f.id = ft.file_id 23 WHERE c.name = ? AND ft.file_id != ? 24 ORDER BY ft.rowid DESC 25 LIMIT 1 26 `, category, excludeFileID).Scan(&value) 27 28 if err == sql.ErrNoRows { 29 return "", fmt.Errorf("no previous tag found for category: %s", category) 30 } 31 if err != nil { 32 return "", err 33 } 34 35 return value, nil 36 } 37 38 func getPreviousFileTags(excludeFileID int) ([]struct{ cat, val string }, error) { 39 rows, err := db.Query(` 40 SELECT c.name, t.value 41 FROM tags t 42 JOIN categories c ON c.id = t.category_id 43 JOIN file_tags ft ON ft.tag_id = t.id 44 WHERE ft.file_id = ( 45 SELECT file_id FROM file_tags 46 WHERE file_id != ? 47 ORDER BY rowid DESC 48 LIMIT 1 49 ) 50 `, excludeFileID) 51 if err != nil { 52 return nil, err 53 } 54 defer rows.Close() 55 56 var tags []struct{ cat, val string } 57 for rows.Next() { 58 var cat, val string 59 if err := rows.Scan(&cat, &val); err != nil { 60 return nil, err 61 } 62 tags = append(tags, struct{ cat, val string }{cat, val}) 63 } 64 if len(tags) == 0 { 65 return nil, fmt.Errorf("no tags found on previous file") 66 } 67 return tags, nil 68 } 69 70 func getFileTagsByID(fileID int) ([]struct{ cat, val string }, error) { 71 rows, err := db.Query(` 72 SELECT c.name, t.value 73 FROM tags t 74 JOIN categories c ON c.id = t.category_id 75 JOIN file_tags ft ON ft.tag_id = t.id 76 WHERE ft.file_id = ? 77 `, fileID) 78 if err != nil { 79 return nil, err 80 } 81 defer rows.Close() 82 83 var tags []struct{ cat, val string } 84 for rows.Next() { 85 var cat, val string 86 if err := rows.Scan(&cat, &val); err != nil { 87 return nil, err 88 } 89 tags = append(tags, struct{ cat, val string }{cat, val}) 90 } 91 if len(tags) == 0 { 92 return nil, fmt.Errorf("no tags found on file id %d", fileID) 93 } 94 return tags, nil 95 } 96 97 func fileHandler(w http.ResponseWriter, r *http.Request) { 98 idStr := strings.TrimPrefix(r.URL.Path, "/file/") 99 if strings.Contains(idStr, "/") { 100 idStr = strings.SplitN(idStr, "/", 2)[0] 101 } 102 103 var f File 104 err := db.QueryRow("SELECT id, filename, path, COALESCE(description, '') as description FROM files WHERE id=?", idStr).Scan(&f.ID, &f.Filename, &f.Path, &f.Description) 105 if err != nil { 106 log.Printf("Error: fileHandler: file not found for id=%s: %v", idStr, err) 107 renderError(w, "File not found", http.StatusNotFound) 108 return 109 } 110 111 f.Tags = make(map[string][]string) 112 rows, err := db.Query(` 113 SELECT c.name, t.value 114 FROM tags t 115 JOIN categories c ON c.id = t.category_id 116 JOIN file_tags ft ON ft.tag_id = t.id 117 WHERE ft.file_id=?`, f.ID) 118 if err != nil { 119 log.Printf("Warning: fileHandler: failed to query tags for file id=%d: %v", f.ID, err) 120 } else { 121 for rows.Next() { 122 var cat, val string 123 if err := rows.Scan(&cat, &val); err != nil { 124 log.Printf("Warning: fileHandler: failed to scan tag row for file id=%d: %v", f.ID, err) 125 continue 126 } 127 f.Tags[cat] = append(f.Tags[cat], val) 128 } 129 rows.Close() 130 } 131 132 if r.Method == http.MethodPost { 133 if r.FormValue("action") == "update_description" { 134 description := r.FormValue("description") 135 if len(description) > 2048 { 136 description = description[:2048] 137 } 138 139 if _, err := db.Exec("UPDATE files SET description = ? WHERE id = ?", description, f.ID); err != nil { 140 log.Printf("Error: fileHandler: failed to update description for file id=%d: %v", f.ID, err) 141 renderError(w, "Failed to update description", http.StatusInternalServerError) 142 return 143 } 144 http.Redirect(w, r, "/file/"+idStr, http.StatusSeeOther) 145 return 146 } 147 148 cat := strings.TrimSpace(r.FormValue("category")) 149 val := strings.TrimSpace(r.FormValue("value")) 150 151 if cat == "!" || (strings.HasPrefix(cat, "!") && len(cat) > 1) { 152 var sourceTags []struct{ cat, val string } 153 var sourceDesc string 154 155 if cat == "!" { 156 var err error 157 sourceTags, err = getPreviousFileTags(f.ID) 158 if err != nil { 159 http.Redirect(w, r, "/file/"+idStr+"?error="+url.QueryEscape("Could not copy tags from previous file: "+err.Error()), http.StatusSeeOther) 160 return 161 } 162 sourceDesc = "previous file" 163 } else { 164 sourceID, err := strconv.Atoi(cat[1:]) 165 if err != nil { 166 http.Redirect(w, r, "/file/"+idStr+"?error="+url.QueryEscape("Invalid file ID in category: "+cat), http.StatusSeeOther) 167 return 168 } 169 sourceTags, err = getFileTagsByID(sourceID) 170 if err != nil { 171 http.Redirect(w, r, "/file/"+idStr+"?error="+url.QueryEscape(fmt.Sprintf("Could not copy tags from file %d: %s", sourceID, err.Error())), http.StatusSeeOther) 172 return 173 } 174 sourceDesc = fmt.Sprintf("file %d", sourceID) 175 } 176 177 for _, tag := range sourceTags { 178 _, tagID, err := getOrCreateCategoryAndTag(tag.cat, tag.val) 179 if err != nil { 180 log.Printf("Error: fileHandler: failed to create tag %s:%s while copying from %s for file id=%d: %v", tag.cat, tag.val, sourceDesc, f.ID, err) 181 continue 182 } 183 if _, err = db.Exec("INSERT OR IGNORE INTO file_tags(file_id, tag_id) VALUES (?, ?)", f.ID, tagID); err != nil { 184 log.Printf("Error: fileHandler: failed to add tag %s:%s to file id=%d: %v", tag.cat, tag.val, f.ID, err) 185 } 186 } 187 http.Redirect(w, r, "/file/"+idStr+"?success="+url.QueryEscape(fmt.Sprintf("Copied %d tag(s) from %s", len(sourceTags), sourceDesc)), http.StatusSeeOther) 188 return 189 } 190 191 if cat == "" || val == "" { 192 http.Redirect(w, r, "/file/"+idStr+"?error="+url.QueryEscape("Category and value must not be empty"), http.StatusSeeOther) 193 return 194 } 195 196 originalVal := val 197 if val == "!" { 198 previousVal, err := getPreviousTagValue(cat, f.ID) 199 if err != nil { 200 http.Redirect(w, r, "/file/"+idStr+"?error="+url.QueryEscape("No previous tag found for category: "+cat), http.StatusSeeOther) 201 return 202 } 203 val = previousVal 204 } 205 _, tagID, err := getOrCreateCategoryAndTag(cat, val) 206 if err != nil { 207 log.Printf("Error: fileHandler: failed to create tag %s:%s for file id=%d: %v", cat, val, f.ID, err) 208 http.Redirect(w, r, "/file/"+idStr+"?error="+url.QueryEscape("Failed to create tag: "+err.Error()), http.StatusSeeOther) 209 return 210 } 211 if _, err = db.Exec("INSERT OR IGNORE INTO file_tags(file_id, tag_id) VALUES (?, ?)", f.ID, tagID); err != nil { 212 log.Printf("Error: fileHandler: failed to add tag %s:%s to file id=%d: %v", cat, val, f.ID, err) 213 http.Redirect(w, r, "/file/"+idStr+"?error="+url.QueryEscape("Failed to add tag: "+err.Error()), http.StatusSeeOther) 214 return 215 } 216 if originalVal == "!" { 217 http.Redirect(w, r, "/file/"+idStr+"?success="+url.QueryEscape("Tag '"+cat+": "+val+"' copied from previous file"), http.StatusSeeOther) 218 return 219 } 220 http.Redirect(w, r, "/file/"+idStr, http.StatusSeeOther) 221 return 222 } 223 224 catRows, err := db.Query(` 225 SELECT DISTINCT c.name 226 FROM categories c 227 JOIN tags t ON t.category_id = c.id 228 JOIN file_tags ft ON ft.tag_id = t.id 229 ORDER BY c.name 230 `) 231 if err != nil { 232 log.Printf("Warning: fileHandler: failed to query categories for file id=%d: %v", f.ID, err) 233 } 234 var cats []string 235 if catRows != nil { 236 for catRows.Next() { 237 var c string 238 if err := catRows.Scan(&c); err != nil { 239 log.Printf("Warning: fileHandler: failed to scan category row: %v", err) 240 continue 241 } 242 cats = append(cats, c) 243 } 244 catRows.Close() 245 } 246 247 propRows, err := db.Query(` 248 SELECT key, value FROM file_properties 249 WHERE file_id = ? 250 ORDER BY key, value 251 `, f.ID) 252 if err != nil { 253 log.Printf("Warning: fileHandler: failed to query properties for file id=%d: %v", f.ID, err) 254 } 255 fileProps := make(map[string]string) 256 if propRows != nil { 257 for propRows.Next() { 258 var k, v string 259 if err := propRows.Scan(&k, &v); err != nil { 260 log.Printf("Warning: fileHandler: failed to scan property row for file id=%d: %v", f.ID, err) 261 continue 262 } 263 fileProps[k] = v 264 } 265 propRows.Close() 266 } 267 268 pageData := buildPageDataWithIP(f.Filename, struct { 269 File File 270 Categories []string 271 EscapedFilename string 272 Properties map[string]string 273 }{f, cats, url.PathEscape(f.Filename), fileProps}) 274 275 renderTemplate(w, "file.html", pageData) 276 } 277 278 func buildPageDataWithIP(title string, data interface{}) PageData { 279 pageData := buildPageData(title, data) 280 ip, err := getLocalIP() 281 if err != nil { 282 log.Printf("Warning: buildPageDataWithIP: could not determine local IP: %v", err) 283 } 284 pageData.IP = ip 285 pageData.Port = strings.TrimPrefix(config.ServerPort, ":") 286 return pageData 287 } 288 289 func getLocalIP() (string, error) { 290 addrs, err := net.InterfaceAddrs() 291 if err != nil { 292 return "", err 293 } 294 for _, addr := range addrs { 295 if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { 296 if ipnet.IP.To4() != nil { 297 return ipnet.IP.String(), nil 298 } 299 } 300 } 301 return "", fmt.Errorf("no connected network interface found") 302 } 303 304 func tagActionHandler(w http.ResponseWriter, r *http.Request, parts []string) { 305 fileID := parts[2] 306 307 if r.Method != http.MethodPost { 308 http.Redirect(w, r, "/file/"+fileID, http.StatusSeeOther) 309 return 310 } 311 312 cat := strings.TrimSpace(html.UnescapeString(r.FormValue("category"))) 313 val := strings.TrimSpace(html.UnescapeString(r.FormValue("value"))) 314 315 if cat != "" && val != "" { 316 var tagID int 317 if err := db.QueryRow(` 318 SELECT t.id 319 FROM tags t 320 JOIN categories c ON c.id=t.category_id 321 WHERE c.name=? AND t.value=?`, cat, val).Scan(&tagID); err != nil && err != sql.ErrNoRows { 322 log.Printf("Warning: tagActionHandler: failed to look up tag %s:%s for file id=%s: %v", cat, val, fileID, err) 323 } 324 if tagID != 0 { 325 if _, err := db.Exec("DELETE FROM file_tags WHERE file_id=? AND tag_id=?", fileID, tagID); err != nil { 326 log.Printf("Error: tagActionHandler: failed to delete file_tag for file id=%s tag id=%d: %v", fileID, tagID, err) 327 } 328 } 329 } 330 http.Redirect(w, r, "/file/"+fileID, http.StatusSeeOther) 331 } 332 333 func getOrCreateCategoryAndTag(category, value string) (int, int, error) { 334 category = strings.TrimSpace(category) 335 value = strings.TrimSpace(value) 336 var catID int 337 err := db.QueryRow("SELECT id FROM categories WHERE name=?", category).Scan(&catID) 338 if err == sql.ErrNoRows { 339 res, err := db.Exec("INSERT INTO categories(name) VALUES(?)", category) 340 if err != nil { 341 return 0, 0, err 342 } 343 cid, _ := res.LastInsertId() 344 catID = int(cid) 345 } else if err != nil { 346 return 0, 0, err 347 } 348 349 var tagID int 350 if value != "" { 351 err = db.QueryRow("SELECT id FROM tags WHERE category_id=? AND value=?", catID, value).Scan(&tagID) 352 if err == sql.ErrNoRows { 353 res, err := db.Exec("INSERT INTO tags(category_id, value) VALUES(?, ?)", catID, value) 354 if err != nil { 355 return 0, 0, err 356 } 357 tid, _ := res.LastInsertId() 358 tagID = int(tid) 359 } else if err != nil { 360 return 0, 0, err 361 } 362 } 363 364 return catID, tagID, nil 365 } 366 367 func listFilesHandler(w http.ResponseWriter, r *http.Request) { 368 page := pageFromRequest(r) 369 perPage := perPageFromConfig(50) 370 371 tagged, taggedTotal, err := getTaggedFilesPaginated(page, perPage) 372 if err != nil { 373 log.Printf("Warning: listFilesHandler: failed to get tagged files: %v", err) 374 } 375 untagged, untaggedTotal, err := getUntaggedFilesPaginated(page, perPage) 376 if err != nil { 377 log.Printf("Warning: listFilesHandler: failed to get untagged files: %v", err) 378 } 379 380 // Use the larger total for pagination 381 total := taggedTotal 382 if untaggedTotal > total { 383 total = untaggedTotal 384 } 385 386 pageData := buildPageDataWithPagination("File Browser", ListData{ 387 Tagged: tagged, 388 Untagged: untagged, 389 Breadcrumbs: []Breadcrumb{}, 390 }, page, total, perPage, r) 391 392 renderTemplate(w, "list.html", pageData) 393 }