tagliatelle

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

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 }