Merge pull request #9377 from fschade/bump-reva-7fb5f11a24cf
enhancement: bump reva and remove explicit automount mountpoint naming
This commit is contained in:
@@ -2,6 +2,7 @@ Enhancement: Bump Reva
|
||||
|
||||
bumps reva version
|
||||
|
||||
https://github.com/owncloud/ocis/pull/9377
|
||||
https://github.com/owncloud/ocis/pull/9330
|
||||
https://github.com/owncloud/ocis/pull/9318
|
||||
https://github.com/owncloud/ocis/pull/9269
|
||||
|
||||
@@ -15,7 +15,7 @@ require (
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible
|
||||
github.com/coreos/go-oidc/v3 v3.10.0
|
||||
github.com/cs3org/go-cs3apis v0.0.0-20231023073225-7748710e0781
|
||||
github.com/cs3org/reva/v2 v2.19.2-0.20240607120909-0a9c7b8bef6a
|
||||
github.com/cs3org/reva/v2 v2.19.2-0.20240618080316-ed0273c9db9b
|
||||
github.com/dhowden/tag v0.0.0-20230630033851-978a0926ee25
|
||||
github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e
|
||||
github.com/egirna/icap-client v0.1.1
|
||||
|
||||
@@ -1024,8 +1024,8 @@ github.com/crewjam/saml v0.4.14 h1:g9FBNx62osKusnFzs3QTN5L9CVA/Egfgm+stJShzw/c=
|
||||
github.com/crewjam/saml v0.4.14/go.mod h1:UVSZCf18jJkk6GpWNVqcyQJMD5HsRugBPf4I1nl2mME=
|
||||
github.com/cs3org/go-cs3apis v0.0.0-20231023073225-7748710e0781 h1:BUdwkIlf8IS2FasrrPg8gGPHQPOrQ18MS1Oew2tmGtY=
|
||||
github.com/cs3org/go-cs3apis v0.0.0-20231023073225-7748710e0781/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY=
|
||||
github.com/cs3org/reva/v2 v2.19.2-0.20240607120909-0a9c7b8bef6a h1:D1Pg9i+pZhzn/h5397/ukX0XYYPBaUVLh6Zhe83vKUg=
|
||||
github.com/cs3org/reva/v2 v2.19.2-0.20240607120909-0a9c7b8bef6a/go.mod h1:lKqw0VuP1NcZbhj0e6tGoAGq3tgWO/pLafVJyDK0yVI=
|
||||
github.com/cs3org/reva/v2 v2.19.2-0.20240618080316-ed0273c9db9b h1:IPFiNd8Xev9WxAt+LkJUxnbyU8Y1rGi5Ha0S+ZNObr8=
|
||||
github.com/cs3org/reva/v2 v2.19.2-0.20240618080316-ed0273c9db9b/go.mod h1:Rb2XnhpGKnH7k6WBFZlMygbyBxW6ma09Z4Uk+ro0v+A=
|
||||
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
|
||||
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
|
||||
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
|
||||
|
||||
@@ -3,17 +3,14 @@ package command
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cs3org/reva/v2/pkg/events"
|
||||
"github.com/cs3org/reva/v2/pkg/events/stream"
|
||||
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/cs3org/reva/v2/pkg/utils"
|
||||
"go-micro.dev/v4/metadata"
|
||||
"google.golang.org/protobuf/types/known/fieldmaskpb"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/middleware"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
@@ -21,15 +18,13 @@ import (
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/tracing"
|
||||
"github.com/owncloud/ocis/v2/services/frontend/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/settings/pkg/store/defaults"
|
||||
"go-micro.dev/v4/metadata"
|
||||
"google.golang.org/protobuf/types/known/fieldmaskpb"
|
||||
|
||||
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"
|
||||
|
||||
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
|
||||
)
|
||||
|
||||
@@ -116,41 +111,23 @@ func AutoAcceptShares(ev events.ShareCreated, autoAcceptDefault bool, l log.Logg
|
||||
return
|
||||
}
|
||||
|
||||
uids, err := getUserIDs(ctx, gatewaySelector, ev.GranteeUserID, ev.GranteeGroupID)
|
||||
userIDs, err := getUserIDs(ctx, gatewaySelector, ev.GranteeUserID, ev.GranteeGroupID)
|
||||
if err != nil {
|
||||
l.Error().Err(err).Msg("cannot get grantees")
|
||||
return
|
||||
}
|
||||
|
||||
gwc, err = gatewaySelector.Next()
|
||||
if err != nil {
|
||||
l.Error().Err(err).Msg("cannot get gateway client")
|
||||
return
|
||||
}
|
||||
info, err := utils.GetResourceByID(ctx, ev.ItemID, gwc)
|
||||
if err != nil {
|
||||
l.Error().Err(err).Msg("error getting resource")
|
||||
return
|
||||
}
|
||||
|
||||
for _, uid := range uids {
|
||||
for _, uid := range userIDs {
|
||||
if !autoAcceptShares(ctx, uid, autoAcceptDefault, vs) {
|
||||
continue
|
||||
}
|
||||
|
||||
mp, err := getMountPoint(ctx, l, ev.ItemID, uid, gatewaySelector, info)
|
||||
if err != nil {
|
||||
l.Error().Err(err).Msg("error getting mount point")
|
||||
continue
|
||||
|
||||
}
|
||||
|
||||
gwc, err := gatewaySelector.Next()
|
||||
if err != nil {
|
||||
l.Error().Err(err).Msg("cannot get gateway client")
|
||||
continue
|
||||
}
|
||||
resp, err := gwc.UpdateReceivedShare(ctx, updateShareRequest(ev.ShareID, uid, mp))
|
||||
resp, err := gwc.UpdateReceivedShare(ctx, updateShareRequest(ev.ShareID, uid))
|
||||
if err != nil {
|
||||
l.Error().Err(err).Msg("error sending grpc request")
|
||||
continue
|
||||
@@ -163,66 +140,6 @@ func AutoAcceptShares(ev events.ShareCreated, autoAcceptDefault bool, l log.Logg
|
||||
|
||||
}
|
||||
|
||||
func getMountPoint(ctx context.Context, l log.Logger, itemid *provider.ResourceId, uid *user.UserId, gatewaySelector pool.Selectable[gateway.GatewayAPIClient], info *provider.ResourceInfo) (string, error) {
|
||||
lrs, err := getSharesList(ctx, gatewaySelector, uid)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// we need to sort the received shares by mount point in order to make things easier to evaluate.
|
||||
base := path.Base(info.GetPath())
|
||||
mount := base
|
||||
mounts := make([]string, 0, len(lrs.Shares))
|
||||
var exists bool
|
||||
|
||||
for _, s := range lrs.Shares {
|
||||
if s.State != collaboration.ShareState_SHARE_STATE_ACCEPTED {
|
||||
// we don't care about unaccepted shares
|
||||
continue
|
||||
}
|
||||
|
||||
if utils.ResourceIDEqual(s.GetShare().GetResourceId(), itemid) {
|
||||
// a share to the same resource already exists and is mounted
|
||||
return s.GetMountPoint().GetPath(), nil
|
||||
}
|
||||
|
||||
if s.GetMountPoint().GetPath() == mount {
|
||||
// does the shared resource still exist?
|
||||
gwc, err := gatewaySelector.Next()
|
||||
if err != nil {
|
||||
l.Error().Err(err).Msg("cannot get gateway client")
|
||||
continue
|
||||
}
|
||||
_, err = utils.GetResourceByID(ctx, s.GetShare().GetResourceId(), gwc)
|
||||
if err == nil {
|
||||
exists = true
|
||||
}
|
||||
// TODO we could delete shares here if the stat returns code NOT FOUND ... but listening for file deletes would be better
|
||||
}
|
||||
// collect all mount points
|
||||
mounts = append(mounts, s.GetMountPoint().GetPath())
|
||||
}
|
||||
|
||||
// If the mount point really already exists, we need to insert a number into the filename
|
||||
if exists {
|
||||
// now we have a list of shares, we want to iterate over all of them and check for name collisions agents a mount points list
|
||||
for i := 1; i <= len(mounts)+1; i++ {
|
||||
ext := filepath.Ext(base)
|
||||
name := strings.TrimSuffix(base, ext)
|
||||
// be smart about .tar.(gz|bz) files
|
||||
if strings.HasSuffix(name, ".tar") {
|
||||
name = strings.TrimSuffix(name, ".tar")
|
||||
ext = ".tar" + ext
|
||||
}
|
||||
mount = name + " (" + strconv.Itoa(i) + ")" + ext
|
||||
if !slices.Contains(mounts, mount) {
|
||||
return mount, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return mount, nil
|
||||
}
|
||||
|
||||
func getUserIDs(ctx context.Context, gatewaySelector pool.Selectable[gateway.GatewayAPIClient], uid *user.UserId, gid *group.GroupId) ([]*user.UserId, error) {
|
||||
if uid != nil {
|
||||
return []*user.UserId{uid}, nil
|
||||
@@ -257,40 +174,15 @@ func autoAcceptShares(ctx context.Context, uid *user.UserId, defaultValue bool,
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func updateShareRequest(shareID *collaboration.ShareId, uid *user.UserId, path string) *collaboration.UpdateReceivedShareRequest {
|
||||
func updateShareRequest(shareID *collaboration.ShareId, uid *user.UserId) *collaboration.UpdateReceivedShareRequest {
|
||||
return &collaboration.UpdateReceivedShareRequest{
|
||||
Opaque: utils.AppendJSONToOpaque(nil, "userid", uid),
|
||||
Share: &collaboration.ReceivedShare{
|
||||
Share: &collaboration.Share{
|
||||
Id: shareID,
|
||||
},
|
||||
MountPoint: &provider.Reference{
|
||||
Path: path,
|
||||
},
|
||||
State: collaboration.ShareState_SHARE_STATE_ACCEPTED,
|
||||
},
|
||||
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"state", "mount_point"}},
|
||||
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"state"}},
|
||||
}
|
||||
}
|
||||
|
||||
// getSharesList gets the list of all shares for the given user.
|
||||
func getSharesList(ctx context.Context, gatewaySelector pool.Selectable[gateway.GatewayAPIClient], uid *user.UserId) (*collaboration.ListReceivedSharesResponse, error) {
|
||||
gwc, err := gatewaySelector.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
shares, err := gwc.ListReceivedShares(ctx, &collaboration.ListReceivedSharesRequest{
|
||||
Opaque: utils.AppendJSONToOpaque(nil, "userid", uid),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if shares.Status.Code != rpc.Code_CODE_OK {
|
||||
if shares.Status.Code == rpc.Code_CODE_NOT_FOUND {
|
||||
return nil, fmt.Errorf("not found")
|
||||
}
|
||||
return nil, fmt.Errorf(shares.GetStatus().GetMessage())
|
||||
}
|
||||
return shares, nil
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
|
||||
"github.com/cs3org/reva/v2/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -19,11 +20,12 @@ import (
|
||||
// This function is particularly useful when dealing with CS3 responses,
|
||||
// and a unified error handling within the application is necessary.
|
||||
func FromCS3Status(status *cs3rpc.Status, inerr error, ignore ...cs3rpc.Code) error {
|
||||
if inerr != nil {
|
||||
return Error{msg: inerr.Error(), errorCode: GeneralException}
|
||||
}
|
||||
err := Error{errorCode: GeneralException, msg: "unspecified error has occurred", origin: ErrorOriginCS3}
|
||||
|
||||
err := Error{errorCode: GeneralException, msg: "unspecified error has occurred"}
|
||||
if inerr != nil {
|
||||
err.msg = inerr.Error()
|
||||
return err
|
||||
}
|
||||
|
||||
if status != nil {
|
||||
err.msg = status.GetMessage()
|
||||
|
||||
@@ -44,6 +44,11 @@ func TestFromCS3Status(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
var e errorcode.Error
|
||||
if errors.As(test.expected, &e) {
|
||||
test.expected = e.WithOrigin(errorcode.ErrorOriginCS3)
|
||||
}
|
||||
|
||||
if got := errorcode.FromCS3Status(test.status, test.err, test.ignore...); !reflect.DeepEqual(got, test.expected) {
|
||||
t.Error("Test Failed: {} expected, received: {}", test.expected, got)
|
||||
}
|
||||
@@ -56,7 +61,7 @@ func TestFromStat(t *testing.T) {
|
||||
err error
|
||||
result error
|
||||
}{
|
||||
{nil, errors.New("some error"), errorcode.New(errorcode.GeneralException, "some error")},
|
||||
{nil, errors.New("some error"), errorcode.New(errorcode.GeneralException, "some error").WithOrigin(errorcode.ErrorOriginCS3)},
|
||||
{&provider.StatResponse{Status: &cs3rpc.Status{Code: cs3rpc.Code_CODE_OK}}, nil, nil},
|
||||
}
|
||||
|
||||
|
||||
@@ -12,15 +12,28 @@ import (
|
||||
libregraph "github.com/owncloud/libre-graph-api-go"
|
||||
)
|
||||
|
||||
// ErrorCode defines code as used in MS Graph - see https://docs.microsoft.com/en-us/graph/errors?context=graph%2Fapi%2F1.0&view=graph-rest-1.0
|
||||
type ErrorCode int
|
||||
|
||||
// Error defines a custom error struct, containing and MS Graph error code and a textual error message
|
||||
type Error struct {
|
||||
errorCode ErrorCode
|
||||
msg string
|
||||
origin ErrorOrigin
|
||||
}
|
||||
|
||||
// ErrorOrigin gives information about where the error originated
|
||||
type ErrorOrigin int
|
||||
|
||||
const (
|
||||
// ErrorOriginUnknown is the default error source
|
||||
// and indicates that the error does not have any information about its origin
|
||||
ErrorOriginUnknown ErrorOrigin = iota
|
||||
|
||||
// ErrorOriginCS3 indicates that the error originated from a CS3 service
|
||||
ErrorOriginCS3
|
||||
)
|
||||
|
||||
// ErrorCode defines code as used in MS Graph - see https://docs.microsoft.com/en-us/graph/errors?context=graph%2Fapi%2F1.0&view=graph-rest-1.0
|
||||
type ErrorCode int
|
||||
|
||||
// List taken from https://github.com/microsoft/microsoft-graph-docs-1/blob/main/concepts/errors.md#code-property
|
||||
const (
|
||||
// AccessDenied defines the error if the caller doesn't have permission to perform the action.
|
||||
@@ -148,7 +161,7 @@ func (e ErrorCode) String() string {
|
||||
return errorCodes[e]
|
||||
}
|
||||
|
||||
// Error return the concatenation of the error string and optional message
|
||||
// Error returns the concatenation of the error string and optional message
|
||||
func (e Error) Error() string {
|
||||
errString := errorCodes[e.errorCode]
|
||||
if e.msg != "" {
|
||||
@@ -161,13 +174,35 @@ func (e Error) GetCode() ErrorCode {
|
||||
return e.errorCode
|
||||
}
|
||||
|
||||
// GetOrigin returns the source of the error
|
||||
func (e Error) GetOrigin() ErrorOrigin {
|
||||
return e.origin
|
||||
}
|
||||
|
||||
// WithOrigin returns a new Error with the provided origin
|
||||
func (e Error) WithOrigin(o ErrorOrigin) Error {
|
||||
e.origin = o
|
||||
return e
|
||||
}
|
||||
|
||||
// RenderError render the Graph Error based on a code or default one
|
||||
func RenderError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
var errcode Error
|
||||
if errors.As(err, &errcode) {
|
||||
errcode.Render(w, r)
|
||||
e, ok := ToError(err)
|
||||
if !ok {
|
||||
GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
|
||||
e.Render(w, r)
|
||||
}
|
||||
|
||||
// ToError checks if the error is of type Error and returns it,
|
||||
// the second parameter indicates if the error conversion was successful
|
||||
func ToError(err error) (Error, bool) {
|
||||
var e Error
|
||||
if errors.As(err, &e) {
|
||||
return e, true
|
||||
}
|
||||
|
||||
return Error{}, false
|
||||
}
|
||||
|
||||
@@ -17,12 +17,6 @@ import (
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
roleconversions "github.com/cs3org/reva/v2/pkg/conversions"
|
||||
revactx "github.com/cs3org/reva/v2/pkg/ctx"
|
||||
"github.com/cs3org/reva/v2/pkg/rgrpc/status"
|
||||
"github.com/cs3org/reva/v2/pkg/storagespace"
|
||||
"github.com/cs3org/reva/v2/pkg/utils"
|
||||
cs3mocks "github.com/cs3org/reva/v2/tests/cs3mocks/mocks"
|
||||
"github.com/go-chi/chi/v5"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
@@ -31,6 +25,13 @@ import (
|
||||
"github.com/tidwall/gjson"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
roleconversions "github.com/cs3org/reva/v2/pkg/conversions"
|
||||
revactx "github.com/cs3org/reva/v2/pkg/ctx"
|
||||
"github.com/cs3org/reva/v2/pkg/rgrpc/status"
|
||||
"github.com/cs3org/reva/v2/pkg/storagespace"
|
||||
"github.com/cs3org/reva/v2/pkg/utils"
|
||||
cs3mocks "github.com/cs3org/reva/v2/tests/cs3mocks/mocks"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/services/graph/mocks"
|
||||
"github.com/owncloud/ocis/v2/services/graph/pkg/config/defaults"
|
||||
@@ -249,7 +250,7 @@ var _ = Describe("DriveItemPermissionsService", func() {
|
||||
statResponse.Status = status.NewNotFound(context.Background(), "not found")
|
||||
permission, err := driveItemPermissionsService.Invite(context.Background(), driveItemId, driveItemInvite)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err).To(MatchError(errorcode.New(errorcode.ItemNotFound, "not found")))
|
||||
Expect(err).To(MatchError(errorcode.New(errorcode.ItemNotFound, "not found").WithOrigin(errorcode.ErrorOriginCS3)))
|
||||
Expect(permission).To(BeZero())
|
||||
})
|
||||
})
|
||||
@@ -999,7 +1000,7 @@ var _ = Describe("DriveItemPermissionsService", func() {
|
||||
|
||||
driveItemPermission.SetExpirationDateTime(expiration)
|
||||
res, err := driveItemPermissionsService.UpdatePermission(context.Background(), driveItemId, "permissionid", driveItemPermission)
|
||||
Expect(err).To(MatchError(errorcode.New(errorcode.InvalidRequest, "expiration date is in the past")))
|
||||
Expect(err).To(MatchError(errorcode.New(errorcode.InvalidRequest, "expiration date is in the past").WithOrigin(errorcode.ErrorOriginCS3)))
|
||||
Expect(res).To(BeZero())
|
||||
})
|
||||
})
|
||||
|
||||
@@ -360,7 +360,7 @@ func (api DrivesDriveItemApi) DeleteDriveItem(w http.ResponseWriter, r *http.Req
|
||||
|
||||
shareID := ExtractShareIdFromResourceId(itemID)
|
||||
if err := api.drivesDriveItemService.UnmountShare(ctx, shareID); err != nil {
|
||||
api.logger.Debug().Err(err).Msg(err.Error())
|
||||
api.logger.Debug().Err(err).Msg(ErrUnmountShare.Error())
|
||||
errorcode.RenderError(w, r, err)
|
||||
return
|
||||
}
|
||||
@@ -518,8 +518,15 @@ func (api DrivesDriveItemApi) CreateDriveItem(w http.ResponseWriter, r *http.Req
|
||||
|
||||
mountedShares, err := api.drivesDriveItemService.MountShare(ctx, &resourceId, requestDriveItem.GetName())
|
||||
if err != nil {
|
||||
api.logger.Debug().Err(err).Msg(err.Error())
|
||||
errorcode.RenderError(w, r, err)
|
||||
api.logger.Debug().Err(err).Msg(ErrMountShare.Error())
|
||||
|
||||
switch e, ok := errorcode.ToError(err); {
|
||||
case ok && e.GetOrigin() == errorcode.ErrorOriginCS3 && e.GetCode() == errorcode.ItemNotFound:
|
||||
ErrDriveItemConversion.Render(w, r)
|
||||
default:
|
||||
errorcode.RenderError(w, r, err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ var _ = Describe("DrivesDriveItemService", func() {
|
||||
Once()
|
||||
|
||||
_, err := drivesDriveItemService.GetShare(context.Background(), &collaborationv1beta1.ShareId{})
|
||||
Expect(err).To(MatchError(errorcode.New(errorcode.GeneralException, someErr.Error())))
|
||||
Expect(err).To(MatchError(errorcode.New(errorcode.GeneralException, someErr.Error()).WithOrigin(errorcode.ErrorOriginCS3)))
|
||||
})
|
||||
|
||||
It("fails if share lookup does not report an error but the status is off", func() {
|
||||
@@ -161,12 +161,12 @@ var _ = Describe("DrivesDriveItemService", func() {
|
||||
EXPECT().
|
||||
GetReceivedShare(context.Background(), mock.Anything, mock.Anything).
|
||||
Return(&collaborationv1beta1.GetReceivedShareResponse{
|
||||
Status: status.NewNotFound(context.Background(), someErr.Error()),
|
||||
Status: status.NewInvalid(context.Background(), someErr.Error()),
|
||||
}, nil).
|
||||
Once()
|
||||
|
||||
_, err := drivesDriveItemService.GetShare(context.Background(), &collaborationv1beta1.ShareId{})
|
||||
Expect(err).To(MatchError(errorcode.New(errorcode.ItemNotFound, someErr.Error())))
|
||||
Expect(err).To(MatchError(errorcode.New(errorcode.InvalidRequest, someErr.Error()).WithOrigin(errorcode.ErrorOriginCS3)))
|
||||
})
|
||||
|
||||
It("successfully returns a share", func() {
|
||||
@@ -219,7 +219,7 @@ var _ = Describe("DrivesDriveItemService", func() {
|
||||
request.Share.State = collaborationv1beta1.ShareState_SHARE_STATE_ACCEPTED
|
||||
request.UpdateMask.Paths = append(request.UpdateMask.Paths, "state")
|
||||
})
|
||||
Expect(err).To(MatchError(errorcode.New(errorcode.GeneralException, someErr.Error())))
|
||||
Expect(err).To(MatchError(errorcode.New(errorcode.GeneralException, someErr.Error()).WithOrigin(errorcode.ErrorOriginCS3)))
|
||||
})
|
||||
|
||||
It("fails if share update does not report an error but the status is off", func() {
|
||||
@@ -236,7 +236,7 @@ var _ = Describe("DrivesDriveItemService", func() {
|
||||
request.Share.State = collaborationv1beta1.ShareState_SHARE_STATE_ACCEPTED
|
||||
request.UpdateMask.Paths = append(request.UpdateMask.Paths, "state")
|
||||
})
|
||||
Expect(err).To(MatchError(errorcode.New(errorcode.ItemNotFound, someErr.Error())))
|
||||
Expect(err).To(MatchError(errorcode.New(errorcode.ItemNotFound, someErr.Error()).WithOrigin(errorcode.ErrorOriginCS3)))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -265,7 +265,7 @@ var _ = Describe("DrivesDriveItemService", func() {
|
||||
request.Share.State = collaborationv1beta1.ShareState_SHARE_STATE_ACCEPTED
|
||||
request.UpdateMask.Paths = append(request.UpdateMask.Paths, "state")
|
||||
})
|
||||
Expect(err).To(MatchError(errorcode.New(errorcode.GeneralException, someErr.Error())))
|
||||
Expect(err).To(MatchError(errorcode.New(errorcode.GeneralException, someErr.Error()).WithOrigin(errorcode.ErrorOriginCS3)))
|
||||
Expect(err.(interface{ Unwrap() []error }).Unwrap()).To(HaveLen(2))
|
||||
Expect(shares).To(HaveLen(1))
|
||||
})
|
||||
@@ -281,7 +281,7 @@ var _ = Describe("DrivesDriveItemService", func() {
|
||||
Once()
|
||||
|
||||
err := drivesDriveItemService.UnmountShare(context.Background(), &collaborationv1beta1.ShareId{})
|
||||
Expect(err).To(MatchError(errorcode.New(errorcode.GeneralException, someErr.Error())))
|
||||
Expect(err).To(MatchError(errorcode.New(errorcode.GeneralException, someErr.Error()).WithOrigin(errorcode.ErrorOriginCS3)))
|
||||
})
|
||||
|
||||
It("requests only accepted shares to be unmounted", func() {
|
||||
@@ -358,7 +358,7 @@ var _ = Describe("DrivesDriveItemService", func() {
|
||||
Times(1)
|
||||
|
||||
err := drivesDriveItemService.UnmountShare(context.Background(), &collaborationv1beta1.ShareId{})
|
||||
Expect(err).To(MatchError(errorcode.New(errorcode.GeneralException, someErr.Error())))
|
||||
Expect(err).To(MatchError(errorcode.New(errorcode.GeneralException, someErr.Error()).WithOrigin(errorcode.ErrorOriginCS3)))
|
||||
Expect(err.(interface{ Unwrap() []error }).Unwrap()).To(HaveLen(1))
|
||||
})
|
||||
})
|
||||
@@ -436,7 +436,7 @@ var _ = Describe("DrivesDriveItemService", func() {
|
||||
Times(3)
|
||||
|
||||
shares, err := drivesDriveItemService.MountShare(context.Background(), nil, "some")
|
||||
Expect(err).To(MatchError(errorcode.New(errorcode.GeneralException, someErr.Error())))
|
||||
Expect(err).To(MatchError(errorcode.New(errorcode.GeneralException, someErr.Error()).WithOrigin(errorcode.ErrorOriginCS3)))
|
||||
Expect(err.(interface{ Unwrap() []error }).Unwrap()).To(HaveLen(3))
|
||||
Expect(shares).To(HaveLen(0))
|
||||
})
|
||||
|
||||
Generated
Vendored
+167
-22
@@ -20,8 +20,11 @@ package usershareprovider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
@@ -46,6 +49,12 @@ import (
|
||||
"github.com/cs3org/reva/v2/pkg/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
_fieldMaskPathMountPoint = "mount_point"
|
||||
_fieldMaskPathPermissions = "permissions"
|
||||
_fieldMaskPathState = "state"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rgrpc.Register("usershareprovider", NewDefault)
|
||||
}
|
||||
@@ -407,7 +416,7 @@ func (s *service) UpdateShare(ctx context.Context, req *collaboration.UpdateShar
|
||||
|
||||
// If this is a permissions update, check if user's permissions on the resource are sufficient to set the desired permissions
|
||||
var newPermissions *provider.ResourcePermissions
|
||||
if slices.Contains(req.GetUpdateMask().GetPaths(), "permissions") {
|
||||
if slices.Contains(req.GetUpdateMask().GetPaths(), _fieldMaskPathPermissions) {
|
||||
newPermissions = req.GetShare().GetPermissions().GetPermissions()
|
||||
} else {
|
||||
newPermissions = req.GetField().GetPermissions().GetPermissions()
|
||||
@@ -497,40 +506,176 @@ func (s *service) GetReceivedShare(ctx context.Context, req *collaboration.GetRe
|
||||
}
|
||||
|
||||
func (s *service) UpdateReceivedShare(ctx context.Context, req *collaboration.UpdateReceivedShareRequest) (*collaboration.UpdateReceivedShareResponse, error) {
|
||||
|
||||
if req.Share == nil {
|
||||
return &collaboration.UpdateReceivedShareResponse{
|
||||
Status: status.NewInvalid(ctx, "updating requires a received share object"),
|
||||
}, nil
|
||||
}
|
||||
if req.Share.Share == nil {
|
||||
return &collaboration.UpdateReceivedShareResponse{
|
||||
Status: status.NewInvalid(ctx, "share missing"),
|
||||
}, nil
|
||||
}
|
||||
if req.Share.Share.Id == nil {
|
||||
return &collaboration.UpdateReceivedShareResponse{
|
||||
Status: status.NewInvalid(ctx, "share id missing"),
|
||||
}, nil
|
||||
}
|
||||
if req.Share.Share.Id.OpaqueId == "" {
|
||||
if req.GetShare().GetShare().GetId().GetOpaqueId() == "" {
|
||||
return &collaboration.UpdateReceivedShareResponse{
|
||||
Status: status.NewInvalid(ctx, "share id empty"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
isStateTransitionShareAccepted := slices.Contains(req.GetUpdateMask().GetPaths(), _fieldMaskPathState) && req.GetShare().GetState() == collaboration.ShareState_SHARE_STATE_ACCEPTED
|
||||
isMountPointSet := slices.Contains(req.GetUpdateMask().GetPaths(), _fieldMaskPathMountPoint) && req.GetShare().GetMountPoint().GetPath() != ""
|
||||
// we calculate a valid mountpoint only if the share should be accepted and the mount point is not set explicitly
|
||||
if isStateTransitionShareAccepted && !isMountPointSet {
|
||||
gatewayClient, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s, err := setReceivedShareMountPoint(ctx, gatewayClient, req)
|
||||
switch {
|
||||
case err != nil:
|
||||
fallthrough
|
||||
case s.GetCode() != rpc.Code_CODE_OK:
|
||||
return &collaboration.UpdateReceivedShareResponse{
|
||||
Status: s,
|
||||
}, err
|
||||
}
|
||||
}
|
||||
|
||||
var uid userpb.UserId
|
||||
_ = utils.ReadJSONFromOpaque(req.Opaque, "userid", &uid)
|
||||
share, err := s.sm.UpdateReceivedShare(ctx, req.Share, req.UpdateMask, &uid)
|
||||
updatedShare, err := s.sm.UpdateReceivedShare(ctx, req.Share, req.UpdateMask, &uid)
|
||||
if err != nil {
|
||||
return &collaboration.UpdateReceivedShareResponse{
|
||||
Status: status.NewInternal(ctx, "error updating received share"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
res := &collaboration.UpdateReceivedShareResponse{
|
||||
return &collaboration.UpdateReceivedShareResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
Share: share,
|
||||
Share: updatedShare,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func setReceivedShareMountPoint(ctx context.Context, gwc gateway.GatewayAPIClient, req *collaboration.UpdateReceivedShareRequest) (*rpc.Status, error) {
|
||||
receivedShare, err := gwc.GetReceivedShare(ctx, &collaboration.GetReceivedShareRequest{
|
||||
Ref: &collaboration.ShareReference{
|
||||
Spec: &collaboration.ShareReference_Id{
|
||||
Id: req.GetShare().GetShare().GetId(),
|
||||
},
|
||||
},
|
||||
})
|
||||
switch {
|
||||
case err != nil:
|
||||
fallthrough
|
||||
case receivedShare.GetStatus().GetCode() != rpc.Code_CODE_OK:
|
||||
return receivedShare.GetStatus(), err
|
||||
}
|
||||
return res, nil
|
||||
|
||||
if receivedShare.GetShare().GetMountPoint().GetPath() != "" {
|
||||
return status.NewOK(ctx), nil
|
||||
}
|
||||
|
||||
resourceStat, err := gwc.Stat(ctx, &provider.StatRequest{
|
||||
Ref: &provider.Reference{
|
||||
ResourceId: receivedShare.GetShare().GetShare().GetResourceId(),
|
||||
},
|
||||
})
|
||||
switch {
|
||||
case err != nil:
|
||||
fallthrough
|
||||
case resourceStat.GetStatus().GetCode() != rpc.Code_CODE_OK:
|
||||
return resourceStat.GetStatus(), err
|
||||
}
|
||||
|
||||
// handle mount point related updates
|
||||
{
|
||||
var userID *userpb.UserId
|
||||
_ = utils.ReadJSONFromOpaque(req.Opaque, "userid", &userID)
|
||||
|
||||
// check if the requested mount point is available and if not, find a suitable one
|
||||
availableMountpoint, _, err := GetMountpointAndUnmountedShares(ctx, gwc,
|
||||
resourceStat.GetInfo().GetId(),
|
||||
resourceStat.GetInfo().GetName(),
|
||||
userID,
|
||||
)
|
||||
if err != nil {
|
||||
return status.NewInternal(ctx, err.Error()), nil
|
||||
}
|
||||
|
||||
if !slices.Contains(req.GetUpdateMask().GetPaths(), _fieldMaskPathMountPoint) {
|
||||
req.GetUpdateMask().Paths = append(req.GetUpdateMask().GetPaths(), _fieldMaskPathMountPoint)
|
||||
}
|
||||
|
||||
req.GetShare().MountPoint = &provider.Reference{
|
||||
Path: availableMountpoint,
|
||||
}
|
||||
}
|
||||
|
||||
return status.NewOK(ctx), nil
|
||||
}
|
||||
|
||||
// GetMountpointAndUnmountedShares returns a new or existing mountpoint for the given info and produces a list of unmounted received shares for the same resource
|
||||
func GetMountpointAndUnmountedShares(ctx context.Context, gwc gateway.GatewayAPIClient, id *provider.ResourceId, name string, userId *userpb.UserId) (string, []*collaboration.ReceivedShare, error) {
|
||||
listReceivedSharesReq := &collaboration.ListReceivedSharesRequest{}
|
||||
if userId != nil {
|
||||
listReceivedSharesReq.Opaque = utils.AppendJSONToOpaque(nil, "userid", userId)
|
||||
}
|
||||
|
||||
listReceivedSharesRes, err := gwc.ListReceivedShares(ctx, listReceivedSharesReq)
|
||||
if err != nil {
|
||||
return "", nil, errtypes.InternalError("grpc list received shares request failed")
|
||||
}
|
||||
|
||||
if err := errtypes.NewErrtypeFromStatus(listReceivedSharesRes.GetStatus()); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
unmountedShares := []*collaboration.ReceivedShare{}
|
||||
base := filepath.Clean(name)
|
||||
mount := base
|
||||
existingMountpoint := ""
|
||||
mountedShares := make([]string, 0, len(listReceivedSharesRes.GetShares()))
|
||||
var pathExists bool
|
||||
|
||||
for _, s := range listReceivedSharesRes.GetShares() {
|
||||
resourceIDEqual := utils.ResourceIDEqual(s.GetShare().GetResourceId(), id)
|
||||
|
||||
if resourceIDEqual && s.State == collaboration.ShareState_SHARE_STATE_ACCEPTED {
|
||||
// a share to the resource already exists and is mounted, remembers the mount point
|
||||
_, err := utils.GetResourceByID(ctx, s.GetShare().GetResourceId(), gwc)
|
||||
if err == nil {
|
||||
existingMountpoint = s.GetMountPoint().GetPath()
|
||||
}
|
||||
}
|
||||
|
||||
if resourceIDEqual && s.State != collaboration.ShareState_SHARE_STATE_ACCEPTED {
|
||||
// a share to the resource already exists but is not mounted, collect the unmounted share
|
||||
unmountedShares = append(unmountedShares, s)
|
||||
}
|
||||
|
||||
if s.State == collaboration.ShareState_SHARE_STATE_ACCEPTED {
|
||||
// collect all accepted mount points
|
||||
mountedShares = append(mountedShares, s.GetMountPoint().GetPath())
|
||||
if s.GetMountPoint().GetPath() == mount {
|
||||
// does the shared resource still exist?
|
||||
_, err := utils.GetResourceByID(ctx, s.GetShare().GetResourceId(), gwc)
|
||||
if err == nil {
|
||||
pathExists = true
|
||||
}
|
||||
// TODO we could delete shares here if the stat returns code NOT FOUND ... but listening for file deletes would be better
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if existingMountpoint != "" {
|
||||
// we want to reuse the same mountpoint for all unmounted shares to the same resource
|
||||
return existingMountpoint, unmountedShares, nil
|
||||
}
|
||||
|
||||
// If the mount point really already exists, we need to insert a number into the filename
|
||||
if pathExists {
|
||||
// now we have a list of shares, we want to iterate over all of them and check for name collisions agents a mount points list
|
||||
for i := 1; i <= len(mountedShares)+1; i++ {
|
||||
ext := filepath.Ext(base)
|
||||
name := strings.TrimSuffix(base, ext)
|
||||
|
||||
mount = name + " (" + strconv.Itoa(i) + ")" + ext
|
||||
if !slices.Contains(mountedShares, mount) {
|
||||
return mount, unmountedShares, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mount, unmountedShares, nil
|
||||
}
|
||||
|
||||
Generated
Vendored
+12
-79
@@ -23,24 +23,22 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
|
||||
ocmv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/protobuf/types/known/fieldmaskpb"
|
||||
|
||||
"github.com/cs3org/reva/v2/internal/grpc/services/usershareprovider"
|
||||
"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/response"
|
||||
"github.com/cs3org/reva/v2/pkg/appctx"
|
||||
"github.com/cs3org/reva/v2/pkg/conversions"
|
||||
"github.com/cs3org/reva/v2/pkg/errtypes"
|
||||
"github.com/cs3org/reva/v2/pkg/utils"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/protobuf/types/known/fieldmaskpb"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -75,8 +73,13 @@ func (h *Handler) AcceptReceivedShare(w http.ResponseWriter, r *http.Request) {
|
||||
response.WriteOCSResponse(w, r, *ocsResponse, nil)
|
||||
return
|
||||
}
|
||||
|
||||
mount, unmountedShares, err := GetMountpointAndUnmountedShares(ctx, client, sharedResource.Info)
|
||||
mount, unmountedShares, err := usershareprovider.GetMountpointAndUnmountedShares(
|
||||
ctx,
|
||||
client,
|
||||
sharedResource.GetInfo().GetId(),
|
||||
sharedResource.GetInfo().GetName(),
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "could not determine mountpoint", err)
|
||||
return
|
||||
@@ -119,76 +122,6 @@ func (h *Handler) AcceptReceivedShare(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// GetMountpointAndUnmountedShares returns a new or existing mountpoint for the given info and produces a list of unmounted received shares for the same resource
|
||||
func GetMountpointAndUnmountedShares(ctx context.Context, gwc gateway.GatewayAPIClient, info *provider.ResourceInfo) (string, []*collaboration.ReceivedShare, error) {
|
||||
unmountedShares := []*collaboration.ReceivedShare{}
|
||||
receivedShares, err := listReceivedShares(ctx, gwc)
|
||||
if err != nil {
|
||||
return "", unmountedShares, err
|
||||
}
|
||||
|
||||
// we need to sort the received shares by mount point in order to make things easier to evaluate.
|
||||
base := filepath.Clean(info.Name)
|
||||
mount := base
|
||||
existingMountpoint := ""
|
||||
mountedShares := make([]string, 0, len(receivedShares))
|
||||
var pathExists bool
|
||||
|
||||
for _, s := range receivedShares {
|
||||
resourceIDEqual := utils.ResourceIDEqual(s.GetShare().GetResourceId(), info.GetId())
|
||||
|
||||
if resourceIDEqual && s.State == collaboration.ShareState_SHARE_STATE_ACCEPTED {
|
||||
// a share to the resource already exists and is mounted, remember the mount point
|
||||
_, err := utils.GetResourceByID(ctx, s.GetShare().GetResourceId(), gwc)
|
||||
if err == nil {
|
||||
existingMountpoint = s.GetMountPoint().GetPath()
|
||||
}
|
||||
}
|
||||
|
||||
if resourceIDEqual && s.State != collaboration.ShareState_SHARE_STATE_ACCEPTED {
|
||||
// a share to the resource already exists but is not mounted, collect the unmounted share
|
||||
unmountedShares = append(unmountedShares, s)
|
||||
}
|
||||
|
||||
if s.State == collaboration.ShareState_SHARE_STATE_ACCEPTED {
|
||||
// collect all accepted mount points
|
||||
mountedShares = append(mountedShares, s.GetMountPoint().GetPath())
|
||||
if s.GetMountPoint().GetPath() == mount {
|
||||
// does the shared resource still exist?
|
||||
_, err := utils.GetResourceByID(ctx, s.GetShare().GetResourceId(), gwc)
|
||||
if err == nil {
|
||||
pathExists = true
|
||||
}
|
||||
// TODO we could delete shares here if the stat returns code NOT FOUND ... but listening for file deletes would be better
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if existingMountpoint != "" {
|
||||
// we want to reuse the same mountpoint for all unmounted shares to the same resource
|
||||
return existingMountpoint, unmountedShares, nil
|
||||
}
|
||||
|
||||
// If the mount point really already exists, we need to insert a number into the filename
|
||||
if pathExists {
|
||||
// now we have a list of shares, we want to iterate over all of them and check for name collisions agents a mount points list
|
||||
for i := 1; i <= len(mountedShares)+1; i++ {
|
||||
ext := filepath.Ext(base)
|
||||
name := strings.TrimSuffix(base, ext)
|
||||
// be smart about .tar.(gz|bz) files
|
||||
if strings.HasSuffix(name, ".tar") {
|
||||
name = strings.TrimSuffix(name, ".tar")
|
||||
ext = ".tar" + ext
|
||||
}
|
||||
mount = name + " (" + strconv.Itoa(i) + ")" + ext
|
||||
if !slices.Contains(mountedShares, mount) {
|
||||
return mount, unmountedShares, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return mount, unmountedShares, nil
|
||||
}
|
||||
|
||||
// RejectReceivedShare handles DELETE Requests on /apps/files_sharing/api/v1/shares/{shareid}
|
||||
func (h *Handler) RejectReceivedShare(w http.ResponseWriter, r *http.Request) {
|
||||
shareID := chi.URLParam(r, "shareid")
|
||||
|
||||
+2
-1
@@ -47,10 +47,11 @@ func (bs *Blobstore) Upload(node *node.Node, source string) error {
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
f, err := os.OpenFile(node.InternalPath(), os.O_CREATE|os.O_WRONLY, 0700)
|
||||
f, err := os.OpenFile(node.InternalPath(), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0700)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not open blob '%s' for writing", node.InternalPath())
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
w := bufio.NewWriter(f)
|
||||
_, err = w.ReadFrom(file)
|
||||
|
||||
Vendored
+1
-1
@@ -366,7 +366,7 @@ github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1
|
||||
github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1
|
||||
github.com/cs3org/go-cs3apis/cs3/tx/v1beta1
|
||||
github.com/cs3org/go-cs3apis/cs3/types/v1beta1
|
||||
# github.com/cs3org/reva/v2 v2.19.2-0.20240607120909-0a9c7b8bef6a
|
||||
# github.com/cs3org/reva/v2 v2.19.2-0.20240618080316-ed0273c9db9b
|
||||
## explicit; go 1.21
|
||||
github.com/cs3org/reva/v2/cmd/revad/internal/grace
|
||||
github.com/cs3org/reva/v2/cmd/revad/runtime
|
||||
|
||||
Reference in New Issue
Block a user