commit 203c498531d7359475fca41271345978300cfada
parent 4c8246c778b8c6bee9c61c3b0319b8dd6501c01b
Author: breadcat <breadcat@users.noreply.github.com>
Date: Wed, 24 Sep 2025 14:50:21 +0100
Large refactor
Diffstat:
M | main.go | | | 474 | +++++++++++++++++++++++++++---------------------------------------------------- |
1 file changed, 164 insertions(+), 310 deletions(-)
diff --git a/main.go b/main.go
@@ -52,6 +52,129 @@ type PageData struct {
Port string
}
+// sanitizeFilename removes problematic characters from filenames
+func sanitizeFilename(filename string) string {
+ if filename == "" {
+ return "file"
+ }
+
+ filename = strings.ReplaceAll(filename, "/", "_")
+ filename = strings.ReplaceAll(filename, "\\", "_")
+ filename = strings.ReplaceAll(filename, "..", "_")
+
+ if filename == "" {
+ return "file"
+ }
+ 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)
+
+ out, err := cmd.Output()
+ if err != nil {
+ return "", fmt.Errorf("failed to probe video codec: %v", err)
+ }
+
+ 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",
+ "-c:a", "aac", "-movflags", "+faststart", outputPath)
+
+ cmd.Stderr = os.Stderr
+ cmd.Stdout = os.Stdout
+
+ 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."
+
+ if err := reencodeHEVCToH264(tempPath, finalPath); err != nil {
+ return "", "", fmt.Errorf("failed to re-encode HEVC video: %v", err)
+ }
+
+ os.Remove(tempPath) // Clean up temp file
+ 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)
+ }
+
+ 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) VALUES (?, ?)", filename, path)
+ if err != nil {
+ return 0, fmt.Errorf("failed to save file to database: %v", err)
+ }
+
+ id, err := res.LastInsertId()
+ if err != nil {
+ return 0, fmt.Errorf("failed to get inserted ID: %v", err)
+ }
+
+ 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)
+ if err != nil {
+ return 0, "", "", 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)
+ }
+
+ // Copy data to temp file
+ _, err = io.Copy(tempFile, src)
+ tempFile.Close()
+ if err != nil {
+ os.Remove(tempPath)
+ return 0, "", "", fmt.Errorf("failed to copy file data: %v", err)
+ }
+
+ // Process video (codec detection and potential re-encoding)
+ processedPath, warningMsg, err := processVideoFile(tempPath, finalPath)
+ if err != nil {
+ os.Remove(tempPath)
+ return 0, "", "", err
+ }
+
+ // Save to database
+ id, err := saveFileToDatabase(finalFilename, processedPath)
+ if err != nil {
+ os.Remove(processedPath)
+ return 0, "", "", err
+ }
+
+ return id, finalFilename, warningMsg, nil
+}
+
func main() {
// Load configuration first
if err := loadConfig(); err != nil {
@@ -198,7 +321,7 @@ func uploadFromURLHandler(w http.ResponseWriter, r *http.Request) {
}
defer resp.Body.Close()
- // Determine filename & extension
+ // Determine filename
var filename string
urlExt := filepath.Ext(parsedURL.Path)
if customFilename != "" {
@@ -214,90 +337,19 @@ func uploadFromURLHandler(w http.ResponseWriter, r *http.Request) {
}
}
- // Sanitize filename
- filename = strings.ReplaceAll(filename, "/", "_")
- filename = strings.ReplaceAll(filename, "\\", "_")
- filename = strings.ReplaceAll(filename, "..", "_")
- if filename == "" {
- filename = "file_from_url"
- }
-
- dstPath := filepath.Join(config.UploadDir, filename)
-
- // Avoid overwriting existing files
- originalFilename := filename
- for i := 1; ; i++ {
- if _, err := os.Stat(dstPath); os.IsNotExist(err) {
- break
- }
- ext := filepath.Ext(originalFilename)
- name := strings.TrimSuffix(originalFilename, ext)
- filename = fmt.Sprintf("%s_%d%s", name, i, ext)
- dstPath = filepath.Join(config.UploadDir, filename)
- }
-
- // Save the downloaded file temporarily
- tmpPath := dstPath + ".tmp"
- tmpFile, err := os.Create(tmpPath)
+ // Process the uploaded file
+ id, _, warningMsg, err := processUploadedFile(resp.Body, filename)
if err != nil {
- http.Error(w, "Failed to save file", http.StatusInternalServerError)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
- _, err = io.Copy(tmpFile, resp.Body)
- tmpFile.Close()
- if err != nil {
- os.Remove(tmpPath)
- http.Error(w, "Failed to save file", http.StatusInternalServerError)
- return
- }
-
- // --- Detect video codec using ffprobe ---
- ffprobeCmd := exec.Command("ffprobe", "-v", "error", "-select_streams", "v:0",
- "-show_entries", "stream=codec_name", "-of", "default=nokey=1:noprint_wrappers=1", tmpPath)
- out, err := ffprobeCmd.Output()
- if err != nil {
- os.Remove(tmpPath)
- http.Error(w, "Failed to inspect video", http.StatusInternalServerError)
- return
- }
- videoCodec := strings.TrimSpace(string(out))
-
- // If HEVC, re-encode to H.264 and warn user
- if videoCodec == "hevc" || videoCodec == "h265" {
- reencodedPath := dstPath // overwrite the final destination
- warningMessage := "The uploaded video uses HEVC and will be re-encoded to H.264 for browser compatibility."
-
- ffmpegCmd := exec.Command("ffmpeg", "-i", tmpPath, "-c:v", "libx264", "-profile:v", "baseline", "-preset", "fast", "-crf", "23", "-c:a", "aac", "-movflags", "+faststart", reencodedPath)
- ffmpegCmd.Stderr = os.Stderr
- ffmpegCmd.Stdout = os.Stdout
- if err := ffmpegCmd.Run(); err != nil {
- os.Remove(tmpPath)
- http.Error(w, "Failed to re-encode HEVC video", http.StatusInternalServerError)
- return
- }
- os.Remove(tmpPath)
- // Optional: flash a warning message to user (could be stored in session or query param)
- http.Redirect(w, r, fmt.Sprintf("/file/%s?warning=%s", filename, url.QueryEscape(warningMessage)), http.StatusSeeOther)
- return
+ // Redirect with optional warning
+ redirectURL := fmt.Sprintf("/file/%d", id)
+ if warningMsg != "" {
+ redirectURL += "?warning=" + url.QueryEscape(warningMsg)
}
-
- // If not HEVC, just move the temp file
- err = os.Rename(tmpPath, dstPath)
- if err != nil {
- http.Error(w, "Failed to save file", http.StatusInternalServerError)
- return
- }
-
- // Add to database
- res, err := db.Exec("INSERT INTO files (filename, path) VALUES (?, ?)", filename, dstPath)
- if err != nil {
- http.Error(w, "Failed to record file", http.StatusInternalServerError)
- return
- }
-
- id, _ := res.LastInsertId()
- http.Redirect(w, r, fmt.Sprintf("/file/%d", id), http.StatusSeeOther)
+ http.Redirect(w, r, redirectURL, http.StatusSeeOther)
}
// List all files, plus untagged files
@@ -394,77 +446,31 @@ func uploadHandler(w http.ResponseWriter, r *http.Request) {
}
defer file.Close()
- finalFilename := header.Filename
- dstPath := filepath.Join(config.UploadDir, finalFilename)
-
- // Check before creating anything
- if _, err := os.Stat(dstPath); err == nil {
- http.Error(w, "A file with that name already exists", http.StatusConflict)
- return
- } else if !os.IsNotExist(err) {
- http.Error(w, "Failed to check for existing file", http.StatusInternalServerError)
- return
- }
-
- // Save uploaded file to a temporary path
- tmpPath := dstPath + ".tmp"
- tmpFile, err := os.Create(tmpPath)
- if err != nil {
- http.Error(w, "Failed to save uploaded file", http.StatusInternalServerError)
- return
- }
- _, err = io.Copy(tmpFile, file)
- tmpFile.Close()
+ // Process the uploaded file
+ id, _, warningMsg, err := processUploadedFile(file, header.Filename)
if err != nil {
- os.Remove(tmpPath)
- http.Error(w, "Failed to save uploaded file", http.StatusInternalServerError)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
- // --- Detect video codec using ffprobe ---
- ffprobeCmd := exec.Command("ffprobe", "-v", "error", "-select_streams", "v:0",
- "-show_entries", "stream=codec_name", "-of", "default=nokey=1:noprint_wrappers=1", tmpPath)
- out, err := ffprobeCmd.Output()
- if err != nil {
- os.Remove(tmpPath)
- http.Error(w, "Failed to inspect video", http.StatusInternalServerError)
- return
- }
- videoCodec := strings.TrimSpace(string(out))
-
- // If HEVC, re-encode to H.264 and warn user
- finalFilename := header.Filename
- if videoCodec == "hevc" || videoCodec == "h265" {
- warningMessage := "The uploaded video uses HEVC and has been re-encoded to H.264 for browser compatibility."
-
- ffmpegCmd := exec.Command("ffmpeg", "-i", tmpPath, "-c:v", "libx264", "-profile:v", "baseline", "-preset", "fast", "-crf", "23", "-c:a", "aac", "-movflags", "+faststart", dstPath)
- ffmpegCmd.Stderr = os.Stderr
- ffmpegCmd.Stdout = os.Stdout
- if err := ffmpegCmd.Run(); err != nil {
- os.Remove(tmpPath)
- http.Error(w, "Failed to re-encode HEVC video", http.StatusInternalServerError)
- return
- }
- os.Remove(tmpPath)
-
- // Insert into database and redirect with warning
- res, _ := db.Exec("INSERT INTO files (filename, path) VALUES (?, ?)", finalFilename, dstPath)
- id, _ := res.LastInsertId()
- http.Redirect(w, r, fmt.Sprintf("/file/%d?warning=%s", id, url.QueryEscape(warningMessage)), http.StatusSeeOther)
- 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)
+}
- // If not HEVC, just move temp file to final destination
- err = os.Rename(tmpPath, dstPath)
- if err != nil {
- http.Error(w, "Failed to save file", http.StatusInternalServerError)
- return
+// 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 {
+ return "", "", fmt.Errorf("a file with that name already exists")
+ } else if !os.IsNotExist(err) {
+ return "", "", fmt.Errorf("failed to check for existing file: %v", err)
}
-
- // Insert into database
- res, _ := db.Exec("INSERT INTO files (filename, path) VALUES (?, ?)", finalFilename, dstPath)
- id, _ := res.LastInsertId()
- http.Redirect(w, r, fmt.Sprintf("/file/%d", id), http.StatusSeeOther)
+ return filename, finalPath, nil
}
// raw local IP for raw address
@@ -582,9 +588,7 @@ func fileRenameHandler(w http.ResponseWriter, r *http.Request, parts []string) {
}
// Sanitize filename
- newFilename = strings.ReplaceAll(newFilename, "/", "_")
- newFilename = strings.ReplaceAll(newFilename, "\\", "_")
- newFilename = strings.ReplaceAll(newFilename, "..", "_")
+ newFilename = sanitizeFilename(newFilename)
// Get current file info
var currentFile File
@@ -1245,8 +1249,8 @@ func bulkTagHandler(w http.ResponseWriter, r *http.Request) {
}
recentRows.Close()
- // Validate basic inputs
- if rangeStr == "" {
+ // Helper function to create error response
+ createErrorResponse := func(errorMsg string) {
pageData := PageData{
Title: "Bulk Tag Editor",
Data: struct {
@@ -1263,7 +1267,7 @@ func bulkTagHandler(w http.ResponseWriter, r *http.Request) {
}{
Categories: cats,
RecentFiles: recentFiles,
- Error: "File range cannot be empty",
+ Error: errorMsg,
Success: "",
FormData: struct {
FileRange string
@@ -1279,193 +1283,43 @@ func bulkTagHandler(w http.ResponseWriter, r *http.Request) {
},
}
tmpl.ExecuteTemplate(w, "bulk-tag.html", pageData)
+ }
+
+ // Validate basic inputs
+ if rangeStr == "" {
+ createErrorResponse("File range cannot be empty")
return
}
if category == "" {
- 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: "Category cannot be empty",
- 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)
+ createErrorResponse("Category cannot be empty")
return
}
// For add operations, value is required. For remove operations, value is optional
if operation == "add" && value == "" {
- 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: "Value cannot be empty when adding tags",
- 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)
+ createErrorResponse("Value cannot be empty when adding tags")
return
}
// Parse file ID range
fileIDs, err := parseFileIDRange(rangeStr)
if err != nil {
- 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: fmt.Sprintf("Invalid file range: %v", err),
- 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)
+ createErrorResponse(fmt.Sprintf("Invalid file range: %v", err))
return
}
// Validate file IDs exist
validFiles, err := validateFileIDs(fileIDs)
if err != nil {
- 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: fmt.Sprintf("File validation error: %v", err),
- 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)
+ createErrorResponse(fmt.Sprintf("File validation error: %v", err))
return
}
// Apply tag operations
err = applyBulkTagOperations(fileIDs, category, value, operation)
if err != nil {
- 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: fmt.Sprintf("Tag operation failed: %v", err),
- 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)
+ createErrorResponse(fmt.Sprintf("Tag operation failed: %v", err))
return
}