package server import ( "archive/zip" "fmt" "io" "net/http" "os" "time" "github.com/gin-gonic/gin" "warpbox/lib/boxstore" "warpbox/lib/helpers" "warpbox/lib/models" ) const boxAuthCookiePrefix = "warpbox_box_" func handleIndex(ctx *gin.Context) { ctx.HTML(http.StatusOK, "index.html", gin.H{ "RetentionOptions": boxstore.RetentionOptions(), "DefaultRetention": boxstore.DefaultRetentionOption().Key, }) } func handleShowBox(ctx *gin.Context) { boxID := ctx.Param("id") if !boxstore.ValidBoxID(boxID) { ctx.String(http.StatusBadRequest, "Invalid box id") return } manifest, hasManifest, ok := authorizeBoxRequest(ctx, boxID, true) if !ok { return } files, err := boxstore.ListFiles(boxID) if err != nil { ctx.String(http.StatusNotFound, "Box not found") return } downloadAll := "/box/" + boxID + "/download" if hasManifest && manifest.DisableZip { downloadAll = "" } ctx.HTML(http.StatusOK, "box.html", gin.H{ "BoxID": boxID, "Files": files, "FileCount": len(files), "DownloadAll": downloadAll, "PollMS": helpers.EnvInt("WARPBOX_BOX_POLL_INTERVAL_MS", 5000, 1000), "RetentionLabel": manifest.RetentionLabel, "ExpiresAt": manifest.ExpiresAt, }) } func handleBoxLogin(ctx *gin.Context) { boxID := ctx.Param("id") if !boxstore.ValidBoxID(boxID) { ctx.String(http.StatusBadRequest, "Invalid box id") return } manifest, err := boxstore.ReadManifest(boxID) if err != nil { ctx.String(http.StatusNotFound, "Box not found") return } if boxstore.IsExpired(manifest) { boxstore.DeleteBox(boxID) ctx.String(http.StatusGone, "Box expired") return } if !boxstore.IsPasswordProtected(manifest) || isBoxAuthorized(ctx, boxID, manifest) { ctx.Redirect(http.StatusSeeOther, "/box/"+boxID) return } renderBoxLogin(ctx, boxID, "") } func handleBoxLoginPost(ctx *gin.Context) { boxID := ctx.Param("id") if !boxstore.ValidBoxID(boxID) { ctx.String(http.StatusBadRequest, "Invalid box id") return } manifest, err := boxstore.ReadManifest(boxID) if err != nil { ctx.String(http.StatusNotFound, "Box not found") return } if boxstore.IsExpired(manifest) { boxstore.DeleteBox(boxID) ctx.String(http.StatusGone, "Box expired") return } if !boxstore.VerifyPassword(manifest, ctx.PostForm("password")) { renderBoxLogin(ctx, boxID, "The password was not accepted.") return } maxAge := 24 * 60 * 60 if !manifest.ExpiresAt.IsZero() { seconds := int(time.Until(manifest.ExpiresAt).Seconds()) if seconds > 0 { maxAge = seconds } } ctx.SetCookie(boxAuthCookieName(boxID), manifest.AuthToken, maxAge, "/box/"+boxID, "", false, true) ctx.Redirect(http.StatusSeeOther, "/box/"+boxID) } func handleBoxStatus(ctx *gin.Context) { boxID := ctx.Param("id") if !boxstore.ValidBoxID(boxID) { ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid box id"}) return } if _, _, ok := authorizeBoxRequest(ctx, boxID, false); !ok { return } files, err := boxstore.ListFiles(boxID) if err != nil { ctx.JSON(http.StatusNotFound, gin.H{"error": "Box not found"}) return } ctx.JSON(http.StatusOK, gin.H{"box_id": boxID, "files": files}) } func handleDownloadBox(ctx *gin.Context) { boxID := ctx.Param("id") if !boxstore.ValidBoxID(boxID) { ctx.String(http.StatusBadRequest, "Invalid box id") return } manifest, hasManifest, ok := authorizeBoxRequest(ctx, boxID, true) if !ok { return } if hasManifest && manifest.DisableZip { ctx.String(http.StatusForbidden, "Zip download disabled for this box") return } files, err := boxstore.ListFiles(boxID) if err != nil { ctx.String(http.StatusNotFound, "Box not found") return } ctx.Header("Content-Type", "application/zip") ctx.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="warpbox-%s.zip"`, boxID)) zipWriter := zip.NewWriter(ctx.Writer) defer zipWriter.Close() for _, file := range files { if !file.IsComplete { continue } if err := boxstore.AddFileToZip(zipWriter, boxID, file.Name); err != nil { ctx.Status(http.StatusInternalServerError) return } } } func handleDownloadFile(ctx *gin.Context) { boxID := ctx.Param("id") filename, ok := helpers.SafeFilename(ctx.Param("filename")) if !boxstore.ValidBoxID(boxID) || !ok { ctx.String(http.StatusBadRequest, "Invalid file") return } if _, _, authorized := authorizeBoxRequest(ctx, boxID, true); !authorized { return } path, ok := boxstore.SafeBoxFilePath(boxID, filename) if !ok { ctx.String(http.StatusBadRequest, "Invalid file") return } if _, err := os.Stat(path); err != nil { ctx.String(http.StatusNotFound, "File not found") return } ctx.FileAttachment(path, filename) } func handleCreateBox(ctx *gin.Context) { boxID, err := boxstore.NewBoxID() if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Could not create upload box"}) return } if err := os.MkdirAll(boxstore.BoxPath(boxID), 0755); err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Could not prepare upload box"}) return } var request models.CreateBoxRequest if err := ctx.ShouldBindJSON(&request); err != nil && err != io.EOF { ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid box payload"}) return } files, err := boxstore.CreateManifest(boxID, request) if err != nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } ctx.JSON(http.StatusOK, gin.H{"box_id": boxID, "box_url": "/box/" + boxID, "files": files}) } func handleManifestFileUpload(ctx *gin.Context) { boxID := ctx.Param("id") fileID := ctx.Param("file_id") if !boxstore.ValidBoxID(boxID) { ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid box id"}) return } file, err := ctx.FormFile("file") if err != nil { boxstore.MarkFileStatus(boxID, fileID, models.FileStatusFailed) ctx.JSON(http.StatusBadRequest, gin.H{"error": "No file received"}) return } savedFile, err := boxstore.SaveManifestUpload(boxID, fileID, file) if err != nil { boxstore.MarkFileStatus(boxID, fileID, models.FileStatusFailed) ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } ctx.JSON(http.StatusOK, gin.H{"box_id": boxID, "box_url": "/box/" + boxID, "file": savedFile}) } func handleFileStatusUpdate(ctx *gin.Context) { boxID := ctx.Param("id") fileID := ctx.Param("file_id") if !boxstore.ValidBoxID(boxID) { ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid box id"}) return } var request models.UpdateFileStatusRequest if err := ctx.ShouldBindJSON(&request); err != nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid status payload"}) return } file, err := boxstore.MarkFileStatus(boxID, fileID, request.Status) if err != nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } ctx.JSON(http.StatusOK, gin.H{"file": file}) } func handleDirectBoxUpload(ctx *gin.Context) { boxID := ctx.Param("id") if !boxstore.ValidBoxID(boxID) { ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid box id"}) return } file, err := ctx.FormFile("file") if err != nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": "No file received"}) return } savedFile, err := boxstore.SaveUpload(boxID, file) if err != nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } ctx.JSON(http.StatusOK, gin.H{"box_id": boxID, "box_url": "/box/" + boxID, "file": savedFile}) } func handleLegacyUpload(ctx *gin.Context) { form, err := ctx.MultipartForm() if err != nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": "No files received"}) return } files := form.File["files"] if len(files) == 0 { ctx.JSON(http.StatusBadRequest, gin.H{"error": "No files received"}) return } boxID, err := boxstore.NewBoxID() if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Could not create upload box"}) return } if err := os.MkdirAll(boxstore.BoxPath(boxID), 0755); err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Could not prepare upload box"}) return } savedFiles := make([]models.BoxFile, 0, len(files)) for _, file := range files { savedFile, err := boxstore.SaveUpload(boxID, file) if err != nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } savedFiles = append(savedFiles, savedFile) } ctx.JSON(http.StatusOK, gin.H{"box_id": boxID, "box_url": "/box/" + boxID, "files": savedFiles}) } func authorizeBoxRequest(ctx *gin.Context, boxID string, wantsHTML bool) (models.BoxManifest, bool, bool) { manifest, err := boxstore.ReadManifest(boxID) if err != nil { return models.BoxManifest{}, false, true } if boxstore.IsExpired(manifest) { boxstore.DeleteBox(boxID) if wantsHTML { ctx.String(http.StatusGone, "Box expired") } else { ctx.JSON(http.StatusGone, gin.H{"error": "Box expired"}) } return manifest, true, false } if boxstore.IsPasswordProtected(manifest) && !isBoxAuthorized(ctx, boxID, manifest) { if wantsHTML { ctx.Redirect(http.StatusSeeOther, "/box/"+boxID+"/login") } else { ctx.JSON(http.StatusUnauthorized, gin.H{"error": "Password required"}) } return manifest, true, false } return manifest, true, true } func isBoxAuthorized(ctx *gin.Context, boxID string, manifest models.BoxManifest) bool { token, err := ctx.Cookie(boxAuthCookieName(boxID)) return err == nil && boxstore.VerifyAuthToken(manifest, token) } func boxAuthCookieName(boxID string) string { return boxAuthCookiePrefix + boxID } func renderBoxLogin(ctx *gin.Context, boxID string, errorMessage string) { ctx.HTML(http.StatusOK, "box_login.html", gin.H{ "BoxID": boxID, "BoxUser": "WarpBox\\" + boxID, "ErrorMessage": errorMessage, }) }