feat: Add Redis upload cache for multi-replica deployments (#5724)
This commit is contained in:
10
cmd/root.go
10
cmd/root.go
@@ -46,6 +46,7 @@ var (
|
||||
"disable-type-detection-by-header": "disableTypeDetectionByHeader",
|
||||
"img-processors": "imageProcessors",
|
||||
"cache-dir": "cacheDir",
|
||||
"redis-cache-url": "redisCacheUrl",
|
||||
"token-expiration-time": "tokenExpirationTime",
|
||||
"baseurl": "baseURL",
|
||||
}
|
||||
@@ -88,6 +89,7 @@ func init() {
|
||||
flags.String("password", "", "hashed password for the first user when using quick setup")
|
||||
flags.Uint32("socketPerm", 0666, "unix socket file permissions")
|
||||
flags.String("cacheDir", "", "file cache directory (disabled if empty)")
|
||||
flags.String("redisCacheUrl", "", "redis cache URL (for multi-instance deployments), e.g. redis://user:pass@host:port")
|
||||
flags.Int("imageProcessors", 4, "image processors count")
|
||||
addServerFlags(flags)
|
||||
}
|
||||
@@ -176,6 +178,12 @@ user created with the credentials from options "username" and "password".`,
|
||||
fileCache = diskcache.New(afero.NewOsFs(), cacheDir)
|
||||
}
|
||||
|
||||
redisCacheURL := v.GetString("redisCacheUrl")
|
||||
uploadCache, err := fbhttp.NewUploadCache(redisCacheURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize upload cache: %w", err)
|
||||
}
|
||||
|
||||
server, err := getServerSettings(v, st.Storage)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -227,7 +235,7 @@ user created with the credentials from options "username" and "password".`,
|
||||
panic(err)
|
||||
}
|
||||
|
||||
handler, err := fbhttp.NewHandler(imageService, fileCache, st.Storage, server, assetsFs)
|
||||
handler, err := fbhttp.NewHandler(imageService, fileCache, uploadCache, st.Storage, server, assetsFs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
17
compose.redis.yaml
Normal file
17
compose.redis.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
# Run using:
|
||||
# docker compose -f compose.yaml -f compose.redis.yaml up --build
|
||||
|
||||
services:
|
||||
redis:
|
||||
container_name: redis
|
||||
image: redis:latest
|
||||
networks:
|
||||
- filebrowser
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
cat > /tmp/users.acl <<EOF
|
||||
user default on >filebrowser ~* +@all
|
||||
EOF
|
||||
redis-server --aclfile /tmp/users.acl
|
||||
37
compose.yaml
Normal file
37
compose.yaml
Normal file
@@ -0,0 +1,37 @@
|
||||
# Run using:
|
||||
# docker compose up --build
|
||||
|
||||
services:
|
||||
filebrowser:
|
||||
container_name: filebrowser
|
||||
build:
|
||||
dockerfile: Dockerfile
|
||||
context: .
|
||||
networks:
|
||||
- filebrowser
|
||||
ports:
|
||||
- 8000:80
|
||||
volumes:
|
||||
- filebrowser:/flux/vault
|
||||
environment:
|
||||
- REDIS_CACHE_URL=redis://default:filebrowser@redis:6379 # Use rediss:// for ssl
|
||||
|
||||
redis:
|
||||
container_name: redis
|
||||
image: redis:latest
|
||||
networks:
|
||||
- filebrowser
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
cat > /tmp/users.acl <<EOF
|
||||
user default on >filebrowser ~* +@all
|
||||
EOF
|
||||
redis-server --aclfile /tmp/users.acl
|
||||
|
||||
networks:
|
||||
filebrowser:
|
||||
|
||||
volumes:
|
||||
filebrowser:
|
||||
3
go.mod
3
go.mod
@@ -16,6 +16,7 @@ require (
|
||||
github.com/marusama/semaphore/v2 v2.5.0
|
||||
github.com/mholt/archives v0.1.5
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/redis/go-redis/v9 v9.17.2
|
||||
github.com/samber/lo v1.52.0
|
||||
github.com/shirou/gopsutil/v4 v4.26.1
|
||||
github.com/spf13/afero v1.15.0
|
||||
@@ -39,8 +40,10 @@ require (
|
||||
github.com/bodgit/plumbing v1.3.0 // indirect
|
||||
github.com/bodgit/sevenzip v1.6.1 // indirect
|
||||
github.com/bodgit/windows v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
||||
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 // indirect
|
||||
|
||||
10
go.sum
10
go.sum
@@ -42,7 +42,13 @@ github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4
|
||||
github.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8=
|
||||
github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
|
||||
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
@@ -53,6 +59,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
|
||||
@@ -194,6 +202,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
|
||||
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
|
||||
@@ -19,6 +19,7 @@ type modifyRequest struct {
|
||||
func NewHandler(
|
||||
imgSvc ImgService,
|
||||
fileCache FileCache,
|
||||
uploadCache UploadCache,
|
||||
store *storage.Storage,
|
||||
server *settings.Server,
|
||||
assetsFs fs.FS,
|
||||
@@ -67,10 +68,10 @@ func NewHandler(
|
||||
api.PathPrefix("/resources").Handler(monkey(resourcePutHandler, "/api/resources")).Methods("PUT")
|
||||
api.PathPrefix("/resources").Handler(monkey(resourcePatchHandler(fileCache), "/api/resources")).Methods("PATCH")
|
||||
|
||||
api.PathPrefix("/tus").Handler(monkey(tusPostHandler(), "/api/tus")).Methods("POST")
|
||||
api.PathPrefix("/tus").Handler(monkey(tusHeadHandler(), "/api/tus")).Methods("HEAD", "GET")
|
||||
api.PathPrefix("/tus").Handler(monkey(tusPatchHandler(), "/api/tus")).Methods("PATCH")
|
||||
api.PathPrefix("/tus").Handler(monkey(tusDeleteHandler(), "/api/tus")).Methods("DELETE")
|
||||
api.PathPrefix("/tus").Handler(monkey(tusPostHandler(uploadCache), "/api/tus")).Methods("POST")
|
||||
api.PathPrefix("/tus").Handler(monkey(tusHeadHandler(uploadCache), "/api/tus")).Methods("HEAD", "GET")
|
||||
api.PathPrefix("/tus").Handler(monkey(tusPatchHandler(uploadCache), "/api/tus")).Methods("PATCH")
|
||||
api.PathPrefix("/tus").Handler(monkey(tusDeleteHandler(uploadCache), "/api/tus")).Methods("DELETE")
|
||||
|
||||
api.PathPrefix("/usage").Handler(monkey(diskUsage, "/api/usage")).Methods("GET")
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package fbhttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -12,48 +11,13 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/jellydator/ttlcache/v3"
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/files"
|
||||
)
|
||||
|
||||
const maxUploadWait = 3 * time.Minute
|
||||
|
||||
// Tracks active uploads along with their respective upload lengths
|
||||
var activeUploads = initActiveUploads()
|
||||
|
||||
func initActiveUploads() *ttlcache.Cache[string, int64] {
|
||||
cache := ttlcache.New[string, int64]()
|
||||
cache.OnEviction(func(_ context.Context, reason ttlcache.EvictionReason, item *ttlcache.Item[string, int64]) {
|
||||
if reason == ttlcache.EvictionReasonExpired {
|
||||
fmt.Printf("deleting incomplete upload file: \"%s\"", item.Key())
|
||||
os.Remove(item.Key())
|
||||
}
|
||||
})
|
||||
go cache.Start()
|
||||
|
||||
return cache
|
||||
}
|
||||
|
||||
func registerUpload(filePath string, fileSize int64) {
|
||||
activeUploads.Set(filePath, fileSize, maxUploadWait)
|
||||
}
|
||||
|
||||
func completeUpload(filePath string) {
|
||||
activeUploads.Delete(filePath)
|
||||
}
|
||||
|
||||
func getActiveUploadLength(filePath string) (int64, error) {
|
||||
item := activeUploads.Get(filePath)
|
||||
if item == nil {
|
||||
return 0, fmt.Errorf("no active upload found for the given path")
|
||||
}
|
||||
|
||||
return item.Value(), nil
|
||||
}
|
||||
|
||||
func keepUploadActive(filePath string) func() {
|
||||
// keepUploadActive periodically touches the cache entry to prevent eviction during transfer
|
||||
func keepUploadActive(cache UploadCache, filePath string) func() {
|
||||
stop := make(chan bool)
|
||||
|
||||
go func() {
|
||||
@@ -65,7 +29,7 @@ func keepUploadActive(filePath string) func() {
|
||||
case <-stop:
|
||||
return
|
||||
case <-ticker.C:
|
||||
activeUploads.Touch(filePath)
|
||||
cache.Touch(filePath)
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -75,7 +39,7 @@ func keepUploadActive(filePath string) func() {
|
||||
}
|
||||
}
|
||||
|
||||
func tusPostHandler() handleFunc {
|
||||
func tusPostHandler(cache UploadCache) handleFunc {
|
||||
return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
if !d.user.Perm.Create || !d.Check(r.URL.Path) {
|
||||
return http.StatusForbidden, nil
|
||||
@@ -146,7 +110,7 @@ func tusPostHandler() handleFunc {
|
||||
}
|
||||
|
||||
// Enables the user to utilize the PATCH endpoint for uploading file data
|
||||
registerUpload(file.RealPath(), uploadLength)
|
||||
cache.Register(file.RealPath(), uploadLength)
|
||||
|
||||
path, err := url.JoinPath("/", d.server.BaseURL, "/api/tus", r.URL.Path)
|
||||
if err != nil {
|
||||
@@ -158,7 +122,7 @@ func tusPostHandler() handleFunc {
|
||||
})
|
||||
}
|
||||
|
||||
func tusHeadHandler() handleFunc {
|
||||
func tusHeadHandler(cache UploadCache) handleFunc {
|
||||
return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
if !d.user.Perm.Create || !d.Check(r.URL.Path) {
|
||||
@@ -177,7 +141,7 @@ func tusHeadHandler() handleFunc {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
|
||||
uploadLength, err := getActiveUploadLength(file.RealPath())
|
||||
uploadLength, err := cache.GetLength(file.RealPath())
|
||||
if err != nil {
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
@@ -189,7 +153,7 @@ func tusHeadHandler() handleFunc {
|
||||
})
|
||||
}
|
||||
|
||||
func tusPatchHandler() handleFunc {
|
||||
func tusPatchHandler(cache UploadCache) handleFunc {
|
||||
return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
if !d.user.Perm.Create || !d.Check(r.URL.Path) {
|
||||
return http.StatusForbidden, nil
|
||||
@@ -219,13 +183,13 @@ func tusPatchHandler() handleFunc {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
|
||||
uploadLength, err := getActiveUploadLength(file.RealPath())
|
||||
uploadLength, err := cache.GetLength(file.RealPath())
|
||||
if err != nil {
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
|
||||
// Prevent the upload from being evicted during the transfer
|
||||
stop := keepUploadActive(file.RealPath())
|
||||
stop := keepUploadActive(cache, file.RealPath())
|
||||
defer stop()
|
||||
|
||||
switch {
|
||||
@@ -266,7 +230,7 @@ func tusPatchHandler() handleFunc {
|
||||
w.Header().Set("Upload-Offset", strconv.FormatInt(newOffset, 10))
|
||||
|
||||
if newOffset >= uploadLength {
|
||||
completeUpload(file.RealPath())
|
||||
cache.Complete(file.RealPath())
|
||||
_ = d.RunHook(func() error { return nil }, "upload", r.URL.Path, "", d.user)
|
||||
}
|
||||
|
||||
@@ -274,7 +238,7 @@ func tusPatchHandler() handleFunc {
|
||||
})
|
||||
}
|
||||
|
||||
func tusDeleteHandler() handleFunc {
|
||||
func tusDeleteHandler(cache UploadCache) handleFunc {
|
||||
return withUser(func(_ http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
if r.URL.Path == "/" || !d.user.Perm.Create {
|
||||
return http.StatusForbidden, nil
|
||||
@@ -292,7 +256,7 @@ func tusDeleteHandler() handleFunc {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
|
||||
_, err = getActiveUploadLength(file.RealPath())
|
||||
_, err = cache.GetLength(file.RealPath())
|
||||
if err != nil {
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
@@ -302,7 +266,7 @@ func tusDeleteHandler() handleFunc {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
|
||||
completeUpload(file.RealPath())
|
||||
cache.Complete(file.RealPath())
|
||||
|
||||
return http.StatusNoContent, nil
|
||||
})
|
||||
|
||||
85
http/upload_cache_memory.go
Normal file
85
http/upload_cache_memory.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package fbhttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/jellydator/ttlcache/v3"
|
||||
)
|
||||
|
||||
const uploadCacheTTL = 3 * time.Minute
|
||||
|
||||
// UploadCache is an interface for tracking active uploads.
|
||||
// Allows for different backends (e.g. in-memory or redis)
|
||||
// to support both single instance and multi replica deployments.
|
||||
type UploadCache interface {
|
||||
// Register stores an upload with its expected file size
|
||||
Register(filePath string, fileSize int64)
|
||||
|
||||
// Complete removes an upload from the cache
|
||||
Complete(filePath string)
|
||||
|
||||
// GetLength returns the expected file size for an active upload
|
||||
GetLength(filePath string) (int64, error)
|
||||
|
||||
// Touch refreshes the TTL for an active upload
|
||||
Touch(filePath string)
|
||||
|
||||
// Close cleans up any resources
|
||||
Close()
|
||||
}
|
||||
|
||||
// memoryUploadCache is an upload cache for single replica deployments
|
||||
type memoryUploadCache struct {
|
||||
cache *ttlcache.Cache[string, int64]
|
||||
}
|
||||
|
||||
func newMemoryUploadCache() *memoryUploadCache {
|
||||
cache := ttlcache.New[string, int64]()
|
||||
cache.OnEviction(func(_ context.Context, reason ttlcache.EvictionReason, item *ttlcache.Item[string, int64]) {
|
||||
if reason == ttlcache.EvictionReasonExpired {
|
||||
fmt.Printf("deleting incomplete upload file: \"%s\"\n", item.Key())
|
||||
os.Remove(item.Key())
|
||||
}
|
||||
})
|
||||
go cache.Start()
|
||||
|
||||
return &memoryUploadCache{cache: cache}
|
||||
}
|
||||
|
||||
func (c *memoryUploadCache) Register(filePath string, fileSize int64) {
|
||||
c.cache.Set(filePath, fileSize, uploadCacheTTL)
|
||||
}
|
||||
|
||||
func (c *memoryUploadCache) Complete(filePath string) {
|
||||
c.cache.Delete(filePath)
|
||||
}
|
||||
|
||||
func (c *memoryUploadCache) GetLength(filePath string) (int64, error) {
|
||||
item := c.cache.Get(filePath)
|
||||
if item == nil {
|
||||
return 0, fmt.Errorf("no active upload found for the given path")
|
||||
}
|
||||
return item.Value(), nil
|
||||
}
|
||||
|
||||
func (c *memoryUploadCache) Touch(filePath string) {
|
||||
c.cache.Touch(filePath)
|
||||
}
|
||||
|
||||
func (c *memoryUploadCache) Close() {
|
||||
c.cache.Stop()
|
||||
}
|
||||
|
||||
// NewUploadCache creates a new upload cache.
|
||||
// If redisURL is empty, an in-memory cache will be used (suitable for single instance deployments).
|
||||
// Otherwise, Redis will be used for the cache (suitable for multi-instance deployments).
|
||||
// The redisURL can include credentials, e.g. redis://user:pass@host:port
|
||||
func NewUploadCache(redisURL string) (UploadCache, error) {
|
||||
if redisURL != "" {
|
||||
return newRedisUploadCache(redisURL)
|
||||
}
|
||||
return newMemoryUploadCache(), nil
|
||||
}
|
||||
82
http/upload_cache_redis.go
Normal file
82
http/upload_cache_redis.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package fbhttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// redisUploadCache is an upload cache for multi replica deployments
|
||||
type redisUploadCache struct {
|
||||
client *redis.Client
|
||||
}
|
||||
|
||||
func newRedisUploadCache(redisURL string) (*redisUploadCache, error) {
|
||||
if redisURL == "" {
|
||||
return nil, fmt.Errorf("redis URL is required")
|
||||
}
|
||||
|
||||
opts, err := redis.ParseURL(redisURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid redis URL: %w", err)
|
||||
}
|
||||
|
||||
client := redis.NewClient(opts)
|
||||
|
||||
// Test connection
|
||||
if err := client.Ping(context.Background()).Err(); err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to redis: %w", err)
|
||||
}
|
||||
|
||||
return &redisUploadCache{client: client}, nil
|
||||
}
|
||||
|
||||
func (c *redisUploadCache) filePathKey(filePath string) string {
|
||||
return "filebrowser:upload:" + filePath
|
||||
}
|
||||
|
||||
func (c *redisUploadCache) Register(filePath string, fileSize int64) {
|
||||
err := c.client.Set(context.Background(), c.filePathKey(filePath), fileSize, uploadCacheTTL).Err()
|
||||
if err != nil {
|
||||
log.Printf("failed to register upload in redis cache: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *redisUploadCache) Complete(filePath string) {
|
||||
err := c.client.Del(context.Background(), c.filePathKey(filePath)).Err()
|
||||
if err != nil {
|
||||
log.Printf("failed to complete upload in redis cache: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *redisUploadCache) GetLength(filePath string) (int64, error) {
|
||||
result, err := c.client.Get(context.Background(), c.filePathKey(filePath)).Result()
|
||||
if err != nil {
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return 0, fmt.Errorf("no active upload found for the given path")
|
||||
}
|
||||
return 0, fmt.Errorf("redis error: %w", err)
|
||||
}
|
||||
|
||||
size, err := strconv.ParseInt(result, 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid upload length in cache: %w", err)
|
||||
}
|
||||
|
||||
return size, nil
|
||||
}
|
||||
|
||||
func (c *redisUploadCache) Touch(filePath string) {
|
||||
err := c.client.Expire(context.Background(), c.filePathKey(filePath), uploadCacheTTL).Err()
|
||||
if err != nil {
|
||||
log.Printf("failed to touch upload in redis cache: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *redisUploadCache) Close() {
|
||||
c.client.Close()
|
||||
}
|
||||
Reference in New Issue
Block a user