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