+22
-8
@@ -127,7 +127,7 @@ func (s *svc) handlePathCopy(w http.ResponseWriter, r *http.Request, ns string)
|
||||
return
|
||||
}
|
||||
|
||||
cp := s.prepareCopy(ctx, w, r, spacelookup.MakeRelativeReference(srcSpace, src, false), spacelookup.MakeRelativeReference(dstSpace, dst, false), &sublog)
|
||||
cp := s.prepareCopy(ctx, w, r, spacelookup.MakeRelativeReference(srcSpace, src, false), spacelookup.MakeRelativeReference(dstSpace, dst, false), &sublog, dstSpace.GetRoot().GetStorageId() == utils.ShareStorageProviderID)
|
||||
if cp == nil {
|
||||
return
|
||||
}
|
||||
@@ -362,7 +362,7 @@ func (s *svc) handleSpacesCopy(w http.ResponseWriter, r *http.Request, spaceID s
|
||||
return
|
||||
}
|
||||
|
||||
cp := s.prepareCopy(ctx, w, r, &srcRef, &dstRef, &sublog)
|
||||
cp := s.prepareCopy(ctx, w, r, &srcRef, &dstRef, &sublog, dstRef.GetResourceId().GetStorageId() == utils.ShareStorageProviderID)
|
||||
if cp == nil {
|
||||
return
|
||||
}
|
||||
@@ -552,7 +552,7 @@ func (s *svc) executeSpacesCopy(ctx context.Context, w http.ResponseWriter, sele
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *svc) prepareCopy(ctx context.Context, w http.ResponseWriter, r *http.Request, srcRef, dstRef *provider.Reference, log *zerolog.Logger) *copy {
|
||||
func (s *svc) prepareCopy(ctx context.Context, w http.ResponseWriter, r *http.Request, srcRef, dstRef *provider.Reference, log *zerolog.Logger, destInShareJail bool) *copy {
|
||||
isChild, err := s.referenceIsChildOf(ctx, s.gatewaySelector, dstRef, srcRef)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
@@ -675,11 +675,6 @@ func (s *svc) prepareCopy(ctx context.Context, w http.ResponseWriter, r *http.Re
|
||||
if dstStatRes.Status.Code == rpc.Code_CODE_OK {
|
||||
successCode = http.StatusNoContent // 204 if target already existed, see https://tools.ietf.org/html/rfc4918#section-9.8.5
|
||||
|
||||
if utils.IsSpaceRoot(dstStatRes.GetInfo()) {
|
||||
log.Error().Msg("overwriting is not allowed")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return nil
|
||||
}
|
||||
if !overwrite {
|
||||
log.Warn().Bool("overwrite", overwrite).Msg("dst already exists")
|
||||
w.WriteHeader(http.StatusPreconditionFailed)
|
||||
@@ -688,10 +683,29 @@ func (s *svc) prepareCopy(ctx context.Context, w http.ResponseWriter, r *http.Re
|
||||
errors.HandleWebdavError(log, w, b, err) // 412, see https://tools.ietf.org/html/rfc4918#section-9.8.5
|
||||
return nil
|
||||
}
|
||||
|
||||
if utils.IsSpaceRoot(dstStatRes.GetInfo()) {
|
||||
log.Error().Msg("overwriting is not allowed")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return nil
|
||||
}
|
||||
|
||||
// delete existing tree when overwriting a directory or replacing a file with a directory
|
||||
if dstStatRes.Info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER ||
|
||||
(dstStatRes.Info.Type == provider.ResourceType_RESOURCE_TYPE_FILE &&
|
||||
srcStatRes.Info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER) {
|
||||
|
||||
// we must not allow to override mountpoints - so we check if we have access to the parent. If not this is a mountpoint
|
||||
if destInShareJail {
|
||||
dir, file := filepath.Split(dstRef.GetPath())
|
||||
if dir == "/" || dir == "" || file == "" {
|
||||
log.Error().Msg("must not overwrite mount points")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte("must not overwrite mount points"))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
delReq := &provider.DeleteRequest{Ref: dstRef}
|
||||
delRes, err := client.Delete(ctx, delReq)
|
||||
if err != nil {
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ import (
|
||||
"github.com/cs3org/reva/v2/pkg/storagespace"
|
||||
"github.com/cs3org/reva/v2/pkg/utils"
|
||||
"github.com/rs/zerolog"
|
||||
tusd "github.com/tus/tusd/pkg/handler"
|
||||
tusd "github.com/tus/tusd/v2/pkg/handler"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
)
|
||||
|
||||
|
||||
+2
-2
@@ -35,7 +35,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
tusd "github.com/tus/tusd/pkg/handler"
|
||||
tusd "github.com/tus/tusd/v2/pkg/handler"
|
||||
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
@@ -266,7 +266,7 @@ func (u *upload) GetInfo(ctx context.Context) (tusd.FileInfo, error) {
|
||||
return u.Info, nil
|
||||
}
|
||||
|
||||
func (u *upload) GetReader(ctx context.Context) (io.Reader, error) {
|
||||
func (u *upload) GetReader(ctx context.Context) (io.ReadCloser, error) {
|
||||
return os.Open(u.BinPath())
|
||||
}
|
||||
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package rgrpc
|
||||
|
||||
import (
|
||||
"math"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
_serverMaxConnectionAgeEnv = "GRPC_MAX_CONNECTION_AGE"
|
||||
|
||||
// same default as grpc
|
||||
infinity = time.Duration(math.MaxInt64)
|
||||
_defaultMaxConnectionAge = infinity
|
||||
)
|
||||
|
||||
// GetMaxConnectionAge returns the maximum grpc connection age.
|
||||
func GetMaxConnectionAge() time.Duration {
|
||||
d, err := time.ParseDuration(os.Getenv(_serverMaxConnectionAgeEnv))
|
||||
if err != nil {
|
||||
return _defaultMaxConnectionAge
|
||||
}
|
||||
return d
|
||||
}
|
||||
+4
@@ -42,6 +42,7 @@ import (
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/keepalive"
|
||||
"google.golang.org/grpc/reflection"
|
||||
)
|
||||
|
||||
@@ -245,6 +246,9 @@ func (s *Server) registerServices() error {
|
||||
if s.conf.TLSSettings.tlsConfig != nil {
|
||||
opts = append(opts, grpc.Creds(credentials.NewTLS(s.conf.TLSSettings.tlsConfig)))
|
||||
}
|
||||
opts = append(opts, grpc.KeepaliveParams(keepalive.ServerParameters{
|
||||
MaxConnectionAge: GetMaxConnectionAge(), // this forces clients to reconnect after 30 seconds, triggering a new DNS lookup to pick up new IPs
|
||||
}))
|
||||
|
||||
grpcServer := grpc.NewServer(opts...)
|
||||
|
||||
|
||||
+33
-3
@@ -20,13 +20,13 @@ package tus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
"regexp"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
tusd "github.com/tus/tusd/pkg/handler"
|
||||
"github.com/rs/zerolog"
|
||||
tusd "github.com/tus/tusd/v2/pkg/handler"
|
||||
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/net"
|
||||
@@ -39,6 +39,7 @@ import (
|
||||
"github.com/cs3org/reva/v2/pkg/storage"
|
||||
"github.com/cs3org/reva/v2/pkg/storagespace"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -99,7 +100,7 @@ func (m *manager) Handler(fs storage.FS) (http.Handler, error) {
|
||||
config := tusd.Config{
|
||||
StoreComposer: composer,
|
||||
NotifyCompleteUploads: true,
|
||||
Logger: log.New(appctx.GetLogger(context.Background()), "", 0),
|
||||
Logger: slog.New(tusdLogger{log: appctx.GetLogger(context.Background())}), // Note: this is a noop logger
|
||||
}
|
||||
|
||||
if m.conf.CorsEnabled {
|
||||
@@ -222,3 +223,32 @@ func setHeaders(fs storage.FS, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
w.Header().Set(net.HeaderOCFileID, storagespace.FormatResourceID(resourceid))
|
||||
}
|
||||
|
||||
// tusdLogger is a logger implementation (slog) for tusd that uses zerolog.
|
||||
type tusdLogger struct {
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
// Handle handles the record
|
||||
func (l tusdLogger) Handle(_ context.Context, r slog.Record) error {
|
||||
switch r.Level {
|
||||
case slog.LevelDebug:
|
||||
l.log.Debug().Msg(r.Message)
|
||||
case slog.LevelInfo:
|
||||
l.log.Info().Msg(r.Message)
|
||||
case slog.LevelWarn:
|
||||
l.log.Warn().Msg(r.Message)
|
||||
case slog.LevelError:
|
||||
l.log.Error().Msg(r.Message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Enabled returns true
|
||||
func (l tusdLogger) Enabled(_ context.Context, _ slog.Level) bool { return true }
|
||||
|
||||
// WithAttrs is not implemented
|
||||
func (l tusdLogger) WithAttrs(_ []slog.Attr) slog.Handler { return l }
|
||||
|
||||
// WithGroup is not implemented
|
||||
func (l tusdLogger) WithGroup(_ string) slog.Handler { return l }
|
||||
|
||||
+2
-2
@@ -39,7 +39,7 @@ import (
|
||||
"github.com/cs3org/reva/v2/pkg/utils"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
tusd "github.com/tus/tusd/pkg/handler"
|
||||
tusd "github.com/tus/tusd/v2/pkg/handler"
|
||||
)
|
||||
|
||||
func (fs *cephfs) Upload(ctx context.Context, req storage.UploadRequest, uff storage.UploadFinishedFunc) (*provider.ResourceInfo, error) {
|
||||
@@ -299,7 +299,7 @@ func (upload *fileUpload) GetInfo(ctx context.Context) (tusd.FileInfo, error) {
|
||||
}
|
||||
|
||||
// GetReader returns an io.Reader for the upload
|
||||
func (upload *fileUpload) GetReader(ctx context.Context) (file io.Reader, err error) {
|
||||
func (upload *fileUpload) GetReader(ctx context.Context) (file io.ReadCloser, err error) {
|
||||
user := upload.fs.makeUser(upload.ctx)
|
||||
user.op(func(cv *cacheVal) {
|
||||
file, err = cv.mount.Open(upload.binPath, os.O_RDONLY, 0)
|
||||
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
// 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 hello
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/cs3org/reva/v2/pkg/errtypes"
|
||||
"github.com/cs3org/reva/v2/pkg/events"
|
||||
"github.com/cs3org/reva/v2/pkg/storage"
|
||||
"github.com/cs3org/reva/v2/pkg/storage/fs/registry"
|
||||
"github.com/cs3org/reva/v2/pkg/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register("hello", New)
|
||||
}
|
||||
|
||||
type hellofs struct {
|
||||
bootTime time.Time
|
||||
}
|
||||
|
||||
const (
|
||||
storageid = "hello-storage-id"
|
||||
spaceid = "hello-space-id"
|
||||
rootid = "hello-root-id"
|
||||
fileid = "hello-file-id"
|
||||
filename = "Hello world.txt"
|
||||
content = "Hello world!"
|
||||
)
|
||||
|
||||
func (fs *hellofs) space(withRoot bool) *provider.StorageSpace {
|
||||
s := &provider.StorageSpace{
|
||||
Id: &provider.StorageSpaceId{OpaqueId: spaceid},
|
||||
Root: &provider.ResourceId{
|
||||
StorageId: storageid,
|
||||
SpaceId: spaceid,
|
||||
OpaqueId: rootid,
|
||||
},
|
||||
Quota: &provider.Quota{
|
||||
QuotaMaxBytes: uint64(len(content)),
|
||||
QuotaMaxFiles: 1,
|
||||
},
|
||||
Name: "Hello Space",
|
||||
SpaceType: "project",
|
||||
RootInfo: fs.rootInfo(),
|
||||
Mtime: utils.TimeToTS(fs.bootTime),
|
||||
}
|
||||
// FIXME move this to the CS3 API
|
||||
s.Opaque = utils.AppendPlainToOpaque(s.Opaque, "spaceAlias", "project/hello")
|
||||
|
||||
if withRoot {
|
||||
s.RootInfo = fs.rootInfo()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (fs *hellofs) rootInfo() *provider.ResourceInfo {
|
||||
return &provider.ResourceInfo{
|
||||
Type: provider.ResourceType_RESOURCE_TYPE_CONTAINER,
|
||||
Id: &provider.ResourceId{
|
||||
StorageId: storageid,
|
||||
SpaceId: spaceid,
|
||||
OpaqueId: rootid,
|
||||
},
|
||||
Etag: calcEtag(fs.bootTime, rootid),
|
||||
MimeType: "httpd/unix-directory",
|
||||
Mtime: utils.TimeToTS(fs.bootTime),
|
||||
Path: ".",
|
||||
PermissionSet: &provider.ResourcePermissions{
|
||||
GetPath: true,
|
||||
GetQuota: true,
|
||||
InitiateFileDownload: true,
|
||||
Stat: true,
|
||||
ListContainer: true,
|
||||
},
|
||||
Size: uint64(len(content)),
|
||||
}
|
||||
}
|
||||
|
||||
func (fs *hellofs) fileInfo() *provider.ResourceInfo {
|
||||
return &provider.ResourceInfo{
|
||||
Type: provider.ResourceType_RESOURCE_TYPE_FILE,
|
||||
Id: &provider.ResourceId{
|
||||
StorageId: storageid,
|
||||
SpaceId: spaceid,
|
||||
OpaqueId: fileid,
|
||||
},
|
||||
Etag: calcEtag(fs.bootTime, fileid),
|
||||
MimeType: "text/plain",
|
||||
Mtime: utils.TimeToTS(fs.bootTime),
|
||||
Path: ".",
|
||||
PermissionSet: &provider.ResourcePermissions{
|
||||
GetPath: true,
|
||||
GetQuota: true,
|
||||
InitiateFileDownload: true,
|
||||
Stat: true,
|
||||
ListContainer: true,
|
||||
},
|
||||
Size: uint64(len(content)),
|
||||
ParentId: &provider.ResourceId{
|
||||
StorageId: storageid,
|
||||
SpaceId: spaceid,
|
||||
OpaqueId: rootid,
|
||||
},
|
||||
Name: filename,
|
||||
Space: fs.space(false),
|
||||
}
|
||||
}
|
||||
|
||||
func calcEtag(t time.Time, nodeid string) string {
|
||||
h := md5.New()
|
||||
_ = binary.Write(h, binary.BigEndian, t.Unix())
|
||||
_ = binary.Write(h, binary.BigEndian, int64(t.Nanosecond()))
|
||||
_ = binary.Write(h, binary.BigEndian, []byte(nodeid))
|
||||
etag := fmt.Sprintf(`"%x"`, h.Sum(nil))
|
||||
return fmt.Sprintf("\"%s\"", strings.Trim(etag, "\""))
|
||||
}
|
||||
|
||||
// New returns an implementation to of the storage.FS interface that talks to
|
||||
// a local filesystem with user homes disabled.
|
||||
func New(_ map[string]interface{}, _ events.Stream) (storage.FS, error) {
|
||||
return &hellofs{
|
||||
bootTime: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Shutdown is called when the process is exiting to give the driver a chance to flush and close all open handles
|
||||
func (fs *hellofs) Shutdown(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListStorageSpaces lists the spaces in the storage.
|
||||
func (fs *hellofs) ListStorageSpaces(ctx context.Context, filter []*provider.ListStorageSpacesRequest_Filter, unrestricted bool) ([]*provider.StorageSpace, error) {
|
||||
return []*provider.StorageSpace{fs.space(true)}, nil
|
||||
}
|
||||
|
||||
// GetQuota returns the quota on the referenced resource
|
||||
func (fs *hellofs) GetQuota(ctx context.Context, ref *provider.Reference) (uint64, uint64, uint64, error) {
|
||||
return uint64(len(content)), uint64(len(content)), 0, nil
|
||||
}
|
||||
|
||||
func (fs *hellofs) lookup(ctx context.Context, ref *provider.Reference) (*provider.ResourceInfo, error) {
|
||||
if ref.GetResourceId().GetStorageId() != storageid || ref.GetResourceId().GetSpaceId() != spaceid {
|
||||
return nil, errtypes.NotFound("")
|
||||
}
|
||||
|
||||
// switch root or file
|
||||
switch ref.GetResourceId().GetOpaqueId() {
|
||||
case rootid:
|
||||
switch ref.GetPath() {
|
||||
case "", ".":
|
||||
return fs.rootInfo(), nil
|
||||
case filename:
|
||||
return fs.fileInfo(), nil
|
||||
default:
|
||||
return nil, errtypes.NotFound("unknown filename")
|
||||
}
|
||||
case fileid:
|
||||
return fs.fileInfo(), nil
|
||||
}
|
||||
|
||||
return nil, errtypes.NotFound("unknown id")
|
||||
}
|
||||
|
||||
// GetPathByID returns the path pointed by the file id
|
||||
func (fs *hellofs) GetPathByID(ctx context.Context, resID *provider.ResourceId) (string, error) {
|
||||
info, err := fs.lookup(ctx, &provider.Reference{ResourceId: resID})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return info.Path, nil
|
||||
}
|
||||
|
||||
// GetMD returns the resuorce info for the referenced resource
|
||||
func (fs *hellofs) GetMD(ctx context.Context, ref *provider.Reference, mdKeys []string, fieldMask []string) (*provider.ResourceInfo, error) {
|
||||
return fs.lookup(ctx, ref)
|
||||
}
|
||||
|
||||
// ListFolder returns the resource infos for all children of the referenced resource
|
||||
func (fs *hellofs) ListFolder(ctx context.Context, ref *provider.Reference, mdKeys, fieldMask []string) ([]*provider.ResourceInfo, error) {
|
||||
info, err := fs.lookup(ctx, ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if info.Type != provider.ResourceType_RESOURCE_TYPE_CONTAINER {
|
||||
return nil, errtypes.InternalError("expected a container")
|
||||
}
|
||||
if info.GetId().GetOpaqueId() != rootid {
|
||||
return nil, errtypes.InternalError("unknown folder")
|
||||
}
|
||||
|
||||
return []*provider.ResourceInfo{
|
||||
fs.fileInfo(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Download returns a ReadCloser for the content of the referenced resource
|
||||
func (fs *hellofs) Download(ctx context.Context, ref *provider.Reference) (io.ReadCloser, error) {
|
||||
info, err := fs.lookup(ctx, ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if info.Type != provider.ResourceType_RESOURCE_TYPE_FILE {
|
||||
return nil, errtypes.InternalError("expected a file")
|
||||
}
|
||||
if info.GetId().GetOpaqueId() != fileid {
|
||||
return nil, errtypes.InternalError("unknown file")
|
||||
}
|
||||
|
||||
b := &bytes.Buffer{}
|
||||
b.WriteString(content)
|
||||
return io.NopCloser(b), nil
|
||||
}
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
// 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 hello
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/url"
|
||||
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/cs3org/reva/v2/pkg/errtypes"
|
||||
"github.com/cs3org/reva/v2/pkg/storage"
|
||||
)
|
||||
|
||||
// hellofs is readonly so these remain unimplemented
|
||||
|
||||
// CreateReference creates a resource of type reference
|
||||
func (fs *hellofs) CreateReference(ctx context.Context, path string, targetURI *url.URL) error {
|
||||
return errtypes.NotSupported("unimplemented")
|
||||
}
|
||||
|
||||
// CreateStorageSpace creates a storage space
|
||||
func (fs *hellofs) CreateStorageSpace(ctx context.Context, req *provider.CreateStorageSpaceRequest) (*provider.CreateStorageSpaceResponse, error) {
|
||||
return nil, errtypes.NotSupported("unimplemented: CreateStorageSpace")
|
||||
}
|
||||
|
||||
// UpdateStorageSpace updates a storage space
|
||||
func (fs *hellofs) UpdateStorageSpace(ctx context.Context, req *provider.UpdateStorageSpaceRequest) (*provider.UpdateStorageSpaceResponse, error) {
|
||||
return nil, errtypes.NotSupported("update storage space")
|
||||
}
|
||||
|
||||
// DeleteStorageSpace deletes a storage space
|
||||
func (fs *hellofs) DeleteStorageSpace(ctx context.Context, req *provider.DeleteStorageSpaceRequest) error {
|
||||
return errtypes.NotSupported("delete storage space")
|
||||
}
|
||||
|
||||
// CreateDir creates a resource of type container
|
||||
func (fs *hellofs) CreateDir(ctx context.Context, ref *provider.Reference) error {
|
||||
return errtypes.NotSupported("unimplemented")
|
||||
}
|
||||
|
||||
// TouchFile sets the mtime of a resource, creating an empty file if it does not exist
|
||||
// FIXME the markprocessing flag is an implementation detail of decomposedfs, remove it from the function
|
||||
// FIXME the mtime should either be a time.Time or a CS3 Timestamp, not a string
|
||||
func (fs *hellofs) TouchFile(ctx context.Context, ref *provider.Reference, _ bool, _ string) error {
|
||||
return errtypes.NotSupported("unimplemented")
|
||||
}
|
||||
|
||||
// Delete deletes a resource.
|
||||
// If the storage driver supports a recycle bin it should moves it to the recycle bin
|
||||
func (fs *hellofs) Delete(ctx context.Context, ref *provider.Reference) error {
|
||||
return errtypes.NotSupported("unimplemented")
|
||||
}
|
||||
|
||||
// Move changes the path of a resource
|
||||
func (fs *hellofs) Move(ctx context.Context, oldRef, newRef *provider.Reference) error {
|
||||
return errtypes.NotSupported("unimplemented")
|
||||
}
|
||||
|
||||
// Upload creates or updates a resource of type file with a new revision
|
||||
func (fs *hellofs) Upload(ctx context.Context, req storage.UploadRequest, uff storage.UploadFinishedFunc) (*provider.ResourceInfo, error) {
|
||||
return nil, errtypes.NotSupported("hellofs: upload not supported")
|
||||
}
|
||||
|
||||
// InitiateUpload returns a list of protocols with urls that can be used to append bytes to a new upload session
|
||||
func (fs *hellofs) InitiateUpload(ctx context.Context, ref *provider.Reference, uploadLength int64, metadata map[string]string) (map[string]string, error) {
|
||||
return nil, errtypes.NotSupported("hellofs: initiate upload not supported")
|
||||
}
|
||||
|
||||
// grants
|
||||
|
||||
// DenyGrant marks a resource as denied for a recipient
|
||||
// The resource and its children must be completely hidden for the recipient
|
||||
func (fs *hellofs) DenyGrant(ctx context.Context, ref *provider.Reference, g *provider.Grantee) error {
|
||||
return errtypes.NotSupported("hellofs: deny grant not supported")
|
||||
}
|
||||
|
||||
// AddGrant adds a grant to a resource
|
||||
func (fs *hellofs) AddGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error {
|
||||
return errtypes.NotSupported("unimplemented")
|
||||
}
|
||||
|
||||
// ListGrants lists all grants on a resource
|
||||
func (fs *hellofs) ListGrants(ctx context.Context, ref *provider.Reference) ([]*provider.Grant, error) {
|
||||
return nil, errtypes.NotSupported("unimplemented")
|
||||
}
|
||||
|
||||
// RemoveGrant removes a grant from a resource
|
||||
func (fs *hellofs) RemoveGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error {
|
||||
return errtypes.NotSupported("unimplemented")
|
||||
}
|
||||
|
||||
// UpdateGrant updates a grant on a resource
|
||||
func (fs *hellofs) UpdateGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error {
|
||||
return errtypes.NotSupported("unimplemented")
|
||||
}
|
||||
|
||||
// arbitrary metadata
|
||||
|
||||
// SetArbitraryMetadata sets arbitraty metadata on a resource
|
||||
func (fs *hellofs) SetArbitraryMetadata(ctx context.Context, ref *provider.Reference, md *provider.ArbitraryMetadata) error {
|
||||
return errtypes.NotSupported("unimplemented")
|
||||
}
|
||||
|
||||
// UnsetArbitraryMetadata removes arbitraty metadata from a resource
|
||||
func (fs *hellofs) UnsetArbitraryMetadata(ctx context.Context, ref *provider.Reference, keys []string) error {
|
||||
return errtypes.NotSupported("unimplemented")
|
||||
}
|
||||
|
||||
// locks
|
||||
|
||||
// GetLock returns an existing lock on the given reference
|
||||
func (fs *hellofs) GetLock(ctx context.Context, ref *provider.Reference) (*provider.Lock, error) {
|
||||
return nil, errtypes.NotSupported("unimplemented")
|
||||
}
|
||||
|
||||
// SetLock puts a lock on the given reference
|
||||
func (fs *hellofs) SetLock(ctx context.Context, ref *provider.Reference, lock *provider.Lock) error {
|
||||
return errtypes.NotSupported("unimplemented")
|
||||
}
|
||||
|
||||
// RefreshLock refreshes an existing lock on the given reference
|
||||
func (fs *hellofs) RefreshLock(ctx context.Context, ref *provider.Reference, lock *provider.Lock, existingLockID string) error {
|
||||
return errtypes.NotSupported("unimplemented")
|
||||
}
|
||||
|
||||
// Unlock removes an existing lock from the given reference
|
||||
func (fs *hellofs) Unlock(ctx context.Context, ref *provider.Reference, lock *provider.Lock) error {
|
||||
return errtypes.NotSupported("unimplemented")
|
||||
}
|
||||
|
||||
// revisions
|
||||
|
||||
// ListRevisions lists all revisions for the referenced resource
|
||||
func (fs *hellofs) ListRevisions(ctx context.Context, ref *provider.Reference) ([]*provider.FileVersion, error) {
|
||||
return nil, errtypes.NotSupported("unimplemented")
|
||||
}
|
||||
|
||||
// DownloadRevision downloads a revision
|
||||
func (fs *hellofs) DownloadRevision(ctx context.Context, ref *provider.Reference, revisionKey string) (io.ReadCloser, error) {
|
||||
return nil, errtypes.NotSupported("unimplemented")
|
||||
}
|
||||
|
||||
// RestoreRevision restores a revision
|
||||
func (fs *hellofs) RestoreRevision(ctx context.Context, ref *provider.Reference, revisionKey string) error {
|
||||
return errtypes.NotSupported("unimplemented")
|
||||
}
|
||||
|
||||
// trash
|
||||
|
||||
// PurgeRecycleItem removes a resource from the recycle bin
|
||||
func (fs *hellofs) PurgeRecycleItem(ctx context.Context, ref *provider.Reference, key, relativePath string) error {
|
||||
return errtypes.NotSupported("unimplemented")
|
||||
}
|
||||
|
||||
// EmptyRecycle removes all resource from the recycle bin
|
||||
func (fs *hellofs) EmptyRecycle(ctx context.Context, ref *provider.Reference) error {
|
||||
return errtypes.NotSupported("unimplemented")
|
||||
}
|
||||
|
||||
// ListRecycle lists the content of the recycle bin
|
||||
func (fs *hellofs) ListRecycle(ctx context.Context, ref *provider.Reference, key, relativePath string) ([]*provider.RecycleItem, error) {
|
||||
return nil, errtypes.NotSupported("unimplemented")
|
||||
}
|
||||
|
||||
// RestoreRecycleItem restores an item from the recyle bin
|
||||
// if restoreRef is nil the resource should be restored at the original path
|
||||
func (fs *hellofs) RestoreRecycleItem(ctx context.Context, ref *provider.Reference, key, relativePath string, restoreRef *provider.Reference) error {
|
||||
return errtypes.NotSupported("unimplemented")
|
||||
}
|
||||
|
||||
// CreateHome creates a users home
|
||||
// Deprecated: use CreateStorageSpace with type personal
|
||||
func (fs *hellofs) CreateHome(ctx context.Context) error {
|
||||
return errtypes.NotSupported("unimplemented")
|
||||
}
|
||||
|
||||
// GetHome returns the path to the users home
|
||||
// Deprecated: use ListStorageSpaces with type personal
|
||||
func (fs *hellofs) GetHome(ctx context.Context) (string, error) {
|
||||
return "", errtypes.NotSupported("unimplemented")
|
||||
}
|
||||
+1
@@ -26,6 +26,7 @@ import (
|
||||
_ "github.com/cs3org/reva/v2/pkg/storage/fs/eosgrpc"
|
||||
_ "github.com/cs3org/reva/v2/pkg/storage/fs/eosgrpchome"
|
||||
_ "github.com/cs3org/reva/v2/pkg/storage/fs/eoshome"
|
||||
_ "github.com/cs3org/reva/v2/pkg/storage/fs/hello"
|
||||
_ "github.com/cs3org/reva/v2/pkg/storage/fs/local"
|
||||
_ "github.com/cs3org/reva/v2/pkg/storage/fs/localhome"
|
||||
_ "github.com/cs3org/reva/v2/pkg/storage/fs/nextcloud"
|
||||
|
||||
+2
-2
@@ -44,7 +44,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
tusd "github.com/tus/tusd/pkg/handler"
|
||||
tusd "github.com/tus/tusd/v2/pkg/handler"
|
||||
)
|
||||
|
||||
var defaultFilePerm = os.FileMode(0664)
|
||||
@@ -388,7 +388,7 @@ func (upload *fileUpload) WriteChunk(ctx context.Context, offset int64, src io.R
|
||||
}
|
||||
|
||||
// GetReader returns an io.Reader for the upload
|
||||
func (upload *fileUpload) GetReader(ctx context.Context) (io.Reader, error) {
|
||||
func (upload *fileUpload) GetReader(ctx context.Context) (io.ReadCloser, error) {
|
||||
return os.Open(upload.binPath)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ import (
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
tusd "github.com/tus/tusd/pkg/handler"
|
||||
tusd "github.com/tus/tusd/v2/pkg/handler"
|
||||
microstore "go-micro.dev/v4/store"
|
||||
|
||||
"github.com/cs3org/reva/v2/pkg/events"
|
||||
|
||||
+107
-35
@@ -23,7 +23,7 @@ import (
|
||||
"io"
|
||||
"net/url"
|
||||
|
||||
tusd "github.com/tus/tusd/pkg/handler"
|
||||
tusd "github.com/tus/tusd/v2/pkg/handler"
|
||||
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1"
|
||||
@@ -31,46 +31,118 @@ import (
|
||||
|
||||
// FS is the interface to implement access to the storage.
|
||||
type FS interface {
|
||||
GetHome(ctx context.Context) (string, error)
|
||||
CreateHome(ctx context.Context) error
|
||||
CreateDir(ctx context.Context, ref *provider.Reference) error
|
||||
TouchFile(ctx context.Context, ref *provider.Reference, markprocessing bool, mtime string) error
|
||||
Delete(ctx context.Context, ref *provider.Reference) error
|
||||
Move(ctx context.Context, oldRef, newRef *provider.Reference) error
|
||||
GetMD(ctx context.Context, ref *provider.Reference, mdKeys, fieldMask []string) (*provider.ResourceInfo, error)
|
||||
ListFolder(ctx context.Context, ref *provider.Reference, mdKeys, fieldMask []string) ([]*provider.ResourceInfo, error)
|
||||
InitiateUpload(ctx context.Context, ref *provider.Reference, uploadLength int64, metadata map[string]string) (map[string]string, error)
|
||||
Upload(ctx context.Context, req UploadRequest, uploadFunc UploadFinishedFunc) (*provider.ResourceInfo, error)
|
||||
Download(ctx context.Context, ref *provider.Reference) (io.ReadCloser, error)
|
||||
ListRevisions(ctx context.Context, ref *provider.Reference) ([]*provider.FileVersion, error)
|
||||
DownloadRevision(ctx context.Context, ref *provider.Reference, key string) (io.ReadCloser, error)
|
||||
RestoreRevision(ctx context.Context, ref *provider.Reference, key string) error
|
||||
ListRecycle(ctx context.Context, ref *provider.Reference, key, relativePath string) ([]*provider.RecycleItem, error)
|
||||
RestoreRecycleItem(ctx context.Context, ref *provider.Reference, key, relativePath string, restoreRef *provider.Reference) error
|
||||
PurgeRecycleItem(ctx context.Context, ref *provider.Reference, key, relativePath string) error
|
||||
EmptyRecycle(ctx context.Context, ref *provider.Reference) error
|
||||
GetPathByID(ctx context.Context, id *provider.ResourceId) (string, error)
|
||||
AddGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error
|
||||
DenyGrant(ctx context.Context, ref *provider.Reference, g *provider.Grantee) error
|
||||
RemoveGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error
|
||||
UpdateGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error
|
||||
ListGrants(ctx context.Context, ref *provider.Reference) ([]*provider.Grant, error)
|
||||
GetQuota(ctx context.Context, ref *provider.Reference) ( /*TotalBytes*/ uint64 /*UsedBytes*/, uint64 /*RemainingBytes*/, uint64, error)
|
||||
CreateReference(ctx context.Context, path string, targetURI *url.URL) error
|
||||
// Minimal set for a readonly storage driver
|
||||
|
||||
// Shutdown is called when the process is exiting to give the driver a chance to flush and close all open handles
|
||||
Shutdown(ctx context.Context) error
|
||||
SetArbitraryMetadata(ctx context.Context, ref *provider.Reference, md *provider.ArbitraryMetadata) error
|
||||
UnsetArbitraryMetadata(ctx context.Context, ref *provider.Reference, keys []string) error
|
||||
SetLock(ctx context.Context, ref *provider.Reference, lock *provider.Lock) error
|
||||
GetLock(ctx context.Context, ref *provider.Reference) (*provider.Lock, error)
|
||||
RefreshLock(ctx context.Context, ref *provider.Reference, lock *provider.Lock, existingLockID string) error
|
||||
Unlock(ctx context.Context, ref *provider.Reference, lock *provider.Lock) error
|
||||
|
||||
// ListStorageSpaces lists the spaces in the storage.
|
||||
// The unrestricted parameter can be used to list other user's spaces when
|
||||
// the user has the necessary permissions.
|
||||
// FIXME The unrestricted parameter is an implementation detail of decomposedfs, remove it from the function?
|
||||
ListStorageSpaces(ctx context.Context, filter []*provider.ListStorageSpacesRequest_Filter, unrestricted bool) ([]*provider.StorageSpace, error)
|
||||
|
||||
// GetQuota returns the quota on the referenced resource
|
||||
GetQuota(ctx context.Context, ref *provider.Reference) ( /*TotalBytes*/ uint64 /*UsedBytes*/, uint64 /*RemainingBytes*/, uint64, error)
|
||||
|
||||
// GetMD returns the resuorce info for the referenced resource
|
||||
GetMD(ctx context.Context, ref *provider.Reference, mdKeys, fieldMask []string) (*provider.ResourceInfo, error)
|
||||
// ListFolder returns the resource infos for all children of the referenced resource
|
||||
ListFolder(ctx context.Context, ref *provider.Reference, mdKeys, fieldMask []string) ([]*provider.ResourceInfo, error)
|
||||
// Download returns a ReadCloser for the content of the referenced resource
|
||||
Download(ctx context.Context, ref *provider.Reference) (io.ReadCloser, error)
|
||||
|
||||
// GetPathByID returns the path for the given resource id relative to the space root
|
||||
// It should only reveal the path visible to the current user to not leak the names uf unshared parent resources
|
||||
// FIXME should be deprecated in favor of calls to GetMD and the fieldmask 'path'
|
||||
GetPathByID(ctx context.Context, id *provider.ResourceId) (string, error)
|
||||
|
||||
// Functions for a writeable storage space
|
||||
|
||||
// CreateReference creates a resource of type reference
|
||||
CreateReference(ctx context.Context, path string, targetURI *url.URL) error
|
||||
// CreateDir creates a resource of type container
|
||||
CreateDir(ctx context.Context, ref *provider.Reference) error
|
||||
// TouchFile sets the mtime of a resource, creating an empty file if it does not exist
|
||||
// FIXME the markprocessing flag is an implementation detail of decomposedfs, remove it from the function
|
||||
// FIXME the mtime should either be a time.Time or a CS3 Timestamp, not a string
|
||||
TouchFile(ctx context.Context, ref *provider.Reference, markprocessing bool, mtime string) error
|
||||
// Delete deletes a resource.
|
||||
// If the storage driver supports a recycle bin it should moves it to the recycle bin
|
||||
Delete(ctx context.Context, ref *provider.Reference) error
|
||||
// Move changes the path of a resource
|
||||
Move(ctx context.Context, oldRef, newRef *provider.Reference) error
|
||||
// InitiateUpload returns a list of protocols with urls that can be used to append bytes to a new upload session
|
||||
InitiateUpload(ctx context.Context, ref *provider.Reference, uploadLength int64, metadata map[string]string) (map[string]string, error)
|
||||
// Upload creates or updates a resource of type file with a new revision
|
||||
Upload(ctx context.Context, req UploadRequest, uploadFunc UploadFinishedFunc) (*provider.ResourceInfo, error)
|
||||
|
||||
// Revisions
|
||||
|
||||
// ListRevisions lists all revisions for the referenced resource
|
||||
ListRevisions(ctx context.Context, ref *provider.Reference) ([]*provider.FileVersion, error)
|
||||
// DownloadRevision downloads a revision
|
||||
DownloadRevision(ctx context.Context, ref *provider.Reference, key string) (io.ReadCloser, error)
|
||||
// RestoreRevision restores a revision
|
||||
RestoreRevision(ctx context.Context, ref *provider.Reference, key string) error
|
||||
|
||||
// Recyce bin
|
||||
|
||||
// ListRecycle lists the content of the recycle bin
|
||||
ListRecycle(ctx context.Context, ref *provider.Reference, key, relativePath string) ([]*provider.RecycleItem, error)
|
||||
// RestoreRecycleItem restores an item from the recyle bin
|
||||
// if restoreRef is nil the resource should be restored at the original path
|
||||
RestoreRecycleItem(ctx context.Context, ref *provider.Reference, key, relativePath string, restoreRef *provider.Reference) error
|
||||
// PurgeRecycleItem removes a resource from the recycle bin
|
||||
PurgeRecycleItem(ctx context.Context, ref *provider.Reference, key, relativePath string) error
|
||||
// EmptyRecycle removes all resource from the recycle bin
|
||||
EmptyRecycle(ctx context.Context, ref *provider.Reference) error
|
||||
|
||||
// Grants
|
||||
|
||||
// AddGrant adds a grant to a resource
|
||||
AddGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error
|
||||
// DenyGrant marks a resource as denied for a recipient
|
||||
// The resource and its children must be completely hidden for the recipient
|
||||
DenyGrant(ctx context.Context, ref *provider.Reference, g *provider.Grantee) error
|
||||
// RemoveGrant removes a grant from a resource
|
||||
RemoveGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error
|
||||
// UpdateGrant updates a grant on a resource
|
||||
UpdateGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error
|
||||
// ListGrants lists all grants on a resource
|
||||
ListGrants(ctx context.Context, ref *provider.Reference) ([]*provider.Grant, error)
|
||||
|
||||
// Arbitrary Metadata
|
||||
|
||||
// SetArbitraryMetadata sets arbitraty metadata on a resource
|
||||
SetArbitraryMetadata(ctx context.Context, ref *provider.Reference, md *provider.ArbitraryMetadata) error
|
||||
// UnsetArbitraryMetadata removes arbitraty metadata from a resource
|
||||
UnsetArbitraryMetadata(ctx context.Context, ref *provider.Reference, keys []string) error
|
||||
|
||||
// Locks
|
||||
|
||||
// GetLock returns an existing lock on the given reference
|
||||
GetLock(ctx context.Context, ref *provider.Reference) (*provider.Lock, error)
|
||||
// SetLock puts a lock on the given reference
|
||||
SetLock(ctx context.Context, ref *provider.Reference, lock *provider.Lock) error
|
||||
// RefreshLock refreshes an existing lock on the given reference
|
||||
RefreshLock(ctx context.Context, ref *provider.Reference, lock *provider.Lock, existingLockID string) error
|
||||
// Unlock removes an existing lock from the given reference
|
||||
Unlock(ctx context.Context, ref *provider.Reference, lock *provider.Lock) error
|
||||
|
||||
// Spaces
|
||||
|
||||
// CreateStorageSpace creates a storage space
|
||||
CreateStorageSpace(ctx context.Context, req *provider.CreateStorageSpaceRequest) (*provider.CreateStorageSpaceResponse, error)
|
||||
// UpdateStorageSpace updates a storage space
|
||||
UpdateStorageSpace(ctx context.Context, req *provider.UpdateStorageSpaceRequest) (*provider.UpdateStorageSpaceResponse, error)
|
||||
// DeleteStorageSpace deletes a storage space
|
||||
DeleteStorageSpace(ctx context.Context, req *provider.DeleteStorageSpaceRequest) error
|
||||
|
||||
// CreateHome creates a users home
|
||||
// Deprecated: use CreateStorageSpace with type personal
|
||||
CreateHome(ctx context.Context) error
|
||||
// GetHome returns the path to the users home
|
||||
// Deprecated: use ListStorageSpaces with type personal
|
||||
GetHome(ctx context.Context) (string, error)
|
||||
}
|
||||
|
||||
// UnscopeFunc is a function that unscopes a user
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ import (
|
||||
|
||||
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"
|
||||
tusd "github.com/tus/tusd/v2/pkg/handler"
|
||||
)
|
||||
|
||||
// UploadFinishedFunc is a callback function used in storage drivers to indicate that an upload has finished
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ import (
|
||||
"github.com/cs3org/reva/v2/pkg/utils"
|
||||
"github.com/jellydator/ttlcache/v2"
|
||||
"github.com/pkg/errors"
|
||||
tusd "github.com/tus/tusd/pkg/handler"
|
||||
tusd "github.com/tus/tusd/v2/pkg/handler"
|
||||
microstore "go-micro.dev/v4/store"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
tusd "github.com/tus/tusd/pkg/handler"
|
||||
tusd "github.com/tus/tusd/v2/pkg/handler"
|
||||
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/cs3org/reva/v2/pkg/appctx"
|
||||
|
||||
Generated
Vendored
+1
-1
@@ -26,7 +26,7 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
tusd "github.com/tus/tusd/pkg/handler"
|
||||
tusd "github.com/tus/tusd/v2/pkg/handler"
|
||||
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rogpeppe/go-internal/lockedfile"
|
||||
tusd "github.com/tus/tusd/pkg/handler"
|
||||
tusd "github.com/tus/tusd/v2/pkg/handler"
|
||||
)
|
||||
|
||||
var _idRegexp = regexp.MustCompile(".*/([^/]+).info")
|
||||
|
||||
+2
-2
@@ -40,7 +40,7 @@ import (
|
||||
"github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/node"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/pkg/errors"
|
||||
tusd "github.com/tus/tusd/pkg/handler"
|
||||
tusd "github.com/tus/tusd/v2/pkg/handler"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
@@ -94,7 +94,7 @@ func (session *OcisSession) GetInfo(_ context.Context) (tusd.FileInfo, error) {
|
||||
}
|
||||
|
||||
// GetReader returns an io.Reader for the upload
|
||||
func (session *OcisSession) GetReader(ctx context.Context) (io.Reader, error) {
|
||||
func (session *OcisSession) GetReader(ctx context.Context) (io.ReadCloser, error) {
|
||||
_, span := tracer.Start(session.Context(ctx), "GetReader")
|
||||
defer span.End()
|
||||
return os.Open(session.binPath())
|
||||
|
||||
+2
-2
@@ -35,7 +35,7 @@ import (
|
||||
"github.com/cs3org/reva/v2/pkg/utils"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
tusd "github.com/tus/tusd/pkg/handler"
|
||||
tusd "github.com/tus/tusd/v2/pkg/handler"
|
||||
)
|
||||
|
||||
var defaultFilePerm = os.FileMode(0664)
|
||||
@@ -296,7 +296,7 @@ func (upload *fileUpload) GetInfo(ctx context.Context) (tusd.FileInfo, error) {
|
||||
}
|
||||
|
||||
// GetReader returns an io.Reader for the upload
|
||||
func (upload *fileUpload) GetReader(ctx context.Context) (io.Reader, error) {
|
||||
func (upload *fileUpload) GetReader(ctx context.Context) (io.ReadCloser, error) {
|
||||
return os.Open(upload.binPath)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ import (
|
||||
"io"
|
||||
"net/url"
|
||||
|
||||
tusd "github.com/tus/tusd/pkg/handler"
|
||||
tusd "github.com/tus/tusd/v2/pkg/handler"
|
||||
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/cs3org/reva/v2/pkg/storage"
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2013-2017 Transloadit Ltd and Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// bodyReader is an io.Reader, which is intended to wrap the request
|
||||
// body reader. If an error occurr during reading the request body, it
|
||||
// will not return this error to the reading entity, but instead store
|
||||
// the error and close the io.Reader, so that the error can be checked
|
||||
// afterwards. This is helpful, so that the stores do not have to handle
|
||||
// the error but this can instead be done in the handler.
|
||||
// In addition, the bodyReader keeps track of how many bytes were read.
|
||||
type bodyReader struct {
|
||||
// bytesCounter is the first field to ensure that it's properly aligned,
|
||||
// otherwise we run into alignment issues on some 32-bit builds.
|
||||
// See https://github.com/tus/tusd/issues/1047
|
||||
// See https://pkg.go.dev/sync/atomic#pkg-note-BUG
|
||||
// TODO: In the future we should move all of these values to the safe
|
||||
// atomic.Uint64 type, which takes care of alignment automatically.
|
||||
bytesCounter int64
|
||||
ctx *httpContext
|
||||
reader io.ReadCloser
|
||||
err error
|
||||
onReadDone func()
|
||||
}
|
||||
|
||||
func newBodyReader(c *httpContext, maxSize int64) *bodyReader {
|
||||
return &bodyReader{
|
||||
ctx: c,
|
||||
reader: http.MaxBytesReader(c.res, c.req.Body, maxSize),
|
||||
onReadDone: func() {},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *bodyReader) Read(b []byte) (int, error) {
|
||||
if r.err != nil {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
n, err := r.reader.Read(b)
|
||||
atomic.AddInt64(&r.bytesCounter, int64(n))
|
||||
if !errors.Is(err, os.ErrDeadlineExceeded) {
|
||||
// If the timeout wasn't exceeded (due to SetReadDeadline), invoke
|
||||
// the callback so the deadline can be extended
|
||||
r.onReadDone()
|
||||
|
||||
}
|
||||
if err != nil {
|
||||
// Note: if an error occurs while reading the body, we must set `r.err` (either in here
|
||||
// or somewhere else, such as in closeWithError). Otherwise, the PATCH handler might not know
|
||||
// that an error occurred and assumes that a request was transferred succesfully even though
|
||||
// it was interrupted. This leads to problems with the RUFH draft.
|
||||
|
||||
// io.EOF means that the request body was fully read and does not represent an error.
|
||||
if err == io.EOF {
|
||||
return n, io.EOF
|
||||
}
|
||||
|
||||
// http.ErrBodyReadAfterClose means that the bodyReader closed the request body because the upload is
|
||||
// is stopped or the server shuts down. In this case, the closeWithError method already
|
||||
// set `r.err` and thus we don't overerwrite it here but just return.
|
||||
if err == http.ErrBodyReadAfterClose {
|
||||
return n, io.EOF
|
||||
}
|
||||
|
||||
// All of the following errors can be understood as the input stream ending too soon:
|
||||
// - io.ErrClosedPipe is returned in the package's unit test with io.Pipe()
|
||||
// - io.UnexpectedEOF means that the client aborted the request.
|
||||
if err == io.ErrClosedPipe || err == io.ErrUnexpectedEOF {
|
||||
err = ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
// Connection resets are not dropped silently, but responded to the client.
|
||||
// We change the error because otherwise the message would contain the local address,
|
||||
// which is unnecessary to be included in the response.
|
||||
if strings.HasSuffix(err.Error(), "read: connection reset by peer") {
|
||||
err = ErrConnectionReset
|
||||
}
|
||||
|
||||
// For timeouts, we also send a nicer response to the clients.
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
err = ErrReadTimeout
|
||||
}
|
||||
|
||||
// MaxBytesError is returned from http.MaxBytesReader, which we use to limit
|
||||
// the request body size.
|
||||
maxBytesErr := &http.MaxBytesError{}
|
||||
if errors.As(err, &maxBytesErr) {
|
||||
err = ErrSizeExceeded
|
||||
}
|
||||
|
||||
// Other errors are stored for retrival with hasError, but is not returned
|
||||
// to the consumer. We do not overwrite an error if it has been set already.
|
||||
if r.err == nil {
|
||||
r.err = err
|
||||
}
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (r bodyReader) hasError() error {
|
||||
if r.err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
|
||||
return r.err
|
||||
}
|
||||
|
||||
func (r *bodyReader) bytesRead() int64 {
|
||||
return atomic.LoadInt64(&r.bytesCounter)
|
||||
}
|
||||
|
||||
func (r *bodyReader) closeWithError(err error) {
|
||||
r.err = err
|
||||
|
||||
// SetReadDeadline with the current time causes concurrent reads to the body to time out,
|
||||
// so the body will be closed sooner with less delay.
|
||||
if err := r.ctx.resC.SetReadDeadline(time.Now()); err != nil {
|
||||
r.ctx.log.Warn("NetworkTimeoutError", "error", err)
|
||||
}
|
||||
|
||||
r.reader.Close()
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package handler
|
||||
|
||||
// StoreComposer represents a composable data store. It consists of the core
|
||||
// data store and optional extensions. Please consult the package's overview
|
||||
// for a more detailed introduction in how to use this structure.
|
||||
type StoreComposer struct {
|
||||
Core DataStore
|
||||
|
||||
UsesTerminater bool
|
||||
Terminater TerminaterDataStore
|
||||
UsesLocker bool
|
||||
Locker Locker
|
||||
UsesConcater bool
|
||||
Concater ConcaterDataStore
|
||||
UsesLengthDeferrer bool
|
||||
LengthDeferrer LengthDeferrerDataStore
|
||||
}
|
||||
|
||||
// NewStoreComposer creates a new and empty store composer.
|
||||
func NewStoreComposer() *StoreComposer {
|
||||
return &StoreComposer{}
|
||||
}
|
||||
|
||||
// Capabilities returns a string representing the provided extensions in a
|
||||
// human-readable format meant for debugging.
|
||||
func (store *StoreComposer) Capabilities() string {
|
||||
str := "Core: "
|
||||
|
||||
if store.Core != nil {
|
||||
str += "✓"
|
||||
} else {
|
||||
str += "✗"
|
||||
}
|
||||
|
||||
str += ` Terminater: `
|
||||
if store.UsesTerminater {
|
||||
str += "✓"
|
||||
} else {
|
||||
str += "✗"
|
||||
}
|
||||
str += ` Locker: `
|
||||
if store.UsesLocker {
|
||||
str += "✓"
|
||||
} else {
|
||||
str += "✗"
|
||||
}
|
||||
str += ` Concater: `
|
||||
if store.UsesConcater {
|
||||
str += "✓"
|
||||
} else {
|
||||
str += "✗"
|
||||
}
|
||||
str += ` LengthDeferrer: `
|
||||
if store.UsesLengthDeferrer {
|
||||
str += "✓"
|
||||
} else {
|
||||
str += "✗"
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
// UseCore will set the used core data store. If the argument is nil, the
|
||||
// property will be unset.
|
||||
func (store *StoreComposer) UseCore(core DataStore) {
|
||||
store.Core = core
|
||||
}
|
||||
|
||||
func (store *StoreComposer) UseTerminater(ext TerminaterDataStore) {
|
||||
store.UsesTerminater = ext != nil
|
||||
store.Terminater = ext
|
||||
}
|
||||
|
||||
func (store *StoreComposer) UseLocker(ext Locker) {
|
||||
store.UsesLocker = ext != nil
|
||||
store.Locker = ext
|
||||
}
|
||||
|
||||
func (store *StoreComposer) UseConcater(ext ConcaterDataStore) {
|
||||
store.UsesConcater = ext != nil
|
||||
store.Concater = ext
|
||||
}
|
||||
|
||||
func (store *StoreComposer) UseLengthDeferrer(ext LengthDeferrerDataStore) {
|
||||
store.UsesLengthDeferrer = ext != nil
|
||||
store.LengthDeferrer = ext
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package handler
|
||||
|
||||
#define USE_FUNC(TYPE) \
|
||||
func (store *StoreComposer) Use ## TYPE(ext TYPE ## DataStore) { \
|
||||
store.Uses ## TYPE = ext != nil; \
|
||||
store.TYPE = ext; \
|
||||
}
|
||||
|
||||
#define USE_FIELD(TYPE) Uses ## TYPE bool; \
|
||||
TYPE TYPE ## DataStore
|
||||
|
||||
#define USE_FROM(TYPE) if mod, ok := store.(TYPE ## DataStore); ok { \
|
||||
composer.Use ## TYPE (mod) \
|
||||
}
|
||||
|
||||
#define USE_CAP(TYPE) str += ` TYPE: `; \
|
||||
if store.Uses ## TYPE { \
|
||||
str += "✓" \
|
||||
} else { \
|
||||
str += "✗" \
|
||||
}
|
||||
|
||||
// StoreComposer represents a composable data store. It consists of the core
|
||||
// data store and optional extensions. Please consult the package's overview
|
||||
// for a more detailed introduction in how to use this structure.
|
||||
type StoreComposer struct {
|
||||
Core DataStore
|
||||
|
||||
USE_FIELD(Terminater)
|
||||
USE_FIELD(Finisher)
|
||||
USE_FIELD(Locker)
|
||||
USE_FIELD(GetReader)
|
||||
USE_FIELD(Concater)
|
||||
USE_FIELD(LengthDeferrer)
|
||||
}
|
||||
|
||||
// NewStoreComposer creates a new and empty store composer.
|
||||
func NewStoreComposer() *StoreComposer {
|
||||
return &StoreComposer{}
|
||||
}
|
||||
|
||||
// Capabilities returns a string representing the provided extensions in a
|
||||
// human-readable format meant for debugging.
|
||||
func (store *StoreComposer) Capabilities() string {
|
||||
str := "Core: "
|
||||
|
||||
if store.Core != nil {
|
||||
str += "✓"
|
||||
} else {
|
||||
str += "✗"
|
||||
}
|
||||
|
||||
USE_CAP(Terminater)
|
||||
USE_CAP(Finisher)
|
||||
USE_CAP(Locker)
|
||||
USE_CAP(GetReader)
|
||||
USE_CAP(Concater)
|
||||
USE_CAP(LengthDeferrer)
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
// UseCore will set the used core data store. If the argument is nil, the
|
||||
// property will be unset.
|
||||
func (store *StoreComposer) UseCore(core DataStore) {
|
||||
store.Core = core
|
||||
}
|
||||
|
||||
USE_FUNC(Terminater)
|
||||
USE_FUNC(Finisher)
|
||||
USE_FUNC(Locker)
|
||||
USE_FUNC(GetReader)
|
||||
USE_FUNC(Concater)
|
||||
USE_FUNC(LengthDeferrer)
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
||||
// Config provides a way to configure the Handler depending on your needs.
|
||||
type Config struct {
|
||||
// StoreComposer points to the store composer from which the core data store
|
||||
// and optional dependencies should be taken. May only be nil if DataStore is
|
||||
// set.
|
||||
StoreComposer *StoreComposer
|
||||
// MaxSize defines how many bytes may be stored in one single upload. If its
|
||||
// value is is 0 or smaller no limit will be enforced.
|
||||
MaxSize int64
|
||||
// BasePath defines the URL path used for handling uploads, e.g. "/files/".
|
||||
// If no trailing slash is presented it will be added. You may specify an
|
||||
// absolute URL containing a scheme, e.g. "http://tus.io"
|
||||
BasePath string
|
||||
isAbs bool
|
||||
// EnableExperimentalProtocol controls whether the new resumable upload protocol draft
|
||||
// from the IETF's HTTP working group is accepted next to the current tus v1 protocol.
|
||||
// See https://datatracker.ietf.org/doc/draft-ietf-httpbis-resumable-upload/
|
||||
EnableExperimentalProtocol bool
|
||||
// DisableDownload indicates whether the server will refuse downloads of the
|
||||
// uploaded file, by not mounting the GET handler.
|
||||
DisableDownload bool
|
||||
// DisableTermination indicates whether the server will refuse termination
|
||||
// requests of the uploaded file, by not mounting the DELETE handler.
|
||||
DisableTermination bool
|
||||
// Cors can be used to customize the handling of Cross-Origin Resource Sharing (CORS).
|
||||
// See the CorsConfig struct for more details.
|
||||
// Defaults to DefaultCorsConfig.
|
||||
Cors *CorsConfig
|
||||
// NotifyCompleteUploads indicates whether sending notifications about
|
||||
// completed uploads using the CompleteUploads channel should be enabled.
|
||||
NotifyCompleteUploads bool
|
||||
// NotifyTerminatedUploads indicates whether sending notifications about
|
||||
// terminated uploads using the TerminatedUploads channel should be enabled.
|
||||
NotifyTerminatedUploads bool
|
||||
// NotifyUploadProgress indicates whether sending notifications about
|
||||
// the upload progress using the UploadProgress channel should be enabled.
|
||||
NotifyUploadProgress bool
|
||||
// NotifyCreatedUploads indicates whether sending notifications about
|
||||
// the upload having been created using the CreatedUploads channel should be enabled.
|
||||
NotifyCreatedUploads bool
|
||||
// UploadProgressInterval specifies the interval at which the upload progress
|
||||
// notifications are sent to the UploadProgress channel, if enabled.
|
||||
// Defaults to 1s.
|
||||
UploadProgressInterval time.Duration
|
||||
// Logger is the logger to use internally, mostly for printing requests.
|
||||
Logger *slog.Logger
|
||||
// Respect the X-Forwarded-Host, X-Forwarded-Proto and Forwarded headers
|
||||
// potentially set by proxies when generating an absolute URL in the
|
||||
// response to POST requests.
|
||||
RespectForwardedHeaders bool
|
||||
// PreUploadCreateCallback will be invoked before a new upload is created, if the
|
||||
// property is supplied. If the callback returns no error, the upload will be created
|
||||
// and optional values from HTTPResponse will be contained in the HTTP response.
|
||||
// If the error is non-nil, the upload will not be created. This can be used to implement
|
||||
// validation of upload metadata etc. Furthermore, HTTPResponse will be ignored and
|
||||
// the error value can contain values for the HTTP response.
|
||||
// If the error is nil, FileInfoChanges can be filled out to specify individual properties
|
||||
// that should be overwriten before the upload is create. See its type definition for
|
||||
// more details on its behavior. If you do not want to make any changes, return an empty struct.
|
||||
PreUploadCreateCallback func(hook HookEvent) (HTTPResponse, FileInfoChanges, error)
|
||||
// PreFinishResponseCallback will be invoked after an upload is completed but before
|
||||
// a response is returned to the client. This can be used to implement post-processing validation.
|
||||
// If the callback returns no error, optional values from HTTPResponse will be contained in the HTTP response.
|
||||
// If the error is non-nil, the error will be forwarded to the client. Furthermore,
|
||||
// HTTPResponse will be ignored and the error value can contain values for the HTTP response.
|
||||
PreFinishResponseCallback func(hook HookEvent) (HTTPResponse, error)
|
||||
// GracefulRequestCompletionTimeout is the timeout for operations to complete after an HTTP
|
||||
// request has ended (successfully or by error). For example, if an HTTP request is interrupted,
|
||||
// instead of stopping immediately, the handler and data store will be given some additional
|
||||
// time to wrap up their operations and save any uploaded data. GracefulRequestCompletionTimeout
|
||||
// controls this time.
|
||||
// See HookEvent.Context for more details.
|
||||
// Defaults to 10s.
|
||||
GracefulRequestCompletionTimeout time.Duration
|
||||
// AcquireLockTimeout is the duration that a request handler will wait to acquire a lock for
|
||||
// an upload. If the timeout is reached, it will stop waiting and send an error response to the
|
||||
// client.
|
||||
// Defaults to 20s.
|
||||
AcquireLockTimeout time.Duration
|
||||
// NetworkTimeout is the timeout for individual read operations on the request body. If the
|
||||
// read operation succeeds in this time window, the handler will continue consuming the body.
|
||||
// If a read operation times out, the handler will stop reading and close the request.
|
||||
// This ensures that an upload is consumed while data is being transmitted, while also closing
|
||||
// dead connections.
|
||||
// Under the hood, this is passed to ResponseController.SetReadDeadline
|
||||
// Defaults to 60s
|
||||
NetworkTimeout time.Duration
|
||||
}
|
||||
|
||||
// CorsConfig provides a way to customize the the handling of Cross-Origin Resource Sharing (CORS).
|
||||
// More details about CORS are available at https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS.
|
||||
type CorsConfig struct {
|
||||
// Disable instructs the handler to ignore all CORS-related headers and never set a
|
||||
// CORS-related header in a response. This is useful if CORS is already handled by a proxy.
|
||||
Disable bool
|
||||
// AllowOrigin is a regular expression used to check if a request is allowed to participate in the
|
||||
// CORS protocol. If the request's Origin header matches the regular expression, CORS is allowed.
|
||||
// If not, a 403 Forbidden response is sent, rejecting the CORS request.
|
||||
AllowOrigin *regexp.Regexp
|
||||
// AllowCredentials defines whether the `Access-Control-Allow-Credentials: true` header should be
|
||||
// included in CORS responses. This allows clients to share credentials using the Cookie and
|
||||
// Authorization header
|
||||
AllowCredentials bool
|
||||
// AllowMethods defines the value for the `Access-Control-Allow-Methods` header in the response to
|
||||
// preflight requests. You can add custom methods here, but make sure that all tus-specific methods
|
||||
// from DefaultConfig.AllowMethods are included as well.
|
||||
AllowMethods string
|
||||
// AllowHeaders defines the value for the `Access-Control-Allow-Headers` header in the response to
|
||||
// preflight requests. You can add custom headers here, but make sure that all tus-specific header
|
||||
// from DefaultConfig.AllowHeaders are included as well.
|
||||
AllowHeaders string
|
||||
// MaxAge defines the value for the `Access-Control-Max-Age` header in the response to preflight
|
||||
// requests.
|
||||
MaxAge string
|
||||
// ExposeHeaders defines the value for the `Access-Control-Expose-Headers` header in the response to
|
||||
// actual requests. You can add custom headers here, but make sure that all tus-specific header
|
||||
// from DefaultConfig.ExposeHeaders are included as well.
|
||||
ExposeHeaders string
|
||||
}
|
||||
|
||||
// DefaultCorsConfig is the configuration that will be used in none is provided.
|
||||
var DefaultCorsConfig = CorsConfig{
|
||||
Disable: false,
|
||||
AllowOrigin: regexp.MustCompile(".*"),
|
||||
AllowCredentials: false,
|
||||
AllowMethods: "POST, HEAD, PATCH, OPTIONS, GET, DELETE",
|
||||
AllowHeaders: "Authorization, Origin, X-Requested-With, X-Request-ID, X-HTTP-Method-Override, Content-Type, Upload-Length, Upload-Offset, Tus-Resumable, Upload-Metadata, Upload-Defer-Length, Upload-Concat, Upload-Incomplete, Upload-Complete, Upload-Draft-Interop-Version",
|
||||
MaxAge: "86400",
|
||||
ExposeHeaders: "Upload-Offset, Location, Upload-Length, Tus-Version, Tus-Resumable, Tus-Max-Size, Tus-Extension, Upload-Metadata, Upload-Defer-Length, Upload-Concat, Upload-Incomplete, Upload-Complete, Upload-Draft-Interop-Version",
|
||||
}
|
||||
|
||||
func (config *Config) validate() error {
|
||||
if config.Logger == nil {
|
||||
config.Logger = slog.Default()
|
||||
}
|
||||
|
||||
base := config.BasePath
|
||||
uri, err := url.Parse(base)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure base path ends with slash to remove logic from absFileURL
|
||||
if base != "" && string(base[len(base)-1]) != "/" {
|
||||
base += "/"
|
||||
}
|
||||
|
||||
// Ensure base path begins with slash if not absolute (starts with scheme)
|
||||
if !uri.IsAbs() && len(base) > 0 && string(base[0]) != "/" {
|
||||
base = "/" + base
|
||||
}
|
||||
config.BasePath = base
|
||||
config.isAbs = uri.IsAbs()
|
||||
|
||||
if config.StoreComposer == nil {
|
||||
return errors.New("tusd: StoreComposer must no be nil")
|
||||
}
|
||||
|
||||
if config.StoreComposer.Core == nil {
|
||||
return errors.New("tusd: StoreComposer in Config needs to contain a non-nil core")
|
||||
}
|
||||
|
||||
if config.UploadProgressInterval <= 0 {
|
||||
config.UploadProgressInterval = 1 * time.Second
|
||||
}
|
||||
|
||||
if config.GracefulRequestCompletionTimeout <= 0 {
|
||||
config.GracefulRequestCompletionTimeout = 10 * time.Second
|
||||
}
|
||||
|
||||
if config.AcquireLockTimeout <= 0 {
|
||||
config.AcquireLockTimeout = 20 * time.Second
|
||||
}
|
||||
|
||||
if config.NetworkTimeout <= 0 {
|
||||
config.NetworkTimeout = 60 * time.Second
|
||||
}
|
||||
|
||||
if config.Cors == nil {
|
||||
config.Cors = &DefaultCorsConfig
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
||||
// httpContext is wrapper around context.Context that also carries the
|
||||
// corresponding HTTP request and response writer, as well as an
|
||||
// optional body reader
|
||||
type httpContext struct {
|
||||
context.Context
|
||||
|
||||
// res and req are the native request and response instances
|
||||
res http.ResponseWriter
|
||||
resC *http.ResponseController
|
||||
req *http.Request
|
||||
|
||||
// body is nil by default and set by the user if the request body is consumed.
|
||||
body *bodyReader
|
||||
|
||||
// cancel allows a user to cancel the internal request context, causing
|
||||
// the request body to be closed.
|
||||
cancel context.CancelCauseFunc
|
||||
|
||||
// log is the logger for this request. It gets extended with more properties as the
|
||||
// request progresses and is identified.
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
// newContext constructs a new httpContext for the given request. This should only be done once
|
||||
// per request and the context should be stored in the request, so it can be fetched with getContext.
|
||||
func (h UnroutedHandler) newContext(w http.ResponseWriter, r *http.Request) *httpContext {
|
||||
// requestCtx is the context from the native request instance. It gets cancelled
|
||||
// if the connection closes, the request is cancelled (HTTP/2), ServeHTTP returns
|
||||
// or the server's base context is cancelled.
|
||||
requestCtx := r.Context()
|
||||
// On top of requestCtx, we construct a context that we can cancel, for example when
|
||||
// the post-receive hook stops an upload or if another uploads requests a lock to be released.
|
||||
cancellableCtx, cancelHandling := context.WithCancelCause(requestCtx)
|
||||
// On top of cancellableCtx, we construct a new context which gets cancelled with a delay.
|
||||
// See HookEvent.Context for more details, but the gist is that we want to give data stores
|
||||
// some more time to finish their buisness.
|
||||
delayedCtx := newDelayedContext(cancellableCtx, h.config.GracefulRequestCompletionTimeout)
|
||||
|
||||
ctx := &httpContext{
|
||||
Context: delayedCtx,
|
||||
res: w,
|
||||
resC: http.NewResponseController(w),
|
||||
req: r,
|
||||
body: nil, // body can be filled later for PATCH requests
|
||||
cancel: cancelHandling,
|
||||
log: h.logger.With("method", r.Method, "path", r.URL.Path, "requestId", getRequestId(r)),
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-cancellableCtx.Done()
|
||||
|
||||
// If the cause is one of our own errors, close a potential body and relay the error.
|
||||
cause := context.Cause(cancellableCtx)
|
||||
if (errors.Is(cause, ErrServerShutdown) || errors.Is(cause, ErrUploadInterrupted) || errors.Is(cause, ErrUploadStoppedByServer)) && ctx.body != nil {
|
||||
ctx.body.closeWithError(cause)
|
||||
}
|
||||
}()
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
// getContext tries to retrieve a httpContext from the request or constructs a new one.
|
||||
func (h UnroutedHandler) getContext(w http.ResponseWriter, r *http.Request) *httpContext {
|
||||
c, ok := r.Context().(*httpContext)
|
||||
if !ok {
|
||||
c = h.newContext(w, r)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c httpContext) Value(key any) any {
|
||||
// We overwrite the Value function to ensure that the values from the request
|
||||
// context are returned because c.Context does not contain any values.
|
||||
return c.req.Context().Value(key)
|
||||
}
|
||||
|
||||
// newDelayedContext returns a context that is cancelled with a delay. If the parent context
|
||||
// is done, the new context will also be cancelled but only after waiting the specified delay.
|
||||
// Note: The parent context MUST be cancelled or otherwise this will leak resources. In the
|
||||
// case of http.Request.Context, the net/http package ensures that the context is always cancelled.
|
||||
func newDelayedContext(parent context.Context, delay time.Duration) context.Context {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
<-parent.Done()
|
||||
<-time.After(delay)
|
||||
cancel()
|
||||
}()
|
||||
|
||||
return ctx
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
)
|
||||
|
||||
type MetaData map[string]string
|
||||
|
||||
// FileInfo contains information about a single upload resource.
|
||||
type FileInfo struct {
|
||||
// ID is the unique identifier of the upload resource.
|
||||
ID string
|
||||
// Total file size in bytes specified in the NewUpload call
|
||||
Size int64
|
||||
// Indicates whether the total file size is deferred until later
|
||||
SizeIsDeferred bool
|
||||
// Offset in bytes (zero-based)
|
||||
Offset int64
|
||||
MetaData MetaData
|
||||
// Indicates that this is a partial upload which will later be used to form
|
||||
// a final upload by concatenation. Partial uploads should not be processed
|
||||
// when they are finished since they are only incomplete chunks of files.
|
||||
IsPartial bool
|
||||
// Indicates that this is a final upload
|
||||
IsFinal bool
|
||||
// If the upload is a final one (see IsFinal) this will be a non-empty
|
||||
// ordered slice containing the ids of the uploads of which the final upload
|
||||
// will consist after concatenation.
|
||||
PartialUploads []string
|
||||
// Storage contains information about where the data storage saves the upload,
|
||||
// for example a file path. The available values vary depending on what data
|
||||
// store is used. This map may also be nil.
|
||||
Storage map[string]string
|
||||
|
||||
// stopUpload is a callback for communicating that an upload should by stopped
|
||||
// and interrupt the writes to DataStore#WriteChunk.
|
||||
stopUpload func(HTTPResponse)
|
||||
}
|
||||
|
||||
// StopUpload interrupts a running upload from the server-side. This means that
|
||||
// the current request body is closed, so that the data store does not get any
|
||||
// more data. Furthermore, a response is sent to notify the client of the
|
||||
// interrupting and the upload is terminated (if supported by the data store),
|
||||
// so the upload cannot be resumed anymore. The response to the client can be
|
||||
// optionally modified by providing values in the HTTPResponse struct.
|
||||
func (f FileInfo) StopUpload(response HTTPResponse) {
|
||||
if f.stopUpload != nil {
|
||||
f.stopUpload(response)
|
||||
}
|
||||
}
|
||||
|
||||
// FileInfoChanges collects changes the should be made to a FileInfo struct. This
|
||||
// can be done using the PreUploadCreateCallback to modify certain properties before
|
||||
// an upload is created. Properties which should not be modified (e.g. Size or Offset)
|
||||
// are intentionally left out here.
|
||||
type FileInfoChanges struct {
|
||||
// If ID is not empty, it will be passed to the data store, allowing
|
||||
// hooks to influence the upload ID. Be aware that a data store is not required to
|
||||
// respect a pre-defined upload ID and might overwrite or modify it. However,
|
||||
// all data stores in the github.com/tus/tusd package do respect pre-defined IDs.
|
||||
ID string
|
||||
|
||||
// If MetaData is not nil, it replaces the entire user-defined meta data from
|
||||
// the upload creation request. You can add custom meta data fields this way
|
||||
// or ensure that only certain fields from the user-defined meta data are saved.
|
||||
// If you want to retain only specific entries from the user-defined meta data, you must
|
||||
// manually copy them into this MetaData field.
|
||||
// If you do not want to store any meta data, set this field to an empty map (`MetaData{}`).
|
||||
// If you want to keep the entire user-defined meta data, set this field to nil.
|
||||
MetaData MetaData
|
||||
|
||||
// If Storage is not nil, it is passed to the data store to allow for minor adjustments
|
||||
// to the upload storage (e.g. destination file name). The details are specific for each
|
||||
// data store and should be looked up in their respective documentation.
|
||||
// Please be aware that this behavior is currently not supported by any data store in
|
||||
// the github.com/tus/tusd package.
|
||||
Storage map[string]string
|
||||
}
|
||||
|
||||
type Upload interface {
|
||||
// Write the chunk read from src into the file specified by the id at the
|
||||
// given offset. The handler will take care of validating the offset and
|
||||
// limiting the size of the src to not overflow the file's size.
|
||||
// The handler will also lock resources while they are written to ensure only one
|
||||
// write happens per time.
|
||||
// The function call must return the number of bytes written.
|
||||
WriteChunk(ctx context.Context, offset int64, src io.Reader) (int64, error)
|
||||
// Read the fileinformation used to validate the offset and respond to HEAD
|
||||
// requests.
|
||||
GetInfo(ctx context.Context) (FileInfo, error)
|
||||
// GetReader returns an io.ReadCloser which allows iterating of the content of an
|
||||
// upload. It should attempt to provide a reader even if the upload has not
|
||||
// been finished yet but it's not required.
|
||||
GetReader(ctx context.Context) (io.ReadCloser, error)
|
||||
// FinisherDataStore is the interface which can be implemented by DataStores
|
||||
// which need to do additional operations once an entire upload has been
|
||||
// completed. These tasks may include but are not limited to freeing unused
|
||||
// resources or notifying other services. For example, S3Store uses this
|
||||
// interface for removing a temporary object.
|
||||
FinishUpload(ctx context.Context) error
|
||||
}
|
||||
|
||||
// DataStore is the base interface for storages to implement. It provides functions
|
||||
// to create new uploads and fetch existing ones.
|
||||
//
|
||||
// Note: the context values passed to all functions is not the request's context,
|
||||
// but a similar context. See HookEvent.Context for more details.
|
||||
type DataStore interface {
|
||||
// Create a new upload using the size as the file's length. The method must
|
||||
// return an unique id which is used to identify the upload. If no backend
|
||||
// (e.g. Riak) specifes the id you may want to use the uid package to
|
||||
// generate one. The properties Size and MetaData will be filled.
|
||||
NewUpload(ctx context.Context, info FileInfo) (upload Upload, err error)
|
||||
|
||||
// GetUpload fetches the upload with a given ID. If no such upload can be found,
|
||||
// ErrNotFound must be returned.
|
||||
GetUpload(ctx context.Context, id string) (upload Upload, err error)
|
||||
}
|
||||
|
||||
type TerminatableUpload interface {
|
||||
// Terminate an upload so any further requests to the upload resource will
|
||||
// return the ErrNotFound error.
|
||||
Terminate(ctx context.Context) error
|
||||
}
|
||||
|
||||
// TerminaterDataStore is the interface which must be implemented by DataStores
|
||||
// if they want to receive DELETE requests using the Handler. If this interface
|
||||
// is not implemented, no request handler for this method is attached.
|
||||
type TerminaterDataStore interface {
|
||||
AsTerminatableUpload(upload Upload) TerminatableUpload
|
||||
}
|
||||
|
||||
// ConcaterDataStore is the interface required to be implemented if the
|
||||
// Concatenation extension should be enabled. Only in this case, the handler
|
||||
// will parse and respect the Upload-Concat header.
|
||||
type ConcaterDataStore interface {
|
||||
AsConcatableUpload(upload Upload) ConcatableUpload
|
||||
}
|
||||
|
||||
type ConcatableUpload interface {
|
||||
// ConcatUploads concatenates the content from the provided partial uploads
|
||||
// and writes the result in the destination upload.
|
||||
// The caller (usually the handler) must and will ensure that this
|
||||
// destination upload has been created before with enough space to hold all
|
||||
// partial uploads. The order, in which the partial uploads are supplied,
|
||||
// must be respected during concatenation.
|
||||
ConcatUploads(ctx context.Context, partialUploads []Upload) error
|
||||
}
|
||||
|
||||
// LengthDeferrerDataStore is the interface that must be implemented if the
|
||||
// creation-defer-length extension should be enabled. The extension enables a
|
||||
// client to upload files when their total size is not yet known. Instead, the
|
||||
// client must send the total size as soon as it becomes known.
|
||||
type LengthDeferrerDataStore interface {
|
||||
AsLengthDeclarableUpload(upload Upload) LengthDeclarableUpload
|
||||
}
|
||||
|
||||
type LengthDeclarableUpload interface {
|
||||
DeclareLength(ctx context.Context, length int64) error
|
||||
}
|
||||
|
||||
// Locker is the interface required for custom lock persisting mechanisms.
|
||||
// Common ways to store this information is in memory, on disk or using an
|
||||
// external service, such as Redis.
|
||||
// When multiple processes are attempting to access an upload, whether it be
|
||||
// by reading or writing, a synchronization mechanism is required to prevent
|
||||
// data corruption, especially to ensure correct offset values and the proper
|
||||
// order of chunks inside a single upload.
|
||||
type Locker interface {
|
||||
// NewLock creates a new unlocked lock object for the given upload ID.
|
||||
NewLock(id string) (Lock, error)
|
||||
}
|
||||
|
||||
// Lock is the interface for a lock as returned from a Locker.
|
||||
type Lock interface {
|
||||
// Lock attempts to obtain an exclusive lock for the upload specified
|
||||
// by its id.
|
||||
// If the lock can be acquired, it will return without error. The requestUnlock
|
||||
// callback is invoked when another caller attempts to create a lock. In this
|
||||
// case, the holder of the lock should attempt to release the lock as soon
|
||||
// as possible
|
||||
// If the lock is already held, the holder's requestUnlock function will be
|
||||
// invoked to request the lock to be released. If the context is cancelled before
|
||||
// the lock can be acquired, ErrLockTimeout will be returned without acquiring
|
||||
// the lock.
|
||||
Lock(ctx context.Context, requestUnlock func()) error
|
||||
// Unlock releases an existing lock for the given upload.
|
||||
Unlock() error
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
Package handler provides ways to accept tus 1.0 calls using HTTP.
|
||||
|
||||
tus is a protocol based on HTTP for resumable file uploads. Resumable means that
|
||||
an upload can be interrupted at any moment and can be resumed without
|
||||
re-uploading the previous data again. An interruption may happen willingly, if
|
||||
the user wants to pause, or by accident in case of an network issue or server
|
||||
outage (http://tus.io).
|
||||
|
||||
# The basics of tusd
|
||||
|
||||
tusd was designed in way which allows an flexible and customizable usage. We
|
||||
wanted to avoid binding this package to a specific storage system – particularly
|
||||
a proprietary third-party software. Therefore tusd is an abstract layer whose
|
||||
only job is to accept incoming HTTP requests, validate them according to the
|
||||
specification and finally passes them to the data store.
|
||||
|
||||
The data store is another important component in tusd's architecture whose
|
||||
purpose is to do the actual file handling. It has to write the incoming upload
|
||||
to a persistent storage system and retrieve information about an upload's
|
||||
current state. Therefore it is the only part of the system which communicates
|
||||
directly with the underlying storage system, whether it be the local disk, a
|
||||
remote FTP server or cloud providers such as AWS S3.
|
||||
|
||||
# Using a store composer
|
||||
|
||||
The only hard requirements for a data store can be found in the DataStore
|
||||
interface. It contains methods for creating uploads (NewUpload), writing to
|
||||
them (WriteChunk) and retrieving their status (GetInfo). However, there
|
||||
are many more features which are not mandatory but may still be used.
|
||||
These are contained in their own interfaces which all share the *DataStore
|
||||
suffix. For example, GetReaderDataStore which enables downloading uploads or
|
||||
TerminaterDataStore which allows uploads to be terminated.
|
||||
|
||||
The store composer offers a way to combine the basic data store - the core -
|
||||
implementation and these additional extensions:
|
||||
|
||||
composer := tusd.NewStoreComposer()
|
||||
composer.UseCore(dataStore) // Implements DataStore
|
||||
composer.UseTerminater(terminater) // Implements TerminaterDataStore
|
||||
composer.UseLocker(locker) // Implements LockerDataStore
|
||||
|
||||
The corresponding methods for adding an extension to the composer are prefixed
|
||||
with Use* followed by the name of the corresponding interface. However, most
|
||||
data store provide multiple extensions and adding all of them manually can be
|
||||
tedious and error-prone. Therefore, all data store distributed with tusd provide
|
||||
an UseIn() method which does this job automatically. For example, this is the
|
||||
S3 store in action (see S3Store.UseIn):
|
||||
|
||||
store := s3store.New(…)
|
||||
locker := memorylocker.New()
|
||||
composer := tusd.NewStoreComposer()
|
||||
store.UseIn(composer)
|
||||
locker.UseIn(composer)
|
||||
|
||||
Finally, once you are done with composing your data store, you can pass it
|
||||
inside the Config struct in order to create create a new tusd HTTP handler:
|
||||
|
||||
config := tusd.Config{
|
||||
StoreComposer: composer,
|
||||
BasePath: "/files/",
|
||||
}
|
||||
handler, err := tusd.NewHandler(config)
|
||||
|
||||
This handler can then be mounted to a specific path, e.g. /files:
|
||||
|
||||
http.Handle("/files/", http.StripPrefix("/files/", handler))
|
||||
*/
|
||||
package handler
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package handler
|
||||
|
||||
// Error represents an error with the intent to be sent in the HTTP
|
||||
// response to the client. Therefore, it also contains a HTTPResponse,
|
||||
// next to an error code and error message.
|
||||
type Error struct {
|
||||
ErrorCode string
|
||||
Message string
|
||||
HTTPResponse HTTPResponse
|
||||
}
|
||||
|
||||
func (e Error) Error() string {
|
||||
return e.ErrorCode + ": " + e.Message
|
||||
}
|
||||
|
||||
func (e1 Error) Is(target error) bool {
|
||||
e2, ok := target.(Error)
|
||||
return ok && e1.ErrorCode == e2.ErrorCode
|
||||
}
|
||||
|
||||
// NewError constructs a new Error object with the given error code and message.
|
||||
// The corresponding HTTP response will have the provided status code
|
||||
// and a body consisting of the error details.
|
||||
// responses. See the net/http package for standardized status codes.
|
||||
func NewError(errCode string, message string, statusCode int) Error {
|
||||
return Error{
|
||||
ErrorCode: errCode,
|
||||
Message: message,
|
||||
HTTPResponse: HTTPResponse{
|
||||
StatusCode: statusCode,
|
||||
Body: errCode + ": " + message + "\n",
|
||||
Header: HTTPHeader{
|
||||
"Content-Type": "text/plain; charset=utf-8",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Handler is a ready to use handler with routing
|
||||
type Handler struct {
|
||||
*UnroutedHandler
|
||||
http.Handler
|
||||
}
|
||||
|
||||
// NewHandler creates a routed tus protocol handler. This is the simplest
|
||||
// way to use tusd but may not be as configurable as you require. If you are
|
||||
// integrating this into an existing app you may like to use tusd.NewUnroutedHandler
|
||||
// instead. Using tusd.NewUnroutedHandler allows the tus handlers to be combined into
|
||||
// your existing router (aka mux) directly. It also allows the GET and DELETE
|
||||
// endpoints to be customized. These are not part of the protocol so can be
|
||||
// changed depending on your needs.
|
||||
func NewHandler(config Config) (*Handler, error) {
|
||||
if err := config.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
handler, err := NewUnroutedHandler(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
routedHandler := &Handler{
|
||||
UnroutedHandler: handler,
|
||||
}
|
||||
|
||||
mux := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
method := r.Method
|
||||
path := strings.Trim(r.URL.Path, "/")
|
||||
|
||||
switch path {
|
||||
case "":
|
||||
// Root endpoint for upload creation
|
||||
switch method {
|
||||
case "POST":
|
||||
handler.PostFile(w, r)
|
||||
default:
|
||||
w.Header().Add("Allow", "POST")
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
w.Write([]byte(`method not allowed`))
|
||||
}
|
||||
default:
|
||||
// URL points to an upload resource
|
||||
switch {
|
||||
case method == "HEAD" && r.URL.Path != "":
|
||||
// Offset retrieval
|
||||
handler.HeadFile(w, r)
|
||||
case method == "PATCH" && r.URL.Path != "":
|
||||
// Upload apppending
|
||||
handler.PatchFile(w, r)
|
||||
case method == "GET" && r.URL.Path != "" && !config.DisableDownload:
|
||||
// Upload download
|
||||
handler.GetFile(w, r)
|
||||
case method == "DELETE" && r.URL.Path != "" && config.StoreComposer.UsesTerminater && !config.DisableTermination:
|
||||
// Upload termination
|
||||
handler.DelFile(w, r)
|
||||
default:
|
||||
// TODO: Only add GET and DELETE if they are supported
|
||||
w.Header().Add("Allow", "GET, HEAD, PATCH, DELETE")
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
w.Write([]byte(`method not allowed`))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
routedHandler.Handler = handler.Middleware(mux)
|
||||
|
||||
return routedHandler, nil
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// HookEvent represents an event from tusd which can be handled by the application.
|
||||
type HookEvent struct {
|
||||
// Context provides access to the context from the HTTP request. This context is
|
||||
// not the exact value as the request context from http.Request.Context() but
|
||||
// a similar context that retains the same values as the request context. In
|
||||
// addition, Context will be cancelled after a short delay when the request context
|
||||
// is done. This delay is controlled by Config.GracefulRequestCompletionTimeout.
|
||||
//
|
||||
// The reason is that we want stores to be able to continue processing a request after
|
||||
// its context has been cancelled. For example, assume a PATCH request is incoming. If
|
||||
// the end-user pauses the upload, the connection is closed causing the request context
|
||||
// to be cancelled immediately. However, we want the store to be able to save the last
|
||||
// few bytes that were transmitted before the request was aborted. To allow this, we
|
||||
// copy the request context but cancel it with a brief delay to give the data store
|
||||
// time to finish its operations.
|
||||
Context context.Context `json:"-"`
|
||||
// Upload contains information about the upload that caused this hook
|
||||
// to be fired.
|
||||
Upload FileInfo
|
||||
// HTTPRequest contains details about the HTTP request that reached
|
||||
// tusd.
|
||||
HTTPRequest HTTPRequest
|
||||
}
|
||||
|
||||
func newHookEvent(c *httpContext, info FileInfo) HookEvent {
|
||||
// The Host header field is not present in the header map, see https://pkg.go.dev/net/http#Request:
|
||||
// > For incoming requests, the Host header is promoted to the
|
||||
// > Request.Host field and removed from the Header map.
|
||||
// That's why we add it back manually.
|
||||
c.req.Header.Set("Host", c.req.Host)
|
||||
|
||||
return HookEvent{
|
||||
Context: c,
|
||||
Upload: info,
|
||||
HTTPRequest: HTTPRequest{
|
||||
Method: c.req.Method,
|
||||
URI: c.req.RequestURI,
|
||||
RemoteAddr: c.req.RemoteAddr,
|
||||
Header: c.req.Header,
|
||||
},
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// HTTPRequest contains basic details of an incoming HTTP request.
|
||||
type HTTPRequest struct {
|
||||
// Method is the HTTP method, e.g. POST or PATCH.
|
||||
Method string
|
||||
// URI is the full HTTP request URI, e.g. /files/fooo.
|
||||
URI string
|
||||
// RemoteAddr contains the network address that sent the request.
|
||||
RemoteAddr string
|
||||
// Header contains all HTTP headers as present in the HTTP request.
|
||||
Header http.Header
|
||||
}
|
||||
|
||||
type HTTPHeader map[string]string
|
||||
|
||||
// HTTPResponse contains basic details of an outgoing HTTP response.
|
||||
type HTTPResponse struct {
|
||||
// StatusCode is status code, e.g. 200 or 400.
|
||||
StatusCode int
|
||||
// Body is the response body.
|
||||
Body string
|
||||
// Header contains additional HTTP headers for the response.
|
||||
Header HTTPHeader
|
||||
}
|
||||
|
||||
// writeTo writes the HTTP response into w, as specified by the fields in resp.
|
||||
func (resp HTTPResponse) writeTo(w http.ResponseWriter) {
|
||||
headers := w.Header()
|
||||
for key, value := range resp.Header {
|
||||
headers.Set(key, value)
|
||||
}
|
||||
|
||||
if len(resp.Body) > 0 {
|
||||
headers.Set("Content-Length", strconv.Itoa(len(resp.Body)))
|
||||
}
|
||||
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
|
||||
if len(resp.Body) > 0 {
|
||||
w.Write([]byte(resp.Body))
|
||||
}
|
||||
}
|
||||
|
||||
// MergeWith returns a copy of resp1, where non-default values from resp2 overwrite
|
||||
// values from resp1.
|
||||
func (resp1 HTTPResponse) MergeWith(resp2 HTTPResponse) HTTPResponse {
|
||||
// Clone the response 1 and use it as a basis
|
||||
newResp := resp1
|
||||
|
||||
// Take the status code and body from response 2 to
|
||||
// overwrite values from response 1.
|
||||
if resp2.StatusCode != 0 {
|
||||
newResp.StatusCode = resp2.StatusCode
|
||||
}
|
||||
|
||||
if len(resp2.Body) > 0 {
|
||||
newResp.Body = resp2.Body
|
||||
}
|
||||
|
||||
// For the headers, me must make a new map to avoid writing
|
||||
// into the header map from response 1.
|
||||
newResp.Header = make(HTTPHeader, len(resp1.Header)+len(resp2.Header))
|
||||
|
||||
for key, value := range resp1.Header {
|
||||
newResp.Header[key] = value
|
||||
}
|
||||
|
||||
for key, value := range resp2.Header {
|
||||
newResp.Header[key] = value
|
||||
}
|
||||
|
||||
return newResp
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// Metrics provides numbers about the usage of the tusd handler. Since these may
|
||||
// be accessed from multiple goroutines, it is necessary to read and modify them
|
||||
// atomically using the functions exposed in the sync/atomic package, such as
|
||||
// atomic.LoadUint64. In addition the maps must not be modified to prevent data
|
||||
// races.
|
||||
type Metrics struct {
|
||||
// RequestTotal counts the number of incoming requests per method
|
||||
RequestsTotal map[string]*uint64
|
||||
// ErrorsTotal counts the number of returned errors by their message
|
||||
ErrorsTotal *ErrorsTotalMap
|
||||
BytesReceived *uint64
|
||||
UploadsFinished *uint64
|
||||
UploadsCreated *uint64
|
||||
UploadsTerminated *uint64
|
||||
}
|
||||
|
||||
// incRequestsTotal increases the counter for this request method atomically by
|
||||
// one. The method must be one of GET, HEAD, POST, PATCH, DELETE.
|
||||
func (m Metrics) incRequestsTotal(method string) {
|
||||
if ptr, ok := m.RequestsTotal[method]; ok {
|
||||
atomic.AddUint64(ptr, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// incErrorsTotal increases the counter for this error atomically by one.
|
||||
func (m Metrics) incErrorsTotal(err Error) {
|
||||
ptr := m.ErrorsTotal.retrievePointerFor(err)
|
||||
atomic.AddUint64(ptr, 1)
|
||||
}
|
||||
|
||||
// incBytesReceived increases the number of received bytes atomically be the
|
||||
// specified number.
|
||||
func (m Metrics) incBytesReceived(delta uint64) {
|
||||
atomic.AddUint64(m.BytesReceived, delta)
|
||||
}
|
||||
|
||||
// incUploadsFinished increases the counter for finished uploads atomically by one.
|
||||
func (m Metrics) incUploadsFinished() {
|
||||
atomic.AddUint64(m.UploadsFinished, 1)
|
||||
}
|
||||
|
||||
// incUploadsCreated increases the counter for completed uploads atomically by one.
|
||||
func (m Metrics) incUploadsCreated() {
|
||||
atomic.AddUint64(m.UploadsCreated, 1)
|
||||
}
|
||||
|
||||
// incUploadsTerminated increases the counter for completed uploads atomically by one.
|
||||
func (m Metrics) incUploadsTerminated() {
|
||||
atomic.AddUint64(m.UploadsTerminated, 1)
|
||||
}
|
||||
|
||||
func newMetrics() Metrics {
|
||||
return Metrics{
|
||||
RequestsTotal: map[string]*uint64{
|
||||
"GET": new(uint64),
|
||||
"HEAD": new(uint64),
|
||||
"POST": new(uint64),
|
||||
"PATCH": new(uint64),
|
||||
"DELETE": new(uint64),
|
||||
"OPTIONS": new(uint64),
|
||||
},
|
||||
ErrorsTotal: newErrorsTotalMap(),
|
||||
BytesReceived: new(uint64),
|
||||
UploadsFinished: new(uint64),
|
||||
UploadsCreated: new(uint64),
|
||||
UploadsTerminated: new(uint64),
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorsTotalMap stores the counters for the different HTTP errors.
|
||||
type ErrorsTotalMap struct {
|
||||
lock sync.RWMutex
|
||||
counter map[ErrorsTotalMapEntry]*uint64
|
||||
}
|
||||
|
||||
type ErrorsTotalMapEntry struct {
|
||||
ErrorCode string
|
||||
StatusCode int
|
||||
}
|
||||
|
||||
func newErrorsTotalMap() *ErrorsTotalMap {
|
||||
m := make(map[ErrorsTotalMapEntry]*uint64, 20)
|
||||
return &ErrorsTotalMap{
|
||||
counter: m,
|
||||
}
|
||||
}
|
||||
|
||||
// retrievePointerFor returns (after creating it if necessary) the pointer to
|
||||
// the counter for the error.
|
||||
func (e *ErrorsTotalMap) retrievePointerFor(err Error) *uint64 {
|
||||
serr := ErrorsTotalMapEntry{
|
||||
ErrorCode: err.ErrorCode,
|
||||
StatusCode: err.HTTPResponse.StatusCode,
|
||||
}
|
||||
|
||||
e.lock.RLock()
|
||||
ptr, ok := e.counter[serr]
|
||||
e.lock.RUnlock()
|
||||
if ok {
|
||||
return ptr
|
||||
}
|
||||
|
||||
// For pointer creation, a write-lock is required
|
||||
e.lock.Lock()
|
||||
// We ensure that the pointer wasn't created in the meantime
|
||||
if ptr, ok = e.counter[serr]; !ok {
|
||||
ptr = new(uint64)
|
||||
e.counter[serr] = ptr
|
||||
}
|
||||
e.lock.Unlock()
|
||||
|
||||
return ptr
|
||||
}
|
||||
|
||||
// Load retrieves the map of the counter pointers atomically
|
||||
func (e *ErrorsTotalMap) Load() map[ErrorsTotalMapEntry]*uint64 {
|
||||
m := make(map[ErrorsTotalMapEntry]*uint64, len(e.counter))
|
||||
e.lock.RLock()
|
||||
for err, ptr := range e.counter {
|
||||
m[err] = ptr
|
||||
}
|
||||
e.lock.RUnlock()
|
||||
|
||||
return m
|
||||
}
|
||||
+1569
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user