include-viewer.go (6536B)
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 pageData := buildPageDataWithIP(f.Filename, struct { 124 File File 125 Categories []string 126 EscapedFilename string 127 }{f, cats, url.PathEscape(f.Filename)}) 128 129 renderTemplate(w, "file.html", pageData) 130 } 131 132 func buildPageDataWithIP(title string, data interface{}) PageData { 133 pageData := buildPageData(title, data) 134 ip, _ := getLocalIP() 135 pageData.IP = ip 136 pageData.Port = strings.TrimPrefix(config.ServerPort, ":") 137 return pageData 138 } 139 140 func getLocalIP() (string, error) { 141 addrs, err := net.InterfaceAddrs() 142 if err != nil { 143 return "", err 144 } 145 for _, addr := range addrs { 146 if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { 147 if ipnet.IP.To4() != nil { 148 return ipnet.IP.String(), nil 149 } 150 } 151 } 152 return "", fmt.Errorf("no connected network interface found") 153 } 154 155 func tagActionHandler(w http.ResponseWriter, r *http.Request, parts []string) { 156 fileID := parts[2] 157 cat := parts[4] 158 val := parts[5] 159 action := parts[6] 160 161 if action == "delete" && r.Method == http.MethodPost { 162 var tagID int 163 db.QueryRow(` 164 SELECT t.id 165 FROM tags t 166 JOIN categories c ON c.id=t.category_id 167 WHERE c.name=? AND t.value=?`, cat, val).Scan(&tagID) 168 if tagID != 0 { 169 db.Exec("DELETE FROM file_tags WHERE file_id=? AND tag_id=?", fileID, tagID) 170 } 171 } 172 http.Redirect(w, r, "/file/"+fileID, http.StatusSeeOther) 173 } 174 175 func getOrCreateCategoryAndTag(category, value string) (int, int, error) { 176 category = strings.TrimSpace(category) 177 value = strings.TrimSpace(value) 178 var catID int 179 err := db.QueryRow("SELECT id FROM categories WHERE name=?", category).Scan(&catID) 180 if err == sql.ErrNoRows { 181 res, err := db.Exec("INSERT INTO categories(name) VALUES(?)", category) 182 if err != nil { 183 return 0, 0, err 184 } 185 cid, _ := res.LastInsertId() 186 catID = int(cid) 187 } else if err != nil { 188 return 0, 0, err 189 } 190 191 var tagID int 192 if value != "" { 193 err = db.QueryRow("SELECT id FROM tags WHERE category_id=? AND value=?", catID, value).Scan(&tagID) 194 if err == sql.ErrNoRows { 195 res, err := db.Exec("INSERT INTO tags(category_id, value) VALUES(?, ?)", catID, value) 196 if err != nil { 197 return 0, 0, err 198 } 199 tid, _ := res.LastInsertId() 200 tagID = int(tid) 201 } else if err != nil { 202 return 0, 0, err 203 } 204 } 205 206 return catID, tagID, nil 207 } 208 209 func listFilesHandler(w http.ResponseWriter, r *http.Request) { 210 // Get page number from query params 211 pageStr := r.URL.Query().Get("page") 212 page := 1 213 if pageStr != "" { 214 if p, err := strconv.Atoi(pageStr); err == nil && p > 0 { 215 page = p 216 } 217 } 218 219 // Get per page from config 220 perPage := 50 221 if config.ItemsPerPage != "" { 222 if pp, err := strconv.Atoi(config.ItemsPerPage); err == nil && pp > 0 { 223 perPage = pp 224 } 225 } 226 227 tagged, taggedTotal, _ := getTaggedFilesPaginated(page, perPage) 228 untagged, untaggedTotal, _ := getUntaggedFilesPaginated(page, perPage) 229 230 // Use the larger total for pagination 231 total := taggedTotal 232 if untaggedTotal > total { 233 total = untaggedTotal 234 } 235 236 pageData := buildPageDataWithPagination("File Browser", ListData{ 237 Tagged: tagged, 238 Untagged: untagged, 239 Breadcrumbs: []Breadcrumb{}, 240 }, page, total, perPage) 241 242 renderTemplate(w, "list.html", pageData) 243 }