282 lines
6.5 KiB
Go
282 lines
6.5 KiB
Go
package server
|
|
|
|
import (
|
|
"archive/zip"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"sync"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"warpbox/lib/boxstore"
|
|
"warpbox/lib/helpers"
|
|
"warpbox/lib/models"
|
|
)
|
|
|
|
var oneTimeDownloadLocks sync.Map
|
|
|
|
func (app *App) handleDownloadBox(ctx *gin.Context) {
|
|
boxID := ctx.Param("id")
|
|
if !boxstore.ValidBoxID(boxID) {
|
|
ctx.String(http.StatusBadRequest, "Invalid box id")
|
|
return
|
|
}
|
|
|
|
if !app.config.ZipDownloadsEnabled {
|
|
ctx.String(http.StatusForbidden, "Zip downloads are disabled")
|
|
return
|
|
}
|
|
|
|
manifest, hasManifest, ok := app.authorizeBoxRequest(ctx, boxID, true)
|
|
if !ok {
|
|
return
|
|
}
|
|
if hasManifest && manifest.OneTimeDownload {
|
|
app.handleOneTimeDownloadBox(ctx, boxID)
|
|
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
|
|
}
|
|
if !app.writeBoxZip(ctx, boxID, files) {
|
|
return
|
|
}
|
|
if hasManifest && app.config.RenewOnDownloadEnabled {
|
|
boxstore.RenewManifest(boxID, manifest.RetentionSecs)
|
|
}
|
|
}
|
|
|
|
func (app *App) handleOneTimeDownloadBox(ctx *gin.Context, boxID string) {
|
|
lock := oneTimeDownloadLock(boxID)
|
|
lock.Lock()
|
|
defer lock.Unlock()
|
|
defer oneTimeDownloadLocks.Delete(boxID)
|
|
|
|
manifest, hasManifest, ok := app.authorizeBoxRequest(ctx, boxID, true)
|
|
if !ok {
|
|
return
|
|
}
|
|
if !hasManifest || !manifest.OneTimeDownload || manifest.Consumed {
|
|
ctx.String(http.StatusGone, "Box already consumed")
|
|
return
|
|
}
|
|
|
|
files, err := boxstore.ListFiles(boxID)
|
|
if err != nil {
|
|
ctx.String(http.StatusNotFound, "Box not found")
|
|
return
|
|
}
|
|
if !allFilesComplete(files) {
|
|
ctx.String(http.StatusConflict, "Box is not ready yet")
|
|
return
|
|
}
|
|
|
|
if app.config.OneTimeDownloadRetryOnFailure {
|
|
app.handleRetryableOneTimeZip(ctx, boxID, manifest, files)
|
|
return
|
|
}
|
|
|
|
manifest.Consumed = true
|
|
if err := boxstore.WriteManifest(boxID, manifest); err != nil {
|
|
ctx.String(http.StatusInternalServerError, "Could not mark box as consumed")
|
|
return
|
|
}
|
|
if !app.writeBoxZip(ctx, boxID, files) {
|
|
boxstore.DeleteBox(boxID)
|
|
return
|
|
}
|
|
boxstore.DeleteBox(boxID)
|
|
}
|
|
|
|
func (app *App) writeBoxZip(ctx *gin.Context, boxID string, files []models.BoxFile) bool {
|
|
writeBoxZipHeaders(ctx, boxID)
|
|
if err := writeBoxZipTo(ctx.Writer, boxID, files); err != nil {
|
|
ctx.Status(http.StatusInternalServerError)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (app *App) handleRetryableOneTimeZip(ctx *gin.Context, boxID string, manifest models.BoxManifest, files []models.BoxFile) {
|
|
tempZip, err := os.CreateTemp("", "warpbox-"+boxID+"-*.zip")
|
|
if err != nil {
|
|
ctx.String(http.StatusInternalServerError, "Could not prepare ZIP download")
|
|
return
|
|
}
|
|
tempPath := tempZip.Name()
|
|
defer os.Remove(tempPath)
|
|
|
|
if err := writeBoxZipTo(tempZip, boxID, files); err != nil {
|
|
tempZip.Close()
|
|
ctx.String(http.StatusInternalServerError, "Could not build ZIP download")
|
|
return
|
|
}
|
|
if _, err := tempZip.Seek(0, 0); err != nil {
|
|
tempZip.Close()
|
|
ctx.String(http.StatusInternalServerError, "Could not read ZIP download")
|
|
return
|
|
}
|
|
|
|
writeBoxZipHeaders(ctx, boxID)
|
|
if _, err := io.Copy(ctx.Writer, tempZip); err != nil {
|
|
tempZip.Close()
|
|
return
|
|
}
|
|
if err := tempZip.Close(); err != nil {
|
|
return
|
|
}
|
|
|
|
manifest.Consumed = true
|
|
if err := boxstore.WriteManifest(boxID, manifest); err != nil {
|
|
return
|
|
}
|
|
boxstore.DeleteBox(boxID)
|
|
}
|
|
|
|
func writeBoxZipHeaders(ctx *gin.Context, boxID string) {
|
|
ctx.Header("Content-Type", "application/zip")
|
|
ctx.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="warpbox-%s.zip"`, boxID))
|
|
}
|
|
|
|
func writeBoxZipTo(destination io.Writer, boxID string, files []models.BoxFile) error {
|
|
zipWriter := zip.NewWriter(destination)
|
|
|
|
for _, file := range files {
|
|
if !file.IsComplete {
|
|
continue
|
|
}
|
|
if err := boxstore.AddFileToZip(zipWriter, boxID, file.Name); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := zipWriter.Close(); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func oneTimeDownloadLock(boxID string) *sync.Mutex {
|
|
lock, _ := oneTimeDownloadLocks.LoadOrStore(boxID, &sync.Mutex{})
|
|
return lock.(*sync.Mutex)
|
|
}
|
|
|
|
func allFilesComplete(files []models.BoxFile) bool {
|
|
if len(files) == 0 {
|
|
return false
|
|
}
|
|
|
|
for _, file := range files {
|
|
if !file.IsComplete {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func manifestFilesReady(files []models.BoxFile) bool {
|
|
if len(files) == 0 {
|
|
return false
|
|
}
|
|
for _, file := range files {
|
|
if file.Status != models.FileStatusReady {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func stripOneTimeThumbnailState(files []models.BoxFile) []models.BoxFile {
|
|
stripped := make([]models.BoxFile, 0, len(files))
|
|
for _, file := range files {
|
|
file.ThumbnailPath = nil
|
|
file.ThumbnailURL = ""
|
|
if file.ThumbnailStatus == "" {
|
|
file.ThumbnailStatus = models.ThumbnailStatusUnsupported
|
|
}
|
|
stripped = append(stripped, file)
|
|
}
|
|
return stripped
|
|
}
|
|
|
|
func (app *App) 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
|
|
}
|
|
|
|
manifest, hasManifest, authorized := app.authorizeBoxRequest(ctx, boxID, true)
|
|
if !authorized {
|
|
return
|
|
}
|
|
if hasManifest && manifest.OneTimeDownload {
|
|
ctx.String(http.StatusForbidden, "Individual downloads disabled for this box")
|
|
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
|
|
}
|
|
if !boxstore.IsSafeRegularBoxFile(boxID, filename) {
|
|
ctx.String(http.StatusBadRequest, "Invalid file")
|
|
return
|
|
}
|
|
|
|
ctx.FileAttachment(path, filename)
|
|
if hasManifest && app.config.RenewOnDownloadEnabled {
|
|
boxstore.RenewManifest(boxID, manifest.RetentionSecs)
|
|
}
|
|
}
|
|
|
|
func (app *App) handleDownloadThumbnail(ctx *gin.Context) {
|
|
boxID := ctx.Param("id")
|
|
fileID := ctx.Param("file_id")
|
|
if !boxstore.ValidBoxID(boxID) {
|
|
ctx.String(http.StatusBadRequest, "Invalid box id")
|
|
return
|
|
}
|
|
|
|
manifest, hasManifest, authorized := app.authorizeBoxRequest(ctx, boxID, true)
|
|
if !authorized {
|
|
return
|
|
}
|
|
if hasManifest && manifest.OneTimeDownload {
|
|
ctx.String(http.StatusForbidden, "Thumbnails disabled for one-time boxes")
|
|
return
|
|
}
|
|
|
|
path, ok := boxstore.ThumbnailFilePath(boxID, fileID)
|
|
if !ok {
|
|
ctx.String(http.StatusBadRequest, "Invalid thumbnail")
|
|
return
|
|
}
|
|
|
|
if _, err := os.Stat(path); err != nil {
|
|
ctx.String(http.StatusNotFound, "Thumbnail not found")
|
|
return
|
|
}
|
|
|
|
ctx.Header("Content-Type", "image/jpeg")
|
|
ctx.File(path)
|
|
}
|