commit 6a3370ea740165340a9a2ea2cc234f55bc8c75a0
parent 0afd2efb20e94da6f945a937f56d672e696ef7b0
Author: breadcat <breadcat@users.noreply.github.com>
Date: Fri, 26 Sep 2025 23:30:14 +0100
Another big ol' refactor
Diffstat:
M | main.go | | | 1113 | ++++++++++++++++++++++++++++++------------------------------------------------- |
1 file changed, 421 insertions(+), 692 deletions(-)
diff --git a/main.go b/main.go
@@ -21,18 +21,18 @@ import (
)
var (
- db *sql.DB
- tmpl *template.Template
+ db *sql.DB
+ tmpl *template.Template
config Config
)
type File struct {
- ID int
- Filename string
+ ID int
+ Filename string
EscapedFilename string
- Path string
- Description string
- Tags map[string]string
+ Path string
+ Description string
+ Tags map[string]string
}
type Config struct {
@@ -53,11 +53,79 @@ type PageData struct {
IP string
Port string
Files []File
- Tags map[string][]TagDisplay
+ Tags map[string][]TagDisplay
+}
+
+// Common file query helpers
+func queryFilesWithTags(query string, args ...interface{}) ([]File, error) {
+ rows, err := db.Query(query, args...)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var files []File
+ for rows.Next() {
+ var f File
+ if err := rows.Scan(&f.ID, &f.Filename, &f.Path, &f.Description); err != nil {
+ return nil, err
+ }
+ f.EscapedFilename = url.PathEscape(f.Filename)
+ files = append(files, f)
+ }
+ return files, nil
+}
+
+func getTaggedFiles() ([]File, error) {
+ return queryFilesWithTags(`
+ 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
+ `)
+}
+
+func getUntaggedFiles() ([]File, error) {
+ return queryFilesWithTags(`
+ 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
+ ORDER BY f.id DESC
+ `)
+}
+
+// Common page data builder
+func buildPageData(title string, data interface{}) PageData {
+ tagMap, _ := getTagData()
+ return PageData{
+ Title: title,
+ Data: data,
+ Tags: tagMap,
+ }
+}
+
+func buildPageDataWithIP(title string, data interface{}) PageData {
+ pageData := buildPageData(title, data)
+ ip, _ := getLocalIP()
+ pageData.IP = ip
+ pageData.Port = strings.TrimPrefix(config.ServerPort, ":")
+ return pageData
+}
+
+// Common error response helper
+func renderError(w http.ResponseWriter, message string, statusCode int) {
+ http.Error(w, message, statusCode)
+}
+
+// Common template rendering with error handling
+func renderTemplate(w http.ResponseWriter, tmplName string, data PageData) {
+ if err := tmpl.ExecuteTemplate(w, tmplName, data); err != nil {
+ renderError(w, "Template rendering failed", http.StatusInternalServerError)
+ }
}
func main() {
- // Load configuration first
if err := loadConfig(); err != nil {
log.Fatalf("Failed to load config: %v", err)
}
@@ -123,7 +191,6 @@ func main() {
http.HandleFunc("/settings", settingsHandler)
http.HandleFunc("/orphans", orphansHandler)
- // Use configured upload directory for file serving
http.Handle("/uploads/", http.StripPrefix("/uploads/", http.FileServer(http.Dir(config.UploadDir))))
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
@@ -133,55 +200,71 @@ func main() {
http.ListenAndServe(config.ServerPort, nil)
}
-
func searchHandler(w http.ResponseWriter, r *http.Request) {
- query := strings.TrimSpace(r.URL.Query().Get("q"))
-
- var files []File
- var searchTitle string
-
- if query != "" {
- // Convert wildcards to SQL LIKE pattern
- sqlPattern := strings.ReplaceAll(query, "*", "%")
- sqlPattern = strings.ReplaceAll(sqlPattern, "?", "_")
-
- rows, err := db.Query(
- "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)
- return
- }
- defer rows.Close()
-
- for rows.Next() {
- var f File
- if err := rows.Scan(&f.ID, &f.Filename, &f.Path, &f.Description); err != nil {
- http.Error(w, "Failed to read results", http.StatusInternalServerError)
- return
- }
- f.EscapedFilename = url.PathEscape(f.Filename)
- files = append(files, f)
- }
-
- searchTitle = fmt.Sprintf("Search Results for: %s", query)
- } else {
- searchTitle = "Search Files"
- }
-
- pageData := PageData{
- Title: searchTitle,
- Query: query,
- Files: files,
- }
-
- if err := tmpl.ExecuteTemplate(w, "search.html", pageData); err != nil {
- http.Error(w, "Template rendering failed", http.StatusInternalServerError)
- }
+ query := strings.TrimSpace(r.URL.Query().Get("q"))
+
+ var files []File
+ var searchTitle string
+
+ if query != "" {
+ sqlPattern := strings.ReplaceAll(query, "*", "%")
+ sqlPattern = strings.ReplaceAll(sqlPattern, "?", "_")
+
+ var err error
+ files, err = queryFilesWithTags(
+ "SELECT id, filename, path, COALESCE(description, '') as description FROM files WHERE filename LIKE ? OR description LIKE ? ORDER BY filename",
+ sqlPattern, sqlPattern,
+ )
+ if err != nil {
+ renderError(w, "Search failed", http.StatusInternalServerError)
+ return
+ }
+ searchTitle = fmt.Sprintf("Search Results for: %s", query)
+ } else {
+ searchTitle = "Search Files"
+ }
+
+ pageData := buildPageData(searchTitle, files)
+ pageData.Query = query
+ pageData.Files = files
+ renderTemplate(w, "search.html", pageData)
+}
+
+// Unified upload processing function
+func processUpload(src io.Reader, filename string) (int64, string, error) {
+ finalFilename, finalPath, err := checkFileConflictStrict(filename)
+ if err != nil {
+ return 0, "", err
+ }
+
+ tempPath := finalPath + ".tmp"
+ tempFile, err := os.Create(tempPath)
+ if err != nil {
+ return 0, "", fmt.Errorf("failed to create temp file: %v", err)
+ }
+
+ _, err = io.Copy(tempFile, src)
+ tempFile.Close()
+ if err != nil {
+ os.Remove(tempPath)
+ return 0, "", fmt.Errorf("failed to copy file data: %v", err)
+ }
+
+ processedPath, warningMsg, err := processVideoFile(tempPath, finalPath)
+ if err != nil {
+ os.Remove(tempPath)
+ return 0, "", err
+ }
+
+ id, err := saveFileToDatabase(finalFilename, processedPath)
+ if err != nil {
+ os.Remove(processedPath)
+ return 0, "", err
+ }
+
+ return id, warningMsg, nil
}
-// Upload file from URL
func uploadFromURLHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Redirect(w, r, "/upload", http.StatusSeeOther)
@@ -190,7 +273,7 @@ func uploadFromURLHandler(w http.ResponseWriter, r *http.Request) {
fileURL := r.FormValue("fileurl")
if fileURL == "" {
- http.Error(w, "No URL provided", http.StatusBadRequest)
+ renderError(w, "No URL provided", http.StatusBadRequest)
return
}
@@ -198,18 +281,17 @@ func uploadFromURLHandler(w http.ResponseWriter, r *http.Request) {
parsedURL, err := url.ParseRequestURI(fileURL)
if err != nil || !(parsedURL.Scheme == "http" || parsedURL.Scheme == "https") {
- http.Error(w, "Invalid URL", http.StatusBadRequest)
+ renderError(w, "Invalid URL", http.StatusBadRequest)
return
}
resp, err := http.Get(fileURL)
if err != nil || resp.StatusCode != http.StatusOK {
- http.Error(w, "Failed to download file", http.StatusBadRequest)
+ renderError(w, "Failed to download file", http.StatusBadRequest)
return
}
defer resp.Body.Close()
- // Determine filename
var filename string
urlExt := filepath.Ext(parsedURL.Path)
if customFilename != "" {
@@ -225,138 +307,65 @@ func uploadFromURLHandler(w http.ResponseWriter, r *http.Request) {
}
}
- // Process the uploaded file
- id, _, warningMsg, err := processUploadedFile(resp.Body, filename)
+ id, warningMsg, err := processUpload(resp.Body, filename)
if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ renderError(w, err.Error(), http.StatusInternalServerError)
return
}
- // Redirect with optional warning
- redirectURL := fmt.Sprintf("/file/%d", id)
- if warningMsg != "" {
- redirectURL += "?warning=" + url.QueryEscape(warningMsg)
- }
- http.Redirect(w, r, redirectURL, http.StatusSeeOther)
+ redirectWithWarning(w, r, fmt.Sprintf("/file/%d", id), warningMsg)
}
-// List all files, plus untagged files
func listFilesHandler(w http.ResponseWriter, r *http.Request) {
- tagMap, _ := getTagData()
- // Tagged files
- rows, _ := db.Query(`
- 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
- `)
- defer rows.Close()
- var tagged []File
- for rows.Next() {
- var f File
- 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, 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
- ORDER BY f.id DESC
- `)
- defer untaggedRows.Close()
- var untagged []File
- for untaggedRows.Next() {
- var f File
- untaggedRows.Scan(&f.ID, &f.Filename, &f.Path, &f.Description)
- f.EscapedFilename = url.PathEscape(f.Filename)
- untagged = append(untagged, f)
- }
+ tagged, _ := getTaggedFiles()
+ untagged, _ := getUntaggedFiles()
- pageData := PageData{
- Title: "Home",
- Data: struct {
- Tagged []File
- Untagged []File
- }{tagged, untagged},
- Tags: tagMap,
- }
+ pageData := buildPageData("Home", struct {
+ Tagged []File
+ Untagged []File
+ }{tagged, untagged})
- tmpl.ExecuteTemplate(w, "list.html", pageData)
+ renderTemplate(w, "list.html", pageData)
}
-// Show untagged files at /untagged
func untaggedFilesHandler(w http.ResponseWriter, r *http.Request) {
- rows, _ := db.Query(`
- SELECT f.id, f.filename, f.path, COALESCE(f.description, '') as description
- FROM files f
- WHERE NOT EXISTS (
- SELECT 1
- FROM file_tags ft
- WHERE ft.file_id = f.id
- )
- ORDER BY f.id DESC
- `)
- defer rows.Close()
-
- var files []File
- for rows.Next() {
- var f File
- rows.Scan(&f.ID, &f.Filename, &f.Path, &f.Description)
- f.EscapedFilename = url.PathEscape(f.Filename)
- files = append(files, f)
- }
-
- tagMap, _ := getTagData()
- pageData := PageData{
- Title: "Untagged Files",
- Data: files,
- Tags: tagMap,
- }
-
- tmpl.ExecuteTemplate(w, "untagged.html", pageData)
+ files, _ := getUntaggedFiles()
+ pageData := buildPageData("Untagged Files", files)
+ renderTemplate(w, "untagged.html", pageData)
}
-// Add a file
func uploadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
- tagMap, _ := getTagData()
- pageData := PageData{
- Title: "Add File",
- Data: nil,
- Tags: tagMap, // <- header menu
- }
- tmpl.ExecuteTemplate(w, "add.html", pageData)
+ pageData := buildPageData("Add File", nil)
+ renderTemplate(w, "add.html", pageData)
return
}
file, header, err := r.FormFile("file")
if err != nil {
- http.Error(w, "Failed to read uploaded file", http.StatusBadRequest)
+ renderError(w, "Failed to read uploaded file", http.StatusBadRequest)
return
}
defer file.Close()
- // Process the uploaded file
- id, _, warningMsg, err := processUploadedFile(file, header.Filename)
+ id, warningMsg, err := processUpload(file, header.Filename)
if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ renderError(w, err.Error(), http.StatusInternalServerError)
return
}
- // Redirect with optional warning
- redirectURL := fmt.Sprintf("/file/%d", id)
+ redirectWithWarning(w, r, fmt.Sprintf("/file/%d", id), warningMsg)
+}
+
+// Helper function for redirects with optional warnings
+func redirectWithWarning(w http.ResponseWriter, r *http.Request, baseURL, warningMsg string) {
+ redirectURL := baseURL
if warningMsg != "" {
redirectURL += "?warning=" + url.QueryEscape(warningMsg)
}
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
}
-// checkFileConflictStrict checks if a file already exists in the upload directory.
-// It returns the final filename, full path, or an error if a duplicate exists.
func checkFileConflictStrict(filename string) (string, string, error) {
finalPath := filepath.Join(config.UploadDir, filename)
if _, err := os.Stat(finalPath); err == nil {
@@ -367,49 +376,42 @@ func checkFileConflictStrict(filename string) (string, string, error) {
return filename, finalPath, nil
}
-// raw local IP for raw address
func getLocalIP() (string, error) {
- addrs, err := net.InterfaceAddrs()
- if err != nil {
- return "", err
- }
- for _, addr := range addrs {
- if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
- if ipnet.IP.To4() != nil {
- return ipnet.IP.String(), nil
- }
- }
- }
- return "", fmt.Errorf("no connected network interface found")
+ addrs, err := net.InterfaceAddrs()
+ if err != nil {
+ return "", err
+ }
+ for _, addr := range addrs {
+ if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
+ if ipnet.IP.To4() != nil {
+ return ipnet.IP.String(), nil
+ }
+ }
+ }
+ return "", fmt.Errorf("no connected network interface found")
}
-// Router for file operations, tag deletion, rename, and delete
func fileRouter(w http.ResponseWriter, r *http.Request) {
parts := strings.Split(r.URL.Path, "/")
- // Handle delete: /file/{id}/delete
if len(parts) >= 4 && parts[3] == "delete" {
fileDeleteHandler(w, r, parts)
return
}
- // Handle rename: /file/{id}/rename
if len(parts) >= 4 && parts[3] == "rename" {
fileRenameHandler(w, r, parts)
return
}
- // Handle tag deletion: /file/{id}/tag/{category}/{value}/delete
if len(parts) >= 7 && parts[3] == "tag" {
tagActionHandler(w, r, parts)
return
}
- // Default file handler
fileHandler(w, r)
}
-// Handle file deletion
func fileDeleteHandler(w http.ResponseWriter, r *http.Request, parts []string) {
if r.Method != http.MethodPost {
http.Redirect(w, r, "/file/"+parts[2], http.StatusSeeOther)
@@ -418,55 +420,46 @@ func fileDeleteHandler(w http.ResponseWriter, r *http.Request, parts []string) {
fileID := parts[2]
- // Get current file info
var currentFile File
err := db.QueryRow("SELECT id, filename, path FROM files WHERE id=?", fileID).Scan(¤tFile.ID, ¤tFile.Filename, ¤tFile.Path)
if err != nil {
- http.Error(w, "File not found", http.StatusNotFound)
+ renderError(w, "File not found", http.StatusNotFound)
return
}
- // Start a transaction to ensure data consistency
tx, err := db.Begin()
if err != nil {
- http.Error(w, "Failed to start transaction", http.StatusInternalServerError)
+ renderError(w, "Failed to start transaction", http.StatusInternalServerError)
return
}
- defer tx.Rollback() // Will be ignored if tx.Commit() is called
+ defer tx.Rollback()
- // Delete file_tags relationships
_, err = tx.Exec("DELETE FROM file_tags WHERE file_id=?", fileID)
if err != nil {
- http.Error(w, "Failed to delete file tags", http.StatusInternalServerError)
+ renderError(w, "Failed to delete file tags", http.StatusInternalServerError)
return
}
- // Delete file record
_, err = tx.Exec("DELETE FROM files WHERE id=?", fileID)
if err != nil {
- http.Error(w, "Failed to delete file record", http.StatusInternalServerError)
+ renderError(w, "Failed to delete file record", http.StatusInternalServerError)
return
}
- // Commit the database transaction
err = tx.Commit()
if err != nil {
- http.Error(w, "Failed to commit transaction", http.StatusInternalServerError)
+ renderError(w, "Failed to commit transaction", http.StatusInternalServerError)
return
}
- // Delete the physical file (after successful database deletion)
err = os.Remove(currentFile.Path)
if err != nil {
- // Log the error but don't fail the request - database is already clean
log.Printf("Warning: Failed to delete physical file %s: %v", currentFile.Path, err)
}
- // Redirect to home page with success
http.Redirect(w, r, "/?deleted="+currentFile.Filename, http.StatusSeeOther)
}
-// Handle file renaming
func fileRenameHandler(w http.ResponseWriter, r *http.Request, parts []string) {
if r.Method != http.MethodPost {
http.Redirect(w, r, "/file/"+parts[2], http.StatusSeeOther)
@@ -477,55 +470,46 @@ func fileRenameHandler(w http.ResponseWriter, r *http.Request, parts []string) {
newFilename := strings.TrimSpace(r.FormValue("newfilename"))
if newFilename == "" {
- http.Error(w, "New filename cannot be empty", http.StatusBadRequest)
+ renderError(w, "New filename cannot be empty", http.StatusBadRequest)
return
}
- // Sanitize filename
newFilename = sanitizeFilename(newFilename)
- // Get current file info
var currentFile File
err := db.QueryRow("SELECT id, filename, path FROM files WHERE id=?", fileID).Scan(¤tFile.ID, ¤tFile.Filename, ¤tFile.Path)
if err != nil {
- http.Error(w, "File not found", http.StatusNotFound)
+ renderError(w, "File not found", http.StatusNotFound)
return
}
- // Skip if filename hasn't changed
if currentFile.Filename == newFilename {
http.Redirect(w, r, "/file/"+fileID, http.StatusSeeOther)
return
}
- // Check if new filename already exists
newPath := filepath.Join(config.UploadDir, newFilename)
if _, err := os.Stat(newPath); !os.IsNotExist(err) {
- http.Error(w, "A file with that name already exists", http.StatusConflict)
+ renderError(w, "A file with that name already exists", http.StatusConflict)
return
}
- // Rename the physical file
err = os.Rename(currentFile.Path, newPath)
if err != nil {
- http.Error(w, "Failed to rename physical file: "+err.Error(), http.StatusInternalServerError)
+ renderError(w, "Failed to rename physical file: "+err.Error(), http.StatusInternalServerError)
return
}
- // Update database
_, err = db.Exec("UPDATE files SET filename=?, path=? WHERE id=?", newFilename, newPath, fileID)
if err != nil {
- // Try to rename file back if database update fails
os.Rename(newPath, currentFile.Path)
- http.Error(w, "Failed to update database", http.StatusInternalServerError)
+ renderError(w, "Failed to update database", http.StatusInternalServerError)
return
}
- // Redirect back to file page
http.Redirect(w, r, "/file/"+fileID, http.StatusSeeOther)
}
-// File detail and add tags
func fileHandler(w http.ResponseWriter, r *http.Request) {
idStr := strings.TrimPrefix(r.URL.Path, "/file/")
if strings.Contains(idStr, "/") {
@@ -535,7 +519,7 @@ func fileHandler(w http.ResponseWriter, r *http.Request) {
var f File
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)
+ renderError(w, "File not found", http.StatusNotFound)
return
}
@@ -563,24 +547,21 @@ 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)
+ renderError(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 != "" {
@@ -604,29 +585,16 @@ func fileHandler(w http.ResponseWriter, r *http.Request) {
return
}
- // IP and port for raw URL
- ip, _ := getLocalIP()
- port := strings.TrimPrefix(config.ServerPort, ":")
- // Escape filename for copy/paste
- escaped := url.PathEscape(f.Filename)
-
- tagMap, _ := getTagData()
- pageData := PageData{
- Title: f.Filename,
- Tags: tagMap,
- Data: struct {
- File File
- Categories []string
- EscapedFilename string
- }{f, cats, escaped},
- IP: ip,
- Port: port,
- }
-
- tmpl.ExecuteTemplate(w, "file.html", pageData)
+ escaped := url.PathEscape(f.Filename)
+ pageData := buildPageDataWithIP(f.Filename, struct {
+ File File
+ Categories []string
+ EscapedFilename string
+ }{f, cats, escaped})
+
+ renderTemplate(w, "file.html", pageData)
}
-// Delete tag from file
func tagActionHandler(w http.ResponseWriter, r *http.Request, parts []string) {
fileID := parts[2]
cat := parts[4]
@@ -647,24 +615,16 @@ func tagActionHandler(w http.ResponseWriter, r *http.Request, parts []string) {
http.Redirect(w, r, "/file/"+fileID, http.StatusSeeOther)
}
-// Show all tags
func tagsHandler(w http.ResponseWriter, r *http.Request) {
- tagMap, _ := getTagData()
-
- pageData := PageData{
- Title: "All Tags",
- Data: tagMap,
- Tags: tagMap,
- }
-
- tmpl.ExecuteTemplate(w, "tags.html", pageData)
+ pageData := buildPageData("All Tags", nil)
+ pageData.Data = pageData.Tags
+ renderTemplate(w, "tags.html", pageData)
}
-// Filter files by tags
func tagFilterHandler(w http.ResponseWriter, r *http.Request) {
pathParts := strings.Split(strings.TrimPrefix(r.URL.Path, "/tag/"), "/")
if len(pathParts)%2 != 0 {
- http.Error(w, "Invalid tag filter path", http.StatusBadRequest)
+ renderError(w, "Invalid tag filter path", http.StatusBadRequest)
return
}
@@ -682,7 +642,6 @@ func tagFilterHandler(w http.ResponseWriter, r *http.Request) {
args := []interface{}{}
for _, f := range filters {
if f.Value == "unassigned" {
- // Files without any tag in this category
query += `
AND NOT EXISTS (
SELECT 1
@@ -693,7 +652,6 @@ func tagFilterHandler(w http.ResponseWriter, r *http.Request) {
)`
args = append(args, f.Category)
} else {
- // Files with this specific tag value
query += `
AND EXISTS (
SELECT 1
@@ -706,16 +664,7 @@ func tagFilterHandler(w http.ResponseWriter, r *http.Request) {
}
}
- rows, _ := db.Query(query, args...)
- defer rows.Close()
-
- var files []File
- for rows.Next() {
- var f File
- rows.Scan(&f.ID, &f.Filename, &f.Path, &f.Description)
- f.EscapedFilename = url.PathEscape(f.Filename)
- files = append(files, f)
- }
+ files, _ := queryFilesWithTags(query, args...)
var titleParts []string
for _, f := range filters {
@@ -723,35 +672,30 @@ func tagFilterHandler(w http.ResponseWriter, r *http.Request) {
}
title := "Tagged: " + strings.Join(titleParts, ", ")
- tagMap, _ := getTagData()
- pageData := PageData{
- Title: title,
- Tags: tagMap,
- Data: struct {
- Tagged []File
- Untagged []File
- }{files, nil},
- }
+ pageData := buildPageData(title, struct {
+ Tagged []File
+ Untagged []File
+ }{files, nil})
- tmpl.ExecuteTemplate(w, "list.html", pageData)
+ renderTemplate(w, "list.html", pageData)
}
func loadConfig() error {
- // Set defaults
config = Config{
DatabasePath: "./database.db",
UploadDir: "uploads",
ServerPort: ":8080",
+ InstanceName: "Taggart",
+ GallerySize: "400px",
+ ItemsPerPage: "100",
}
- // Try to load existing config
if data, err := ioutil.ReadFile("config.json"); err == nil {
if err := json.Unmarshal(data, &config); err != nil {
return err
}
}
- // Ensure upload directory exists
return os.MkdirAll(config.UploadDir, 0755)
}
@@ -764,22 +708,18 @@ func saveConfig() error {
}
func validateConfig(newConfig Config) error {
- // Validate database path is not empty
if newConfig.DatabasePath == "" {
return fmt.Errorf("database path cannot be empty")
}
- // Validate upload directory is not empty
if newConfig.UploadDir == "" {
return fmt.Errorf("upload directory cannot be empty")
}
- // Validate server port format
if newConfig.ServerPort == "" || !strings.HasPrefix(newConfig.ServerPort, ":") {
return fmt.Errorf("server port must be in format ':8080'")
}
- // Try to create upload directory if it doesn't exist
if err := os.MkdirAll(newConfig.UploadDir, 0755); err != nil {
return fmt.Errorf("cannot create upload directory: %v", err)
}
@@ -787,48 +727,36 @@ func validateConfig(newConfig Config) error {
return nil
}
-// Add this settings handler function
func settingsHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
- // Handle settings update
newConfig := Config{
DatabasePath: strings.TrimSpace(r.FormValue("database_path")),
UploadDir: strings.TrimSpace(r.FormValue("upload_dir")),
ServerPort: strings.TrimSpace(r.FormValue("server_port")),
}
- // Validate new configuration
if err := validateConfig(newConfig); err != nil {
- pageData := PageData{
- Title: "Settings",
- Data: struct {
- Config Config
- Error string
- }{config, err.Error()},
- }
- tmpl.ExecuteTemplate(w, "settings.html", pageData)
+ pageData := buildPageData("Settings", struct {
+ Config Config
+ Error string
+ }{config, err.Error()})
+ renderTemplate(w, "settings.html", pageData)
return
}
- // Check if database path changed and requires restart
needsRestart := (newConfig.DatabasePath != config.DatabasePath ||
- newConfig.ServerPort != config.ServerPort)
+ newConfig.ServerPort != config.ServerPort)
- // Save new configuration
config = newConfig
if err := saveConfig(); err != nil {
- pageData := PageData{
- Title: "Settings",
- Data: struct {
- Config Config
- Error string
- }{config, "Failed to save configuration: " + err.Error()},
- }
- tmpl.ExecuteTemplate(w, "settings.html", pageData)
+ pageData := buildPageData("Settings", struct {
+ Config Config
+ Error string
+ }{config, "Failed to save configuration: " + err.Error()})
+ renderTemplate(w, "settings.html", pageData)
return
}
- // Show success message
var message string
if needsRestart {
message = "Settings saved successfully! Please restart the server for database/port changes to take effect."
@@ -836,114 +764,90 @@ func settingsHandler(w http.ResponseWriter, r *http.Request) {
message = "Settings saved successfully!"
}
- pageData := PageData{
- Title: "Settings",
- Data: struct {
- Config Config
- Error string
- Success string
- }{config, "", message},
- }
- tmpl.ExecuteTemplate(w, "settings.html", pageData)
- return
- }
-
- // Show settings form
- tagMap, _ := getTagData()
- pageData := PageData{
- Title: "Settings",
- Tags: tagMap,
- Data: struct {
+ pageData := buildPageData("Settings", struct {
Config Config
Error string
Success string
- }{config, "", ""},
+ }{config, "", message})
+ renderTemplate(w, "settings.html", pageData)
+ return
}
- tmpl.ExecuteTemplate(w, "settings.html", pageData)
+
+ pageData := buildPageData("Settings", struct {
+ Config Config
+ Error string
+ Success string
+ }{config, "", ""})
+ renderTemplate(w, "settings.html", pageData)
}
-// yt-dlp Handler
func ytdlpHandler(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- http.Redirect(w, r, "/upload", http.StatusSeeOther)
- return
- }
-
- videoURL := r.FormValue("url")
- if videoURL == "" {
- http.Error(w, "No URL provided", http.StatusBadRequest)
- return
- }
-
- // Step 1: Get the expected filename first
- outTemplate := filepath.Join(config.UploadDir, "%(title)s.%(ext)s")
- filenameCmd := exec.Command("yt-dlp", "--playlist-items", "1", "-f", "mp4", "-o", outTemplate, "--get-filename", videoURL)
- filenameBytes, err := filenameCmd.Output()
- if err != nil {
- http.Error(w, fmt.Sprintf("Failed to get filename: %v", err), http.StatusInternalServerError)
- return
- }
- expectedFullPath := strings.TrimSpace(string(filenameBytes))
-
- // Extract just the filename from the full path
- expectedFilename := filepath.Base(expectedFullPath)
-
- // Enforce strict duplicate check
- finalFilename, finalPath, err := checkFileConflictStrict(expectedFilename)
- if err != nil {
- http.Error(w, err.Error(), http.StatusConflict)
- return
- }
-
- // Step 2: Download with yt-dlp using the full output template
- downloadCmd := exec.Command("yt-dlp", "--playlist-items", "1", "-f", "mp4", "-o", outTemplate, videoURL)
- downloadCmd.Stdout = os.Stdout
- downloadCmd.Stderr = os.Stderr
- if err := downloadCmd.Run(); err != nil {
- http.Error(w, fmt.Sprintf("Failed to download video: %v", err), http.StatusInternalServerError)
- return
- }
-
- // Step 3: The file should now be at expectedFullPath, move it to finalPath if different
- if expectedFullPath != finalPath {
- if err := os.Rename(expectedFullPath, finalPath); err != nil {
- http.Error(w, fmt.Sprintf("Failed to move downloaded file: %v", err), http.StatusInternalServerError)
- return
- }
- }
-
- // Step 4: Process video (codec detection and potential re-encoding)
- // Create a temporary path for processing
- tempPath := finalPath + ".tmp"
- if err := os.Rename(finalPath, tempPath); err != nil {
- http.Error(w, fmt.Sprintf("Failed to create temp file for processing: %v", err), http.StatusInternalServerError)
- return
- }
-
- processedPath, warningMsg, err := processVideoFile(tempPath, finalPath)
- if err != nil {
- os.Remove(tempPath) // Clean up
- http.Error(w, fmt.Sprintf("Failed to process video: %v", err), http.StatusInternalServerError)
- return
- }
-
- // Step 5: Save to database
- id, err := saveFileToDatabase(finalFilename, processedPath)
- if err != nil {
- os.Remove(processedPath)
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
-
- // Step 6: Redirect with optional warning
- redirectURL := fmt.Sprintf("/file/%d", id)
- if warningMsg != "" {
- redirectURL += "?warning=" + url.QueryEscape(warningMsg)
- }
- http.Redirect(w, r, redirectURL, http.StatusSeeOther)
+ if r.Method != http.MethodPost {
+ http.Redirect(w, r, "/upload", http.StatusSeeOther)
+ return
+ }
+
+ videoURL := r.FormValue("url")
+ if videoURL == "" {
+ renderError(w, "No URL provided", http.StatusBadRequest)
+ return
+ }
+
+ outTemplate := filepath.Join(config.UploadDir, "%(title)s.%(ext)s")
+ filenameCmd := exec.Command("yt-dlp", "--playlist-items", "1", "-f", "mp4", "-o", outTemplate, "--get-filename", videoURL)
+ filenameBytes, err := filenameCmd.Output()
+ if err != nil {
+ renderError(w, fmt.Sprintf("Failed to get filename: %v", err), http.StatusInternalServerError)
+ return
+ }
+ expectedFullPath := strings.TrimSpace(string(filenameBytes))
+
+ expectedFilename := filepath.Base(expectedFullPath)
+
+ finalFilename, finalPath, err := checkFileConflictStrict(expectedFilename)
+ if err != nil {
+ renderError(w, err.Error(), http.StatusConflict)
+ return
+ }
+
+ downloadCmd := exec.Command("yt-dlp", "--playlist-items", "1", "-f", "mp4", "-o", outTemplate, videoURL)
+ downloadCmd.Stdout = os.Stdout
+ downloadCmd.Stderr = os.Stderr
+ if err := downloadCmd.Run(); err != nil {
+ renderError(w, fmt.Sprintf("Failed to download video: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ if expectedFullPath != finalPath {
+ if err := os.Rename(expectedFullPath, finalPath); err != nil {
+ renderError(w, fmt.Sprintf("Failed to move downloaded file: %v", err), http.StatusInternalServerError)
+ return
+ }
+ }
+
+ tempPath := finalPath + ".tmp"
+ if err := os.Rename(finalPath, tempPath); err != nil {
+ renderError(w, fmt.Sprintf("Failed to create temp file for processing: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ processedPath, warningMsg, err := processVideoFile(tempPath, finalPath)
+ if err != nil {
+ os.Remove(tempPath)
+ renderError(w, fmt.Sprintf("Failed to process video: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ id, err := saveFileToDatabase(finalFilename, processedPath)
+ if err != nil {
+ os.Remove(processedPath)
+ renderError(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ redirectWithWarning(w, r, fmt.Sprintf("/file/%d", id), warningMsg)
}
-// Parse file ID ranges like "1-3,6,9" into a slice of integers
func parseFileIDRange(rangeStr string) ([]int, error) {
var fileIDs []int
parts := strings.Split(rangeStr, ",")
@@ -955,7 +859,6 @@ func parseFileIDRange(rangeStr string) ([]int, error) {
}
if strings.Contains(part, "-") {
- // Handle range like "1-3"
rangeParts := strings.Split(part, "-")
if len(rangeParts) != 2 {
return nil, fmt.Errorf("invalid range format: %s", part)
@@ -979,7 +882,6 @@ func parseFileIDRange(rangeStr string) ([]int, error) {
fileIDs = append(fileIDs, i)
}
} else {
- // Handle single ID like "6"
id, err := strconv.Atoi(part)
if err != nil {
return nil, fmt.Errorf("invalid file ID: %s", part)
@@ -988,7 +890,6 @@ func parseFileIDRange(rangeStr string) ([]int, error) {
}
}
- // Remove duplicates and sort
uniqueIDs := make(map[int]bool)
var result []int
for _, id := range fileIDs {
@@ -1001,13 +902,11 @@ func parseFileIDRange(rangeStr string) ([]int, error) {
return result, nil
}
-// Validate that all file IDs exist in the database
func validateFileIDs(fileIDs []int) ([]File, error) {
if len(fileIDs) == 0 {
return nil, fmt.Errorf("no file IDs provided")
}
- // Build placeholders for the IN clause
placeholders := make([]string, len(fileIDs))
args := make([]interface{}, len(fileIDs))
for i, id := range fileIDs {
@@ -1037,7 +936,6 @@ func validateFileIDs(fileIDs []int) ([]File, error) {
foundIDs[f.ID] = true
}
- // Check if any IDs were not found
var missingIDs []int
for _, id := range fileIDs {
if !foundIDs[id] {
@@ -1052,25 +950,21 @@ func validateFileIDs(fileIDs []int) ([]File, error) {
return files, nil
}
-// Apply tag operations to multiple files
func applyBulkTagOperations(fileIDs []int, category, value, operation string) error {
if category == "" {
return fmt.Errorf("category cannot be empty")
}
- // For add operations, value is required. For remove operations, value is optional
if operation == "add" && value == "" {
return fmt.Errorf("value cannot be empty when adding tags")
}
- // Start transaction
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("failed to start transaction: %v", err)
}
defer tx.Rollback()
- // Get or create category
var catID int
err = tx.QueryRow("SELECT id FROM categories WHERE name=?", category).Scan(&catID)
if err != nil && err != sql.ErrNoRows {
@@ -1089,7 +983,6 @@ func applyBulkTagOperations(fileIDs []int, category, value, operation string) er
catID = int(cid)
}
- // Get or create tag (only needed for specific value operations)
var tagID int
if value != "" {
err = tx.QueryRow("SELECT id FROM tags WHERE category_id=? AND value=?", catID, value).Scan(&tagID)
@@ -1110,7 +1003,6 @@ func applyBulkTagOperations(fileIDs []int, category, value, operation string) er
}
}
- // Apply operation to all files
for _, fileID := range fileIDs {
if operation == "add" {
_, err = tx.Exec("INSERT OR IGNORE INTO file_tags(file_id, tag_id) VALUES (?, ?)", fileID, tagID)
@@ -1119,13 +1011,11 @@ func applyBulkTagOperations(fileIDs []int, category, value, operation string) er
}
} else if operation == "remove" {
if value != "" {
- // Remove specific tag value
_, err = tx.Exec("DELETE FROM file_tags WHERE file_id=? AND tag_id=?", fileID, tagID)
if err != nil {
return fmt.Errorf("failed to remove tag from file %d: %v", fileID, err)
}
} else {
- // Remove all tags in this category
_, err = tx.Exec(`
DELETE FROM file_tags
WHERE file_id=? AND tag_id IN (
@@ -1140,142 +1030,80 @@ func applyBulkTagOperations(fileIDs []int, category, value, operation string) er
}
}
- // Commit transaction
- err = tx.Commit()
- if err != nil {
- return fmt.Errorf("failed to commit transaction: %v", err)
+ return tx.Commit()
+}
+
+// Consolidated bulk tag form data structure
+type BulkTagFormData struct {
+ Categories []string
+ RecentFiles []File
+ Error string
+ Success string
+ FormData struct {
+ FileRange string
+ Category string
+ Value string
+ Operation string
}
+}
- return nil
+func getBulkTagFormData() BulkTagFormData {
+ catRows, _ := db.Query("SELECT name FROM categories ORDER BY name")
+ var cats []string
+ for catRows.Next() {
+ var c string
+ catRows.Scan(&c)
+ cats = append(cats, c)
+ }
+ catRows.Close()
+
+ recentRows, _ := db.Query("SELECT id, filename FROM files ORDER BY id DESC LIMIT 20")
+ var recentFiles []File
+ for recentRows.Next() {
+ var f File
+ recentRows.Scan(&f.ID, &f.Filename)
+ recentFiles = append(recentFiles, f)
+ }
+ recentRows.Close()
+
+ return BulkTagFormData{
+ Categories: cats,
+ RecentFiles: recentFiles,
+ FormData: struct {
+ FileRange string
+ Category string
+ Value string
+ Operation string
+ }{Operation: "add"},
+ }
}
-// Bulk tag handler
func bulkTagHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
- // Show bulk tag form
-
- // Get all existing categories for the dropdown
- catRows, _ := db.Query("SELECT name FROM categories ORDER BY name")
- var cats []string
- for catRows.Next() {
- var c string
- catRows.Scan(&c)
- cats = append(cats, c)
- }
- catRows.Close()
-
- // Get recent files for reference
- recentRows, _ := db.Query("SELECT id, filename FROM files ORDER BY id DESC LIMIT 20")
- var recentFiles []File
- for recentRows.Next() {
- var f File
- recentRows.Scan(&f.ID, &f.Filename)
- recentFiles = append(recentFiles, f)
- }
- recentRows.Close()
-
- tagMap, _ := getTagData()
- pageData := PageData{
- Title: "Bulk Tag Editor",
- Tags: tagMap,
- Data: struct {
- Categories []string
- RecentFiles []File
- Error string
- Success string
- FormData struct {
- FileRange string
- Category string
- Value string
- Operation string
- }
- }{
- Categories: cats,
- RecentFiles: recentFiles,
- Error: "",
- Success: "",
- FormData: struct {
- FileRange string
- Category string
- Value string
- Operation string
- }{
- FileRange: "",
- Category: "",
- Value: "",
- Operation: "add",
- },
- },
- }
-
- tmpl.ExecuteTemplate(w, "bulk-tag.html", pageData)
+ formData := getBulkTagFormData()
+ pageData := buildPageData("Bulk Tag Editor", formData)
+ renderTemplate(w, "bulk-tag.html", pageData)
return
}
if r.Method == http.MethodPost {
- // Process bulk tag operation
rangeStr := strings.TrimSpace(r.FormValue("file_range"))
category := strings.TrimSpace(r.FormValue("category"))
value := strings.TrimSpace(r.FormValue("value"))
- operation := r.FormValue("operation") // "add" or "remove"
-
- // Get categories for form redisplay
- catRows, _ := db.Query("SELECT name FROM categories ORDER BY name")
- var cats []string
- for catRows.Next() {
- var c string
- catRows.Scan(&c)
- cats = append(cats, c)
- }
- catRows.Close()
-
- // Get recent files for reference
- recentRows, _ := db.Query("SELECT id, filename FROM files ORDER BY id DESC LIMIT 20")
- var recentFiles []File
- for recentRows.Next() {
- var f File
- recentRows.Scan(&f.ID, &f.Filename)
- recentFiles = append(recentFiles, f)
- }
- recentRows.Close()
+ operation := r.FormValue("operation")
+
+ formData := getBulkTagFormData()
+ formData.FormData.FileRange = rangeStr
+ formData.FormData.Category = category
+ formData.FormData.Value = value
+ formData.FormData.Operation = operation
- // Helper function to create error response
createErrorResponse := func(errorMsg string) {
- pageData := PageData{
- Title: "Bulk Tag Editor",
- Data: struct {
- Categories []string
- RecentFiles []File
- Error string
- Success string
- FormData struct {
- FileRange string
- Category string
- Value string
- Operation string
- }
- }{
- Categories: cats,
- RecentFiles: recentFiles,
- Error: errorMsg,
- Success: "",
- FormData: struct {
- FileRange string
- Category string
- Value string
- Operation string
- }{
- FileRange: rangeStr,
- Category: category,
- Value: value,
- Operation: operation,
- },
- },
- }
- tmpl.ExecuteTemplate(w, "bulk-tag.html", pageData)
+ formData.Error = errorMsg
+ pageData := buildPageData("Bulk Tag Editor", formData)
+ renderTemplate(w, "bulk-tag.html", pageData)
}
- // Validate basic inputs
if rangeStr == "" {
createErrorResponse("File range cannot be empty")
return
@@ -1286,50 +1114,37 @@ func bulkTagHandler(w http.ResponseWriter, r *http.Request) {
return
}
- // For add operations, value is required. For remove operations, value is optional
if operation == "add" && value == "" {
createErrorResponse("Value cannot be empty when adding tags")
return
}
- // Parse file ID range
fileIDs, err := parseFileIDRange(rangeStr)
if err != nil {
createErrorResponse(fmt.Sprintf("Invalid file range: %v", err))
return
}
- // Validate file IDs exist
validFiles, err := validateFileIDs(fileIDs)
if err != nil {
createErrorResponse(fmt.Sprintf("File validation error: %v", err))
return
}
- // Apply tag operations
err = applyBulkTagOperations(fileIDs, category, value, operation)
if err != nil {
createErrorResponse(fmt.Sprintf("Tag operation failed: %v", err))
return
}
- // Success message
- var operationText string
var successMsg string
-
if operation == "add" {
- operationText = "added to"
- successMsg = fmt.Sprintf("Tag '%s: %s' %s %d files",
- category, value, operationText, len(validFiles))
+ successMsg = fmt.Sprintf("Tag '%s: %s' added to %d files", category, value, len(validFiles))
} else {
if value != "" {
- operationText = "removed from"
- successMsg = fmt.Sprintf("Tag '%s: %s' %s %d files",
- category, value, operationText, len(validFiles))
+ successMsg = fmt.Sprintf("Tag '%s: %s' removed from %d files", category, value, len(validFiles))
} else {
- operationText = "removed from"
- successMsg = fmt.Sprintf("All '%s' category tags %s %d files",
- category, operationText, len(validFiles))
+ successMsg = fmt.Sprintf("All '%s' category tags removed from %d files", category, len(validFiles))
}
}
@@ -1341,53 +1156,20 @@ func bulkTagHandler(w http.ResponseWriter, r *http.Request) {
if len(filenames) <= 5 {
successMsg += fmt.Sprintf(": %s", strings.Join(filenames, ", "))
} else {
- successMsg += fmt.Sprintf(": %s and %d more",
- strings.Join(filenames[:5], ", "), len(filenames)-5)
+ successMsg += fmt.Sprintf(": %s and %d more", strings.Join(filenames[:5], ", "), len(filenames)-5)
}
- tagMap, _ := getTagData()
- pageData := PageData{
- Title: "Bulk Tag Editor",
- Tags: tagMap,
- Data: struct {
- Categories []string
- RecentFiles []File
- Error string
- Success string
- FormData struct {
- FileRange string
- Category string
- Value string
- Operation string
- }
- }{
- Categories: cats,
- RecentFiles: recentFiles,
- Error: "",
- Success: successMsg,
- FormData: struct {
- FileRange string
- Category string
- Value string
- Operation string
- }{
- FileRange: rangeStr,
- Category: category,
- Value: value,
- Operation: operation,
- },
- },
- }
-
- tmpl.ExecuteTemplate(w, "bulk-tag.html", pageData)
+ formData.Success = successMsg
+ pageData := buildPageData("Bulk Tag Editor", formData)
+ renderTemplate(w, "bulk-tag.html", pageData)
return
}
- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ renderError(w, "Method not allowed", http.StatusMethodNotAllowed)
}
func getTagData() (map[string][]TagDisplay, error) {
- rows, err := db.Query(`
+ rows, err := db.Query(`
SELECT c.name, t.value, COUNT(ft.file_id)
FROM tags t
JOIN categories c ON c.id = t.category_id
@@ -1395,22 +1177,21 @@ func getTagData() (map[string][]TagDisplay, error) {
GROUP BY t.id
HAVING COUNT(ft.file_id) > 0
ORDER BY c.name, t.value`)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
-
- tagMap := make(map[string][]TagDisplay)
- for rows.Next() {
- var cat, val string
- var count int
- rows.Scan(&cat, &val, &count)
- tagMap[cat] = append(tagMap[cat], TagDisplay{Value: val, Count: count})
- }
- return tagMap, nil
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ tagMap := make(map[string][]TagDisplay)
+ for rows.Next() {
+ var cat, val string
+ var count int
+ rows.Scan(&cat, &val, &count)
+ tagMap[cat] = append(tagMap[cat], TagDisplay{Value: val, Count: count})
+ }
+ return tagMap, nil
}
-// sanitizeFilename removes problematic characters from filenames
func sanitizeFilename(filename string) string {
if filename == "" {
return "file"
@@ -1426,7 +1207,6 @@ func sanitizeFilename(filename string) string {
return filename
}
-// detectVideoCodec uses ffprobe to detect the video codec
func detectVideoCodec(filePath string) (string, error) {
cmd := exec.Command("ffprobe", "-v", "error", "-select_streams", "v:0",
"-show_entries", "stream=codec_name", "-of", "default=nokey=1:noprint_wrappers=1", filePath)
@@ -1439,7 +1219,6 @@ func detectVideoCodec(filePath string) (string, error) {
return strings.TrimSpace(string(out)), nil
}
-// reencodeHEVCToH264 converts HEVC videos to H.264 for browser compatibility
func reencodeHEVCToH264(inputPath, outputPath string) error {
cmd := exec.Command("ffmpeg", "-i", inputPath,
"-c:v", "libx264", "-profile:v", "baseline", "-preset", "fast", "-crf", "23",
@@ -1451,15 +1230,12 @@ func reencodeHEVCToH264(inputPath, outputPath string) error {
return cmd.Run()
}
-// processVideoFile handles codec detection and re-encoding if needed
-// Returns final path, warning message (if any), and error
func processVideoFile(tempPath, finalPath string) (string, string, error) {
codec, err := detectVideoCodec(tempPath)
if err != nil {
return "", "", err
}
- // If HEVC, re-encode to H.264
if codec == "hevc" || codec == "h265" {
warningMsg := "The video uses HEVC and has been re-encoded to H.264 for browser compatibility."
@@ -1467,11 +1243,10 @@ func processVideoFile(tempPath, finalPath string) (string, string, error) {
return "", "", fmt.Errorf("failed to re-encode HEVC video: %v", err)
}
- os.Remove(tempPath) // Clean up temp file
+ os.Remove(tempPath)
return finalPath, warningMsg, nil
}
- // If not HEVC, just move temp file to final destination
if err := os.Rename(tempPath, finalPath); err != nil {
return "", "", fmt.Errorf("failed to move file: %v", err)
}
@@ -1479,7 +1254,6 @@ func processVideoFile(tempPath, finalPath string) (string, string, error) {
return finalPath, "", nil
}
-// 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, description) VALUES (?, ?, '')", filename, path)
if err != nil {
@@ -1494,109 +1268,64 @@ func saveFileToDatabase(filename, path string) (int64, error) {
return id, nil
}
-// processUploadedFile handles the complete file processing workflow
-func processUploadedFile(src io.Reader, originalFilename string) (int64, string, string, error) {
- // Strict duplicate check — error out if file already exists
- finalFilename, finalPath, err := checkFileConflictStrict(originalFilename)
+// File system operations for orphan detection
+func getFilesOnDisk(uploadDir string) ([]string, error) {
+ entries, err := os.ReadDir(uploadDir)
if err != nil {
- return 0, "", "", err
+ return nil, err
}
- // Create temporary file
- tempPath := finalPath + ".tmp"
- tempFile, err := os.Create(tempPath)
- if err != nil {
- return 0, "", "", fmt.Errorf("failed to create temp file: %v", err)
+ var files []string
+ for _, e := range entries {
+ if !e.IsDir() {
+ files = append(files, e.Name())
+ }
}
+ return files, nil
+}
- // Copy data to temp file
- _, err = io.Copy(tempFile, src)
- tempFile.Close()
+func getFilesInDB() (map[string]bool, error) {
+ rows, err := db.Query(`SELECT filename FROM files`)
if err != nil {
- os.Remove(tempPath)
- return 0, "", "", fmt.Errorf("failed to copy file data: %v", err)
+ return nil, err
}
+ defer rows.Close()
- // Process video (codec detection and potential re-encoding)
- processedPath, warningMsg, err := processVideoFile(tempPath, finalPath)
- if err != nil {
- os.Remove(tempPath)
- return 0, "", "", err
+ fileMap := make(map[string]bool)
+ for rows.Next() {
+ var name string
+ rows.Scan(&name)
+ fileMap[name] = true
}
+ return fileMap, nil
+}
- // Save to database
- id, err := saveFileToDatabase(finalFilename, processedPath)
+func getOrphanedFiles(uploadDir string) ([]string, error) {
+ diskFiles, err := getFilesOnDisk(uploadDir)
if err != nil {
- os.Remove(processedPath)
- return 0, "", "", err
+ return nil, err
}
- return id, finalFilename, warningMsg, nil
-}
-
-func getFilesOnDisk(uploadDir string) ([]string, error) {
- entries, err := os.ReadDir(uploadDir)
- if err != nil {
- return nil, err
- }
- var files []string
- for _, e := range entries {
- if !e.IsDir() {
- files = append(files, e.Name())
- }
- }
- return files, nil
-}
-
-func getFilesInDB() (map[string]bool, error) {
- rows, err := db.Query(`SELECT filename FROM files`)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
-
- fileMap := make(map[string]bool)
- for rows.Next() {
- var name string
- rows.Scan(&name)
- fileMap[name] = true
- }
- return fileMap, nil
-}
+ dbFiles, err := getFilesInDB()
+ if err != nil {
+ return nil, err
+ }
-func getOrphanedFiles(uploadDir string) ([]string, error) {
- diskFiles, err := getFilesOnDisk(uploadDir)
- if err != nil {
- return nil, err
- }
-
- dbFiles, err := getFilesInDB()
- if err != nil {
- return nil, err
- }
-
- var orphans []string
- for _, f := range diskFiles {
- if !dbFiles[f] {
- orphans = append(orphans, f)
- }
- }
- return orphans, nil
+ var orphans []string
+ for _, f := range diskFiles {
+ if !dbFiles[f] {
+ orphans = append(orphans, f)
+ }
+ }
+ return orphans, nil
}
func orphansHandler(w http.ResponseWriter, r *http.Request) {
- tagMap, _ := getTagData() // so header still works
-
- orphans, err := getOrphanedFiles(config.UploadDir)
- if err != nil {
- http.Error(w, "Error reading orphaned files", 500)
- return
- }
-
- pageData := PageData{
- Title: "Orphaned Files",
- Data: orphans, // just a slice of strings
- Tags: tagMap,
- }
+ orphans, err := getOrphanedFiles(config.UploadDir)
+ if err != nil {
+ renderError(w, "Error reading orphaned files", http.StatusInternalServerError)
+ return
+ }
- tmpl.ExecuteTemplate(w, "orphans.html", pageData)
+ pageData := buildPageData("Orphaned Files", orphans)
+ renderTemplate(w, "orphans.html", pageData)
}
\ No newline at end of file