commit 0528d7d098e7b15edea009980c2db3a48fafeca4
parent 2b9cb19ebc893d52c1a9dda5cfc849f5544007df
Author: breadcat <breadcat@users.noreply.github.com>
Date: Thu, 25 Sep 2025 16:26:34 +0100
Add searchable description field to uploads
All this JS needs organising for the love of God
Diffstat:
M | main.go | | | 62 | ++++++++++++++++++++++++++++++++++++++++++++++---------------- |
M | templates/file.html | | | 82 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
2 files changed, 128 insertions(+), 16 deletions(-)
diff --git a/main.go b/main.go
@@ -31,6 +31,7 @@ type File struct {
Filename string
EscapedFilename string
Path string
+ Description string
Tags map[string]string
}
@@ -125,7 +126,7 @@ func processVideoFile(tempPath, finalPath string) (string, string, error) {
// saveFileToDatabase adds file record to database and returns the ID
func saveFileToDatabase(filename, path string) (int64, error) {
- res, err := db.Exec("INSERT INTO files (filename, path) VALUES (?, ?)", filename, path)
+ res, err := db.Exec("INSERT INTO files (filename, path, description) VALUES (?, ?, '')", filename, path)
if err != nil {
return 0, fmt.Errorf("failed to save file to database: %v", err)
}
@@ -194,7 +195,8 @@ func main() {
CREATE TABLE IF NOT EXISTS files (
id INTEGER PRIMARY KEY AUTOINCREMENT,
filename TEXT,
- path TEXT
+ path TEXT,
+ description TEXT DEFAULT ''
);
CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -216,6 +218,12 @@ func main() {
log.Fatal(err)
}
+ // Add description column if it doesn't exist (for existing databases)
+ _, err = db.Exec(`ALTER TABLE files ADD COLUMN description TEXT DEFAULT ''`)
+ if err != nil {
+ log.Printf("Note: Could not add description column (may already exist): %v", err)
+ }
+
os.MkdirAll(config.UploadDir, 0755)
os.MkdirAll("static", 0755)
@@ -265,8 +273,8 @@ func searchHandler(w http.ResponseWriter, r *http.Request) {
sqlPattern = strings.ReplaceAll(sqlPattern, "?", "_")
rows, err := db.Query(
- "SELECT id, filename, path FROM files WHERE filename LIKE ? ORDER BY filename",
- sqlPattern,
+ "SELECT id, filename, path, COALESCE(description, '') as description FROM files WHERE filename LIKE ? OR description LIKE ? ORDER BY filename",
+ sqlPattern, sqlPattern,
)
if err != nil {
http.Error(w, "Search failed", http.StatusInternalServerError)
@@ -276,7 +284,7 @@ func searchHandler(w http.ResponseWriter, r *http.Request) {
for rows.Next() {
var f File
- if err := rows.Scan(&f.ID, &f.Filename, &f.Path); err != nil {
+ if err := rows.Scan(&f.ID, &f.Filename, &f.Path, &f.Description); err != nil {
http.Error(w, "Failed to read results", http.StatusInternalServerError)
return
}
@@ -363,7 +371,7 @@ func uploadFromURLHandler(w http.ResponseWriter, r *http.Request) {
func listFilesHandler(w http.ResponseWriter, r *http.Request) {
// Tagged files
rows, _ := db.Query(`
- SELECT DISTINCT f.id, f.filename, f.path
+ SELECT DISTINCT f.id, f.filename, f.path, COALESCE(f.description, '') as description
FROM files f
JOIN file_tags ft ON ft.file_id = f.id
ORDER BY f.id DESC
@@ -372,14 +380,14 @@ func listFilesHandler(w http.ResponseWriter, r *http.Request) {
var tagged []File
for rows.Next() {
var f File
- rows.Scan(&f.ID, &f.Filename, &f.Path)
+ rows.Scan(&f.ID, &f.Filename, &f.Path, &f.Description)
f.EscapedFilename = url.PathEscape(f.Filename)
tagged = append(tagged, f)
}
// Untagged files
untaggedRows, _ := db.Query(`
- SELECT f.id, f.filename, f.path
+ SELECT f.id, f.filename, f.path, COALESCE(f.description, '') as description
FROM files f
LEFT JOIN file_tags ft ON ft.file_id = f.id
WHERE ft.file_id IS NULL
@@ -389,7 +397,7 @@ func listFilesHandler(w http.ResponseWriter, r *http.Request) {
var untagged []File
for untaggedRows.Next() {
var f File
- untaggedRows.Scan(&f.ID, &f.Filename, &f.Path)
+ untaggedRows.Scan(&f.ID, &f.Filename, &f.Path, &f.Description)
f.EscapedFilename = url.PathEscape(f.Filename)
untagged = append(untagged, f)
}
@@ -408,7 +416,7 @@ func listFilesHandler(w http.ResponseWriter, r *http.Request) {
// Show untagged files at /untagged
func untaggedFilesHandler(w http.ResponseWriter, r *http.Request) {
rows, _ := db.Query(`
- SELECT f.id, f.filename, f.path
+ SELECT f.id, f.filename, f.path, COALESCE(f.description, '') as description
FROM files f
WHERE NOT EXISTS (
SELECT 1
@@ -422,7 +430,7 @@ func untaggedFilesHandler(w http.ResponseWriter, r *http.Request) {
var files []File
for rows.Next() {
var f File
- rows.Scan(&f.ID, &f.Filename, &f.Path)
+ rows.Scan(&f.ID, &f.Filename, &f.Path, &f.Description)
f.EscapedFilename = url.PathEscape(f.Filename)
files = append(files, f)
}
@@ -646,7 +654,11 @@ func fileHandler(w http.ResponseWriter, r *http.Request) {
}
var f File
- db.QueryRow("SELECT id, filename, path FROM files WHERE id=?", idStr).Scan(&f.ID, &f.Filename, &f.Path)
+ 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)
+ if err != nil {
+ http.Error(w, "File not found", http.StatusNotFound)
+ return
+ }
f.Tags = make(map[string]string)
rows, _ := db.Query(`
@@ -672,6 +684,24 @@ func fileHandler(w http.ResponseWriter, r *http.Request) {
catRows.Close()
if r.Method == http.MethodPost {
+ // Handle description update
+ if r.FormValue("action") == "update_description" {
+ description := r.FormValue("description")
+ // Limit description to 2KB (2048 characters)
+ if len(description) > 2048 {
+ description = description[:2048]
+ }
+
+ _, err := db.Exec("UPDATE files SET description = ? WHERE id = ?", description, f.ID)
+ if err != nil {
+ http.Error(w, "Failed to update description", http.StatusInternalServerError)
+ return
+ }
+ http.Redirect(w, r, "/file/"+idStr, http.StatusSeeOther)
+ return
+ }
+
+ // Handle tag addition (existing functionality)
cat := r.FormValue("category")
val := r.FormValue("value")
if cat != "" && val != "" {
@@ -704,8 +734,8 @@ func fileHandler(w http.ResponseWriter, r *http.Request) {
pageData := PageData{
Title: f.Filename,
Data: struct {
- File File
- Categories []string
+ File File
+ Categories []string
EscapedFilename string
}{f, cats, escaped},
IP: ip,
@@ -782,7 +812,7 @@ func tagFilterHandler(w http.ResponseWriter, r *http.Request) {
filters = append(filters, filter{pathParts[i], pathParts[i+1]})
}
- query := `SELECT f.id, f.filename, f.path FROM files f WHERE 1=1`
+ query := `SELECT f.id, f.filename, f.path, COALESCE(f.description, '') as description FROM files f WHERE 1=1`
args := []interface{}{}
for _, f := range filters {
if f.Value == "unassigned" {
@@ -816,7 +846,7 @@ func tagFilterHandler(w http.ResponseWriter, r *http.Request) {
var files []File
for rows.Next() {
var f File
- rows.Scan(&f.ID, &f.Filename, &f.Path)
+ rows.Scan(&f.ID, &f.Filename, &f.Path, &f.Description)
f.EscapedFilename = url.PathEscape(f.Filename)
files = append(files, f)
}
diff --git a/templates/file.html b/templates/file.html
@@ -11,6 +11,88 @@
<a href="/uploads/{{.Data.EscapedFilename}}">Download file</a><br>
{{end}}
+<div class="description-section" style="margin: 20px 0; padding: 15px;">
+ <h3 style="margin-top: 0;">Description</h3>
+
+ <!-- Display Mode -->
+ <div id="description-display">
+ {{if .Data.File.Description}}
+ <div class="current-description" style="background: #f9f9f9; padding: 10px; border-radius: 3px; margin-bottom: 10px; white-space: pre-wrap; min-height: 20px;">{{.Data.File.Description}}</div>
+ {{else}}
+ <div class="no-description" style="color: #666; font-style: italic; margin-bottom: 10px; padding: 10px;">No description set</div>
+ {{end}}
+ <button id="edit-description-btn" onclick="toggleDescriptionEdit()" style="background: #007cba; color: white; padding: 6px 12px; border: none; border-radius: 3px; cursor: pointer; font-size: 14px;">
+ {{if .Data.File.Description}}Edit Description{{else}}Add Description{{end}}
+ </button>
+ </div>
+
+ <!-- Edit Mode (initially hidden) -->
+ <div id="description-edit" style="display: none;">
+ <form method="post">
+ <input type="hidden" name="action" value="update_description">
+ <div>
+ <textarea
+ id="description-textarea"
+ name="description"
+ rows="6"
+ style="width: 100%; max-width: 600px; padding: 8px; border: 1px solid #ccc; border-radius: 3px; font-family: inherit; resize: vertical;"
+ maxlength="2048"
+ placeholder="Enter description..."
+ >{{.Data.File.Description}}</textarea>
+ </div>
+ <div style="margin-top: 8px; display: flex; align-items: center; gap: 10px;">
+ <input type="submit" value="Save Description" style="background: #28a745; color: white; padding: 8px 16px; border: none; border-radius: 3px; cursor: pointer;">
+ <button type="button" onclick="cancelDescriptionEdit()" style="background: #6c757d; color: white; padding: 8px 16px; border: none; border-radius: 3px; cursor: pointer;">Cancel</button>
+ </div>
+ </form>
+ </div>
+</div>
+
+<script>
+function toggleDescriptionEdit() {
+ const displayDiv = document.getElementById('description-display');
+ const editDiv = document.getElementById('description-edit');
+
+ displayDiv.style.display = 'none';
+ editDiv.style.display = 'block';
+
+ // Focus the textarea and update character count
+ const textarea = document.getElementById('description-textarea');
+ textarea.focus();
+
+ // Move cursor to end of text if there's existing content
+ if (textarea.value) {
+ textarea.setSelectionRange(textarea.value.length, textarea.value.length);
+ }
+}
+
+function cancelDescriptionEdit() {
+ const displayDiv = document.getElementById('description-display');
+ const editDiv = document.getElementById('description-edit');
+ const textarea = document.getElementById('description-textarea');
+
+ // Reset textarea to original value
+ textarea.value = {{if .Data.File.Description}}`{{.Data.File.Description}}`{{else}}``{{end}};
+
+ displayDiv.style.display = 'block';
+ editDiv.style.display = 'none';
+}
+
+// Auto-resize textarea as content changes
+document.addEventListener('DOMContentLoaded', function() {
+ const textarea = document.getElementById('description-textarea');
+ if (textarea) {
+ textarea.addEventListener('input', function() {
+ // Reset height to auto to get the correct scrollHeight
+ this.style.height = 'auto';
+ // Set the height to match the content, with a minimum of 6 rows
+ const minHeight = parseInt(getComputedStyle(this).lineHeight) * 6;
+ this.style.height = Math.max(minHeight, this.scrollHeight) + 'px';
+ });
+ }
+});
+</script>
+
<h3>Raw URL</h3>
<pre id="raw-url">http://{{.IP}}:{{.Port}}/uploads/{{.Data.EscapedFilename}}</pre>
<button id="copy-btn" style="margin-top: 5px;">Copy to Clipboard</button>