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