tagliatelle

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

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 }