tagliatelle

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

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 }