include-viewer.go (7547B)
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 fileHandler(w http.ResponseWriter, r *http.Request) { 39 idStr := strings.TrimPrefix(r.URL.Path, "/file/") 40 if strings.Contains(idStr, "/") { 41 idStr = strings.SplitN(idStr, "/", 2)[0] 42 } 43 44 var f File 45 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) 46 if err != nil { 47 log.Printf("Error: fileHandler: file not found for id=%s: %v", idStr, err) 48 renderError(w, "File not found", http.StatusNotFound) 49 return 50 } 51 52 f.Tags = make(map[string][]string) 53 rows, _ := db.Query(` 54 SELECT c.name, t.value 55 FROM tags t 56 JOIN categories c ON c.id = t.category_id 57 JOIN file_tags ft ON ft.tag_id = t.id 58 WHERE ft.file_id=?`, f.ID) 59 for rows.Next() { 60 var cat, val string 61 rows.Scan(&cat, &val) 62 f.Tags[cat] = append(f.Tags[cat], val) 63 } 64 rows.Close() 65 66 if r.Method == http.MethodPost { 67 if r.FormValue("action") == "update_description" { 68 description := r.FormValue("description") 69 if len(description) > 2048 { 70 description = description[:2048] 71 } 72 73 if _, err := db.Exec("UPDATE files SET description = ? WHERE id = ?", description, f.ID); err != nil { 74 log.Printf("Error: fileHandler: failed to update description for file id=%d: %v", f.ID, err) 75 renderError(w, "Failed to update description", http.StatusInternalServerError) 76 return 77 } 78 http.Redirect(w, r, "/file/"+idStr, http.StatusSeeOther) 79 return 80 } 81 cat := strings.TrimSpace(r.FormValue("category")) 82 val := strings.TrimSpace(r.FormValue("value")) 83 if cat != "" && val != "" { 84 originalVal := val 85 if val == "!" { 86 previousVal, err := getPreviousTagValue(cat, f.ID) 87 if err != nil { 88 http.Redirect(w, r, "/file/"+idStr+"?error="+url.QueryEscape("No previous tag found for category: "+cat), http.StatusSeeOther) 89 return 90 } 91 val = previousVal 92 } 93 _, tagID, err := getOrCreateCategoryAndTag(cat, val) 94 if err != nil { 95 http.Redirect(w, r, "/file/"+idStr+"?error="+url.QueryEscape("Failed to create tag: "+err.Error()), http.StatusSeeOther) 96 return 97 } 98 _, err = db.Exec("INSERT OR IGNORE INTO file_tags(file_id, tag_id) VALUES (?, ?)", f.ID, tagID) 99 if err != nil { 100 http.Redirect(w, r, "/file/"+idStr+"?error="+url.QueryEscape("Failed to add tag: "+err.Error()), http.StatusSeeOther) 101 return 102 } 103 if originalVal == "!" { 104 http.Redirect(w, r, "/file/"+idStr+"?success="+url.QueryEscape("Tag '"+cat+": "+val+"' copied from previous file"), http.StatusSeeOther) 105 return 106 } 107 } 108 http.Redirect(w, r, "/file/"+idStr, http.StatusSeeOther) 109 return 110 } 111 112 catRows, err := db.Query(` 113 SELECT DISTINCT c.name 114 FROM categories c 115 JOIN tags t ON t.category_id = c.id 116 JOIN file_tags ft ON ft.tag_id = t.id 117 ORDER BY c.name 118 `) 119 if err != nil { 120 log.Printf("Warning: fileHandler: failed to query categories for file id=%d: %v", f.ID, err) 121 } 122 var cats []string 123 for catRows.Next() { 124 var c string 125 catRows.Scan(&c) 126 cats = append(cats, c) 127 } 128 catRows.Close() 129 130 propRows, err := db.Query(` 131 SELECT key, value FROM file_properties 132 WHERE file_id = ? 133 ORDER BY key, value 134 `, f.ID) 135 if err != nil { 136 log.Printf("Warning: fileHandler: failed to query properties for file id=%d: %v", f.ID, err) 137 } 138 fileProps := make(map[string]string) 139 for propRows.Next() { 140 var k, v string 141 propRows.Scan(&k, &v) 142 fileProps[k] = v 143 } 144 propRows.Close() 145 146 pageData := buildPageDataWithIP(f.Filename, struct { 147 File File 148 Categories []string 149 EscapedFilename string 150 Properties map[string]string 151 }{f, cats, url.PathEscape(f.Filename), fileProps}) 152 153 renderTemplate(w, "file.html", pageData) 154 } 155 156 func buildPageDataWithIP(title string, data interface{}) PageData { 157 pageData := buildPageData(title, data) 158 ip, err := getLocalIP() 159 if err != nil { 160 log.Printf("Warning: buildPageDataWithIP: could not determine local IP: %v", err) 161 } 162 pageData.IP = ip 163 pageData.Port = strings.TrimPrefix(config.ServerPort, ":") 164 return pageData 165 } 166 167 func getLocalIP() (string, error) { 168 addrs, err := net.InterfaceAddrs() 169 if err != nil { 170 return "", err 171 } 172 for _, addr := range addrs { 173 if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { 174 if ipnet.IP.To4() != nil { 175 return ipnet.IP.String(), nil 176 } 177 } 178 } 179 return "", fmt.Errorf("no connected network interface found") 180 } 181 182 func tagActionHandler(w http.ResponseWriter, r *http.Request, parts []string) { 183 fileID := parts[2] 184 185 if r.Method != http.MethodPost { 186 http.Redirect(w, r, "/file/"+fileID, http.StatusSeeOther) 187 return 188 } 189 190 cat := strings.TrimSpace(html.UnescapeString(r.FormValue("category"))) 191 val := strings.TrimSpace(html.UnescapeString(r.FormValue("value"))) 192 193 if cat != "" && val != "" { 194 var tagID int 195 db.QueryRow(` 196 SELECT t.id 197 FROM tags t 198 JOIN categories c ON c.id=t.category_id 199 WHERE c.name=? AND t.value=?`, cat, val).Scan(&tagID) 200 if tagID != 0 { 201 db.Exec("DELETE FROM file_tags WHERE file_id=? AND tag_id=?", fileID, tagID) 202 } 203 } 204 http.Redirect(w, r, "/file/"+fileID, http.StatusSeeOther) 205 } 206 207 func getOrCreateCategoryAndTag(category, value string) (int, int, error) { 208 category = strings.TrimSpace(category) 209 value = strings.TrimSpace(value) 210 var catID int 211 err := db.QueryRow("SELECT id FROM categories WHERE name=?", category).Scan(&catID) 212 if err == sql.ErrNoRows { 213 res, err := db.Exec("INSERT INTO categories(name) VALUES(?)", category) 214 if err != nil { 215 return 0, 0, err 216 } 217 cid, _ := res.LastInsertId() 218 catID = int(cid) 219 } else if err != nil { 220 return 0, 0, err 221 } 222 223 var tagID int 224 if value != "" { 225 err = db.QueryRow("SELECT id FROM tags WHERE category_id=? AND value=?", catID, value).Scan(&tagID) 226 if err == sql.ErrNoRows { 227 res, err := db.Exec("INSERT INTO tags(category_id, value) VALUES(?, ?)", catID, value) 228 if err != nil { 229 return 0, 0, err 230 } 231 tid, _ := res.LastInsertId() 232 tagID = int(tid) 233 } else if err != nil { 234 return 0, 0, err 235 } 236 } 237 238 return catID, tagID, nil 239 } 240 241 func listFilesHandler(w http.ResponseWriter, r *http.Request) { 242 // Get page number from query params 243 pageStr := r.URL.Query().Get("page") 244 page := 1 245 if pageStr != "" { 246 if p, err := strconv.Atoi(pageStr); err == nil && p > 0 { 247 page = p 248 } 249 } 250 251 // Get per page from config 252 perPage := 50 253 if config.ItemsPerPage != "" { 254 if pp, err := strconv.Atoi(config.ItemsPerPage); err == nil && pp > 0 { 255 perPage = pp 256 } 257 } 258 259 tagged, taggedTotal, _ := getTaggedFilesPaginated(page, perPage) 260 untagged, untaggedTotal, _ := getUntaggedFilesPaginated(page, perPage) 261 262 // Use the larger total for pagination 263 total := taggedTotal 264 if untaggedTotal > total { 265 total = untaggedTotal 266 } 267 268 pageData := buildPageDataWithPagination("File Browser", ListData{ 269 Tagged: tagged, 270 Untagged: untagged, 271 Breadcrumbs: []Breadcrumb{}, 272 }, page, total, perPage, r) 273 274 renderTemplate(w, "list.html", pageData) 275 }