bump reva to use UploadSessionLister interface

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
This commit is contained in:
Jörn Friedrich Dreyer
2023-12-04 10:52:41 +01:00
parent 26411f1f3b
commit a6a7c69e8f
10 changed files with 373 additions and 164 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ require (
github.com/coreos/go-oidc v2.2.1+incompatible github.com/coreos/go-oidc v2.2.1+incompatible
github.com/coreos/go-oidc/v3 v3.8.0 github.com/coreos/go-oidc/v3 v3.8.0
github.com/cs3org/go-cs3apis v0.0.0-20231023073225-7748710e0781 github.com/cs3org/go-cs3apis v0.0.0-20231023073225-7748710e0781
github.com/cs3org/reva/v2 v2.16.1-0.20231128104331-ea8d1336afc9 github.com/cs3org/reva/v2 v2.16.1-0.20231201122033-a389ddc645c4
github.com/dhowden/tag v0.0.0-20230630033851-978a0926ee25 github.com/dhowden/tag v0.0.0-20230630033851-978a0926ee25
github.com/disintegration/imaging v1.6.2 github.com/disintegration/imaging v1.6.2
github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e
+2 -2
View File
@@ -1017,8 +1017,8 @@ github.com/crewjam/saml v0.4.14 h1:g9FBNx62osKusnFzs3QTN5L9CVA/Egfgm+stJShzw/c=
github.com/crewjam/saml v0.4.14/go.mod h1:UVSZCf18jJkk6GpWNVqcyQJMD5HsRugBPf4I1nl2mME= github.com/crewjam/saml v0.4.14/go.mod h1:UVSZCf18jJkk6GpWNVqcyQJMD5HsRugBPf4I1nl2mME=
github.com/cs3org/go-cs3apis v0.0.0-20231023073225-7748710e0781 h1:BUdwkIlf8IS2FasrrPg8gGPHQPOrQ18MS1Oew2tmGtY= github.com/cs3org/go-cs3apis v0.0.0-20231023073225-7748710e0781 h1:BUdwkIlf8IS2FasrrPg8gGPHQPOrQ18MS1Oew2tmGtY=
github.com/cs3org/go-cs3apis v0.0.0-20231023073225-7748710e0781/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY= github.com/cs3org/go-cs3apis v0.0.0-20231023073225-7748710e0781/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY=
github.com/cs3org/reva/v2 v2.16.1-0.20231128104331-ea8d1336afc9 h1:5vKQcL1hPHEZKu9e8C9rl0ap3ofMBznmoSgi4lRYXec= github.com/cs3org/reva/v2 v2.16.1-0.20231201122033-a389ddc645c4 h1:61AwMfov2OxrUElWXXKHZfBsuxgNIVwZVQW4PlJoqnM=
github.com/cs3org/reva/v2 v2.16.1-0.20231128104331-ea8d1336afc9/go.mod h1:zcrrYVsBv/DwhpyO2/W5hoSZ/k6az6Z2EYQok65uqZY= github.com/cs3org/reva/v2 v2.16.1-0.20231201122033-a389ddc645c4/go.mod h1:zcrrYVsBv/DwhpyO2/W5hoSZ/k6az6Z2EYQok65uqZY=
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+17 -23
View File
@@ -3,11 +3,8 @@ package command
import ( import (
"fmt" "fmt"
"os" "os"
"strconv"
"sync" "sync"
"time"
tusd "github.com/tus/tusd/pkg/handler"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"github.com/cs3org/reva/v2/pkg/storage" "github.com/cs3org/reva/v2/pkg/storage"
@@ -51,20 +48,21 @@ func ListUploads(cfg *config.Config) *cli.Command {
return err return err
} }
managingFS, ok := fs.(storage.UploadsManager) managingFS, ok := fs.(storage.UploadSessionLister)
if !ok { if !ok {
fmt.Fprintf(os.Stderr, "'%s' storage does not support listing expired uploads\n", cfg.Driver) fmt.Fprintf(os.Stderr, "'%s' storage does not support listing expired uploads\n", cfg.Driver)
os.Exit(1) os.Exit(1)
} }
falseValue := false
uploads, err := managingFS.ListUploads() uploads, err := managingFS.ListUploadSessions(c.Context, storage.UploadSessionFilter{Expired: &falseValue})
if err != nil { if err != nil {
return err return err
} }
fmt.Println("Incomplete uploads:") fmt.Println("Incomplete uploads:")
for _, u := range uploads { for _, u := range uploads {
fmt.Printf(" - %s (%s, Size: %d, Expires: %s)\n", u.ID, u.MetaData["filename"], u.Size, expiredString(u.MetaData["expires"])) ref := u.Reference()
fmt.Printf(" - %s (Space: %s, Name: %s, Size: %d/%d, Expires: %s, Processing: %t)\n", ref.GetResourceId().GetSpaceId(), u.ID(), u.Filename(), u.Offset(), u.Size(), u.Expires(), u.IsProcessing())
} }
return nil return nil
}, },
@@ -92,7 +90,7 @@ func PurgeExpiredUploads(cfg *config.Config) *cli.Command {
return err return err
} }
managingFS, ok := fs.(storage.UploadsManager) managingFS, ok := fs.(storage.UploadSessionLister)
if !ok { if !ok {
fmt.Fprintf(os.Stderr, "'%s' storage does not support clean expired uploads\n", cfg.Driver) fmt.Fprintf(os.Stderr, "'%s' storage does not support clean expired uploads\n", cfg.Driver)
os.Exit(1) os.Exit(1)
@@ -100,18 +98,23 @@ func PurgeExpiredUploads(cfg *config.Config) *cli.Command {
wg := sync.WaitGroup{} wg := sync.WaitGroup{}
wg.Add(1) wg.Add(1)
purgedChannel := make(chan tusd.FileInfo) falseValue := false
trueValue := false
uploads, err := managingFS.ListUploadSessions(c.Context, storage.UploadSessionFilter{Expired: &trueValue, Processing: &falseValue})
if err != nil {
return err
}
fmt.Println("Cleaned uploads:") fmt.Println("purging uploads:")
go func() { go func() {
for purged := range purgedChannel { for _, u := range uploads {
fmt.Printf(" - %s (%s, Size: %d, Expires: %s)\n", purged.ID, purged.MetaData["filename"], purged.Size, expiredString(purged.MetaData["expires"])) ref := u.Reference()
fmt.Printf(" - %s (Space: %s, Name: %s, Size: %d/%d, Expires: %s, Processing: %t)\n", ref.GetResourceId().GetSpaceId(), u.ID(), u.Filename(), u.Offset(), u.Size(), u.Expires(), u.IsProcessing())
u.Purge(c.Context)
} }
wg.Done() wg.Done()
}() }()
err = managingFS.PurgeExpiredUploads(purgedChannel)
close(purgedChannel)
wg.Wait() wg.Wait()
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Failed to clean expired uploads '%s'\n", err) fmt.Fprintf(os.Stderr, "Failed to clean expired uploads '%s'\n", err)
@@ -121,12 +124,3 @@ func PurgeExpiredUploads(cfg *config.Config) *cli.Command {
}, },
} }
} }
func expiredString(e string) string {
expired := "N/A"
iExpires, err := strconv.Atoi(e)
if err == nil {
expired = time.Unix(int64(iExpires), 0).Format(time.RFC3339)
}
return expired
}
+20 -28
View File
@@ -23,13 +23,11 @@ import (
"log" "log"
"net/http" "net/http"
"path" "path"
"path/filepath"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
tusd "github.com/tus/tusd/pkg/handler" tusd "github.com/tus/tusd/pkg/handler"
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/net" "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/net"
"github.com/cs3org/reva/v2/pkg/appctx" "github.com/cs3org/reva/v2/pkg/appctx"
@@ -40,8 +38,8 @@ import (
"github.com/cs3org/reva/v2/pkg/rhttp/datatx/metrics" "github.com/cs3org/reva/v2/pkg/rhttp/datatx/metrics"
"github.com/cs3org/reva/v2/pkg/storage" "github.com/cs3org/reva/v2/pkg/storage"
"github.com/cs3org/reva/v2/pkg/storage/cache" "github.com/cs3org/reva/v2/pkg/storage/cache"
"github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/upload"
"github.com/cs3org/reva/v2/pkg/storagespace" "github.com/cs3org/reva/v2/pkg/storagespace"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
) )
@@ -103,33 +101,27 @@ func (m *manager) Handler(fs storage.FS) (http.Handler, error) {
return nil, err return nil, err
} }
go func() { if _, ok := fs.(storage.UploadSessionLister); ok {
for { // We can currently only send updates if the fs is decomposedfs as we read very specific keys from the storage map of the tus info
ev := <-handler.CompleteUploads go func() {
info := ev.Upload for {
spaceOwner := &userv1beta1.UserId{ ev := <-handler.CompleteUploads
OpaqueId: info.Storage["SpaceOwnerOrManager"], // We should be able to get the upload progress with fs.GetUploadProgress, but currently tus will erase the info files
} // so we create a Progress instance here that is used to read the correct properties
owner := &userv1beta1.UserId{ up := upload.Progress{
Idp: info.Storage["Idp"], Info: ev.Upload,
OpaqueId: info.Storage["UserId"], }
} executant := up.Executant()
ref := &provider.Reference{ ref := up.Reference()
ResourceId: &provider.ResourceId{ datatx.InvalidateCache(&executant, &ref, m.statCache)
StorageId: info.MetaData["providerID"], if m.publisher != nil {
SpaceId: info.Storage["SpaceRoot"], if err := datatx.EmitFileUploadedEvent(up.SpaceOwner(), &executant, &ref, m.publisher); err != nil {
OpaqueId: info.Storage["SpaceRoot"], appctx.GetLogger(context.Background()).Error().Err(err).Msg("failed to publish FileUploaded event")
}, }
Path: utils.MakeRelativePath(filepath.Join(info.MetaData["dir"], info.MetaData["filename"])),
}
datatx.InvalidateCache(owner, ref, m.statCache)
if m.publisher != nil {
if err := datatx.EmitFileUploadedEvent(spaceOwner, owner, ref, m.publisher); err != nil {
appctx.GetLogger(context.Background()).Error().Err(err).Msg("failed to publish FileUploaded event")
} }
} }
} }()
}() }
h := handler.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { h := handler.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
method := r.Method method := r.Method
-18
View File
@@ -23,16 +23,10 @@ import (
"io" "io"
"net/url" "net/url"
tusd "github.com/tus/tusd/pkg/handler"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1" registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1"
) )
// UploadFinishedFunc is a callback function used in storage drivers to indicate that an upload has finished
type UploadFinishedFunc func(spaceOwner, owner *userpb.UserId, ref *provider.Reference)
// FS is the interface to implement access to the storage. // FS is the interface to implement access to the storage.
type FS interface { type FS interface {
GetHome(ctx context.Context) (string, error) GetHome(ctx context.Context) (string, error)
@@ -77,12 +71,6 @@ type FS interface {
DeleteStorageSpace(ctx context.Context, req *provider.DeleteStorageSpaceRequest) error DeleteStorageSpace(ctx context.Context, req *provider.DeleteStorageSpaceRequest) error
} }
// UploadsManager defines the interface for FS implementations that allow for managing uploads
type UploadsManager interface {
ListUploads() ([]tusd.FileInfo, error)
PurgeExpiredUploads(chan<- tusd.FileInfo) error
}
// Registry is the interface that storage registries implement // Registry is the interface that storage registries implement
// for discovering storage providers // for discovering storage providers
type Registry interface { type Registry interface {
@@ -98,9 +86,3 @@ type PathWrapper interface {
Unwrap(ctx context.Context, rp string) (string, error) Unwrap(ctx context.Context, rp string) (string, error)
Wrap(ctx context.Context, rp string) (string, error) Wrap(ctx context.Context, rp string) (string, error)
} }
type UploadRequest struct {
Ref *provider.Reference
Body io.ReadCloser
Length int64
}
+86
View File
@@ -0,0 +1,86 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package storage
import (
"context"
"io"
"time"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
tusd "github.com/tus/tusd/pkg/handler"
)
// UploadFinishedFunc is a callback function used in storage drivers to indicate that an upload has finished
type UploadFinishedFunc func(spaceOwner, executant *userpb.UserId, ref *provider.Reference)
// UploadRequest us used in FS.Upload() to carry required upload metadata
type UploadRequest struct {
Ref *provider.Reference
Body io.ReadCloser
Length int64
}
// UploadsManager defines the interface for storage drivers that allow for managing uploads
// Deprecated: No longer used. Storage drivers should implement the UploadSessionLister.
type UploadsManager interface {
ListUploads() ([]tusd.FileInfo, error)
PurgeExpiredUploads(chan<- tusd.FileInfo) error
}
// UploadSessionLister defines the interface for FS implementations that allow listing and purging upload sessions
type UploadSessionLister interface {
// ListUploadSessions returns the upload sessions matching the given filter
ListUploadSessions(ctx context.Context, filter UploadSessionFilter) ([]UploadSession, error)
}
// UploadSession is the interface that storage drivers need to return whan listing upload sessions.
type UploadSession interface {
// ID returns the upload id
ID() string
// Filename returns the filename of the file
Filename() string
// Size returns the size of the upload
Size() int64
// Offset returns the current offset
Offset() int64
// Reference returns a reference for the file being uploaded. May be absolute id based or relative to e.g. a space root
Reference() provider.Reference
// Executant returns the userid of the user that created the upload
Executant() userpb.UserId
// SpaceOwner returns the owner of a space if set. optional
SpaceOwner() *userpb.UserId
// Expires returns the time when the upload can no longer be used
Expires() time.Time
// IsProcessing returns true if postprocessing has not finished, yet
// The actual postprocessing state is tracked in the postprocessing service.
IsProcessing() bool
// Purge allows completely removing an upload. Should emit a PostprocessingFinished event with a Delete outcome
Purge(ctx context.Context) error
}
// UploadSessionFilter can be used to filter upload sessions
type UploadSessionFilter struct {
ID *string
Processing *bool
Expired *bool
}
+97 -50
View File
@@ -27,6 +27,9 @@ import (
"strings" "strings"
"time" "time"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/v2/pkg/appctx" "github.com/cs3org/reva/v2/pkg/appctx"
@@ -35,7 +38,6 @@ import (
"github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/metadata/prefixes" "github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/metadata/prefixes"
"github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/node" "github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/node"
"github.com/cs3org/reva/v2/pkg/storagespace" "github.com/cs3org/reva/v2/pkg/storagespace"
"github.com/pkg/errors"
) )
// Recycle items are stored inside the node folder and start with the uuid of the deleted node. // Recycle items are stored inside the node folder and start with the uuid of the deleted node.
@@ -214,66 +216,111 @@ func readTrashLink(path string) (string, string, string, error) {
func (fs *Decomposedfs) listTrashRoot(ctx context.Context, spaceID string) ([]*provider.RecycleItem, error) { func (fs *Decomposedfs) listTrashRoot(ctx context.Context, spaceID string) ([]*provider.RecycleItem, error) {
log := appctx.GetLogger(ctx) log := appctx.GetLogger(ctx)
items := make([]*provider.RecycleItem, 0)
trashRoot := fs.getRecycleRoot(spaceID) trashRoot := fs.getRecycleRoot(spaceID)
matches, err := filepath.Glob(trashRoot + "/*/*/*/*/*")
subTrees, err := filepath.Glob(trashRoot + "/*")
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, itemPath := range matches { numWorkers := fs.o.MaxConcurrency
nodePath, nodeID, timeSuffix, err := readTrashLink(itemPath) if len(subTrees) < numWorkers {
if err != nil { numWorkers = len(subTrees)
log.Error().Err(err).Str("trashRoot", trashRoot).Str("item", itemPath).Msg("error reading trash link, skipping") }
continue
}
md, err := os.Stat(nodePath) work := make(chan string, len(subTrees))
if err != nil { results := make(chan *provider.RecycleItem, len(subTrees))
log.Error().Err(err).Str("trashRoot", trashRoot).Str("item", itemPath).Str("node_path", nodePath).Msg("could not stat trash item, skipping")
continue
}
attrs, err := fs.lu.MetadataBackend().All(ctx, nodePath) g, ctx := errgroup.WithContext(ctx)
if err != nil {
log.Error().Err(err).Str("trashRoot", trashRoot).Str("item", itemPath).Str("node_path", nodePath).Msg("could not get extended attributes, skipping")
continue
}
nodeType := fs.lu.TypeFromPath(ctx, nodePath) // Distribute work
if nodeType == provider.ResourceType_RESOURCE_TYPE_INVALID { g.Go(func() error {
log.Error().Err(err).Str("trashRoot", trashRoot).Str("item", itemPath).Str("node_path", nodePath).Msg("invalid node type, skipping") defer close(work)
continue for _, itemPath := range subTrees {
} select {
case work <- itemPath:
item := &provider.RecycleItem{ case <-ctx.Done():
Type: nodeType, return ctx.Err()
Size: uint64(md.Size()),
Key: nodeID,
}
if deletionTime, err := time.Parse(time.RFC3339Nano, timeSuffix); err == nil {
item.DeletionTime = &types.Timestamp{
Seconds: uint64(deletionTime.Unix()),
// TODO nanos
} }
} else {
log.Error().Err(err).Str("trashRoot", trashRoot).Str("item", itemPath).Str("node", nodeID).Str("dtime", timeSuffix).Msg("could not parse time format, ignoring")
} }
return nil
})
// lookup origin path in extended attributes // Spawn workers that'll concurrently work the queue
if attr, ok := attrs[prefixes.TrashOriginAttr]; ok { for i := 0; i < numWorkers; i++ {
item.Ref = &provider.Reference{Path: string(attr)} g.Go(func() error {
} else { for subTree := range work {
log.Error().Str("trashRoot", trashRoot).Str("item", itemPath).Str("node", nodeID).Str("dtime", timeSuffix).Msg("could not read origin path, skipping") matches, err := filepath.Glob(subTree + "/*/*/*/*")
continue if err != nil {
} return err
// TODO filter results by permission ... on the original parent? or the trashed node? }
// if it were on the original parent it would be possible to see files that were trashed before the current user got access
// so -> check the trash node itself for _, itemPath := range matches {
// hmm listing trash currently lists the current users trash or the 'root' trash. from ocs only the home storage is queried for trash items. nodePath, nodeID, timeSuffix, err := readTrashLink(itemPath)
// for now we can only really check if the current user is the owner if err != nil {
items = append(items, item) log.Error().Err(err).Str("trashRoot", trashRoot).Str("item", itemPath).Msg("error reading trash link, skipping")
continue
}
md, err := os.Stat(nodePath)
if err != nil {
log.Error().Err(err).Str("trashRoot", trashRoot).Str("item", itemPath).Str("node_path", nodePath).Msg("could not stat trash item, skipping")
continue
}
attrs, err := fs.lu.MetadataBackend().All(ctx, nodePath)
if err != nil {
log.Error().Err(err).Str("trashRoot", trashRoot).Str("item", itemPath).Str("node_path", nodePath).Msg("could not get extended attributes, skipping")
continue
}
nodeType := fs.lu.TypeFromPath(ctx, nodePath)
if nodeType == provider.ResourceType_RESOURCE_TYPE_INVALID {
log.Error().Err(err).Str("trashRoot", trashRoot).Str("item", itemPath).Str("node_path", nodePath).Msg("invalid node type, skipping")
continue
}
item := &provider.RecycleItem{
Type: nodeType,
Size: uint64(md.Size()),
Key: nodeID,
}
if deletionTime, err := time.Parse(time.RFC3339Nano, timeSuffix); err == nil {
item.DeletionTime = &types.Timestamp{
Seconds: uint64(deletionTime.Unix()),
// TODO nanos
}
} else {
log.Error().Err(err).Str("trashRoot", trashRoot).Str("item", itemPath).Str("node", nodeID).Str("dtime", timeSuffix).Msg("could not parse time format, ignoring")
}
// lookup origin path in extended attributes
if attr, ok := attrs[prefixes.TrashOriginAttr]; ok {
item.Ref = &provider.Reference{Path: string(attr)}
} else {
log.Error().Str("trashRoot", trashRoot).Str("item", itemPath).Str("node", nodeID).Str("dtime", timeSuffix).Msg("could not read origin path")
}
select {
case results <- item:
case <-ctx.Done():
return ctx.Err()
}
}
}
return nil
})
}
// Wait for things to settle down, then close results chan
go func() {
_ = g.Wait() // error is checked later
close(results)
}()
// Collect results
items := []*provider.RecycleItem{}
for ri := range results {
items = append(items, ri)
} }
return items, nil return items, nil
} }
+66 -41
View File
@@ -20,10 +20,10 @@ package decomposedfs
import ( import (
"context" "context"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strconv"
"strings" "strings"
"time" "time"
@@ -96,14 +96,14 @@ func (fs *Decomposedfs) Upload(ctx context.Context, req storage.UploadRequest, u
}, },
Path: utils.MakeRelativePath(filepath.Join(info.MetaData["dir"], info.MetaData["filename"])), Path: utils.MakeRelativePath(filepath.Join(info.MetaData["dir"], info.MetaData["filename"])),
} }
owner, ok := ctxpkg.ContextGetUser(uploadInfo.Ctx) executant, ok := ctxpkg.ContextGetUser(uploadInfo.Ctx)
if !ok { if !ok {
return provider.ResourceInfo{}, errtypes.PreconditionFailed("error getting user from uploadinfo context") return provider.ResourceInfo{}, errtypes.PreconditionFailed("error getting user from uploadinfo context")
} }
spaceOwner := &userpb.UserId{ spaceOwner := &userpb.UserId{
OpaqueId: info.Storage["SpaceOwnerOrManager"], OpaqueId: info.Storage["SpaceOwnerOrManager"],
} }
uff(spaceOwner, owner.Id, uploadRef) uff(spaceOwner, executant.Id, uploadRef)
} }
ri := provider.ResourceInfo{ ri := provider.ResourceInfo{
@@ -243,36 +243,42 @@ func (fs *Decomposedfs) GetUpload(ctx context.Context, id string) (tusd.Upload,
return upload.Get(ctx, id, fs.lu, fs.tp, fs.o.Root, fs.stream, fs.o.AsyncFileUploads, fs.o.Tokens) return upload.Get(ctx, id, fs.lu, fs.tp, fs.o.Root, fs.stream, fs.o.AsyncFileUploads, fs.o.Tokens)
} }
// ListUploads returns a list of all incomplete uploads // ListUploadSessions returns the upload sessions for the given filter
func (fs *Decomposedfs) ListUploads() ([]tusd.FileInfo, error) { func (fs *Decomposedfs) ListUploadSessions(ctx context.Context, filter storage.UploadSessionFilter) ([]storage.UploadSession, error) {
return fs.uploadInfos(context.Background()) var sessions []storage.UploadSession
} if filter.ID != nil && *filter.ID != "" {
session, err := fs.getUploadSession(ctx, filepath.Join(fs.o.Root, "uploads", *filter.ID+".info"))
// PurgeExpiredUploads scans the fs for expired downloads and removes any leftovers
func (fs *Decomposedfs) PurgeExpiredUploads(purgedChan chan<- tusd.FileInfo) error {
infos, err := fs.uploadInfos(context.Background())
if err != nil {
return err
}
for _, info := range infos {
expires, err := strconv.Atoi(info.MetaData["expires"])
if err != nil { if err != nil {
return nil, err
}
sessions = []storage.UploadSession{session}
} else {
var err error
sessions, err = fs.uploadSessions(ctx)
if err != nil {
return nil, err
}
}
filteredSessions := []storage.UploadSession{}
now := time.Now()
for _, session := range sessions {
if filter.Processing != nil && *filter.Processing != session.IsProcessing() {
continue continue
} }
if int64(expires) < time.Now().Unix() { if filter.Expired != nil {
purgedChan <- info if *filter.Expired {
err = os.Remove(info.Storage["BinPath"]) if now.Before(session.Expires()) {
if err != nil { continue
return err }
} } else {
err = os.Remove(filepath.Join(fs.o.Root, "uploads", info.ID+".info")) if now.After(session.Expires()) {
if err != nil { continue
return err }
} }
} }
filteredSessions = append(filteredSessions, session)
} }
return nil return filteredSessions, nil
} }
// AsTerminatableUpload returns a TerminatableUpload // AsTerminatableUpload returns a TerminatableUpload
@@ -296,28 +302,47 @@ func (fs *Decomposedfs) AsConcatableUpload(up tusd.Upload) tusd.ConcatableUpload
return up.(*upload.Upload) return up.(*upload.Upload)
} }
func (fs *Decomposedfs) uploadInfos(ctx context.Context) ([]tusd.FileInfo, error) { func (fs *Decomposedfs) uploadSessions(ctx context.Context) ([]storage.UploadSession, error) {
infos := []tusd.FileInfo{} uploads := []storage.UploadSession{}
infoFiles, err := filepath.Glob(filepath.Join(fs.o.Root, "uploads", "*.info")) infoFiles, err := filepath.Glob(filepath.Join(fs.o.Root, "uploads", "*.info"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, info := range infoFiles { for _, info := range infoFiles {
match := _idRegexp.FindStringSubmatch(info) progress, err := fs.getUploadSession(ctx, info)
if match == nil || len(match) < 2 { if err != nil {
appctx.GetLogger(ctx).Error().Interface("path", info).Msg("Decomposedfs: could not getUploadSession")
continue continue
} }
up, err := fs.GetUpload(ctx, match[1])
if err != nil {
return nil, err
}
info, err := up.GetInfo(context.Background())
if err != nil {
return nil, err
}
infos = append(infos, info) uploads = append(uploads, progress)
} }
return infos, nil return uploads, nil
}
func (fs *Decomposedfs) getUploadSession(ctx context.Context, path string) (storage.UploadSession, error) {
match := _idRegexp.FindStringSubmatch(path)
if match == nil || len(match) < 2 {
return nil, fmt.Errorf("invalid upload path")
}
up, err := fs.GetUpload(ctx, match[1])
if err != nil {
return nil, err
}
info, err := up.GetInfo(context.Background())
if err != nil {
return nil, err
}
// upload processing state is stored in the node, for decomposedfs the NodeId is always set by InitiateUpload
n, err := node.ReadNode(ctx, fs.lu, info.Storage["SpaceRoot"], info.Storage["NodeId"], true, nil, true)
if err != nil {
return nil, err
}
progress := upload.Progress{
Path: path,
Info: info,
Processing: n.IsProcessing(ctx),
}
return progress, nil
} }
@@ -21,6 +21,7 @@ package upload
import ( import (
"context" "context"
"encoding/json" "encoding/json"
stderrors "errors"
"fmt" "fmt"
iofs "io/fs" iofs "io/fs"
"os" "os"
@@ -497,3 +498,85 @@ func lookupNode(ctx context.Context, spaceRoot *node.Node, path string, lu *look
} }
return n, nil return n, nil
} }
// Progress adapts the persisted upload metadata for the UploadSessionLister interface
type Progress struct {
Path string
Info tusd.FileInfo
Processing bool
}
// ID implements the storage.UploadSession interface
func (p Progress) ID() string {
return p.Info.ID
}
// Filename implements the storage.UploadSession interface
func (p Progress) Filename() string {
return p.Info.MetaData["filename"]
}
// Size implements the storage.UploadSession interface
func (p Progress) Size() int64 {
return p.Info.Size
}
// Offset implements the storage.UploadSession interface
func (p Progress) Offset() int64 {
return p.Info.Offset
}
// Reference implements the storage.UploadSession interface
func (p Progress) Reference() provider.Reference {
return provider.Reference{
ResourceId: &provider.ResourceId{
StorageId: p.Info.MetaData["providerID"],
SpaceId: p.Info.Storage["SpaceRoot"],
OpaqueId: p.Info.Storage["NodeId"], // Node id is always set in InitiateUpload
},
}
}
// Executant implements the storage.UploadSession interface
func (p Progress) Executant() userpb.UserId {
return userpb.UserId{
Idp: p.Info.Storage["Idp"],
OpaqueId: p.Info.Storage["UserId"],
Type: utils.UserTypeMap(p.Info.Storage["UserType"]),
}
}
// SpaceOwner implements the storage.UploadSession interface
func (p Progress) SpaceOwner() *userpb.UserId {
return &userpb.UserId{
// idp and type do not seem to be consumed and the node currently only stores the user id anyway
OpaqueId: p.Info.Storage["SpaceOwnerOrManager"],
}
}
// Expires implements the storage.UploadSession interface
func (p Progress) Expires() time.Time {
mt, _ := utils.MTimeToTime(p.Info.MetaData["expires"])
return mt
}
// IsProcessing implements the storage.UploadSession interface
func (p Progress) IsProcessing() bool {
return p.Processing
}
// Purge implements the storage.UploadSession interface
func (p Progress) Purge(ctx context.Context) error {
berr := os.Remove(p.Info.Storage["BinPath"])
if berr != nil {
appctx.GetLogger(ctx).Error().Str("id", p.Info.ID).Interface("path", p.Info.Storage["BinPath"]).Msg("Decomposedfs: could not purge bin path for upload session")
}
// remove upload metadata
merr := os.Remove(p.Path)
if merr != nil {
appctx.GetLogger(ctx).Error().Str("id", p.Info.ID).Interface("path", p.Path).Msg("Decomposedfs: could not purge metadata path for upload session")
}
return stderrors.Join(berr, merr)
}
+1 -1
View File
@@ -357,7 +357,7 @@ github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1
github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1 github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1
github.com/cs3org/go-cs3apis/cs3/tx/v1beta1 github.com/cs3org/go-cs3apis/cs3/tx/v1beta1
github.com/cs3org/go-cs3apis/cs3/types/v1beta1 github.com/cs3org/go-cs3apis/cs3/types/v1beta1
# github.com/cs3org/reva/v2 v2.16.1-0.20231128104331-ea8d1336afc9 # github.com/cs3org/reva/v2 v2.16.1-0.20231201122033-a389ddc645c4
## explicit; go 1.20 ## explicit; go 1.20
github.com/cs3org/reva/v2/cmd/revad/internal/grace github.com/cs3org/reva/v2/cmd/revad/internal/grace
github.com/cs3org/reva/v2/cmd/revad/runtime github.com/cs3org/reva/v2/cmd/revad/runtime