mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
This is necessary to provide external services with temporary access to specific media files without giving them permanent access, a regular download, or an access token. Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
14
internal/api/download/cache.go
Normal file
14
internal/api/download/cache.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
gc "github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
var cache = gc.New(time.Minute*15, 5*time.Minute)
|
||||
|
||||
// Flush resets the download cache.
|
||||
func Flush() {
|
||||
cache.Flush()
|
||||
}
|
||||
31
internal/api/download/download.go
Normal file
31
internal/api/download/download.go
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
Package download allows to register files so that they can be temporarily made available for download.
|
||||
|
||||
Copyright (c) 2018 - 2025 PhotoPrism UG. All rights reserved.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under Version 3 of the GNU Affero General Public License (the "AGPL"):
|
||||
<https://docs.photoprism.app/license/agpl>
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
The AGPL is supplemented by our Trademark and Brand Guidelines,
|
||||
which describe how our Brand Assets may be used:
|
||||
<https://www.photoprism.app/trademark>
|
||||
|
||||
Feel free to send an email to hello@photoprism.app if you have questions,
|
||||
want to support our work, or just want to say hello.
|
||||
|
||||
Additional information can be found in our Developer Guide:
|
||||
<https://docs.photoprism.app/developer-guide/>
|
||||
*/
|
||||
package download
|
||||
|
||||
import (
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
)
|
||||
|
||||
var log = event.Log
|
||||
24
internal/api/download/find.go
Normal file
24
internal/api/download/find.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
// Find returns the fileName for the given download id or an error if the id is invalid.
|
||||
func Find(uniqueId string) (fileName string, err error) {
|
||||
|
||||
if uniqueId == "" || !rnd.IsUUID(uniqueId) {
|
||||
return fileName, fmt.Errorf("id has an invalid format")
|
||||
}
|
||||
|
||||
// Cached?
|
||||
if cacheData, hit := cache.Get(uniqueId); hit {
|
||||
log.Tracef("download: cache hit for %s", uniqueId)
|
||||
return cacheData.(string), nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("invalid id %s", clean.LogQuote(uniqueId))
|
||||
}
|
||||
22
internal/api/download/register.go
Normal file
22
internal/api/download/register.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
// Register makes the specified file available for download with the
|
||||
// returned id until the cache expires, or the server is restarted.
|
||||
func Register(fileName string) (string, error) {
|
||||
if !fs.FileExists(fileName) {
|
||||
return "", fmt.Errorf("%s does not exists", clean.Log(fileName))
|
||||
}
|
||||
|
||||
uniqueId := rnd.UUID()
|
||||
cache.SetDefault(uniqueId, fileName)
|
||||
|
||||
return uniqueId, nil
|
||||
}
|
||||
37
internal/api/download/register_test.go
Normal file
37
internal/api/download/register_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
fileName := fs.Abs("./testdata/image.jpg")
|
||||
uniqueId, err := Register(fileName)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, rnd.IsUUID(uniqueId))
|
||||
|
||||
findName, findErr := Find(uniqueId)
|
||||
|
||||
assert.NoError(t, findErr)
|
||||
assert.Equal(t, fileName, findName)
|
||||
|
||||
Flush()
|
||||
|
||||
findName, findErr = Find(uniqueId)
|
||||
|
||||
assert.Error(t, findErr)
|
||||
assert.Equal(t, "", findName)
|
||||
})
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
fileName := fs.Abs("./testdata/invalid.jpg")
|
||||
uniqueId, err := Register(fileName)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "", uniqueId)
|
||||
})
|
||||
}
|
||||
BIN
internal/api/download/testdata/image.jpg
vendored
Normal file
BIN
internal/api/download/testdata/image.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -2,17 +2,18 @@ package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config/customize"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/api/download"
|
||||
"github.com/photoprism/photoprism/internal/config/customize"
|
||||
"github.com/photoprism/photoprism/internal/entity/query"
|
||||
"github.com/photoprism/photoprism/internal/photoprism"
|
||||
"github.com/photoprism/photoprism/internal/photoprism/get"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
// TODO: GET /api/v1/dl/file/:hash
|
||||
@@ -41,18 +42,35 @@ func DownloadName(c *gin.Context) customize.DownloadName {
|
||||
// @Produce application/octet-stream
|
||||
// @Failure 403,404 {file} image/svg+xml
|
||||
// @Success 200 {file} application/octet-stream
|
||||
// @Param hash path string true "File Hash"
|
||||
// @Router /api/v1/dl/{hash} [get]
|
||||
// @Param file path string true "file hash or unique download id"
|
||||
// @Router /api/v1/dl/{file} [get]
|
||||
func GetDownload(router *gin.RouterGroup) {
|
||||
router.GET("/dl/:hash", func(c *gin.Context) {
|
||||
router.GET("/dl/:file", func(c *gin.Context) {
|
||||
id := clean.Token(c.Param("file"))
|
||||
|
||||
// Check for temporary download if the file is identified by a UUID string.
|
||||
if rnd.IsUUID(id) {
|
||||
fileName, fileErr := download.Find(id)
|
||||
|
||||
if fileErr != nil {
|
||||
AbortForbidden(c)
|
||||
return
|
||||
} else if !fs.FileExists(fileName) {
|
||||
AbortNotFound(c)
|
||||
return
|
||||
}
|
||||
|
||||
c.FileAttachment(fileName, filepath.Base(fileName))
|
||||
return
|
||||
}
|
||||
|
||||
// If the file is identified by its hash, a valid download token is required.
|
||||
if InvalidDownloadToken(c) {
|
||||
c.Data(http.StatusForbidden, "image/svg+xml", brokenIconSvg)
|
||||
return
|
||||
}
|
||||
|
||||
fileHash := clean.Token(c.Param("hash"))
|
||||
|
||||
f, err := query.FileByHash(fileHash)
|
||||
f, err := query.FileByHash(id)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(404, gin.H{"error": err.Error()})
|
||||
|
||||
@@ -44,7 +44,7 @@ func FolderCover(router *gin.RouterGroup) {
|
||||
conf := get.Config()
|
||||
uid := c.Param("uid")
|
||||
thumbName := thumb.Name(clean.Token(c.Param("size")))
|
||||
download := c.Query("download") != ""
|
||||
attachment := c.Query("download") != ""
|
||||
|
||||
size, ok := thumb.Sizes[thumbName]
|
||||
|
||||
@@ -80,7 +80,7 @@ func FolderCover(router *gin.RouterGroup) {
|
||||
|
||||
AddCoverCacheHeader(c)
|
||||
|
||||
if download {
|
||||
if attachment {
|
||||
c.FileAttachment(cached.FileName, cached.ShareName)
|
||||
} else {
|
||||
c.File(cached.FileName)
|
||||
@@ -110,7 +110,7 @@ func FolderCover(router *gin.RouterGroup) {
|
||||
}
|
||||
|
||||
// Use original file if thumb size exceeds limit, see https://github.com/photoprism/photoprism/issues/157
|
||||
if size.ExceedsLimit() && !download {
|
||||
if size.ExceedsLimit() && !attachment {
|
||||
log.Debugf("%s: using original, size exceeds limit (width %d, height %d)", folderCover, size.Width, size.Height)
|
||||
AddCoverCacheHeader(c)
|
||||
c.File(fileName)
|
||||
@@ -140,7 +140,7 @@ func FolderCover(router *gin.RouterGroup) {
|
||||
|
||||
AddCoverCacheHeader(c)
|
||||
|
||||
if download {
|
||||
if attachment {
|
||||
c.FileAttachment(thumbnail, f.DownloadName(DownloadName(c), 0))
|
||||
} else {
|
||||
c.File(thumbnail)
|
||||
|
||||
@@ -42,7 +42,7 @@ func GetThumb(router *gin.RouterGroup) {
|
||||
|
||||
start := time.Now()
|
||||
conf := get.Config()
|
||||
download := c.Query("download") != ""
|
||||
attachment := c.Query("download") != ""
|
||||
fileHash, cropArea := crop.ParseThumb(clean.Token(c.Param("thumb")))
|
||||
|
||||
// Is cropped thumbnail?
|
||||
@@ -72,7 +72,7 @@ func GetThumb(router *gin.RouterGroup) {
|
||||
// Add HTTP cache header.
|
||||
AddImmutableCacheHeader(c)
|
||||
|
||||
if download {
|
||||
if attachment {
|
||||
c.FileAttachment(fileName, cropName.Jpeg())
|
||||
} else {
|
||||
c.File(fileName)
|
||||
@@ -118,7 +118,7 @@ func GetThumb(router *gin.RouterGroup) {
|
||||
// Add HTTP cache header.
|
||||
AddImmutableCacheHeader(c)
|
||||
|
||||
if download {
|
||||
if attachment {
|
||||
c.FileAttachment(cached.FileName, cached.ShareName)
|
||||
} else {
|
||||
c.File(cached.FileName)
|
||||
@@ -128,7 +128,7 @@ func GetThumb(router *gin.RouterGroup) {
|
||||
}
|
||||
|
||||
// Return existing thumbs straight away.
|
||||
if !download {
|
||||
if !attachment {
|
||||
if fileName, err := size.ResolvedName(fileHash, conf.ThumbCachePath()); err == nil {
|
||||
// Add HTTP cache header.
|
||||
AddImmutableCacheHeader(c)
|
||||
@@ -188,7 +188,7 @@ func GetThumb(router *gin.RouterGroup) {
|
||||
}
|
||||
|
||||
// Use original file if thumb size exceeds limit, see https://github.com/photoprism/photoprism/issues/157
|
||||
if size.ExceedsLimit() && !download {
|
||||
if size.ExceedsLimit() && !attachment {
|
||||
log.Debugf("%s: using original, size exceeds limit (width %d, height %d)", logPrefix, size.Width, size.Height)
|
||||
|
||||
// Add HTTP cache header.
|
||||
@@ -228,7 +228,7 @@ func GetThumb(router *gin.RouterGroup) {
|
||||
AddImmutableCacheHeader(c)
|
||||
|
||||
// Return requested content.
|
||||
if download {
|
||||
if attachment {
|
||||
c.FileAttachment(thumbName, f.DownloadName(DownloadName(c), 0))
|
||||
} else {
|
||||
c.File(thumbName)
|
||||
|
||||
Reference in New Issue
Block a user