From 0ad0d9bf789f107473851df14dbc06b5dabe9961 Mon Sep 17 00:00:00 2001 From: jkoberg Date: Tue, 30 Jul 2024 14:00:29 +0200 Subject: [PATCH] feat(reva): bump reva Signed-off-by: jkoberg --- changelog/unreleased/bump-reva.md | 3 +- go.mod | 3 +- go.sum | 8 +- .../http/services/owncloud/ocdav/copy.go | 30 +- .../http/services/owncloud/ocdav/tus.go | 2 +- .../v2/pkg/ocm/storage/received/upload.go | 4 +- .../cs3org/reva/v2/pkg/rgrpc/keepalive.go | 24 + .../cs3org/reva/v2/pkg/rgrpc/rgrpc.go | 4 + .../v2/pkg/rhttp/datatx/manager/tus/tus.go | 36 +- .../reva/v2/pkg/storage/fs/cephfs/upload.go | 4 +- .../reva/v2/pkg/storage/fs/hello/hello.go | 241 +++ .../v2/pkg/storage/fs/hello/unimplemented.go | 198 +++ .../reva/v2/pkg/storage/fs/loader/loader.go | 1 + .../v2/pkg/storage/fs/owncloudsql/upload.go | 4 +- .../reva/v2/pkg/storage/fs/posix/posix.go | 2 +- .../cs3org/reva/v2/pkg/storage/storage.go | 142 +- .../cs3org/reva/v2/pkg/storage/uploads.go | 2 +- .../utils/decomposedfs/decomposedfs.go | 2 +- .../pkg/storage/utils/decomposedfs/upload.go | 2 +- .../utils/decomposedfs/upload/session.go | 2 +- .../utils/decomposedfs/upload/store.go | 2 +- .../utils/decomposedfs/upload/upload.go | 4 +- .../v2/pkg/storage/utils/localfs/upload.go | 4 +- .../storage/utils/middleware/middleware.go | 2 +- vendor/github.com/tus/tusd/v2/LICENSE.txt | 19 + .../tus/tusd/v2/pkg/handler/body_reader.go | 132 ++ .../tus/tusd/v2/pkg/handler/composer.go | 87 + .../tus/tusd/v2/pkg/handler/composer.mgo | 74 + .../tus/tusd/v2/pkg/handler/config.go | 195 ++ .../tus/tusd/v2/pkg/handler/context.go | 102 ++ .../tus/tusd/v2/pkg/handler/datastore.go | 190 ++ .../github.com/tus/tusd/v2/pkg/handler/doc.go | 69 + .../tus/tusd/v2/pkg/handler/error.go | 37 + .../tus/tusd/v2/pkg/handler/handler.go | 77 + .../tus/tusd/v2/pkg/handler/hooks.go | 48 + .../tus/tusd/v2/pkg/handler/http.go | 79 + .../tus/tusd/v2/pkg/handler/metrics.go | 132 ++ .../tusd/v2/pkg/handler/unrouted_handler.go | 1569 +++++++++++++++++ vendor/golang.org/x/exp/slog/attr.go | 102 ++ vendor/golang.org/x/exp/slog/doc.go | 316 ++++ vendor/golang.org/x/exp/slog/handler.go | 577 ++++++ .../x/exp/slog/internal/buffer/buffer.go | 84 + .../x/exp/slog/internal/ignorepc.go | 9 + vendor/golang.org/x/exp/slog/json_handler.go | 336 ++++ vendor/golang.org/x/exp/slog/level.go | 201 +++ vendor/golang.org/x/exp/slog/logger.go | 343 ++++ vendor/golang.org/x/exp/slog/noplog.bench | 36 + vendor/golang.org/x/exp/slog/record.go | 207 +++ vendor/golang.org/x/exp/slog/text_handler.go | 161 ++ vendor/golang.org/x/exp/slog/value.go | 456 +++++ vendor/golang.org/x/exp/slog/value_119.go | 53 + vendor/golang.org/x/exp/slog/value_120.go | 39 + vendor/modules.txt | 9 +- 53 files changed, 6396 insertions(+), 69 deletions(-) create mode 100644 vendor/github.com/cs3org/reva/v2/pkg/rgrpc/keepalive.go create mode 100644 vendor/github.com/cs3org/reva/v2/pkg/storage/fs/hello/hello.go create mode 100644 vendor/github.com/cs3org/reva/v2/pkg/storage/fs/hello/unimplemented.go create mode 100644 vendor/github.com/tus/tusd/v2/LICENSE.txt create mode 100644 vendor/github.com/tus/tusd/v2/pkg/handler/body_reader.go create mode 100644 vendor/github.com/tus/tusd/v2/pkg/handler/composer.go create mode 100644 vendor/github.com/tus/tusd/v2/pkg/handler/composer.mgo create mode 100644 vendor/github.com/tus/tusd/v2/pkg/handler/config.go create mode 100644 vendor/github.com/tus/tusd/v2/pkg/handler/context.go create mode 100644 vendor/github.com/tus/tusd/v2/pkg/handler/datastore.go create mode 100644 vendor/github.com/tus/tusd/v2/pkg/handler/doc.go create mode 100644 vendor/github.com/tus/tusd/v2/pkg/handler/error.go create mode 100644 vendor/github.com/tus/tusd/v2/pkg/handler/handler.go create mode 100644 vendor/github.com/tus/tusd/v2/pkg/handler/hooks.go create mode 100644 vendor/github.com/tus/tusd/v2/pkg/handler/http.go create mode 100644 vendor/github.com/tus/tusd/v2/pkg/handler/metrics.go create mode 100644 vendor/github.com/tus/tusd/v2/pkg/handler/unrouted_handler.go create mode 100644 vendor/golang.org/x/exp/slog/attr.go create mode 100644 vendor/golang.org/x/exp/slog/doc.go create mode 100644 vendor/golang.org/x/exp/slog/handler.go create mode 100644 vendor/golang.org/x/exp/slog/internal/buffer/buffer.go create mode 100644 vendor/golang.org/x/exp/slog/internal/ignorepc.go create mode 100644 vendor/golang.org/x/exp/slog/json_handler.go create mode 100644 vendor/golang.org/x/exp/slog/level.go create mode 100644 vendor/golang.org/x/exp/slog/logger.go create mode 100644 vendor/golang.org/x/exp/slog/noplog.bench create mode 100644 vendor/golang.org/x/exp/slog/record.go create mode 100644 vendor/golang.org/x/exp/slog/text_handler.go create mode 100644 vendor/golang.org/x/exp/slog/value.go create mode 100644 vendor/golang.org/x/exp/slog/value_119.go create mode 100644 vendor/golang.org/x/exp/slog/value_120.go diff --git a/changelog/unreleased/bump-reva.md b/changelog/unreleased/bump-reva.md index e2d1ab5186..ffb06ca615 100644 --- a/changelog/unreleased/bump-reva.md +++ b/changelog/unreleased/bump-reva.md @@ -1,3 +1,4 @@ Enhancement: Bump reva -https://github.com/owncloud/ocis/pull/9715 \ No newline at end of file +https://github.com/owncloud/ocis/pull/9714 +https://github.com/owncloud/ocis/pull/9715 diff --git a/go.mod b/go.mod index ffeab5eb9f..dbc22712f9 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/cenkalti/backoff v2.2.1+incompatible github.com/coreos/go-oidc/v3 v3.11.0 github.com/cs3org/go-cs3apis v0.0.0-20240724121416-062c4e3046cb - github.com/cs3org/reva/v2 v2.22.1-0.20240730105121-548644c31544 + github.com/cs3org/reva/v2 v2.22.1-0.20240806075425-8bcdd93dfa20 github.com/dhowden/tag v0.0.0-20230630033851-978a0926ee25 github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e github.com/egirna/icap-client v0.1.1 @@ -323,6 +323,7 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect github.com/trustelem/zxcvbn v1.0.1 // indirect + github.com/tus/tusd/v2 v2.4.0 // indirect github.com/wk8/go-ordered-map v1.0.0 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect diff --git a/go.sum b/go.sum index 28d3f67d18..3f1fe00df2 100644 --- a/go.sum +++ b/go.sum @@ -766,6 +766,8 @@ dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= +github.com/Acconut/go-httptest-recorder v1.0.0 h1:TAv2dfnqp/l+SUvIaMAUK4GeN4+wqb6KZsFFFTGhoJg= +github.com/Acconut/go-httptest-recorder v1.0.0/go.mod h1:CwQyhTH1kq/gLyWiRieo7c0uokpu3PXeyF/nZjUNtmM= github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k= github.com/Azure/azure-sdk-for-go v32.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-storage-blob-go v0.14.0/go.mod h1:SMqIBi+SuiQH32bvyjngEewEeXoPfKMgWlBDaYf6fck= @@ -1024,8 +1026,8 @@ github.com/crewjam/saml v0.4.14 h1:g9FBNx62osKusnFzs3QTN5L9CVA/Egfgm+stJShzw/c= github.com/crewjam/saml v0.4.14/go.mod h1:UVSZCf18jJkk6GpWNVqcyQJMD5HsRugBPf4I1nl2mME= github.com/cs3org/go-cs3apis v0.0.0-20240724121416-062c4e3046cb h1:KmYZDReplv/yfwc1LNYpDcVhVujC3Pasv6WjXx1haSU= github.com/cs3org/go-cs3apis v0.0.0-20240724121416-062c4e3046cb/go.mod h1:yyP8PRo0EZou3nSH7H4qjlzQwaydPeIRNgX50npQHpE= -github.com/cs3org/reva/v2 v2.22.1-0.20240730105121-548644c31544 h1:cBqx8oou5aXM9SqiG96bYGBD4akYwecPoopsFva51yI= -github.com/cs3org/reva/v2 v2.22.1-0.20240730105121-548644c31544/go.mod h1:R6OO/ZPMr8MivSiESfk7pUfsdXdr709L8kJErLgqvDI= +github.com/cs3org/reva/v2 v2.22.1-0.20240806075425-8bcdd93dfa20 h1:GVxOUoabZQ5kgeS3oBOHZZn+sY0Sb5VOyhjsEFttJ4g= +github.com/cs3org/reva/v2 v2.22.1-0.20240806075425-8bcdd93dfa20/go.mod h1:l/llk1KaiB/XiJYfk+gznzkXZqVv+TbxJKgDbpti444= github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= @@ -2056,6 +2058,8 @@ github.com/trustelem/zxcvbn v1.0.1 h1:mp4JFtzdDYGj9WYSD3KQSkwwUumWNFzXaAjckaTYps github.com/trustelem/zxcvbn v1.0.1/go.mod h1:zonUyKeh7sw6psPf/e3DtRqkRyZvAbOfjNz/aO7YQ5s= github.com/tus/tusd v1.13.0 h1:W7rtb1XPSpde/GPZAgdfUS3vus2Jt2KmckS6OUd3CU8= github.com/tus/tusd v1.13.0/go.mod h1:1tX4CDGlx8koHGFJdSaJ5ybUIm2NeVloJgZEPSKRcQA= +github.com/tus/tusd/v2 v2.4.0 h1:SpXmzQPCtiedkhNPl5Gn4ApQXLChPLdYrWbZQI42uJE= +github.com/tus/tusd/v2 v2.4.0/go.mod h1:X+fc/MU+T+NDD5gNJHHE58jo6cQj1vlMstlT16+xlrg= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= diff --git a/vendor/github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/copy.go b/vendor/github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/copy.go index c009e59789..cccc3c2f08 100644 --- a/vendor/github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/copy.go +++ b/vendor/github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/copy.go @@ -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 { diff --git a/vendor/github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/tus.go b/vendor/github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/tus.go index 2b85a1d231..0851a7ecb5 100644 --- a/vendor/github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/tus.go +++ b/vendor/github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/tus.go @@ -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" ) diff --git a/vendor/github.com/cs3org/reva/v2/pkg/ocm/storage/received/upload.go b/vendor/github.com/cs3org/reva/v2/pkg/ocm/storage/received/upload.go index b2e39e5a79..951bec8992 100644 --- a/vendor/github.com/cs3org/reva/v2/pkg/ocm/storage/received/upload.go +++ b/vendor/github.com/cs3org/reva/v2/pkg/ocm/storage/received/upload.go @@ -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()) } diff --git a/vendor/github.com/cs3org/reva/v2/pkg/rgrpc/keepalive.go b/vendor/github.com/cs3org/reva/v2/pkg/rgrpc/keepalive.go new file mode 100644 index 0000000000..1baab9efb2 --- /dev/null +++ b/vendor/github.com/cs3org/reva/v2/pkg/rgrpc/keepalive.go @@ -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 +} diff --git a/vendor/github.com/cs3org/reva/v2/pkg/rgrpc/rgrpc.go b/vendor/github.com/cs3org/reva/v2/pkg/rgrpc/rgrpc.go index 1085a5e034..c04bedc3da 100644 --- a/vendor/github.com/cs3org/reva/v2/pkg/rgrpc/rgrpc.go +++ b/vendor/github.com/cs3org/reva/v2/pkg/rgrpc/rgrpc.go @@ -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...) diff --git a/vendor/github.com/cs3org/reva/v2/pkg/rhttp/datatx/manager/tus/tus.go b/vendor/github.com/cs3org/reva/v2/pkg/rhttp/datatx/manager/tus/tus.go index f3ddd77c70..829440ecaf 100644 --- a/vendor/github.com/cs3org/reva/v2/pkg/rhttp/datatx/manager/tus/tus.go +++ b/vendor/github.com/cs3org/reva/v2/pkg/rhttp/datatx/manager/tus/tus.go @@ -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 } diff --git a/vendor/github.com/cs3org/reva/v2/pkg/storage/fs/cephfs/upload.go b/vendor/github.com/cs3org/reva/v2/pkg/storage/fs/cephfs/upload.go index 031508cbea..4243957153 100644 --- a/vendor/github.com/cs3org/reva/v2/pkg/storage/fs/cephfs/upload.go +++ b/vendor/github.com/cs3org/reva/v2/pkg/storage/fs/cephfs/upload.go @@ -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) diff --git a/vendor/github.com/cs3org/reva/v2/pkg/storage/fs/hello/hello.go b/vendor/github.com/cs3org/reva/v2/pkg/storage/fs/hello/hello.go new file mode 100644 index 0000000000..0a9d72e2d0 --- /dev/null +++ b/vendor/github.com/cs3org/reva/v2/pkg/storage/fs/hello/hello.go @@ -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 +} diff --git a/vendor/github.com/cs3org/reva/v2/pkg/storage/fs/hello/unimplemented.go b/vendor/github.com/cs3org/reva/v2/pkg/storage/fs/hello/unimplemented.go new file mode 100644 index 0000000000..3490b81083 --- /dev/null +++ b/vendor/github.com/cs3org/reva/v2/pkg/storage/fs/hello/unimplemented.go @@ -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") +} diff --git a/vendor/github.com/cs3org/reva/v2/pkg/storage/fs/loader/loader.go b/vendor/github.com/cs3org/reva/v2/pkg/storage/fs/loader/loader.go index 68575752c9..bb662eb238 100644 --- a/vendor/github.com/cs3org/reva/v2/pkg/storage/fs/loader/loader.go +++ b/vendor/github.com/cs3org/reva/v2/pkg/storage/fs/loader/loader.go @@ -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" diff --git a/vendor/github.com/cs3org/reva/v2/pkg/storage/fs/owncloudsql/upload.go b/vendor/github.com/cs3org/reva/v2/pkg/storage/fs/owncloudsql/upload.go index 872cf0503b..5e9877d97c 100644 --- a/vendor/github.com/cs3org/reva/v2/pkg/storage/fs/owncloudsql/upload.go +++ b/vendor/github.com/cs3org/reva/v2/pkg/storage/fs/owncloudsql/upload.go @@ -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) } diff --git a/vendor/github.com/cs3org/reva/v2/pkg/storage/fs/posix/posix.go b/vendor/github.com/cs3org/reva/v2/pkg/storage/fs/posix/posix.go index 4308f94867..dfffdf04b9 100644 --- a/vendor/github.com/cs3org/reva/v2/pkg/storage/fs/posix/posix.go +++ b/vendor/github.com/cs3org/reva/v2/pkg/storage/fs/posix/posix.go @@ -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" diff --git a/vendor/github.com/cs3org/reva/v2/pkg/storage/storage.go b/vendor/github.com/cs3org/reva/v2/pkg/storage/storage.go index 6fc19a0a23..11cab0c5e7 100644 --- a/vendor/github.com/cs3org/reva/v2/pkg/storage/storage.go +++ b/vendor/github.com/cs3org/reva/v2/pkg/storage/storage.go @@ -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 diff --git a/vendor/github.com/cs3org/reva/v2/pkg/storage/uploads.go b/vendor/github.com/cs3org/reva/v2/pkg/storage/uploads.go index 50a8fe0eb4..d928f3d694 100644 --- a/vendor/github.com/cs3org/reva/v2/pkg/storage/uploads.go +++ b/vendor/github.com/cs3org/reva/v2/pkg/storage/uploads.go @@ -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 diff --git a/vendor/github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/decomposedfs.go b/vendor/github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/decomposedfs.go index 529f32d0eb..2bb6b1076c 100644 --- a/vendor/github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/decomposedfs.go +++ b/vendor/github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/decomposedfs.go @@ -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" diff --git a/vendor/github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/upload.go b/vendor/github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/upload.go index dc2a075515..d0b84f6a79 100644 --- a/vendor/github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/upload.go +++ b/vendor/github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/upload.go @@ -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" diff --git a/vendor/github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/upload/session.go b/vendor/github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/upload/session.go index c008c274dd..1c1386d9b4 100644 --- a/vendor/github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/upload/session.go +++ b/vendor/github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/upload/session.go @@ -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" diff --git a/vendor/github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/upload/store.go b/vendor/github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/upload/store.go index cd5e3a6091..9abc856fc8 100644 --- a/vendor/github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/upload/store.go +++ b/vendor/github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/upload/store.go @@ -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") diff --git a/vendor/github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/upload/upload.go b/vendor/github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/upload/upload.go index 92c9438f7e..4a3bfc61c2 100644 --- a/vendor/github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/upload/upload.go +++ b/vendor/github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/upload/upload.go @@ -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()) diff --git a/vendor/github.com/cs3org/reva/v2/pkg/storage/utils/localfs/upload.go b/vendor/github.com/cs3org/reva/v2/pkg/storage/utils/localfs/upload.go index 45c61dc8a1..1c9219bb83 100644 --- a/vendor/github.com/cs3org/reva/v2/pkg/storage/utils/localfs/upload.go +++ b/vendor/github.com/cs3org/reva/v2/pkg/storage/utils/localfs/upload.go @@ -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) } diff --git a/vendor/github.com/cs3org/reva/v2/pkg/storage/utils/middleware/middleware.go b/vendor/github.com/cs3org/reva/v2/pkg/storage/utils/middleware/middleware.go index 4566007171..b90cb2a088 100644 --- a/vendor/github.com/cs3org/reva/v2/pkg/storage/utils/middleware/middleware.go +++ b/vendor/github.com/cs3org/reva/v2/pkg/storage/utils/middleware/middleware.go @@ -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" diff --git a/vendor/github.com/tus/tusd/v2/LICENSE.txt b/vendor/github.com/tus/tusd/v2/LICENSE.txt new file mode 100644 index 0000000000..bdf13822ce --- /dev/null +++ b/vendor/github.com/tus/tusd/v2/LICENSE.txt @@ -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. diff --git a/vendor/github.com/tus/tusd/v2/pkg/handler/body_reader.go b/vendor/github.com/tus/tusd/v2/pkg/handler/body_reader.go new file mode 100644 index 0000000000..ab9c8d3cd9 --- /dev/null +++ b/vendor/github.com/tus/tusd/v2/pkg/handler/body_reader.go @@ -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() +} diff --git a/vendor/github.com/tus/tusd/v2/pkg/handler/composer.go b/vendor/github.com/tus/tusd/v2/pkg/handler/composer.go new file mode 100644 index 0000000000..abea0a6900 --- /dev/null +++ b/vendor/github.com/tus/tusd/v2/pkg/handler/composer.go @@ -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 +} diff --git a/vendor/github.com/tus/tusd/v2/pkg/handler/composer.mgo b/vendor/github.com/tus/tusd/v2/pkg/handler/composer.mgo new file mode 100644 index 0000000000..f4fc3ef87b --- /dev/null +++ b/vendor/github.com/tus/tusd/v2/pkg/handler/composer.mgo @@ -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) diff --git a/vendor/github.com/tus/tusd/v2/pkg/handler/config.go b/vendor/github.com/tus/tusd/v2/pkg/handler/config.go new file mode 100644 index 0000000000..e5c6702632 --- /dev/null +++ b/vendor/github.com/tus/tusd/v2/pkg/handler/config.go @@ -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 +} diff --git a/vendor/github.com/tus/tusd/v2/pkg/handler/context.go b/vendor/github.com/tus/tusd/v2/pkg/handler/context.go new file mode 100644 index 0000000000..892009bab5 --- /dev/null +++ b/vendor/github.com/tus/tusd/v2/pkg/handler/context.go @@ -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 +} diff --git a/vendor/github.com/tus/tusd/v2/pkg/handler/datastore.go b/vendor/github.com/tus/tusd/v2/pkg/handler/datastore.go new file mode 100644 index 0000000000..8888a2faad --- /dev/null +++ b/vendor/github.com/tus/tusd/v2/pkg/handler/datastore.go @@ -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 +} diff --git a/vendor/github.com/tus/tusd/v2/pkg/handler/doc.go b/vendor/github.com/tus/tusd/v2/pkg/handler/doc.go new file mode 100644 index 0000000000..262eba9f3a --- /dev/null +++ b/vendor/github.com/tus/tusd/v2/pkg/handler/doc.go @@ -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 diff --git a/vendor/github.com/tus/tusd/v2/pkg/handler/error.go b/vendor/github.com/tus/tusd/v2/pkg/handler/error.go new file mode 100644 index 0000000000..6400a77a93 --- /dev/null +++ b/vendor/github.com/tus/tusd/v2/pkg/handler/error.go @@ -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", + }, + }, + } +} diff --git a/vendor/github.com/tus/tusd/v2/pkg/handler/handler.go b/vendor/github.com/tus/tusd/v2/pkg/handler/handler.go new file mode 100644 index 0000000000..7953a97022 --- /dev/null +++ b/vendor/github.com/tus/tusd/v2/pkg/handler/handler.go @@ -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 +} diff --git a/vendor/github.com/tus/tusd/v2/pkg/handler/hooks.go b/vendor/github.com/tus/tusd/v2/pkg/handler/hooks.go new file mode 100644 index 0000000000..c1c6f11333 --- /dev/null +++ b/vendor/github.com/tus/tusd/v2/pkg/handler/hooks.go @@ -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, + }, + } +} diff --git a/vendor/github.com/tus/tusd/v2/pkg/handler/http.go b/vendor/github.com/tus/tusd/v2/pkg/handler/http.go new file mode 100644 index 0000000000..d521fd1ac4 --- /dev/null +++ b/vendor/github.com/tus/tusd/v2/pkg/handler/http.go @@ -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 +} diff --git a/vendor/github.com/tus/tusd/v2/pkg/handler/metrics.go b/vendor/github.com/tus/tusd/v2/pkg/handler/metrics.go new file mode 100644 index 0000000000..1a2c6aad99 --- /dev/null +++ b/vendor/github.com/tus/tusd/v2/pkg/handler/metrics.go @@ -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 +} diff --git a/vendor/github.com/tus/tusd/v2/pkg/handler/unrouted_handler.go b/vendor/github.com/tus/tusd/v2/pkg/handler/unrouted_handler.go new file mode 100644 index 0000000000..73be46437e --- /dev/null +++ b/vendor/github.com/tus/tusd/v2/pkg/handler/unrouted_handler.go @@ -0,0 +1,1569 @@ +package handler + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "io" + "math" + "mime" + "net/http" + "regexp" + "strconv" + "strings" + "time" + + "golang.org/x/exp/slog" +) + +const UploadLengthDeferred = "1" + +type draftVersion string + +// These are the different interoperability versions defines in the different +// versions of the resumable uploads draft from the HTTP working group. +// See https://datatracker.ietf.org/doc/draft-ietf-httpbis-resumable-upload/ +const ( + interopVersion3 draftVersion = "3" // From draft version -01 + interopVersion4 draftVersion = "4" // From draft version -02 + interopVersion5 draftVersion = "5" // From draft version -03 +) + +var ( + reForwardedHost = regexp.MustCompile(`host="?([^;"]+)`) + reForwardedProto = regexp.MustCompile(`proto=(https?)`) + reMimeType = regexp.MustCompile(`^[a-z]+\/[a-z0-9\-\+\.]+$`) + // We only allow certain URL-safe characters in upload IDs. URL-safe in this means + // that their are allowed in a URI's path component according to RFC 3986. + // See https://datatracker.ietf.org/doc/html/rfc3986#section-3.3 + reValidUploadId = regexp.MustCompile(`^[A-Za-z0-9\-._~%!$'()*+,;=/:@]*$`) +) + +var ( + ErrUnsupportedVersion = NewError("ERR_UNSUPPORTED_VERSION", "missing, invalid or unsupported Tus-Resumable header", http.StatusPreconditionFailed) + ErrMaxSizeExceeded = NewError("ERR_MAX_SIZE_EXCEEDED", "maximum size exceeded", http.StatusRequestEntityTooLarge) + ErrInvalidContentType = NewError("ERR_INVALID_CONTENT_TYPE", "missing or invalid Content-Type header", http.StatusBadRequest) + ErrInvalidUploadLength = NewError("ERR_INVALID_UPLOAD_LENGTH", "missing or invalid Upload-Length header", http.StatusBadRequest) + ErrInvalidOffset = NewError("ERR_INVALID_OFFSET", "missing or invalid Upload-Offset header", http.StatusBadRequest) + ErrNotFound = NewError("ERR_UPLOAD_NOT_FOUND", "upload not found", http.StatusNotFound) + ErrFileLocked = NewError("ERR_UPLOAD_LOCKED", "file currently locked", http.StatusLocked) + ErrLockTimeout = NewError("ERR_LOCK_TIMEOUT", "failed to acquire lock before timeout", http.StatusInternalServerError) + ErrMismatchOffset = NewError("ERR_MISMATCHED_OFFSET", "mismatched offset", http.StatusConflict) + ErrSizeExceeded = NewError("ERR_UPLOAD_SIZE_EXCEEDED", "upload's size exceeded", http.StatusRequestEntityTooLarge) + ErrNotImplemented = NewError("ERR_NOT_IMPLEMENTED", "feature not implemented", http.StatusNotImplemented) + ErrUploadNotFinished = NewError("ERR_UPLOAD_NOT_FINISHED", "one of the partial uploads is not finished", http.StatusBadRequest) + ErrInvalidConcat = NewError("ERR_INVALID_CONCAT", "invalid Upload-Concat header", http.StatusBadRequest) + ErrModifyFinal = NewError("ERR_MODIFY_FINAL", "modifying a final upload is not allowed", http.StatusForbidden) + ErrUploadLengthAndUploadDeferLength = NewError("ERR_AMBIGUOUS_UPLOAD_LENGTH", "provided both Upload-Length and Upload-Defer-Length", http.StatusBadRequest) + ErrInvalidUploadDeferLength = NewError("ERR_INVALID_UPLOAD_LENGTH_DEFER", "invalid Upload-Defer-Length header", http.StatusBadRequest) + ErrUploadStoppedByServer = NewError("ERR_UPLOAD_STOPPED", "upload has been stopped by server", http.StatusBadRequest) + ErrUploadRejectedByServer = NewError("ERR_UPLOAD_REJECTED", "upload creation has been rejected by server", http.StatusBadRequest) + ErrUploadInterrupted = NewError("ERR_UPLOAD_INTERRUPTED", "upload has been interrupted by another request for this upload resource", http.StatusBadRequest) + ErrServerShutdown = NewError("ERR_SERVER_SHUTDOWN", "request has been interrupted because the server is shutting down", http.StatusServiceUnavailable) + ErrOriginNotAllowed = NewError("ERR_ORIGIN_NOT_ALLOWED", "request origin is not allowed", http.StatusForbidden) + ErrUnexpectedEOF = NewError("ERR_UNEXPECTED_EOF", "server expected to receive more bytes", http.StatusBadRequest) + + // These two responses are 500 for backwards compatability. Clients might receive a timeout response + // when the upload got interrupted. Most clients will not retry 4XX but only 5XX, so we responsd with 500 here. + ErrReadTimeout = NewError("ERR_READ_TIMEOUT", "timeout while reading request body", http.StatusInternalServerError) + ErrConnectionReset = NewError("ERR_CONNECTION_RESET", "TCP connection reset by peer", http.StatusInternalServerError) +) + +// UnroutedHandler exposes methods to handle requests as part of the tus protocol, +// such as PostFile, HeadFile, PatchFile and DelFile. In addition the GetFile method +// is provided which is, however, not part of the specification. +type UnroutedHandler struct { + config Config + composer *StoreComposer + isBasePathAbs bool + basePath string + logger *slog.Logger + extensions string + + // CompleteUploads is used to send notifications whenever an upload is + // completed by a user. The HookEvent will contain information about this + // upload after it is completed. Sending to this channel will only + // happen if the NotifyCompleteUploads field is set to true in the Config + // structure. Notifications will also be sent for completions using the + // Concatenation extension. + CompleteUploads chan HookEvent + // TerminatedUploads is used to send notifications whenever an upload is + // terminated by a user. The HookEvent will contain information about this + // upload gathered before the termination. Sending to this channel will only + // happen if the NotifyTerminatedUploads field is set to true in the Config + // structure. + TerminatedUploads chan HookEvent + // UploadProgress is used to send notifications about the progress of the + // currently running uploads. For each open PATCH request, every second + // a HookEvent instance will be send over this channel with the Offset field + // being set to the number of bytes which have been transfered to the server. + // Please be aware that this number may be higher than the number of bytes + // which have been stored by the data store! Sending to this channel will only + // happen if the NotifyUploadProgress field is set to true in the Config + // structure. + UploadProgress chan HookEvent + // CreatedUploads is used to send notifications about the uploads having been + // created. It triggers post creation and therefore has all the HookEvent incl. + // the ID available already. It facilitates the post-create hook. Sending to + // this channel will only happen if the NotifyCreatedUploads field is set to + // true in the Config structure. + CreatedUploads chan HookEvent + // Metrics provides numbers of the usage for this handler. + Metrics Metrics +} + +// NewUnroutedHandler creates a new handler without routing using the given +// configuration. It exposes the http handlers which need to be combined with +// a router (aka mux) of your choice. If you are looking for preconfigured +// handler see NewHandler. +func NewUnroutedHandler(config Config) (*UnroutedHandler, error) { + if err := config.validate(); err != nil { + return nil, err + } + + // Only promote extesions using the Tus-Extension header which are implemented + extensions := "creation,creation-with-upload" + if config.StoreComposer.UsesTerminater { + extensions += ",termination" + } + if config.StoreComposer.UsesConcater { + extensions += ",concatenation" + } + if config.StoreComposer.UsesLengthDeferrer { + extensions += ",creation-defer-length" + } + + handler := &UnroutedHandler{ + config: config, + composer: config.StoreComposer, + basePath: config.BasePath, + isBasePathAbs: config.isAbs, + CompleteUploads: make(chan HookEvent), + TerminatedUploads: make(chan HookEvent), + UploadProgress: make(chan HookEvent), + CreatedUploads: make(chan HookEvent), + logger: config.Logger, + extensions: extensions, + Metrics: newMetrics(), + } + + return handler, nil +} + +// SupportedExtensions returns a comma-separated list of the supported tus extensions. +// The availability of an extension usually depends on whether the provided data store +// implements some additional interfaces. +func (handler *UnroutedHandler) SupportedExtensions() string { + return handler.extensions +} + +// Middleware checks various aspects of the request and ensures that it +// conforms with the spec. Also handles method overriding for clients which +// cannot make PATCH AND DELETE requests. If you are using the tusd handlers +// directly you will need to wrap at least the POST and PATCH endpoints in +// this middleware. +func (handler *UnroutedHandler) Middleware(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Construct our own context and make it available in the request. Successive logic + // should use handler.getContext to retrieve it + c := handler.newContext(w, r) + r = r.WithContext(c) + + // Set the initial read deadline for consuming the request body. All headers have already been read, + // so this is only for reading the request body. While reading, we regularly update the read deadline + // so this deadline is usually not final. See the bodyReader and writeChunk. + // We also update the write deadline, but makes sure that it is larger than the read deadline, so we + // can still write a response in the case of a read timeout. + if err := c.resC.SetReadDeadline(time.Now().Add(handler.config.NetworkTimeout)); err != nil { + c.log.Warn("NetworkControlError", "error", err) + } + if err := c.resC.SetWriteDeadline(time.Now().Add(2 * handler.config.NetworkTimeout)); err != nil { + c.log.Warn("NetworkControlError", "error", err) + } + + // Allow overriding the HTTP method. The reason for this is + // that some libraries/environments do not support PATCH and + // DELETE requests, e.g. Flash in a browser and parts of Java. + if newMethod := r.Header.Get("X-HTTP-Method-Override"); r.Method == "POST" && newMethod != "" { + r.Method = newMethod + } + + c.log.Info("RequestIncoming") + + handler.Metrics.incRequestsTotal(r.Method) + + header := w.Header() + + cors := handler.config.Cors + if origin := r.Header.Get("Origin"); !cors.Disable && origin != "" { + originIsAllowed := cors.AllowOrigin.MatchString(origin) + if !originIsAllowed { + handler.sendError(c, ErrOriginNotAllowed) + return + } + + header.Set("Access-Control-Allow-Origin", origin) + header.Set("Vary", "Origin") + + if cors.AllowCredentials { + header.Add("Access-Control-Allow-Credentials", "true") + } + + if r.Method == "OPTIONS" { + // Preflight request + header.Add("Access-Control-Allow-Methods", cors.AllowMethods) + header.Add("Access-Control-Allow-Headers", cors.AllowHeaders) + header.Set("Access-Control-Max-Age", cors.MaxAge) + } else { + // Actual request + header.Add("Access-Control-Expose-Headers", cors.ExposeHeaders) + } + } + + // Detect requests with tus v1 protocol vs the IETF resumable upload draft + isTusV1 := !handler.usesIETFDraft(r) + + if isTusV1 { + // Set current version used by the server + header.Set("Tus-Resumable", "1.0.0") + } + + // Add nosniff to all responses https://golang.org/src/net/http/server.go#L1429 + header.Set("X-Content-Type-Options", "nosniff") + + // Set appropriated headers in case of OPTIONS method allowing protocol + // discovery and end with an 204 No Content + if r.Method == "OPTIONS" { + if handler.config.MaxSize > 0 { + header.Set("Tus-Max-Size", strconv.FormatInt(handler.config.MaxSize, 10)) + } + + header.Set("Tus-Version", "1.0.0") + header.Set("Tus-Extension", handler.extensions) + + // Although the 204 No Content status code is a better fit in this case, + // since we do not have a response body included, we cannot use it here + // as some browsers only accept 200 OK as successful response to a + // preflight request. If we send them the 204 No Content the response + // will be ignored or interpreted as a rejection. + // For example, the Presto engine, which is used in older versions of + // Opera, Opera Mobile and Opera Mini, handles CORS this way. + handler.sendResp(c, HTTPResponse{ + StatusCode: http.StatusOK, + }) + return + } + + // Test if the version sent by the client is supported + // GET and HEAD methods are not checked since a browser may visit this URL and does + // not include this header. GET requests are not part of the specification. + if r.Method != "GET" && r.Method != "HEAD" && r.Header.Get("Tus-Resumable") != "1.0.0" && isTusV1 { + handler.sendError(c, ErrUnsupportedVersion) + return + } + + // Proceed with routing the request + h.ServeHTTP(w, r) + }) +} + +// PostFile creates a new file upload using the datastore after validating the +// length and parsing the metadata. +func (handler *UnroutedHandler) PostFile(w http.ResponseWriter, r *http.Request) { + if handler.usesIETFDraft(r) { + handler.PostFileV2(w, r) + return + } + + c := handler.getContext(w, r) + + // Check for presence of application/offset+octet-stream. If another content + // type is defined, it will be ignored and treated as none was set because + // some HTTP clients may enforce a default value for this header. + containsChunk := r.Header.Get("Content-Type") == "application/offset+octet-stream" + + // Only use the proper Upload-Concat header if the concatenation extension + // is even supported by the data store. + var concatHeader string + if handler.composer.UsesConcater { + concatHeader = r.Header.Get("Upload-Concat") + } + + // Parse Upload-Concat header + isPartial, isFinal, partialUploadIDs, err := parseConcat(concatHeader, handler.basePath) + if err != nil { + handler.sendError(c, err) + return + } + + // If the upload is a final upload created by concatenation multiple partial + // uploads the size is sum of all sizes of these files (no need for + // Upload-Length header) + var size int64 + var sizeIsDeferred bool + var partialUploads []Upload + if isFinal { + // A final upload must not contain a chunk within the creation request + if containsChunk { + handler.sendError(c, ErrModifyFinal) + return + } + + partialUploads, size, err = handler.sizeOfUploads(c, partialUploadIDs) + if err != nil { + handler.sendError(c, err) + return + } + } else { + uploadLengthHeader := r.Header.Get("Upload-Length") + uploadDeferLengthHeader := r.Header.Get("Upload-Defer-Length") + size, sizeIsDeferred, err = handler.validateNewUploadLengthHeaders(uploadLengthHeader, uploadDeferLengthHeader) + if err != nil { + handler.sendError(c, err) + return + } + } + + // Test whether the size is still allowed + if handler.config.MaxSize > 0 && size > handler.config.MaxSize { + handler.sendError(c, ErrMaxSizeExceeded) + return + } + + // Parse metadata + meta := ParseMetadataHeader(r.Header.Get("Upload-Metadata")) + + info := FileInfo{ + Size: size, + SizeIsDeferred: sizeIsDeferred, + MetaData: meta, + IsPartial: isPartial, + IsFinal: isFinal, + PartialUploads: partialUploadIDs, + } + + resp := HTTPResponse{ + StatusCode: http.StatusCreated, + Header: HTTPHeader{}, + } + + if handler.config.PreUploadCreateCallback != nil { + resp2, changes, err := handler.config.PreUploadCreateCallback(newHookEvent(c, info)) + if err != nil { + handler.sendError(c, err) + return + } + resp = resp.MergeWith(resp2) + + // Apply changes returned from the pre-create hook. + if changes.ID != "" { + if err := validateUploadId(changes.ID); err != nil { + handler.sendError(c, err) + return + } + + info.ID = changes.ID + } + + if changes.MetaData != nil { + info.MetaData = changes.MetaData + } + + if changes.Storage != nil { + info.Storage = changes.Storage + } + } + + upload, err := handler.composer.Core.NewUpload(c, info) + if err != nil { + handler.sendError(c, err) + return + } + + info, err = upload.GetInfo(c) + if err != nil { + handler.sendError(c, err) + return + } + + id := info.ID + + // Add the Location header directly after creating the new resource to even + // include it in cases of failure when an error is returned + url := handler.absFileURL(r, id) + resp.Header["Location"] = url + + handler.Metrics.incUploadsCreated() + c.log = c.log.With("id", id) + c.log.Info("UploadCreated", "id", id, "size", size, "url", url) + + if handler.config.NotifyCreatedUploads { + handler.CreatedUploads <- newHookEvent(c, info) + } + + if isFinal { + concatableUpload := handler.composer.Concater.AsConcatableUpload(upload) + if err := concatableUpload.ConcatUploads(c, partialUploads); err != nil { + handler.sendError(c, err) + return + } + info.Offset = size + + if handler.config.NotifyCompleteUploads { + handler.CompleteUploads <- newHookEvent(c, info) + } + } + + if containsChunk { + if handler.composer.UsesLocker { + lock, err := handler.lockUpload(c, id) + if err != nil { + handler.sendError(c, err) + return + } + + defer lock.Unlock() + } + + resp, err = handler.writeChunk(c, resp, upload, info) + if err != nil { + handler.sendError(c, err) + return + } + } else if !sizeIsDeferred && size == 0 { + // Directly finish the upload if the upload is empty (i.e. has a size of 0). + // This statement is in an else-if block to avoid causing duplicate calls + // to finishUploadIfComplete if an upload is empty and contains a chunk. + resp, err = handler.finishUploadIfComplete(c, resp, upload, info) + if err != nil { + handler.sendError(c, err) + return + } + + } + + handler.sendResp(c, resp) +} + +// PostFile creates a new file upload using the datastore after validating the +// length and parsing the metadata. +func (handler *UnroutedHandler) PostFileV2(w http.ResponseWriter, r *http.Request) { + currentUploadDraftInteropVersion := getIETFDraftInteropVersion(r) + c := handler.getContext(w, r) + + // Parse headers + contentType := r.Header.Get("Content-Type") + contentDisposition := r.Header.Get("Content-Disposition") + willCompleteUpload := isIETFDraftUploadComplete(r) + + info := FileInfo{ + MetaData: make(MetaData), + } + if willCompleteUpload && r.ContentLength != -1 { + // If the client wants to perform the upload in one request with Content-Length, we know the final upload size. + info.Size = r.ContentLength + } else { + // Error out if the storage does not support upload length deferring, but we need it. + if !handler.composer.UsesLengthDeferrer { + handler.sendError(c, ErrNotImplemented) + return + } + + info.SizeIsDeferred = true + } + + // Parse Content-Type and Content-Disposition to get file type or file name + if contentType != "" { + fileType, _, err := mime.ParseMediaType(contentType) + if err != nil { + handler.sendError(c, err) + return + } + + info.MetaData["filetype"] = fileType + } + + if contentDisposition != "" { + _, values, err := mime.ParseMediaType(contentDisposition) + if err != nil { + handler.sendError(c, err) + return + } + + if values["filename"] != "" { + info.MetaData["filename"] = values["filename"] + } + } + + resp := HTTPResponse{ + StatusCode: http.StatusCreated, + Header: HTTPHeader{}, + } + + // 1. Create upload resource + if handler.config.PreUploadCreateCallback != nil { + resp2, changes, err := handler.config.PreUploadCreateCallback(newHookEvent(c, info)) + if err != nil { + handler.sendError(c, err) + return + } + resp = resp.MergeWith(resp2) + + // Apply changes returned from the pre-create hook. + if changes.ID != "" { + if err := validateUploadId(changes.ID); err != nil { + handler.sendError(c, err) + return + } + + info.ID = changes.ID + } + + if changes.MetaData != nil { + info.MetaData = changes.MetaData + } + + if changes.Storage != nil { + info.Storage = changes.Storage + } + } + + upload, err := handler.composer.Core.NewUpload(c, info) + if err != nil { + handler.sendError(c, err) + return + } + + info, err = upload.GetInfo(c) + if err != nil { + handler.sendError(c, err) + return + } + + id := info.ID + url := handler.absFileURL(r, id) + resp.Header["Location"] = url + + // Send 104 response + w.Header().Set("Location", url) + w.Header().Set("Upload-Draft-Interop-Version", string(currentUploadDraftInteropVersion)) + w.WriteHeader(104) + + handler.Metrics.incUploadsCreated() + c.log = c.log.With("id", id) + c.log.Info("UploadCreated", "size", info.Size, "url", url) + + if handler.config.NotifyCreatedUploads { + handler.CreatedUploads <- newHookEvent(c, info) + } + + // 2. Lock upload + if handler.composer.UsesLocker { + lock, err := handler.lockUpload(c, id) + if err != nil { + handler.sendError(c, err) + return + } + + defer lock.Unlock() + } + + // 3. Write chunk + resp, err = handler.writeChunk(c, resp, upload, info) + if err != nil { + handler.sendError(c, err) + return + } + + // 4. Finish upload, if necessary + if willCompleteUpload && info.SizeIsDeferred { + info, err = upload.GetInfo(c) + if err != nil { + handler.sendError(c, err) + return + } + + uploadLength := info.Offset + + lengthDeclarableUpload := handler.composer.LengthDeferrer.AsLengthDeclarableUpload(upload) + if err := lengthDeclarableUpload.DeclareLength(c, uploadLength); err != nil { + handler.sendError(c, err) + return + } + + info.Size = uploadLength + info.SizeIsDeferred = false + + resp, err = handler.finishUploadIfComplete(c, resp, upload, info) + if err != nil { + handler.sendError(c, err) + return + } + + } + + handler.sendResp(c, resp) +} + +// HeadFile returns the length and offset for the HEAD request +func (handler *UnroutedHandler) HeadFile(w http.ResponseWriter, r *http.Request) { + c := handler.getContext(w, r) + + id, err := extractIDFromPath(r.URL.Path) + if err != nil { + handler.sendError(c, err) + return + } + c.log = c.log.With("id", id) + + if handler.composer.UsesLocker { + lock, err := handler.lockUpload(c, id) + if err != nil { + handler.sendError(c, err) + return + } + + defer lock.Unlock() + } + + upload, err := handler.composer.Core.GetUpload(c, id) + if err != nil { + handler.sendError(c, err) + return + } + + info, err := upload.GetInfo(c) + if err != nil { + handler.sendError(c, err) + return + } + + resp := HTTPResponse{ + Header: HTTPHeader{ + "Cache-Control": "no-store", + "Upload-Offset": strconv.FormatInt(info.Offset, 10), + }, + } + + if !handler.usesIETFDraft(r) { + // Add Upload-Concat header if possible + if info.IsPartial { + resp.Header["Upload-Concat"] = "partial" + } + + if info.IsFinal { + v := "final;" + for _, uploadID := range info.PartialUploads { + v += handler.absFileURL(r, uploadID) + " " + } + // Remove trailing space + v = v[:len(v)-1] + + resp.Header["Upload-Concat"] = v + } + + if len(info.MetaData) != 0 { + resp.Header["Upload-Metadata"] = SerializeMetadataHeader(info.MetaData) + } + + if info.SizeIsDeferred { + resp.Header["Upload-Defer-Length"] = UploadLengthDeferred + } else { + resp.Header["Upload-Length"] = strconv.FormatInt(info.Size, 10) + resp.Header["Content-Length"] = strconv.FormatInt(info.Size, 10) + } + + resp.StatusCode = http.StatusOK + } else { + isUploadCompleteNow := !info.SizeIsDeferred && info.Offset == info.Size + setIETFDraftUploadComplete(r, resp, isUploadCompleteNow) + resp.Header["Upload-Draft-Interop-Version"] = string(getIETFDraftInteropVersion(r)) + + // Draft -01 and -02 require a 204 No Content response. Version -03 allows 200 OK as well, + // but we stick to 204 to not make the logic less complex. + resp.StatusCode = http.StatusNoContent + } + + handler.sendResp(c, resp) +} + +// PatchFile adds a chunk to an upload. This operation is only allowed +// if enough space in the upload is left. +func (handler *UnroutedHandler) PatchFile(w http.ResponseWriter, r *http.Request) { + c := handler.getContext(w, r) + + isTusV1 := !handler.usesIETFDraft(r) + + // Check for presence of application/offset+octet-stream + if isTusV1 && r.Header.Get("Content-Type") != "application/offset+octet-stream" { + handler.sendError(c, ErrInvalidContentType) + return + } + + // Check for presence of a valid Upload-Offset Header + offset, err := strconv.ParseInt(r.Header.Get("Upload-Offset"), 10, 64) + if err != nil || offset < 0 { + handler.sendError(c, ErrInvalidOffset) + return + } + + id, err := extractIDFromPath(r.URL.Path) + if err != nil { + handler.sendError(c, err) + return + } + c.log = c.log.With("id", id) + + if handler.composer.UsesLocker { + lock, err := handler.lockUpload(c, id) + if err != nil { + handler.sendError(c, err) + return + } + + defer lock.Unlock() + } + + upload, err := handler.composer.Core.GetUpload(c, id) + if err != nil { + handler.sendError(c, err) + return + } + + info, err := upload.GetInfo(c) + if err != nil { + handler.sendError(c, err) + return + } + + // Modifying a final upload is not allowed + if info.IsFinal { + handler.sendError(c, ErrModifyFinal) + return + } + + if offset != info.Offset { + handler.sendError(c, ErrMismatchOffset) + return + } + + // TODO: If (Upload-Incomplete: ?0 OR Upload-Complete: ?1) and (Content-Length is set), we can + // - declare the length already here + // - validate that the length from this request matches info.Size if !info.SizeIsDeferred + + resp := HTTPResponse{ + StatusCode: http.StatusNoContent, + Header: make(HTTPHeader, 1), // Initialize map, so writeChunk can set the Upload-Offset header. + } + + // Do not proxy the call to the data store if the upload is already completed + if !info.SizeIsDeferred && info.Offset == info.Size { + resp.Header["Upload-Offset"] = strconv.FormatInt(offset, 10) + handler.sendResp(c, resp) + return + } + + if r.Header.Get("Upload-Length") != "" { + if !handler.composer.UsesLengthDeferrer { + handler.sendError(c, ErrNotImplemented) + return + } + if !info.SizeIsDeferred { + handler.sendError(c, ErrInvalidUploadLength) + return + } + uploadLength, err := strconv.ParseInt(r.Header.Get("Upload-Length"), 10, 64) + if err != nil || uploadLength < 0 || uploadLength < info.Offset || (handler.config.MaxSize > 0 && uploadLength > handler.config.MaxSize) { + handler.sendError(c, ErrInvalidUploadLength) + return + } + + lengthDeclarableUpload := handler.composer.LengthDeferrer.AsLengthDeclarableUpload(upload) + if err := lengthDeclarableUpload.DeclareLength(c, uploadLength); err != nil { + handler.sendError(c, err) + return + } + + info.Size = uploadLength + info.SizeIsDeferred = false + } + + resp, err = handler.writeChunk(c, resp, upload, info) + if err != nil { + handler.sendError(c, err) + return + } + + willCompleteUpload := isIETFDraftUploadComplete(r) + if willCompleteUpload && info.SizeIsDeferred { + info, err = upload.GetInfo(c) + if err != nil { + handler.sendError(c, err) + return + } + + uploadLength := info.Offset + + lengthDeclarableUpload := handler.composer.LengthDeferrer.AsLengthDeclarableUpload(upload) + if err := lengthDeclarableUpload.DeclareLength(c, uploadLength); err != nil { + handler.sendError(c, err) + return + } + + info.Size = uploadLength + info.SizeIsDeferred = false + + resp, err = handler.finishUploadIfComplete(c, resp, upload, info) + if err != nil { + handler.sendError(c, err) + return + } + } + + handler.sendResp(c, resp) +} + +// writeChunk reads the body from the requests r and appends it to the upload +// with the corresponding id. Afterwards, it will set the necessary response +// headers but will not send the response. +func (handler *UnroutedHandler) writeChunk(c *httpContext, resp HTTPResponse, upload Upload, info FileInfo) (HTTPResponse, error) { + // Get Content-Length if possible + r := c.req + length := r.ContentLength + offset := info.Offset + + // Test if this upload fits into the file's size + if !info.SizeIsDeferred && offset+length > info.Size { + return resp, ErrSizeExceeded + } + + maxSize := info.Size - offset + // If the upload's length is deferred and the PATCH request does not contain the Content-Length + // header (which is allowed if 'Transfer-Encoding: chunked' is used), we still need to set limits for + // the body size. + if info.SizeIsDeferred { + if handler.config.MaxSize > 0 { + // Ensure that the upload does not exceed the maximum upload size + maxSize = handler.config.MaxSize - offset + } else { + // If no upload limit is given, we allow arbitrary sizes + maxSize = math.MaxInt64 + } + } + if length > 0 { + maxSize = length + } + + c.log.Info("ChunkWriteStart", "maxSize", maxSize, "offset", offset) + + var bytesWritten int64 + var err error + // Prevent a nil pointer dereference when accessing the body which may not be + // available in the case of a malicious request. + if r.Body != nil { + // Limit the data read from the request's body to the allowed maximum. We use + // http.MaxBytesReader instead of io.LimitedReader because it returns an error + // if too much data is provided (handled in bodyReader) and also stops the server + // from reading the remaining request body. + c.body = newBodyReader(c, maxSize) + c.body.onReadDone = func() { + // Update the read deadline for every successful read operation. This ensures that the request handler + // keeps going while data is transmitted but that dead connections can also time out and be cleaned up. + if err := c.resC.SetReadDeadline(time.Now().Add(handler.config.NetworkTimeout)); err != nil { + c.log.Warn("NetworkTimeoutError", "error", err) + } + + // The write deadline is updated accordingly to ensure that we can also write responses. + if err := c.resC.SetWriteDeadline(time.Now().Add(2 * handler.config.NetworkTimeout)); err != nil { + c.log.Warn("NetworkTimeoutError", "error", err) + } + } + + // We use a callback to allow the hook system to cancel an upload. The callback + // cancels the request context causing the request body to be closed with the + // provided error. + info.stopUpload = func(res HTTPResponse) { + cause := ErrUploadStoppedByServer + cause.HTTPResponse = cause.HTTPResponse.MergeWith(res) + c.cancel(cause) + } + + if handler.config.NotifyUploadProgress { + handler.sendProgressMessages(c, info) + } + + bytesWritten, err = upload.WriteChunk(c, offset, c.body) + + // If we encountered an error while reading the body from the HTTP request, log it, but only include + // it in the response, if the store did not also return an error. + bodyErr := c.body.hasError() + if bodyErr != nil { + c.log.Error("BodyReadError", "error", bodyErr.Error()) + if err == nil { + err = bodyErr + } + } + + // Terminate the upload if it was stopped, as indicated by the ErrUploadStoppedByServer error. + terminateUpload := errors.Is(bodyErr, ErrUploadStoppedByServer) + if terminateUpload && handler.composer.UsesTerminater { + if terminateErr := handler.terminateUpload(c, upload, info); terminateErr != nil { + // We only log this error and not show it to the user since this + // termination error is not relevant to the uploading client + c.log.Error("UploadStopTerminateError", "error", terminateErr.Error()) + } + } + } + + c.log.Info("ChunkWriteComplete", "bytesWritten", bytesWritten) + + // Send new offset to client + newOffset := offset + bytesWritten + resp.Header["Upload-Offset"] = strconv.FormatInt(newOffset, 10) + handler.Metrics.incBytesReceived(uint64(bytesWritten)) + info.Offset = newOffset + + // We try to finish the upload, even if an error occurred. If we have a previous error, + // we return it and its HTTP response. + finishResp, finishErr := handler.finishUploadIfComplete(c, resp, upload, info) + if err != nil { + return resp, err + } + + return finishResp, finishErr +} + +// finishUploadIfComplete checks whether an upload is completed (i.e. upload offset +// matches upload size) and if so, it will call the data store's FinishUpload +// function and send the necessary message on the CompleteUpload channel. +func (handler *UnroutedHandler) finishUploadIfComplete(c *httpContext, resp HTTPResponse, upload Upload, info FileInfo) (HTTPResponse, error) { + // If the upload is completed, ... + if !info.SizeIsDeferred && info.Offset == info.Size { + // ... allow the data storage to finish and cleanup the upload + if err := upload.FinishUpload(c); err != nil { + return resp, err + } + + // ... allow the hook callback to run before sending the response + if handler.config.PreFinishResponseCallback != nil { + resp2, err := handler.config.PreFinishResponseCallback(newHookEvent(c, info)) + if err != nil { + return resp, err + } + resp = resp.MergeWith(resp2) + } + + c.log.Info("UploadFinished", "size", info.Size) + handler.Metrics.incUploadsFinished() + + // ... send the info out to the channel + if handler.config.NotifyCompleteUploads { + handler.CompleteUploads <- newHookEvent(c, info) + } + } + + return resp, nil +} + +// GetFile handles requests to download a file using a GET request. This is not +// part of the specification. +func (handler *UnroutedHandler) GetFile(w http.ResponseWriter, r *http.Request) { + c := handler.getContext(w, r) + + id, err := extractIDFromPath(r.URL.Path) + if err != nil { + handler.sendError(c, err) + return + } + c.log = c.log.With("id", id) + + if handler.composer.UsesLocker { + lock, err := handler.lockUpload(c, id) + if err != nil { + handler.sendError(c, err) + return + } + + defer lock.Unlock() + } + + upload, err := handler.composer.Core.GetUpload(c, id) + if err != nil { + handler.sendError(c, err) + return + } + + info, err := upload.GetInfo(c) + if err != nil { + handler.sendError(c, err) + return + } + + contentType, contentDisposition := filterContentType(info) + resp := HTTPResponse{ + StatusCode: http.StatusOK, + Header: HTTPHeader{ + "Content-Length": strconv.FormatInt(info.Offset, 10), + "Content-Type": contentType, + "Content-Disposition": contentDisposition, + }, + Body: "", // Body is intentionally left empty, and we copy it manually in later. + } + + // If no data has been uploaded yet, respond with an empty "204 No Content" status. + if info.Offset == 0 { + resp.StatusCode = http.StatusNoContent + handler.sendResp(c, resp) + return + } + + src, err := upload.GetReader(c) + if err != nil { + handler.sendError(c, err) + return + } + + handler.sendResp(c, resp) + io.Copy(w, src) + + src.Close() +} + +// mimeInlineBrowserWhitelist is a map containing MIME types which should be +// allowed to be rendered by browser inline, instead of being forced to be +// downloaded. For example, HTML or SVG files are not allowed, since they may +// contain malicious JavaScript. In a similiar fashion PDF is not on this list +// as their parsers commonly contain vulnerabilities which can be exploited. +// The values of this map does not convey any meaning and are therefore just +// empty structs. +var mimeInlineBrowserWhitelist = map[string]struct{}{ + "text/plain": {}, + + "image/png": {}, + "image/jpeg": {}, + "image/gif": {}, + "image/bmp": {}, + "image/webp": {}, + + "audio/wave": {}, + "audio/wav": {}, + "audio/x-wav": {}, + "audio/x-pn-wav": {}, + "audio/webm": {}, + "video/webm": {}, + "audio/ogg": {}, + "video/ogg": {}, + "application/ogg": {}, +} + +// filterContentType returns the values for the Content-Type and +// Content-Disposition headers for a given upload. These values should be used +// in responses for GET requests to ensure that only non-malicious file types +// are shown directly in the browser. It will extract the file name and type +// from the "fileame" and "filetype". +// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition +func filterContentType(info FileInfo) (contentType string, contentDisposition string) { + filetype := info.MetaData["filetype"] + + if reMimeType.MatchString(filetype) { + // If the filetype from metadata is well formed, we forward use this + // for the Content-Type header. However, only whitelisted mime types + // will be allowed to be shown inline in the browser + contentType = filetype + if _, isWhitelisted := mimeInlineBrowserWhitelist[filetype]; isWhitelisted { + contentDisposition = "inline" + } else { + contentDisposition = "attachment" + } + } else { + // If the filetype from the metadata is not well formed, we use a + // default type and force the browser to download the content. + contentType = "application/octet-stream" + contentDisposition = "attachment" + } + + // Add a filename to Content-Disposition if one is available in the metadata + if filename, ok := info.MetaData["filename"]; ok { + contentDisposition += ";filename=" + strconv.Quote(filename) + } + + return contentType, contentDisposition +} + +// DelFile terminates an upload permanently. +func (handler *UnroutedHandler) DelFile(w http.ResponseWriter, r *http.Request) { + c := handler.getContext(w, r) + + // Abort the request handling if the required interface is not implemented + if !handler.composer.UsesTerminater { + handler.sendError(c, ErrNotImplemented) + return + } + + id, err := extractIDFromPath(r.URL.Path) + if err != nil { + handler.sendError(c, err) + return + } + c.log = c.log.With("id", id) + + if handler.composer.UsesLocker { + lock, err := handler.lockUpload(c, id) + if err != nil { + handler.sendError(c, err) + return + } + + defer lock.Unlock() + } + + upload, err := handler.composer.Core.GetUpload(c, id) + if err != nil { + handler.sendError(c, err) + return + } + + var info FileInfo + if handler.config.NotifyTerminatedUploads { + info, err = upload.GetInfo(c) + if err != nil { + handler.sendError(c, err) + return + } + } + + err = handler.terminateUpload(c, upload, info) + if err != nil { + handler.sendError(c, err) + return + } + + handler.sendResp(c, HTTPResponse{ + StatusCode: http.StatusNoContent, + }) +} + +// terminateUpload passes a given upload to the DataStore's Terminater, +// send the corresponding upload info on the TerminatedUploads channnel +// and updates the statistics. +// Note the the info argument is only needed if the terminated uploads +// notifications are enabled. +func (handler *UnroutedHandler) terminateUpload(c *httpContext, upload Upload, info FileInfo) error { + terminatableUpload := handler.composer.Terminater.AsTerminatableUpload(upload) + + err := terminatableUpload.Terminate(c) + if err != nil { + return err + } + + if handler.config.NotifyTerminatedUploads { + handler.TerminatedUploads <- newHookEvent(c, info) + } + + c.log.Info("UploadTerminated") + handler.Metrics.incUploadsTerminated() + + return nil +} + +// Send the error in the response body. The status code will be looked up in +// ErrStatusCodes. If none is found 500 Internal Error will be used. +func (handler *UnroutedHandler) sendError(c *httpContext, err error) { + r := c.req + + detailedErr, ok := err.(Error) + if !ok { + c.log.Error("InternalServerError", "message", err.Error()) + detailedErr = NewError("ERR_INTERNAL_SERVER_ERROR", err.Error(), http.StatusInternalServerError) + } + + // If we are sending the response for a HEAD request, ensure that we are not including + // any response body. + if r.Method == "HEAD" { + detailedErr.HTTPResponse.Body = "" + } + + handler.sendResp(c, detailedErr.HTTPResponse) + handler.Metrics.incErrorsTotal(detailedErr) +} + +// sendResp writes the header to w with the specified status code. +func (handler *UnroutedHandler) sendResp(c *httpContext, resp HTTPResponse) { + resp.writeTo(c.res) + + c.log.Info("ResponseOutgoing", "status", resp.StatusCode, "body", resp.Body) +} + +// Make an absolute URLs to the given upload id. If the base path is absolute +// it will be prepended else the host and protocol from the request is used. +func (handler *UnroutedHandler) absFileURL(r *http.Request, id string) string { + if handler.isBasePathAbs { + return handler.basePath + id + } + + // Read origin and protocol from request + host, proto := getHostAndProtocol(r, handler.config.RespectForwardedHeaders) + + url := proto + "://" + host + handler.basePath + id + + return url +} + +// sendProgressMessage will send a notification over the UploadProgress channel +// indicating how much data has been transfered to the server. +// It will stop sending these instances once the provided context is done. +func (handler *UnroutedHandler) sendProgressMessages(c *httpContext, info FileInfo) { + hook := newHookEvent(c, info) + + previousOffset := int64(0) + originalOffset := hook.Upload.Offset + + emitProgress := func() { + hook.Upload.Offset = originalOffset + c.body.bytesRead() + if hook.Upload.Offset != previousOffset { + handler.UploadProgress <- hook + previousOffset = hook.Upload.Offset + } + } + + go func() { + for { + select { + case <-c.Done(): + emitProgress() + return + case <-time.After(handler.config.UploadProgressInterval): + emitProgress() + } + } + }() +} + +// getHostAndProtocol extracts the host and used protocol (either HTTP or HTTPS) +// from the given request. If `allowForwarded` is set, the X-Forwarded-Host, +// X-Forwarded-Proto and Forwarded headers will also be checked to +// support proxies. +func getHostAndProtocol(r *http.Request, allowForwarded bool) (host, proto string) { + if r.TLS != nil { + proto = "https" + } else { + proto = "http" + } + + host = r.Host + + if !allowForwarded { + return + } + + if h := r.Header.Get("X-Forwarded-Host"); h != "" { + host = h + } + + if h := r.Header.Get("X-Forwarded-Proto"); h == "http" || h == "https" { + proto = h + } + + if h := r.Header.Get("Forwarded"); h != "" { + if r := reForwardedHost.FindStringSubmatch(h); len(r) == 2 { + host = r[1] + } + + if r := reForwardedProto.FindStringSubmatch(h); len(r) == 2 { + proto = r[1] + } + } + + return +} + +// The get sum of all sizes for a list of upload ids while checking whether +// all of these uploads are finished yet. This is used to calculate the size +// of a final resource. +func (handler *UnroutedHandler) sizeOfUploads(ctx context.Context, ids []string) (partialUploads []Upload, size int64, err error) { + partialUploads = make([]Upload, len(ids)) + + for i, id := range ids { + upload, err := handler.composer.Core.GetUpload(ctx, id) + if err != nil { + return nil, 0, err + } + + info, err := upload.GetInfo(ctx) + if err != nil { + return nil, 0, err + } + + if info.SizeIsDeferred || info.Offset != info.Size { + err = ErrUploadNotFinished + return nil, 0, err + } + + size += info.Size + partialUploads[i] = upload + } + + return +} + +// Verify that the Upload-Length and Upload-Defer-Length headers are acceptable for creating a +// new upload +func (handler *UnroutedHandler) validateNewUploadLengthHeaders(uploadLengthHeader string, uploadDeferLengthHeader string) (uploadLength int64, uploadLengthDeferred bool, err error) { + haveBothLengthHeaders := uploadLengthHeader != "" && uploadDeferLengthHeader != "" + haveInvalidDeferHeader := uploadDeferLengthHeader != "" && uploadDeferLengthHeader != UploadLengthDeferred + lengthIsDeferred := uploadDeferLengthHeader == UploadLengthDeferred + + if lengthIsDeferred && !handler.composer.UsesLengthDeferrer { + err = ErrNotImplemented + } else if haveBothLengthHeaders { + err = ErrUploadLengthAndUploadDeferLength + } else if haveInvalidDeferHeader { + err = ErrInvalidUploadDeferLength + } else if lengthIsDeferred { + uploadLengthDeferred = true + } else { + uploadLength, err = strconv.ParseInt(uploadLengthHeader, 10, 64) + if err != nil || uploadLength < 0 { + err = ErrInvalidUploadLength + } + } + + return +} + +// lockUpload creates a new lock for the given upload ID and attempts to lock it. +// The created lock is returned if it was aquired successfully. +func (handler *UnroutedHandler) lockUpload(c *httpContext, id string) (Lock, error) { + lock, err := handler.composer.Locker.NewLock(id) + if err != nil { + return nil, err + } + + ctx, cancelContext := context.WithTimeout(c, handler.config.AcquireLockTimeout) + defer cancelContext() + + // No need to wrap this in a sync.OnceFunc because c.cancel will be a noop after the first call. + releaseLock := func() { + c.log.Info("UploadInterrupted") + c.cancel(ErrUploadInterrupted) + } + + if err := lock.Lock(ctx, releaseLock); err != nil { + return nil, err + } + + return lock, nil +} + +// usesIETFDraft returns whether a HTTP request uses a supported version of the resumable upload draft from IETF +// (instead of tus v1) and support has been enabled in tusd. +func (handler UnroutedHandler) usesIETFDraft(r *http.Request) bool { + interopVersionHeader := getIETFDraftInteropVersion(r) + return handler.config.EnableExperimentalProtocol && interopVersionHeader != "" +} + +// getIETFDraftInteropVersion returns the resumable upload draft interop version from the headers. +func getIETFDraftInteropVersion(r *http.Request) draftVersion { + version := draftVersion(r.Header.Get("Upload-Draft-Interop-Version")) + switch version { + case interopVersion3, interopVersion4, interopVersion5: + return version + default: + return "" + } +} + +// isIETFDraftUploadComplete returns whether a HTTP request upload is complete +// according to the set resumable upload draft version from IETF. +func isIETFDraftUploadComplete(r *http.Request) bool { + currentUploadDraftInteropVersion := getIETFDraftInteropVersion(r) + switch currentUploadDraftInteropVersion { + case interopVersion4, interopVersion5: + return r.Header.Get("Upload-Complete") == "?1" + case interopVersion3: + return r.Header.Get("Upload-Incomplete") == "?0" + default: + return false + } +} + +// setIETFDraftUploadComplete sets the Upload-Complete (Upload-Incomplete) to the provided +// value, depending on the interop version used in the request. +func setIETFDraftUploadComplete(r *http.Request, resp HTTPResponse, isComplete bool) { + currentUploadDraftInteropVersion := getIETFDraftInteropVersion(r) + + switch currentUploadDraftInteropVersion { + case interopVersion3: + if isComplete { + resp.Header["Upload-Incomplete"] = "?0" + } else { + resp.Header["Upload-Incomplete"] = "?1" + } + case interopVersion4, interopVersion5: + if isComplete { + resp.Header["Upload-Complete"] = "?1" + } else { + resp.Header["Upload-Complete"] = "?0" + } + } +} + +// ParseMetadataHeader parses the Upload-Metadata header as defined in the +// File Creation extension. +// e.g. Upload-Metadata: name bHVucmpzLnBuZw==,type aW1hZ2UvcG5n +func ParseMetadataHeader(header string) map[string]string { + meta := make(map[string]string) + + for _, element := range strings.Split(header, ",") { + element := strings.TrimSpace(element) + + parts := strings.Split(element, " ") + + if len(parts) > 2 { + continue + } + + key := parts[0] + if key == "" { + continue + } + + value := "" + if len(parts) == 2 { + // Ignore current element if the value is no valid base64 + dec, err := base64.StdEncoding.DecodeString(parts[1]) + if err != nil { + continue + } + + value = string(dec) + } + + meta[key] = value + } + + return meta +} + +// SerializeMetadataHeader serializes a map of strings into the Upload-Metadata +// header format used in the response for HEAD requests. +// e.g. Upload-Metadata: name bHVucmpzLnBuZw==,type aW1hZ2UvcG5n +func SerializeMetadataHeader(meta map[string]string) string { + header := "" + for key, value := range meta { + valueBase64 := base64.StdEncoding.EncodeToString([]byte(value)) + header += key + " " + valueBase64 + "," + } + + // Remove trailing comma + if len(header) > 0 { + header = header[:len(header)-1] + } + + return header +} + +// Parse the Upload-Concat header, e.g. +// Upload-Concat: partial +// Upload-Concat: final;http://tus.io/files/a /files/b/ +func parseConcat(header string, basePath string) (isPartial bool, isFinal bool, partialUploads []string, err error) { + if len(header) == 0 { + return + } + + if header == "partial" { + isPartial = true + return + } + + l := len("final;") + if strings.HasPrefix(header, "final;") && len(header) > l { + isFinal = true + + list := strings.Split(header[l:], " ") + for _, value := range list { + value := strings.TrimSpace(value) + if value == "" { + continue + } + + id, extractErr := extractIDFromURL(value, basePath) + if extractErr != nil { + err = extractErr + return + } + + partialUploads = append(partialUploads, id) + } + } + + // If no valid partial upload ids are extracted this is not a final upload. + if len(partialUploads) == 0 { + isFinal = false + err = ErrInvalidConcat + } + + return +} + +// extractIDFromPath extracts the upload ID from a path, which has already +// been stripped of the base path (done by the user). Effectively, we only +// remove leading and trailing slashes. +func extractIDFromPath(path string) (string, error) { + return strings.Trim(path, "/"), nil +} + +// extractIDFromURL extracts the upload ID from a full URL or a full path +// (including the base path). For example: +// +// https://example.com/files/1234/5678 -> 1234/5678 +// /files/1234/5678 -> 1234/5678 +func extractIDFromURL(url string, basePath string) (string, error) { + _, id, ok := strings.Cut(url, basePath) + if !ok { + return "", ErrNotFound + } + + return extractIDFromPath(id) +} + +// getRequestId returns the value of the X-Request-ID header, if available, +// and also takes care of truncating the input. +func getRequestId(r *http.Request) string { + reqId := r.Header.Get("X-Request-ID") + if reqId == "" { + return "" + } + + // Limit the length of the request ID to 36 characters, which is enough + // to fit a UUID. + if len(reqId) > 36 { + reqId = reqId[:36] + } + + return reqId +} + +// validateUploadId checks whether an ID included in a FileInfoChange struct is allowed. +func validateUploadId(newId string) error { + if newId == "" { + // An empty ID from FileInfoChanges is allowed. The store will then + // just pick an ID. + return nil + } + + if strings.HasPrefix(newId, "/") || strings.HasSuffix(newId, "/") { + // Disallow leading and trailing slashes, as these would be + // stripped away by extractIDFromPath, which can cause problems and confusion. + return fmt.Errorf("validation error in FileInfoChanges: ID must not begin or end with a forward slash (got: %s)", newId) + } + + if !reValidUploadId.MatchString(newId) { + // Disallow some non-URL-safe characters in the upload ID to + // prevent issues with URL parsing, which are though to debug for users. + return fmt.Errorf("validation error in FileInfoChanges: ID must contain only URL-safe character: %s (got: %s)", reValidUploadId.String(), newId) + } + + return nil +} diff --git a/vendor/golang.org/x/exp/slog/attr.go b/vendor/golang.org/x/exp/slog/attr.go new file mode 100644 index 0000000000..a180d0e1d3 --- /dev/null +++ b/vendor/golang.org/x/exp/slog/attr.go @@ -0,0 +1,102 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package slog + +import ( + "fmt" + "time" +) + +// An Attr is a key-value pair. +type Attr struct { + Key string + Value Value +} + +// String returns an Attr for a string value. +func String(key, value string) Attr { + return Attr{key, StringValue(value)} +} + +// Int64 returns an Attr for an int64. +func Int64(key string, value int64) Attr { + return Attr{key, Int64Value(value)} +} + +// Int converts an int to an int64 and returns +// an Attr with that value. +func Int(key string, value int) Attr { + return Int64(key, int64(value)) +} + +// Uint64 returns an Attr for a uint64. +func Uint64(key string, v uint64) Attr { + return Attr{key, Uint64Value(v)} +} + +// Float64 returns an Attr for a floating-point number. +func Float64(key string, v float64) Attr { + return Attr{key, Float64Value(v)} +} + +// Bool returns an Attr for a bool. +func Bool(key string, v bool) Attr { + return Attr{key, BoolValue(v)} +} + +// Time returns an Attr for a time.Time. +// It discards the monotonic portion. +func Time(key string, v time.Time) Attr { + return Attr{key, TimeValue(v)} +} + +// Duration returns an Attr for a time.Duration. +func Duration(key string, v time.Duration) Attr { + return Attr{key, DurationValue(v)} +} + +// Group returns an Attr for a Group Value. +// The first argument is the key; the remaining arguments +// are converted to Attrs as in [Logger.Log]. +// +// Use Group to collect several key-value pairs under a single +// key on a log line, or as the result of LogValue +// in order to log a single value as multiple Attrs. +func Group(key string, args ...any) Attr { + return Attr{key, GroupValue(argsToAttrSlice(args)...)} +} + +func argsToAttrSlice(args []any) []Attr { + var ( + attr Attr + attrs []Attr + ) + for len(args) > 0 { + attr, args = argsToAttr(args) + attrs = append(attrs, attr) + } + return attrs +} + +// Any returns an Attr for the supplied value. +// See [Value.AnyValue] for how values are treated. +func Any(key string, value any) Attr { + return Attr{key, AnyValue(value)} +} + +// Equal reports whether a and b have equal keys and values. +func (a Attr) Equal(b Attr) bool { + return a.Key == b.Key && a.Value.Equal(b.Value) +} + +func (a Attr) String() string { + return fmt.Sprintf("%s=%s", a.Key, a.Value) +} + +// isEmpty reports whether a has an empty key and a nil value. +// That can be written as Attr{} or Any("", nil). +func (a Attr) isEmpty() bool { + return a.Key == "" && a.Value.num == 0 && a.Value.any == nil +} diff --git a/vendor/golang.org/x/exp/slog/doc.go b/vendor/golang.org/x/exp/slog/doc.go new file mode 100644 index 0000000000..4beaf86748 --- /dev/null +++ b/vendor/golang.org/x/exp/slog/doc.go @@ -0,0 +1,316 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package slog provides structured logging, +in which log records include a message, +a severity level, and various other attributes +expressed as key-value pairs. + +It defines a type, [Logger], +which provides several methods (such as [Logger.Info] and [Logger.Error]) +for reporting events of interest. + +Each Logger is associated with a [Handler]. +A Logger output method creates a [Record] from the method arguments +and passes it to the Handler, which decides how to handle it. +There is a default Logger accessible through top-level functions +(such as [Info] and [Error]) that call the corresponding Logger methods. + +A log record consists of a time, a level, a message, and a set of key-value +pairs, where the keys are strings and the values may be of any type. +As an example, + + slog.Info("hello", "count", 3) + +creates a record containing the time of the call, +a level of Info, the message "hello", and a single +pair with key "count" and value 3. + +The [Info] top-level function calls the [Logger.Info] method on the default Logger. +In addition to [Logger.Info], there are methods for Debug, Warn and Error levels. +Besides these convenience methods for common levels, +there is also a [Logger.Log] method which takes the level as an argument. +Each of these methods has a corresponding top-level function that uses the +default logger. + +The default handler formats the log record's message, time, level, and attributes +as a string and passes it to the [log] package. + + 2022/11/08 15:28:26 INFO hello count=3 + +For more control over the output format, create a logger with a different handler. +This statement uses [New] to create a new logger with a TextHandler +that writes structured records in text form to standard error: + + logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) + +[TextHandler] output is a sequence of key=value pairs, easily and unambiguously +parsed by machine. This statement: + + logger.Info("hello", "count", 3) + +produces this output: + + time=2022-11-08T15:28:26.000-05:00 level=INFO msg=hello count=3 + +The package also provides [JSONHandler], whose output is line-delimited JSON: + + logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) + logger.Info("hello", "count", 3) + +produces this output: + + {"time":"2022-11-08T15:28:26.000000000-05:00","level":"INFO","msg":"hello","count":3} + +Both [TextHandler] and [JSONHandler] can be configured with [HandlerOptions]. +There are options for setting the minimum level (see Levels, below), +displaying the source file and line of the log call, and +modifying attributes before they are logged. + +Setting a logger as the default with + + slog.SetDefault(logger) + +will cause the top-level functions like [Info] to use it. +[SetDefault] also updates the default logger used by the [log] package, +so that existing applications that use [log.Printf] and related functions +will send log records to the logger's handler without needing to be rewritten. + +Some attributes are common to many log calls. +For example, you may wish to include the URL or trace identifier of a server request +with all log events arising from the request. +Rather than repeat the attribute with every log call, you can use [Logger.With] +to construct a new Logger containing the attributes: + + logger2 := logger.With("url", r.URL) + +The arguments to With are the same key-value pairs used in [Logger.Info]. +The result is a new Logger with the same handler as the original, but additional +attributes that will appear in the output of every call. + +# Levels + +A [Level] is an integer representing the importance or severity of a log event. +The higher the level, the more severe the event. +This package defines constants for the most common levels, +but any int can be used as a level. + +In an application, you may wish to log messages only at a certain level or greater. +One common configuration is to log messages at Info or higher levels, +suppressing debug logging until it is needed. +The built-in handlers can be configured with the minimum level to output by +setting [HandlerOptions.Level]. +The program's `main` function typically does this. +The default value is LevelInfo. + +Setting the [HandlerOptions.Level] field to a [Level] value +fixes the handler's minimum level throughout its lifetime. +Setting it to a [LevelVar] allows the level to be varied dynamically. +A LevelVar holds a Level and is safe to read or write from multiple +goroutines. +To vary the level dynamically for an entire program, first initialize +a global LevelVar: + + var programLevel = new(slog.LevelVar) // Info by default + +Then use the LevelVar to construct a handler, and make it the default: + + h := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: programLevel}) + slog.SetDefault(slog.New(h)) + +Now the program can change its logging level with a single statement: + + programLevel.Set(slog.LevelDebug) + +# Groups + +Attributes can be collected into groups. +A group has a name that is used to qualify the names of its attributes. +How this qualification is displayed depends on the handler. +[TextHandler] separates the group and attribute names with a dot. +[JSONHandler] treats each group as a separate JSON object, with the group name as the key. + +Use [Group] to create a Group attribute from a name and a list of key-value pairs: + + slog.Group("request", + "method", r.Method, + "url", r.URL) + +TextHandler would display this group as + + request.method=GET request.url=http://example.com + +JSONHandler would display it as + + "request":{"method":"GET","url":"http://example.com"} + +Use [Logger.WithGroup] to qualify all of a Logger's output +with a group name. Calling WithGroup on a Logger results in a +new Logger with the same Handler as the original, but with all +its attributes qualified by the group name. + +This can help prevent duplicate attribute keys in large systems, +where subsystems might use the same keys. +Pass each subsystem a different Logger with its own group name so that +potential duplicates are qualified: + + logger := slog.Default().With("id", systemID) + parserLogger := logger.WithGroup("parser") + parseInput(input, parserLogger) + +When parseInput logs with parserLogger, its keys will be qualified with "parser", +so even if it uses the common key "id", the log line will have distinct keys. + +# Contexts + +Some handlers may wish to include information from the [context.Context] that is +available at the call site. One example of such information +is the identifier for the current span when tracing is enabled. + +The [Logger.Log] and [Logger.LogAttrs] methods take a context as a first +argument, as do their corresponding top-level functions. + +Although the convenience methods on Logger (Info and so on) and the +corresponding top-level functions do not take a context, the alternatives ending +in "Context" do. For example, + + slog.InfoContext(ctx, "message") + +It is recommended to pass a context to an output method if one is available. + +# Attrs and Values + +An [Attr] is a key-value pair. The Logger output methods accept Attrs as well as +alternating keys and values. The statement + + slog.Info("hello", slog.Int("count", 3)) + +behaves the same as + + slog.Info("hello", "count", 3) + +There are convenience constructors for [Attr] such as [Int], [String], and [Bool] +for common types, as well as the function [Any] for constructing Attrs of any +type. + +The value part of an Attr is a type called [Value]. +Like an [any], a Value can hold any Go value, +but it can represent typical values, including all numbers and strings, +without an allocation. + +For the most efficient log output, use [Logger.LogAttrs]. +It is similar to [Logger.Log] but accepts only Attrs, not alternating +keys and values; this allows it, too, to avoid allocation. + +The call + + logger.LogAttrs(nil, slog.LevelInfo, "hello", slog.Int("count", 3)) + +is the most efficient way to achieve the same output as + + slog.Info("hello", "count", 3) + +# Customizing a type's logging behavior + +If a type implements the [LogValuer] interface, the [Value] returned from its LogValue +method is used for logging. You can use this to control how values of the type +appear in logs. For example, you can redact secret information like passwords, +or gather a struct's fields in a Group. See the examples under [LogValuer] for +details. + +A LogValue method may return a Value that itself implements [LogValuer]. The [Value.Resolve] +method handles these cases carefully, avoiding infinite loops and unbounded recursion. +Handler authors and others may wish to use Value.Resolve instead of calling LogValue directly. + +# Wrapping output methods + +The logger functions use reflection over the call stack to find the file name +and line number of the logging call within the application. This can produce +incorrect source information for functions that wrap slog. For instance, if you +define this function in file mylog.go: + + func Infof(format string, args ...any) { + slog.Default().Info(fmt.Sprintf(format, args...)) + } + +and you call it like this in main.go: + + Infof(slog.Default(), "hello, %s", "world") + +then slog will report the source file as mylog.go, not main.go. + +A correct implementation of Infof will obtain the source location +(pc) and pass it to NewRecord. +The Infof function in the package-level example called "wrapping" +demonstrates how to do this. + +# Working with Records + +Sometimes a Handler will need to modify a Record +before passing it on to another Handler or backend. +A Record contains a mixture of simple public fields (e.g. Time, Level, Message) +and hidden fields that refer to state (such as attributes) indirectly. This +means that modifying a simple copy of a Record (e.g. by calling +[Record.Add] or [Record.AddAttrs] to add attributes) +may have unexpected effects on the original. +Before modifying a Record, use [Clone] to +create a copy that shares no state with the original, +or create a new Record with [NewRecord] +and build up its Attrs by traversing the old ones with [Record.Attrs]. + +# Performance considerations + +If profiling your application demonstrates that logging is taking significant time, +the following suggestions may help. + +If many log lines have a common attribute, use [Logger.With] to create a Logger with +that attribute. The built-in handlers will format that attribute only once, at the +call to [Logger.With]. The [Handler] interface is designed to allow that optimization, +and a well-written Handler should take advantage of it. + +The arguments to a log call are always evaluated, even if the log event is discarded. +If possible, defer computation so that it happens only if the value is actually logged. +For example, consider the call + + slog.Info("starting request", "url", r.URL.String()) // may compute String unnecessarily + +The URL.String method will be called even if the logger discards Info-level events. +Instead, pass the URL directly: + + slog.Info("starting request", "url", &r.URL) // calls URL.String only if needed + +The built-in [TextHandler] will call its String method, but only +if the log event is enabled. +Avoiding the call to String also preserves the structure of the underlying value. +For example [JSONHandler] emits the components of the parsed URL as a JSON object. +If you want to avoid eagerly paying the cost of the String call +without causing the handler to potentially inspect the structure of the value, +wrap the value in a fmt.Stringer implementation that hides its Marshal methods. + +You can also use the [LogValuer] interface to avoid unnecessary work in disabled log +calls. Say you need to log some expensive value: + + slog.Debug("frobbing", "value", computeExpensiveValue(arg)) + +Even if this line is disabled, computeExpensiveValue will be called. +To avoid that, define a type implementing LogValuer: + + type expensive struct { arg int } + + func (e expensive) LogValue() slog.Value { + return slog.AnyValue(computeExpensiveValue(e.arg)) + } + +Then use a value of that type in log calls: + + slog.Debug("frobbing", "value", expensive{arg}) + +Now computeExpensiveValue will only be called when the line is enabled. + +The built-in handlers acquire a lock before calling [io.Writer.Write] +to ensure that each record is written in one piece. User-defined +handlers are responsible for their own locking. +*/ +package slog diff --git a/vendor/golang.org/x/exp/slog/handler.go b/vendor/golang.org/x/exp/slog/handler.go new file mode 100644 index 0000000000..bd635cb818 --- /dev/null +++ b/vendor/golang.org/x/exp/slog/handler.go @@ -0,0 +1,577 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package slog + +import ( + "context" + "fmt" + "io" + "reflect" + "strconv" + "sync" + "time" + + "golang.org/x/exp/slices" + "golang.org/x/exp/slog/internal/buffer" +) + +// A Handler handles log records produced by a Logger.. +// +// A typical handler may print log records to standard error, +// or write them to a file or database, or perhaps augment them +// with additional attributes and pass them on to another handler. +// +// Any of the Handler's methods may be called concurrently with itself +// or with other methods. It is the responsibility of the Handler to +// manage this concurrency. +// +// Users of the slog package should not invoke Handler methods directly. +// They should use the methods of [Logger] instead. +type Handler interface { + // Enabled reports whether the handler handles records at the given level. + // The handler ignores records whose level is lower. + // It is called early, before any arguments are processed, + // to save effort if the log event should be discarded. + // If called from a Logger method, the first argument is the context + // passed to that method, or context.Background() if nil was passed + // or the method does not take a context. + // The context is passed so Enabled can use its values + // to make a decision. + Enabled(context.Context, Level) bool + + // Handle handles the Record. + // It will only be called when Enabled returns true. + // The Context argument is as for Enabled. + // It is present solely to provide Handlers access to the context's values. + // Canceling the context should not affect record processing. + // (Among other things, log messages may be necessary to debug a + // cancellation-related problem.) + // + // Handle methods that produce output should observe the following rules: + // - If r.Time is the zero time, ignore the time. + // - If r.PC is zero, ignore it. + // - Attr's values should be resolved. + // - If an Attr's key and value are both the zero value, ignore the Attr. + // This can be tested with attr.Equal(Attr{}). + // - If a group's key is empty, inline the group's Attrs. + // - If a group has no Attrs (even if it has a non-empty key), + // ignore it. + Handle(context.Context, Record) error + + // WithAttrs returns a new Handler whose attributes consist of + // both the receiver's attributes and the arguments. + // The Handler owns the slice: it may retain, modify or discard it. + WithAttrs(attrs []Attr) Handler + + // WithGroup returns a new Handler with the given group appended to + // the receiver's existing groups. + // The keys of all subsequent attributes, whether added by With or in a + // Record, should be qualified by the sequence of group names. + // + // How this qualification happens is up to the Handler, so long as + // this Handler's attribute keys differ from those of another Handler + // with a different sequence of group names. + // + // A Handler should treat WithGroup as starting a Group of Attrs that ends + // at the end of the log event. That is, + // + // logger.WithGroup("s").LogAttrs(level, msg, slog.Int("a", 1), slog.Int("b", 2)) + // + // should behave like + // + // logger.LogAttrs(level, msg, slog.Group("s", slog.Int("a", 1), slog.Int("b", 2))) + // + // If the name is empty, WithGroup returns the receiver. + WithGroup(name string) Handler +} + +type defaultHandler struct { + ch *commonHandler + // log.Output, except for testing + output func(calldepth int, message string) error +} + +func newDefaultHandler(output func(int, string) error) *defaultHandler { + return &defaultHandler{ + ch: &commonHandler{json: false}, + output: output, + } +} + +func (*defaultHandler) Enabled(_ context.Context, l Level) bool { + return l >= LevelInfo +} + +// Collect the level, attributes and message in a string and +// write it with the default log.Logger. +// Let the log.Logger handle time and file/line. +func (h *defaultHandler) Handle(ctx context.Context, r Record) error { + buf := buffer.New() + buf.WriteString(r.Level.String()) + buf.WriteByte(' ') + buf.WriteString(r.Message) + state := h.ch.newHandleState(buf, true, " ", nil) + defer state.free() + state.appendNonBuiltIns(r) + + // skip [h.output, defaultHandler.Handle, handlerWriter.Write, log.Output] + return h.output(4, buf.String()) +} + +func (h *defaultHandler) WithAttrs(as []Attr) Handler { + return &defaultHandler{h.ch.withAttrs(as), h.output} +} + +func (h *defaultHandler) WithGroup(name string) Handler { + return &defaultHandler{h.ch.withGroup(name), h.output} +} + +// HandlerOptions are options for a TextHandler or JSONHandler. +// A zero HandlerOptions consists entirely of default values. +type HandlerOptions struct { + // AddSource causes the handler to compute the source code position + // of the log statement and add a SourceKey attribute to the output. + AddSource bool + + // Level reports the minimum record level that will be logged. + // The handler discards records with lower levels. + // If Level is nil, the handler assumes LevelInfo. + // The handler calls Level.Level for each record processed; + // to adjust the minimum level dynamically, use a LevelVar. + Level Leveler + + // ReplaceAttr is called to rewrite each non-group attribute before it is logged. + // The attribute's value has been resolved (see [Value.Resolve]). + // If ReplaceAttr returns an Attr with Key == "", the attribute is discarded. + // + // The built-in attributes with keys "time", "level", "source", and "msg" + // are passed to this function, except that time is omitted + // if zero, and source is omitted if AddSource is false. + // + // The first argument is a list of currently open groups that contain the + // Attr. It must not be retained or modified. ReplaceAttr is never called + // for Group attributes, only their contents. For example, the attribute + // list + // + // Int("a", 1), Group("g", Int("b", 2)), Int("c", 3) + // + // results in consecutive calls to ReplaceAttr with the following arguments: + // + // nil, Int("a", 1) + // []string{"g"}, Int("b", 2) + // nil, Int("c", 3) + // + // ReplaceAttr can be used to change the default keys of the built-in + // attributes, convert types (for example, to replace a `time.Time` with the + // integer seconds since the Unix epoch), sanitize personal information, or + // remove attributes from the output. + ReplaceAttr func(groups []string, a Attr) Attr +} + +// Keys for "built-in" attributes. +const ( + // TimeKey is the key used by the built-in handlers for the time + // when the log method is called. The associated Value is a [time.Time]. + TimeKey = "time" + // LevelKey is the key used by the built-in handlers for the level + // of the log call. The associated value is a [Level]. + LevelKey = "level" + // MessageKey is the key used by the built-in handlers for the + // message of the log call. The associated value is a string. + MessageKey = "msg" + // SourceKey is the key used by the built-in handlers for the source file + // and line of the log call. The associated value is a string. + SourceKey = "source" +) + +type commonHandler struct { + json bool // true => output JSON; false => output text + opts HandlerOptions + preformattedAttrs []byte + groupPrefix string // for text: prefix of groups opened in preformatting + groups []string // all groups started from WithGroup + nOpenGroups int // the number of groups opened in preformattedAttrs + mu sync.Mutex + w io.Writer +} + +func (h *commonHandler) clone() *commonHandler { + // We can't use assignment because we can't copy the mutex. + return &commonHandler{ + json: h.json, + opts: h.opts, + preformattedAttrs: slices.Clip(h.preformattedAttrs), + groupPrefix: h.groupPrefix, + groups: slices.Clip(h.groups), + nOpenGroups: h.nOpenGroups, + w: h.w, + } +} + +// enabled reports whether l is greater than or equal to the +// minimum level. +func (h *commonHandler) enabled(l Level) bool { + minLevel := LevelInfo + if h.opts.Level != nil { + minLevel = h.opts.Level.Level() + } + return l >= minLevel +} + +func (h *commonHandler) withAttrs(as []Attr) *commonHandler { + h2 := h.clone() + // Pre-format the attributes as an optimization. + prefix := buffer.New() + defer prefix.Free() + prefix.WriteString(h.groupPrefix) + state := h2.newHandleState((*buffer.Buffer)(&h2.preformattedAttrs), false, "", prefix) + defer state.free() + if len(h2.preformattedAttrs) > 0 { + state.sep = h.attrSep() + } + state.openGroups() + for _, a := range as { + state.appendAttr(a) + } + // Remember the new prefix for later keys. + h2.groupPrefix = state.prefix.String() + // Remember how many opened groups are in preformattedAttrs, + // so we don't open them again when we handle a Record. + h2.nOpenGroups = len(h2.groups) + return h2 +} + +func (h *commonHandler) withGroup(name string) *commonHandler { + if name == "" { + return h + } + h2 := h.clone() + h2.groups = append(h2.groups, name) + return h2 +} + +func (h *commonHandler) handle(r Record) error { + state := h.newHandleState(buffer.New(), true, "", nil) + defer state.free() + if h.json { + state.buf.WriteByte('{') + } + // Built-in attributes. They are not in a group. + stateGroups := state.groups + state.groups = nil // So ReplaceAttrs sees no groups instead of the pre groups. + rep := h.opts.ReplaceAttr + // time + if !r.Time.IsZero() { + key := TimeKey + val := r.Time.Round(0) // strip monotonic to match Attr behavior + if rep == nil { + state.appendKey(key) + state.appendTime(val) + } else { + state.appendAttr(Time(key, val)) + } + } + // level + key := LevelKey + val := r.Level + if rep == nil { + state.appendKey(key) + state.appendString(val.String()) + } else { + state.appendAttr(Any(key, val)) + } + // source + if h.opts.AddSource { + state.appendAttr(Any(SourceKey, r.source())) + } + key = MessageKey + msg := r.Message + if rep == nil { + state.appendKey(key) + state.appendString(msg) + } else { + state.appendAttr(String(key, msg)) + } + state.groups = stateGroups // Restore groups passed to ReplaceAttrs. + state.appendNonBuiltIns(r) + state.buf.WriteByte('\n') + + h.mu.Lock() + defer h.mu.Unlock() + _, err := h.w.Write(*state.buf) + return err +} + +func (s *handleState) appendNonBuiltIns(r Record) { + // preformatted Attrs + if len(s.h.preformattedAttrs) > 0 { + s.buf.WriteString(s.sep) + s.buf.Write(s.h.preformattedAttrs) + s.sep = s.h.attrSep() + } + // Attrs in Record -- unlike the built-in ones, they are in groups started + // from WithGroup. + s.prefix = buffer.New() + defer s.prefix.Free() + s.prefix.WriteString(s.h.groupPrefix) + s.openGroups() + r.Attrs(func(a Attr) bool { + s.appendAttr(a) + return true + }) + if s.h.json { + // Close all open groups. + for range s.h.groups { + s.buf.WriteByte('}') + } + // Close the top-level object. + s.buf.WriteByte('}') + } +} + +// attrSep returns the separator between attributes. +func (h *commonHandler) attrSep() string { + if h.json { + return "," + } + return " " +} + +// handleState holds state for a single call to commonHandler.handle. +// The initial value of sep determines whether to emit a separator +// before the next key, after which it stays true. +type handleState struct { + h *commonHandler + buf *buffer.Buffer + freeBuf bool // should buf be freed? + sep string // separator to write before next key + prefix *buffer.Buffer // for text: key prefix + groups *[]string // pool-allocated slice of active groups, for ReplaceAttr +} + +var groupPool = sync.Pool{New: func() any { + s := make([]string, 0, 10) + return &s +}} + +func (h *commonHandler) newHandleState(buf *buffer.Buffer, freeBuf bool, sep string, prefix *buffer.Buffer) handleState { + s := handleState{ + h: h, + buf: buf, + freeBuf: freeBuf, + sep: sep, + prefix: prefix, + } + if h.opts.ReplaceAttr != nil { + s.groups = groupPool.Get().(*[]string) + *s.groups = append(*s.groups, h.groups[:h.nOpenGroups]...) + } + return s +} + +func (s *handleState) free() { + if s.freeBuf { + s.buf.Free() + } + if gs := s.groups; gs != nil { + *gs = (*gs)[:0] + groupPool.Put(gs) + } +} + +func (s *handleState) openGroups() { + for _, n := range s.h.groups[s.h.nOpenGroups:] { + s.openGroup(n) + } +} + +// Separator for group names and keys. +const keyComponentSep = '.' + +// openGroup starts a new group of attributes +// with the given name. +func (s *handleState) openGroup(name string) { + if s.h.json { + s.appendKey(name) + s.buf.WriteByte('{') + s.sep = "" + } else { + s.prefix.WriteString(name) + s.prefix.WriteByte(keyComponentSep) + } + // Collect group names for ReplaceAttr. + if s.groups != nil { + *s.groups = append(*s.groups, name) + } +} + +// closeGroup ends the group with the given name. +func (s *handleState) closeGroup(name string) { + if s.h.json { + s.buf.WriteByte('}') + } else { + (*s.prefix) = (*s.prefix)[:len(*s.prefix)-len(name)-1 /* for keyComponentSep */] + } + s.sep = s.h.attrSep() + if s.groups != nil { + *s.groups = (*s.groups)[:len(*s.groups)-1] + } +} + +// appendAttr appends the Attr's key and value using app. +// It handles replacement and checking for an empty key. +// after replacement). +func (s *handleState) appendAttr(a Attr) { + if rep := s.h.opts.ReplaceAttr; rep != nil && a.Value.Kind() != KindGroup { + var gs []string + if s.groups != nil { + gs = *s.groups + } + // Resolve before calling ReplaceAttr, so the user doesn't have to. + a.Value = a.Value.Resolve() + a = rep(gs, a) + } + a.Value = a.Value.Resolve() + // Elide empty Attrs. + if a.isEmpty() { + return + } + // Special case: Source. + if v := a.Value; v.Kind() == KindAny { + if src, ok := v.Any().(*Source); ok { + if s.h.json { + a.Value = src.group() + } else { + a.Value = StringValue(fmt.Sprintf("%s:%d", src.File, src.Line)) + } + } + } + if a.Value.Kind() == KindGroup { + attrs := a.Value.Group() + // Output only non-empty groups. + if len(attrs) > 0 { + // Inline a group with an empty key. + if a.Key != "" { + s.openGroup(a.Key) + } + for _, aa := range attrs { + s.appendAttr(aa) + } + if a.Key != "" { + s.closeGroup(a.Key) + } + } + } else { + s.appendKey(a.Key) + s.appendValue(a.Value) + } +} + +func (s *handleState) appendError(err error) { + s.appendString(fmt.Sprintf("!ERROR:%v", err)) +} + +func (s *handleState) appendKey(key string) { + s.buf.WriteString(s.sep) + if s.prefix != nil { + // TODO: optimize by avoiding allocation. + s.appendString(string(*s.prefix) + key) + } else { + s.appendString(key) + } + if s.h.json { + s.buf.WriteByte(':') + } else { + s.buf.WriteByte('=') + } + s.sep = s.h.attrSep() +} + +func (s *handleState) appendString(str string) { + if s.h.json { + s.buf.WriteByte('"') + *s.buf = appendEscapedJSONString(*s.buf, str) + s.buf.WriteByte('"') + } else { + // text + if needsQuoting(str) { + *s.buf = strconv.AppendQuote(*s.buf, str) + } else { + s.buf.WriteString(str) + } + } +} + +func (s *handleState) appendValue(v Value) { + defer func() { + if r := recover(); r != nil { + // If it panics with a nil pointer, the most likely cases are + // an encoding.TextMarshaler or error fails to guard against nil, + // in which case "" seems to be the feasible choice. + // + // Adapted from the code in fmt/print.go. + if v := reflect.ValueOf(v.any); v.Kind() == reflect.Pointer && v.IsNil() { + s.appendString("") + return + } + + // Otherwise just print the original panic message. + s.appendString(fmt.Sprintf("!PANIC: %v", r)) + } + }() + + var err error + if s.h.json { + err = appendJSONValue(s, v) + } else { + err = appendTextValue(s, v) + } + if err != nil { + s.appendError(err) + } +} + +func (s *handleState) appendTime(t time.Time) { + if s.h.json { + appendJSONTime(s, t) + } else { + writeTimeRFC3339Millis(s.buf, t) + } +} + +// This takes half the time of Time.AppendFormat. +func writeTimeRFC3339Millis(buf *buffer.Buffer, t time.Time) { + year, month, day := t.Date() + buf.WritePosIntWidth(year, 4) + buf.WriteByte('-') + buf.WritePosIntWidth(int(month), 2) + buf.WriteByte('-') + buf.WritePosIntWidth(day, 2) + buf.WriteByte('T') + hour, min, sec := t.Clock() + buf.WritePosIntWidth(hour, 2) + buf.WriteByte(':') + buf.WritePosIntWidth(min, 2) + buf.WriteByte(':') + buf.WritePosIntWidth(sec, 2) + ns := t.Nanosecond() + buf.WriteByte('.') + buf.WritePosIntWidth(ns/1e6, 3) + _, offsetSeconds := t.Zone() + if offsetSeconds == 0 { + buf.WriteByte('Z') + } else { + offsetMinutes := offsetSeconds / 60 + if offsetMinutes < 0 { + buf.WriteByte('-') + offsetMinutes = -offsetMinutes + } else { + buf.WriteByte('+') + } + buf.WritePosIntWidth(offsetMinutes/60, 2) + buf.WriteByte(':') + buf.WritePosIntWidth(offsetMinutes%60, 2) + } +} diff --git a/vendor/golang.org/x/exp/slog/internal/buffer/buffer.go b/vendor/golang.org/x/exp/slog/internal/buffer/buffer.go new file mode 100644 index 0000000000..7786c166e0 --- /dev/null +++ b/vendor/golang.org/x/exp/slog/internal/buffer/buffer.go @@ -0,0 +1,84 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package buffer provides a pool-allocated byte buffer. +package buffer + +import ( + "sync" +) + +// Buffer adapted from go/src/fmt/print.go +type Buffer []byte + +// Having an initial size gives a dramatic speedup. +var bufPool = sync.Pool{ + New: func() any { + b := make([]byte, 0, 1024) + return (*Buffer)(&b) + }, +} + +func New() *Buffer { + return bufPool.Get().(*Buffer) +} + +func (b *Buffer) Free() { + // To reduce peak allocation, return only smaller buffers to the pool. + const maxBufferSize = 16 << 10 + if cap(*b) <= maxBufferSize { + *b = (*b)[:0] + bufPool.Put(b) + } +} + +func (b *Buffer) Reset() { + *b = (*b)[:0] +} + +func (b *Buffer) Write(p []byte) (int, error) { + *b = append(*b, p...) + return len(p), nil +} + +func (b *Buffer) WriteString(s string) { + *b = append(*b, s...) +} + +func (b *Buffer) WriteByte(c byte) { + *b = append(*b, c) +} + +func (b *Buffer) WritePosInt(i int) { + b.WritePosIntWidth(i, 0) +} + +// WritePosIntWidth writes non-negative integer i to the buffer, padded on the left +// by zeroes to the given width. Use a width of 0 to omit padding. +func (b *Buffer) WritePosIntWidth(i, width int) { + // Cheap integer to fixed-width decimal ASCII. + // Copied from log/log.go. + + if i < 0 { + panic("negative int") + } + + // Assemble decimal in reverse order. + var bb [20]byte + bp := len(bb) - 1 + for i >= 10 || width > 1 { + width-- + q := i / 10 + bb[bp] = byte('0' + i - q*10) + bp-- + i = q + } + // i < 10 + bb[bp] = byte('0' + i) + b.Write(bb[bp:]) +} + +func (b *Buffer) String() string { + return string(*b) +} diff --git a/vendor/golang.org/x/exp/slog/internal/ignorepc.go b/vendor/golang.org/x/exp/slog/internal/ignorepc.go new file mode 100644 index 0000000000..d1256426ff --- /dev/null +++ b/vendor/golang.org/x/exp/slog/internal/ignorepc.go @@ -0,0 +1,9 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package internal + +// If IgnorePC is true, do not invoke runtime.Callers to get the pc. +// This is solely for benchmarking the slowdown from runtime.Callers. +var IgnorePC = false diff --git a/vendor/golang.org/x/exp/slog/json_handler.go b/vendor/golang.org/x/exp/slog/json_handler.go new file mode 100644 index 0000000000..157ada8692 --- /dev/null +++ b/vendor/golang.org/x/exp/slog/json_handler.go @@ -0,0 +1,336 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package slog + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "strconv" + "time" + "unicode/utf8" + + "golang.org/x/exp/slog/internal/buffer" +) + +// JSONHandler is a Handler that writes Records to an io.Writer as +// line-delimited JSON objects. +type JSONHandler struct { + *commonHandler +} + +// NewJSONHandler creates a JSONHandler that writes to w, +// using the given options. +// If opts is nil, the default options are used. +func NewJSONHandler(w io.Writer, opts *HandlerOptions) *JSONHandler { + if opts == nil { + opts = &HandlerOptions{} + } + return &JSONHandler{ + &commonHandler{ + json: true, + w: w, + opts: *opts, + }, + } +} + +// Enabled reports whether the handler handles records at the given level. +// The handler ignores records whose level is lower. +func (h *JSONHandler) Enabled(_ context.Context, level Level) bool { + return h.commonHandler.enabled(level) +} + +// WithAttrs returns a new JSONHandler whose attributes consists +// of h's attributes followed by attrs. +func (h *JSONHandler) WithAttrs(attrs []Attr) Handler { + return &JSONHandler{commonHandler: h.commonHandler.withAttrs(attrs)} +} + +func (h *JSONHandler) WithGroup(name string) Handler { + return &JSONHandler{commonHandler: h.commonHandler.withGroup(name)} +} + +// Handle formats its argument Record as a JSON object on a single line. +// +// If the Record's time is zero, the time is omitted. +// Otherwise, the key is "time" +// and the value is output as with json.Marshal. +// +// If the Record's level is zero, the level is omitted. +// Otherwise, the key is "level" +// and the value of [Level.String] is output. +// +// If the AddSource option is set and source information is available, +// the key is "source" +// and the value is output as "FILE:LINE". +// +// The message's key is "msg". +// +// To modify these or other attributes, or remove them from the output, use +// [HandlerOptions.ReplaceAttr]. +// +// Values are formatted as with an [encoding/json.Encoder] with SetEscapeHTML(false), +// with two exceptions. +// +// First, an Attr whose Value is of type error is formatted as a string, by +// calling its Error method. Only errors in Attrs receive this special treatment, +// not errors embedded in structs, slices, maps or other data structures that +// are processed by the encoding/json package. +// +// Second, an encoding failure does not cause Handle to return an error. +// Instead, the error message is formatted as a string. +// +// Each call to Handle results in a single serialized call to io.Writer.Write. +func (h *JSONHandler) Handle(_ context.Context, r Record) error { + return h.commonHandler.handle(r) +} + +// Adapted from time.Time.MarshalJSON to avoid allocation. +func appendJSONTime(s *handleState, t time.Time) { + if y := t.Year(); y < 0 || y >= 10000 { + // RFC 3339 is clear that years are 4 digits exactly. + // See golang.org/issue/4556#c15 for more discussion. + s.appendError(errors.New("time.Time year outside of range [0,9999]")) + } + s.buf.WriteByte('"') + *s.buf = t.AppendFormat(*s.buf, time.RFC3339Nano) + s.buf.WriteByte('"') +} + +func appendJSONValue(s *handleState, v Value) error { + switch v.Kind() { + case KindString: + s.appendString(v.str()) + case KindInt64: + *s.buf = strconv.AppendInt(*s.buf, v.Int64(), 10) + case KindUint64: + *s.buf = strconv.AppendUint(*s.buf, v.Uint64(), 10) + case KindFloat64: + // json.Marshal is funny about floats; it doesn't + // always match strconv.AppendFloat. So just call it. + // That's expensive, but floats are rare. + if err := appendJSONMarshal(s.buf, v.Float64()); err != nil { + return err + } + case KindBool: + *s.buf = strconv.AppendBool(*s.buf, v.Bool()) + case KindDuration: + // Do what json.Marshal does. + *s.buf = strconv.AppendInt(*s.buf, int64(v.Duration()), 10) + case KindTime: + s.appendTime(v.Time()) + case KindAny: + a := v.Any() + _, jm := a.(json.Marshaler) + if err, ok := a.(error); ok && !jm { + s.appendString(err.Error()) + } else { + return appendJSONMarshal(s.buf, a) + } + default: + panic(fmt.Sprintf("bad kind: %s", v.Kind())) + } + return nil +} + +func appendJSONMarshal(buf *buffer.Buffer, v any) error { + // Use a json.Encoder to avoid escaping HTML. + var bb bytes.Buffer + enc := json.NewEncoder(&bb) + enc.SetEscapeHTML(false) + if err := enc.Encode(v); err != nil { + return err + } + bs := bb.Bytes() + buf.Write(bs[:len(bs)-1]) // remove final newline + return nil +} + +// appendEscapedJSONString escapes s for JSON and appends it to buf. +// It does not surround the string in quotation marks. +// +// Modified from encoding/json/encode.go:encodeState.string, +// with escapeHTML set to false. +func appendEscapedJSONString(buf []byte, s string) []byte { + char := func(b byte) { buf = append(buf, b) } + str := func(s string) { buf = append(buf, s...) } + + start := 0 + for i := 0; i < len(s); { + if b := s[i]; b < utf8.RuneSelf { + if safeSet[b] { + i++ + continue + } + if start < i { + str(s[start:i]) + } + char('\\') + switch b { + case '\\', '"': + char(b) + case '\n': + char('n') + case '\r': + char('r') + case '\t': + char('t') + default: + // This encodes bytes < 0x20 except for \t, \n and \r. + str(`u00`) + char(hex[b>>4]) + char(hex[b&0xF]) + } + i++ + start = i + continue + } + c, size := utf8.DecodeRuneInString(s[i:]) + if c == utf8.RuneError && size == 1 { + if start < i { + str(s[start:i]) + } + str(`\ufffd`) + i += size + start = i + continue + } + // U+2028 is LINE SEPARATOR. + // U+2029 is PARAGRAPH SEPARATOR. + // They are both technically valid characters in JSON strings, + // but don't work in JSONP, which has to be evaluated as JavaScript, + // and can lead to security holes there. It is valid JSON to + // escape them, so we do so unconditionally. + // See http://timelessrepo.com/json-isnt-a-javascript-subset for discussion. + if c == '\u2028' || c == '\u2029' { + if start < i { + str(s[start:i]) + } + str(`\u202`) + char(hex[c&0xF]) + i += size + start = i + continue + } + i += size + } + if start < len(s) { + str(s[start:]) + } + return buf +} + +var hex = "0123456789abcdef" + +// Copied from encoding/json/tables.go. +// +// safeSet holds the value true if the ASCII character with the given array +// position can be represented inside a JSON string without any further +// escaping. +// +// All values are true except for the ASCII control characters (0-31), the +// double quote ("), and the backslash character ("\"). +var safeSet = [utf8.RuneSelf]bool{ + ' ': true, + '!': true, + '"': false, + '#': true, + '$': true, + '%': true, + '&': true, + '\'': true, + '(': true, + ')': true, + '*': true, + '+': true, + ',': true, + '-': true, + '.': true, + '/': true, + '0': true, + '1': true, + '2': true, + '3': true, + '4': true, + '5': true, + '6': true, + '7': true, + '8': true, + '9': true, + ':': true, + ';': true, + '<': true, + '=': true, + '>': true, + '?': true, + '@': true, + 'A': true, + 'B': true, + 'C': true, + 'D': true, + 'E': true, + 'F': true, + 'G': true, + 'H': true, + 'I': true, + 'J': true, + 'K': true, + 'L': true, + 'M': true, + 'N': true, + 'O': true, + 'P': true, + 'Q': true, + 'R': true, + 'S': true, + 'T': true, + 'U': true, + 'V': true, + 'W': true, + 'X': true, + 'Y': true, + 'Z': true, + '[': true, + '\\': false, + ']': true, + '^': true, + '_': true, + '`': true, + 'a': true, + 'b': true, + 'c': true, + 'd': true, + 'e': true, + 'f': true, + 'g': true, + 'h': true, + 'i': true, + 'j': true, + 'k': true, + 'l': true, + 'm': true, + 'n': true, + 'o': true, + 'p': true, + 'q': true, + 'r': true, + 's': true, + 't': true, + 'u': true, + 'v': true, + 'w': true, + 'x': true, + 'y': true, + 'z': true, + '{': true, + '|': true, + '}': true, + '~': true, + '\u007f': true, +} diff --git a/vendor/golang.org/x/exp/slog/level.go b/vendor/golang.org/x/exp/slog/level.go new file mode 100644 index 0000000000..b2365f0aa5 --- /dev/null +++ b/vendor/golang.org/x/exp/slog/level.go @@ -0,0 +1,201 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package slog + +import ( + "errors" + "fmt" + "strconv" + "strings" + "sync/atomic" +) + +// A Level is the importance or severity of a log event. +// The higher the level, the more important or severe the event. +type Level int + +// Level numbers are inherently arbitrary, +// but we picked them to satisfy three constraints. +// Any system can map them to another numbering scheme if it wishes. +// +// First, we wanted the default level to be Info, Since Levels are ints, Info is +// the default value for int, zero. +// + +// Second, we wanted to make it easy to use levels to specify logger verbosity. +// Since a larger level means a more severe event, a logger that accepts events +// with smaller (or more negative) level means a more verbose logger. Logger +// verbosity is thus the negation of event severity, and the default verbosity +// of 0 accepts all events at least as severe as INFO. +// +// Third, we wanted some room between levels to accommodate schemes with named +// levels between ours. For example, Google Cloud Logging defines a Notice level +// between Info and Warn. Since there are only a few of these intermediate +// levels, the gap between the numbers need not be large. Our gap of 4 matches +// OpenTelemetry's mapping. Subtracting 9 from an OpenTelemetry level in the +// DEBUG, INFO, WARN and ERROR ranges converts it to the corresponding slog +// Level range. OpenTelemetry also has the names TRACE and FATAL, which slog +// does not. But those OpenTelemetry levels can still be represented as slog +// Levels by using the appropriate integers. +// +// Names for common levels. +const ( + LevelDebug Level = -4 + LevelInfo Level = 0 + LevelWarn Level = 4 + LevelError Level = 8 +) + +// String returns a name for the level. +// If the level has a name, then that name +// in uppercase is returned. +// If the level is between named values, then +// an integer is appended to the uppercased name. +// Examples: +// +// LevelWarn.String() => "WARN" +// (LevelInfo+2).String() => "INFO+2" +func (l Level) String() string { + str := func(base string, val Level) string { + if val == 0 { + return base + } + return fmt.Sprintf("%s%+d", base, val) + } + + switch { + case l < LevelInfo: + return str("DEBUG", l-LevelDebug) + case l < LevelWarn: + return str("INFO", l-LevelInfo) + case l < LevelError: + return str("WARN", l-LevelWarn) + default: + return str("ERROR", l-LevelError) + } +} + +// MarshalJSON implements [encoding/json.Marshaler] +// by quoting the output of [Level.String]. +func (l Level) MarshalJSON() ([]byte, error) { + // AppendQuote is sufficient for JSON-encoding all Level strings. + // They don't contain any runes that would produce invalid JSON + // when escaped. + return strconv.AppendQuote(nil, l.String()), nil +} + +// UnmarshalJSON implements [encoding/json.Unmarshaler] +// It accepts any string produced by [Level.MarshalJSON], +// ignoring case. +// It also accepts numeric offsets that would result in a different string on +// output. For example, "Error-8" would marshal as "INFO". +func (l *Level) UnmarshalJSON(data []byte) error { + s, err := strconv.Unquote(string(data)) + if err != nil { + return err + } + return l.parse(s) +} + +// MarshalText implements [encoding.TextMarshaler] +// by calling [Level.String]. +func (l Level) MarshalText() ([]byte, error) { + return []byte(l.String()), nil +} + +// UnmarshalText implements [encoding.TextUnmarshaler]. +// It accepts any string produced by [Level.MarshalText], +// ignoring case. +// It also accepts numeric offsets that would result in a different string on +// output. For example, "Error-8" would marshal as "INFO". +func (l *Level) UnmarshalText(data []byte) error { + return l.parse(string(data)) +} + +func (l *Level) parse(s string) (err error) { + defer func() { + if err != nil { + err = fmt.Errorf("slog: level string %q: %w", s, err) + } + }() + + name := s + offset := 0 + if i := strings.IndexAny(s, "+-"); i >= 0 { + name = s[:i] + offset, err = strconv.Atoi(s[i:]) + if err != nil { + return err + } + } + switch strings.ToUpper(name) { + case "DEBUG": + *l = LevelDebug + case "INFO": + *l = LevelInfo + case "WARN": + *l = LevelWarn + case "ERROR": + *l = LevelError + default: + return errors.New("unknown name") + } + *l += Level(offset) + return nil +} + +// Level returns the receiver. +// It implements Leveler. +func (l Level) Level() Level { return l } + +// A LevelVar is a Level variable, to allow a Handler level to change +// dynamically. +// It implements Leveler as well as a Set method, +// and it is safe for use by multiple goroutines. +// The zero LevelVar corresponds to LevelInfo. +type LevelVar struct { + val atomic.Int64 +} + +// Level returns v's level. +func (v *LevelVar) Level() Level { + return Level(int(v.val.Load())) +} + +// Set sets v's level to l. +func (v *LevelVar) Set(l Level) { + v.val.Store(int64(l)) +} + +func (v *LevelVar) String() string { + return fmt.Sprintf("LevelVar(%s)", v.Level()) +} + +// MarshalText implements [encoding.TextMarshaler] +// by calling [Level.MarshalText]. +func (v *LevelVar) MarshalText() ([]byte, error) { + return v.Level().MarshalText() +} + +// UnmarshalText implements [encoding.TextUnmarshaler] +// by calling [Level.UnmarshalText]. +func (v *LevelVar) UnmarshalText(data []byte) error { + var l Level + if err := l.UnmarshalText(data); err != nil { + return err + } + v.Set(l) + return nil +} + +// A Leveler provides a Level value. +// +// As Level itself implements Leveler, clients typically supply +// a Level value wherever a Leveler is needed, such as in HandlerOptions. +// Clients who need to vary the level dynamically can provide a more complex +// Leveler implementation such as *LevelVar. +type Leveler interface { + Level() Level +} diff --git a/vendor/golang.org/x/exp/slog/logger.go b/vendor/golang.org/x/exp/slog/logger.go new file mode 100644 index 0000000000..e87ec9936c --- /dev/null +++ b/vendor/golang.org/x/exp/slog/logger.go @@ -0,0 +1,343 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package slog + +import ( + "context" + "log" + "runtime" + "sync/atomic" + "time" + + "golang.org/x/exp/slog/internal" +) + +var defaultLogger atomic.Value + +func init() { + defaultLogger.Store(New(newDefaultHandler(log.Output))) +} + +// Default returns the default Logger. +func Default() *Logger { return defaultLogger.Load().(*Logger) } + +// SetDefault makes l the default Logger. +// After this call, output from the log package's default Logger +// (as with [log.Print], etc.) will be logged at LevelInfo using l's Handler. +func SetDefault(l *Logger) { + defaultLogger.Store(l) + // If the default's handler is a defaultHandler, then don't use a handleWriter, + // or we'll deadlock as they both try to acquire the log default mutex. + // The defaultHandler will use whatever the log default writer is currently + // set to, which is correct. + // This can occur with SetDefault(Default()). + // See TestSetDefault. + if _, ok := l.Handler().(*defaultHandler); !ok { + capturePC := log.Flags()&(log.Lshortfile|log.Llongfile) != 0 + log.SetOutput(&handlerWriter{l.Handler(), LevelInfo, capturePC}) + log.SetFlags(0) // we want just the log message, no time or location + } +} + +// handlerWriter is an io.Writer that calls a Handler. +// It is used to link the default log.Logger to the default slog.Logger. +type handlerWriter struct { + h Handler + level Level + capturePC bool +} + +func (w *handlerWriter) Write(buf []byte) (int, error) { + if !w.h.Enabled(context.Background(), w.level) { + return 0, nil + } + var pc uintptr + if !internal.IgnorePC && w.capturePC { + // skip [runtime.Callers, w.Write, Logger.Output, log.Print] + var pcs [1]uintptr + runtime.Callers(4, pcs[:]) + pc = pcs[0] + } + + // Remove final newline. + origLen := len(buf) // Report that the entire buf was written. + if len(buf) > 0 && buf[len(buf)-1] == '\n' { + buf = buf[:len(buf)-1] + } + r := NewRecord(time.Now(), w.level, string(buf), pc) + return origLen, w.h.Handle(context.Background(), r) +} + +// A Logger records structured information about each call to its +// Log, Debug, Info, Warn, and Error methods. +// For each call, it creates a Record and passes it to a Handler. +// +// To create a new Logger, call [New] or a Logger method +// that begins "With". +type Logger struct { + handler Handler // for structured logging +} + +func (l *Logger) clone() *Logger { + c := *l + return &c +} + +// Handler returns l's Handler. +func (l *Logger) Handler() Handler { return l.handler } + +// With returns a new Logger that includes the given arguments, converted to +// Attrs as in [Logger.Log]. +// The Attrs will be added to each output from the Logger. +// The new Logger shares the old Logger's context. +// The new Logger's handler is the result of calling WithAttrs on the receiver's +// handler. +func (l *Logger) With(args ...any) *Logger { + c := l.clone() + c.handler = l.handler.WithAttrs(argsToAttrSlice(args)) + return c +} + +// WithGroup returns a new Logger that starts a group. The keys of all +// attributes added to the Logger will be qualified by the given name. +// (How that qualification happens depends on the [Handler.WithGroup] +// method of the Logger's Handler.) +// The new Logger shares the old Logger's context. +// +// The new Logger's handler is the result of calling WithGroup on the receiver's +// handler. +func (l *Logger) WithGroup(name string) *Logger { + c := l.clone() + c.handler = l.handler.WithGroup(name) + return c + +} + +// New creates a new Logger with the given non-nil Handler and a nil context. +func New(h Handler) *Logger { + if h == nil { + panic("nil Handler") + } + return &Logger{handler: h} +} + +// With calls Logger.With on the default logger. +func With(args ...any) *Logger { + return Default().With(args...) +} + +// Enabled reports whether l emits log records at the given context and level. +func (l *Logger) Enabled(ctx context.Context, level Level) bool { + if ctx == nil { + ctx = context.Background() + } + return l.Handler().Enabled(ctx, level) +} + +// NewLogLogger returns a new log.Logger such that each call to its Output method +// dispatches a Record to the specified handler. The logger acts as a bridge from +// the older log API to newer structured logging handlers. +func NewLogLogger(h Handler, level Level) *log.Logger { + return log.New(&handlerWriter{h, level, true}, "", 0) +} + +// Log emits a log record with the current time and the given level and message. +// The Record's Attrs consist of the Logger's attributes followed by +// the Attrs specified by args. +// +// The attribute arguments are processed as follows: +// - If an argument is an Attr, it is used as is. +// - If an argument is a string and this is not the last argument, +// the following argument is treated as the value and the two are combined +// into an Attr. +// - Otherwise, the argument is treated as a value with key "!BADKEY". +func (l *Logger) Log(ctx context.Context, level Level, msg string, args ...any) { + l.log(ctx, level, msg, args...) +} + +// LogAttrs is a more efficient version of [Logger.Log] that accepts only Attrs. +func (l *Logger) LogAttrs(ctx context.Context, level Level, msg string, attrs ...Attr) { + l.logAttrs(ctx, level, msg, attrs...) +} + +// Debug logs at LevelDebug. +func (l *Logger) Debug(msg string, args ...any) { + l.log(nil, LevelDebug, msg, args...) +} + +// DebugContext logs at LevelDebug with the given context. +func (l *Logger) DebugContext(ctx context.Context, msg string, args ...any) { + l.log(ctx, LevelDebug, msg, args...) +} + +// DebugCtx logs at LevelDebug with the given context. +// Deprecated: Use Logger.DebugContext. +func (l *Logger) DebugCtx(ctx context.Context, msg string, args ...any) { + l.log(ctx, LevelDebug, msg, args...) +} + +// Info logs at LevelInfo. +func (l *Logger) Info(msg string, args ...any) { + l.log(nil, LevelInfo, msg, args...) +} + +// InfoContext logs at LevelInfo with the given context. +func (l *Logger) InfoContext(ctx context.Context, msg string, args ...any) { + l.log(ctx, LevelInfo, msg, args...) +} + +// InfoCtx logs at LevelInfo with the given context. +// Deprecated: Use Logger.InfoContext. +func (l *Logger) InfoCtx(ctx context.Context, msg string, args ...any) { + l.log(ctx, LevelInfo, msg, args...) +} + +// Warn logs at LevelWarn. +func (l *Logger) Warn(msg string, args ...any) { + l.log(nil, LevelWarn, msg, args...) +} + +// WarnContext logs at LevelWarn with the given context. +func (l *Logger) WarnContext(ctx context.Context, msg string, args ...any) { + l.log(ctx, LevelWarn, msg, args...) +} + +// WarnCtx logs at LevelWarn with the given context. +// Deprecated: Use Logger.WarnContext. +func (l *Logger) WarnCtx(ctx context.Context, msg string, args ...any) { + l.log(ctx, LevelWarn, msg, args...) +} + +// Error logs at LevelError. +func (l *Logger) Error(msg string, args ...any) { + l.log(nil, LevelError, msg, args...) +} + +// ErrorContext logs at LevelError with the given context. +func (l *Logger) ErrorContext(ctx context.Context, msg string, args ...any) { + l.log(ctx, LevelError, msg, args...) +} + +// ErrorCtx logs at LevelError with the given context. +// Deprecated: Use Logger.ErrorContext. +func (l *Logger) ErrorCtx(ctx context.Context, msg string, args ...any) { + l.log(ctx, LevelError, msg, args...) +} + +// log is the low-level logging method for methods that take ...any. +// It must always be called directly by an exported logging method +// or function, because it uses a fixed call depth to obtain the pc. +func (l *Logger) log(ctx context.Context, level Level, msg string, args ...any) { + if !l.Enabled(ctx, level) { + return + } + var pc uintptr + if !internal.IgnorePC { + var pcs [1]uintptr + // skip [runtime.Callers, this function, this function's caller] + runtime.Callers(3, pcs[:]) + pc = pcs[0] + } + r := NewRecord(time.Now(), level, msg, pc) + r.Add(args...) + if ctx == nil { + ctx = context.Background() + } + _ = l.Handler().Handle(ctx, r) +} + +// logAttrs is like [Logger.log], but for methods that take ...Attr. +func (l *Logger) logAttrs(ctx context.Context, level Level, msg string, attrs ...Attr) { + if !l.Enabled(ctx, level) { + return + } + var pc uintptr + if !internal.IgnorePC { + var pcs [1]uintptr + // skip [runtime.Callers, this function, this function's caller] + runtime.Callers(3, pcs[:]) + pc = pcs[0] + } + r := NewRecord(time.Now(), level, msg, pc) + r.AddAttrs(attrs...) + if ctx == nil { + ctx = context.Background() + } + _ = l.Handler().Handle(ctx, r) +} + +// Debug calls Logger.Debug on the default logger. +func Debug(msg string, args ...any) { + Default().log(nil, LevelDebug, msg, args...) +} + +// DebugContext calls Logger.DebugContext on the default logger. +func DebugContext(ctx context.Context, msg string, args ...any) { + Default().log(ctx, LevelDebug, msg, args...) +} + +// Info calls Logger.Info on the default logger. +func Info(msg string, args ...any) { + Default().log(nil, LevelInfo, msg, args...) +} + +// InfoContext calls Logger.InfoContext on the default logger. +func InfoContext(ctx context.Context, msg string, args ...any) { + Default().log(ctx, LevelInfo, msg, args...) +} + +// Warn calls Logger.Warn on the default logger. +func Warn(msg string, args ...any) { + Default().log(nil, LevelWarn, msg, args...) +} + +// WarnContext calls Logger.WarnContext on the default logger. +func WarnContext(ctx context.Context, msg string, args ...any) { + Default().log(ctx, LevelWarn, msg, args...) +} + +// Error calls Logger.Error on the default logger. +func Error(msg string, args ...any) { + Default().log(nil, LevelError, msg, args...) +} + +// ErrorContext calls Logger.ErrorContext on the default logger. +func ErrorContext(ctx context.Context, msg string, args ...any) { + Default().log(ctx, LevelError, msg, args...) +} + +// DebugCtx calls Logger.DebugContext on the default logger. +// Deprecated: call DebugContext. +func DebugCtx(ctx context.Context, msg string, args ...any) { + Default().log(ctx, LevelDebug, msg, args...) +} + +// InfoCtx calls Logger.InfoContext on the default logger. +// Deprecated: call InfoContext. +func InfoCtx(ctx context.Context, msg string, args ...any) { + Default().log(ctx, LevelInfo, msg, args...) +} + +// WarnCtx calls Logger.WarnContext on the default logger. +// Deprecated: call WarnContext. +func WarnCtx(ctx context.Context, msg string, args ...any) { + Default().log(ctx, LevelWarn, msg, args...) +} + +// ErrorCtx calls Logger.ErrorContext on the default logger. +// Deprecated: call ErrorContext. +func ErrorCtx(ctx context.Context, msg string, args ...any) { + Default().log(ctx, LevelError, msg, args...) +} + +// Log calls Logger.Log on the default logger. +func Log(ctx context.Context, level Level, msg string, args ...any) { + Default().log(ctx, level, msg, args...) +} + +// LogAttrs calls Logger.LogAttrs on the default logger. +func LogAttrs(ctx context.Context, level Level, msg string, attrs ...Attr) { + Default().logAttrs(ctx, level, msg, attrs...) +} diff --git a/vendor/golang.org/x/exp/slog/noplog.bench b/vendor/golang.org/x/exp/slog/noplog.bench new file mode 100644 index 0000000000..ed9296ff61 --- /dev/null +++ b/vendor/golang.org/x/exp/slog/noplog.bench @@ -0,0 +1,36 @@ +goos: linux +goarch: amd64 +pkg: golang.org/x/exp/slog +cpu: Intel(R) Xeon(R) CPU @ 2.20GHz +BenchmarkNopLog/attrs-8 1000000 1090 ns/op 0 B/op 0 allocs/op +BenchmarkNopLog/attrs-8 1000000 1097 ns/op 0 B/op 0 allocs/op +BenchmarkNopLog/attrs-8 1000000 1078 ns/op 0 B/op 0 allocs/op +BenchmarkNopLog/attrs-8 1000000 1095 ns/op 0 B/op 0 allocs/op +BenchmarkNopLog/attrs-8 1000000 1096 ns/op 0 B/op 0 allocs/op +BenchmarkNopLog/attrs-parallel-8 4007268 308.2 ns/op 0 B/op 0 allocs/op +BenchmarkNopLog/attrs-parallel-8 4016138 299.7 ns/op 0 B/op 0 allocs/op +BenchmarkNopLog/attrs-parallel-8 4020529 305.9 ns/op 0 B/op 0 allocs/op +BenchmarkNopLog/attrs-parallel-8 3977829 303.4 ns/op 0 B/op 0 allocs/op +BenchmarkNopLog/attrs-parallel-8 3225438 318.5 ns/op 0 B/op 0 allocs/op +BenchmarkNopLog/keys-values-8 1179256 994.2 ns/op 0 B/op 0 allocs/op +BenchmarkNopLog/keys-values-8 1000000 1002 ns/op 0 B/op 0 allocs/op +BenchmarkNopLog/keys-values-8 1216710 993.2 ns/op 0 B/op 0 allocs/op +BenchmarkNopLog/keys-values-8 1000000 1013 ns/op 0 B/op 0 allocs/op +BenchmarkNopLog/keys-values-8 1000000 1016 ns/op 0 B/op 0 allocs/op +BenchmarkNopLog/WithContext-8 989066 1163 ns/op 0 B/op 0 allocs/op +BenchmarkNopLog/WithContext-8 994116 1163 ns/op 0 B/op 0 allocs/op +BenchmarkNopLog/WithContext-8 1000000 1152 ns/op 0 B/op 0 allocs/op +BenchmarkNopLog/WithContext-8 991675 1165 ns/op 0 B/op 0 allocs/op +BenchmarkNopLog/WithContext-8 965268 1166 ns/op 0 B/op 0 allocs/op +BenchmarkNopLog/WithContext-parallel-8 3955503 303.3 ns/op 0 B/op 0 allocs/op +BenchmarkNopLog/WithContext-parallel-8 3861188 307.8 ns/op 0 B/op 0 allocs/op +BenchmarkNopLog/WithContext-parallel-8 3967752 303.9 ns/op 0 B/op 0 allocs/op +BenchmarkNopLog/WithContext-parallel-8 3955203 302.7 ns/op 0 B/op 0 allocs/op +BenchmarkNopLog/WithContext-parallel-8 3948278 301.1 ns/op 0 B/op 0 allocs/op +BenchmarkNopLog/Ctx-8 940622 1247 ns/op 0 B/op 0 allocs/op +BenchmarkNopLog/Ctx-8 936381 1257 ns/op 0 B/op 0 allocs/op +BenchmarkNopLog/Ctx-8 959730 1266 ns/op 0 B/op 0 allocs/op +BenchmarkNopLog/Ctx-8 943473 1290 ns/op 0 B/op 0 allocs/op +BenchmarkNopLog/Ctx-8 919414 1259 ns/op 0 B/op 0 allocs/op +PASS +ok golang.org/x/exp/slog 40.566s diff --git a/vendor/golang.org/x/exp/slog/record.go b/vendor/golang.org/x/exp/slog/record.go new file mode 100644 index 0000000000..38b3440f77 --- /dev/null +++ b/vendor/golang.org/x/exp/slog/record.go @@ -0,0 +1,207 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package slog + +import ( + "runtime" + "time" + + "golang.org/x/exp/slices" +) + +const nAttrsInline = 5 + +// A Record holds information about a log event. +// Copies of a Record share state. +// Do not modify a Record after handing out a copy to it. +// Use [Record.Clone] to create a copy with no shared state. +type Record struct { + // The time at which the output method (Log, Info, etc.) was called. + Time time.Time + + // The log message. + Message string + + // The level of the event. + Level Level + + // The program counter at the time the record was constructed, as determined + // by runtime.Callers. If zero, no program counter is available. + // + // The only valid use for this value is as an argument to + // [runtime.CallersFrames]. In particular, it must not be passed to + // [runtime.FuncForPC]. + PC uintptr + + // Allocation optimization: an inline array sized to hold + // the majority of log calls (based on examination of open-source + // code). It holds the start of the list of Attrs. + front [nAttrsInline]Attr + + // The number of Attrs in front. + nFront int + + // The list of Attrs except for those in front. + // Invariants: + // - len(back) > 0 iff nFront == len(front) + // - Unused array elements are zero. Used to detect mistakes. + back []Attr +} + +// NewRecord creates a Record from the given arguments. +// Use [Record.AddAttrs] to add attributes to the Record. +// +// NewRecord is intended for logging APIs that want to support a [Handler] as +// a backend. +func NewRecord(t time.Time, level Level, msg string, pc uintptr) Record { + return Record{ + Time: t, + Message: msg, + Level: level, + PC: pc, + } +} + +// Clone returns a copy of the record with no shared state. +// The original record and the clone can both be modified +// without interfering with each other. +func (r Record) Clone() Record { + r.back = slices.Clip(r.back) // prevent append from mutating shared array + return r +} + +// NumAttrs returns the number of attributes in the Record. +func (r Record) NumAttrs() int { + return r.nFront + len(r.back) +} + +// Attrs calls f on each Attr in the Record. +// Iteration stops if f returns false. +func (r Record) Attrs(f func(Attr) bool) { + for i := 0; i < r.nFront; i++ { + if !f(r.front[i]) { + return + } + } + for _, a := range r.back { + if !f(a) { + return + } + } +} + +// AddAttrs appends the given Attrs to the Record's list of Attrs. +func (r *Record) AddAttrs(attrs ...Attr) { + n := copy(r.front[r.nFront:], attrs) + r.nFront += n + // Check if a copy was modified by slicing past the end + // and seeing if the Attr there is non-zero. + if cap(r.back) > len(r.back) { + end := r.back[:len(r.back)+1][len(r.back)] + if !end.isEmpty() { + panic("copies of a slog.Record were both modified") + } + } + r.back = append(r.back, attrs[n:]...) +} + +// Add converts the args to Attrs as described in [Logger.Log], +// then appends the Attrs to the Record's list of Attrs. +func (r *Record) Add(args ...any) { + var a Attr + for len(args) > 0 { + a, args = argsToAttr(args) + if r.nFront < len(r.front) { + r.front[r.nFront] = a + r.nFront++ + } else { + if r.back == nil { + r.back = make([]Attr, 0, countAttrs(args)) + } + r.back = append(r.back, a) + } + } + +} + +// countAttrs returns the number of Attrs that would be created from args. +func countAttrs(args []any) int { + n := 0 + for i := 0; i < len(args); i++ { + n++ + if _, ok := args[i].(string); ok { + i++ + } + } + return n +} + +const badKey = "!BADKEY" + +// argsToAttr turns a prefix of the nonempty args slice into an Attr +// and returns the unconsumed portion of the slice. +// If args[0] is an Attr, it returns it. +// If args[0] is a string, it treats the first two elements as +// a key-value pair. +// Otherwise, it treats args[0] as a value with a missing key. +func argsToAttr(args []any) (Attr, []any) { + switch x := args[0].(type) { + case string: + if len(args) == 1 { + return String(badKey, x), nil + } + return Any(x, args[1]), args[2:] + + case Attr: + return x, args[1:] + + default: + return Any(badKey, x), args[1:] + } +} + +// Source describes the location of a line of source code. +type Source struct { + // Function is the package path-qualified function name containing the + // source line. If non-empty, this string uniquely identifies a single + // function in the program. This may be the empty string if not known. + Function string `json:"function"` + // File and Line are the file name and line number (1-based) of the source + // line. These may be the empty string and zero, respectively, if not known. + File string `json:"file"` + Line int `json:"line"` +} + +// attrs returns the non-zero fields of s as a slice of attrs. +// It is similar to a LogValue method, but we don't want Source +// to implement LogValuer because it would be resolved before +// the ReplaceAttr function was called. +func (s *Source) group() Value { + var as []Attr + if s.Function != "" { + as = append(as, String("function", s.Function)) + } + if s.File != "" { + as = append(as, String("file", s.File)) + } + if s.Line != 0 { + as = append(as, Int("line", s.Line)) + } + return GroupValue(as...) +} + +// source returns a Source for the log event. +// If the Record was created without the necessary information, +// or if the location is unavailable, it returns a non-nil *Source +// with zero fields. +func (r Record) source() *Source { + fs := runtime.CallersFrames([]uintptr{r.PC}) + f, _ := fs.Next() + return &Source{ + Function: f.Function, + File: f.File, + Line: f.Line, + } +} diff --git a/vendor/golang.org/x/exp/slog/text_handler.go b/vendor/golang.org/x/exp/slog/text_handler.go new file mode 100644 index 0000000000..75b66b716f --- /dev/null +++ b/vendor/golang.org/x/exp/slog/text_handler.go @@ -0,0 +1,161 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package slog + +import ( + "context" + "encoding" + "fmt" + "io" + "reflect" + "strconv" + "unicode" + "unicode/utf8" +) + +// TextHandler is a Handler that writes Records to an io.Writer as a +// sequence of key=value pairs separated by spaces and followed by a newline. +type TextHandler struct { + *commonHandler +} + +// NewTextHandler creates a TextHandler that writes to w, +// using the given options. +// If opts is nil, the default options are used. +func NewTextHandler(w io.Writer, opts *HandlerOptions) *TextHandler { + if opts == nil { + opts = &HandlerOptions{} + } + return &TextHandler{ + &commonHandler{ + json: false, + w: w, + opts: *opts, + }, + } +} + +// Enabled reports whether the handler handles records at the given level. +// The handler ignores records whose level is lower. +func (h *TextHandler) Enabled(_ context.Context, level Level) bool { + return h.commonHandler.enabled(level) +} + +// WithAttrs returns a new TextHandler whose attributes consists +// of h's attributes followed by attrs. +func (h *TextHandler) WithAttrs(attrs []Attr) Handler { + return &TextHandler{commonHandler: h.commonHandler.withAttrs(attrs)} +} + +func (h *TextHandler) WithGroup(name string) Handler { + return &TextHandler{commonHandler: h.commonHandler.withGroup(name)} +} + +// Handle formats its argument Record as a single line of space-separated +// key=value items. +// +// If the Record's time is zero, the time is omitted. +// Otherwise, the key is "time" +// and the value is output in RFC3339 format with millisecond precision. +// +// If the Record's level is zero, the level is omitted. +// Otherwise, the key is "level" +// and the value of [Level.String] is output. +// +// If the AddSource option is set and source information is available, +// the key is "source" and the value is output as FILE:LINE. +// +// The message's key is "msg". +// +// To modify these or other attributes, or remove them from the output, use +// [HandlerOptions.ReplaceAttr]. +// +// If a value implements [encoding.TextMarshaler], the result of MarshalText is +// written. Otherwise, the result of fmt.Sprint is written. +// +// Keys and values are quoted with [strconv.Quote] if they contain Unicode space +// characters, non-printing characters, '"' or '='. +// +// Keys inside groups consist of components (keys or group names) separated by +// dots. No further escaping is performed. +// Thus there is no way to determine from the key "a.b.c" whether there +// are two groups "a" and "b" and a key "c", or a single group "a.b" and a key "c", +// or single group "a" and a key "b.c". +// If it is necessary to reconstruct the group structure of a key +// even in the presence of dots inside components, use +// [HandlerOptions.ReplaceAttr] to encode that information in the key. +// +// Each call to Handle results in a single serialized call to +// io.Writer.Write. +func (h *TextHandler) Handle(_ context.Context, r Record) error { + return h.commonHandler.handle(r) +} + +func appendTextValue(s *handleState, v Value) error { + switch v.Kind() { + case KindString: + s.appendString(v.str()) + case KindTime: + s.appendTime(v.time()) + case KindAny: + if tm, ok := v.any.(encoding.TextMarshaler); ok { + data, err := tm.MarshalText() + if err != nil { + return err + } + // TODO: avoid the conversion to string. + s.appendString(string(data)) + return nil + } + if bs, ok := byteSlice(v.any); ok { + // As of Go 1.19, this only allocates for strings longer than 32 bytes. + s.buf.WriteString(strconv.Quote(string(bs))) + return nil + } + s.appendString(fmt.Sprintf("%+v", v.Any())) + default: + *s.buf = v.append(*s.buf) + } + return nil +} + +// byteSlice returns its argument as a []byte if the argument's +// underlying type is []byte, along with a second return value of true. +// Otherwise it returns nil, false. +func byteSlice(a any) ([]byte, bool) { + if bs, ok := a.([]byte); ok { + return bs, true + } + // Like Printf's %s, we allow both the slice type and the byte element type to be named. + t := reflect.TypeOf(a) + if t != nil && t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8 { + return reflect.ValueOf(a).Bytes(), true + } + return nil, false +} + +func needsQuoting(s string) bool { + if len(s) == 0 { + return true + } + for i := 0; i < len(s); { + b := s[i] + if b < utf8.RuneSelf { + // Quote anything except a backslash that would need quoting in a + // JSON string, as well as space and '=' + if b != '\\' && (b == ' ' || b == '=' || !safeSet[b]) { + return true + } + i++ + continue + } + r, size := utf8.DecodeRuneInString(s[i:]) + if r == utf8.RuneError || unicode.IsSpace(r) || !unicode.IsPrint(r) { + return true + } + i += size + } + return false +} diff --git a/vendor/golang.org/x/exp/slog/value.go b/vendor/golang.org/x/exp/slog/value.go new file mode 100644 index 0000000000..3550c46fc0 --- /dev/null +++ b/vendor/golang.org/x/exp/slog/value.go @@ -0,0 +1,456 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package slog + +import ( + "fmt" + "math" + "runtime" + "strconv" + "strings" + "time" + "unsafe" + + "golang.org/x/exp/slices" +) + +// A Value can represent any Go value, but unlike type any, +// it can represent most small values without an allocation. +// The zero Value corresponds to nil. +type Value struct { + _ [0]func() // disallow == + // num holds the value for Kinds Int64, Uint64, Float64, Bool and Duration, + // the string length for KindString, and nanoseconds since the epoch for KindTime. + num uint64 + // If any is of type Kind, then the value is in num as described above. + // If any is of type *time.Location, then the Kind is Time and time.Time value + // can be constructed from the Unix nanos in num and the location (monotonic time + // is not preserved). + // If any is of type stringptr, then the Kind is String and the string value + // consists of the length in num and the pointer in any. + // Otherwise, the Kind is Any and any is the value. + // (This implies that Attrs cannot store values of type Kind, *time.Location + // or stringptr.) + any any +} + +// Kind is the kind of a Value. +type Kind int + +// The following list is sorted alphabetically, but it's also important that +// KindAny is 0 so that a zero Value represents nil. + +const ( + KindAny Kind = iota + KindBool + KindDuration + KindFloat64 + KindInt64 + KindString + KindTime + KindUint64 + KindGroup + KindLogValuer +) + +var kindStrings = []string{ + "Any", + "Bool", + "Duration", + "Float64", + "Int64", + "String", + "Time", + "Uint64", + "Group", + "LogValuer", +} + +func (k Kind) String() string { + if k >= 0 && int(k) < len(kindStrings) { + return kindStrings[k] + } + return "" +} + +// Unexported version of Kind, just so we can store Kinds in Values. +// (No user-provided value has this type.) +type kind Kind + +// Kind returns v's Kind. +func (v Value) Kind() Kind { + switch x := v.any.(type) { + case Kind: + return x + case stringptr: + return KindString + case timeLocation: + return KindTime + case groupptr: + return KindGroup + case LogValuer: + return KindLogValuer + case kind: // a kind is just a wrapper for a Kind + return KindAny + default: + return KindAny + } +} + +//////////////// Constructors + +// IntValue returns a Value for an int. +func IntValue(v int) Value { + return Int64Value(int64(v)) +} + +// Int64Value returns a Value for an int64. +func Int64Value(v int64) Value { + return Value{num: uint64(v), any: KindInt64} +} + +// Uint64Value returns a Value for a uint64. +func Uint64Value(v uint64) Value { + return Value{num: v, any: KindUint64} +} + +// Float64Value returns a Value for a floating-point number. +func Float64Value(v float64) Value { + return Value{num: math.Float64bits(v), any: KindFloat64} +} + +// BoolValue returns a Value for a bool. +func BoolValue(v bool) Value { + u := uint64(0) + if v { + u = 1 + } + return Value{num: u, any: KindBool} +} + +// Unexported version of *time.Location, just so we can store *time.Locations in +// Values. (No user-provided value has this type.) +type timeLocation *time.Location + +// TimeValue returns a Value for a time.Time. +// It discards the monotonic portion. +func TimeValue(v time.Time) Value { + if v.IsZero() { + // UnixNano on the zero time is undefined, so represent the zero time + // with a nil *time.Location instead. time.Time.Location method never + // returns nil, so a Value with any == timeLocation(nil) cannot be + // mistaken for any other Value, time.Time or otherwise. + return Value{any: timeLocation(nil)} + } + return Value{num: uint64(v.UnixNano()), any: timeLocation(v.Location())} +} + +// DurationValue returns a Value for a time.Duration. +func DurationValue(v time.Duration) Value { + return Value{num: uint64(v.Nanoseconds()), any: KindDuration} +} + +// AnyValue returns a Value for the supplied value. +// +// If the supplied value is of type Value, it is returned +// unmodified. +// +// Given a value of one of Go's predeclared string, bool, or +// (non-complex) numeric types, AnyValue returns a Value of kind +// String, Bool, Uint64, Int64, or Float64. The width of the +// original numeric type is not preserved. +// +// Given a time.Time or time.Duration value, AnyValue returns a Value of kind +// KindTime or KindDuration. The monotonic time is not preserved. +// +// For nil, or values of all other types, including named types whose +// underlying type is numeric, AnyValue returns a value of kind KindAny. +func AnyValue(v any) Value { + switch v := v.(type) { + case string: + return StringValue(v) + case int: + return Int64Value(int64(v)) + case uint: + return Uint64Value(uint64(v)) + case int64: + return Int64Value(v) + case uint64: + return Uint64Value(v) + case bool: + return BoolValue(v) + case time.Duration: + return DurationValue(v) + case time.Time: + return TimeValue(v) + case uint8: + return Uint64Value(uint64(v)) + case uint16: + return Uint64Value(uint64(v)) + case uint32: + return Uint64Value(uint64(v)) + case uintptr: + return Uint64Value(uint64(v)) + case int8: + return Int64Value(int64(v)) + case int16: + return Int64Value(int64(v)) + case int32: + return Int64Value(int64(v)) + case float64: + return Float64Value(v) + case float32: + return Float64Value(float64(v)) + case []Attr: + return GroupValue(v...) + case Kind: + return Value{any: kind(v)} + case Value: + return v + default: + return Value{any: v} + } +} + +//////////////// Accessors + +// Any returns v's value as an any. +func (v Value) Any() any { + switch v.Kind() { + case KindAny: + if k, ok := v.any.(kind); ok { + return Kind(k) + } + return v.any + case KindLogValuer: + return v.any + case KindGroup: + return v.group() + case KindInt64: + return int64(v.num) + case KindUint64: + return v.num + case KindFloat64: + return v.float() + case KindString: + return v.str() + case KindBool: + return v.bool() + case KindDuration: + return v.duration() + case KindTime: + return v.time() + default: + panic(fmt.Sprintf("bad kind: %s", v.Kind())) + } +} + +// Int64 returns v's value as an int64. It panics +// if v is not a signed integer. +func (v Value) Int64() int64 { + if g, w := v.Kind(), KindInt64; g != w { + panic(fmt.Sprintf("Value kind is %s, not %s", g, w)) + } + return int64(v.num) +} + +// Uint64 returns v's value as a uint64. It panics +// if v is not an unsigned integer. +func (v Value) Uint64() uint64 { + if g, w := v.Kind(), KindUint64; g != w { + panic(fmt.Sprintf("Value kind is %s, not %s", g, w)) + } + return v.num +} + +// Bool returns v's value as a bool. It panics +// if v is not a bool. +func (v Value) Bool() bool { + if g, w := v.Kind(), KindBool; g != w { + panic(fmt.Sprintf("Value kind is %s, not %s", g, w)) + } + return v.bool() +} + +func (v Value) bool() bool { + return v.num == 1 +} + +// Duration returns v's value as a time.Duration. It panics +// if v is not a time.Duration. +func (v Value) Duration() time.Duration { + if g, w := v.Kind(), KindDuration; g != w { + panic(fmt.Sprintf("Value kind is %s, not %s", g, w)) + } + + return v.duration() +} + +func (v Value) duration() time.Duration { + return time.Duration(int64(v.num)) +} + +// Float64 returns v's value as a float64. It panics +// if v is not a float64. +func (v Value) Float64() float64 { + if g, w := v.Kind(), KindFloat64; g != w { + panic(fmt.Sprintf("Value kind is %s, not %s", g, w)) + } + + return v.float() +} + +func (v Value) float() float64 { + return math.Float64frombits(v.num) +} + +// Time returns v's value as a time.Time. It panics +// if v is not a time.Time. +func (v Value) Time() time.Time { + if g, w := v.Kind(), KindTime; g != w { + panic(fmt.Sprintf("Value kind is %s, not %s", g, w)) + } + return v.time() +} + +func (v Value) time() time.Time { + loc := v.any.(timeLocation) + if loc == nil { + return time.Time{} + } + return time.Unix(0, int64(v.num)).In(loc) +} + +// LogValuer returns v's value as a LogValuer. It panics +// if v is not a LogValuer. +func (v Value) LogValuer() LogValuer { + return v.any.(LogValuer) +} + +// Group returns v's value as a []Attr. +// It panics if v's Kind is not KindGroup. +func (v Value) Group() []Attr { + if sp, ok := v.any.(groupptr); ok { + return unsafe.Slice((*Attr)(sp), v.num) + } + panic("Group: bad kind") +} + +func (v Value) group() []Attr { + return unsafe.Slice((*Attr)(v.any.(groupptr)), v.num) +} + +//////////////// Other + +// Equal reports whether v and w represent the same Go value. +func (v Value) Equal(w Value) bool { + k1 := v.Kind() + k2 := w.Kind() + if k1 != k2 { + return false + } + switch k1 { + case KindInt64, KindUint64, KindBool, KindDuration: + return v.num == w.num + case KindString: + return v.str() == w.str() + case KindFloat64: + return v.float() == w.float() + case KindTime: + return v.time().Equal(w.time()) + case KindAny, KindLogValuer: + return v.any == w.any // may panic if non-comparable + case KindGroup: + return slices.EqualFunc(v.group(), w.group(), Attr.Equal) + default: + panic(fmt.Sprintf("bad kind: %s", k1)) + } +} + +// append appends a text representation of v to dst. +// v is formatted as with fmt.Sprint. +func (v Value) append(dst []byte) []byte { + switch v.Kind() { + case KindString: + return append(dst, v.str()...) + case KindInt64: + return strconv.AppendInt(dst, int64(v.num), 10) + case KindUint64: + return strconv.AppendUint(dst, v.num, 10) + case KindFloat64: + return strconv.AppendFloat(dst, v.float(), 'g', -1, 64) + case KindBool: + return strconv.AppendBool(dst, v.bool()) + case KindDuration: + return append(dst, v.duration().String()...) + case KindTime: + return append(dst, v.time().String()...) + case KindGroup: + return fmt.Append(dst, v.group()) + case KindAny, KindLogValuer: + return fmt.Append(dst, v.any) + default: + panic(fmt.Sprintf("bad kind: %s", v.Kind())) + } +} + +// A LogValuer is any Go value that can convert itself into a Value for logging. +// +// This mechanism may be used to defer expensive operations until they are +// needed, or to expand a single value into a sequence of components. +type LogValuer interface { + LogValue() Value +} + +const maxLogValues = 100 + +// Resolve repeatedly calls LogValue on v while it implements LogValuer, +// and returns the result. +// If v resolves to a group, the group's attributes' values are not recursively +// resolved. +// If the number of LogValue calls exceeds a threshold, a Value containing an +// error is returned. +// Resolve's return value is guaranteed not to be of Kind KindLogValuer. +func (v Value) Resolve() (rv Value) { + orig := v + defer func() { + if r := recover(); r != nil { + rv = AnyValue(fmt.Errorf("LogValue panicked\n%s", stack(3, 5))) + } + }() + + for i := 0; i < maxLogValues; i++ { + if v.Kind() != KindLogValuer { + return v + } + v = v.LogValuer().LogValue() + } + err := fmt.Errorf("LogValue called too many times on Value of type %T", orig.Any()) + return AnyValue(err) +} + +func stack(skip, nFrames int) string { + pcs := make([]uintptr, nFrames+1) + n := runtime.Callers(skip+1, pcs) + if n == 0 { + return "(no stack)" + } + frames := runtime.CallersFrames(pcs[:n]) + var b strings.Builder + i := 0 + for { + frame, more := frames.Next() + fmt.Fprintf(&b, "called from %s (%s:%d)\n", frame.Function, frame.File, frame.Line) + if !more { + break + } + i++ + if i >= nFrames { + fmt.Fprintf(&b, "(rest of stack elided)\n") + break + } + } + return b.String() +} diff --git a/vendor/golang.org/x/exp/slog/value_119.go b/vendor/golang.org/x/exp/slog/value_119.go new file mode 100644 index 0000000000..29b0d73292 --- /dev/null +++ b/vendor/golang.org/x/exp/slog/value_119.go @@ -0,0 +1,53 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.19 && !go1.20 + +package slog + +import ( + "reflect" + "unsafe" +) + +type ( + stringptr unsafe.Pointer // used in Value.any when the Value is a string + groupptr unsafe.Pointer // used in Value.any when the Value is a []Attr +) + +// StringValue returns a new Value for a string. +func StringValue(value string) Value { + hdr := (*reflect.StringHeader)(unsafe.Pointer(&value)) + return Value{num: uint64(hdr.Len), any: stringptr(hdr.Data)} +} + +func (v Value) str() string { + var s string + hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) + hdr.Data = uintptr(v.any.(stringptr)) + hdr.Len = int(v.num) + return s +} + +// String returns Value's value as a string, formatted like fmt.Sprint. Unlike +// the methods Int64, Float64, and so on, which panic if v is of the +// wrong kind, String never panics. +func (v Value) String() string { + if sp, ok := v.any.(stringptr); ok { + // Inlining this code makes a huge difference. + var s string + hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) + hdr.Data = uintptr(sp) + hdr.Len = int(v.num) + return s + } + return string(v.append(nil)) +} + +// GroupValue returns a new Value for a list of Attrs. +// The caller must not subsequently mutate the argument slice. +func GroupValue(as ...Attr) Value { + hdr := (*reflect.SliceHeader)(unsafe.Pointer(&as)) + return Value{num: uint64(hdr.Len), any: groupptr(hdr.Data)} +} diff --git a/vendor/golang.org/x/exp/slog/value_120.go b/vendor/golang.org/x/exp/slog/value_120.go new file mode 100644 index 0000000000..f7d4c09325 --- /dev/null +++ b/vendor/golang.org/x/exp/slog/value_120.go @@ -0,0 +1,39 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.20 + +package slog + +import "unsafe" + +type ( + stringptr *byte // used in Value.any when the Value is a string + groupptr *Attr // used in Value.any when the Value is a []Attr +) + +// StringValue returns a new Value for a string. +func StringValue(value string) Value { + return Value{num: uint64(len(value)), any: stringptr(unsafe.StringData(value))} +} + +// GroupValue returns a new Value for a list of Attrs. +// The caller must not subsequently mutate the argument slice. +func GroupValue(as ...Attr) Value { + return Value{num: uint64(len(as)), any: groupptr(unsafe.SliceData(as))} +} + +// String returns Value's value as a string, formatted like fmt.Sprint. Unlike +// the methods Int64, Float64, and so on, which panic if v is of the +// wrong kind, String never panics. +func (v Value) String() string { + if sp, ok := v.any.(stringptr); ok { + return unsafe.String(sp, v.num) + } + return string(v.append(nil)) +} + +func (v Value) str() string { + return unsafe.String(v.any.(stringptr), v.num) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 044d64cd95..8992dc2f0a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -370,7 +370,7 @@ github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1 github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1 github.com/cs3org/go-cs3apis/cs3/tx/v1beta1 github.com/cs3org/go-cs3apis/cs3/types/v1beta1 -# github.com/cs3org/reva/v2 v2.22.1-0.20240730105121-548644c31544 +# github.com/cs3org/reva/v2 v2.22.1-0.20240806075425-8bcdd93dfa20 ## explicit; go 1.21 github.com/cs3org/reva/v2/cmd/revad/internal/grace github.com/cs3org/reva/v2/cmd/revad/runtime @@ -659,6 +659,7 @@ github.com/cs3org/reva/v2/pkg/storage/fs/eos 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/loader github.com/cs3org/reva/v2/pkg/storage/fs/local github.com/cs3org/reva/v2/pkg/storage/fs/localhome @@ -1884,6 +1885,9 @@ github.com/trustelem/zxcvbn/scoring # github.com/tus/tusd v1.13.0 ## explicit; go 1.16 github.com/tus/tusd/pkg/handler +# github.com/tus/tusd/v2 v2.4.0 +## explicit; go 1.20 +github.com/tus/tusd/v2/pkg/handler # github.com/unrolled/secure v1.14.0 => github.com/DeepDiver1975/secure v0.0.0-20240611112133-abc838fb797c ## explicit; go 1.13 github.com/unrolled/secure @@ -2156,6 +2160,9 @@ golang.org/x/crypto/ssh/knownhosts golang.org/x/exp/constraints golang.org/x/exp/maps golang.org/x/exp/slices +golang.org/x/exp/slog +golang.org/x/exp/slog/internal +golang.org/x/exp/slog/internal/buffer # golang.org/x/image v0.18.0 ## explicit; go 1.18 golang.org/x/image/bmp