include-uploads.go (9184B)
1 package main 2 3 import ( 4 "fmt" 5 "io" 6 "log" 7 "net/http" 8 "net/url" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "strconv" 13 "strings" 14 ) 15 16 func uploadHandler(w http.ResponseWriter, r *http.Request) { 17 if r.Method == http.MethodGet { 18 pageData := buildPageData("Add File", nil) 19 renderTemplate(w, "add.html", pageData) 20 return 21 } 22 23 // Parse the multipart form (with max memory limit, e.g., 32MB) 24 err := r.ParseMultipartForm(32 << 20) 25 if err != nil { 26 renderError(w, "Failed to parse form", http.StatusBadRequest) 27 return 28 } 29 30 files := r.MultipartForm.File["file"] 31 if len(files) == 0 { 32 renderError(w, "No files uploaded", http.StatusBadRequest) 33 return 34 } 35 36 var warnings []string 37 var lastID int64 38 39 // Process each file 40 for _, fileHeader := range files { 41 file, err := fileHeader.Open() 42 if err != nil { 43 renderError(w, "Failed to open uploaded file", http.StatusInternalServerError) 44 return 45 } 46 defer file.Close() 47 48 id, warningMsg, err := processUpload(file, fileHeader.Filename) 49 if err != nil { 50 renderError(w, err.Error(), http.StatusInternalServerError) 51 return 52 } 53 54 lastID = id 55 56 if warningMsg != "" { 57 warnings = append(warnings, warningMsg) 58 } 59 } 60 61 var warningMsg string 62 if len(warnings) > 0 { 63 warningMsg = strings.Join(warnings, "; ") 64 } 65 66 redirectTarget := "/untagged" 67 if len(files) == 1 && lastID != 0 { 68 redirectTarget = "/file/" + strconv.FormatInt(lastID, 10) 69 } 70 71 redirectWithWarning(w, r, redirectTarget, warningMsg) 72 } 73 74 func processUpload(src io.Reader, filename string) (int64, string, error) { 75 finalFilename, finalPath, conflictID, err := checkFileConflictStrict(filename) 76 if err != nil { 77 return 0, "", err 78 } 79 if conflictID != 0 { 80 return conflictID, "", nil 81 } 82 83 tempPath := finalPath + ".tmp" 84 tempFile, err := os.Create(tempPath) 85 if err != nil { 86 return 0, "", fmt.Errorf("failed to create temp file: %v", err) 87 } 88 89 _, err = io.Copy(tempFile, src) 90 tempFile.Close() 91 if err != nil { 92 os.Remove(tempPath) 93 return 0, "", fmt.Errorf("failed to copy file data: %v", err) 94 } 95 96 ext := strings.ToLower(filepath.Ext(filename)) 97 videoExts := map[string]bool{ 98 ".mp4": true, ".mov": true, ".avi": true, 99 ".mkv": true, ".webm": true, ".m4v": true, 100 } 101 102 var processedPath string 103 var warningMsg string 104 105 if videoExts[ext] || ext == ".cbz" { 106 // Process videos and CBZ files 107 processedPath, warningMsg, err = processVideoFile(tempPath, finalPath) 108 if err != nil { 109 os.Remove(tempPath) 110 return 0, "", err 111 } 112 } else { 113 // Non-video, non-CBZ → just rename temp file to final 114 if err := os.Rename(tempPath, finalPath); err != nil { 115 return 0, "", fmt.Errorf("failed to move file: %v", err) 116 } 117 processedPath = finalPath 118 } 119 120 id, err := saveFileToDatabase(finalFilename, processedPath) 121 if err != nil { 122 os.Remove(processedPath) 123 return 0, "", err 124 } 125 126 return id, warningMsg, nil 127 } 128 129 func uploadFromURLHandler(w http.ResponseWriter, r *http.Request) { 130 if r.Method != http.MethodPost { 131 http.Redirect(w, r, "/upload", http.StatusSeeOther) 132 return 133 } 134 135 fileURL := r.FormValue("fileurl") 136 if fileURL == "" { 137 renderError(w, "No URL provided", http.StatusBadRequest) 138 return 139 } 140 141 customFilename := strings.TrimSpace(r.FormValue("filename")) 142 143 parsedURL, err := url.ParseRequestURI(fileURL) 144 if err != nil || !(parsedURL.Scheme == "http" || parsedURL.Scheme == "https") { 145 renderError(w, "Invalid URL", http.StatusBadRequest) 146 return 147 } 148 149 resp, err := http.Get(fileURL) 150 if err != nil || resp.StatusCode != http.StatusOK { 151 renderError(w, "Failed to download file", http.StatusBadRequest) 152 return 153 } 154 defer resp.Body.Close() 155 156 var filename string 157 urlExt := filepath.Ext(parsedURL.Path) 158 if customFilename != "" { 159 filename = customFilename 160 if filepath.Ext(filename) == "" && urlExt != "" { 161 filename += urlExt 162 } 163 } else { 164 parts := strings.Split(parsedURL.Path, "/") 165 filename = parts[len(parts)-1] 166 if filename == "" { 167 filename = "file_from_url" 168 } 169 } 170 171 id, warningMsg, err := processUpload(resp.Body, filename) 172 if err != nil { 173 renderError(w, err.Error(), http.StatusInternalServerError) 174 return 175 } 176 177 redirectWithWarning(w, r, fmt.Sprintf("/file/%d", id), warningMsg) 178 } 179 180 func ytdlpHandler(w http.ResponseWriter, r *http.Request) { 181 if r.Method != http.MethodPost { 182 http.Redirect(w, r, "/upload", http.StatusSeeOther) 183 return 184 } 185 186 videoURL := r.FormValue("url") 187 if videoURL == "" { 188 renderError(w, "No URL provided", http.StatusBadRequest) 189 return 190 } 191 192 outTemplate := filepath.Join(config.UploadDir, "%(title)s.%(ext)s") 193 filenameCmd := exec.Command("yt-dlp", "--playlist-items", "1", "-f", "mp4", "-o", outTemplate, "--get-filename", videoURL) 194 filenameBytes, err := filenameCmd.Output() 195 if err != nil { 196 renderError(w, fmt.Sprintf("Failed to get filename: %v", err), http.StatusInternalServerError) 197 return 198 } 199 expectedFullPath := strings.TrimSpace(string(filenameBytes)) 200 expectedFilename := filepath.Base(expectedFullPath) 201 202 finalFilename, finalPath, conflictID, err := checkFileConflictStrict(expectedFilename) 203 if err != nil { 204 renderError(w, err.Error(), http.StatusConflict) 205 return 206 } 207 if conflictID != 0 { 208 redirectWithWarning(w, r, fmt.Sprintf("/file/%d", conflictID), "") 209 return 210 } 211 212 downloadCmd := exec.Command("yt-dlp", "--playlist-items", "1", "-f", "mp4", "-o", outTemplate, videoURL) 213 downloadCmd.Stdout = os.Stdout 214 downloadCmd.Stderr = os.Stderr 215 if err := downloadCmd.Run(); err != nil { 216 renderError(w, fmt.Sprintf("Failed to download video: %v", err), http.StatusInternalServerError) 217 return 218 } 219 220 if expectedFullPath != finalPath { 221 if err := os.Rename(expectedFullPath, finalPath); err != nil { 222 renderError(w, fmt.Sprintf("Failed to move downloaded file: %v", err), http.StatusInternalServerError) 223 return 224 } 225 } 226 227 tempPath := finalPath + ".tmp" 228 if err := os.Rename(finalPath, tempPath); err != nil { 229 renderError(w, fmt.Sprintf("Failed to create temp file for processing: %v", err), http.StatusInternalServerError) 230 return 231 } 232 233 processedPath, warningMsg, err := processVideoFile(tempPath, finalPath) 234 if err != nil { 235 os.Remove(tempPath) 236 renderError(w, fmt.Sprintf("Failed to process video: %v", err), http.StatusInternalServerError) 237 return 238 } 239 240 id, err := saveFileToDatabase(finalFilename, processedPath) 241 if err != nil { 242 os.Remove(processedPath) 243 renderError(w, err.Error(), http.StatusInternalServerError) 244 return 245 } 246 247 redirectWithWarning(w, r, fmt.Sprintf("/file/%d", id), warningMsg) 248 } 249 250 func processVideoFile(tempPath, finalPath string) (string, string, error) { 251 ext := strings.ToLower(filepath.Ext(finalPath)) 252 253 // Handle CBZ files 254 if ext == ".cbz" { 255 if err := os.Rename(tempPath, finalPath); err != nil { 256 return "", "", fmt.Errorf("failed to move file: %v", err) 257 } 258 if err := generateCBZThumbnail(finalPath, config.UploadDir, filepath.Base(finalPath)); err != nil { 259 log.Printf("Warning: could not generate CBZ thumbnail: %v", err) 260 } 261 return finalPath, "", nil 262 } 263 264 // Handle video files 265 codec, err := detectVideoCodec(tempPath) 266 if err != nil { 267 return "", "", err 268 } 269 if codec == "hevc" || codec == "h265" { 270 warningMsg := "The video uses HEVC and has been re-encoded to H.264 for browser compatibility." 271 if err := reencodeHEVCToH264(tempPath, finalPath); err != nil { 272 return "", "", fmt.Errorf("failed to re-encode HEVC video: %v", err) 273 } 274 os.Remove(tempPath) 275 return finalPath, warningMsg, nil 276 } 277 if err := os.Rename(tempPath, finalPath); err != nil { 278 return "", "", fmt.Errorf("failed to move file: %v", err) 279 } 280 281 // Generate thumbnail for video files 282 if ext == ".mp4" || ext == ".mov" || ext == ".avi" || ext == ".mkv" || ext == ".webm" || ext == ".m4v" { 283 if err := generateThumbnail(finalPath, config.UploadDir, filepath.Base(finalPath)); err != nil { 284 log.Printf("Warning: could not generate thumbnail: %v", err) 285 } 286 } 287 288 return finalPath, "", nil 289 } 290 291 func saveFileToDatabase(filename, path string) (int64, error) { 292 res, err := db.Exec("INSERT INTO files (filename, path, description) VALUES (?, ?, '')", filename, path) 293 if err != nil { 294 return 0, fmt.Errorf("failed to save file to database: %v", err) 295 } 296 id, err := res.LastInsertId() 297 if err != nil { 298 return 0, fmt.Errorf("failed to get inserted ID: %v", err) 299 } 300 computeProperties(id, path) 301 return id, nil 302 } 303 304 305 func detectVideoCodec(filePath string) (string, error) { 306 cmd := exec.Command("ffprobe", "-v", "error", "-select_streams", "v:0", 307 "-show_entries", "stream=codec_name", "-of", "default=nokey=1:noprint_wrappers=1", filePath) 308 out, err := cmd.Output() 309 if err != nil { 310 return "", fmt.Errorf("failed to probe video codec: %v", err) 311 } 312 return strings.TrimSpace(string(out)), nil 313 } 314 315 func reencodeHEVCToH264(inputPath, outputPath string) error { 316 cmd := exec.Command("ffmpeg", "-i", inputPath, 317 "-c:v", "libx264", "-profile:v", "baseline", "-preset", "fast", "-crf", "23", 318 "-c:a", "aac", "-movflags", "+faststart", outputPath) 319 cmd.Stderr = os.Stderr 320 cmd.Stdout = os.Stdout 321 return cmd.Run() 322 }