tagliatelle

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

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 }