From 77a4c452b06f70864da7a43e0970de72af868a4a Mon Sep 17 00:00:00 2001 From: Gianmaria Del Monte <39946305+gmgigi96@users.noreply.github.com> Date: Mon, 14 Aug 2023 09:57:18 +0200 Subject: [PATCH] Notitications (#47) --- .drone.env | 2 +- .../unreleased/datatx-storage-drivers.md | 4 + changelog/unreleased/datatx-transfer-size.md | 4 + .../unreleased/notifications-framework.md | 13 + .../remove-job-on-cancel-transfer.md | 4 + cmd/reva/main.go | 1 - cmd/reva/transfer-create.go | 146 ------ cmd/revad/runtime/loader.go | 2 + ...ransfer-tutorial.md => datatx-tutorial.md} | 11 +- examples/datatx/datatx.toml | 24 +- go.mod | 56 +-- go.sum | 123 ++--- internal/grpc/services/datatx/datatx.go | 177 ++----- .../grpc/services/gateway/ocmshareprovider.go | 13 +- .../ocmshareprovider/ocmshareprovider.go | 33 +- .../publicshareprovider.go | 2 +- internal/http/services/loader/loader.go | 1 - internal/http/services/mailer/mailer.go | 378 --------------- internal/http/services/ocmd/ocm.go | 3 - .../http/services/owncloud/ocdav/ocdav.go | 18 +- internal/http/services/owncloud/ocdav/put.go | 51 +++ .../services/owncloud/ocs/config/config.go | 1 + .../services/owncloud/ocs/conversions/main.go | 24 +- .../ocs/handlers/apps/sharing/shares/group.go | 12 +- .../handlers/apps/sharing/shares/public.go | 104 ++++- .../handlers/apps/sharing/shares/shares.go | 144 +++++- .../ocs/handlers/apps/sharing/shares/user.go | 11 +- internal/http/services/owncloud/ocs/ocs.go | 12 +- internal/serverless/services/loader/loader.go | 1 + .../services/notifications/notifications.go | 403 ++++++++++++++++ pkg/cbox/publicshare/sql/sql.go | 52 ++- pkg/cbox/utils/conversions.go | 60 +-- pkg/datatx/datatx.go | 22 + pkg/datatx/manager/loader/loader.go | 2 + pkg/datatx/manager/rclone/rclone.go | 432 ++++++++---------- .../manager/rclone/repository/json/json.go | 171 +++++++ .../rclone/repository/registry/registry.go | 36 ++ .../manager/rclone/repository/repository.go | 44 ++ pkg/datatx/repository/json/json.go | 211 +++++++++ pkg/datatx/repository/registry/registry.go | 36 ++ pkg/notification/db_changes.sql | 54 +++ pkg/notification/db_sqlite.sql | 51 +++ .../handler/emailhandler/emailhandler.go | 116 +++++ pkg/notification/handler/handler.go | 24 + pkg/notification/handler/loader/loader.go | 25 + pkg/notification/handler/registry/registry.go | 60 +++ pkg/notification/manager/loader/loader.go | 25 + pkg/notification/manager/registry/registry.go | 36 ++ pkg/notification/manager/sql/sql.go | 199 ++++++++ .../manager/sql/sql_suite_test.go | 19 +- pkg/notification/manager/sql/sql_test.go | 252 ++++++++++ pkg/notification/manager/sql/test.sqlite | Bin 0 -> 32768 bytes pkg/notification/notification.go | 114 +++++ .../notificationhelper/notificationhelper.go | 250 ++++++++++ .../template/registry/registry.go | 69 +++ pkg/notification/template/template.go | 170 +++++++ pkg/notification/trigger/trigger.go | 41 ++ pkg/notification/utils/nats.go | 63 +++ pkg/publicshare/manager/json/json.go | 28 +- pkg/publicshare/manager/memory/memory.go | 28 +- pkg/publicshare/publicshare.go | 2 +- pkg/utils/accumulator/accumulator.go | 126 +++++ .../expected-failures-on-EOS-storage.md | 5 +- .../expected-failures-on-OCIS-storage.md | 164 ++++--- .../expected-failures-on-S3NG-storage.md | 165 ++++--- tests/ocis | 2 +- 66 files changed, 3646 insertions(+), 1286 deletions(-) create mode 100644 changelog/unreleased/datatx-storage-drivers.md create mode 100644 changelog/unreleased/datatx-transfer-size.md create mode 100644 changelog/unreleased/notifications-framework.md create mode 100644 changelog/unreleased/remove-job-on-cancel-transfer.md delete mode 100644 cmd/reva/transfer-create.go rename docs/content/en/docs/tutorials/{data-transfer-tutorial.md => datatx-tutorial.md} (95%) delete mode 100644 internal/http/services/mailer/mailer.go create mode 100644 internal/serverless/services/notifications/notifications.go create mode 100644 pkg/datatx/manager/rclone/repository/json/json.go create mode 100644 pkg/datatx/manager/rclone/repository/registry/registry.go create mode 100644 pkg/datatx/manager/rclone/repository/repository.go create mode 100644 pkg/datatx/repository/json/json.go create mode 100644 pkg/datatx/repository/registry/registry.go create mode 100644 pkg/notification/db_changes.sql create mode 100644 pkg/notification/db_sqlite.sql create mode 100644 pkg/notification/handler/emailhandler/emailhandler.go create mode 100644 pkg/notification/handler/handler.go create mode 100644 pkg/notification/handler/loader/loader.go create mode 100644 pkg/notification/handler/registry/registry.go create mode 100644 pkg/notification/manager/loader/loader.go create mode 100644 pkg/notification/manager/registry/registry.go create mode 100644 pkg/notification/manager/sql/sql.go rename internal/http/services/ocmd/notifications.go => pkg/notification/manager/sql/sql_suite_test.go (70%) create mode 100644 pkg/notification/manager/sql/sql_test.go create mode 100644 pkg/notification/manager/sql/test.sqlite create mode 100644 pkg/notification/notification.go create mode 100644 pkg/notification/notificationhelper/notificationhelper.go create mode 100644 pkg/notification/template/registry/registry.go create mode 100644 pkg/notification/template/template.go create mode 100644 pkg/notification/trigger/trigger.go create mode 100644 pkg/notification/utils/nats.go create mode 100644 pkg/utils/accumulator/accumulator.go diff --git a/.drone.env b/.drone.env index 4b25ea503b..f3660b888d 100644 --- a/.drone.env +++ b/.drone.env @@ -1,4 +1,4 @@ # The test runner source for API tests -APITESTS_COMMITID=1788406b5273782d5bad44543ba5b9f94d48370d +APITESTS_COMMITID=7094891f4de381102b05c6503751dc85d82c0782 APITESTS_BRANCH=master APITESTS_REPO_GIT_URL=https://github.com/owncloud/ocis.git diff --git a/changelog/unreleased/datatx-storage-drivers.md b/changelog/unreleased/datatx-storage-drivers.md new file mode 100644 index 0000000000..e81b30e001 --- /dev/null +++ b/changelog/unreleased/datatx-storage-drivers.md @@ -0,0 +1,4 @@ +Enhancement: Storage drivers setup for datatx + +https://github.com/cs3org/reva/pull/3915 +https://github.com/cs3org/reva/issues/3914 \ No newline at end of file diff --git a/changelog/unreleased/datatx-transfer-size.md b/changelog/unreleased/datatx-transfer-size.md new file mode 100644 index 0000000000..7820fc7b8b --- /dev/null +++ b/changelog/unreleased/datatx-transfer-size.md @@ -0,0 +1,4 @@ +Enhancement: Provide data transfer size with datatx share + +https://github.com/cs3org/reva/pull/3891 +https://github.com/cs3org/reva/issues/2104 \ No newline at end of file diff --git a/changelog/unreleased/notifications-framework.md b/changelog/unreleased/notifications-framework.md new file mode 100644 index 0000000000..83ccd26118 --- /dev/null +++ b/changelog/unreleased/notifications-framework.md @@ -0,0 +1,13 @@ +Enhancement: Notifications framework + +Adds a notifications framework to Reva. + +The new notifications service communicates with the rest of +reva using NATS. It provides helper functions to register new +notifications and to send them. + +Notification templates are provided in the configuration files +for each service, and they are registered into the notifications +service on initialization. + +https://github.com/cs3org/reva/pull/3825 diff --git a/changelog/unreleased/remove-job-on-cancel-transfer.md b/changelog/unreleased/remove-job-on-cancel-transfer.md new file mode 100644 index 0000000000..2063fea34c --- /dev/null +++ b/changelog/unreleased/remove-job-on-cancel-transfer.md @@ -0,0 +1,4 @@ +Bugfix: Remove transfer on cancel should also remove transfer job + +https://github.com/cs3org/reva/pull/3882 +https://github.com/cs3org/reva/issues/3881 \ No newline at end of file diff --git a/cmd/reva/main.go b/cmd/reva/main.go index 03f96278aa..f04984a0a7 100644 --- a/cmd/reva/main.go +++ b/cmd/reva/main.go @@ -81,7 +81,6 @@ var ( shareUpdateCommand(), shareListReceivedCommand(), shareUpdateReceivedCommand(), - transferCreateCommand(), transferGetStatusCommand(), transferCancelCommand(), transferListCommand(), diff --git a/cmd/reva/transfer-create.go b/cmd/reva/transfer-create.go deleted file mode 100644 index 969e3f1bc0..0000000000 --- a/cmd/reva/transfer-create.go +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright 2018-2023 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 main - -import ( - "io" - "os" - "strings" - "time" - - userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" - invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1" - ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1" - rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" - ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" - provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" - "github.com/cs3org/reva/pkg/ocm/share" - "github.com/cs3org/reva/pkg/utils" - "github.com/jedib0t/go-pretty/table" - "github.com/pkg/errors" -) - -func transferCreateCommand() *command { - cmd := newCommand("transfer-create") - cmd.Description = func() string { return "create transfer between 2 sites" } - cmd.Usage = func() string { return "Usage: transfer-create [-flags] " } - grantee := cmd.String("grantee", "", "the grantee, receiver of the transfer") - granteeType := cmd.String("granteeType", "user", "the grantee type, one of: user, group (defaults to user)") - idp := cmd.String("idp", "", "the idp of the grantee") - userType := cmd.String("user-type", "primary", "the type of user account, defaults to primary") - - cmd.Action = func(w ...io.Writer) error { - if cmd.NArg() < 1 { - return errors.New("Invalid arguments: " + cmd.Usage()) - } - - if *grantee == "" { - return errors.New("Grantee cannot be empty: use -grantee flag\n" + cmd.Usage()) - } - if *idp == "" { - return errors.New("Idp cannot be empty: use -idp flag\n" + cmd.Usage()) - } - - // the resource to transfer; the path - fn := cmd.Args()[0] - - ctx := getAuthContext() - client, err := getClient() - if err != nil { - return err - } - - u := &userpb.UserId{OpaqueId: *grantee, Idp: *idp, Type: utils.UserTypeMap(*userType)} - - // check if invitation has been accepted - acceptedUserRes, err := client.GetAcceptedUser(ctx, &invitepb.GetAcceptedUserRequest{ - RemoteUserId: u, - }) - if err != nil { - return err - } - if acceptedUserRes.Status.Code != rpc.Code_CODE_OK { - return formatError(acceptedUserRes.Status) - } - - // verify resource stats - statReq := &provider.StatRequest{ - Ref: &provider.Reference{Path: fn}, - } - statRes, err := client.Stat(ctx, statReq) - if err != nil { - return err - } - if statRes.Status.Code != rpc.Code_CODE_OK { - return formatError(statRes.Status) - } - - providerInfoResp, err := client.GetInfoByDomain(ctx, &ocmprovider.GetInfoByDomainRequest{ - Domain: *idp, - }) - if err != nil { - return err - } - - gt := provider.GranteeType_GRANTEE_TYPE_USER - if strings.ToLower(*granteeType) == "group" { - gt = provider.GranteeType_GRANTEE_TYPE_GROUP - } - - createShareReq := &ocm.CreateOCMShareRequest{ - Grantee: &provider.Grantee{ - Type: gt, - Id: &provider.Grantee_UserId{ - UserId: u, - }, - }, - ResourceId: statRes.Info.Id, - AccessMethods: []*ocm.AccessMethod{ - share.NewTransferAccessMethod(), - }, - RecipientMeshProvider: providerInfoResp.ProviderInfo, - } - - createShareResponse, err := client.CreateOCMShare(ctx, createShareReq) - if err != nil { - return err - } - if createShareResponse.Status.Code != rpc.Code_CODE_OK { - if createShareResponse.Status.Code == rpc.Code_CODE_NOT_FOUND { - return formatError(statRes.Status) - } - return err - } - - t := table.NewWriter() - t.SetOutputMirror(os.Stdout) - t.AppendHeader(table.Row{"#", "Owner.Idp", "Owner.OpaqueId", "ResourceId", "Type", "Grantee.Idp", "Grantee.OpaqueId", "ShareType", "Created", "Updated"}) - - s := createShareResponse.Share - t.AppendRows([]table.Row{ - {s.Id.OpaqueId, s.Owner.Idp, s.Owner.OpaqueId, s.ResourceId.String(), - s.Grantee.Type.String(), s.Grantee.GetUserId().Idp, s.Grantee.GetUserId().OpaqueId, s.ShareType.String(), - time.Unix(int64(s.Ctime.Seconds), 0), time.Unix(int64(s.Mtime.Seconds), 0)}, - }) - t.Render() - return nil - } - - return cmd -} diff --git a/cmd/revad/runtime/loader.go b/cmd/revad/runtime/loader.go index 52488936d3..ad174b9130 100644 --- a/cmd/revad/runtime/loader.go +++ b/cmd/revad/runtime/loader.go @@ -37,6 +37,8 @@ import ( _ "github.com/cs3org/reva/pkg/datatx/manager/loader" _ "github.com/cs3org/reva/pkg/group/manager/loader" _ "github.com/cs3org/reva/pkg/metrics/driver/loader" + _ "github.com/cs3org/reva/pkg/notification/handler/loader" + _ "github.com/cs3org/reva/pkg/notification/manager/loader" _ "github.com/cs3org/reva/pkg/ocm/invite/repository/loader" _ "github.com/cs3org/reva/pkg/ocm/provider/authorizer/loader" _ "github.com/cs3org/reva/pkg/ocm/share/repository/loader" diff --git a/docs/content/en/docs/tutorials/data-transfer-tutorial.md b/docs/content/en/docs/tutorials/datatx-tutorial.md similarity index 95% rename from docs/content/en/docs/tutorials/data-transfer-tutorial.md rename to docs/content/en/docs/tutorials/datatx-tutorial.md index 48aa76d94a..4e24cf3a0d 100644 --- a/docs/content/en/docs/tutorials/data-transfer-tutorial.md +++ b/docs/content/en/docs/tutorials/datatx-tutorial.md @@ -158,9 +158,14 @@ transfer-retry -txId fe671ae3-0fbf-4b06-b7df-32418c2ebfcb ``` ## 6 Cleanup transfers -Transfers will be removed from the db using the `transfer-cancel` command when the configuration property `remove_on_cancel` of the datatx service has been set to `true` as follows: +Transfers will be removed from the db using the `transfer-cancel` command when the configuration property `remove_transfer_on_cancel` and `remove_transfer_job_on_cancel` of the datatx service and rclone driver respectively have been set to `true` as follows: ``` [grpc.services.datatx] -remove_on_cancel = true +remove_transfer_on_cancel = true + +[grpc.services.datatx.txdrivers.rclone] +remove_transfer_job_on_cancel = true ``` -Currently this setting is recommended. \ No newline at end of file +Currently this setting is recommended. + +*Note that with these settings `transfer-cancel` will remove transfers & jobs even when a transfer cannot actually be cancelled because it was already in an end-state, eg. `finished` or `failed`. So `transfer-cancel` will act like a 'delete' function. \ No newline at end of file diff --git a/examples/datatx/datatx.toml b/examples/datatx/datatx.toml index bb19821f9e..b5296f0cb1 100644 --- a/examples/datatx/datatx.toml +++ b/examples/datatx/datatx.toml @@ -9,10 +9,11 @@ data_transfers_folder = "" [grpc.services.datatx] # rclone is currently the only data transfer driver implementation txdriver = "rclone" -# the shares,transfers db file (default: /var/tmp/reva/datatx-shares.json) -tx_shares_file = "" -# base folder of the data transfers (eg. /home/DataTransfers) -data_transfers_folder = "" +# the storage driver +storagedriver = "json" +# if set to 'true' the transfer will always be removed from the db upon cancel request +# recommended value is true +remove_transfer_on_cancel = true # rclone driver [grpc.services.datatx.txdrivers.rclone] @@ -27,12 +28,23 @@ auth_pass = "{rclone user secret}" # "x-access-token" will result in rclone using request header: X-Access-Token: "...token..." # If not set "bearer" is assumed auth_header = "x-access-token" -# the transfers(jobs) db file (default: /var/tmp/reva/datatx-transfers.json) -file = "" # check status job interval in milliseconds job_status_check_interval = 2000 # the job timeout in milliseconds (must be long enough for big transfers!) job_timeout = 120000 +# the storage driver +storagedriver = "json" +# if set to 'true' the transfer job will always be removed from the db upon transfer cancel request +# recommended value is true +remove_transfer_job_on_cancel = true + +[grpc.services.datatx.storagedrivers.json] +# the datatx transfers db file (defaults to: /var/tmp/reva/datatx-transfers.json) +file = "" + +[grpc.services.datatx.txdrivers.rclone.storagedrivers.json] +# the transfers jobs db file (defaults to: /var/tmp/reva/transfer-jobs.json) +file = "" [http.services.ocdav] # reva supports http third party copy diff --git a/go.mod b/go.mod index 4386d9d1b8..ec7b60403c 100644 --- a/go.mod +++ b/go.mod @@ -14,9 +14,11 @@ require ( github.com/cenkalti/backoff v2.2.1+incompatible github.com/ceph/go-ceph v0.15.0 github.com/cheggaaa/pb v1.0.29 + github.com/coreos/go-oidc/v3 v3.5.0 github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e github.com/cs3org/go-cs3apis v0.0.0-20230508132523-e0d062e63b3b github.com/dgraph-io/ristretto v0.1.1 + github.com/disintegration/imaging v1.6.2 github.com/dolthub/go-mysql-server v0.14.0 github.com/eventials/go-tus v0.0.0-20200718001131-45c7ec8f5d59 github.com/gdexlab/go-render v1.0.1 @@ -24,14 +26,14 @@ require ( github.com/go-chi/chi/v5 v5.0.8 github.com/go-ldap/ldap/v3 v3.4.4 github.com/go-playground/validator/v10 v10.11.2 - github.com/go-sql-driver/mysql v1.6.0 + github.com/go-sql-driver/mysql v1.7.0 github.com/golang-jwt/jwt v3.2.2+incompatible - github.com/golang/protobuf v1.5.2 + github.com/golang/protobuf v1.5.3 github.com/gomodule/redigo v1.8.9 github.com/google/go-cmp v0.5.9 github.com/google/uuid v1.3.0 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 - github.com/hashicorp/go-hclog v1.4.0 + github.com/hashicorp/go-hclog v1.5.0 github.com/hashicorp/go-plugin v1.4.9 github.com/jedib0t/go-pretty v4.3.0+incompatible github.com/juliangruber/go-intersect v1.1.0 @@ -40,10 +42,11 @@ require ( github.com/mileusna/useragent v1.2.1 github.com/minio/minio-go/v7 v7.0.45 github.com/mitchellh/mapstructure v1.5.0 - github.com/nats-io/nats-server/v2 v2.9.11 - github.com/nats-io/nats-streaming-server v0.25.2 + github.com/nats-io/nats-server/v2 v2.9.16 + github.com/nats-io/nats-streaming-server v0.25.4 + github.com/nats-io/nats.go v1.25.0 github.com/onsi/ginkgo v1.16.5 - github.com/onsi/gomega v1.27.2 + github.com/onsi/gomega v1.27.6 github.com/pkg/errors v0.9.1 github.com/pkg/xattr v0.4.5 github.com/prometheus/alertmanager v0.24.0 @@ -51,7 +54,7 @@ require ( github.com/rs/zerolog v1.28.0 github.com/sciencemesh/meshdirectory-web v1.0.4 github.com/sethvargo/go-password v0.2.0 - github.com/stretchr/testify v1.8.1 + github.com/stretchr/testify v1.8.2 github.com/studio-b12/gowebdav v0.0.0-20230203202212-3282f94193f2 github.com/thanhpk/randstr v1.0.4 github.com/tus/tusd v1.10.0 @@ -64,38 +67,35 @@ require ( go.opentelemetry.io/otel/sdk v1.11.2 go.opentelemetry.io/otel/trace v1.11.2 go.step.sm/crypto v0.23.2 - golang.org/x/crypto v0.5.0 + golang.org/x/crypto v0.8.0 golang.org/x/oauth2 v0.3.0 golang.org/x/sync v0.1.0 - golang.org/x/sys v0.5.0 - golang.org/x/term v0.5.0 - golang.org/x/text v0.7.0 + golang.org/x/sys v0.7.0 + golang.org/x/term v0.7.0 + golang.org/x/text v0.9.0 google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef google.golang.org/grpc v1.52.0 google.golang.org/protobuf v1.28.1 gotest.tools v2.2.0+incompatible ) -require github.com/disintegration/imaging v1.6.2 -require github.com/go-jose/go-jose/v3 v3.0.0 // indirect - require ( github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver v1.5.0 // indirect - github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878 // indirect + github.com/armon/go-metrics v0.4.1 // indirect github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect - github.com/coreos/go-oidc/v3 v3.5.0 github.com/davecgh/go-spew v1.1.1 // indirect github.com/dolthub/vitess v0.0.0-20221031111135-9aad77e7b39f // indirect github.com/dustin/go-humanize v1.0.0 // indirect github.com/fatih/color v1.13.0 // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect + github.com/go-jose/go-jose/v3 v3.0.0 // indirect github.com/go-kit/kit v0.10.0 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect @@ -113,14 +113,15 @@ require ( github.com/google/flatbuffers v2.0.6+incompatible // indirect github.com/hashicorp/go-immutable-radix v1.0.0 // indirect github.com/hashicorp/go-msgpack v1.1.5 // indirect + github.com/hashicorp/go-msgpack/v2 v2.1.0 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect - github.com/hashicorp/raft v1.3.11 // indirect + github.com/hashicorp/raft v1.4.0 // indirect github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb // indirect github.com/huandu/xstrings v1.3.3 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.15.11 // indirect + github.com/klauspost/compress v1.16.4 // indirect github.com/klauspost/cpuid/v2 v2.1.0 // indirect github.com/leodido/go-urn v1.2.1 // indirect github.com/lestrrat-go/strftime v1.0.4 // indirect @@ -139,11 +140,10 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/nats-io/jwt/v2 v2.3.0 // indirect - github.com/nats-io/nats.go v1.19.0 // indirect - github.com/nats-io/nkeys v0.3.0 // indirect + github.com/nats-io/jwt/v2 v2.4.1 // indirect + github.com/nats-io/nkeys v0.4.4 // indirect github.com/nats-io/nuid v1.0.1 // indirect - github.com/nats-io/stan.go v0.10.3 // indirect + github.com/nats-io/stan.go v0.10.4 // indirect github.com/nxadm/tail v1.4.8 // indirect github.com/oklog/run v1.1.0 // indirect github.com/oklog/ulid v1.3.1 // indirect @@ -155,7 +155,7 @@ require ( github.com/prometheus/client_golang v1.13.0 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.37.0 // indirect - github.com/prometheus/procfs v0.8.0 // indirect + github.com/prometheus/procfs v0.9.0 // indirect github.com/prometheus/statsd_exporter v0.22.7 // indirect github.com/rs/xid v1.4.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect @@ -164,14 +164,14 @@ require ( github.com/sirupsen/logrus v1.9.0 // indirect github.com/stretchr/objx v0.5.0 // indirect github.com/tidwall/pretty v1.2.0 // indirect - go.etcd.io/bbolt v1.3.6 // indirect + go.etcd.io/bbolt v1.3.7 // indirect go.mongodb.org/mongo-driver v1.8.3 // indirect go.opentelemetry.io/otel/metric v0.34.0 // indirect golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect - golang.org/x/mod v0.8.0 // indirect - golang.org/x/net v0.7.0 // indirect - golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect - golang.org/x/tools v0.6.0 // indirect + golang.org/x/mod v0.9.0 // indirect + golang.org/x/net v0.9.0 // indirect + golang.org/x/time v0.3.0 // indirect + golang.org/x/tools v0.7.0 // indirect google.golang.org/appengine v1.6.7 // indirect gopkg.in/ini.v1 v1.66.6 // indirect gopkg.in/src-d/go-errors.v1 v1.0.0 // indirect diff --git a/go.sum b/go.sum index 59cd3d6f40..93863191ea 100644 --- a/go.sum +++ b/go.sum @@ -156,7 +156,7 @@ github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= -github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= @@ -201,8 +201,8 @@ github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878 h1:EFSB7Zo9Eg91v7MJPVsifUysc/wPdN+NOnVe6bWbdBM= -github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg= +github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= +github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= @@ -450,13 +450,14 @@ github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVL github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw= github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= @@ -541,8 +542,9 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -640,15 +642,16 @@ github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-hclog v0.9.1/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= -github.com/hashicorp/go-hclog v1.1.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v1.4.0 h1:ctuWFGrhFha8BnnzxqeRGidlEcQkDyL5u8J8t5eA11I= -github.com/hashicorp/go-hclog v1.4.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= +github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-msgpack v1.1.5 h1:9byZdVjKTe5mce63pRVNP1L7UAmdHOTEMGehn6KvJWs= github.com/hashicorp/go-msgpack v1.1.5/go.mod h1:gWVc3sv/wbDmR3rQsj1CAktEZzoz1YNK9NfGLXJ69/4= +github.com/hashicorp/go-msgpack/v2 v2.1.0 h1:J2g2hMyjSefUPTnkLRU2MnsLLsPRB1n4Z/wJRN07GuA= +github.com/hashicorp/go-msgpack/v2 v2.1.0/go.mod h1:Tv81cKI2JmHZDjmzEmc1n+8h1DO5k+3pG6BPlNMQds0= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-plugin v1.4.9 h1:ESiK220/qE0aGxWdzKIvRH69iLiuN/PjoLTm69RoWtU= github.com/hashicorp/go-plugin v1.4.9/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s= @@ -673,8 +676,8 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/memberlist v0.3.1/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= -github.com/hashicorp/raft v1.3.11 h1:p3v6gf6l3S797NnK5av3HcczOC1T5CLoaRvg0g9ys4A= -github.com/hashicorp/raft v1.3.11/go.mod h1:J8naEwc6XaaCfts7+28whSeRvCqTd6e20BlCU3LtEO4= +github.com/hashicorp/raft v1.4.0 h1:tn28S/AWv0BtRQgwZv/1NELu8sCvI0FixqL8C8MYKeY= +github.com/hashicorp/raft v1.4.0/go.mod h1:nz64BIjXphDLATfKGG5RzHtNUPioLeKFsXEm88yTVew= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= @@ -712,6 +715,7 @@ github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -733,8 +737,8 @@ github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQL github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= -github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/klauspost/compress v1.16.4 h1:91KN02FnsOYhuunwU4ssRe8lc2JosWmizWa91B5v1PU= +github.com/klauspost/compress v1.16.4/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8rF0= @@ -767,8 +771,7 @@ github.com/lestrrat-go/strftime v1.0.4 h1:T1Rb9EPkAhgxKqbcMIPguPq8glqXTA1koF8n9B github.com/lestrrat-go/strftime v1.0.4/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= -github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.8 h1:3fdt97i/cwSU83+E0hZTC/Xpc9mTZxc6UWSCRcSbxiE= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/linode/linodego v0.25.3/go.mod h1:GSBKPpjoQfxEfryoCRcgkuUOCuVtGHWhzI8OMdycNTE= @@ -877,27 +880,26 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= -github.com/nats-io/jwt/v2 v2.3.0 h1:z2mA1a7tIf5ShggOFlR1oBPgd6hGqcDYsISxZByUzdI= -github.com/nats-io/jwt/v2 v2.3.0/go.mod h1:0tqz9Hlu6bCBFLWAASKhE5vUA4c24L9KPUUgvwumE/k= +github.com/nats-io/jwt/v2 v2.4.1 h1:Y35W1dgbbz2SQUYDPCaclXcuqleVmpbRa7646Jf2EX4= +github.com/nats-io/jwt/v2 v2.4.1/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+vu/+ibI= github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= -github.com/nats-io/nats-server/v2 v2.9.3/go.mod h1:4sq8wvrpbvSzL1n3ZfEYnH4qeUuIl5W990j3kw13rRk= -github.com/nats-io/nats-server/v2 v2.9.11 h1:4y5SwWvWI59V5mcqtuoqKq6L9NDUydOP3Ekwuwl8cZI= -github.com/nats-io/nats-server/v2 v2.9.11/go.mod h1:b0oVuxSlkvS3ZjMkncFeACGyZohbO4XhSqW1Lt7iRRY= -github.com/nats-io/nats-streaming-server v0.25.2 h1:cWjytvYksYPgnXnSocqnRWVrSgLclusnPGBNHQR4SqI= -github.com/nats-io/nats-streaming-server v0.25.2/go.mod h1:bRbgx+iCG6EZEXpqVMroRDuCGwR1iW+ta84aEGBaMhI= +github.com/nats-io/nats-server/v2 v2.9.16 h1:SuNe6AyCcVy0g5326wtyU8TdqYmcPqzTjhkHojAjprc= +github.com/nats-io/nats-server/v2 v2.9.16/go.mod h1:z1cc5Q+kqJkz9mLUdlcSsdYnId4pyImHjNgoh6zxSC0= +github.com/nats-io/nats-streaming-server v0.25.4 h1:aaMmKcEMXLvviM9y73BmPquTgQ/fg+0EmFLzFSbXUqQ= +github.com/nats-io/nats-streaming-server v0.25.4/go.mod h1:zrOyvkrVEz/y72m1nAVb149k/CAumm3H9ze682Lg9uE= github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= -github.com/nats-io/nats.go v1.16.0/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w= -github.com/nats-io/nats.go v1.17.0/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w= -github.com/nats-io/nats.go v1.19.0 h1:H6j8aBnTQFoVrTGB6Xjd903UMdE7jz6DS4YkmAqgZ9Q= -github.com/nats-io/nats.go v1.19.0/go.mod h1:tLqubohF7t4z3du1QDPYJIQQyhb4wl6DhjxEajSI7UA= +github.com/nats-io/nats.go v1.22.1/go.mod h1:tLqubohF7t4z3du1QDPYJIQQyhb4wl6DhjxEajSI7UA= +github.com/nats-io/nats.go v1.25.0 h1:t5/wCPGciR7X3Mu8QOi4jiJaXaWM8qtkLu4lzGZvYHE= +github.com/nats-io/nats.go v1.25.0/go.mod h1:D2WALIhz7V8M0pH8Scx8JZXlg6Oqz5VG+nQkK8nJdvg= github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= -github.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8= github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4= +github.com/nats-io/nkeys v0.4.4 h1:xvBJ8d69TznjcQl9t6//Q5xXuVhyYiSos6RPtvQNTwA= +github.com/nats-io/nkeys v0.4.4/go.mod h1:XUkxdLPTufzlihbamfzQ7mw/VGx6ObUs+0bN5sNvt64= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/nats-io/stan.go v0.10.3 h1:8DOyQJ0+nza3zSVJZ19/cpikkrWA4rSKB3YvckIGOTI= -github.com/nats-io/stan.go v0.10.3/go.mod h1:Cgf5zk6kKpOCqqUIJeuBz6ZDz9osT791VhS6m28sSQQ= +github.com/nats-io/stan.go v0.10.4 h1:19GS/eD1SeQJaVkeM9EkvEYattnvnWrZ3wkSWSw4uXw= +github.com/nats-io/stan.go v0.10.4/go.mod h1:3XJXH8GagrGqajoO/9+HgPyKV5MWsv7S5ccdda+pc6k= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nrdcg/auroradns v1.0.1/go.mod h1:y4pc0i9QXYlFCWrhWrUSIETnZgrf4KuwjDIWmmXo3JI= @@ -924,12 +926,12 @@ github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.8.4 h1:gf5mIQ8cLFieruNLAdgijHF1PYfLphKm2dxxcUtcqK0= +github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.27.2 h1:SKU0CXeKE/WVgIV1T61kSa3+IRE8Ekrv9rdXDwwTqnY= -github.com/onsi/gomega v1.27.2/go.mod h1:5mR3phAHpkAVIDkHEUBY6HGVsU+cpcEscrGPB4oPlZI= +github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= +github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= @@ -978,16 +980,15 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/pquerna/otp v1.3.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= -github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prometheus/alertmanager v0.24.0 h1:HBWR3lk4uy3ys+naDZthDdV7yEsxpaNeZuUS+hJgrOw= github.com/prometheus/alertmanager v0.24.0/go.mod h1:r6fy/D7FRuZh5YbnX6J3MBY0eI4Pb5yPYS7/bPSXXqI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= @@ -1002,12 +1003,12 @@ github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6T github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.29.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= @@ -1019,7 +1020,6 @@ github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57J github.com/prometheus/exporter-toolkit v0.7.1/go.mod h1:ZUBIj498ePooX9t/2xtDjeQYwvRpiPP2lh5u4iblj2g= github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= @@ -1028,8 +1028,9 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/prometheus/statsd_exporter v0.22.7 h1:7Pji/i2GuhK6Lu7DHrtTkFmNBCudCPT1pX2CziuyQR0= github.com/prometheus/statsd_exporter v0.22.7/go.mod h1:N/TevpjkIh9ccs6nuzY3jQn9dFqnUakOjnEuMPJJJnI= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= @@ -1121,8 +1122,9 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/studio-b12/gowebdav v0.0.0-20230203202212-3282f94193f2 h1:VsBj3UD2xyAOu7kJw6O/2jjG2UXLFoBzihqDU9Ofg9M= github.com/studio-b12/gowebdav v0.0.0-20230203202212-3282f94193f2/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE= github.com/stvp/go-udp-testing v0.0.0-20201019212854-469649b16807/go.mod h1:7jxmlfBCDBXRzr0eAQJ48XC1hBu1np4CS5+cHEYfwpc= @@ -1178,8 +1180,8 @@ go-micro.dev/v4 v4.3.1-0.20211108085239-0c2041e43908 h1:4ori3xawGl2unFIOQPEgUuHd go-micro.dev/v4 v4.3.1-0.20211108085239-0c2041e43908/go.mod h1:tw47Xfg2YywfPUnglZgXQsSf7p0ST6mQL3v0JooGmSY= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= -go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= +go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= +go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.m3o.com v0.1.0/go.mod h1:p8FdLqZH3R9a0y04qiMNT+clw69d3SxyQPFzCNbDRtk= go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg= @@ -1216,7 +1218,6 @@ go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/automaxprocs v1.5.1/go.mod h1:BF4eumQw0P9GtnuxxovUd06vwm1o18oMzFtK66vU6XU= go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= @@ -1254,11 +1255,9 @@ golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5 golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1303,8 +1302,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1372,8 +1371,9 @@ golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfS golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1482,7 +1482,6 @@ golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201110211018-35f3e6cf4a65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1535,20 +1534,21 @@ golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201113234701-d7a72108b828/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= -golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1560,8 +1560,9 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1569,8 +1570,8 @@ golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220922220347-f3bd1da661af h1:Yx9k8YCG3dvF87UAn2tu2HQLf2dt/eR1bXxpLMWeH+Y= -golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1647,8 +1648,8 @@ golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/grpc/services/datatx/datatx.go b/internal/grpc/services/datatx/datatx.go index b3ac0b4398..b9f5011a3d 100644 --- a/internal/grpc/services/datatx/datatx.go +++ b/internal/grpc/services/datatx/datatx.go @@ -20,15 +20,13 @@ package datatx import ( "context" - "encoding/json" - "io" - "os" - "sync" ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" datatx "github.com/cs3org/go-cs3apis/cs3/tx/v1beta1" + ctxpkg "github.com/cs3org/reva/pkg/ctx" txdriver "github.com/cs3org/reva/pkg/datatx" txregistry "github.com/cs3org/reva/pkg/datatx/manager/registry" + repoRegistry "github.com/cs3org/reva/pkg/datatx/repository/registry" "github.com/cs3org/reva/pkg/errtypes" "github.com/cs3org/reva/pkg/rgrpc" "github.com/cs3org/reva/pkg/rgrpc/status" @@ -43,44 +41,23 @@ func init() { type config struct { // transfer driver - TxDriver string `mapstructure:"txdriver"` - TxDrivers map[string]map[string]interface{} `mapstructure:"txdrivers"` - // storage driver to persist share/transfer relation - StorageDriver string `mapstructure:"storage_driver"` - StorageDrivers map[string]map[string]interface{} `mapstructure:"storage_drivers"` - TxSharesFile string `mapstructure:"tx_shares_file"` - RemoveOnCancel bool `mapstructure:"remove_on_cancel"` + TxDriver string `mapstructure:"txdriver"` + TxDrivers map[string]map[string]interface{} `mapstructure:"txdrivers"` + StorageDriver string `mapstructure:"storagedriver"` + StorageDrivers map[string]map[string]interface{} `mapstructure:"storagedrivers"` + RemoveOnCancel bool `mapstructure:"remove_transfer_on_cancel"` } type service struct { conf *config txManager txdriver.Manager - txShareDriver *txShareDriver -} - -type txShareDriver struct { - sync.Mutex // concurrent access to the file - model *txShareModel -} -type txShareModel struct { - File string - TxShares map[string]*txShare `json:"shares"` -} - -type txShare struct { - TxID string - SrcTargetURI string - DestTargetURI string - ShareID string + storageDriver txdriver.Repository } func (c *config) init() { if c.TxDriver == "" { c.TxDriver = "rclone" } - if c.TxSharesFile == "" { - c.TxSharesFile = "/var/tmp/reva/datatx-shares.json" - } } func (s *service) Register(ss *grpc.Server) { @@ -94,6 +71,13 @@ func getDatatxManager(c *config) (txdriver.Manager, error) { return nil, errtypes.NotFound("datatx service: driver not found: " + c.TxDriver) } +func getStorageManager(c *config) (txdriver.Repository, error) { + if f, ok := repoRegistry.NewFuncs[c.StorageDriver]; ok { + return f(c.StorageDrivers[c.StorageDriver]) + } + return nil, errtypes.NotFound("datatx service: driver not found: " + c.StorageDriver) +} + func parseConfig(m map[string]interface{}) (*config, error) { c := &config{} if err := mapstructure.Decode(m, c); err != nil { @@ -116,19 +100,15 @@ func New(m map[string]interface{}, ss *grpc.Server) (rgrpc.Service, error) { return nil, err } - model, err := loadOrCreate(c.TxSharesFile) + storageDriver, err := getStorageManager(c) if err != nil { - err = errors.Wrap(err, "datatx service: error loading the file containing the transfer shares") return nil, err } - txShareDriver := &txShareDriver{ - model: model, - } service := &service{ conf: c, txManager: txManager, - txShareDriver: txShareDriver, + storageDriver: storageDriver, } return service, nil @@ -147,18 +127,16 @@ func (s *service) CreateTransfer(ctx context.Context, req *datatx.CreateTransfer // we always save the transfer regardless of start transfer outcome // only then, if starting fails, can we try to restart it - txShare := &txShare{ + userID := ctxpkg.ContextMustGetUser(ctx).GetId() + transfer := &txdriver.Transfer{ TxID: txInfo.GetId().OpaqueId, SrcTargetURI: req.SrcTargetUri, DestTargetURI: req.DestTargetUri, ShareID: req.GetShareId().OpaqueId, + UserID: userID, } - s.txShareDriver.Lock() - defer s.txShareDriver.Unlock() - - s.txShareDriver.model.TxShares[txInfo.GetId().OpaqueId] = txShare - if err := s.txShareDriver.model.saveTxShare(); err != nil { - err = errors.Wrap(err, "datatx service: error saving transfer share: "+datatx.Status_STATUS_INVALID.String()) + if err := s.storageDriver.StoreTransfer(transfer); err != nil { + err = errors.Wrap(err, "datatx service: error NEW saving transfer share: "+datatx.Status_STATUS_INVALID.String()) return &datatx.CreateTransferResponse{ Status: status.NewInvalid(ctx, "error creating transfer"), }, err @@ -180,8 +158,8 @@ func (s *service) CreateTransfer(ctx context.Context, req *datatx.CreateTransfer } func (s *service) GetTransferStatus(ctx context.Context, req *datatx.GetTransferStatusRequest) (*datatx.GetTransferStatusResponse, error) { - txShare, ok := s.txShareDriver.model.TxShares[req.GetTxId().GetOpaqueId()] - if !ok { + transfer, err := s.storageDriver.GetTransfer(req.TxId.OpaqueId) + if err != nil { return nil, errtypes.InternalError("datatx service: transfer not found") } @@ -194,7 +172,7 @@ func (s *service) GetTransferStatus(ctx context.Context, req *datatx.GetTransfer }, err } - txInfo.ShareId = &ocm.ShareId{OpaqueId: txShare.ShareID} + txInfo.ShareId = &ocm.ShareId{OpaqueId: transfer.ShareID} return &datatx.GetTransferStatusResponse{ Status: status.NewOK(ctx), @@ -203,15 +181,14 @@ func (s *service) GetTransferStatus(ctx context.Context, req *datatx.GetTransfer } func (s *service) CancelTransfer(ctx context.Context, req *datatx.CancelTransferRequest) (*datatx.CancelTransferResponse, error) { - txShare, ok := s.txShareDriver.model.TxShares[req.GetTxId().OpaqueId] - if !ok { + transfer, err := s.storageDriver.GetTransfer(req.TxId.OpaqueId) + if err != nil { return nil, errtypes.InternalError("datatx service: transfer not found") } transferRemovedMessage := "" if s.conf.RemoveOnCancel { - delete(s.txShareDriver.model.TxShares, req.TxId.GetOpaqueId()) - if err := s.txShareDriver.model.saveTxShare(); err != nil { + if err := s.storageDriver.DeleteTransfer(transfer); err != nil { err = errors.Wrap(err, "datatx service: error deleting transfer: "+datatx.Status_STATUS_INVALID.String()) return &datatx.CancelTransferResponse{ Status: status.NewInvalid(ctx, "error cancelling transfer"), @@ -222,7 +199,7 @@ func (s *service) CancelTransfer(ctx context.Context, req *datatx.CancelTransfer txInfo, err := s.txManager.CancelTransfer(ctx, req.GetTxId().OpaqueId) if err != nil { - txInfo.ShareId = &ocm.ShareId{OpaqueId: txShare.ShareID} + txInfo.ShareId = &ocm.ShareId{OpaqueId: transfer.ShareID} err = errors.Wrapf(err, "(%v) datatx service: error cancelling transfer", transferRemovedMessage) return &datatx.CancelTransferResponse{ Status: status.NewInternal(ctx, err, "error cancelling transfer"), @@ -230,7 +207,7 @@ func (s *service) CancelTransfer(ctx context.Context, req *datatx.CancelTransfer }, err } - txInfo.ShareId = &ocm.ShareId{OpaqueId: txShare.ShareID} + txInfo.ShareId = &ocm.ShareId{OpaqueId: transfer.ShareID} return &datatx.CancelTransferResponse{ Status: status.NewOK(ctx), @@ -239,26 +216,23 @@ func (s *service) CancelTransfer(ctx context.Context, req *datatx.CancelTransfer } func (s *service) ListTransfers(ctx context.Context, req *datatx.ListTransfersRequest) (*datatx.ListTransfersResponse, error) { - filters := req.Filters - var txInfos []*datatx.TxInfo - for _, txShare := range s.txShareDriver.model.TxShares { - if len(filters) == 0 { - txInfos = append(txInfos, &datatx.TxInfo{ - Id: &datatx.TxId{OpaqueId: txShare.TxID}, - ShareId: &ocm.ShareId{OpaqueId: txShare.ShareID}, - }) - } else { - for _, f := range filters { - if f.Type == datatx.ListTransfersRequest_Filter_TYPE_SHARE_ID { - if f.GetShareId().GetOpaqueId() == txShare.ShareID { - txInfos = append(txInfos, &datatx.TxInfo{ - Id: &datatx.TxId{OpaqueId: txShare.TxID}, - ShareId: &ocm.ShareId{OpaqueId: txShare.ShareID}, - }) - } - } - } - } + userID := ctxpkg.ContextMustGetUser(ctx).GetId() + transfers, err := s.storageDriver.ListTransfers(req.Filters, userID) + if err != nil { + err = errors.Wrap(err, "datatx service: error listing transfers") + var txInfos []*datatx.TxInfo + return &datatx.ListTransfersResponse{ + Status: status.NewInternal(ctx, err, "error listing transfers"), + Transfers: txInfos, + }, err + } + + txInfos := []*datatx.TxInfo{} + for _, transfer := range transfers { + txInfos = append(txInfos, &datatx.TxInfo{ + Id: &datatx.TxId{OpaqueId: transfer.TxID}, + ShareId: &ocm.ShareId{OpaqueId: transfer.ShareID}, + }) } return &datatx.ListTransfersResponse{ @@ -268,8 +242,8 @@ func (s *service) ListTransfers(ctx context.Context, req *datatx.ListTransfersRe } func (s *service) RetryTransfer(ctx context.Context, req *datatx.RetryTransferRequest) (*datatx.RetryTransferResponse, error) { - txShare, ok := s.txShareDriver.model.TxShares[req.GetTxId().GetOpaqueId()] - if !ok { + transfer, err := s.storageDriver.GetTransfer(req.TxId.OpaqueId) + if err != nil { return nil, errtypes.InternalError("datatx service: transfer not found") } @@ -282,61 +256,10 @@ func (s *service) RetryTransfer(ctx context.Context, req *datatx.RetryTransferRe }, err } - txInfo.ShareId = &ocm.ShareId{OpaqueId: txShare.ShareID} + txInfo.ShareId = &ocm.ShareId{OpaqueId: transfer.ShareID} return &datatx.RetryTransferResponse{ Status: status.NewOK(ctx), TxInfo: txInfo, }, nil } - -func loadOrCreate(file string) (*txShareModel, error) { - _, err := os.Stat(file) - if os.IsNotExist(err) { - if err := os.WriteFile(file, []byte("{}"), 0700); err != nil { - err = errors.Wrap(err, "datatx service: error creating the transfer shares storage file: "+file) - return nil, err - } - } - - fd, err := os.OpenFile(file, os.O_CREATE, 0644) - if err != nil { - err = errors.Wrap(err, "datatx service: error opening the transfer shares storage file: "+file) - return nil, err - } - defer fd.Close() - - data, err := io.ReadAll(fd) - if err != nil { - err = errors.Wrap(err, "datatx service: error reading the data") - return nil, err - } - - model := &txShareModel{} - if err := json.Unmarshal(data, model); err != nil { - err = errors.Wrap(err, "datatx service: error decoding transfer shares data to json") - return nil, err - } - - if model.TxShares == nil { - model.TxShares = make(map[string]*txShare) - } - - model.File = file - return model, nil -} - -func (m *txShareModel) saveTxShare() error { - data, err := json.Marshal(m) - if err != nil { - err = errors.Wrap(err, "datatx service: error encoding transfer share data to json") - return err - } - - if err := os.WriteFile(m.File, data, 0644); err != nil { - err = errors.Wrap(err, "datatx service: error writing transfer share data to file: "+m.File) - return err - } - - return nil -} diff --git a/internal/grpc/services/gateway/ocmshareprovider.go b/internal/grpc/services/gateway/ocmshareprovider.go index 01f96f9b75..59957712ee 100644 --- a/internal/grpc/services/gateway/ocmshareprovider.go +++ b/internal/grpc/services/gateway/ocmshareprovider.go @@ -170,7 +170,7 @@ func (s *svc) UpdateReceivedOCMShare(ctx context.Context, req *ocm.UpdateReceive }, nil } - // retrieve the persisted received share + // retrieve the current received share getShareReq := &ocm.GetReceivedOCMShareRequest{ Ref: &ocm.ShareReference{ Spec: &ocm.ShareReference_Id{ @@ -234,7 +234,16 @@ func (s *svc) UpdateReceivedOCMShare(ctx context.Context, req *ocm.UpdateReceive } } // handle transfer in case it has not already been accepted - if s.isTransferShare(share) && req.GetShare().State == ocm.ShareState_SHARE_STATE_ACCEPTED && share.State != ocm.ShareState_SHARE_STATE_ACCEPTED { + if s.isTransferShare(share) && req.GetShare().State == ocm.ShareState_SHARE_STATE_ACCEPTED { + if share.State == ocm.ShareState_SHARE_STATE_ACCEPTED { + log.Err(err).Msg("gateway: error calling UpdateReceivedShare, share already accepted.") + return &ocm.UpdateReceivedOCMShareResponse{ + Status: &rpc.Status{ + Code: rpc.Code_CODE_FAILED_PRECONDITION, + Message: "Share already accepted.", + }, + }, err + } // get provided destination path transferDestinationPath, err := s.getTransferDestinationPath(ctx, req) if err != nil { diff --git a/internal/grpc/services/ocmshareprovider/ocmshareprovider.go b/internal/grpc/services/ocmshareprovider/ocmshareprovider.go index 37b0307545..88e11393d9 100644 --- a/internal/grpc/services/ocmshareprovider/ocmshareprovider.go +++ b/internal/grpc/services/ocmshareprovider/ocmshareprovider.go @@ -44,6 +44,7 @@ import ( "github.com/cs3org/reva/pkg/rgrpc/status" "github.com/cs3org/reva/pkg/rgrpc/todo/pool" "github.com/cs3org/reva/pkg/sharedconf" + "github.com/cs3org/reva/pkg/storage/utils/walker" "github.com/cs3org/reva/pkg/utils" "github.com/mitchellh/mapstructure" "github.com/pkg/errors" @@ -71,6 +72,7 @@ type service struct { client *client.OCMClient gateway gateway.GatewayAPIClient webappTmpl *template.Template + walker walker.Walker } func (c *config) init() { @@ -134,6 +136,7 @@ func New(m map[string]interface{}, ss *grpc.Server) (rgrpc.Service, error) { if err != nil { return nil, err } + walker := walker.NewWalker(gateway) service := &service{ conf: c, @@ -141,6 +144,7 @@ func New(m map[string]interface{}, ss *grpc.Server) (rgrpc.Service, error) { client: client, gateway: gateway, webappTmpl: tpl, + walker: walker, } return service, nil @@ -210,13 +214,38 @@ func (s *service) getWebappProtocol(share *ocm.Share) *ocmd.Webapp { } func (s *service) getDataTransferProtocol(ctx context.Context, share *ocm.Share) *ocmd.Datatx { - // TODO discover the size + var size uint64 + // get the path of the share + statRes, err := s.gateway.Stat(ctx, &providerpb.StatRequest{ + Ref: &providerpb.Reference{ + ResourceId: share.ResourceId, + }, + }) + if err != nil { + panic(err) + } + + path := statRes.GetInfo().Path + err = s.walk(ctx, path, func(path string, info *providerpb.ResourceInfo, err error) error { + if info.Type == providerpb.ResourceType_RESOURCE_TYPE_FILE { + size += info.Size + } + return nil + }) + if err != nil { + panic(err) + } return &ocmd.Datatx{ SourceURI: s.webdavURL(ctx, share), - Size: 0, + Size: size, } } +// walk traverses the path recursively to discover all resources in the tree. +func (s *service) walk(ctx context.Context, path string, fn walker.WalkFunc) error { + return s.walker.Walk(ctx, path, fn) +} + func (s *service) getProtocols(ctx context.Context, share *ocm.Share) ocmd.Protocols { var p ocmd.Protocols for _, m := range share.AccessMethods { diff --git a/internal/grpc/services/publicshareprovider/publicshareprovider.go b/internal/grpc/services/publicshareprovider/publicshareprovider.go index bdb2d003a0..87ae6765a3 100644 --- a/internal/grpc/services/publicshareprovider/publicshareprovider.go +++ b/internal/grpc/services/publicshareprovider/publicshareprovider.go @@ -145,7 +145,7 @@ func (s *service) CreatePublicShare(ctx context.Context, req *link.CreatePublicS log.Error().Msg("error getting user from context") } - share, err := s.sm.CreatePublicShare(ctx, u, req.ResourceInfo, req.Grant, req.Description, req.Internal) + share, err := s.sm.CreatePublicShare(ctx, u, req.ResourceInfo, req.Grant, req.Description, req.Internal, req.NotifyUploads, req.NotifyUploadsExtraRecipients) switch err.(type) { case nil: return &link.CreatePublicShareResponse{ diff --git a/internal/http/services/loader/loader.go b/internal/http/services/loader/loader.go index ed0ec11c40..38874241af 100644 --- a/internal/http/services/loader/loader.go +++ b/internal/http/services/loader/loader.go @@ -25,7 +25,6 @@ import ( _ "github.com/cs3org/reva/internal/http/services/datagateway" _ "github.com/cs3org/reva/internal/http/services/dataprovider" _ "github.com/cs3org/reva/internal/http/services/helloworld" - _ "github.com/cs3org/reva/internal/http/services/mailer" _ "github.com/cs3org/reva/internal/http/services/mentix" _ "github.com/cs3org/reva/internal/http/services/meshdirectory" _ "github.com/cs3org/reva/internal/http/services/metrics" diff --git a/internal/http/services/mailer/mailer.go b/internal/http/services/mailer/mailer.go deleted file mode 100644 index 6d9c23c9c5..0000000000 --- a/internal/http/services/mailer/mailer.go +++ /dev/null @@ -1,378 +0,0 @@ -// Copyright 2018-2023 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 mailer - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/smtp" - "os" - "path/filepath" - "strings" - "text/template" - - gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" - group "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1" - user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" - rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" - collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" - provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" - ctxpkg "github.com/cs3org/reva/pkg/ctx" - "github.com/cs3org/reva/pkg/errtypes" - "github.com/cs3org/reva/pkg/rgrpc/todo/pool" - "github.com/cs3org/reva/pkg/rhttp/global" - "github.com/cs3org/reva/pkg/sharedconf" - "github.com/mitchellh/mapstructure" - "github.com/rs/zerolog" -) - -func init() { - global.Register("mailer", New) -} - -type config struct { - SMTPAddress string `mapstructure:"smtp_server" docs:";The hostname and port of the SMTP server."` - SenderLogin string `mapstructure:"sender_login" docs:";The email to be used to send mails."` - SenderPassword string `mapstructure:"sender_password" docs:";The sender's password."` - DisableAuth bool `mapstructure:"disable_auth" docs:"false;Whether to disable SMTP auth."` - Prefix string `mapstructure:"prefix"` - BodyTemplatePath string `mapstructure:"body_template_path"` - SubjectTemplate string `mapstructure:"subject_template"` - GatewaySVC string `mapstructure:"gateway_svc"` -} - -type svc struct { - conf *config - client gateway.GatewayAPIClient - tplBody *template.Template - tplSubj *template.Template -} - -// New creates a new mailer service. -func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) { - conf := &config{} - if err := mapstructure.Decode(m, conf); err != nil { - return nil, err - } - - conf.init() - - client, err := pool.GetGatewayServiceClient(pool.Endpoint(conf.GatewaySVC)) - if err != nil { - return nil, err - } - - s := &svc{ - conf: conf, - client: client, - } - - if err = s.initBodyTemplate(); err != nil { - return nil, err - } - if err = s.initSubjectTemplate(); err != nil { - return nil, err - } - - return s, nil -} - -func (s *svc) Close() error { - return nil -} - -func (s *svc) initBodyTemplate() error { - f, err := os.Open(s.conf.BodyTemplatePath) - if err != nil { - return err - } - defer f.Close() - - data, err := io.ReadAll(f) - if err != nil { - return err - } - - tpl, err := template.New("tpl_body").Parse(string(data)) - if err != nil { - return err - } - - s.tplBody = tpl - return nil -} - -func (s *svc) initSubjectTemplate() error { - tpl, err := template.New("tpl_subj").Parse(s.conf.SubjectTemplate) - if err != nil { - return err - } - s.tplSubj = tpl - return nil -} - -func (c *config) init() { - if c.Prefix == "" { - c.Prefix = "mailer" - } - - if c.SubjectTemplate == "" { - c.SubjectTemplate = "{{.OwnerName}} ({{.OwnerUsername}}) shared {{if .IsDir}}folder{{else}}file{{end}} '{{.Filename}}' with you" - } - - c.GatewaySVC = sharedconf.GetGatewaySVC(c.GatewaySVC) -} - -func (s *svc) Prefix() string { - return s.conf.Prefix -} - -func (s *svc) Unprotected() []string { - return nil -} - -type out struct { - Recipients []string `json:"recipients"` -} - -func getIDsFromRequest(r *http.Request) ([]string, error) { - if err := r.ParseForm(); err != nil { - return nil, err - } - - idsSet := make(map[string]struct{}) - - for _, id := range r.Form["id"] { - if _, ok := idsSet[id]; ok { - continue - } - idsSet[id] = struct{}{} - } - - ids := make([]string, 0, len(idsSet)) - for id := range idsSet { - ids = append(ids, id) - } - - return ids, nil -} - -func (s *svc) Handler() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) - return - } - - ctx := r.Context() - - ids, err := getIDsFromRequest(r) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - } - - if len(ids) == 0 { - http.Error(w, "share id not provided", http.StatusBadRequest) - return - } - - var recipients []string - for _, id := range ids { - recipient, err := s.sendMailForShare(ctx, id) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - recipients = append(recipients, recipient) - } - - w.WriteHeader(http.StatusOK) - w.Header().Add("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(out{Recipients: recipients}) - }) -} - -type shareInfo struct { - RecipientEmail string - RecipientUsername string - OwnerEmail string - OwnerName string - OwnerUsername string - ShareType string - Filename string - Path string - IsDir bool - ShareID string -} - -func (s *svc) getAuth() smtp.Auth { - if s.conf.DisableAuth { - return nil - } - return smtp.PlainAuth("", s.conf.SenderLogin, s.conf.SenderPassword, strings.SplitN(s.conf.SMTPAddress, ":", 2)[0]) -} - -func (s *svc) sendMailForShare(ctx context.Context, id string) (string, error) { - share, err := s.getShareInfoByID(ctx, id) - if err != nil { - return "", err - } - - msg, err := s.generateMsg(share.OwnerEmail, share.RecipientEmail, share) - if err != nil { - return "", err - } - - return share.RecipientEmail, smtp.SendMail(s.conf.SMTPAddress, s.getAuth(), share.OwnerEmail, []string{share.RecipientEmail}, msg) -} - -func (s *svc) generateMsg(from, to string, share *shareInfo) ([]byte, error) { - subj, err := s.generateEmailSubject(share) - if err != nil { - return nil, err - } - - body, err := s.generateEmailBody(share) - if err != nil { - return nil, err - } - - msg := fmt.Sprintf("From: %s\r\n"+ - "To: %s\r\n"+ - "Subject: %s\r\n\r\n%s\r\n", from, to, subj, body) - return []byte(msg), nil -} - -func (s *svc) getShareInfoByID(ctx context.Context, id string) (*shareInfo, error) { - user, ok := ctxpkg.ContextGetUser(ctx) - if !ok { - return nil, errtypes.UserRequired("user not in context") - } - - shareRes, err := s.client.GetShare(ctx, &collaboration.GetShareRequest{ - Ref: &collaboration.ShareReference{ - Spec: &collaboration.ShareReference_Id{ - Id: &collaboration.ShareId{ - OpaqueId: id, - }, - }, - }, - }) - - switch { - case err != nil: - return nil, err - case shareRes.Status.Code == rpc.Code_CODE_NOT_FOUND: - return nil, errtypes.NotFound(fmt.Sprintf("share %s not found", id)) - case shareRes.Status.Code != rpc.Code_CODE_OK: - return nil, errtypes.InternalError(shareRes.Status.Message) - } - - share := shareRes.Share - statRes, err := s.client.Stat(ctx, &provider.StatRequest{ - Ref: &provider.Reference{ - ResourceId: share.ResourceId, - }, - }) - - switch { - case err != nil: - return nil, err - case statRes.Status.Code == rpc.Code_CODE_NOT_FOUND: - return nil, errtypes.NotFound("reference not found") - case statRes.Status.Code != rpc.Code_CODE_OK: - return nil, errtypes.InternalError(statRes.Status.Message) - } - - file := statRes.Info - - info := &shareInfo{} - switch g := share.Grantee.Id.(type) { - case *provider.Grantee_UserId: - grantee, err := s.getUser(ctx, g.UserId) - if err != nil { - return nil, err - } - info.RecipientEmail = grantee.Mail - info.RecipientUsername = grantee.Username - info.ShareType = "user" - case *provider.Grantee_GroupId: - grantee, err := s.getGroup(ctx, g.GroupId) - if err != nil { - return nil, err - } - info.RecipientEmail = grantee.Mail - info.RecipientUsername = grantee.GroupName - info.ShareType = "group" - } - - info.OwnerEmail = user.Mail - info.OwnerName = user.DisplayName - info.OwnerUsername = user.Username - - info.Path = file.Path - info.Filename = filepath.Base(file.Path) - if file.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { - info.IsDir = true - } else { - info.IsDir = false - } - - info.ShareID = id - - return info, nil -} - -func (s *svc) getUser(ctx context.Context, userID *user.UserId) (*user.User, error) { - res, err := s.client.GetUser(ctx, &user.GetUserRequest{ - UserId: userID, - }) - if err != nil { - return nil, err - } - - return res.User, nil -} - -func (s *svc) getGroup(ctx context.Context, groupID *group.GroupId) (*group.Group, error) { - res, err := s.client.GetGroup(ctx, &group.GetGroupRequest{ - GroupId: groupID, - }) - if err != nil { - return nil, err - } - - return res.Group, nil -} - -func (s *svc) generateEmailSubject(share *shareInfo) (string, error) { - var buf bytes.Buffer - err := s.tplSubj.Execute(&buf, share) - return buf.String(), err -} - -func (s *svc) generateEmailBody(share *shareInfo) (string, error) { - var buf bytes.Buffer - err := s.tplBody.Execute(&buf, share) - return buf.String(), err -} diff --git a/internal/http/services/ocmd/ocm.go b/internal/http/services/ocmd/ocm.go index ab178796f6..fa92fb4121 100644 --- a/internal/http/services/ocmd/ocm.go +++ b/internal/http/services/ocmd/ocm.go @@ -75,19 +75,16 @@ func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) func (s *svc) routerInit() error { sharesHandler := new(sharesHandler) - notificationsHandler := new(notificationsHandler) invitesHandler := new(invitesHandler) if err := sharesHandler.init(s.Conf); err != nil { return err } - notificationsHandler.init(s.Conf) if err := invitesHandler.init(s.Conf); err != nil { return err } s.router.Post("/shares", sharesHandler.CreateShare) - s.router.Post("/notifications", notificationsHandler.SendNotification) s.router.Post("/invite-accepted", invitesHandler.AcceptInvite) return nil } diff --git a/internal/http/services/owncloud/ocdav/ocdav.go b/internal/http/services/owncloud/ocdav/ocdav.go index 518812da46..b83b027e6b 100644 --- a/internal/http/services/owncloud/ocdav/ocdav.go +++ b/internal/http/services/owncloud/ocdav/ocdav.go @@ -33,6 +33,7 @@ import ( "github.com/cs3org/reva/pkg/appctx" ctxpkg "github.com/cs3org/reva/pkg/ctx" "github.com/cs3org/reva/pkg/errtypes" + "github.com/cs3org/reva/pkg/notification/notificationhelper" "github.com/cs3org/reva/pkg/rgrpc/todo/pool" "github.com/cs3org/reva/pkg/rhttp" "github.com/cs3org/reva/pkg/rhttp/global" @@ -118,6 +119,7 @@ type Config struct { FavoriteStorageDriver string `mapstructure:"favorite_storage_driver"` FavoriteStorageDrivers map[string]map[string]interface{} `mapstructure:"favorite_storage_drivers"` PublicLinkDownload *ConfigPublicLinkDownload `mapstructure:"publiclink_download"` + Notifications map[string]interface{} `mapstructure:"notifications" docs:"Settingsg for the Notification Helper"` } func (c *Config) init() { @@ -134,11 +136,12 @@ func (c *Config) init() { } type svc struct { - c *Config - webDavHandler *WebDavHandler - davHandler *DavHandler - favoritesManager favorite.Manager - client *http.Client + c *Config + webDavHandler *WebDavHandler + davHandler *DavHandler + favoritesManager favorite.Manager + client *http.Client + notificationHelper *notificationhelper.NotificationHelper } func getFavoritesManager(c *Config) (favorite.Manager, error) { @@ -170,8 +173,10 @@ func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) rhttp.Timeout(time.Duration(conf.Timeout*int64(time.Second))), rhttp.Insecure(conf.Insecure), ), - favoritesManager: fm, + favoritesManager: fm, + notificationHelper: notificationhelper.New("ocdav", conf.Notifications, log), } + // initialize handlers and set default configs if err := s.webDavHandler.init(conf.WebdavNamespace, true); err != nil { return nil, err @@ -187,6 +192,7 @@ func (s *svc) Prefix() string { } func (s *svc) Close() error { + s.notificationHelper.Stop() return nil } diff --git a/internal/http/services/owncloud/ocdav/put.go b/internal/http/services/owncloud/ocdav/put.go index 4abb404b79..bc7a314292 100644 --- a/internal/http/services/owncloud/ocdav/put.go +++ b/internal/http/services/owncloud/ocdav/put.go @@ -20,19 +20,23 @@ package ocdav import ( "context" + "encoding/json" "net/http" "path" + "path/filepath" "strconv" "strings" "time" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + linkv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/cs3org/reva/internal/http/services/datagateway" "github.com/cs3org/reva/pkg/appctx" ctxpkg "github.com/cs3org/reva/pkg/ctx" "github.com/cs3org/reva/pkg/errtypes" + "github.com/cs3org/reva/pkg/notification/trigger" "github.com/cs3org/reva/pkg/rhttp" "github.com/cs3org/reva/pkg/storage/utils/chunking" rtrace "github.com/cs3org/reva/pkg/trace" @@ -344,6 +348,53 @@ func (s *svc) handlePut(ctx context.Context, w http.ResponseWriter, r *http.Requ lastModifiedString := t.Format(time.RFC1123Z) w.Header().Set(HeaderLastModified, lastModifiedString) + var m map[string]*typespb.OpaqueEntry + if sRes.Info.GetOpaque() != nil { + m = sRes.Info.GetOpaque().Map + } + + if ls, ok := m["link-share"]; ok { + l := &linkv1beta1.PublicShare{} + switch ls.Decoder { + case "json": + _ = json.Unmarshal(ls.Value, l) + default: + log.Error().Msgf("opaque entry decoder %s not recognized", ls.Decoder) + } + + path := "" + folder := "" + _, shareFileName := filepath.Split(ref.Path) + + if f, ok := m["eos"]; ok { + eosOpaque := make(map[string]interface{}) + switch f.Decoder { + case "json": + _ = json.Unmarshal(f.Value, &eosOpaque) + default: + log.Error().Msgf("opaque entry decoder %s not recognized", f.Decoder) + } + + if p, ok := eosOpaque["file"]; ok { + path, _ = filepath.Split(p.(string)) + } + } + + if path != "" { + folder = filepath.Base(path) + } + + trg := &trigger.Trigger{ + Ref: l.Id.OpaqueId, + TemplateData: map[string]interface{}{ + "path": path, + "folder": folder, + "fileName": shareFileName, + }, + } + s.notificationHelper.TriggerNotification(trg) + } + // file was new if info == nil { w.WriteHeader(http.StatusCreated) diff --git a/internal/http/services/owncloud/ocs/config/config.go b/internal/http/services/owncloud/ocs/config/config.go index 31b1850eb1..123cdd47be 100644 --- a/internal/http/services/owncloud/ocs/config/config.go +++ b/internal/http/services/owncloud/ocs/config/config.go @@ -45,6 +45,7 @@ type Config struct { AllowedLanguages []string `mapstructure:"allowed_languages"` OCMMountPoint string `mapstructure:"ocm_mount_point"` ListOCMShares bool `mapstructure:"list_ocm_shares"` + Notifications map[string]interface{} `mapstructure:"notifications"` } // Init sets sane defaults. diff --git a/internal/http/services/owncloud/ocs/conversions/main.go b/internal/http/services/owncloud/ocs/conversions/main.go index 24a50b9246..1650ab3182 100644 --- a/internal/http/services/owncloud/ocs/conversions/main.go +++ b/internal/http/services/owncloud/ocs/conversions/main.go @@ -149,6 +149,10 @@ type ShareData struct { Quicklink bool `json:"quicklink,omitempty" xml:"quicklink,omitempty"` // Description of the public share Description string `json:"description" xml:"description"` + // Whether to notify owner of file uploads to the public share + NotifyUploads bool `json:"notify_uploads" xml:"notify_uploads"` + // Additional recipients for the file upload to public share notification + NotifyUploadsExtraRecipients string `json:"notify_uploads_extra_recipients" xml:"notify_uploads_extra_recipients"` } // ShareeData holds share recipient search results. @@ -214,15 +218,17 @@ func PublicShare2ShareData(share *link.PublicShare, r *http.Request, publicURL s sd := &ShareData{ // share.permissions are mapped below // Displaynames are added later - ShareType: ShareTypePublicLink, - Token: share.Token, - Name: share.DisplayName, - MailSend: 0, - URL: publicURL + path.Join("/", "s/"+share.Token), - UIDOwner: LocalUserIDToString(share.Creator), - UIDFileOwner: LocalUserIDToString(share.Owner), - Quicklink: share.Quicklink, - Description: share.Description, + ShareType: ShareTypePublicLink, + Token: share.Token, + Name: share.DisplayName, + MailSend: 0, + URL: publicURL + path.Join("/", "s/"+share.Token), + UIDOwner: LocalUserIDToString(share.Creator), + UIDFileOwner: LocalUserIDToString(share.Owner), + Quicklink: share.Quicklink, + Description: share.Description, + NotifyUploads: share.NotifyUploads, + NotifyUploadsExtraRecipients: share.NotifyUploadsExtraRecipients, } if share.Id != nil { sd.ID = share.Id.OpaqueId diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/group.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/group.go index c1cc10bba3..8041595938 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/group.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/group.go @@ -20,6 +20,7 @@ package shares import ( "net/http" + "strconv" grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" @@ -28,6 +29,7 @@ import ( types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/cs3org/reva/internal/http/services/owncloud/ocs/conversions" "github.com/cs3org/reva/internal/http/services/owncloud/ocs/response" + ctxpkg "github.com/cs3org/reva/pkg/ctx" "github.com/cs3org/reva/pkg/rgrpc/todo/pool" ) @@ -80,5 +82,13 @@ func (h *Handler) createGroupShare(w http.ResponseWriter, r *http.Request, statI }, } - h.createCs3Share(ctx, w, r, c, createShareReq, statInfo) + if shareID, ok := h.createCs3Share(ctx, w, r, c, createShareReq, statInfo); ok { + notify, _ := strconv.ParseBool(r.FormValue("notify")) + if notify { + granter, ok := ctxpkg.ContextGetUser(ctx) + if ok { + h.SendShareNotification(shareID.OpaqueId, granter, groupRes.Group, statInfo) + } + } + } } diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/public.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/public.go index 9d20a59669..fd341cb3ea 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/public.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/public.go @@ -34,6 +34,8 @@ import ( "github.com/cs3org/reva/internal/http/services/owncloud/ocs/conversions" "github.com/cs3org/reva/internal/http/services/owncloud/ocs/response" "github.com/cs3org/reva/pkg/appctx" + ctxpkg "github.com/cs3org/reva/pkg/ctx" + "github.com/cs3org/reva/pkg/notification" "github.com/cs3org/reva/pkg/publicshare" "github.com/cs3org/reva/pkg/rgrpc/todo/pool" "github.com/pkg/errors" @@ -112,6 +114,8 @@ func (h *Handler) createPublicLinkShare(w http.ResponseWriter, r *http.Request, } internal, _ := strconv.ParseBool(r.FormValue("internal")) + notifyUploads, _ := strconv.ParseBool(r.FormValue("notifyUploads")) + notifyUploadsExtraRecipients := r.FormValue("notifyUploadsExtraRecipients") req := link.CreatePublicShareRequest{ ResourceInfo: statInfo, @@ -121,8 +125,10 @@ func (h *Handler) createPublicLinkShare(w http.ResponseWriter, r *http.Request, }, Password: r.FormValue("password"), }, - Description: r.FormValue("description"), - Internal: internal, + Description: r.FormValue("description"), + Internal: internal, + NotifyUploads: notifyUploads, + NotifyUploadsExtraRecipients: notifyUploadsExtraRecipients, } expireTimeString, ok := r.Form["expireDate"] @@ -338,6 +344,25 @@ func (h *Handler) updatePublicShare(w http.ResponseWriter, r *http.Request, shar }, }) } + + // remove notifications when a public link stops having 'uploader' permissions + if !isPermissionUploader(newPermissions) { + if before.Share.NotifyUploads { + updates = append(updates, &link.UpdatePublicShareRequest_Update{ + Type: link.UpdatePublicShareRequest_Update_TYPE_NOTIFYUPLOADS, + NotifyUploads: false, + }) + } + + if before.Share.NotifyUploadsExtraRecipients != "" { + updates = append(updates, &link.UpdatePublicShareRequest_Update{ + Type: link.UpdatePublicShareRequest_Update_TYPE_NOTIFYUPLOADSEXTRARECIPIENTS, + NotifyUploadsExtraRecipients: "", + }) + } + + h.notificationHelper.UnregisterNotification(shareID) + } } // ExpireDate @@ -392,6 +417,64 @@ func (h *Handler) updatePublicShare(w http.ResponseWriter, r *http.Request, shar }) } + // NotifyUploads + newNotifyUploads, ok := r.Form["notifyUploads"] + + if ok { + ok2 := permissionsStayUploader(before, newPermissions) + u, ok3 := ctxpkg.ContextGetUser(r.Context()) + + if ok2 && ok3 { + notifyUploads, _ := strconv.ParseBool(newNotifyUploads[0]) + updatesFound = true + + logger.Info().Str("shares", "update").Msgf("notify uploads updated to '%v'", notifyUploads) + updates = append(updates, &link.UpdatePublicShareRequest_Update{ + Type: link.UpdatePublicShareRequest_Update_TYPE_NOTIFYUPLOADS, + NotifyUploads: notifyUploads, + }) + + if notifyUploads { + n := ¬ification.Notification{ + TemplateName: "sharedfolder-upload-mail", + Ref: shareID, + Recipients: []string{u.Mail}, + } + h.notificationHelper.RegisterNotification(n) + } else { + h.notificationHelper.UnregisterNotification(shareID) + } + } + } + + // NotifyUploadsExtraRecipients + newNotifyUploadsExtraRecipients, ok := r.Form["notifyUploadsExtraRecipients"] + + if ok { + ok2 := permissionsStayUploader(before, newPermissions) + u, ok3 := ctxpkg.ContextGetUser(r.Context()) + + if ok2 && ok3 { + notifyUploadsExtraRecipients := newNotifyUploadsExtraRecipients[0] + updatesFound = true + logger.Info().Str("shares", "update").Msgf("notify uploads extra recipients updated to '%v'", notifyUploadsExtraRecipients) + + updates = append(updates, &link.UpdatePublicShareRequest_Update{ + Type: link.UpdatePublicShareRequest_Update_TYPE_NOTIFYUPLOADSEXTRARECIPIENTS, + NotifyUploadsExtraRecipients: notifyUploadsExtraRecipients, + }) + + if len(notifyUploadsExtraRecipients) > 0 { + n := ¬ification.Notification{ + TemplateName: "sharedfolder-upload-mail", + Ref: shareID, + Recipients: []string{u.Mail, notifyUploadsExtraRecipients}, + } + h.notificationHelper.RegisterNotification(n) + } + } + } + publicShare := before.Share // Updates are atomical. See: https://github.com/cs3org/cs3apis/pull/67#issuecomment-617651428 so in order to get the latest updated version @@ -473,6 +556,8 @@ func (h *Handler) removePublicShare(w http.ResponseWriter, r *http.Request, shar return } + h.notificationHelper.UnregisterNotification(shareID) + response.WriteOCSSuccess(w, r, nil) } @@ -536,6 +621,21 @@ func permissionFromRequest(r *http.Request, h *Handler) (*provider.ResourcePermi return p, err } +func isPermissionUploader(permissions *provider.ResourcePermissions) bool { + if permissions == nil { + return false + } + + publicSharePermissions := &link.PublicSharePermissions{ + Permissions: permissions, + } + return conversions.RoleFromResourcePermissions(publicSharePermissions.Permissions).Name == conversions.RoleUploader +} + +func permissionsStayUploader(before *link.GetPublicShareResponse, newPermissions *provider.ResourcePermissions) bool { + return (newPermissions == nil && isPermissionUploader(before.Share.GetPermissions().Permissions)) || isPermissionUploader(newPermissions) +} + // TODO: add mapping for user share permissions to role // Maps oc10 public link permissions to roles. diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go index 37a7aa05cb..119fe88774 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go @@ -26,6 +26,7 @@ import ( "mime" "net/http" "path" + "path/filepath" "strconv" "strings" "sync" @@ -45,6 +46,10 @@ import ( "github.com/cs3org/reva/internal/http/services/owncloud/ocs/conversions" "github.com/cs3org/reva/internal/http/services/owncloud/ocs/response" "github.com/cs3org/reva/pkg/appctx" + ctxpkg "github.com/cs3org/reva/pkg/ctx" + "github.com/cs3org/reva/pkg/notification" + "github.com/cs3org/reva/pkg/notification/notificationhelper" + "github.com/cs3org/reva/pkg/notification/trigger" "github.com/cs3org/reva/pkg/publicshare" "github.com/cs3org/reva/pkg/rgrpc/todo/pool" "github.com/cs3org/reva/pkg/share" @@ -75,6 +80,8 @@ type Handler struct { resourceInfoCache cache.ResourceInfoCache resourceInfoCacheTTL time.Duration listOCMShares bool + notificationHelper *notificationhelper.NotificationHelper + Log *zerolog.Logger } // we only cache the minimal set of data instead of the full user metadata. @@ -99,7 +106,7 @@ func getCacheManager(c *config.Config) (cache.ResourceInfoCache, error) { } // Init initializes this and any contained handlers. -func (h *Handler) Init(c *config.Config) { +func (h *Handler) Init(c *config.Config, l *zerolog.Logger) { h.gatewayAddr = c.GatewaySvc h.storageRegistryAddr = c.StorageregistrySvc h.publicURL = c.Config.Host @@ -107,7 +114,8 @@ func (h *Handler) Init(c *config.Config) { h.homeNamespace = c.HomeNamespace h.ocmMountPoint = c.OCMMountPoint h.listOCMShares = c.ListOCMShares - + h.Log = l + h.notificationHelper = notificationhelper.New("ocs", c.Notifications, l) h.additionalInfoTemplate, _ = template.New("additionalInfo").Parse(c.AdditionalInfoAttribute) h.resourceInfoCacheTTL = time.Second * time.Duration(c.ResourceInfoCacheTTL) @@ -235,6 +243,125 @@ func (h *Handler) CreateShare(w http.ResponseWriter, r *http.Request) { } } +// NotifyShare handles GET requests on /apps/files_sharing/api/v1/shares/(shareid)/notify. +func (h *Handler) NotifyShare(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + opaqueID := chi.URLParam(r, "shareid") + + c, err := pool.GetGatewayServiceClient(pool.Endpoint(h.gatewayAddr)) + if err != nil { + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err) + return + } + + shareRes, err := c.GetShare(ctx, &collaboration.GetShareRequest{ + Ref: &collaboration.ShareReference{ + Spec: &collaboration.ShareReference_Id{ + Id: &collaboration.ShareId{ + OpaqueId: opaqueID, + }, + }, + }, + }) + if err != nil || shareRes.Status.Code == rpc.Code_CODE_NOT_FOUND { + h.Log.Error().Err(err).Msg("error getting share") + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting share", err) + return + } + + granter, ok := ctxpkg.ContextGetUser(ctx) + if !ok { + h.Log.Error().Err(err).Msgf("error getting granter data") + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting granter data", err) + } + + resourceID := shareRes.Share.ResourceId + statInfo, status, err := h.getResourceInfoByID(ctx, c, resourceID) + if err != nil || status.Code != rpc.Code_CODE_OK { + h.Log.Error().Err(err).Msg("error mapping share data") + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error mapping share data", err) + return + } + + var recipient string + + granteeType := shareRes.Share.Grantee.Type + if granteeType == provider.GranteeType_GRANTEE_TYPE_USER { + granteeID := shareRes.Share.Grantee.GetUserId().OpaqueId + granteeRes, err := c.GetUserByClaim(ctx, &userpb.GetUserByClaimRequest{ + Claim: "username", + Value: granteeID, + SkipFetchingUserGroups: true, + }) + if err != nil { + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grantee data", err) + return + } + + recipient = h.SendShareNotification(opaqueID, granter, granteeRes.User, statInfo) + } else if granteeType == provider.GranteeType_GRANTEE_TYPE_GROUP { + granteeID := shareRes.Share.Grantee.GetGroupId().OpaqueId + granteeRes, err := c.GetGroupByClaim(ctx, &grouppb.GetGroupByClaimRequest{ + Claim: "group_name", + Value: granteeID, + SkipFetchingMembers: true, + }) + if err != nil { + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grantee data", err) + return + } + + recipient = h.SendShareNotification(opaqueID, granter, granteeRes.Group, statInfo) + } + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "application/json") + rb, _ := json.Marshal(map[string]interface{}{"recipients": []string{recipient}}) + _, err = w.Write(rb) + if err != nil { + h.Log.Error().Err(err).Msg("error writing response") + } +} + +// SendShareNotification sends a notification with information from a Share. +func (h *Handler) SendShareNotification(opaqueID string, granter *userpb.User, grantee interface{}, statInfo *provider.ResourceInfo) string { + var granteeDisplayName, granteeName, recipient string + isGranteeGroup := false + + if u, ok := grantee.(*userpb.User); ok { + granteeDisplayName = u.DisplayName + granteeName = u.Username + recipient = u.Mail + } else if g, ok := grantee.(*grouppb.Group); ok { + granteeDisplayName = g.DisplayName + granteeName = g.GroupName + recipient = g.Mail + isGranteeGroup = true + } + + h.notificationHelper.TriggerNotification(&trigger.Trigger{ + Notification: ¬ification.Notification{ + TemplateName: "share-create-mail", + Ref: opaqueID, + Recipients: []string{recipient}, + }, + Ref: opaqueID, + TemplateData: map[string]interface{}{ + "granteeDisplayName": granteeDisplayName, + "granteeUserName": granteeName, + "granterDisplayName": granter.DisplayName, + "granterUserName": granter.Username, + "path": statInfo.Path, + "isFolder": statInfo.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER, + "isGranteeGroup": isGranteeGroup, + "base": filepath.Base(statInfo.Path), + }, + }) + h.Log.Debug().Msgf("notification trigger %s created", opaqueID) + + return recipient +} + func (h *Handler) extractPermissions(w http.ResponseWriter, r *http.Request, ri *provider.ResourceInfo, defaultPermissions *conversions.Role) (*conversions.Role, []byte, error) { reqRole, reqPermissions := r.FormValue("role"), r.FormValue("permissions") var role *conversions.Role @@ -1134,33 +1261,34 @@ func (h *Handler) getResourceInfo(ctx context.Context, client gateway.GatewayAPI return pinfo, status, nil } -func (h *Handler) createCs3Share(ctx context.Context, w http.ResponseWriter, r *http.Request, client gateway.GatewayAPIClient, req *collaboration.CreateShareRequest, info *provider.ResourceInfo) { +func (h *Handler) createCs3Share(ctx context.Context, w http.ResponseWriter, r *http.Request, client gateway.GatewayAPIClient, req *collaboration.CreateShareRequest, info *provider.ResourceInfo) (*collaboration.ShareId, bool) { createShareResponse, err := client.CreateShare(ctx, req) if err != nil { response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error sending a grpc create share request", err) - return + return nil, false } if createShareResponse.Status.Code != rpc.Code_CODE_OK { if createShareResponse.Status.Code == rpc.Code_CODE_NOT_FOUND { response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "not found", nil) - return + return nil, false } response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc create share request failed", err) - return + return nil, false } s, err := conversions.CS3Share2ShareData(ctx, createShareResponse.Share) if err != nil { response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error mapping share data", err) - return + return nil, false } err = h.addFileInfo(ctx, s, info) if err != nil { response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error adding fileinfo to share", err) - return + return nil, false } h.mapUserIds(ctx, client, s) response.WriteOCSSuccess(w, r, s) + return createShareResponse.Share.Id, true } func mapState(state collaboration.ShareState) int { diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/user.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/user.go index 20146ed3af..c0f212eac2 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/user.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/user.go @@ -21,6 +21,7 @@ package shares import ( "context" "net/http" + "strconv" "sync" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" @@ -87,7 +88,15 @@ func (h *Handler) createUserShare(w http.ResponseWriter, r *http.Request, statIn }, } - h.createCs3Share(ctx, w, r, c, createShareReq, statInfo) + if shareID, ok := h.createCs3Share(ctx, w, r, c, createShareReq, statInfo); ok { + notify, _ := strconv.ParseBool(r.FormValue("notify")) + if notify { + granter, ok := ctxpkg.ContextGetUser(ctx) + if ok { + h.SendShareNotification(shareID.OpaqueId, granter, userRes.User, statInfo) + } + } + } } func (h *Handler) isUserShare(r *http.Request, oid string) bool { diff --git a/internal/http/services/owncloud/ocs/ocs.go b/internal/http/services/owncloud/ocs/ocs.go index c579fa04f9..5aef6c9ffb 100644 --- a/internal/http/services/owncloud/ocs/ocs.go +++ b/internal/http/services/owncloud/ocs/ocs.go @@ -62,7 +62,7 @@ func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) router: r, } - if err := s.routerInit(); err != nil { + if err := s.routerInit(log); err != nil { return nil, err } @@ -86,7 +86,7 @@ func (s *svc) Unprotected() []string { return []string{"/v1.php/cloud/capabilities", "/v2.php/cloud/capabilities"} } -func (s *svc) routerInit() error { +func (s *svc) routerInit(l *zerolog.Logger) error { capabilitiesHandler := new(capabilities.Handler) userHandler := new(user.Handler) usersHandler := new(users.Handler) @@ -97,7 +97,7 @@ func (s *svc) routerInit() error { usersHandler.Init(s.c) userHandler.Init(s.c) configHandler.Init(s.c) - sharesHandler.Init(s.c) + sharesHandler.Init(s.c, l) shareesHandler.Init(s.c) s.router.Route("/v{version:(1|2)}.php", func(r chi.Router) { @@ -119,16 +119,12 @@ func (s *svc) routerInit() error { }) r.Get("/{shareid}", sharesHandler.GetShare) r.Put("/{shareid}", sharesHandler.UpdateShare) + r.Get("/{shareid}/notify", sharesHandler.NotifyShare) r.Delete("/{shareid}", sharesHandler.RemoveShare) }) r.Get("/sharees", shareesHandler.FindSharees) }) - // placeholder for notifications - r.Get("/apps/notifications/api/v1/notifications", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - }) - r.Get("/config", configHandler.GetConfig) r.Route("/cloud", func(r chi.Router) { diff --git a/internal/serverless/services/loader/loader.go b/internal/serverless/services/loader/loader.go index 1b466a144c..71ab9afb1d 100644 --- a/internal/serverless/services/loader/loader.go +++ b/internal/serverless/services/loader/loader.go @@ -21,5 +21,6 @@ package loader import ( // Load core serverless services. _ "github.com/cs3org/reva/internal/serverless/services/helloworld" + _ "github.com/cs3org/reva/internal/serverless/services/notifications" // Add your own service here. ) diff --git a/internal/serverless/services/notifications/notifications.go b/internal/serverless/services/notifications/notifications.go new file mode 100644 index 0000000000..b51b163fd4 --- /dev/null +++ b/internal/serverless/services/notifications/notifications.go @@ -0,0 +1,403 @@ +// Copyright 2018-2023 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 notifications + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/pkg/errors" + + "github.com/cs3org/reva/pkg/errtypes" + "github.com/cs3org/reva/pkg/notification" + "github.com/cs3org/reva/pkg/notification/handler" + handlerRegistry "github.com/cs3org/reva/pkg/notification/handler/registry" + notificationManagerRegistry "github.com/cs3org/reva/pkg/notification/manager/registry" + "github.com/cs3org/reva/pkg/notification/template" + templateRegistry "github.com/cs3org/reva/pkg/notification/template/registry" + "github.com/cs3org/reva/pkg/notification/trigger" + "github.com/cs3org/reva/pkg/notification/utils" + "github.com/cs3org/reva/pkg/rserverless" + "github.com/cs3org/reva/pkg/utils/accumulator" + "github.com/mitchellh/mapstructure" + "github.com/nats-io/nats.go" + "github.com/rs/zerolog" +) + +type config struct { + NatsAddress string `mapstructure:"nats_address" docs:";The NATS server address."` + NatsToken string `mapstructure:"nats_token" docs:"The token to authenticate against the NATS server"` + NatsPrefix string `mapstructure:"nats_prefix" docs:"reva-notifications;The notifications NATS stream."` + HandlerConf map[string]interface{} `mapstructure:"handlers" docs:";Settings for the different notification handlers."` + GroupingInterval int `mapstructure:"grouping_interval" docs:"60;Time in seconds to group incoming notification triggers"` + GroupingMaxSize int `mapstructure:"grouping_max_size" docs:"100;Maximum number of notifications to group"` + StorageDriver string `mapstructure:"storage_driver" docs:"mysql;The driver used to store notifications"` + StorageDrivers map[string]map[string]interface{} `mapstructure:"storage_drivers"` +} + +func defaultConfig() *config { + return &config{ + NatsPrefix: "reva-notifications", + GroupingInterval: 60, + GroupingMaxSize: 100, + StorageDriver: "sql", + } +} + +type svc struct { + nc *nats.Conn + js nats.JetStreamContext + kv nats.KeyValue + conf *config + log *zerolog.Logger + handlers map[string]handler.Handler + templates templateRegistry.Registry + nm notification.Manager + accumulators map[string]*accumulator.Accumulator[trigger.Trigger] +} + +func init() { + rserverless.Register("notifications", New) +} + +func getNotificationManager(c *config, l *zerolog.Logger) (notification.Manager, error) { + if f, ok := notificationManagerRegistry.NewFuncs[c.StorageDriver]; ok { + return f(c.StorageDrivers[c.StorageDriver]) + } + return nil, errtypes.NotFound(fmt.Sprintf("storage driver %s not found", c.StorageDriver)) +} + +// New returns a new Notifications service. +func New(m map[string]interface{}, log *zerolog.Logger) (rserverless.Service, error) { + conf := defaultConfig() + + if err := mapstructure.Decode(m, conf); err != nil { + return nil, err + } + + nm, err := getNotificationManager(conf, log) + if err != nil { + return nil, err + } + log.Info().Msgf("notification storage %s initialized", conf.StorageDriver) + + s := &svc{ + conf: conf, + log: log, + nm: nm, + } + + return s, nil +} + +// Start starts the Notifications service. +func (s *svc) Start() { + s.templates = *templateRegistry.New() + s.handlers = handlerRegistry.InitHandlers(s.conf.HandlerConf, s.log) + s.accumulators = make(map[string]*accumulator.Accumulator[trigger.Trigger]) + + s.log.Debug().Msgf("connecting to nats server at %s", s.conf.NatsAddress) + err := s.connect() + if err != nil { + s.log.Error().Err(err).Msg("connecting to nats failed") + } + s.log.Info().Msg("notifications service ready") +} + +// Close performs cleanup. +func (s *svc) Close(ctx context.Context) error { + return s.nc.Drain() +} + +func (s *svc) connect() error { + nc, err := utils.ConnectToNats(s.conf.NatsAddress, s.conf.NatsToken, *s.log) + if err != nil { + return err + } + s.nc = nc + + js, err := nc.JetStream(nats.PublishAsyncMaxPending(256)) + if err != nil { + return errors.Wrap(err, "jetstream initialization failed") + } + + s.js = js + + if err := s.initNatsKV("template", s.handleMsgTemplate); err != nil { + return err + } + if err := s.initNatsStream("notification-register", s.handleMsgRegisterNotification); err != nil { + return err + } + if err := s.initNatsStream("notification-unregister", s.handleMsgUnregisterNotification); err != nil { + return err + } + return s.initNatsStream("trigger", s.handleMsgTrigger) +} + +func (s *svc) initNatsKV(name string, handler func(msg []byte)) error { + bucketName := fmt.Sprintf("%s-%s", s.conf.NatsPrefix, name) + kv, err := s.js.CreateKeyValue(&nats.KeyValueConfig{ + Bucket: bucketName, + }) + if err != nil { + return errors.Wrap(err, "template store creation failed, probably because nats server is unreachable") + } + + s.kv = kv + + w, _ := kv.WatchAll() + + go func() { + for { + msg := <-w.Updates() + + if msg != nil { + handler(msg.Value()) + } + } + }() + + return nil +} + +func (s *svc) initNatsStream(name string, handler func(msg *nats.Msg)) error { + streamName := fmt.Sprintf("%s-%s", s.conf.NatsPrefix, name) + consumerName := fmt.Sprintf("%s-consumer-%s", s.conf.NatsPrefix, name) + subjectName := fmt.Sprintf("%s.%s", s.conf.NatsPrefix, name) + deliverySubjectName := fmt.Sprintf("%s-delivery.%s", s.conf.NatsPrefix, name) + + // Creates a NATS stream with given name if it does not exist already + if _, err := s.js.AddStream(&nats.StreamConfig{ + Name: streamName, + Subjects: []string{subjectName}, + }); err != nil { + return errors.Wrapf(err, "nats %s stream creation failed", name) + } + + // Adds a consumer with the given name to the JetStream context + if _, err := s.js.AddConsumer(streamName, &nats.ConsumerConfig{ + Durable: consumerName, + DeliverSubject: deliverySubjectName, + }); err != nil { + return errors.Wrapf(err, "nats %s consumer creation failed", name) + } + + // Subscribes the JetStream context to the consumer we just created + _, err := s.js.Subscribe("", func(msg *nats.Msg) { handler(msg) }, nats.Bind(streamName, consumerName)) + if err != nil { + return errors.Wrapf(err, "nats subscription to consumer %s failed", consumerName) + } + + return nil +} + +func (s *svc) handleMsgTemplate(msg []byte) { + if len(msg) == 0 { + return + } + + name, err := s.templates.Put(msg, s.handlers) + if err != nil { + s.log.Error().Err(err).Msgf("template registration failed %v", err) + + // If a template file was not found, delete that template from the registry altogether, + // this way we ensure templates that are deleted from the config are deleted from the + // store too. + wrappedErr := errors.Unwrap(errors.Unwrap(err)) + _, isFileNotFoundError := wrappedErr.(*template.FileNotFoundError) + if isFileNotFoundError && name != "" { + err := s.kv.Purge(name) + if err != nil { + s.log.Error().Err(err).Msgf("deletion of template %s from store failed", name) + } + s.log.Info().Msgf("template %s unregistered", name) + } + } else { + s.log.Info().Msgf("template %s registered", name) + } +} + +func (s *svc) handleMsgRegisterNotification(msg *nats.Msg) { + var data map[string]interface{} + err := json.Unmarshal(msg.Data, &data) + if err != nil { + s.log.Error().Err(err).Msg("notification registration unmarshall failed") + return + } + + n := ¬ification.Notification{} + if err := mapstructure.Decode(data, n); err != nil { + s.log.Error().Err(err).Msg("notification registration decoding failed") + return + } + + templ, err := s.templates.Get(n.TemplateName) + if err != nil { + s.log.Error().Err(err).Msg("notification template get failed") + return + } + + n.Template = *templ + err = s.nm.UpsertNotification(*n) + if err != nil { + s.log.Error().Err(err).Msgf("registering notification %s failed", n.Ref) + } else { + s.log.Info().Msgf("notification %s registered", n.Ref) + } +} + +func (s *svc) handleMsgUnregisterNotification(msg *nats.Msg) { + ref := string(msg.Data) + + err := s.nm.DeleteNotification(ref) + if err != nil { + _, isNotFoundError := err.(*notification.NotFoundError) + if isNotFoundError { + s.log.Debug().Msgf("a notification with ref %s does not exist", ref) + } else { + s.log.Error().Err(err).Msgf("notification unregister failed") + } + } else { + s.log.Debug().Msgf("notification %s unregistered", ref) + } +} + +func (s *svc) getAccumulatorForTrigger(tr trigger.Trigger) *accumulator.Accumulator[trigger.Trigger] { + a, ok := s.accumulators[tr.Ref] + + if !ok || a == nil { + timeout := time.Duration(s.conf.GroupingInterval) * time.Second + maxSize := s.conf.GroupingMaxSize + + a = accumulator.New[trigger.Trigger](timeout, maxSize, s.log) + _ = a.Start(s.notificationSendCallback) + s.accumulators[tr.Ref] = a + + s.log.Debug().Msgf("created new accumulator for trigger %s", tr.Ref) + } + + return a +} + +func (s *svc) handleMsgTrigger(msg *nats.Msg) { + var data map[string]interface{} + err := json.Unmarshal(msg.Data, &data) + if err != nil { + s.log.Error().Err(err).Msg("notification trigger unmarshall failed") + return + } + + tr := &trigger.Trigger{} + if err := mapstructure.Decode(data, tr); err != nil { + s.log.Error().Err(err).Msg("trigger creation failed") + return + } + + s.log.Info().Msgf("notification trigger %s received", tr.Ref) + + notif := tr.Notification + if notif == nil { + notif, err = s.nm.GetNotification(tr.Ref) + if err != nil { + _, isNotFoundError := err.(*notification.NotFoundError) + if isNotFoundError { + s.log.Debug().Msgf("trigger %s does not have a notification attached", tr.Ref) + return + } + s.log.Error().Err(err).Msgf("notification retrieval from store failed") + return + } + } + + templ, err := s.templates.Get(notif.TemplateName) + if err != nil { + s.log.Error().Err(err).Msgf("template %s for trigger %s not found", notif.TemplateName, tr.Ref) + return + } + + notif.Template = *templ + tr.Notification = notif + a := s.getAccumulatorForTrigger(*tr) + a.Input <- *tr +} + +func (s *svc) notificationSendCallback(ts []trigger.Trigger) { + const itemCount = 10 + var tr trigger.Trigger + + if len(ts) == 1 { + tr = ts[0] + s.log.Info().Msgf("sending single notification for trigger %s", tr.Ref) + } else { + moreCount := len(ts) - itemCount + if moreCount < 0 { + moreCount = 0 + } + + // create a new trigger + tr = trigger.Trigger{ + Ref: ts[0].Ref, + Sender: ts[0].Sender, + TemplateData: map[string]interface{}{ + "_count": len(ts), + "_items": []map[string]interface{}{}, + "_moreCount": moreCount, + }, + } + + // add template data of the first ten elements, ignore the rest + l := itemCount + templateData := []map[string]interface{}{} + if l > len(ts) { + l = len(ts) + } + for _, t := range ts[:l] { + templateData = append(templateData, t.TemplateData) + } + tr.TemplateData["_items"] = templateData + + // initialize the new trigger + notif, err := s.nm.GetNotification(tr.Ref) + if err != nil { + s.log.Error().Msgf("notification retrieval from store failed") + return + } + + templ, err := s.templates.Get(notif.TemplateName) + if err != nil { + s.log.Error().Err(err).Msgf("template %s for trigger %s not found", notif.TemplateName, tr.Ref) + return + } + + notif.Template = *templ + tr.Notification = notif + + s.log.Info().Msgf("sending multi notification for %d triggers %s", tr.TemplateData["_count"], tr.Ref) + } + + // destroy old accumulator + s.accumulators[tr.Ref] = nil + + if err := tr.Send(); err != nil { + s.log.Error().Err(err).Msgf("notification send failed") + } +} diff --git a/pkg/cbox/publicshare/sql/sql.go b/pkg/cbox/publicshare/sql/sql.go index 84e09a9b39..a3f662aecb 100644 --- a/pkg/cbox/publicshare/sql/sql.go +++ b/pkg/cbox/publicshare/sql/sql.go @@ -127,7 +127,7 @@ func New(m map[string]interface{}) (publicshare.Manager, error) { return &mgr, nil } -func (m *manager) CreatePublicShare(ctx context.Context, u *user.User, rInfo *provider.ResourceInfo, g *link.Grant, description string, internal bool) (*link.PublicShare, error) { +func (m *manager) CreatePublicShare(ctx context.Context, u *user.User, rInfo *provider.ResourceInfo, g *link.Grant, description string, internal bool, notifyUploads bool, notifyUploadsExtraRecipients string) (*link.PublicShare, error) { tkn := utils.RandString(15) now := time.Now().Unix() @@ -154,8 +154,8 @@ func (m *manager) CreatePublicShare(ctx context.Context, u *user.User, rInfo *pr fileSource = 0 } - query := "insert into oc_share set share_type=?,uid_owner=?,uid_initiator=?,item_type=?,fileid_prefix=?,item_source=?,file_source=?,permissions=?,stime=?,token=?,share_name=?,quicklink=?,description=?,internal=?" - params := []interface{}{publicShareType, owner, creator, itemType, prefix, itemSource, fileSource, permissions, now, tkn, displayName, quicklink, description, internal} + query := "insert into oc_share set share_type=?,uid_owner=?,uid_initiator=?,item_type=?,fileid_prefix=?,item_source=?,file_source=?,permissions=?,stime=?,token=?,share_name=?,quicklink=?,description=?,internal=?,notify_uploads=?,notify_uploads_extra_recipients=?" + params := []interface{}{publicShareType, owner, creator, itemType, prefix, itemSource, fileSource, permissions, now, tkn, displayName, quicklink, description, internal, notifyUploads, notifyUploadsExtraRecipients} var passwordProtected bool password := g.Password @@ -193,18 +193,20 @@ func (m *manager) CreatePublicShare(ctx context.Context, u *user.User, rInfo *pr Id: &link.PublicShareId{ OpaqueId: strconv.FormatInt(lastID, 10), }, - Owner: rInfo.GetOwner(), - Creator: u.Id, - ResourceId: rInfo.Id, - Token: tkn, - Permissions: g.Permissions, - Ctime: createdAt, - Mtime: createdAt, - PasswordProtected: passwordProtected, - Expiration: g.Expiration, - DisplayName: displayName, - Quicklink: quicklink, - Description: description, + Owner: rInfo.GetOwner(), + Creator: u.Id, + ResourceId: rInfo.Id, + Token: tkn, + Permissions: g.Permissions, + Ctime: createdAt, + Mtime: createdAt, + PasswordProtected: passwordProtected, + Expiration: g.Expiration, + DisplayName: displayName, + Quicklink: quicklink, + Description: description, + NotifyUploads: notifyUploads, + NotifyUploadsExtraRecipients: notifyUploadsExtraRecipients, }, nil } @@ -235,6 +237,10 @@ func (m *manager) UpdatePublicShare(ctx context.Context, u *user.User, req *link } case link.UpdatePublicShareRequest_Update_TYPE_DESCRIPTION: paramsMap["description"] = req.Update.GetDescription() + case link.UpdatePublicShareRequest_Update_TYPE_NOTIFYUPLOADS: + paramsMap["notify_uploads"] = req.Update.GetNotifyUploads() + case link.UpdatePublicShareRequest_Update_TYPE_NOTIFYUPLOADSEXTRARECIPIENTS: + paramsMap["notify_uploads_extra_recipients"] = req.Update.GetNotifyUploadsExtraRecipients() default: return nil, fmt.Errorf("invalid update type: %v", req.GetUpdate().GetType()) } @@ -268,8 +274,8 @@ func (m *manager) UpdatePublicShare(ctx context.Context, u *user.User, req *link func (m *manager) getByToken(ctx context.Context, token string, u *user.User) (*link.PublicShare, string, error) { s := conversions.DBShare{Token: token} - query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, id, stime, permissions, quicklink, description FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND share_type=? AND token=?" - if err := m.db.QueryRow(query, publicShareType, token).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions, &s.Quicklink, &s.Description); err != nil { + query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, id, stime, permissions, quicklink, description, notify_uploads, notify_uploads_extra_recipients FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND share_type=? AND token=?" + if err := m.db.QueryRow(query, publicShareType, token).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions, &s.Quicklink, &s.Description, &s.NotifyUploads, &s.NotifyUploadsExtraRecipients); err != nil { if err == sql.ErrNoRows { return nil, "", errtypes.NotFound(token) } @@ -281,8 +287,8 @@ func (m *manager) getByToken(ctx context.Context, token string, u *user.User) (* func (m *manager) getByID(ctx context.Context, id *link.PublicShareId, u *user.User) (*link.PublicShare, string, error) { uid := conversions.FormatUserID(u.Id) s := conversions.DBShare{ID: id.OpaqueId} - query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(token,'') as token, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, stime, permissions, quicklink, description FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND share_type=? AND id=? AND (uid_owner=? OR uid_initiator=?)" - if err := m.db.QueryRow(query, publicShareType, id.OpaqueId, uid, uid).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.Token, &s.Expiration, &s.ShareName, &s.STime, &s.Permissions, &s.Quicklink, &s.Description); err != nil { + query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(token,'') as token, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, stime, permissions, quicklink, description, notify_uploads, notify_uploads_extra_recipients FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND share_type=? AND id=? AND (uid_owner=? OR uid_initiator=?)" + if err := m.db.QueryRow(query, publicShareType, id.OpaqueId, uid, uid).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.Token, &s.Expiration, &s.ShareName, &s.STime, &s.Permissions, &s.Quicklink, &s.Description, &s.NotifyUploads, &s.NotifyUploadsExtraRecipients); err != nil { if err == sql.ErrNoRows { return nil, "", errtypes.NotFound(id.OpaqueId) } @@ -357,7 +363,7 @@ func (m *manager) isProjectAdmin(ctx context.Context, u *user.User) bool { } func (m *manager) ListPublicShares(ctx context.Context, u *user.User, filters []*link.ListPublicSharesRequest_Filter, md *provider.ResourceInfo, sign bool) ([]*link.PublicShare, error) { - query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(token,'') as token, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, id, stime, permissions, quicklink, description FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND (share_type=?) AND internal=false" + query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(token,'') as token, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, id, stime, permissions, quicklink, description, notify_uploads, notify_uploads_extra_recipients FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND (share_type=?) AND internal=false" var resourceFilters, ownerFilters, creatorFilters string var resourceParams, ownerParams, creatorParams []interface{} params := []interface{}{publicShareType} @@ -415,7 +421,7 @@ func (m *manager) ListPublicShares(ctx context.Context, u *user.User, filters [] var s conversions.DBShare shares := []*link.PublicShare{} for rows.Next() { - if err := rows.Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.Token, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions, &s.Quicklink, &s.Description); err != nil { + if err := rows.Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.Token, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions, &s.Quicklink, &s.Description, &s.NotifyUploads, &s.NotifyUploadsExtraRecipients); err != nil { continue } cs3Share := conversions.ConvertToCS3PublicShare(s) @@ -474,8 +480,8 @@ func (m *manager) RevokePublicShare(ctx context.Context, u *user.User, ref *link func (m *manager) GetPublicShareByToken(ctx context.Context, token string, auth *link.PublicShareAuthentication, sign bool) (*link.PublicShare, error) { s := conversions.DBShare{Token: token} - query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, id, stime, permissions, quicklink, description FROM oc_share WHERE share_type=? AND token=?" - if err := m.db.QueryRow(query, publicShareType, token).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions, &s.Quicklink, &s.Description); err != nil { + query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, id, stime, permissions, quicklink, description, notify_uploads, notify_uploads_extra_recipients FROM oc_share WHERE share_type=? AND token=?" + if err := m.db.QueryRow(query, publicShareType, token).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions, &s.Quicklink, &s.Description, &s.NotifyUploads, &s.NotifyUploadsExtraRecipients); err != nil { if err == sql.ErrNoRows { return nil, errtypes.NotFound(token) } diff --git a/pkg/cbox/utils/conversions.go b/pkg/cbox/utils/conversions.go index 8fee7ac57a..534ed830a8 100644 --- a/pkg/cbox/utils/conversions.go +++ b/pkg/cbox/utils/conversions.go @@ -33,23 +33,25 @@ import ( // DBShare stores information about user and public shares. type DBShare struct { - ID string - UIDOwner string - UIDInitiator string - Prefix string - ItemSource string - ItemType string - ShareWith string - Token string - Expiration string - Permissions int - ShareType int - ShareName string - STime int - FileTarget string - State int - Quicklink bool - Description string + ID string + UIDOwner string + UIDInitiator string + Prefix string + ItemSource string + ItemType string + ShareWith string + Token string + Expiration string + Permissions int + ShareType int + ShareName string + STime int + FileTarget string + State int + Quicklink bool + Description string + NotifyUploads bool + NotifyUploadsExtraRecipients string } // FormatGrantee formats a CS3API grantee to a string. @@ -243,16 +245,18 @@ func ConvertToCS3PublicShare(s DBShare) *link.PublicShare { StorageId: s.Prefix, OpaqueId: s.ItemSource, }, - Permissions: &link.PublicSharePermissions{Permissions: IntTosharePerm(s.Permissions, s.ItemType)}, - Owner: ExtractUserID(s.UIDOwner), - Creator: ExtractUserID(s.UIDInitiator), - Token: s.Token, - DisplayName: s.ShareName, - PasswordProtected: pwd, - Expiration: expires, - Ctime: ts, - Mtime: ts, - Quicklink: s.Quicklink, - Description: s.Description, + Permissions: &link.PublicSharePermissions{Permissions: IntTosharePerm(s.Permissions, s.ItemType)}, + Owner: ExtractUserID(s.UIDOwner), + Creator: ExtractUserID(s.UIDInitiator), + Token: s.Token, + DisplayName: s.ShareName, + PasswordProtected: pwd, + Expiration: expires, + Ctime: ts, + Mtime: ts, + Quicklink: s.Quicklink, + Description: s.Description, + NotifyUploads: s.NotifyUploads, + NotifyUploadsExtraRecipients: s.NotifyUploadsExtraRecipients, } } diff --git a/pkg/datatx/datatx.go b/pkg/datatx/datatx.go index 8c430e2bbc..616a5a64ca 100644 --- a/pkg/datatx/datatx.go +++ b/pkg/datatx/datatx.go @@ -21,6 +21,7 @@ package datatx import ( "context" + userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" datatx "github.com/cs3org/go-cs3apis/cs3/tx/v1beta1" ) @@ -37,3 +38,24 @@ type Manager interface { // Note that tokens must still be valid. RetryTransfer(ctx context.Context, transferID string) (*datatx.TxInfo, error) } + +// Transfer represents datatx transfer. +type Transfer struct { + TxID string + SrcTargetURI string + DestTargetURI string + ShareID string + UserID *userv1beta1.UserId +} + +// Repository the interface that any storage driver should implement. +type Repository interface { + // StoreTransfer stores the transfer by its TxID + StoreTransfer(transfer *Transfer) error + // StoreTransfer deletes the transfer by its TxID + DeleteTransfer(transfer *Transfer) error + // GetTransfer returns the transfer with the specified transfer id + GetTransfer(txID string) (*Transfer, error) + // ListTransfers returns a filtered list of transfers + ListTransfers(Filters []*datatx.ListTransfersRequest_Filter, UserID *userv1beta1.UserId) ([]*Transfer, error) +} diff --git a/pkg/datatx/manager/loader/loader.go b/pkg/datatx/manager/loader/loader.go index 1452dc5dae..2c9d77d745 100644 --- a/pkg/datatx/manager/loader/loader.go +++ b/pkg/datatx/manager/loader/loader.go @@ -21,5 +21,7 @@ package loader import ( // Load datatx drivers. _ "github.com/cs3org/reva/pkg/datatx/manager/rclone" + _ "github.com/cs3org/reva/pkg/datatx/manager/rclone/repository/json" + _ "github.com/cs3org/reva/pkg/datatx/repository/json" // Add your own here. ) diff --git a/pkg/datatx/manager/rclone/rclone.go b/pkg/datatx/manager/rclone/rclone.go index 21e3d6e3f0..932677a106 100644 --- a/pkg/datatx/manager/rclone/rclone.go +++ b/pkg/datatx/manager/rclone/rclone.go @@ -23,19 +23,18 @@ import ( "context" "encoding/json" "fmt" - "io" "net/http" "net/url" - "os" "path" "strconv" - "sync" "time" datatx "github.com/cs3org/go-cs3apis/cs3/tx/v1beta1" typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/cs3org/reva/pkg/appctx" txdriver "github.com/cs3org/reva/pkg/datatx" + "github.com/cs3org/reva/pkg/datatx/manager/rclone/repository" + repoRegistry "github.com/cs3org/reva/pkg/datatx/manager/rclone/repository/registry" registry "github.com/cs3org/reva/pkg/datatx/manager/registry" "github.com/cs3org/reva/pkg/errtypes" "github.com/cs3org/reva/pkg/rhttp" @@ -50,9 +49,6 @@ func init() { func (c *config) init(m map[string]interface{}) { // set sane defaults - if c.File == "" { - c.File = "/var/tmp/reva/datatx-transfers.json" - } if c.JobStatusCheckInterval == 0 { c.JobStatusCheckInterval = 2000 } @@ -62,20 +58,22 @@ func (c *config) init(m map[string]interface{}) { } type config struct { - Endpoint string `mapstructure:"endpoint"` - AuthUser string `mapstructure:"auth_user"` // rclone basicauth user - AuthPass string `mapstructure:"auth_pass"` // rclone basicauth pass - AuthHeader string `mapstructure:"auth_header"` - File string `mapstructure:"file"` - JobStatusCheckInterval int `mapstructure:"job_status_check_interval"` - JobTimeout int `mapstructure:"job_timeout"` - Insecure bool `mapstructure:"insecure"` + Endpoint string `mapstructure:"endpoint"` + AuthUser string `mapstructure:"auth_user"` // rclone basicauth user + AuthPass string `mapstructure:"auth_pass"` // rclone basicauth pass + AuthHeader string `mapstructure:"auth_header"` + JobStatusCheckInterval int `mapstructure:"job_status_check_interval"` + JobTimeout int `mapstructure:"job_timeout"` + Insecure bool `mapstructure:"insecure"` + RemoveTransferJobOnCancel bool `mapstructure:"remove_transfer_job_on_cancel"` + StorageDriver string `mapstructure:"storagedriver"` + StorageDrivers map[string]map[string]interface{} `mapstructure:"storagedrivers"` } type rclone struct { config *config client *http.Client - pDriver *pDriver + storage repository.Repository } type rcloneHTTPErrorRes struct { @@ -85,30 +83,6 @@ type rcloneHTTPErrorRes struct { Status int `json:"status"` } -type transferModel struct { - File string - Transfers map[string]*transfer `json:"transfers"` -} - -// persistency driver. -type pDriver struct { - sync.Mutex // concurrent access to the file - model *transferModel -} - -type transfer struct { - TransferID string - JobID int64 - TransferStatus datatx.Status - SrcToken string - SrcRemote string - SrcPath string - DestToken string - DestRemote string - DestPath string - Ctime string -} - // txEndStatuses final statuses that cannot be changed anymore. var txEndStatuses = map[string]int32{ "STATUS_INVALID": 0, @@ -137,21 +111,15 @@ func New(m map[string]interface{}) (txdriver.Manager, error) { client := rhttp.GetHTTPClient(rhttp.Insecure(c.Insecure)) - // The persistency driver - // Load or create 'db' - model, err := loadOrCreate(c.File) + storage, err := getStorageManager(c) if err != nil { - err = errors.Wrap(err, "error loading the file containing the transfers") return nil, err } - pDriver := &pDriver{ - model: model, - } return &rclone{ config: c, client: client, - pDriver: pDriver, + storage: storage, }, nil } @@ -164,56 +132,11 @@ func parseConfig(m map[string]interface{}) (*config, error) { return c, nil } -func loadOrCreate(file string) (*transferModel, error) { - _, err := os.Stat(file) - if os.IsNotExist(err) { - if err := os.WriteFile(file, []byte("{}"), 0700); err != nil { - err = errors.Wrap(err, "error creating the transfers storage file: "+file) - return nil, err - } - } - - fd, err := os.OpenFile(file, os.O_CREATE, 0644) - if err != nil { - err = errors.Wrap(err, "error opening the transfers storage file: "+file) - return nil, err - } - defer fd.Close() - - data, err := io.ReadAll(fd) - if err != nil { - err = errors.Wrap(err, "error reading the data") - return nil, err - } - - model := &transferModel{} - if err := json.Unmarshal(data, model); err != nil { - err = errors.Wrap(err, "error decoding transfers data to json") - return nil, err - } - - if model.Transfers == nil { - model.Transfers = make(map[string]*transfer) - } - - model.File = file - return model, nil -} - -// saveTransfer saves the transfer. If an error is specified than that error will be returned, possibly wrapped with additional errors. -func (m *transferModel) saveTransfer(e error) error { - data, err := json.Marshal(m) - if err != nil { - e = errors.Wrap(err, "error encoding transfer data to json") - return e +func getStorageManager(c *config) (repository.Repository, error) { + if f, ok := repoRegistry.NewFuncs[c.StorageDriver]; ok { + return f(c.StorageDrivers[c.StorageDriver]) } - - if err := os.WriteFile(m.File, data, 0644); err != nil { - e = errors.Wrap(err, "error writing transfer data to file: "+m.File) - return e - } - - return e + return nil, errtypes.NotFound("rclone service: storage driver not found: " + c.StorageDriver) } // CreateTransfer creates a transfer job and returns a TxInfo object that includes a unique transfer id. @@ -243,50 +166,54 @@ func (driver *rclone) CreateTransfer(ctx context.Context, srcTargetURI string, d func (driver *rclone) startJob(ctx context.Context, transferID string, srcRemote string, srcPath string, srcToken string, destRemote string, destPath string, destToken string) (*datatx.TxInfo, error) { logger := appctx.GetLogger(ctx) - driver.pDriver.Lock() - defer driver.pDriver.Unlock() - var txID string var cTime *typespb.Timestamp if transferID == "" { txID = uuid.New().String() cTime = &typespb.Timestamp{Seconds: uint64(time.Now().Unix())} - } else { // restart existing transfer if transferID is specified - logger.Debug().Msgf("Restarting transfer (txID: %s)", transferID) + } else { // restart existing transfer job if transferID is specified + logger.Debug().Msgf("Restarting transfer job (txID: %s)", transferID) txID = transferID - transfer, err := driver.pDriver.model.getTransfer(txID) + job, err := driver.storage.GetJob(txID) if err != nil { - err = errors.Wrap(err, "rclone: error retrying transfer (transferID: "+txID+")") + err = errors.Wrap(err, "rclone: error retrying transfer job (transferID: "+txID+")") return &datatx.TxInfo{ Id: &datatx.TxId{OpaqueId: txID}, Status: datatx.Status_STATUS_INVALID, Ctime: nil, }, err } - seconds, _ := strconv.ParseInt(transfer.Ctime, 10, 64) + seconds, _ := strconv.ParseInt(job.Ctime, 10, 64) cTime = &typespb.Timestamp{Seconds: uint64(seconds)} - _, endStatusFound := txEndStatuses[transfer.TransferStatus.String()] + _, endStatusFound := txEndStatuses[job.TransferStatus.String()] if !endStatusFound { - err := errors.New("rclone: transfer still running, unable to restart") + err := errors.New("rclone: job still running, unable to restart") return &datatx.TxInfo{ Id: &datatx.TxId{OpaqueId: txID}, - Status: transfer.TransferStatus, + Status: job.TransferStatus, + Ctime: cTime, + }, err + } + srcToken = job.SrcToken + srcRemote = job.SrcRemote + srcPath = job.SrcPath + destToken = job.DestToken + destRemote = job.DestRemote + destPath = job.DestPath + if err := driver.storage.DeleteJob(job); err != nil { + err = errors.Wrap(err, "rclone: transfer still running, unable to restart") + return &datatx.TxInfo{ + Id: &datatx.TxId{OpaqueId: txID}, + Status: job.TransferStatus, Ctime: cTime, }, err } - srcToken = transfer.SrcToken - srcRemote = transfer.SrcRemote - srcPath = transfer.SrcPath - destToken = transfer.DestToken - destRemote = transfer.DestRemote - destPath = transfer.DestPath - delete(driver.pDriver.model.Transfers, txID) } transferStatus := datatx.Status_STATUS_TRANSFER_NEW - transfer := &transfer{ + job := &repository.Job{ TransferID: txID, JobID: int64(-1), TransferStatus: transferStatus, @@ -299,8 +226,6 @@ func (driver *rclone) startJob(ctx context.Context, transferID string, srcRemote Ctime: fmt.Sprint(cTime.Seconds), // TODO do we need nanos here? } - driver.pDriver.model.Transfers[txID] = transfer - type rcloneAsyncReqJSON struct { SrcFs string `json:"srcFs"` DstFs string `json:"dstFs"` @@ -324,70 +249,94 @@ func (driver *rclone) startJob(ctx context.Context, transferID string, srcRemote } data, err := json.Marshal(rcloneReq) if err != nil { - err = errors.Wrap(err, "rclone: error pulling transfer: error marshalling rclone req data") - transfer.TransferStatus = datatx.Status_STATUS_INVALID + err = errors.Wrap(err, "rclone: transfer job error: error marshalling rclone req data") + job.TransferStatus = datatx.Status_STATUS_INVALID + var e error + if e = driver.storage.StoreJob(job); e != nil { + e = errors.Wrap(e, err.Error()) + } return &datatx.TxInfo{ Id: &datatx.TxId{OpaqueId: txID}, - Status: datatx.Status_STATUS_INVALID, + Status: job.TransferStatus, Ctime: cTime, - }, driver.pDriver.model.saveTransfer(err) + }, e } transferFileMethod := "/sync/copy" remotePathIsFolder, err := driver.remotePathIsFolder(srcRemote, srcPath, srcToken) if err != nil { - err = errors.Wrap(err, "rclone: error pulling transfer: error stating src path") - transfer.TransferStatus = datatx.Status_STATUS_INVALID + err = errors.Wrap(err, "rclone: transfer job error: error stating src path") + job.TransferStatus = datatx.Status_STATUS_INVALID + var e error + if e = driver.storage.StoreJob(job); e != nil { + e = errors.Wrap(e, err.Error()) + } return &datatx.TxInfo{ Id: &datatx.TxId{OpaqueId: txID}, - Status: datatx.Status_STATUS_INVALID, + Status: job.TransferStatus, Ctime: cTime, - }, driver.pDriver.model.saveTransfer(err) + }, e } if !remotePathIsFolder { - err = errors.Wrap(err, "rclone: error pulling transfer: path is a file, only folder transfer is implemented") - transfer.TransferStatus = datatx.Status_STATUS_INVALID + err = errors.Wrap(err, "rclone: transfer job error: path is a file, only folder transfer is implemented") + job.TransferStatus = datatx.Status_STATUS_INVALID + var e error + if e = driver.storage.StoreJob(job); e != nil { + e = errors.Wrap(e, err.Error()) + } return &datatx.TxInfo{ Id: &datatx.TxId{OpaqueId: txID}, - Status: datatx.Status_STATUS_INVALID, + Status: job.TransferStatus, Ctime: cTime, - }, driver.pDriver.model.saveTransfer(err) + }, e } u, err := url.Parse(driver.config.Endpoint) if err != nil { - err = errors.Wrap(err, "rclone: error pulling transfer: error parsing driver endpoint") - transfer.TransferStatus = datatx.Status_STATUS_INVALID + err = errors.Wrap(err, "rclone: transfer job error: error parsing driver endpoint") + job.TransferStatus = datatx.Status_STATUS_INVALID + var e error + if e = driver.storage.StoreJob(job); e != nil { + e = errors.Wrap(e, err.Error()) + } return &datatx.TxInfo{ Id: &datatx.TxId{OpaqueId: txID}, - Status: datatx.Status_STATUS_INVALID, + Status: job.TransferStatus, Ctime: cTime, - }, driver.pDriver.model.saveTransfer(err) + }, e } u.Path = path.Join(u.Path, transferFileMethod) requestURL := u.String() req, err := http.NewRequest(http.MethodPost, requestURL, bytes.NewReader(data)) if err != nil { - err = errors.Wrap(err, "rclone: error pulling transfer: error framing post request") - transfer.TransferStatus = datatx.Status_STATUS_TRANSFER_FAILED + err = errors.Wrap(err, "rclone: transfer job error: error framing post request") + job.TransferStatus = datatx.Status_STATUS_TRANSFER_FAILED + var e error + if e = driver.storage.StoreJob(job); e != nil { + e = errors.Wrap(e, err.Error()) + } return &datatx.TxInfo{ Id: &datatx.TxId{OpaqueId: txID}, - Status: transfer.TransferStatus, + Status: job.TransferStatus, Ctime: cTime, - }, driver.pDriver.model.saveTransfer(err) + }, e } req.Header.Set("Content-Type", "application/json") req.SetBasicAuth(driver.config.AuthUser, driver.config.AuthPass) res, err := driver.client.Do(req) if err != nil { - err = errors.Wrap(err, "rclone: error pulling transfer: error sending post request") - transfer.TransferStatus = datatx.Status_STATUS_TRANSFER_FAILED + err = errors.Wrap(err, "rclone: transfer job error: error sending post request") + job.TransferStatus = datatx.Status_STATUS_TRANSFER_FAILED + var e error + if e = driver.storage.StoreJob(job); e != nil { + e = errors.Wrap(e, err.Error()) + } return &datatx.TxInfo{ Id: &datatx.TxId{OpaqueId: txID}, - Status: transfer.TransferStatus, + Status: job.TransferStatus, Ctime: cTime, - }, driver.pDriver.model.saveTransfer(err) + }, e } defer res.Body.Close() @@ -395,21 +344,29 @@ func (driver *rclone) startJob(ctx context.Context, transferID string, srcRemote if res.StatusCode != http.StatusOK { var errorResData rcloneHTTPErrorRes if err = json.NewDecoder(res.Body).Decode(&errorResData); err != nil { - err = errors.Wrap(err, "rclone driver: error decoding response data") - transfer.TransferStatus = datatx.Status_STATUS_TRANSFER_FAILED + err = errors.Wrap(err, "rclone driver: error decoding rclone response data") + job.TransferStatus = datatx.Status_STATUS_TRANSFER_FAILED + var e error + if e = driver.storage.StoreJob(job); e != nil { + e = errors.Wrap(e, err.Error()) + } return &datatx.TxInfo{ Id: &datatx.TxId{OpaqueId: txID}, - Status: transfer.TransferStatus, + Status: job.TransferStatus, Ctime: cTime, - }, driver.pDriver.model.saveTransfer(err) + }, e + } + err := errors.New("rclone driver: rclone request responded with error, " + fmt.Sprintf(" status: %v, error: %v", errorResData.Status, errorResData.Error)) + job.TransferStatus = datatx.Status_STATUS_TRANSFER_FAILED + var e error + if e = driver.storage.StoreJob(job); e != nil { + e = errors.Wrap(e, err.Error()) } - e := errors.New("rclone: rclone request responded with error, " + fmt.Sprintf(" status: %v, error: %v", errorResData.Status, errorResData.Error)) - transfer.TransferStatus = datatx.Status_STATUS_TRANSFER_FAILED return &datatx.TxInfo{ Id: &datatx.TxId{OpaqueId: txID}, - Status: transfer.TransferStatus, + Status: job.TransferStatus, Ctime: cTime, - }, driver.pDriver.model.saveTransfer(e) + }, e } type rcloneAsyncResJSON struct { @@ -417,19 +374,33 @@ func (driver *rclone) startJob(ctx context.Context, transferID string, srcRemote } var resData rcloneAsyncResJSON if err = json.NewDecoder(res.Body).Decode(&resData); err != nil { - err = errors.Wrap(err, "rclone: error decoding response data") - transfer.TransferStatus = datatx.Status_STATUS_TRANSFER_FAILED + err = errors.Wrap(err, "rclone driver: error decoding response data") + job.TransferStatus = datatx.Status_STATUS_TRANSFER_FAILED + var e error + if e = driver.storage.StoreJob(job); e != nil { + e = errors.Wrap(e, err.Error()) + } return &datatx.TxInfo{ Id: &datatx.TxId{OpaqueId: txID}, - Status: transfer.TransferStatus, + Status: job.TransferStatus, Ctime: cTime, - }, driver.pDriver.model.saveTransfer(err) + }, e } - transfer.JobID = resData.JobID + job.JobID = resData.JobID - if err := driver.pDriver.model.saveTransfer(nil); err != nil { - err = errors.Wrap(err, "rclone: error pulling transfer") + if err := driver.storage.StoreJob(job); err != nil { + err = errors.Wrap(err, "rclone driver: transfer job error") + return &datatx.TxInfo{ + Id: &datatx.TxId{OpaqueId: txID}, + Status: datatx.Status_STATUS_INVALID, + Ctime: cTime, + }, err + } + + // the initial save when everything went ok + if err := driver.storage.StoreJob(job); err != nil { + err = errors.Wrap(err, "rclone driver: error starting transfer job") return &datatx.TxInfo{ Id: &datatx.TxId{OpaqueId: txID}, Status: datatx.Status_STATUS_INVALID, @@ -443,22 +414,17 @@ func (driver *rclone) startJob(ctx context.Context, transferID string, srcRemote startTimeMs := time.Now().Nanosecond() / 1000 timeout := driver.config.JobTimeout - driver.pDriver.Lock() - defer driver.pDriver.Unlock() - for { - transfer, err := driver.pDriver.model.getTransfer(txID) + job, err := driver.storage.GetJob(txID) if err != nil { - transfer.TransferStatus = datatx.Status_STATUS_INVALID - err = driver.pDriver.model.saveTransfer(err) - logger.Error().Err(err).Msgf("rclone driver: unable to retrieve transfer with id: %v", txID) + logger.Error().Err(err).Msgf("rclone driver: unable to retrieve transfer job with id: %v", txID) break } // check for end status first - _, endStatusFound := txEndStatuses[transfer.TransferStatus.String()] - if endStatusFound { - logger.Info().Msgf("rclone driver: transfer endstatus reached: %v", transfer.TransferStatus) + _, endStatusreached := txEndStatuses[job.TransferStatus.String()] + if endStatusreached { + logger.Info().Msgf("rclone driver: transfer job endstatus reached: %v", job.TransferStatus) break } @@ -467,16 +433,16 @@ func (driver *rclone) startJob(ctx context.Context, transferID string, srcRemote timePastMs := currentTimeMs - startTimeMs if timePastMs > timeout { - logger.Info().Msgf("rclone driver: transfer timed out: %vms (timeout = %v)", timePastMs, timeout) + logger.Info().Msgf("rclone driver: transfer job timed out: %vms (timeout = %v)", timePastMs, timeout) // set status to EXPIRED and save - transfer.TransferStatus = datatx.Status_STATUS_TRANSFER_EXPIRED - if err := driver.pDriver.model.saveTransfer(nil); err != nil { - logger.Error().Err(err).Msgf("rclone driver: save transfer failed: %v", err) + job.TransferStatus = datatx.Status_STATUS_TRANSFER_EXPIRED + if err := driver.storage.StoreJob(job); err != nil { + logger.Error().Err(err).Msgf("rclone driver: save transfer job failed: %v", err) } break } - jobID := transfer.JobID + jobID := job.JobID type rcloneStatusReqJSON struct { JobID int64 `json:"jobid"` } @@ -487,9 +453,9 @@ func (driver *rclone) startJob(ctx context.Context, transferID string, srcRemote data, err := json.Marshal(rcloneStatusReq) if err != nil { logger.Error().Err(err).Msgf("rclone driver: marshalling request failed: %v", err) - transfer.TransferStatus = datatx.Status_STATUS_INVALID - if err := driver.pDriver.model.saveTransfer(nil); err != nil { - logger.Error().Err(err).Msgf("rclone driver: save transfer failed: %v", err) + job.TransferStatus = datatx.Status_STATUS_INVALID + if err := driver.storage.StoreJob(job); err != nil { + logger.Error().Err(err).Msgf("rclone driver: save transfer job failed: %v", err) } break } @@ -499,9 +465,9 @@ func (driver *rclone) startJob(ctx context.Context, transferID string, srcRemote u, err := url.Parse(driver.config.Endpoint) if err != nil { logger.Error().Err(err).Msgf("rclone driver: could not parse driver endpoint: %v", err) - transfer.TransferStatus = datatx.Status_STATUS_INVALID - if err := driver.pDriver.model.saveTransfer(nil); err != nil { - logger.Error().Err(err).Msgf("rclone driver: save transfer failed: %v", err) + job.TransferStatus = datatx.Status_STATUS_INVALID + if err := driver.storage.StoreJob(job); err != nil { + logger.Error().Err(err).Msgf("rclone driver: save transfer job failed: %v", err) } break } @@ -511,9 +477,9 @@ func (driver *rclone) startJob(ctx context.Context, transferID string, srcRemote req, err := http.NewRequest(http.MethodPost, requestURL, bytes.NewReader(data)) if err != nil { logger.Error().Err(err).Msgf("rclone driver: error framing post request: %v", err) - transfer.TransferStatus = datatx.Status_STATUS_INVALID - if err := driver.pDriver.model.saveTransfer(nil); err != nil { - logger.Error().Err(err).Msgf("rclone driver: save transfer failed: %v", err) + job.TransferStatus = datatx.Status_STATUS_INVALID + if err := driver.storage.StoreJob(job); err != nil { + logger.Error().Err(err).Msgf("rclone driver: save transfer job failed: %v", err) } break } @@ -522,9 +488,9 @@ func (driver *rclone) startJob(ctx context.Context, transferID string, srcRemote res, err := driver.client.Do(req) if err != nil { logger.Error().Err(err).Msgf("rclone driver: error sending post request: %v", err) - transfer.TransferStatus = datatx.Status_STATUS_INVALID - if err := driver.pDriver.model.saveTransfer(nil); err != nil { - logger.Error().Err(err).Msgf("rclone driver: save transfer failed: %v", err) + job.TransferStatus = datatx.Status_STATUS_INVALID + if err := driver.storage.StoreJob(job); err != nil { + logger.Error().Err(err).Msgf("rclone driver: save transfer job failed: %v", err) } break } @@ -538,9 +504,9 @@ func (driver *rclone) startJob(ctx context.Context, transferID string, srcRemote logger.Error().Err(err).Msgf("rclone driver: error reading response body: %v", err) } logger.Error().Err(err).Msgf("rclone driver: rclone request responded with error, status: %v, error: %v", errorResData.Status, errorResData.Error) - transfer.TransferStatus = datatx.Status_STATUS_INVALID - if err := driver.pDriver.model.saveTransfer(nil); err != nil { - logger.Error().Err(err).Msgf("rclone driver: save transfer failed: %v", err) + job.TransferStatus = datatx.Status_STATUS_INVALID + if err := driver.storage.StoreJob(job); err != nil { + logger.Error().Err(err).Msgf("rclone driver: save transfer job failed: %v", err) } break } @@ -565,9 +531,9 @@ func (driver *rclone) startJob(ctx context.Context, transferID string, srcRemote if resData.Error != "" { logger.Error().Err(err).Msgf("rclone driver: rclone responded with error: %v", resData.Error) - transfer.TransferStatus = datatx.Status_STATUS_TRANSFER_FAILED - if err := driver.pDriver.model.saveTransfer(nil); err != nil { - logger.Error().Err(err).Msgf("rclone driver: error saving transfer: %v", err) + job.TransferStatus = datatx.Status_STATUS_TRANSFER_FAILED + if err := driver.storage.StoreJob(job); err != nil { + logger.Error().Err(err).Msgf("rclone driver: error saving transfer job: %v", err) break } break @@ -576,9 +542,9 @@ func (driver *rclone) startJob(ctx context.Context, transferID string, srcRemote // transfer complete if resData.Finished && resData.Success { logger.Info().Msg("rclone driver: transfer job finished") - transfer.TransferStatus = datatx.Status_STATUS_TRANSFER_COMPLETE - if err := driver.pDriver.model.saveTransfer(nil); err != nil { - logger.Error().Err(err).Msgf("rclone driver: error saving transfer: %v", err) + job.TransferStatus = datatx.Status_STATUS_TRANSFER_COMPLETE + if err := driver.storage.StoreJob(job); err != nil { + logger.Error().Err(err).Msgf("rclone driver: error saving transfer job: %v", err) break } break @@ -587,9 +553,9 @@ func (driver *rclone) startJob(ctx context.Context, transferID string, srcRemote // transfer completed unsuccessfully without error if resData.Finished && !resData.Success { logger.Info().Msgf("rclone driver: transfer job failed") - transfer.TransferStatus = datatx.Status_STATUS_TRANSFER_FAILED - if err := driver.pDriver.model.saveTransfer(nil); err != nil { - logger.Error().Err(err).Msgf("rclone driver: error saving transfer: %v", err) + job.TransferStatus = datatx.Status_STATUS_TRANSFER_FAILED + if err := driver.storage.StoreJob(job); err != nil { + logger.Error().Err(err).Msgf("rclone driver: error saving transfer job: %v", err) break } break @@ -598,9 +564,9 @@ func (driver *rclone) startJob(ctx context.Context, transferID string, srcRemote // transfer not yet finished: continue if !resData.Finished { logger.Info().Msgf("rclone driver: transfer job in progress") - transfer.TransferStatus = datatx.Status_STATUS_TRANSFER_IN_PROGRESS - if err := driver.pDriver.model.saveTransfer(nil); err != nil { - logger.Error().Err(err).Msgf("rclone driver: error saving transfer: %v", err) + job.TransferStatus = datatx.Status_STATUS_TRANSFER_IN_PROGRESS + if err := driver.storage.StoreJob(job); err != nil { + logger.Error().Err(err).Msgf("rclone driver: error saving transfer job: %v", err) break } } @@ -618,7 +584,7 @@ func (driver *rclone) startJob(ctx context.Context, transferID string, srcRemote // GetTransferStatus returns the status of the transfer with the specified job id. func (driver *rclone) GetTransferStatus(ctx context.Context, transferID string) (*datatx.TxInfo, error) { - transfer, err := driver.pDriver.model.getTransfer(transferID) + job, err := driver.storage.GetJob(transferID) if err != nil { return &datatx.TxInfo{ Id: &datatx.TxId{OpaqueId: transferID}, @@ -626,17 +592,17 @@ func (driver *rclone) GetTransferStatus(ctx context.Context, transferID string) Ctime: nil, }, err } - cTime, _ := strconv.ParseInt(transfer.Ctime, 10, 64) + cTime, _ := strconv.ParseInt(job.Ctime, 10, 64) return &datatx.TxInfo{ Id: &datatx.TxId{OpaqueId: transferID}, - Status: transfer.TransferStatus, + Status: job.TransferStatus, Ctime: &typespb.Timestamp{Seconds: uint64(cTime)}, }, nil } // CancelTransfer cancels the transfer with the specified transfer id. func (driver *rclone) CancelTransfer(ctx context.Context, transferID string) (*datatx.TxInfo, error) { - transfer, err := driver.pDriver.model.getTransfer(transferID) + job, err := driver.storage.GetJob(transferID) if err != nil { return &datatx.TxInfo{ Id: &datatx.TxId{OpaqueId: transferID}, @@ -644,10 +610,24 @@ func (driver *rclone) CancelTransfer(ctx context.Context, transferID string) (*d Ctime: nil, }, err } - cTime, _ := strconv.ParseInt(transfer.Ctime, 10, 64) - _, endStatusFound := txEndStatuses[transfer.TransferStatus.String()] + + cTime, _ := strconv.ParseInt(job.Ctime, 10, 64) + // rclone cancel may fail so remove job from model first to be sure + transferRemovedMessage := "" + if driver.config.RemoveTransferJobOnCancel { + if err := driver.storage.DeleteJob(job); err != nil { + return &datatx.TxInfo{ + Id: &datatx.TxId{OpaqueId: transferID}, + Status: datatx.Status_STATUS_INVALID, + Ctime: &typespb.Timestamp{Seconds: uint64(cTime)}, + }, err + } + transferRemovedMessage = "(transfer job successfully removed)" + } + + _, endStatusFound := txEndStatuses[job.TransferStatus.String()] if endStatusFound { - err := errors.New("rclone driver: transfer already in end state") + err := errors.Wrapf(errors.New("rclone driver: job already in end state"), transferRemovedMessage) return &datatx.TxInfo{ Id: &datatx.TxId{OpaqueId: transferID}, Status: datatx.Status_STATUS_INVALID, @@ -660,12 +640,12 @@ func (driver *rclone) CancelTransfer(ctx context.Context, transferID string) (*d JobID int64 `json:"jobid"` } rcloneCancelTransferReq := &rcloneStopRequest{ - JobID: transfer.JobID, + JobID: job.JobID, } data, err := json.Marshal(rcloneCancelTransferReq) if err != nil { - err = errors.Wrap(err, "rclone driver: error marshalling rclone req data") + err := errors.Wrapf(errors.New("rclone driver: error marshalling rclone job/stop req data"), transferRemovedMessage) return &datatx.TxInfo{ Id: &datatx.TxId{OpaqueId: transferID}, Status: datatx.Status_STATUS_INVALID, @@ -677,7 +657,7 @@ func (driver *rclone) CancelTransfer(ctx context.Context, transferID string) (*d u, err := url.Parse(driver.config.Endpoint) if err != nil { - err = errors.Wrap(err, "rclone driver: error parsing driver endpoint") + err := errors.Wrapf(errors.New("rclone driver: error parsing driver endpoint"), transferRemovedMessage) return &datatx.TxInfo{ Id: &datatx.TxId{OpaqueId: transferID}, Status: datatx.Status_STATUS_INVALID, @@ -689,7 +669,7 @@ func (driver *rclone) CancelTransfer(ctx context.Context, transferID string) (*d req, err := http.NewRequest(http.MethodPost, requestURL, bytes.NewReader(data)) if err != nil { - err = errors.Wrap(err, "rclone driver: error framing post request") + err := errors.Wrapf(errors.New("rclone driver: error framing post request"), transferRemovedMessage) return &datatx.TxInfo{ Id: &datatx.TxId{OpaqueId: transferID}, Status: datatx.Status_STATUS_INVALID, @@ -702,7 +682,7 @@ func (driver *rclone) CancelTransfer(ctx context.Context, transferID string) (*d res, err := driver.client.Do(req) if err != nil { - err = errors.Wrap(err, "rclone driver: error sending post request") + err := errors.Wrapf(errors.New("rclone driver: error sending post request"), transferRemovedMessage) return &datatx.TxInfo{ Id: &datatx.TxId{OpaqueId: transferID}, Status: datatx.Status_STATUS_INVALID, @@ -715,14 +695,14 @@ func (driver *rclone) CancelTransfer(ctx context.Context, transferID string) (*d if res.StatusCode != http.StatusOK { var errorResData rcloneHTTPErrorRes if err = json.NewDecoder(res.Body).Decode(&errorResData); err != nil { - err = errors.Wrap(err, "rclone driver: error decoding response data") + err := errors.Wrapf(errors.New("rclone driver: error decoding response data"), transferRemovedMessage) return &datatx.TxInfo{ Id: &datatx.TxId{OpaqueId: transferID}, Status: datatx.Status_STATUS_INVALID, Ctime: &typespb.Timestamp{Seconds: uint64(cTime)}, }, err } - err = errors.Wrap(errors.Errorf("status: %v, error: %v", errorResData.Status, errorResData.Error), "rclone driver: rclone request responded with error") + err = errors.Wrap(errors.Errorf("%v, status: %v, error: %v", transferRemovedMessage, errorResData.Status, errorResData.Error), "rclone driver: rclone request responded with error") return &datatx.TxInfo{ Id: &datatx.TxId{OpaqueId: transferID}, Status: datatx.Status_STATUS_INVALID, @@ -744,7 +724,7 @@ func (driver *rclone) CancelTransfer(ctx context.Context, transferID string) (*d } var resData rcloneCancelTransferResJSON if err = json.NewDecoder(res.Body).Decode(&resData); err != nil { - err = errors.Wrap(err, "rclone driver: error decoding response data") + err := errors.Wrapf(errors.New("rclone driver: error decoding response data"), transferRemovedMessage) return &datatx.TxInfo{ Id: &datatx.TxId{OpaqueId: transferID}, Status: datatx.Status_STATUS_INVALID, @@ -760,13 +740,16 @@ func (driver *rclone) CancelTransfer(ctx context.Context, transferID string) (*d }, errors.New(resData.Error) } - transfer.TransferStatus = datatx.Status_STATUS_TRANSFER_CANCELLED - if err := driver.pDriver.model.saveTransfer(nil); err != nil { - return &datatx.TxInfo{ - Id: &datatx.TxId{OpaqueId: transferID}, - Status: datatx.Status_STATUS_INVALID, - Ctime: &typespb.Timestamp{Seconds: uint64(cTime)}, - }, err + // only update when job's not removed + if !driver.config.RemoveTransferJobOnCancel { + job.TransferStatus = datatx.Status_STATUS_TRANSFER_CANCELLED + if err := driver.storage.StoreJob(job); err != nil { + return &datatx.TxInfo{ + Id: &datatx.TxId{OpaqueId: transferID}, + Status: datatx.Status_STATUS_INVALID, + Ctime: &typespb.Timestamp{Seconds: uint64(cTime)}, + }, err + } } return &datatx.TxInfo{ @@ -782,15 +765,6 @@ func (driver *rclone) RetryTransfer(ctx context.Context, transferID string) (*da return driver.startJob(ctx, transferID, "", "", "", "", "", "") } -// getTransfer returns the transfer with the specified transfer ID. -func (m *transferModel) getTransfer(transferID string) (*transfer, error) { - transfer, ok := m.Transfers[transferID] - if !ok { - return nil, errors.New("rclone driver: invalid transfer ID") - } - return transfer, nil -} - func (driver *rclone) remotePathIsFolder(remote string, remotePath string, remoteToken string) (bool, error) { type rcloneListReqJSON struct { Fs string `json:"fs"` diff --git a/pkg/datatx/manager/rclone/repository/json/json.go b/pkg/datatx/manager/rclone/repository/json/json.go new file mode 100644 index 0000000000..2d31f70ff1 --- /dev/null +++ b/pkg/datatx/manager/rclone/repository/json/json.go @@ -0,0 +1,171 @@ +// Copyright 2018-2023 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 json + +import ( + "encoding/json" + "io" + "os" + "sync" + + "github.com/cs3org/reva/pkg/datatx/manager/rclone/repository" + "github.com/cs3org/reva/pkg/datatx/manager/rclone/repository/registry" + "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" +) + +func init() { + registry.Register("json", New) +} + +type config struct { + File string `mapstructure:"file"` +} + +type mgr struct { + config *config + sync.Mutex // concurrent access to the file + model *rcloneJobsModel +} + +type rcloneJobsModel struct { + RcloneJobs map[string]*repository.Job `json:"rcloneJobs"` +} + +func parseConfig(m map[string]interface{}) (*config, error) { + c := &config{} + if err := mapstructure.Decode(m, c); err != nil { + err = errors.Wrap(err, "rclone repository json driver: error decoding configuration") + return nil, err + } + return c, nil +} + +func (c *config) init() { + if c.File == "" { + c.File = "/var/tmp/reva/transfer-jobs.json" + } +} + +// New returns a json storage driver. +func New(m map[string]interface{}) (repository.Repository, error) { + c, err := parseConfig(m) + if err != nil { + return nil, err + } + c.init() + + model, err := loadOrCreate(c.File) + if err != nil { + err = errors.Wrap(err, "rclone repository json driver: error loading the file containing the transfer shares") + return nil, err + } + + mgr := &mgr{ + config: c, + model: model, + } + + return mgr, nil +} + +func (m *mgr) StoreJob(job *repository.Job) error { + m.Lock() + defer m.Unlock() + + m.model.RcloneJobs[job.TransferID] = job + err := m.saveModel() + if err != nil { + return errors.Wrap(err, "error storing jobs") + } + + return nil +} + +func (m *mgr) GetJob(transferID string) (*repository.Job, error) { + m.Lock() + defer m.Unlock() + + job, ok := m.model.RcloneJobs[transferID] + if !ok { + return nil, errors.New("rclone repository json driver: error getting job: not found") + } + return job, nil +} + +func (m *mgr) DeleteJob(job *repository.Job) error { + m.Lock() + defer m.Unlock() + + delete(m.model.RcloneJobs, job.TransferID) + if err := m.saveModel(); err != nil { + return errors.New("rclone repository json driver: error deleting job: error updating model") + } + return nil +} + +func (m *mgr) saveModel() error { + data, err := json.Marshal(m.model) + if err != nil { + err = errors.Wrap(err, "rclone repository json driver: error encoding job data to json") + return err + } + + if err := os.WriteFile(m.config.File, data, 0644); err != nil { + err = errors.Wrap(err, "rclone repository json driver: error writing job data to file: "+m.config.File) + return err + } + + return nil +} + +func loadOrCreate(file string) (*rcloneJobsModel, error) { + _, err := os.Stat(file) + if os.IsNotExist(err) { + if err := os.WriteFile(file, []byte("{}"), 0700); err != nil { + err = errors.Wrap(err, "rclone repository json driver: error creating the jobs storage file: "+file) + return nil, err + } + } + + fd, err := os.OpenFile(file, os.O_CREATE, 0644) + if err != nil { + err = errors.Wrap(err, "rclone repository json driver: error opening the jobs storage file: "+file) + return nil, err + } + defer fd.Close() + + data, err := io.ReadAll(fd) + if err != nil { + err = errors.Wrap(err, "rclone repository json driver: error reading the data") + return nil, err + } + + model := &rcloneJobsModel{} + if err := json.Unmarshal(data, model); err != nil { + err = errors.Wrap(err, "rclone repository json driver: error decoding jobs data to json") + return nil, err + } + + if model.RcloneJobs == nil { + model.RcloneJobs = make(map[string]*repository.Job) + } + + return model, nil +} diff --git a/pkg/datatx/manager/rclone/repository/registry/registry.go b/pkg/datatx/manager/rclone/repository/registry/registry.go new file mode 100644 index 0000000000..113586f3ad --- /dev/null +++ b/pkg/datatx/manager/rclone/repository/registry/registry.go @@ -0,0 +1,36 @@ +// Copyright 2018-2023 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 registry + +import ( + "github.com/cs3org/reva/pkg/datatx/manager/rclone/repository" +) + +// NewFunc is the function that rclone repository implementations +// should register at init time. +type NewFunc func(map[string]interface{}) (repository.Repository, error) + +// NewFuncs is a map containing all the registered datatx backends. +var NewFuncs = map[string]NewFunc{} + +// Register registers a new datatx backend new function. +// Not safe for concurrent use. Safe for use from package init. +func Register(name string, f NewFunc) { + NewFuncs[name] = f +} diff --git a/pkg/datatx/manager/rclone/repository/repository.go b/pkg/datatx/manager/rclone/repository/repository.go new file mode 100644 index 0000000000..fd2bcbe927 --- /dev/null +++ b/pkg/datatx/manager/rclone/repository/repository.go @@ -0,0 +1,44 @@ +// Copyright 2018-2023 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 repository + +import ( + datatx "github.com/cs3org/go-cs3apis/cs3/tx/v1beta1" +) + +// Job represents transfer job. +type Job struct { + TransferID string + JobID int64 + TransferStatus datatx.Status + SrcToken string + SrcRemote string + SrcPath string + DestToken string + DestRemote string + DestPath string + Ctime string +} + +// Repository the interface that any storage driver should implement. +type Repository interface { + StoreJob(job *Job) error + GetJob(transferID string) (*Job, error) + DeleteJob(job *Job) error +} diff --git a/pkg/datatx/repository/json/json.go b/pkg/datatx/repository/json/json.go new file mode 100644 index 0000000000..7fcf18cb78 --- /dev/null +++ b/pkg/datatx/repository/json/json.go @@ -0,0 +1,211 @@ +// Copyright 2018-2023 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 json + +import ( + "encoding/json" + "io" + "os" + "sync" + + userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + txv1beta "github.com/cs3org/go-cs3apis/cs3/tx/v1beta1" + "github.com/cs3org/reva/pkg/datatx" + "github.com/cs3org/reva/pkg/datatx/repository/registry" + "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" +) + +func init() { + registry.Register("json", New) +} + +type config struct { + File string `mapstructure:"file"` +} + +type mgr struct { + config *config + sync.Mutex // concurrent access to the file + model *transfersModel +} + +type transfersModel struct { + Transfers map[string]*datatx.Transfer `json:"transfers"` +} + +func parseConfig(m map[string]interface{}) (*config, error) { + c := &config{} + if err := mapstructure.Decode(m, c); err != nil { + err = errors.Wrap(err, "datatx repository json driver: error decoding configuration") + return nil, err + } + return c, nil +} + +func (c *config) init() { + if c.File == "" { + c.File = "/var/tmp/reva/datatx-transfers.json" + } +} + +// New returns a json storage driver. +func New(m map[string]interface{}) (datatx.Repository, error) { + c, err := parseConfig(m) + if err != nil { + return nil, err + } + c.init() + + model, err := loadOrCreate(c.File) + if err != nil { + err = errors.Wrap(err, "datatx repository json driver: error loading the file containing the transfer shares") + return nil, err + } + + mgr := &mgr{ + config: c, + model: model, + } + + return mgr, nil +} + +func (m *mgr) StoreTransfer(transfer *datatx.Transfer) error { + m.Lock() + defer m.Unlock() + + m.model.Transfers[transfer.TxID] = transfer + err := m.saveModel() + if err != nil { + return errors.Wrap(err, "error storing transfer") + } + + return nil +} + +func (m *mgr) DeleteTransfer(transfer *datatx.Transfer) error { + m.Lock() + defer m.Unlock() + + delete(m.model.Transfers, transfer.TxID) + if err := m.saveModel(); err != nil { + return errors.New("datatx repository json driver: error deleting transfer: error updating model") + } + return nil +} + +func (m *mgr) GetTransfer(txID string) (*datatx.Transfer, error) { + m.Lock() + defer m.Unlock() + + transfer, ok := m.model.Transfers[txID] + if !ok { + return nil, errors.New("datatx repository json driver: error getting transfer: not found") + } + return transfer, nil +} + +func (m *mgr) ListTransfers(filters []*txv1beta.ListTransfersRequest_Filter, userID *userv1beta1.UserId) ([]*datatx.Transfer, error) { + m.Lock() + defer m.Unlock() + + var transfers []*datatx.Transfer + if userID == nil { + return transfers, errors.New("datatx repository json driver: error listing transfers, userID must be provided") + } + for _, transfer := range m.model.Transfers { + if transfer.UserID.OpaqueId == userID.OpaqueId { + if len(filters) == 0 { + transfers = append(transfers, &datatx.Transfer{ + TxID: transfer.TxID, + SrcTargetURI: transfer.SrcTargetURI, + DestTargetURI: transfer.DestTargetURI, + ShareID: transfer.ShareID, + UserID: transfer.UserID, + }) + } else { + for _, f := range filters { + if f.Type == txv1beta.ListTransfersRequest_Filter_TYPE_SHARE_ID { + if f.GetShareId().GetOpaqueId() == transfer.ShareID { + transfers = append(transfers, &datatx.Transfer{ + TxID: transfer.TxID, + SrcTargetURI: transfer.SrcTargetURI, + DestTargetURI: transfer.DestTargetURI, + ShareID: transfer.ShareID, + UserID: transfer.UserID, + }) + } + } + } + } + } + } + return transfers, nil +} + +func (m *mgr) saveModel() error { + data, err := json.Marshal(m.model) + if err != nil { + err = errors.Wrap(err, "datatx repository json driver: error encoding transfer data to json") + return err + } + + if err := os.WriteFile(m.config.File, data, 0644); err != nil { + err = errors.Wrap(err, "datatx repository json driver: error writing transfer data to file: "+m.config.File) + return err + } + + return nil +} + +func loadOrCreate(file string) (*transfersModel, error) { + _, err := os.Stat(file) + if os.IsNotExist(err) { + if err := os.WriteFile(file, []byte("{}"), 0700); err != nil { + err = errors.Wrap(err, "datatx repository json driver: error creating the datatx shares storage file: "+file) + return nil, err + } + } + + fd, err := os.OpenFile(file, os.O_CREATE, 0644) + if err != nil { + err = errors.Wrap(err, "datatx repository json driver: error opening the datatx shares storage file: "+file) + return nil, err + } + defer fd.Close() + + data, err := io.ReadAll(fd) + if err != nil { + err = errors.Wrap(err, "datatx repository json driver: error reading the data") + return nil, err + } + + model := &transfersModel{} + if err := json.Unmarshal(data, model); err != nil { + err = errors.Wrap(err, "datatx repository json driver: error decoding datatx shares data to json") + return nil, err + } + + if model.Transfers == nil { + model.Transfers = make(map[string]*datatx.Transfer) + } + + return model, nil +} diff --git a/pkg/datatx/repository/registry/registry.go b/pkg/datatx/repository/registry/registry.go new file mode 100644 index 0000000000..66dceb7d6a --- /dev/null +++ b/pkg/datatx/repository/registry/registry.go @@ -0,0 +1,36 @@ +// Copyright 2018-2023 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 registry + +import ( + "github.com/cs3org/reva/pkg/datatx" +) + +// NewFunc is the function that datatx repository implementations +// should register at init time. +type NewFunc func(map[string]interface{}) (datatx.Repository, error) + +// NewFuncs is a map containing all the registered datatx repository backends. +var NewFuncs = map[string]NewFunc{} + +// Register registers a new datatx repository backend new function. +// Not safe for concurrent use. Safe for use from package init. +func Register(name string, f NewFunc) { + NewFuncs[name] = f +} diff --git a/pkg/notification/db_changes.sql b/pkg/notification/db_changes.sql new file mode 100644 index 0000000000..7b7be6ad03 --- /dev/null +++ b/pkg/notification/db_changes.sql @@ -0,0 +1,54 @@ +-- Copyright 2018-2023 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. + +-- This file can be used to make the required changes to the MySQL DB. This is +-- not a proper migration but it should work on most situations. + +USE cernboxngcopy; + +CREATE TABLE `cbox_notifications` ( + `id` INT PRIMARY KEY AUTO_INCREMENT, + `ref` VARCHAR(3072) UNIQUE NOT NULL, + `template_name` VARCHAR(320) NOT NULL +); + +COMMIT; + +CREATE TABLE `cbox_notification_recipients` ( + `id` INT PRIMARY KEY AUTO_INCREMENT, + `notification_id` INT NOT NULL, + `recipient` VARCHAR(320) NOT NULL, + FOREIGN KEY (notification_id) + REFERENCES cbox_notifications (id) + ON DELETE CASCADE +); + +COMMIT; + +CREATE INDEX `cbox_notifications_ix0` ON `cbox_notifications` (`ref`); + +CREATE INDEX `cbox_notification_recipients_ix0` ON `cbox_notification_recipients` (`notification_id`); +CREATE INDEX `cbox_notification_recipients_ix1` ON `cbox_notification_recipients` (`user_name`); + +-- changes for added notifications on ocm shares + +ALTER TABLE cernboxngcopy.oc_share ADD notify_uploads BOOL DEFAULT false; + +UPDATE cernboxngcopy.oc_share SET notify_uploads = false; + +ALTER TABLE cernboxngcopy.oc_share MODIFY notify_uploads BOOL DEFAULT false NOT NULL; diff --git a/pkg/notification/db_sqlite.sql b/pkg/notification/db_sqlite.sql new file mode 100644 index 0000000000..8e110fe153 --- /dev/null +++ b/pkg/notification/db_sqlite.sql @@ -0,0 +1,51 @@ +-- Copyright 2018-2023 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. + +-- This file can be used to quickstart a SQLite DB for running the tests in +-- ./manager/sql/sql_test.go + +CREATE TABLE `cbox_notifications` ( + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `ref` VARCHAR(3072) UNIQUE NOT NULL, + `template_name` VARCHAR(320) NOT NULL +); + +COMMIT; + +CREATE TABLE `cbox_notification_recipients` ( + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `notification_id` INTEGER NOT NULL, + `recipient` VARCHAR(320) NOT NULL, + FOREIGN KEY (notification_id) + REFERENCES cbox_notifications (id) + ON DELETE CASCADE +); + +COMMIT; + +CREATE INDEX `cbox_notifications_ix0` ON `cbox_notifications` (`ref`); + +CREATE INDEX `cbox_notification_recipients_ix0` ON `cbox_notification_recipients` (`notification_id`); +CREATE INDEX `cbox_notification_recipients_ix1` ON `cbox_notification_recipients` (`recipient`); + +COMMIT; + +INSERT INTO `cbox_notifications` (`id`, `ref`, `template_name`) VALUES (1, "notification-test", "notification-template-test"); +INSERT INTO `cbox_notification_recipients` (`id`, `notification_id`, `recipient`) VALUES (1, 1, "jdoe"), (2, 1, "testuser"); + +COMMIT; diff --git a/pkg/notification/handler/emailhandler/emailhandler.go b/pkg/notification/handler/emailhandler/emailhandler.go new file mode 100644 index 0000000000..47152f51c8 --- /dev/null +++ b/pkg/notification/handler/emailhandler/emailhandler.go @@ -0,0 +1,116 @@ +// Copyright 2018-2023 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 emailhandler + +import ( + "fmt" + "net/smtp" + "regexp" + "strings" + + "github.com/cs3org/reva/pkg/notification/handler" + "github.com/cs3org/reva/pkg/notification/handler/registry" + "github.com/mitchellh/mapstructure" + "github.com/rs/zerolog" +) + +func init() { + registry.Register("email", New) +} + +// EmailHandler is the notification handler for emails. +type EmailHandler struct { + conf *config + Log *zerolog.Logger +} + +type config struct { + SMTPAddress string `mapstructure:"smtp_server" docs:";The hostname and port of the SMTP server."` + SenderLogin string `mapstructure:"sender_login" docs:";The email to be used to send mails."` + SenderPassword string `mapstructure:"sender_password" docs:";The sender's password."` + DisableAuth bool `mapstructure:"disable_auth" docs:"false;Whether to disable SMTP auth."` + DefaultSender string `mapstructure:"default_sender" docs:"no-reply@cernbox.cern.ch;Default sender when not specified in the trigger."` +} + +func defaultConfig() *config { + return &config{ + DefaultSender: "no-reply@cernbox.cern.ch", + } +} + +// New returns a new email handler. +func New(log *zerolog.Logger, conf interface{}) (handler.Handler, error) { + c := defaultConfig() + if err := mapstructure.Decode(conf, c); err != nil { + return nil, err + } + + return &EmailHandler{ + conf: c, + Log: log, + }, nil +} + +// Send is the method run when a notification is triggered for this handler. +func (e *EmailHandler) Send(sender, recipient, subject, body string) error { + if sender == "" { + sender = e.conf.DefaultSender + } + + msg := e.generateMsg(sender, recipient, subject, body) + err := smtp.SendMail(e.conf.SMTPAddress, e.getAuth(), sender, []string{recipient}, msg) + if err != nil { + return err + } + + e.Log.Debug().Msgf("mail sent to recipient %s", recipient) + + return nil +} + +func (e *EmailHandler) getAuth() smtp.Auth { + if e.conf.DisableAuth { + return nil + } + + return smtp.PlainAuth("", e.conf.SenderLogin, e.conf.SenderPassword, strings.SplitN(e.conf.SMTPAddress, ":", 2)[0]) +} + +func (e *EmailHandler) generateMsg(from, to, subject, body string) []byte { + re := regexp.MustCompile(`\r?\n`) + cleanSubject := re.ReplaceAllString(strings.TrimSpace(subject), " ") + headers := []string{ + fmt.Sprintf("From: %s", from), + fmt.Sprintf("To: %s", to), + fmt.Sprintf("Subject: %s", cleanSubject), + "MIME-version: 1.0;", + "Content-Type: text/html; charset=\"UTF-8\";", + } + + var sb strings.Builder + + for _, h := range headers { + sb.WriteString(h) + sb.WriteString("\r\n") + } + sb.WriteString("\r\n") + sb.WriteString(body) + + return []byte(sb.String()) +} diff --git a/pkg/notification/handler/handler.go b/pkg/notification/handler/handler.go new file mode 100644 index 0000000000..a42e5cd047 --- /dev/null +++ b/pkg/notification/handler/handler.go @@ -0,0 +1,24 @@ +// Copyright 2018-2023 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 handler + +// Handler is the interface notification handlers have to implement. +type Handler interface { + Send(sender, recipient, subject, body string) error +} diff --git a/pkg/notification/handler/loader/loader.go b/pkg/notification/handler/loader/loader.go new file mode 100644 index 0000000000..3aa73e6077 --- /dev/null +++ b/pkg/notification/handler/loader/loader.go @@ -0,0 +1,25 @@ +// Copyright 2018-2023 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 loader + +import ( + // Load notification handlers. + _ "github.com/cs3org/reva/pkg/notification/handler/emailhandler" + // Add your own here. +) diff --git a/pkg/notification/handler/registry/registry.go b/pkg/notification/handler/registry/registry.go new file mode 100644 index 0000000000..e4a9271eab --- /dev/null +++ b/pkg/notification/handler/registry/registry.go @@ -0,0 +1,60 @@ +// Copyright 2018-2023 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 registry + +import ( + "github.com/cs3org/reva/pkg/notification/handler" + "github.com/rs/zerolog" +) + +// NewHandlerFunc is the function that notification handlers should register to +// at init time. +type NewHandlerFunc func(Log *zerolog.Logger, conf interface{}) (handler.Handler, error) + +// NewHandlerFuncs is a map containing all the registered notification handlers. +var NewHandlerFuncs = map[string]NewHandlerFunc{} + +// Register registers a new notification handler new function. Not safe for +// concurrent use. Safe for use from package init. +func Register(name string, f NewHandlerFunc) { + NewHandlerFuncs[name] = f +} + +// InitHandlers initializes the notification handlers with the configuration +// and the log from a service. +func InitHandlers(handlerConf map[string]interface{}, log *zerolog.Logger) map[string]handler.Handler { + handlers := make(map[string]handler.Handler) + hCount := 0 + + for n, f := range NewHandlerFuncs { + if c, ok := handlerConf[n]; ok { + nh, err := f(log, c) + if err != nil { + log.Err(err).Msgf("error initializing notification handler %s", n) + } + handlers[n] = nh + hCount++ + } else { + log.Warn().Msgf("missing config for notification handler %s", n) + } + } + log.Info().Msgf("%d handlers initialized", hCount) + + return handlers +} diff --git a/pkg/notification/manager/loader/loader.go b/pkg/notification/manager/loader/loader.go new file mode 100644 index 0000000000..af580197b0 --- /dev/null +++ b/pkg/notification/manager/loader/loader.go @@ -0,0 +1,25 @@ +// Copyright 2018-2023 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 loader + +import ( + // Load core notification manager drivers. + _ "github.com/cs3org/reva/pkg/notification/manager/sql" + // Add your own here. +) diff --git a/pkg/notification/manager/registry/registry.go b/pkg/notification/manager/registry/registry.go new file mode 100644 index 0000000000..4ae551145a --- /dev/null +++ b/pkg/notification/manager/registry/registry.go @@ -0,0 +1,36 @@ +// Copyright 2018-2023 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 registry + +import "github.com/cs3org/reva/pkg/notification" + +// import "github.com/cs3org/reva/pkg/share" + +// NewFunc is the function that notification managers +// should register at init time. +type NewFunc func(map[string]interface{}) (notification.Manager, error) + +// NewFuncs is a map containing all the registered notification managers. +var NewFuncs = map[string]NewFunc{} + +// Register registers a new notification manager new function. +// Not safe for concurrent use. Safe for use from package init. +func Register(name string, f NewFunc) { + NewFuncs[name] = f +} diff --git a/pkg/notification/manager/sql/sql.go b/pkg/notification/manager/sql/sql.go new file mode 100644 index 0000000000..1e71dad7ab --- /dev/null +++ b/pkg/notification/manager/sql/sql.go @@ -0,0 +1,199 @@ +// Copyright 2018-2023 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 sql + +import ( + "database/sql" + "fmt" + + "github.com/cs3org/reva/pkg/notification" + "github.com/cs3org/reva/pkg/notification/manager/registry" + "github.com/mitchellh/mapstructure" +) + +func init() { + registry.Register("sql", NewMysql) +} + +type config struct { + DBUsername string `mapstructure:"db_username"` + DBPassword string `mapstructure:"db_password"` + DBHost string `mapstructure:"db_host"` + DBPort int `mapstructure:"db_port"` + DBName string `mapstructure:"db_name"` + GatewaySvc string `mapstructure:"gatewaysvc"` +} + +type mgr struct { + driver string + db *sql.DB +} + +// NewMysql returns an instance of the sql notifications manager. +func NewMysql(m map[string]interface{}) (notification.Manager, error) { + c := &config{} + if err := mapstructure.Decode(m, c); err != nil { + return nil, err + } + + db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", c.DBUsername, c.DBPassword, c.DBHost, c.DBPort, c.DBName)) + if err != nil { + return nil, err + } + + return New("mysql", db) +} + +// New returns a new Notifications driver connecting to the given sql.DB. +func New(driver string, db *sql.DB) (notification.Manager, error) { + return &mgr{ + driver: driver, + db: db, + }, nil +} + +// UpsertNotification creates or updates a notification. +func (m *mgr) UpsertNotification(n notification.Notification) error { + if err := n.CheckNotification(); err != nil { + return err + } + + tx, err := m.db.Begin() + if err != nil { + return err + } + + // Create/update notification + stmt, err := m.db.Prepare("REPLACE INTO cbox_notifications (ref, template_name) VALUES (?, ?)") + if err != nil { + return err + } + defer stmt.Close() + + result, err := stmt.Exec(n.Ref, n.TemplateName) + if err != nil { + _ = tx.Rollback() + return err + } + + // Create/update recipients for the notification + notificationID, err := result.LastInsertId() + if err != nil { + _ = tx.Rollback() + return err + } + + stmt, err = tx.Prepare("REPLACE INTO cbox_notification_recipients (notification_id, recipient) VALUES (?, ?)") + if err != nil { + _ = tx.Rollback() + return err + } + defer stmt.Close() + + for _, recipient := range n.Recipients { + _, err := stmt.Exec(notificationID, recipient) + if err != nil { + _ = tx.Rollback() + return err + } + } + + err = tx.Commit() + if err != nil { + return err + } + + return nil +} + +// GetNotification reads a notification. +func (m *mgr) GetNotification(ref string) (*notification.Notification, error) { + query := ` + SELECT n.id, n.ref, n.template_name, nr.recipient + FROM cbox_notifications AS n + JOIN cbox_notification_recipients AS nr ON n.id = nr.notification_id + WHERE n.ref = ? + ` + + rows, err := m.db.Query(query, ref) + if err != nil { + return nil, err + } + defer rows.Close() + + var n notification.Notification + count := 0 + n.Recipients = make([]string, 0) + + for rows.Next() { + var id string + var recipient string + err := rows.Scan(&id, &n.Ref, &n.TemplateName, &recipient) + if err != nil { + return nil, err + } + n.Recipients = append(n.Recipients, recipient) + count++ + } + if err = rows.Err(); err != nil { + return nil, err + } + if count == 0 { + return nil, ¬ification.NotFoundError{ + Ref: n.Ref, + } + } + + return &n, nil +} + +// DeleteNotification deletes a notification. +func (m *mgr) DeleteNotification(ref string) error { + tx, err := m.db.Begin() + if err != nil { + return err + } + + // Delete notification + stmt, err := m.db.Prepare("DELETE FROM cbox_notifications WHERE ref = ?") + if err != nil { + return err + } + defer stmt.Close() + + result, err := stmt.Exec(ref) + if err != nil { + _ = tx.Rollback() + return err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + _ = tx.Rollback() + return err + } + + if rowsAffected == 0 { + return ¬ification.NotFoundError{ + Ref: ref, + } + } + + return nil +} diff --git a/internal/http/services/ocmd/notifications.go b/pkg/notification/manager/sql/sql_suite_test.go similarity index 70% rename from internal/http/services/ocmd/notifications.go rename to pkg/notification/manager/sql/sql_suite_test.go index 1e3f591298..e3c6fbd8e9 100644 --- a/internal/http/services/ocmd/notifications.go +++ b/pkg/notification/manager/sql/sql_suite_test.go @@ -16,19 +16,16 @@ // granted to it by virtue of its status as an Intergovernmental Organization // or submit itself to any jurisdiction. -package ocmd +package sql_test import ( - "net/http" -) - -type notificationsHandler struct { -} + "testing" -func (h *notificationsHandler) init(c *config) { -} + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) -// SendNotification is used to let the provider know that a user has removed a share. -func (h *notificationsHandler) SendNotification(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNotImplemented) +func TestSql(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Sql Suite") } diff --git a/pkg/notification/manager/sql/sql_test.go b/pkg/notification/manager/sql/sql_test.go new file mode 100644 index 0000000000..60080ffab0 --- /dev/null +++ b/pkg/notification/manager/sql/sql_test.go @@ -0,0 +1,252 @@ +// Copyright 2018-2023 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 sql_test + +import ( + "database/sql" + "fmt" + "os" + + "github.com/cs3org/reva/pkg/notification" + sqlmanager "github.com/cs3org/reva/pkg/notification/manager/sql" + _ "github.com/mattn/go-sqlite3" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("SQL manager for notifications", func() { + var ( + db *sql.DB + testDBFile *os.File + mgr notification.Manager + n1 = ¬ification.Notification{ + Ref: "notification-test", + TemplateName: "notification-template-test", + Recipients: []string{"jdoe", "testuser"}, + } + n2 = ¬ification.Notification{ + Ref: "new-notification", + TemplateName: "new-template", + Recipients: []string{"newuser1", "newuser2"}, + } + nn *notification.Notification + ref string + err error + selectNotificationsSQL = "SELECT ref, template_name FROM cbox_notifications WHERE ref = ?" + selectNotificationRecipientsSQL = "SELECT COUNT(*) FROM cbox_notification_recipients WHERE notification_id = ?" + ) + + AfterEach(func() { + os.Remove(testDBFile.Name()) + }) + + BeforeEach(func() { + var err error + ref = "notification-test" + + testDBFile, err = os.CreateTemp("", "testdbfile") + Expect(err).ToNot(HaveOccurred()) + + dbData, err := os.ReadFile("test.sqlite") + Expect(err).ToNot(HaveOccurred()) + + _, err = testDBFile.Write(dbData) + Expect(err).ToNot(HaveOccurred()) + + err = testDBFile.Close() + Expect(err).ToNot(HaveOccurred()) + + db, err = sql.Open("sqlite3", fmt.Sprintf("%v?_foreign_keys=on", testDBFile.Name())) + Expect(err).ToNot(HaveOccurred()) + Expect(db).ToNot(BeNil()) + + mgr, err = sqlmanager.New("sqlite3", db) + Expect(err).ToNot(HaveOccurred()) + }) + + AfterEach(func() { + os.Remove(testDBFile.Name()) + }) + + Context("Creating notifications", func() { + When("creating a non-existing notification", func() { + JustBeforeEach(func() { + err = mgr.UpsertNotification(*n2) + }) + + It("should not return an error", func() { + Expect(err).ToNot(HaveOccurred()) + }) + + It("should create a notification entry", func() { + var newRef, newTemplateName string + err = db.QueryRow(selectNotificationsSQL, n2.Ref).Scan(&newRef, &newTemplateName) + Expect(newRef).To(Equal(n2.Ref)) + Expect(newTemplateName).To(Equal(n2.TemplateName)) + }) + + It("should create notification recipients entries", func() { + var notificationID int + err = db.QueryRow("SELECT id FROM cbox_notifications WHERE ref = ?", n2.Ref).Scan(¬ificationID) + Expect(err).ToNot(HaveOccurred()) + var newRecipientCount int + err = db.QueryRow(selectNotificationRecipientsSQL, notificationID).Scan(&newRecipientCount) + Expect(err).ToNot(HaveOccurred()) + Expect(newRecipientCount).To(Equal(len(n2.Recipients))) + }) + }) + + When("updating an existing notification", func() { + var m = ¬ification.Notification{ + Ref: "notification-test", + TemplateName: "new-notification-template-test", + Recipients: []string{"jdoe", "testuser2", "thirduser"}, + } + + JustBeforeEach(func() { + err = mgr.UpsertNotification(*m) + }) + + It("should not return an error", func() { + Expect(err).ToNot(HaveOccurred()) + }) + + It("should not increase the number of entries in the notification table", func() { + var count int + err = db.QueryRow("SELECT COUNT(*) FROM cbox_notifications").Scan(&count) + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(Equal(1)) + }) + + It("should update the existing notification data", func() { + var newRef, newTemplateName string + err = db.QueryRow(selectNotificationsSQL, m.Ref).Scan(&newRef, &newTemplateName) + Expect(newRef).To(Equal(m.Ref)) + Expect(newTemplateName).To(Equal(m.TemplateName)) + }) + + It("should delete old entries in notification recipients", func() { + var count int + err = db.QueryRow("SELECT COUNT(*) FROM cbox_notification_recipients WHERE recipient = 'testuser'").Scan(&count) + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(BeZero()) + }) + + It("should create new entries in notification recipients", func() { + var notificationID int + err = db.QueryRow("SELECT id FROM cbox_notifications WHERE ref = ?", m.Ref).Scan(¬ificationID) + Expect(err).ToNot(HaveOccurred()) + var newRecipientCount int + err = db.QueryRow(selectNotificationRecipientsSQL, notificationID).Scan(&newRecipientCount) + Expect(err).ToNot(HaveOccurred()) + Expect(newRecipientCount).To(Equal(len(m.Recipients))) + }) + }) + + When("creating an invalid notification", func() { + o := ¬ification.Notification{} + + JustBeforeEach(func() { + err = mgr.UpsertNotification(*o) + }) + + It("should return an InvalidNotificationError", func() { + _, isInvalidNotificationError := err.(*notification.InvalidNotificationError) + Expect(err).To(HaveOccurred()) + Expect(isInvalidNotificationError).To(BeTrue()) + }) + }) + }) + + Context("Getting notifications", func() { + When("getting an existing notification", func() { + JustBeforeEach(func() { + nn, err = mgr.GetNotification(ref) + }) + + It("should not return an error", func() { + Expect(err).ToNot(HaveOccurred()) + }) + + It("should return a notification", func() { + Expect(nn.Ref).To(Equal(n1.Ref)) + Expect(nn.TemplateName).To(Equal(n1.TemplateName)) + Expect(nn.Recipients).To(Equal(n1.Recipients)) + }) + }) + + When("getting a non-existing notification", func() { + JustBeforeEach(func() { + nn, err = mgr.GetNotification("non-existent-ref") + }) + + It("should return a NotFoundError", func() { + _, isNotFoundError := err.(*notification.NotFoundError) + Expect(err).To(HaveOccurred()) + Expect(isNotFoundError).To(BeTrue()) + }) + }) + }) + + Context("Deleting notifications", func() { + When("deleting an existing notification", func() { + JustBeforeEach(func() { + err = mgr.DeleteNotification(ref) + + }) + + It("should not return an error", func() { + Expect(err).ToNot(HaveOccurred()) + }) + + It("should delete the notification from the database", func() { + var count int + err = db.QueryRow("SELECT COUNT(*) FROM cbox_notifications WHERE ref = ?", ref).Scan(&count) + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(BeZero()) + }) + + It("should cascade the deletions to notification_recipients table", func() { + var count int + err = db.QueryRow("SELECT COUNT(*) FROM cbox_notification_recipients WHERE notification_id = ?", 1).Scan(&count) + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(BeZero()) + }) + }) + + When("deleting a non-existing notification", func() { + JustBeforeEach(func() { + err = mgr.DeleteNotification("non-existent-ref") + + }) + + It("should not change the db and return a NotFoundError error", func() { + Expect(err).To(HaveOccurred()) + isNotFoundError, _ := err.(*notification.NotFoundError) + Expect(isNotFoundError).ToNot(BeNil()) + var count int + err = db.QueryRow("SELECT COUNT(*) FROM cbox_notifications").Scan(&count) + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(Equal(1)) + }) + }) + + }) + +}) diff --git a/pkg/notification/manager/sql/test.sqlite b/pkg/notification/manager/sql/test.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..7bf9289cdda218362bae3993b3e552879705b4f0 GIT binary patch literal 32768 zcmeI*Pfyxl90%~HEmDeqx@0q)Or9f+PC*Z5mt`G3IwBUWFlTn42qozvptL4tmj##X zh3pk9*^Ah5*S!R{-8T}f?rs_0CXsrx>m*wW|e^Lsvh+TmJ!pL_V=}$KXx3O@FQ_#rtG_9W=8eI*ESj{s)mvJ7I(8iD##KU14XR)Mqfk>< z3-t_}Q?;~%$5A=KnDF+ESKivT2L&`}-B2gkg8nqeE)|O_7O$J{gaTV)CS5hNE4sN! zm)R!Oi-mmF;9FZ^Mq!F)?2oux%E%X}QOxE1Os9K*KG)66Gu=$i&89TklV>q+vh0!( zRFdr5s1{cglPxln85y=tLk)Fl(y98HcyCx>ImSCsMqkh93oNd+%Mm$|P74S1^8wxP z*2GJ&T;8eTw3DtX)y?0sYu2ekYJOIuMI*abWIY<)%${A}*)Dr_sZp-mJ;i$Q;@YYd zO-xP-n?VcR-EBT3ExGpYp53U}eT$JZZR=Z^^lO>oB{lVklmfMve?{%JEGHhNg?8fn zqtHK1gpWNp+KK(5lS5GKg5%8JXS^HMEG;t>O>CqE5>(v*DDQdAV7V0T4W;l2%a!O!x~40uX=z1Rwwb2tWV=5P$##AOL~eAuuCE$c)~5_35E!yI$W1$CqS+ zHF4f9LH$nn3kd=cfB*y_009U<00Izz00bZaf!ir!L!$mr4{t~V z5e5Vx009U<00Izz00bZa0SG_<0@oB65hFyI@Sg?PbM01)KPI4zzp6HE-2Z<~P!xjz z1Rwwb2tWV=5P$##AOHaf+)ROU@BjD5|DyVnsK59d5(FRs0SG_<0uX=z1Rwwb2tWV= zcS#^3DS{|TigG;u=fD4V6#N2UlsD3!{QnOi-lYbkEf9bJ1Rwwb2tWV=5P$##AOL~? UL|{ak5KbNd5M^mx@$dit4QC@5C;$Ke literal 0 HcmV?d00001 diff --git a/pkg/notification/notification.go b/pkg/notification/notification.go new file mode 100644 index 0000000000..fdf615f369 --- /dev/null +++ b/pkg/notification/notification.go @@ -0,0 +1,114 @@ +// Copyright 2018-2023 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 notification + +import ( + "fmt" + + "github.com/cs3org/reva/pkg/notification/template" +) + +// Notification is the representation of a notification. +type Notification struct { + TemplateName string + Template template.Template + Ref string + Recipients []string +} + +// Manager is the interface notification storage managers have to implement. +type Manager interface { + // UpsertNotification insert or updates a notification. + UpsertNotification(n Notification) error + // GetNotification reads a notification. + GetNotification(ref string) (*Notification, error) + // DeleteNotification deletes a notifcation. + DeleteNotification(ref string) error +} + +// NotFoundError is the error returned when a notification does not exist. +type NotFoundError struct { + Ref string +} + +// InvalidNotificationError is the error returned when a notification has invalid data. +type InvalidNotificationError struct { + Ref string + Msg string + Err error +} + +// Error returns the string error msg for NotFoundError. +func (n *NotFoundError) Error() string { + return fmt.Sprintf("notification %s not found", n.Ref) +} + +// Error returns the string error msg for InvalidNotificationError. +func (i *InvalidNotificationError) Error() string { + return i.Msg +} + +// Send is the method run when a notification is triggered. +func (n *Notification) Send(sender string, templateData map[string]interface{}) error { + subject, err := n.Template.RenderSubject(templateData) + if err != nil { + return err + } + + body, err := n.Template.RenderBody(templateData) + if err != nil { + return err + } + + for _, recipient := range n.Recipients { + err := n.Template.Handler.Send(sender, recipient, subject, body) + if err != nil { + return err + } + } + + return nil +} + +// CheckNotification checks if a notification has correct data. +func (n *Notification) CheckNotification() error { + if len(n.Ref) == 0 { + return &InvalidNotificationError{ + Ref: n.Ref, + Msg: "empty ref", + } + } + + if err := template.CheckTemplateName(n.TemplateName); err != nil { + return &InvalidNotificationError{ + Ref: n.Ref, + Msg: fmt.Sprintf("invalid template name %s", n.TemplateName), + Err: err, + } + } + + if len(n.Recipients) == 0 { + return &InvalidNotificationError{ + Ref: n.Ref, + Msg: "empty recipient list", + } + } + + return nil +} diff --git a/pkg/notification/notificationhelper/notificationhelper.go b/pkg/notification/notificationhelper/notificationhelper.go new file mode 100644 index 0000000000..af92e2f4d1 --- /dev/null +++ b/pkg/notification/notificationhelper/notificationhelper.go @@ -0,0 +1,250 @@ +// Copyright 2018-2023 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 notificationhelper + +import ( + "encoding/json" + "fmt" + + "github.com/cs3org/reva/pkg/notification" + "github.com/cs3org/reva/pkg/notification/template" + "github.com/cs3org/reva/pkg/notification/trigger" + "github.com/cs3org/reva/pkg/notification/utils" + "github.com/mitchellh/mapstructure" + "github.com/nats-io/nats.go" + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +// NotificationHelper is the type used in services to work with notifications. +type NotificationHelper struct { + Name string + Conf *Config + Log *zerolog.Logger + nc *nats.Conn + js nats.JetStreamContext + kv nats.KeyValue +} + +// Config contains the configuration for the Notification Helper. +type Config struct { + NatsAddress string `mapstructure:"nats_address" docs:";The NATS server address."` + NatsToken string `mapstructure:"nats_token" docs:";The token to authenticate against the NATS server"` + NatsStream string `mapstructure:"nats_stream" docs:"reva-notifications;The notifications NATS stream."` + Templates map[string]interface{} `mapstructure:"templates" docs:";Notification templates for the service."` +} + +func defaultConfig() *Config { + return &Config{ + NatsStream: "reva-notifications", + } +} + +// New creates a new Notification Helper. +func New(name string, m map[string]interface{}, log *zerolog.Logger) *NotificationHelper { + conf := defaultConfig() + nh := &NotificationHelper{ + Name: name, + Conf: conf, + Log: log, + } + + if err := mapstructure.Decode(m, conf); err != nil { + log.Error().Err(err).Msgf("decoding config failed, notifications will be disabled") + return nh + } + + annotatedLogger := log.With().Str("service", nh.Name).Str("scope", "notifications").Logger() + nh.Log = &annotatedLogger + + if err := nh.connect(); err != nil { + log.Error().Err(err).Msgf("connecting to nats failed, notifications will be disabled") + return nh + } + + nh.registerTemplates(nh.Conf.Templates) + + return nh +} + +func (nh *NotificationHelper) connect() error { + nc, err := utils.ConnectToNats(nh.Conf.NatsAddress, nh.Conf.NatsToken, *nh.Log) + if err != nil { + return err + } + nh.nc = nc + + js, err := nh.nc.JetStream(nats.PublishAsyncMaxPending(256)) + if err != nil { + return errors.Wrap(err, "jetstream initialization failed") + } + stream, _ := js.StreamInfo(nh.Conf.NatsStream) + if stream != nil { + if _, err := js.AddStream(&nats.StreamConfig{ + Name: nh.Conf.NatsStream, + Subjects: []string{ + fmt.Sprintf("%s.notification", nh.Conf.NatsStream), + fmt.Sprintf("%s.trigger", nh.Conf.NatsStream), + }, + }); err != nil { + return errors.Wrap(err, "nats stream creation failed") + } + } + nh.js = js + + bucketName := fmt.Sprintf("%s-template", nh.Conf.NatsStream) + kv, err := nh.js.CreateKeyValue(&nats.KeyValueConfig{ + Bucket: bucketName, + }) + if err != nil { + return errors.Wrap(err, "template store creation failed, probably because nats server is unreachable") + } + nh.kv = kv + return nil +} + +// Stop stops the notification helper. +func (nh *NotificationHelper) Stop() { + if err := nh.nc.Drain(); err != nil { + nh.Log.Error().Err(err) + } +} + +func (nh *NotificationHelper) registerTemplates(ts map[string]interface{}) { + if len(ts) == 0 { + nh.Log.Info().Msg("no templates to register") + return + } + + tCount := 0 + for tn, tm := range ts { + var tc template.RegistrationRequest + if err := mapstructure.Decode(tm, &tc); err != nil { + nh.Log.Error().Err(err).Msgf("template '%s' definition decoding failed", tn) + continue + } + if err := template.CheckTemplateName(tc.Name); err != nil { + nh.Log.Error().Err(err).Msgf("template name '%s' is incorrect", tc.Name) + continue + } + if tc.Handler == "" { + nh.Log.Error().Msgf("template definition '%s' is missing handler field", tn) + continue + } + if tc.BodyTmplPath == "" { + nh.Log.Error().Msgf("template definition '%s' is missing body_template_path field", tn) + continue + } + + nh.registerTemplate(&tc) + tCount++ + } + + nh.Log.Info().Msgf("%d templates to register", tCount) +} + +func (nh *NotificationHelper) registerTemplate(rr *template.RegistrationRequest) { + if nh.kv == nil { + nh.Log.Info().Msgf("template registration skipped, helper is misconfigured") + return + } + + tb, err := json.Marshal(rr) + if err != nil { + nh.Log.Error().Err(err).Msgf("template registration json marshalling failed") + } + + go func() { + _, err := nh.kv.Put(rr.Name, tb) + if err != nil { + nh.Log.Error().Err(err).Msgf("template registration publish failed") + return + } + nh.Log.Debug().Msgf("%s template registration published", rr.Name) + }() +} + +// RegisterNotification registers a notification in the notification service. +func (nh *NotificationHelper) RegisterNotification(n *notification.Notification) { + if nh.js == nil { + nh.Log.Info().Msgf("notification registration skipped, helper is misconfigured") + return + } + + nb, err := json.Marshal(n) + if err != nil { + nh.Log.Error().Err(err).Msgf("notification registration json marshalling failed") + return + } + + notificationSubject := fmt.Sprintf("%s.notification-register", nh.Conf.NatsStream) + + go func() { + _, err := nh.js.Publish(notificationSubject, nb) + if err != nil { + nh.Log.Error().Err(err).Msgf("notification registration publish failed") + return + } + nh.Log.Debug().Msgf("%s notification registration published", n.Ref) + }() +} + +// UnregisterNotification unregisters a notification in the notification service. +func (nh *NotificationHelper) UnregisterNotification(ref string) { + if nh.js == nil { + nh.Log.Info().Msgf("notification unregistration skipped, notification helper is misconfigured") + return + } + + notificationSubject := fmt.Sprintf("%s.notification-unregister", nh.Conf.NatsStream) + + go func() { + _, err := nh.js.Publish(notificationSubject, []byte(ref)) + if err != nil { + nh.Log.Error().Err(err).Msgf("notification unregistration publish failed") + return + } + nh.Log.Debug().Msgf("%s notification unregistration published", ref) + }() +} + +// TriggerNotification sends a notification trigger to the notifications service. +func (nh *NotificationHelper) TriggerNotification(tr *trigger.Trigger) { + if nh.js == nil { + nh.Log.Info().Msgf("notification trigger skipped, notification helper is misconfigured") + return + } + + trb, err := json.Marshal(tr) + if err != nil { + nh.Log.Error().Err(err).Msgf("notification trigger json marshalling failed") + return + } + + triggerSubject := fmt.Sprintf("%s.trigger", nh.Conf.NatsStream) + + go func() { + _, err := nh.js.Publish(triggerSubject, trb) + if err != nil { + nh.Log.Error().Err(err).Msgf("notification trigger publish failed") + return + } + nh.Log.Debug().Msgf("%s notification trigger published", tr.Ref) + }() +} diff --git a/pkg/notification/template/registry/registry.go b/pkg/notification/template/registry/registry.go new file mode 100644 index 0000000000..bb13fe6a8f --- /dev/null +++ b/pkg/notification/template/registry/registry.go @@ -0,0 +1,69 @@ +// Copyright 2018-2023 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 registry + +import ( + "encoding/json" + "fmt" + + "github.com/cs3org/reva/pkg/notification/handler" + "github.com/cs3org/reva/pkg/notification/template" + "github.com/pkg/errors" +) + +// Registry provides with means for dynamically registering notification templates. +type Registry struct { + store map[string]template.Template +} + +// New returns a new Template Registry. +func New() *Registry { + r := &Registry{ + store: make(map[string]template.Template), + } + + return r +} + +// Put registers a handler in the registry. +func (r *Registry) Put(tb []byte, hs map[string]handler.Handler) (string, error) { + var data map[string]interface{} + + err := json.Unmarshal(tb, &data) + if err != nil { + return "", errors.Wrapf(err, "template registration unmarshall failed") + } + + t, name, err := template.New(data, hs) + if err != nil { + return name, errors.Wrapf(err, "template %s registration failed", name) + } + + r.store[t.Name] = *t + return t.Name, nil +} + +// Get retrieves a handler from the registry. +func (r *Registry) Get(n string) (*template.Template, error) { + if t, ok := r.store[n]; ok { + return &t, nil + } + + return nil, fmt.Errorf("template %s not found", n) +} diff --git a/pkg/notification/template/template.go b/pkg/notification/template/template.go new file mode 100644 index 0000000000..39d5fc0c18 --- /dev/null +++ b/pkg/notification/template/template.go @@ -0,0 +1,170 @@ +// Copyright 2018-2023 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 template + +import ( + "bytes" + "errors" + "fmt" + htmlTemplate "html/template" + "io" + "os" + "path/filepath" + "regexp" + textTemplate "text/template" + + "github.com/cs3org/reva/pkg/notification/handler" + "github.com/mitchellh/mapstructure" +) + +const validTemplateNameRegex = "[a-zA-Z0-9-]" + +// RegistrationRequest represents a Template registration request. +type RegistrationRequest struct { + Name string `mapstructure:"name" json:"name"` + Handler string `mapstructure:"handler" json:"handler"` + BodyTmplPath string `mapstructure:"body_template_path" json:"body_template_path"` + SubjectTmplPath string `mapstructure:"subject_template_path" json:"subject_template_path"` + Persistent bool `mapstructure:"persistent" json:"persistent"` +} + +// Template represents a notification template. +type Template struct { + Name string + Handler handler.Handler + Persistent bool + tmplSubject *textTemplate.Template + tmplBody *htmlTemplate.Template +} + +// FileNotFoundError is the error returned when a template file is missing. +type FileNotFoundError struct { + TemplateFileName string + Err error +} + +// Error returns the string error msg for FileNotFoundError. +func (t *FileNotFoundError) Error() string { + return fmt.Sprintf("template file %s not found", t.TemplateFileName) +} + +// New creates a new Template from a RegistrationRequest. +func New(m map[string]interface{}, hs map[string]handler.Handler) (*Template, string, error) { + rr := &RegistrationRequest{} + if err := mapstructure.Decode(m, rr); err != nil { + return nil, rr.Name, err + } + + h, ok := hs[rr.Handler] + if !ok { + return nil, rr.Name, fmt.Errorf("unknown handler %s", rr.Handler) + } + + tmplSubject, err := parseTmplFile(rr.SubjectTmplPath, "subject") + if err != nil { + return nil, rr.Name, err + } + + tmplBody, err := parseTmplFile(rr.BodyTmplPath, "body") + if err != nil { + return nil, rr.Name, err + } + + t := &Template{ + Name: rr.Name, + Handler: h, + tmplSubject: tmplSubject.(*textTemplate.Template), + tmplBody: tmplBody.(*htmlTemplate.Template), + } + + if err := CheckTemplateName(t.Name); err != nil { + return nil, rr.Name, err + } + + return t, rr.Name, nil +} + +// RenderSubject renders the subject template. +func (t *Template) RenderSubject(arguments map[string]interface{}) (string, error) { + var buf bytes.Buffer + err := t.tmplSubject.Execute(&buf, arguments) + return buf.String(), err +} + +// RenderBody renders the body template. +func (t *Template) RenderBody(arguments map[string]interface{}) (string, error) { + var buf bytes.Buffer + err := t.tmplBody.Execute(&buf, arguments) + return buf.String(), err +} + +// CheckTemplateName validates the name of the template. +func CheckTemplateName(name string) error { + if name == "" { + return errors.New("template name cannot be empty") + } + + re := regexp.MustCompile(validTemplateNameRegex) + invalidChars := re.ReplaceAllString(name, "") + if len(invalidChars) > 0 { + return fmt.Errorf("template name %s must contain only %s", name, validTemplateNameRegex) + } + + return nil +} + +func parseTmplFile(path, name string) (interface{}, error) { + if path == "" { + return textTemplate.New(name).Parse("") + } + + ext := filepath.Ext(path) + f, err := os.Open(path) + if err != nil { + return nil, &FileNotFoundError{ + TemplateFileName: path, + Err: err, + } + } + defer f.Close() + + data, err := io.ReadAll(f) + if err != nil { + return nil, err + } + + switch ext { + case ".txt": + tmpl, err := textTemplate.New(name).Parse(string(data)) + if err != nil { + return nil, err + } + + return tmpl, nil + case ".html": + tmpl, err := htmlTemplate.New(name).Parse(string(data)) + if err != nil { + return nil, err + } + + return tmpl, nil + default: + return nil, errors.New("unknown template type") + } +} diff --git a/pkg/notification/trigger/trigger.go b/pkg/notification/trigger/trigger.go new file mode 100644 index 0000000000..0fc60885c1 --- /dev/null +++ b/pkg/notification/trigger/trigger.go @@ -0,0 +1,41 @@ +// Copyright 2018-2023 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 trigger + +import ( + "github.com/cs3org/reva/pkg/notification" +) + +// Trigger represents a notification Trigger. +type Trigger struct { + Notification *notification.Notification + Ref string + Sender string + TemplateData map[string]interface{} +} + +// Send is the method run when a notification is triggered. +func (t *Trigger) Send() error { + err := t.Notification.Send(t.Sender, t.TemplateData) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/notification/utils/nats.go b/pkg/notification/utils/nats.go new file mode 100644 index 0000000000..cf206f46e8 --- /dev/null +++ b/pkg/notification/utils/nats.go @@ -0,0 +1,63 @@ +// Copyright 2018-2023 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 utils contains utilities related to the notifications service and helper. +package utils + +import ( + "time" + + "github.com/nats-io/nats.go" + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +// ConnectToNats returns a resilient connection to the specified NATS server. +func ConnectToNats(natsAddress, natsToken string, log zerolog.Logger) (*nats.Conn, error) { + nc, err := nats.Connect( + natsAddress, + nats.DrainTimeout(9*time.Second), // reva timeout on graceful shutdown is 10 seconds + nats.MaxReconnects(-1), + nats.Token(natsToken), + nats.ErrorHandler(func(c *nats.Conn, s *nats.Subscription, err error) { + log.Error().Err(err).Msgf("nats error") + }), + nats.ClosedHandler(func(c *nats.Conn) { + log.Error().Err(c.LastError()).Msgf("connection to nats server closed") + }), + nats.DisconnectErrHandler(func(_ *nats.Conn, err error) { + log.Error().Err(err).Msgf("connection to nats server disconnected") + }), + nats.CustomReconnectDelay(func(attempts int) time.Duration { + if attempts%3 == 0 { + log.Info().Msg("connection to nats server failed 3 times, backing off") + return 5 * time.Minute + } + + return 2 * time.Second + }), + nats.ReconnectHandler(func(_ *nats.Conn) { + log.Info().Msgf("connection to nats server reconnected") + }), + ) + if err != nil { + return nil, errors.Wrapf(err, "connection to nats server at '%s' failed", natsAddress) + } + + return nc, nil +} diff --git a/pkg/publicshare/manager/json/json.go b/pkg/publicshare/manager/json/json.go index f8c9f009c2..11419b474f 100644 --- a/pkg/publicshare/manager/json/json.go +++ b/pkg/publicshare/manager/json/json.go @@ -138,7 +138,7 @@ func (m *manager) startJanitorRun() { } // CreatePublicShare adds a new entry to manager.shares. -func (m *manager) CreatePublicShare(ctx context.Context, u *user.User, rInfo *provider.ResourceInfo, g *link.Grant, description string, internal bool) (*link.PublicShare, error) { +func (m *manager) CreatePublicShare(ctx context.Context, u *user.User, rInfo *provider.ResourceInfo, g *link.Grant, description string, internal bool, notifyUploads bool, notifyUploadsExtraRecipients string) (*link.PublicShare, error) { id := &link.PublicShareId{ OpaqueId: utils.RandString(15), } @@ -168,18 +168,20 @@ func (m *manager) CreatePublicShare(ctx context.Context, u *user.User, rInfo *pr } s := link.PublicShare{ - Id: id, - Owner: rInfo.GetOwner(), - Creator: u.Id, - ResourceId: rInfo.Id, - Token: tkn, - Permissions: g.Permissions, - Ctime: createdAt, - Mtime: createdAt, - PasswordProtected: passwordProtected, - Expiration: g.Expiration, - DisplayName: displayName, - Description: description, + Id: id, + Owner: rInfo.GetOwner(), + Creator: u.Id, + ResourceId: rInfo.Id, + Token: tkn, + Permissions: g.Permissions, + Ctime: createdAt, + Mtime: createdAt, + PasswordProtected: passwordProtected, + Expiration: g.Expiration, + DisplayName: displayName, + Description: description, + NotifyUploads: notifyUploads, + NotifyUploadsExtraRecipients: notifyUploadsExtraRecipients, } ps := &publicShare{ diff --git a/pkg/publicshare/manager/memory/memory.go b/pkg/publicshare/manager/memory/memory.go index 557992974a..8af0b16bcd 100644 --- a/pkg/publicshare/manager/memory/memory.go +++ b/pkg/publicshare/manager/memory/memory.go @@ -58,7 +58,7 @@ var ( ) // CreatePublicShare adds a new entry to manager.shares. -func (m *manager) CreatePublicShare(ctx context.Context, u *user.User, rInfo *provider.ResourceInfo, g *link.Grant, description string, internal bool) (*link.PublicShare, error) { +func (m *manager) CreatePublicShare(ctx context.Context, u *user.User, rInfo *provider.ResourceInfo, g *link.Grant, description string, internal bool, notifyUploads bool, notifyUploadsExtraRecipients string) (*link.PublicShare, error) { id := &link.PublicShareId{ OpaqueId: randString(15), } @@ -86,18 +86,20 @@ func (m *manager) CreatePublicShare(ctx context.Context, u *user.User, rInfo *pr } s := link.PublicShare{ - Id: id, - Owner: rInfo.GetOwner(), - Creator: u.Id, - ResourceId: rInfo.Id, - Token: tkn, - Permissions: g.Permissions, - Ctime: createdAt, - Mtime: modifiedAt, - PasswordProtected: passwordProtected, - Expiration: g.Expiration, - DisplayName: displayName, - Description: description, + Id: id, + Owner: rInfo.GetOwner(), + Creator: u.Id, + ResourceId: rInfo.Id, + Token: tkn, + Permissions: g.Permissions, + Ctime: createdAt, + Mtime: modifiedAt, + PasswordProtected: passwordProtected, + Expiration: g.Expiration, + DisplayName: displayName, + Description: description, + NotifyUploads: notifyUploads, + NotifyUploadsExtraRecipients: notifyUploadsExtraRecipients, } m.shares.Store(s.Token, &s) diff --git a/pkg/publicshare/publicshare.go b/pkg/publicshare/publicshare.go index 39d81cfbb9..9add4e398f 100644 --- a/pkg/publicshare/publicshare.go +++ b/pkg/publicshare/publicshare.go @@ -35,7 +35,7 @@ import ( // Manager manipulates public shares. type Manager interface { - CreatePublicShare(ctx context.Context, u *user.User, md *provider.ResourceInfo, g *link.Grant, description string, internal bool) (*link.PublicShare, error) + CreatePublicShare(ctx context.Context, u *user.User, md *provider.ResourceInfo, g *link.Grant, description string, internal bool, notifyUploads bool, notifyUploadsExtraRecipients string) (*link.PublicShare, error) UpdatePublicShare(ctx context.Context, u *user.User, req *link.UpdatePublicShareRequest, g *link.Grant) (*link.PublicShare, error) GetPublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference, sign bool) (*link.PublicShare, error) ListPublicShares(ctx context.Context, u *user.User, filters []*link.ListPublicSharesRequest_Filter, md *provider.ResourceInfo, sign bool) ([]*link.PublicShare, error) diff --git a/pkg/utils/accumulator/accumulator.go b/pkg/utils/accumulator/accumulator.go new file mode 100644 index 0000000000..710812f172 --- /dev/null +++ b/pkg/utils/accumulator/accumulator.go @@ -0,0 +1,126 @@ +// Copyright 2018-2023 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 accumulator + +import ( + "errors" + "time" + + "github.com/rs/zerolog" +) + +// Accumulator gathers items arriving spaced in time and groups them. +type Accumulator[T any] struct { + started bool + timeout time.Duration + timeoutChan chan bool + timeoutResetChan chan bool + maxSize int + Input chan T + pool []T + log *zerolog.Logger +} + +// New creates a new accumulator. An Accumulator gathers items arriving spaced +// in time and groups them. +// +// The main parameters are timeout and maxSize, determining the limits for the +// accumulator. +// +// An accumulator is started with the start method, which takes fn, a func([]T) +// argument that will be run every time the limit parameters are reached. After +// running fn, the accumulator pool is emptied. +// +// Items are put into the accumulator using the <-input channel, making it +// thread-safe. +func New[T any](timeout time.Duration, maxSize int, log *zerolog.Logger) *Accumulator[T] { + if timeout == 0 { + timeout = time.Duration(60) * time.Second + log.Warn().Msgf("timeout must be a positive duration greater than zero, using default (%d)", timeout) + } + + if maxSize == 0 { + maxSize = 100 + log.Warn().Msgf("maxSize must be a positive integer greater than zero, using default (%d)", maxSize) + } + + input := make(chan T) + accumulator := &Accumulator[T]{ + timeout: timeout, + timeoutResetChan: make(chan bool, 1), + maxSize: maxSize, + Input: input, + log: log, + } + + return accumulator +} + +func (a *Accumulator[T]) startTimeout() { + if !a.started { + a.started = true + a.timeoutChan = make(chan bool) + go func() { + select { + case <-a.timeoutResetChan: + a.timeoutChan = nil + case <-time.After(a.timeout): + a.timeoutChan <- true + a.timeoutChan = nil + } + a.started = false + }() + } +} + +// Start starts the accumulator. +// +// This does not mean the timer will start running. That happens once the first +// item arrives through the <-input channel. Once the time reaches the timeout +// or the max size of the accumulator is reached, fn will be run with the slice +// of items currently in the accumulator. +func (a *Accumulator[T]) Start(fn func([]T)) error { + if fn == nil { + return errors.New("fn must be a callback function") + } + + go func() { + for { + select { + case i := <-a.Input: + a.startTimeout() + a.pool = append(a.pool, i) + + if len(a.pool) >= a.maxSize { + fn(a.pool) + a.pool = nil + a.timeoutResetChan <- true + a.timeoutChan = nil + } + case <-a.timeoutChan: + if len(a.pool) > 0 { + fn(a.pool) + a.pool = nil + } + } + } + }() + + return nil +} diff --git a/tests/acceptance/expected-failures-on-EOS-storage.md b/tests/acceptance/expected-failures-on-EOS-storage.md index c60cf6b12a..7d4ddd9deb 100644 --- a/tests/acceptance/expected-failures-on-EOS-storage.md +++ b/tests/acceptance/expected-failures-on-EOS-storage.md @@ -62,7 +62,7 @@ The expected failures in this file are from features in the owncloud/ocis repo. - [coreApiWebdavProperties1/getQuota.feature:48](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavProperties1/getQuota.feature#L48) - [coreApiWebdavProperties1/getQuota.feature:49](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavProperties1/getQuota.feature#L49) - [coreApiWebdavProperties1/getQuota.feature:61](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavProperties1/getQuota.feature#L61) -- [coreApiWebdavProperties1/getQuota.feature:62](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavProperties1/getQuota.feature#L62) +- [coreApiWebdavProperties1/getQuota.feature:54](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavProperties1/getQuota.feature#L54) - [coreApiWebdavProperties1/getQuota.feature:77](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavProperties1/getQuota.feature#L77) - [coreApiWebdavProperties1/getQuota.feature:78](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavProperties1/getQuota.feature#L78) ### [no command equivalent to occ](https://github.com/owncloud/ocis/issues/1317) @@ -539,9 +539,6 @@ The expected failures in this file are from features in the owncloud/ocis repo. - [coreApiTrashbin/trashbinFilesFolders.feature:302](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L302) - [coreApiTrashbin/trashbinFilesFolders.feature:303](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L303) - [coreApiTrashbin/trashbinFilesFolders.feature:304](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L304) -- [coreApiTrashbin/trashbinFilesFolders.feature:308](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L308) -- [coreApiTrashbin/trashbinFilesFolders.feature:309](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L309) -- [coreApiTrashbin/trashbinFilesFolders.feature:310](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L310) - [coreApiTrashbin/trashbinRestore.feature:31](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinRestore.feature#L31) - [coreApiTrashbin/trashbinRestore.feature:32](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinRestore.feature#L32) - [coreApiTrashbin/trashbinRestore.feature:62](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinRestore.feature#L62) diff --git a/tests/acceptance/expected-failures-on-OCIS-storage.md b/tests/acceptance/expected-failures-on-OCIS-storage.md index 42a67ce9e2..eaa4a0fbeb 100644 --- a/tests/acceptance/expected-failures-on-OCIS-storage.md +++ b/tests/acceptance/expected-failures-on-OCIS-storage.md @@ -191,24 +191,20 @@ File and sync features in a shared scenario - [coreApiSharePublicLink1/accessToPublicLinkShare.feature:13](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/accessToPublicLinkShare.feature#L13) - [coreApiSharePublicLink1/accessToPublicLinkShare.feature:32](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/accessToPublicLinkShare.feature#L32) -- [coreApiSharePublicLink1/createPublicLinkShare.feature:170](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L170) -- [coreApiSharePublicLink1/createPublicLinkShare.feature:171](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L171) -- [coreApiSharePublicLink1/createPublicLinkShare.feature:345](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L345) -- [coreApiSharePublicLink1/createPublicLinkShare.feature:355](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L355) +- [coreApiSharePublicLink1/createPublicLinkShare.feature:163](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L163) +- [coreApiSharePublicLink1/createPublicLinkShare.feature:164](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L164) +- [coreApiSharePublicLink1/createPublicLinkShare.feature:317](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L317) +- [coreApiSharePublicLink1/createPublicLinkShare.feature:327](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L327) -#### [Ability to return error messages in Webdav response bodies](https://github.com/owncloud/ocis/issues/1293) - -- [coreApiSharePublicLink1/createPublicLinkShare.feature:71](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L71) -- [coreApiSharePublicLink1/createPublicLinkShare.feature:72](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L72) #### [copying a folder within a public link folder to folder with same name as an already existing file overwrites the parent file](https://github.com/owncloud/ocis/issues/1232) -- [coreApiSharePublicLink2/copyFromPublicLink.feature:63](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L63) -- [coreApiSharePublicLink2/copyFromPublicLink.feature:89](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L89) -- [coreApiSharePublicLink2/copyFromPublicLink.feature:173](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L173) -- [coreApiSharePublicLink2/copyFromPublicLink.feature:174](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L174) -- [coreApiSharePublicLink2/copyFromPublicLink.feature:189](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L189) -- [coreApiSharePublicLink2/copyFromPublicLink.feature:190](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L190) +- [coreApiSharePublicLink2/copyFromPublicLink.feature:66](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L66) +- [coreApiSharePublicLink2/copyFromPublicLink.feature:92](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L92) +- [coreApiSharePublicLink2/copyFromPublicLink.feature:176](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L176) +- [coreApiSharePublicLink2/copyFromPublicLink.feature:177](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L177) +- [coreApiSharePublicLink2/copyFromPublicLink.feature:192](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L192) +- [coreApiSharePublicLink2/copyFromPublicLink.feature:193](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L193) #### [Increasing permission of a public link of a folder that was initially shared with share+read permissions is allowed](https://github.com/owncloud/ocis/issues/3881) @@ -219,28 +215,29 @@ File and sync features in a shared scenario #### [Adding public upload to a read only shared folder as a receipient is allowed ](https://github.com/owncloud/ocis/issues/2164) -- [coreApiSharePublicLink3/updatePublicLinkShare.feature:270](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/updatePublicLinkShare.feature#L270) -- [coreApiSharePublicLink3/updatePublicLinkShare.feature:271](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/updatePublicLinkShare.feature#L271) -- [coreApiSharePublicLink3/updatePublicLinkShare.feature:316](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/updatePublicLinkShare.feature#L316) -- [coreApiSharePublicLink3/updatePublicLinkShare.feature:317](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/updatePublicLinkShare.feature#L317) +- [coreApiSharePublicLink3/updatePublicLinkShare.feature:267](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/updatePublicLinkShare.feature#L267) +- [coreApiSharePublicLink3/updatePublicLinkShare.feature:268](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/updatePublicLinkShare.feature#L268) +- [coreApiSharePublicLink3/updatePublicLinkShare.feature:309](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/updatePublicLinkShare.feature#L309) +- [coreApiSharePublicLink3/updatePublicLinkShare.feature:310](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/updatePublicLinkShare.feature#L310) + #### [Upload-only shares must not overwrite but create a separate file](https://github.com/owncloud/ocis/issues/1267) -- [coreApiSharePublicLink3/uploadToPublicLinkShare.feature:10](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/uploadToPublicLinkShare.feature#L10) -- [coreApiSharePublicLink3/uploadToPublicLinkShare.feature:111](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/uploadToPublicLinkShare.feature#L111) +- [coreApiSharePublicLink3/uploadToPublicLinkShare.feature:13](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/uploadToPublicLinkShare.feature#L13) +- [coreApiSharePublicLink3/uploadToPublicLinkShare.feature:114](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/uploadToPublicLinkShare.feature#L114) #### [Set quota over settings](https://github.com/owncloud/ocis/issues/1290) _requires a [CS3 user provisioning api that can update the quota for a user](https://github.com/cs3org/cs3apis/pull/95#issuecomment-772780683)_ -- [coreApiSharePublicLink3/uploadToPublicLinkShare.feature:84](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/uploadToPublicLinkShare.feature#L84) -- [coreApiSharePublicLink3/uploadToPublicLinkShare.feature:93](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/uploadToPublicLinkShare.feature#L93) +- [coreApiSharePublicLink3/uploadToPublicLinkShare.feature:87](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/uploadToPublicLinkShare.feature#L87) +- [coreApiSharePublicLink3/uploadToPublicLinkShare.feature:96](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/uploadToPublicLinkShare.feature#L96) #### [share permissions are not enforced](https://github.com/owncloud/product/issues/270) //todo - [coreApiShareReshareToShares3/reShareUpdate.feature:63](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareUpdate.feature#L63) -- [coreApiShareReshareToShares3/reShareUpdate.feature:62](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareUpdate.feature#L62) +- [coreApiShareReshareToShares3/reShareUpdate.feature:64](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareUpdate.feature#L64) #### [path property in pending shares gives only filename](https://github.com/owncloud/ocis/issues/2156) -- [coreApiShareReshareToShares2/reShareSubfolder.feature:153](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares2/reShareSubfolder.feature#L153) -- [coreApiShareReshareToShares2/reShareSubfolder.feature:152](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares2/reShareSubfolder.feature#L152) +- [coreApiShareReshareToShares2/reShareSubfolder.feature:143](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares2/reShareSubfolder.feature#L143) +- [coreApiShareReshareToShares2/reShareSubfolder.feature:144](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares2/reShareSubfolder.feature#L144) - [coreApiShareManagementBasicToShares/deleteShareFromShares.feature:61](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareManagementBasicToShares/deleteShareFromShares.feature#L61) - [coreApiShareManagementBasicToShares/createShareToSharesFolder.feature:743](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareManagementBasicToShares/createShareToSharesFolder.feature#L743) - [coreApiShareManagementBasicToShares/createShareToSharesFolder.feature:744](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareManagementBasicToShares/createShareToSharesFolder.feature#L744) @@ -306,14 +303,14 @@ _requires a [CS3 user provisioning api that can update the quota for a user](htt #### [Expiration date for shares is not implemented](https://github.com/owncloud/ocis/issues/1250) #### Expiration date of user shares -- [coreApiShareReshareToShares3/reShareWithExpiryDate.feature:33](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareWithExpiryDate.feature#L33) -- [coreApiShareReshareToShares3/reShareWithExpiryDate.feature:32](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareWithExpiryDate.feature#L32) +- [coreApiShareReshareToShares3/reShareWithExpiryDate.feature:35](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareWithExpiryDate.feature#L35) +- [coreApiShareReshareToShares3/reShareWithExpiryDate.feature:36](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareWithExpiryDate.feature#L36) #### Expiration date of group shares -- [coreApiShareReshareToShares3/reShareWithExpiryDate.feature:59](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareWithExpiryDate.feature#L59) -- [coreApiShareReshareToShares3/reShareWithExpiryDate.feature:58](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareWithExpiryDate.feature#L58) -- [coreApiShareReshareToShares3/reShareWithExpiryDate.feature:81](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareWithExpiryDate.feature#L81) +- [coreApiShareReshareToShares3/reShareWithExpiryDate.feature:60](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareWithExpiryDate.feature#L60) +- [coreApiShareReshareToShares3/reShareWithExpiryDate.feature:61](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareWithExpiryDate.feature#L61) - [coreApiShareReshareToShares3/reShareWithExpiryDate.feature:82](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareWithExpiryDate.feature#L82) +- [coreApiShareReshareToShares3/reShareWithExpiryDate.feature:83](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareWithExpiryDate.feature#L83) #### [Getting content of a shared file with same name returns 500](https://github.com/owncloud/ocis/issues/3880) - [coreApiShareCreateSpecialToShares1/createShareUniqueReceivedNames.feature:16](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareCreateSpecialToShares1/createShareUniqueReceivedNames.feature#L16) @@ -321,24 +318,22 @@ _requires a [CS3 user provisioning api that can update the quota for a user](htt #### [Empty OCS response for a share create request using a disabled user](https://github.com/owncloud/ocis/issues/2212) - [coreApiShareCreateSpecialToShares2/createShareWithDisabledUser.feature:24](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareCreateSpecialToShares2/createShareWithDisabledUser.feature#L24) - [coreApiShareCreateSpecialToShares2/createShareWithDisabledUser.feature:21](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareCreateSpecialToShares2/createShareWithDisabledUser.feature#L21) - -//todo -- [coreApiShareUpdateToShares/updateShare.feature:95](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L95) -- [coreApiShareUpdateToShares/updateShare.feature:96](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L96) - [coreApiShareUpdateToShares/updateShare.feature:97](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L97) - [coreApiShareUpdateToShares/updateShare.feature:98](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L98) - [coreApiShareUpdateToShares/updateShare.feature:99](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L99) -- [coreApiShareUpdateToShares/updateShare.feature:94](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L94) -- [coreApiShareUpdateToShares/updateShare.feature:119](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L119) -- [coreApiShareUpdateToShares/updateShare.feature:120](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L120) +- [coreApiShareUpdateToShares/updateShare.feature:100](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L100) +- [coreApiShareUpdateToShares/updateShare.feature:101](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L101) +- [coreApiShareUpdateToShares/updateShare.feature:102](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L102) - [coreApiShareUpdateToShares/updateShare.feature:121](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L121) - [coreApiShareUpdateToShares/updateShare.feature:122](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L122) - [coreApiShareUpdateToShares/updateShare.feature:123](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L123) -- [coreApiShareUpdateToShares/updateShare.feature:118](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L118) +- [coreApiShareUpdateToShares/updateShare.feature:124](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L124) +- [coreApiShareUpdateToShares/updateShare.feature:125](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L125) +- [coreApiShareUpdateToShares/updateShare.feature:126](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L126) #### [Edit user share response has an "name" field](https://github.com/owncloud/ocis/issues/1225) -- [coreApiShareUpdateToShares/updateShare.feature:241](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L241) -- [coreApiShareUpdateToShares/updateShare.feature:240](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L240) +- [coreApiShareUpdateToShares/updateShare.feature:243](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L243) +- [coreApiShareUpdateToShares/updateShare.feature:244](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L244) #### [user can access version metadata of a received share before accepting it](https://github.com/owncloud/ocis/issues/760) - [coreApiVersions/fileVersions.feature:313](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiVersions/fileVersions.feature#L313) @@ -373,26 +368,28 @@ API, search, favorites, config, capabilities, not existing endpoints, CORS and o - [coreApiAuthOcs/ocsGETAuth.feature:124](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthOcs/ocsGETAuth.feature#L124) - [coreApiAuthOcs/ocsPOSTAuth.feature:11](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthOcs/ocsPOSTAuth.feature#L11) - [coreApiAuthOcs/ocsPUTAuth.feature:11](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthOcs/ocsPUTAuth.feature#L11) +- [coreApiSharePublicLink1/createPublicLinkShare.feature:69](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L69) +- [coreApiSharePublicLink1/createPublicLinkShare.feature:70](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L70) #### [sending MKCOL requests to another or non-existing user's webDav endpoints as normal user should return 404](https://github.com/owncloud/ocis/issues/5049) _ocdav: api compatibility, return correct status code_ -- [coreApiAuthWebDav/webDavDELETEAuth.feature:61](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavDELETEAuth.feature#L61) -- [coreApiAuthWebDav/webDavPROPFINDAuth.feature:58](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavPROPFINDAuth.feature#L58) -- [coreApiAuthWebDav/webDavPROPPATCHAuth.feature:59](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavPROPPATCHAuth.feature#L59) -- [coreApiAuthWebDav/webDavMKCOLAuth.feature:55](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavMKCOLAuth.feature#L55) -- [coreApiAuthWebDav/webDavMKCOLAuth.feature:66](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavMKCOLAuth.feature#L66) +- [coreApiAuthWebDav/webDavDELETEAuth.feature:49](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavDELETEAuth.feature#L49) +- [coreApiAuthWebDav/webDavPROPFINDAuth.feature:46](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavPROPFINDAuth.feature#L46) +- [coreApiAuthWebDav/webDavPROPPATCHAuth.feature:47](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavPROPPATCHAuth.feature#L47) +- [coreApiAuthWebDav/webDavMKCOLAuth.feature:43](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavMKCOLAuth.feature#L43) +- [coreApiAuthWebDav/webDavMKCOLAuth.feature:54](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavMKCOLAuth.feature#L54) #### [trying to lock file of another user gives http 200](https://github.com/owncloud/ocis/issues/2176) -- [coreApiAuthWebDav/webDavLOCKAuth.feature:59](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavLOCKAuth.feature#L59) +- [coreApiAuthWebDav/webDavLOCKAuth.feature:47](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavLOCKAuth.feature#L47) #### [send (MOVE, COPY) requests to another user's webDav endpoints as normal user gives 400 instead of 403](https://github.com/owncloud/ocis/issues/3882) _ocdav: api compatibility, return correct status code_ -- [coreApiAuthWebDav/webDavMOVEAuth.feature:58](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavMOVEAuth.feature#L58) Scenario: send MOVE requests to another user's webDav endpoints as normal user -- [coreApiAuthWebDav/webDavCOPYAuth.feature:58](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavCOPYAuth.feature#L58) +- [coreApiAuthWebDav/webDavMOVEAuth.feature:46](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavMOVEAuth.feature#L46) Scenario: send MOVE requests to another user's webDav endpoints as normal user +- [coreApiAuthWebDav/webDavCOPYAuth.feature:46](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavCOPYAuth.feature#L46) #### [send POST requests to another user's webDav endpoints as normal user](https://github.com/owncloud/ocis/issues/1287) _ocdav: api compatibility, return correct status code_ -- [coreApiAuthWebDav/webDavPOSTAuth.feature:59](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavPOSTAuth.feature#L59) +- [coreApiAuthWebDav/webDavPOSTAuth.feature:47](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavPOSTAuth.feature#L47) #### [Using double slash in URL to access a folder gives 501 and other status codes](https://github.com/owncloud/ocis/issues/1667) - [coreApiAuthWebDav/webDavSpecialURLs.feature:37](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavSpecialURLs.feature#L37) @@ -454,7 +451,7 @@ _ocdav: api compatibility, return correct status code_ - [coreApiWebdavOperations/refuseAccess.feature:36](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavOperations/refuseAccess.feature#L36) #### [App Passwords/Tokens for legacy WebDAV clients](https://github.com/owncloud/ocis/issues/197) -- [coreApiAuthWebDav/webDavDELETEAuth.feature:139](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavDELETEAuth.feature#L139) +- [coreApiAuthWebDav/webDavDELETEAuth.feature:109](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavDELETEAuth.feature#L109) #### [Sharing a same file twice to the same group](https://github.com/owncloud/ocis/issues/1710) - [coreApiShareManagementBasicToShares/createShareToSharesFolder.feature:726](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareManagementBasicToShares/createShareToSharesFolder.feature#L726) @@ -502,9 +499,9 @@ Not everything needs to be implemented for ocis. While the oc10 testsuite covers - [coreApiWebdavUploadTUS/checksums.feature:284](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L284) - [coreApiWebdavUploadTUS/checksums.feature:285](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L285) - [coreApiWebdavUploadTUS/optionsRequest.feature:8](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavUploadTUS/optionsRequest.feature#L8) -- [coreApiWebdavUploadTUS/optionsRequest.feature:35](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavUploadTUS/optionsRequest.feature#L35) -- [coreApiWebdavUploadTUS/optionsRequest.feature:62](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavUploadTUS/optionsRequest.feature#L62) -- [coreApiWebdavUploadTUS/optionsRequest.feature:89](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavUploadTUS/optionsRequest.feature#L89) +- [coreApiWebdavUploadTUS/optionsRequest.feature:23](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavUploadTUS/optionsRequest.feature#L23) +- [coreApiWebdavUploadTUS/optionsRequest.feature:38](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavUploadTUS/optionsRequest.feature#L38) +- [coreApiWebdavUploadTUS/optionsRequest.feature:53](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavUploadTUS/optionsRequest.feature#L53) - [coreApiWebdavUploadTUS/uploadToShare.feature:175](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L175) - [coreApiWebdavUploadTUS/uploadToShare.feature:174](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L174) - [coreApiWebdavUploadTUS/uploadToShare.feature:194](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L194) @@ -517,12 +514,12 @@ Not everything needs to be implemented for ocis. While the oc10 testsuite covers - [coreApiWebdavUploadTUS/uploadToShare.feature:293](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L293) #### [Share inaccessible if folder with same name was deleted and recreated](https://github.com/owncloud/ocis/issues/1787) -- [coreApiShareReshareToShares1/reShare.feature:264](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares1/reShare.feature#L264) -- [coreApiShareReshareToShares1/reShare.feature:265](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares1/reShare.feature#L265) -- [coreApiShareReshareToShares1/reShare.feature:282](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares1/reShare.feature#L282) -- [coreApiShareReshareToShares1/reShare.feature:283](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares1/reShare.feature#L283) -- [coreApiShareReshareToShares1/reShare.feature:300](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares1/reShare.feature#L300) -- [coreApiShareReshareToShares1/reShare.feature:301](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares1/reShare.feature#L301) +- [coreApiShareReshareToShares1/reShare.feature:267](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares1/reShare.feature#L267) +- [coreApiShareReshareToShares1/reShare.feature:268](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares1/reShare.feature#L268) +- [coreApiShareReshareToShares1/reShare.feature:285](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares1/reShare.feature#L285) +- [coreApiShareReshareToShares1/reShare.feature:286](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares1/reShare.feature#L286) +- [coreApiShareReshareToShares1/reShare.feature:303](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares1/reShare.feature#L303) +- [coreApiShareReshareToShares1/reShare.feature:304](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares1/reShare.feature#L304) #### [incorrect ocs(v2) status value when getting info of share that does not exist should be 404, gives 998](https://github.com/owncloud/product/issues/250) _ocs: api compatibility, return correct status code_ @@ -567,8 +564,8 @@ _ocs: api compatibility, return correct status code_ - [coreApiWebdavProperties2/getFileProperties.feature:276](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavProperties2/getFileProperties.feature#L276) #### [Cannot move folder/file from one received share to another](https://github.com/owncloud/ocis/issues/2442) -- [coreApiShareUpdateToShares/updateShare.feature:158](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L158) -- [coreApiShareUpdateToShares/updateShare.feature:194](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L194) +- - [coreApiShareUpdateToShares/updateShare.feature:197](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L197) +- [coreApiShareUpdateToShares/updateShare.feature:161](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L161) - [coreApiShareManagementToShares/mergeShare.feature:131](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareManagementToShares/mergeShare.feature#L131) - [coreApiShareCreateSpecialToShares2/createShareReceivedInMultipleWays.feature:262](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareCreateSpecialToShares2/createShareReceivedInMultipleWays.feature#L262) @@ -641,11 +638,11 @@ _ocs: api compatibility, return correct status code_ - [coreApiTrashbin/trashbinFilesFolders.feature:249](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L249) #### [Cannot disable the dav propfind depth infinity for resources](https://github.com/owncloud/ocis/issues/3720) -- [coreApiWebdavOperations/listFiles.feature:364](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavOperations/listFiles.feature#L364) -- [coreApiWebdavOperations/listFiles.feature:365](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavOperations/listFiles.feature#L365) -- [coreApiWebdavOperations/listFiles.feature:384](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavOperations/listFiles.feature#L384) -- [coreApiWebdavOperations/listFiles.feature:403](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavOperations/listFiles.feature#L403) -- [coreApiWebdavOperations/listFiles.feature:404](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavOperations/listFiles.feature#L404) +- [coreApiWebdavOperations/listFiles.feature:355](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavOperations/listFiles.feature#L355) +- [coreApiWebdavOperations/listFiles.feature:356](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavOperations/listFiles.feature#L356) +- [coreApiWebdavOperations/listFiles.feature:375](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavOperations/listFiles.feature#L375) +- [coreApiWebdavOperations/listFiles.feature:394](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavOperations/listFiles.feature#L394) +- [coreApiWebdavOperations/listFiles.feature:395](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavOperations/listFiles.feature#L395) #### [trash-bin propfind responses are wrong in reva master](https://github.com/cs3org/reva/issues/2861) - [coreApiTrashbin/trashbinDelete.feature:29](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinDelete.feature#L29) @@ -667,16 +664,13 @@ _ocs: api compatibility, return correct status code_ - [coreApiTrashbin/trashbinFilesFolders.feature:131](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L131) - [coreApiTrashbin/trashbinFilesFolders.feature:154](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L154) - [coreApiTrashbin/trashbinFilesFolders.feature:287](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L287) -- [coreApiTrashbin/trashbinFilesFolders.feature:308](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L308) - [coreApiTrashbin/trashbinFilesFolders.feature:305](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L305) - [coreApiTrashbin/trashbinFilesFolders.feature:306](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L306) - [coreApiTrashbin/trashbinFilesFolders.feature:307](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L307) -- [coreApiTrashbin/trashbinFilesFolders.feature:309](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L309) -- [coreApiTrashbin/trashbinFilesFolders.feature:310](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L310) -- [coreApiTrashbin/trashbinFilesFolders.feature:332](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L332) -- [coreApiTrashbin/trashbinFilesFolders.feature:352](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L352) -- [coreApiTrashbin/trashbinFilesFolders.feature:443](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L443) -- [coreApiTrashbin/trashbinFilesFolders.feature:406](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L406) +- [coreApiTrashbin/trashbinFilesFolders.feature:326](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L326) +- [coreApiTrashbin/trashbinFilesFolders.feature:346](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L346) +- [coreApiTrashbin/trashbinFilesFolders.feature:400](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L400) +- [coreApiTrashbin/trashbinFilesFolders.feature:437](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L437) - [coreApiTrashbin/trashbinSharingToShares.feature:22](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinSharingToShares.feature#L22) - [coreApiTrashbin/trashbinSharingToShares.feature:205](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinSharingToShares.feature#L205) - [coreApiTrashbin/trashbinSharingToShares.feature:229](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinSharingToShares.feature#L229) @@ -758,12 +752,12 @@ _ocs: api compatibility, return correct status code_ #### [WebDAV MOVE with body returns 400 rather than 415](https://github.com/cs3org/reva/issues/3119) -- [coreApiAuthWebDav/webDavMOVEAuth.feature:136](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavMOVEAuth.feature#L136) +- [coreApiAuthWebDav/webDavMOVEAuth.feature:106](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavMOVEAuth.feature#L106) #### [reShareUpdate API tests failing in reva](https://github.com/cs3org/reva/issues/2916) -- [coreApiShareReshareToShares3/reShareUpdate.feature:158](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareUpdate.feature#L158) -- [coreApiShareReshareToShares3/reShareUpdate.feature:157](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareUpdate.feature#L157) +- [coreApiShareReshareToShares3/reShareUpdate.feature:153](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareUpdate.feature#L153) +- [coreApiShareReshareToShares3/reShareUpdate.feature:154](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareUpdate.feature#L154) #### [coreApiShareOperationsToShares1/gettingShares.feature:28 fails in CI](https://github.com/cs3org/reva/issues/2926) @@ -771,11 +765,11 @@ _ocs: api compatibility, return correct status code_ - [coreApiShareOperationsToShares1/gettingShares.feature:41](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareOperationsToShares1/gettingShares.feature#L41) #### [These tests pass in ocis and reva egde but fail in master with `file_target has unexpected value '/home'`](https://github.com/owncloud/ocis/issues/2113) -- [coreApiSharePublicLink1/createPublicLinkShare.feature:308](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L308) -- [coreApiSharePublicLink1/createPublicLinkShare.feature:309](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L309) +- [coreApiSharePublicLink1/createPublicLinkShare.feature:280](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L280) +- [coreApiSharePublicLink1/createPublicLinkShare.feature:281](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L281) #### [valid WebDAV (DELETE, COPY or MOVE) requests with body must exit with 415](https://github.com/owncloud/ocis/issues/4332) -- [coreApiAuthWebDav/webDavCOPYAuth.feature:136](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavCOPYAuth.feature#L136) +- [coreApiAuthWebDav/webDavCOPYAuth.feature:106](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavCOPYAuth.feature#L106) #### [PROPFIND on (password protected) public link returns invalid XML](https://github.com/owncloud/ocis/issues/39707) The problem has been fixed in reva edge branch but not in reva master @@ -783,10 +777,10 @@ The problem has been fixed in reva edge branch but not in reva master - [coreApiWebdavOperations/propfind.feature:73](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavOperations/propfind.feature#L73) #### [Updating the role of a public link to internal gives returns 400] -- [coreApiSharePublicLink3/updatePublicLinkShare.feature:492](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/updatePublicLinkShare.feature#L492) -- [coreApiSharePublicLink3/updatePublicLinkShare.feature:493](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/updatePublicLinkShare.feature#L493) -- [coreApiSharePublicLink3/updatePublicLinkShare.feature:494](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/updatePublicLinkShare.feature#L494) -- [coreApiSharePublicLink3/updatePublicLinkShare.feature:495](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/updatePublicLinkShare.feature#L495) +- [coreApiSharePublicLink3/updatePublicLinkShare.feature:483](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/updatePublicLinkShare.feature#L483) +- [coreApiSharePublicLink3/updatePublicLinkShare.feature:484](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/updatePublicLinkShare.feature#L484) +- [coreApiSharePublicLink3/updatePublicLinkShare.feature:485](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/updatePublicLinkShare.feature#L485) +- [coreApiSharePublicLink3/updatePublicLinkShare.feature:486](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/updatePublicLinkShare.feature#L486) #### [Default capabilities for normal user and admin user not same as in oC-core](https://github.com/owncloud/ocis/issues/1285) - [coreApiCapabilities/capabilities.feature:11](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiCapabilities/capabilities.feature#L11) @@ -794,5 +788,9 @@ The problem has been fixed in reva edge branch but not in reva master - [coreApiCapabilities/capabilities.feature:175](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiCapabilities/capabilities.feature#L175) - [coreApiCapabilities/capabilities.feature:216](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiCapabilities/capabilities.feature#L216) +#### [Sharing of project space root via public link does no longer work](https://github.com/owncloud/ocis/issues/6278) +- [coreApiShareCreateSpecialToShares2/createShareDefaultFolderForReceivedShares.feature:23](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareCreateSpecialToShares2/createShareDefaultFolderForReceivedShares.feature#L23) +- [coreApiShareCreateSpecialToShares2/createShareDefaultFolderForReceivedShares.feature:24](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareCreateSpecialToShares2/createShareDefaultFolderForReceivedShares.feature#L24) + Note: always have an empty line at the end of this file. The bash script that processes this file may not process a scenario reference on the last line. diff --git a/tests/acceptance/expected-failures-on-S3NG-storage.md b/tests/acceptance/expected-failures-on-S3NG-storage.md index a6f587c185..7b49f59a59 100644 --- a/tests/acceptance/expected-failures-on-S3NG-storage.md +++ b/tests/acceptance/expected-failures-on-S3NG-storage.md @@ -217,24 +217,19 @@ File and sync features in a shared scenario - [coreApiSharePublicLink1/accessToPublicLinkShare.feature:13](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/accessToPublicLinkShare.feature#L13) - [coreApiSharePublicLink1/accessToPublicLinkShare.feature:32](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/accessToPublicLinkShare.feature#L32) -- [coreApiSharePublicLink1/createPublicLinkShare.feature:170](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L170) -- [coreApiSharePublicLink1/createPublicLinkShare.feature:171](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L171) -- [coreApiSharePublicLink1/createPublicLinkShare.feature:345](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L345) -- [coreApiSharePublicLink1/createPublicLinkShare.feature:355](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L355) - -#### [Ability to return error messages in Webdav response bodies](https://github.com/owncloud/ocis/issues/1293) - -- [coreApiSharePublicLink1/createPublicLinkShare.feature:71](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L71) -- [coreApiSharePublicLink1/createPublicLinkShare.feature:72](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L72) +- [coreApiSharePublicLink1/createPublicLinkShare.feature:163](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L163) +- [coreApiSharePublicLink1/createPublicLinkShare.feature:164](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L164) +- [coreApiSharePublicLink1/createPublicLinkShare.feature:317](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L317) +- [coreApiSharePublicLink1/createPublicLinkShare.feature:327](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L327) #### [copying a folder within a public link folder to folder with same name as an already existing file overwrites the parent file](https://github.com/owncloud/ocis/issues/1232) -- [coreApiSharePublicLink2/copyFromPublicLink.feature:63](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L63) -- [coreApiSharePublicLink2/copyFromPublicLink.feature:89](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L89) -- [coreApiSharePublicLink2/copyFromPublicLink.feature:173](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L173) -- [coreApiSharePublicLink2/copyFromPublicLink.feature:174](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L174) -- [coreApiSharePublicLink2/copyFromPublicLink.feature:189](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L189) -- [coreApiSharePublicLink2/copyFromPublicLink.feature:190](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L190) +- [coreApiSharePublicLink2/copyFromPublicLink.feature:66](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L66) +- [coreApiSharePublicLink2/copyFromPublicLink.feature:92](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L92) +- [coreApiSharePublicLink2/copyFromPublicLink.feature:176](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L176) +- [coreApiSharePublicLink2/copyFromPublicLink.feature:177](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L177) +- [coreApiSharePublicLink2/copyFromPublicLink.feature:192](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L192) +- [coreApiSharePublicLink2/copyFromPublicLink.feature:193](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink2/copyFromPublicLink.feature#L193) #### [Increasing permission of a public link of a folder that was initially shared with share+read permissions is allowed](https://github.com/owncloud/ocis/issues/3881) @@ -245,30 +240,30 @@ File and sync features in a shared scenario #### [Adding public upload to a read only shared folder as a receipient is allowed ](https://github.com/owncloud/ocis/issues/2164) -- [coreApiSharePublicLink3/updatePublicLinkShare.feature:270](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/updatePublicLinkShare.feature#L270) -- [coreApiSharePublicLink3/updatePublicLinkShare.feature:271](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/updatePublicLinkShare.feature#L271) -- [coreApiSharePublicLink3/updatePublicLinkShare.feature:316](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/updatePublicLinkShare.feature#L316) -- [coreApiSharePublicLink3/updatePublicLinkShare.feature:317](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/updatePublicLinkShare.feature#L317) +- [coreApiSharePublicLink3/updatePublicLinkShare.feature:267](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/updatePublicLinkShare.feature#L267) +- [coreApiSharePublicLink3/updatePublicLinkShare.feature:268](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/updatePublicLinkShare.feature#L268) +- [coreApiSharePublicLink3/updatePublicLinkShare.feature:309](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/updatePublicLinkShare.feature#L309) +- [coreApiSharePublicLink3/updatePublicLinkShare.feature:310](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/updatePublicLinkShare.feature#L310) #### [Upload-only shares must not overwrite but create a separate file](https://github.com/owncloud/ocis/issues/1267) -- [coreApiSharePublicLink3/uploadToPublicLinkShare.feature:10](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/uploadToPublicLinkShare.feature#L10) -- [coreApiSharePublicLink3/uploadToPublicLinkShare.feature:111](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/uploadToPublicLinkShare.feature#L111) +- [coreApiSharePublicLink3/uploadToPublicLinkShare.feature:13](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/uploadToPublicLinkShare.feature#L13) +- [coreApiSharePublicLink3/uploadToPublicLinkShare.feature:114](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/uploadToPublicLinkShare.feature#L114) #### [Set quota over settings](https://github.com/owncloud/ocis/issues/1290) _requires a [CS3 user provisioning api that can update the quota for a user](https://github.com/cs3org/cs3apis/pull/95#issuecomment-772780683)_ -- [coreApiSharePublicLink3/uploadToPublicLinkShare.feature:84](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/uploadToPublicLinkShare.feature#L84) -- [coreApiSharePublicLink3/uploadToPublicLinkShare.feature:93](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/uploadToPublicLinkShare.feature#L93) +- [coreApiSharePublicLink3/uploadToPublicLinkShare.feature:87](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/uploadToPublicLinkShare.feature#L87) +- [coreApiSharePublicLink3/uploadToPublicLinkShare.feature:96](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/uploadToPublicLinkShare.feature#L96) #### [share permissions are not enforced](https://github.com/owncloud/product/issues/270) //todo - [coreApiShareReshareToShares3/reShareUpdate.feature:63](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareUpdate.feature#L63) -- [coreApiShareReshareToShares3/reShareUpdate.feature:62](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareUpdate.feature#L62) +- [coreApiShareReshareToShares3/reShareUpdate.feature:64](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareUpdate.feature#L64) #### [file_target in share response](https://github.com/owncloud/product/issues/203) //todo -- [coreApiShareReshareToShares2/reShareSubfolder.feature:153](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares2/reShareSubfolder.feature#L153) -- [coreApiShareReshareToShares2/reShareSubfolder.feature:152](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares2/reShareSubfolder.feature#L152) +- [coreApiShareReshareToShares2/reShareSubfolder.feature:143](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares2/reShareSubfolder.feature#L143) +- [coreApiShareReshareToShares2/reShareSubfolder.feature:144](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares2/reShareSubfolder.feature#L144) #### [deleting a file inside a received shared folder is moved to the trash-bin of the sharer not the receiver](https://github.com/owncloud/ocis/issues/1124) @@ -329,14 +324,14 @@ _requires a [CS3 user provisioning api that can update the quota for a user](htt #### [Expiration date for shares is not implemented](https://github.com/owncloud/ocis/issues/1250) #### Expiration date of user shares -- [coreApiShareReshareToShares3/reShareWithExpiryDate.feature:33](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareWithExpiryDate.feature#L33) -- [coreApiShareReshareToShares3/reShareWithExpiryDate.feature:32](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareWithExpiryDate.feature#L32) +- [coreApiShareReshareToShares3/reShareWithExpiryDate.feature:35](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareWithExpiryDate.feature#L35) +- [coreApiShareReshareToShares3/reShareWithExpiryDate.feature:36](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareWithExpiryDate.feature#L36) #### Expiration date of group shares -- [coreApiShareReshareToShares3/reShareWithExpiryDate.feature:59](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareWithExpiryDate.feature#L59) -- [coreApiShareReshareToShares3/reShareWithExpiryDate.feature:58](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareWithExpiryDate.feature#L58) -- [coreApiShareReshareToShares3/reShareWithExpiryDate.feature:81](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareWithExpiryDate.feature#L81) +- [coreApiShareReshareToShares3/reShareWithExpiryDate.feature:60](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareWithExpiryDate.feature#L60) +- [coreApiShareReshareToShares3/reShareWithExpiryDate.feature:61](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareWithExpiryDate.feature#L61) - [coreApiShareReshareToShares3/reShareWithExpiryDate.feature:82](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareWithExpiryDate.feature#L82) +- [coreApiShareReshareToShares3/reShareWithExpiryDate.feature:83](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareWithExpiryDate.feature#L83) #### [Getting content of a shared file with same name returns 500](https://github.com/owncloud/ocis/issues/3880) @@ -345,24 +340,22 @@ _requires a [CS3 user provisioning api that can update the quota for a user](htt #### [Empty OCS response for a share create request using a disabled user](https://github.com/owncloud/ocis/issues/2212) - [coreApiShareCreateSpecialToShares2/createShareWithDisabledUser.feature:24](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareCreateSpecialToShares2/createShareWithDisabledUser.feature#L24) - [coreApiShareCreateSpecialToShares2/createShareWithDisabledUser.feature:21](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareCreateSpecialToShares2/createShareWithDisabledUser.feature#L21) - -//todo -- [coreApiShareUpdateToShares/updateShare.feature:96](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L96) - [coreApiShareUpdateToShares/updateShare.feature:97](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L97) - [coreApiShareUpdateToShares/updateShare.feature:98](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L98) - [coreApiShareUpdateToShares/updateShare.feature:99](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L99) -- [coreApiShareUpdateToShares/updateShare.feature:94](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L94) -- [coreApiShareUpdateToShares/updateShare.feature:95](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L95) -- [coreApiShareUpdateToShares/updateShare.feature:120](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L120) +- [coreApiShareUpdateToShares/updateShare.feature:100](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L100) +- [coreApiShareUpdateToShares/updateShare.feature:101](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L101) +- [coreApiShareUpdateToShares/updateShare.feature:102](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L102) - [coreApiShareUpdateToShares/updateShare.feature:121](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L121) - [coreApiShareUpdateToShares/updateShare.feature:122](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L122) - [coreApiShareUpdateToShares/updateShare.feature:123](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L123) -- [coreApiShareUpdateToShares/updateShare.feature:118](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L118) -- [coreApiShareUpdateToShares/updateShare.feature:119](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L119) +- [coreApiShareUpdateToShares/updateShare.feature:124](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L124) +- [coreApiShareUpdateToShares/updateShare.feature:125](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L125) +- [coreApiShareUpdateToShares/updateShare.feature:126](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L126) #### [Edit user share response has an "name" field](https://github.com/owncloud/ocis/issues/1225) -- [coreApiShareUpdateToShares/updateShare.feature:241](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L241) -- [coreApiShareUpdateToShares/updateShare.feature:240](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L240) +- [coreApiShareUpdateToShares/updateShare.feature:243](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L243) +- [coreApiShareUpdateToShares/updateShare.feature:244](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L244) #### [user can access version metadata of a received share before accepting it](https://github.com/owncloud/ocis/issues/760) - [coreApiVersions/fileVersions.feature:313](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiVersions/fileVersions.feature#L313) @@ -397,26 +390,28 @@ API, search, favorites, config, capabilities, not existing endpoints, CORS and o - [coreApiAuthOcs/ocsGETAuth.feature:124](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthOcs/ocsGETAuth.feature#L124) Scenario: using OCS as admin user with wrong password - [coreApiAuthOcs/ocsPOSTAuth.feature:11](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthOcs/ocsPOSTAuth.feature#L11) Scenario: send POST requests to OCS endpoints as normal user with wrong password - [coreApiAuthOcs/ocsPUTAuth.feature:11](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthOcs/ocsPUTAuth.feature#L11) Scenario: send PUT request to OCS endpoints as admin with wrong password +- [coreApiSharePublicLink1/createPublicLinkShare.feature:69](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L69) +- [coreApiSharePublicLink1/createPublicLinkShare.feature:70](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L70) #### [sending MKCOL requests to another or non-existing user's webDav endpoints as normal user should return 404](https://github.com/owncloud/ocis/issues/5049) _ocdav: api compatibility, return correct status code_ -- [coreApiAuthWebDav/webDavDELETEAuth.feature:61](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavDELETEAuth.feature#L61) Scenario: send DELETE requests to another user's webDav endpoints as normal user -- [coreApiAuthWebDav/webDavPROPFINDAuth.feature:58](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavPROPFINDAuth.feature#L58) Scenario: send PROPFIND requests to another user's webDav endpoints as normal user -- [coreApiAuthWebDav/webDavPROPPATCHAuth.feature:59](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavPROPPATCHAuth.feature#L59) Scenario: send PROPPATCH requests to another user's webDav endpoints as normal user -- [coreApiAuthWebDav/webDavMKCOLAuth.feature:55](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavMKCOLAuth.feature#L55) Scenario: send MKCOL requests to another user's webDav endpoints as normal user -- [coreApiAuthWebDav/webDavMKCOLAuth.feature:66](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavMKCOLAuth.feature#L66) Scenario: send MKCOL requests to another user's webDav endpoints as normal user using the spaces WebDAV API +- [coreApiAuthWebDav/webDavDELETEAuth.feature:49](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavDELETEAuth.feature#L49) Scenario: send DELETE requests to another user's webDav endpoints as normal user +- [coreApiAuthWebDav/webDavPROPFINDAuth.feature:46](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavPROPFINDAuth.feature#L46) Scenario: send PROPFIND requests to another user's webDav endpoints as normal user +- [coreApiAuthWebDav/webDavPROPPATCHAuth.feature:47](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavPROPPATCHAuth.feature#L47) Scenario: send PROPPATCH requests to another user's webDav endpoints as normal user +- [coreApiAuthWebDav/webDavMKCOLAuth.feature:43](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavMKCOLAuth.feature#L43) Scenario: send MKCOL requests to another user's webDav endpoints as normal user +- [coreApiAuthWebDav/webDavMKCOLAuth.feature:54](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavMKCOLAuth.feature#L54) Scenario: send MKCOL requests to another user's webDav endpoints as normal user using the spaces WebDAV API #### [trying to lock file of another user gives http 200](https://github.com/owncloud/ocis/issues/2176) -- [coreApiAuthWebDav/webDavLOCKAuth.feature:59](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavLOCKAuth.feature#L59) Scenario: send LOCK requests to another user's webDav endpoints as normal user +- [coreApiAuthWebDav/webDavLOCKAuth.feature:47](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavLOCKAuth.feature#L47) Scenario: send LOCK requests to another user's webDav endpoints as normal user #### [send (MOVE, COPY) requests to another user's webDav endpoints as normal user gives 400 instead of 403](https://github.com/owncloud/ocis/issues/3882) _ocdav: api compatibility, return correct status code_ -- [coreApiAuthWebDav/webDavMOVEAuth.feature:58](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavMOVEAuth.feature#L58) Scenario: send MOVE requests to another user's webDav endpoints as normal user -- [coreApiAuthWebDav/webDavCOPYAuth.feature:58](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavCOPYAuth.feature#L58) +- [coreApiAuthWebDav/webDavMOVEAuth.feature:46](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavMOVEAuth.feature#L46) Scenario: send MOVE requests to another user's webDav endpoints as normal user +- [coreApiAuthWebDav/webDavCOPYAuth.feature:46](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavCOPYAuth.feature#L46) #### [send POST requests to another user's webDav endpoints as normal user](https://github.com/owncloud/ocis/issues/1287) _ocdav: api compatibility, return correct status code_ -- [coreApiAuthWebDav/webDavPOSTAuth.feature:59](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavPOSTAuth.feature#L59) Scenario: send POST requests to another user's webDav endpoints as normal user +- [coreApiAuthWebDav/webDavPOSTAuth.feature:47](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavPOSTAuth.feature#L47) Scenario: send POST requests to another user's webDav endpoints as normal user #### [Using double slash in URL to access a folder gives 501 and other status codes](https://github.com/owncloud/ocis/issues/1667) - [coreApiAuthWebDav/webDavSpecialURLs.feature:37](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavSpecialURLs.feature#L37) @@ -478,7 +473,7 @@ _ocdav: api compatibility, return correct status code_ - [coreApiWebdavOperations/refuseAccess.feature:36](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavOperations/refuseAccess.feature#L36) #### [App Passwords/Tokens for legacy WebDAV clients](https://github.com/owncloud/ocis/issues/197) -- [coreApiAuthWebDav/webDavDELETEAuth.feature:139](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavDELETEAuth.feature#L139) +- [coreApiAuthWebDav/webDavDELETEAuth.feature:109](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavDELETEAuth.feature#L109) #### [Sharing a same file twice to the same group](https://github.com/owncloud/ocis/issues/1710) - [coreApiShareManagementBasicToShares/createShareToSharesFolder.feature:778](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareManagementBasicToShares/createShareToSharesFolder.feature#L778) @@ -526,9 +521,9 @@ Not everything needs to be implemented for ocis. While the oc10 testsuite covers - [coreApiWebdavUploadTUS/checksums.feature:284](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L284) - [coreApiWebdavUploadTUS/checksums.feature:285](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavUploadTUS/checksums.feature#L285) - [coreApiWebdavUploadTUS/optionsRequest.feature:8](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavUploadTUS/optionsRequest.feature#L8) -- [coreApiWebdavUploadTUS/optionsRequest.feature:35](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavUploadTUS/optionsRequest.feature#L35) -- [coreApiWebdavUploadTUS/optionsRequest.feature:62](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavUploadTUS/optionsRequest.feature#L62) -- [coreApiWebdavUploadTUS/optionsRequest.feature:89](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavUploadTUS/optionsRequest.feature#L89) +- [coreApiWebdavUploadTUS/optionsRequest.feature:23](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavUploadTUS/optionsRequest.feature#L23) +- [coreApiWebdavUploadTUS/optionsRequest.feature:38](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavUploadTUS/optionsRequest.feature#L38) +- [coreApiWebdavUploadTUS/optionsRequest.feature:53](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavUploadTUS/optionsRequest.feature#L53) - [coreApiWebdavUploadTUS/uploadToShare.feature:175](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L175) - [coreApiWebdavUploadTUS/uploadToShare.feature:174](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L174) - [coreApiWebdavUploadTUS/uploadToShare.feature:194](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L194) @@ -541,12 +536,12 @@ Not everything needs to be implemented for ocis. While the oc10 testsuite covers - [coreApiWebdavUploadTUS/uploadToShare.feature:293](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L293) #### [Share inaccessible if folder with same name was deleted and recreated](https://github.com/owncloud/ocis/issues/1787) -- [coreApiShareReshareToShares1/reShare.feature:265](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares1/reShare.feature#L265) -- [coreApiShareReshareToShares1/reShare.feature:264](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares1/reShare.feature#L264) -- [coreApiShareReshareToShares1/reShare.feature:283](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares1/reShare.feature#L283) -- [coreApiShareReshareToShares1/reShare.feature:282](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares1/reShare.feature#L282) -- [coreApiShareReshareToShares1/reShare.feature:301](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares1/reShare.feature#L301) -- [coreApiShareReshareToShares1/reShare.feature:300](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares1/reShare.feature#L300) +- [coreApiShareReshareToShares1/reShare.feature:267](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares1/reShare.feature#L267) +- [coreApiShareReshareToShares1/reShare.feature:268](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares1/reShare.feature#L268) +- [coreApiShareReshareToShares1/reShare.feature:285](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares1/reShare.feature#L285) +- [coreApiShareReshareToShares1/reShare.feature:286](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares1/reShare.feature#L286) +- [coreApiShareReshareToShares1/reShare.feature:303](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares1/reShare.feature#L303) +- [coreApiShareReshareToShares1/reShare.feature:304](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares1/reShare.feature#L304) #### [incorrect ocs(v2) status value when getting info of share that does not exist should be 404, gives 998](https://github.com/owncloud/product/issues/250) _ocs: api compatibility, return correct status code_ @@ -590,8 +585,8 @@ _ocs: api compatibility, return correct status code_ - [coreApiWebdavProperties2/getFileProperties.feature:276](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavProperties2/getFileProperties.feature#L276) #### [Cannot move folder/file from one received share to another](https://github.com/owncloud/ocis/issues/2442) -- [coreApiShareUpdateToShares/updateShare.feature:194](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L194) -- [coreApiShareUpdateToShares/updateShare.feature:158](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L158) +- [coreApiShareUpdateToShares/updateShare.feature:197](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L197) +- [coreApiShareUpdateToShares/updateShare.feature:161](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature#L161) - [coreApiShareManagementToShares/mergeShare.feature:131](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareManagementToShares/mergeShare.feature#L131) #### [Sharing folder and sub-folder with same user but different permission,the permission of sub-folder is not obeyed ](https://github.com/owncloud/ocis/issues/2440) @@ -647,11 +642,11 @@ _ocs: api compatibility, return correct status code_ - [coreApiTrashbin/trashbinFilesFolders.feature:249](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L249) #### [Cannot disable the dav propfind depth infinity for resources](https://github.com/owncloud/ocis/issues/3720) -- [coreApiWebdavOperations/listFiles.feature:364](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavOperations/listFiles.feature#L364) -- [coreApiWebdavOperations/listFiles.feature:365](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavOperations/listFiles.feature#L365) -- [coreApiWebdavOperations/listFiles.feature:384](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavOperations/listFiles.feature#L384) -- [coreApiWebdavOperations/listFiles.feature:403](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavOperations/listFiles.feature#L403) -- [coreApiWebdavOperations/listFiles.feature:404](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavOperations/listFiles.feature#L404) +- [coreApiWebdavOperations/listFiles.feature:355](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavOperations/listFiles.feature#L355) +- [coreApiWebdavOperations/listFiles.feature:356](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavOperations/listFiles.feature#L356) +- [coreApiWebdavOperations/listFiles.feature:375](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavOperations/listFiles.feature#L375) +- [coreApiWebdavOperations/listFiles.feature:394](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavOperations/listFiles.feature#L394) +- [coreApiWebdavOperations/listFiles.feature:395](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavOperations/listFiles.feature#L395) #### [trash-bin propfind responses are wrong in reva master](https://github.com/cs3org/reva/issues/2861) - [coreApiTrashbin/trashbinDelete.feature:29](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinDelete.feature#L29) @@ -673,16 +668,13 @@ _ocs: api compatibility, return correct status code_ - [coreApiTrashbin/trashbinFilesFolders.feature:131](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L131) - [coreApiTrashbin/trashbinFilesFolders.feature:154](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L154) - [coreApiTrashbin/trashbinFilesFolders.feature:287](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L287) -- [coreApiTrashbin/trashbinFilesFolders.feature:308](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L308) - [coreApiTrashbin/trashbinFilesFolders.feature:305](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L305) - [coreApiTrashbin/trashbinFilesFolders.feature:306](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L306) - [coreApiTrashbin/trashbinFilesFolders.feature:307](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L307) -- [coreApiTrashbin/trashbinFilesFolders.feature:309](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L309) -- [coreApiTrashbin/trashbinFilesFolders.feature:310](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L310) -- [coreApiTrashbin/trashbinFilesFolders.feature:332](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L332) -- [coreApiTrashbin/trashbinFilesFolders.feature:352](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L352) -- [coreApiTrashbin/trashbinFilesFolders.feature:443](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L443) -- [coreApiTrashbin/trashbinFilesFolders.feature:406](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L406) +- [coreApiTrashbin/trashbinFilesFolders.feature:326](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L326) +- [coreApiTrashbin/trashbinFilesFolders.feature:346](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L346) +- [coreApiTrashbin/trashbinFilesFolders.feature:400](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L400) +- [coreApiTrashbin/trashbinFilesFolders.feature:437](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinFilesFolders.feature#L437) - [coreApiTrashbin/trashbinSharingToShares.feature:22](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinSharingToShares.feature#L22) - [coreApiTrashbin/trashbinSharingToShares.feature:205](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinSharingToShares.feature#L205) - [coreApiTrashbin/trashbinSharingToShares.feature:229](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiTrashbin/trashbinSharingToShares.feature#L229) @@ -764,12 +756,11 @@ _ocs: api compatibility, return correct status code_ #### [WebDAV MOVE with body returns 400 rather than 415](https://github.com/cs3org/reva/issues/3119) -- [coreApiAuthWebDav/webDavMOVEAuth.feature:136](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavMOVEAuth.feature#L136) +- [coreApiAuthWebDav/webDavMOVEAuth.feature:106](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavMOVEAuth.feature#L106) #### [reShareUpdate API tests failing in reva](https://github.com/cs3org/reva/issues/2916) - -- [coreApiShareReshareToShares3/reShareUpdate.feature:158](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareUpdate.feature#L158) -- [coreApiShareReshareToShares3/reShareUpdate.feature:157](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareUpdate.feature#L157) +- [coreApiShareReshareToShares3/reShareUpdate.feature:153](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareUpdate.feature#L153) +- [coreApiShareReshareToShares3/reShareUpdate.feature:154](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareReshareToShares3/reShareUpdate.feature#L154) #### [coreApiShareOperationsToShares1/gettingShares.feature:28 fails in CI](https://github.com/cs3org/reva/issues/2926) @@ -777,11 +768,11 @@ _ocs: api compatibility, return correct status code_ - [coreApiShareOperationsToShares1/gettingShares.feature:41](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareOperationsToShares1/gettingShares.feature#L41) #### [These tests pass in ocis and reva egde but fail in master with `file_target has unexpected value '/home'`](https://github.com/owncloud/ocis/issues/2113) -- [coreApiSharePublicLink1/createPublicLinkShare.feature:308](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L308) -- [coreApiSharePublicLink1/createPublicLinkShare.feature:309](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L309) +- [coreApiSharePublicLink1/createPublicLinkShare.feature:280](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L280) +- [coreApiSharePublicLink1/createPublicLinkShare.feature:281](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink1/createPublicLinkShare.feature#L281) #### [valid WebDAV (DELETE, COPY or MOVE) requests with body must exit with 415](https://github.com/owncloud/ocis/issues/4332) -- [coreApiAuthWebDav/webDavCOPYAuth.feature:136](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavCOPYAuth.feature#L136) +- [coreApiAuthWebDav/webDavCOPYAuth.feature:106](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiAuthWebDav/webDavCOPYAuth.feature#L106) #### [PROPFIND on (password protected) public link returns invalid XML](https://github.com/owncloud/ocis/issues/39707) The problem has been fixed in reva edge branch but not in reva master @@ -789,15 +780,19 @@ The problem has been fixed in reva edge branch but not in reva master - [coreApiWebdavOperations/propfind.feature:73](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiWebdavOperations/propfind.feature#L73) #### [Updating the role of a public link to internal gives returns 400] -- [coreApiSharePublicLink3/updatePublicLinkShare.feature:492](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/updatePublicLinkShare.feature#L492) -- [coreApiSharePublicLink3/updatePublicLinkShare.feature:493](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/updatePublicLinkShare.feature#L493) -- [coreApiSharePublicLink3/updatePublicLinkShare.feature:494](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/updatePublicLinkShare.feature#L494) -- [coreApiSharePublicLink3/updatePublicLinkShare.feature:495](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/updatePublicLinkShare.feature#L495) +- [coreApiSharePublicLink3/updatePublicLinkShare.feature:483](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/updatePublicLinkShare.feature#L483) +- [coreApiSharePublicLink3/updatePublicLinkShare.feature:484](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/updatePublicLinkShare.feature#L484) +- [coreApiSharePublicLink3/updatePublicLinkShare.feature:485](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/updatePublicLinkShare.feature#L485) +- [coreApiSharePublicLink3/updatePublicLinkShare.feature:486](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiSharePublicLink3/updatePublicLinkShare.feature#L486) #### [Default capabilities for normal user and admin user not same as in oC-core](https://github.com/owncloud/ocis/issues/1285) - [coreApiCapabilities/capabilities.feature:11](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiCapabilities/capabilities.feature#L11) - [coreApiCapabilities/capabilities.feature:136](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiCapabilities/capabilities.feature#L136) - [coreApiCapabilities/capabilities.feature:175](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiCapabilities/capabilities.feature#L175) +#### [Sharing of project space root via public link does no longer work](https://github.com/owncloud/ocis/issues/6278) +- [coreApiShareCreateSpecialToShares2/createShareDefaultFolderForReceivedShares.feature:23](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareCreateSpecialToShares2/createShareDefaultFolderForReceivedShares.feature#L23) +- [coreApiShareCreateSpecialToShares2/createShareDefaultFolderForReceivedShares.feature:24](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/coreApiShareCreateSpecialToShares2/createShareDefaultFolderForReceivedShares.feature#L24) + Note: always have an empty line at the end of this file. The bash script that processes this file may not process a scenario reference on the last line. diff --git a/tests/ocis b/tests/ocis index 1788406b52..7094891f4d 160000 --- a/tests/ocis +++ b/tests/ocis @@ -1 +1 @@ -Subproject commit 1788406b5273782d5bad44543ba5b9f94d48370d +Subproject commit 7094891f4de381102b05c6503751dc85d82c0782