mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-05-20 03:48:41 -05:00
Merge pull request #2454 from aduffeck/search-based-favorites
Implement favorites
This commit is contained in:
@@ -14,7 +14,7 @@ require (
|
||||
github.com/blevesearch/bleve/v2 v2.5.7
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible
|
||||
github.com/coreos/go-oidc/v3 v3.17.0
|
||||
github.com/cs3org/go-cs3apis v0.0.0-20260130145416-2dc593dc27e7
|
||||
github.com/cs3org/go-cs3apis v0.0.0-20260310080202-fb97596763d6
|
||||
github.com/davidbyttow/govips/v2 v2.17.0
|
||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
|
||||
github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e
|
||||
@@ -64,8 +64,8 @@ require (
|
||||
github.com/onsi/gomega v1.39.1
|
||||
github.com/open-policy-agent/opa v1.14.1
|
||||
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89
|
||||
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260204102724-10bcda1b3068
|
||||
github.com/opencloud-eu/reva/v2 v2.42.5
|
||||
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d
|
||||
github.com/opencloud-eu/reva/v2 v2.42.6-0.20260311175421-d77bc89ffe35
|
||||
github.com/opensearch-project/opensearch-go/v4 v4.6.0
|
||||
github.com/orcaman/concurrent-map v1.0.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
@@ -238,11 +238,10 @@ require (
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/gomodule/redigo v1.9.3 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/go-tpm v0.9.8 // indirect
|
||||
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect
|
||||
github.com/google/renameio/v2 v2.0.1 // indirect
|
||||
github.com/google/renameio/v2 v2.0.2 // indirect
|
||||
github.com/gookit/goutil v0.7.1 // indirect
|
||||
github.com/gorilla/handlers v1.5.1 // indirect
|
||||
github.com/gorilla/schema v1.4.1 // indirect
|
||||
@@ -290,7 +289,7 @@ require (
|
||||
github.com/minio/crc64nvme v1.1.1 // indirect
|
||||
github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/minio/minio-go/v7 v7.0.98 // indirect
|
||||
github.com/minio/minio-go/v7 v7.0.99 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
|
||||
@@ -265,8 +265,8 @@ github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo
|
||||
github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4=
|
||||
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-20260130145416-2dc593dc27e7 h1:ez+InorrZ2HXkPmPeeDstPSTkjNmySUZXaT7xEJNOkk=
|
||||
github.com/cs3org/go-cs3apis v0.0.0-20260130145416-2dc593dc27e7/go.mod h1:DedpcqXl193qF/08Y04IO0PpxyyMu8+GrkD6kWK2MEQ=
|
||||
github.com/cs3org/go-cs3apis v0.0.0-20260310080202-fb97596763d6 h1:Akwn9gHJugKd8M48LyV+WeIQ6yMXoxZdgZabR53I9q4=
|
||||
github.com/cs3org/go-cs3apis v0.0.0-20260310080202-fb97596763d6/go.mod h1:DedpcqXl193qF/08Y04IO0PpxyyMu8+GrkD6kWK2MEQ=
|
||||
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
|
||||
github.com/cyphar/filepath-securejoin v0.5.1 h1:eYgfMq5yryL4fbWfkLpFFy2ukSELzaJOTaUTuh+oF48=
|
||||
github.com/cyphar/filepath-securejoin v0.5.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
@@ -542,8 +542,6 @@ github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8l
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
|
||||
github.com/gomodule/redigo v1.9.3 h1:dNPSXeXv6HCq2jdyWfjgmhBdqnR6PRO3m/G05nvpPC8=
|
||||
github.com/gomodule/redigo v1.9.3/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q=
|
||||
@@ -585,8 +583,8 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf
|
||||
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=
|
||||
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/renameio/v2 v2.0.1 h1:HyOM6qd9gF9sf15AvhbptGHUnaLTpEI9akAFFU3VyW0=
|
||||
github.com/google/renameio/v2 v2.0.1/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4=
|
||||
github.com/google/renameio/v2 v2.0.2 h1:qKZs+tfn+arruZZhQ7TKC/ergJunuJicWS6gLDt/dGw=
|
||||
github.com/google/renameio/v2 v2.0.2/go.mod h1:OX+G6WHHpHq3NVj7cAOleLOwJfcQ1s3uUJQCrr78SWo=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
@@ -852,8 +850,8 @@ github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 h1:KGuD/pM2JpL
|
||||
github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
|
||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.0.98 h1:MeAVKjLVz+XJ28zFcuYyImNSAh8Mq725uNW4beRisi0=
|
||||
github.com/minio/minio-go/v7 v7.0.98/go.mod h1:cY0Y+W7yozf0mdIclrttzo1Iiu7mEf9y7nk2uXqMOvM=
|
||||
github.com/minio/minio-go/v7 v7.0.99 h1:2vH/byrwUkIpFQFOilvTfaUpvAX3fEFhEzO+DR3DlCE=
|
||||
github.com/minio/minio-go/v7 v7.0.99/go.mod h1:EtGNKtlX20iL2yaYnxEigaIvj0G0GwSDnifnG8ClIdw=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
@@ -959,10 +957,10 @@ github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89 h1:W1ms+l
|
||||
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89/go.mod h1:vigJkNss1N2QEceCuNw/ullDehncuJNFB6mEnzfq9UI=
|
||||
github.com/opencloud-eu/inotifywaitgo v0.0.0-20251111171128-a390bae3c5e9 h1:dIftlX03Bzfbujhp9B54FbgER0VBDWJi/w8RBxJlzxU=
|
||||
github.com/opencloud-eu/inotifywaitgo v0.0.0-20251111171128-a390bae3c5e9/go.mod h1:JWyDC6H+5oZRdUJUgKuaye+8Ph5hEs6HVzVoPKzWSGI=
|
||||
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260204102724-10bcda1b3068 h1:i09YEVYbiUBMhxyak93REn/ZJOTRhAN4I3PXp2nCXgU=
|
||||
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260204102724-10bcda1b3068/go.mod h1:pzatilMEHZFT3qV7C/X3MqOa3NlRQuYhlRhZTL+hN6Q=
|
||||
github.com/opencloud-eu/reva/v2 v2.42.5 h1:Srhk8++3zJe3KA1u2Vqh4VbmljbblF75DR7t4HW0Kxw=
|
||||
github.com/opencloud-eu/reva/v2 v2.42.5/go.mod h1:U3UaHyAQcutavXyLaLE3UVY5n6t2pRAN9uv09n69lwI=
|
||||
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d h1:JcqGDiyrcaQwVyV861TUyQgO7uEmsjkhfm7aQd84dOw=
|
||||
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d/go.mod h1:pzatilMEHZFT3qV7C/X3MqOa3NlRQuYhlRhZTL+hN6Q=
|
||||
github.com/opencloud-eu/reva/v2 v2.42.6-0.20260311175421-d77bc89ffe35 h1:Qfx8AelrxsyPk3Lacj3feWULGD7BD2Vsg1X6rve7bHU=
|
||||
github.com/opencloud-eu/reva/v2 v2.42.6-0.20260311175421-d77bc89ffe35/go.mod h1:0k9+Qits/aemqVdCXs5hSEgTppPx9RZuxutuxqZKQF0=
|
||||
github.com/opencloud-eu/secure v0.0.0-20260312082735-b6f5cb2244e4 h1:l2oB/RctH+t8r7QBj5p8thfEHCM/jF35aAY3WQ3hADI=
|
||||
github.com/opencloud-eu/secure v0.0.0-20260312082735-b6f5cb2244e4/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
|
||||
@@ -559,6 +559,7 @@ type Entity struct {
|
||||
RemoteItemId *ResourceID `protobuf:"bytes,17,opt,name=remote_item_id,json=remoteItemId,proto3" json:"remote_item_id,omitempty"`
|
||||
Image *Image `protobuf:"bytes,18,opt,name=image,proto3" json:"image,omitempty"`
|
||||
Photo *Photo `protobuf:"bytes,19,opt,name=photo,proto3" json:"photo,omitempty"`
|
||||
Favorites []string `protobuf:"bytes,20,rep,name=favorites,proto3" json:"favorites,omitempty"`
|
||||
}
|
||||
|
||||
func (x *Entity) Reset() {
|
||||
@@ -726,6 +727,13 @@ func (x *Entity) GetPhoto() *Photo {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Entity) GetFavorites() []string {
|
||||
if x != nil {
|
||||
return x.Favorites
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Match struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
@@ -900,7 +908,7 @@ var file_opencloud_messages_search_v0_search_proto_rawDesc = []byte{
|
||||
0x6c, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x42, 0x06, 0x0a, 0x04, 0x5f, 0x69, 0x73, 0x6f, 0x42,
|
||||
0x0e, 0x0a, 0x0c, 0x5f, 0x6f, 0x72, 0x69, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42,
|
||||
0x10, 0x0a, 0x0e, 0x5f, 0x74, 0x61, 0x6b, 0x65, 0x6e, 0x44, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d,
|
||||
0x65, 0x22, 0xdc, 0x06, 0x0a, 0x06, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x39, 0x0a, 0x03,
|
||||
0x65, 0x22, 0xfa, 0x06, 0x0a, 0x06, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x39, 0x0a, 0x03,
|
||||
0x72, 0x65, 0x66, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6f, 0x70, 0x65, 0x6e,
|
||||
0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73,
|
||||
0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e,
|
||||
@@ -954,18 +962,20 @@ var file_opencloud_messages_search_v0_search_proto_rawDesc = []byte{
|
||||
0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64,
|
||||
0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68,
|
||||
0x2e, 0x76, 0x30, 0x2e, 0x50, 0x68, 0x6f, 0x74, 0x6f, 0x52, 0x05, 0x70, 0x68, 0x6f, 0x74, 0x6f,
|
||||
0x22, 0x5b, 0x0a, 0x05, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x3c, 0x0a, 0x06, 0x65, 0x6e, 0x74,
|
||||
0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6f, 0x70, 0x65, 0x6e,
|
||||
0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73,
|
||||
0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52,
|
||||
0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65,
|
||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x02, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x42, 0x4d, 0x5a,
|
||||
0x4b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e,
|
||||
0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2d, 0x65, 0x75, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f,
|
||||
0x75, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x65, 0x6e, 0x2f,
|
||||
0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67,
|
||||
0x65, 0x73, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2f, 0x76, 0x30, 0x62, 0x06, 0x70, 0x72,
|
||||
0x6f, 0x74, 0x6f, 0x33,
|
||||
0x12, 0x1c, 0x0a, 0x09, 0x66, 0x61, 0x76, 0x6f, 0x72, 0x69, 0x74, 0x65, 0x73, 0x18, 0x14, 0x20,
|
||||
0x03, 0x28, 0x09, 0x52, 0x09, 0x66, 0x61, 0x76, 0x6f, 0x72, 0x69, 0x74, 0x65, 0x73, 0x22, 0x5b,
|
||||
0x0a, 0x05, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x3c, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74,
|
||||
0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c,
|
||||
0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61,
|
||||
0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x06, 0x65,
|
||||
0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x02,
|
||||
0x20, 0x01, 0x28, 0x02, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x42, 0x4d, 0x5a, 0x4b, 0x67,
|
||||
0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c,
|
||||
0x6f, 0x75, 0x64, 0x2d, 0x65, 0x75, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64,
|
||||
0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x6f, 0x70,
|
||||
0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73,
|
||||
0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2f, 0x76, 0x30, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -627,13 +627,13 @@ type Bundle struct {
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // @gotags: yaml:"id"
|
||||
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` // @gotags: yaml:"name"
|
||||
Type Bundle_Type `protobuf:"varint,3,opt,name=type,proto3,enum=opencloud.messages.settings.v0.Bundle_Type" json:"type,omitempty"` // @gotags: yaml:"type"
|
||||
Extension string `protobuf:"bytes,4,opt,name=extension,proto3" json:"extension,omitempty"` // @gotags: yaml:"extension"
|
||||
DisplayName string `protobuf:"bytes,5,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` // @gotags: yaml:"display_name"
|
||||
Settings []*Setting `protobuf:"bytes,6,rep,name=settings,proto3" json:"settings,omitempty"` // @gotags: yaml:"settings"
|
||||
Resource *Resource `protobuf:"bytes,7,opt,name=resource,proto3" json:"resource,omitempty"` // @gotags: yaml:"resource"
|
||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty" yaml:"id"` // @gotags: yaml:"id"
|
||||
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty" yaml:"name"` // @gotags: yaml:"name"
|
||||
Type Bundle_Type `protobuf:"varint,3,opt,name=type,proto3,enum=opencloud.messages.settings.v0.Bundle_Type" json:"type,omitempty" yaml:"type"` // @gotags: yaml:"type"
|
||||
Extension string `protobuf:"bytes,4,opt,name=extension,proto3" json:"extension,omitempty" yaml:"extension"` // @gotags: yaml:"extension"
|
||||
DisplayName string `protobuf:"bytes,5,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty" yaml:"display_name"` // @gotags: yaml:"display_name"
|
||||
Settings []*Setting `protobuf:"bytes,6,rep,name=settings,proto3" json:"settings,omitempty" yaml:"settings"` // @gotags: yaml:"settings"
|
||||
Resource *Resource `protobuf:"bytes,7,opt,name=resource,proto3" json:"resource,omitempty" yaml:"resource"` // @gotags: yaml:"resource"
|
||||
}
|
||||
|
||||
func (x *Bundle) Reset() {
|
||||
@@ -722,10 +722,10 @@ type Setting struct {
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // @gotags: yaml:"id"
|
||||
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` // @gotags: yaml:"name"
|
||||
DisplayName string `protobuf:"bytes,3,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` // @gotags: yaml:"display_name"
|
||||
Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` // @gotags: yaml:"description"
|
||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty" yaml:"id"` // @gotags: yaml:"id"
|
||||
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty" yaml:"name"` // @gotags: yaml:"name"
|
||||
DisplayName string `protobuf:"bytes,3,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty" yaml:"display_name"` // @gotags: yaml:"display_name"
|
||||
Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty" yaml:"description"` // @gotags: yaml:"description"
|
||||
// Types that are assignable to Value:
|
||||
//
|
||||
// *Setting_IntValue
|
||||
@@ -736,7 +736,7 @@ type Setting struct {
|
||||
// *Setting_PermissionValue
|
||||
// *Setting_MultiChoiceCollectionValue
|
||||
Value isSetting_Value `protobuf_oneof:"value"`
|
||||
Resource *Resource `protobuf:"bytes,11,opt,name=resource,proto3" json:"resource,omitempty"` // @gotags: yaml:"resource"
|
||||
Resource *Resource `protobuf:"bytes,11,opt,name=resource,proto3" json:"resource,omitempty" yaml:"resource"` // @gotags: yaml:"resource"
|
||||
}
|
||||
|
||||
func (x *Setting) Reset() {
|
||||
@@ -867,31 +867,31 @@ type isSetting_Value interface {
|
||||
}
|
||||
|
||||
type Setting_IntValue struct {
|
||||
IntValue *Int `protobuf:"bytes,5,opt,name=int_value,json=intValue,proto3,oneof"` // @gotags: yaml:"int_value"
|
||||
IntValue *Int `protobuf:"bytes,5,opt,name=int_value,json=intValue,proto3,oneof" yaml:"int_value"` // @gotags: yaml:"int_value"
|
||||
}
|
||||
|
||||
type Setting_StringValue struct {
|
||||
StringValue *String `protobuf:"bytes,6,opt,name=string_value,json=stringValue,proto3,oneof"` // @gotags: yaml:"string_value"
|
||||
StringValue *String `protobuf:"bytes,6,opt,name=string_value,json=stringValue,proto3,oneof" yaml:"string_value"` // @gotags: yaml:"string_value"
|
||||
}
|
||||
|
||||
type Setting_BoolValue struct {
|
||||
BoolValue *Bool `protobuf:"bytes,7,opt,name=bool_value,json=boolValue,proto3,oneof"` // @gotags: yaml:"bool_value"
|
||||
BoolValue *Bool `protobuf:"bytes,7,opt,name=bool_value,json=boolValue,proto3,oneof" yaml:"bool_value"` // @gotags: yaml:"bool_value"
|
||||
}
|
||||
|
||||
type Setting_SingleChoiceValue struct {
|
||||
SingleChoiceValue *SingleChoiceList `protobuf:"bytes,8,opt,name=single_choice_value,json=singleChoiceValue,proto3,oneof"` // @gotags: yaml:"single_choice_value"
|
||||
SingleChoiceValue *SingleChoiceList `protobuf:"bytes,8,opt,name=single_choice_value,json=singleChoiceValue,proto3,oneof" yaml:"single_choice_value"` // @gotags: yaml:"single_choice_value"
|
||||
}
|
||||
|
||||
type Setting_MultiChoiceValue struct {
|
||||
MultiChoiceValue *MultiChoiceList `protobuf:"bytes,9,opt,name=multi_choice_value,json=multiChoiceValue,proto3,oneof"` // @gotags: yaml:"multi_choice_value"
|
||||
MultiChoiceValue *MultiChoiceList `protobuf:"bytes,9,opt,name=multi_choice_value,json=multiChoiceValue,proto3,oneof" yaml:"multi_choice_value"` // @gotags: yaml:"multi_choice_value"
|
||||
}
|
||||
|
||||
type Setting_PermissionValue struct {
|
||||
PermissionValue *Permission `protobuf:"bytes,10,opt,name=permission_value,json=permissionValue,proto3,oneof"` // @gotags: yaml:"permission_value"
|
||||
PermissionValue *Permission `protobuf:"bytes,10,opt,name=permission_value,json=permissionValue,proto3,oneof" yaml:"permission_value"` // @gotags: yaml:"permission_value"
|
||||
}
|
||||
|
||||
type Setting_MultiChoiceCollectionValue struct {
|
||||
MultiChoiceCollectionValue *MultiChoiceCollection `protobuf:"bytes,12,opt,name=multi_choice_collection_value,json=multiChoiceCollectionValue,proto3,oneof"` // @gotags: yaml:"multi_choice_collection_value"
|
||||
MultiChoiceCollectionValue *MultiChoiceCollection `protobuf:"bytes,12,opt,name=multi_choice_collection_value,json=multiChoiceCollectionValue,proto3,oneof" yaml:"multi_choice_collection_value"` // @gotags: yaml:"multi_choice_collection_value"
|
||||
}
|
||||
|
||||
func (*Setting_IntValue) isSetting_Value() {}
|
||||
@@ -913,11 +913,11 @@ type Int struct {
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Default int64 `protobuf:"varint,1,opt,name=default,proto3" json:"default,omitempty"` // @gotags: yaml:"default"
|
||||
Min int64 `protobuf:"varint,2,opt,name=min,proto3" json:"min,omitempty"` // @gotags: yaml:"min"
|
||||
Max int64 `protobuf:"varint,3,opt,name=max,proto3" json:"max,omitempty"` // @gotags: yaml:"max"
|
||||
Step int64 `protobuf:"varint,4,opt,name=step,proto3" json:"step,omitempty"` // @gotags: yaml:"step"
|
||||
Placeholder string `protobuf:"bytes,5,opt,name=placeholder,proto3" json:"placeholder,omitempty"` // @gotags: yaml:"placeholder"
|
||||
Default int64 `protobuf:"varint,1,opt,name=default,proto3" json:"default,omitempty" yaml:"default"` // @gotags: yaml:"default"
|
||||
Min int64 `protobuf:"varint,2,opt,name=min,proto3" json:"min,omitempty" yaml:"min"` // @gotags: yaml:"min"
|
||||
Max int64 `protobuf:"varint,3,opt,name=max,proto3" json:"max,omitempty" yaml:"max"` // @gotags: yaml:"max"
|
||||
Step int64 `protobuf:"varint,4,opt,name=step,proto3" json:"step,omitempty" yaml:"step"` // @gotags: yaml:"step"
|
||||
Placeholder string `protobuf:"bytes,5,opt,name=placeholder,proto3" json:"placeholder,omitempty" yaml:"placeholder"` // @gotags: yaml:"placeholder"
|
||||
}
|
||||
|
||||
func (x *Int) Reset() {
|
||||
@@ -992,11 +992,11 @@ type String struct {
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Default string `protobuf:"bytes,1,opt,name=default,proto3" json:"default,omitempty"` // @gotags: yaml:"default"
|
||||
Required bool `protobuf:"varint,2,opt,name=required,proto3" json:"required,omitempty"` // @gotags: yaml:"required"
|
||||
MinLength int32 `protobuf:"varint,3,opt,name=min_length,json=minLength,proto3" json:"min_length,omitempty"` // @gotags: yaml:"min_length"
|
||||
MaxLength int32 `protobuf:"varint,4,opt,name=max_length,json=maxLength,proto3" json:"max_length,omitempty"` // @gotags: yaml:"max_length"
|
||||
Placeholder string `protobuf:"bytes,5,opt,name=placeholder,proto3" json:"placeholder,omitempty"` // @gotags: yaml:"placeholder"
|
||||
Default string `protobuf:"bytes,1,opt,name=default,proto3" json:"default,omitempty" yaml:"default"` // @gotags: yaml:"default"
|
||||
Required bool `protobuf:"varint,2,opt,name=required,proto3" json:"required,omitempty" yaml:"required"` // @gotags: yaml:"required"
|
||||
MinLength int32 `protobuf:"varint,3,opt,name=min_length,json=minLength,proto3" json:"min_length,omitempty" yaml:"min_length"` // @gotags: yaml:"min_length"
|
||||
MaxLength int32 `protobuf:"varint,4,opt,name=max_length,json=maxLength,proto3" json:"max_length,omitempty" yaml:"max_length"` // @gotags: yaml:"max_length"
|
||||
Placeholder string `protobuf:"bytes,5,opt,name=placeholder,proto3" json:"placeholder,omitempty" yaml:"placeholder"` // @gotags: yaml:"placeholder"
|
||||
}
|
||||
|
||||
func (x *String) Reset() {
|
||||
@@ -1071,8 +1071,8 @@ type Bool struct {
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Default bool `protobuf:"varint,1,opt,name=default,proto3" json:"default,omitempty"` // @gotags: yaml:"default"
|
||||
Label string `protobuf:"bytes,2,opt,name=label,proto3" json:"label,omitempty"` // @gotags: yaml:"label"
|
||||
Default bool `protobuf:"varint,1,opt,name=default,proto3" json:"default,omitempty" yaml:"default"` // @gotags: yaml:"default"
|
||||
Label string `protobuf:"bytes,2,opt,name=label,proto3" json:"label,omitempty" yaml:"label"` // @gotags: yaml:"label"
|
||||
}
|
||||
|
||||
func (x *Bool) Reset() {
|
||||
@@ -1126,7 +1126,7 @@ type SingleChoiceList struct {
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Options []*ListOption `protobuf:"bytes,1,rep,name=options,proto3" json:"options,omitempty"` // @gotags: yaml:"options"
|
||||
Options []*ListOption `protobuf:"bytes,1,rep,name=options,proto3" json:"options,omitempty" yaml:"options"` // @gotags: yaml:"options"
|
||||
}
|
||||
|
||||
func (x *SingleChoiceList) Reset() {
|
||||
@@ -1173,7 +1173,7 @@ type MultiChoiceList struct {
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Options []*ListOption `protobuf:"bytes,1,rep,name=options,proto3" json:"options,omitempty"` // @gotags: yaml:"options"
|
||||
Options []*ListOption `protobuf:"bytes,1,rep,name=options,proto3" json:"options,omitempty" yaml:"options"` // @gotags: yaml:"options"
|
||||
}
|
||||
|
||||
func (x *MultiChoiceList) Reset() {
|
||||
@@ -1220,9 +1220,9 @@ type ListOption struct {
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Value *ListOptionValue `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` // @gotags: yaml:"value"
|
||||
Default bool `protobuf:"varint,2,opt,name=default,proto3" json:"default,omitempty"` // @gotags: yaml:"default"
|
||||
DisplayValue string `protobuf:"bytes,3,opt,name=display_value,json=displayValue,proto3" json:"display_value,omitempty"` // @gotags: yaml:"display_value"
|
||||
Value *ListOptionValue `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty" yaml:"value"` // @gotags: yaml:"value"
|
||||
Default bool `protobuf:"varint,2,opt,name=default,proto3" json:"default,omitempty" yaml:"default"` // @gotags: yaml:"default"
|
||||
DisplayValue string `protobuf:"bytes,3,opt,name=display_value,json=displayValue,proto3" json:"display_value,omitempty" yaml:"display_value"` // @gotags: yaml:"display_value"
|
||||
}
|
||||
|
||||
func (x *ListOption) Reset() {
|
||||
@@ -1283,7 +1283,7 @@ type MultiChoiceCollection struct {
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Options []*MultiChoiceCollectionOption `protobuf:"bytes,1,rep,name=options,proto3" json:"options,omitempty"` // @gotags: yaml:"options"
|
||||
Options []*MultiChoiceCollectionOption `protobuf:"bytes,1,rep,name=options,proto3" json:"options,omitempty" yaml:"options"` // @gotags: yaml:"options"
|
||||
}
|
||||
|
||||
func (x *MultiChoiceCollection) Reset() {
|
||||
@@ -1330,10 +1330,10 @@ type MultiChoiceCollectionOption struct {
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Value *MultiChoiceCollectionOptionValue `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` // @gotags: yaml:"value"
|
||||
Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` // @gotags: yaml:"key"
|
||||
Attribute string `protobuf:"bytes,3,opt,name=attribute,proto3" json:"attribute,omitempty"` // @gotags: yaml:"attribute"
|
||||
DisplayValue string `protobuf:"bytes,4,opt,name=display_value,json=displayValue,proto3" json:"display_value,omitempty"` // @gotags: yaml:"display_value"
|
||||
Value *MultiChoiceCollectionOptionValue `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty" yaml:"value"` // @gotags: yaml:"value"
|
||||
Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty" yaml:"key"` // @gotags: yaml:"key"
|
||||
Attribute string `protobuf:"bytes,3,opt,name=attribute,proto3" json:"attribute,omitempty" yaml:"attribute"` // @gotags: yaml:"attribute"
|
||||
DisplayValue string `protobuf:"bytes,4,opt,name=display_value,json=displayValue,proto3" json:"display_value,omitempty" yaml:"display_value"` // @gotags: yaml:"display_value"
|
||||
}
|
||||
|
||||
func (x *MultiChoiceCollectionOption) Reset() {
|
||||
@@ -1474,15 +1474,15 @@ type isMultiChoiceCollectionOptionValue_Option interface {
|
||||
}
|
||||
|
||||
type MultiChoiceCollectionOptionValue_IntValue struct {
|
||||
IntValue *Int `protobuf:"bytes,1,opt,name=int_value,json=intValue,proto3,oneof"` // @gotags: yaml:"int_value"
|
||||
IntValue *Int `protobuf:"bytes,1,opt,name=int_value,json=intValue,proto3,oneof" yaml:"int_value"` // @gotags: yaml:"int_value"
|
||||
}
|
||||
|
||||
type MultiChoiceCollectionOptionValue_StringValue struct {
|
||||
StringValue *String `protobuf:"bytes,2,opt,name=string_value,json=stringValue,proto3,oneof"` // @gotags: yaml:"string_value"
|
||||
StringValue *String `protobuf:"bytes,2,opt,name=string_value,json=stringValue,proto3,oneof" yaml:"string_value"` // @gotags: yaml:"string_value"
|
||||
}
|
||||
|
||||
type MultiChoiceCollectionOptionValue_BoolValue struct {
|
||||
BoolValue *Bool `protobuf:"bytes,3,opt,name=bool_value,json=boolValue,proto3,oneof"` // @gotags: yaml:"bool_value"
|
||||
BoolValue *Bool `protobuf:"bytes,3,opt,name=bool_value,json=boolValue,proto3,oneof" yaml:"bool_value"` // @gotags: yaml:"bool_value"
|
||||
}
|
||||
|
||||
func (*MultiChoiceCollectionOptionValue_IntValue) isMultiChoiceCollectionOptionValue_Option() {}
|
||||
@@ -1496,8 +1496,8 @@ type Permission struct {
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Operation Permission_Operation `protobuf:"varint,1,opt,name=operation,proto3,enum=opencloud.messages.settings.v0.Permission_Operation" json:"operation,omitempty"` // @gotags: yaml:"operation"
|
||||
Constraint Permission_Constraint `protobuf:"varint,2,opt,name=constraint,proto3,enum=opencloud.messages.settings.v0.Permission_Constraint" json:"constraint,omitempty"` // @gotags: yaml:"constraint"
|
||||
Operation Permission_Operation `protobuf:"varint,1,opt,name=operation,proto3,enum=opencloud.messages.settings.v0.Permission_Operation" json:"operation,omitempty" yaml:"operation"` // @gotags: yaml:"operation"
|
||||
Constraint Permission_Constraint `protobuf:"varint,2,opt,name=constraint,proto3,enum=opencloud.messages.settings.v0.Permission_Constraint" json:"constraint,omitempty" yaml:"constraint"` // @gotags: yaml:"constraint"
|
||||
}
|
||||
|
||||
func (x *Permission) Reset() {
|
||||
@@ -1552,12 +1552,12 @@ type Value struct {
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// id is the id of the Value. It is generated on saving it.
|
||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // @gotags: yaml:"id"
|
||||
BundleId string `protobuf:"bytes,2,opt,name=bundle_id,json=bundleId,proto3" json:"bundle_id,omitempty"` // @gotags: yaml:"bundle_id"
|
||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty" yaml:"id"` // @gotags: yaml:"id"
|
||||
BundleId string `protobuf:"bytes,2,opt,name=bundle_id,json=bundleId,proto3" json:"bundle_id,omitempty" yaml:"bundle_id"` // @gotags: yaml:"bundle_id"
|
||||
// setting_id is the id of the setting from within its bundle.
|
||||
SettingId string `protobuf:"bytes,3,opt,name=setting_id,json=settingId,proto3" json:"setting_id,omitempty"` // @gotags: yaml:"setting_id"
|
||||
AccountUuid string `protobuf:"bytes,4,opt,name=account_uuid,json=accountUuid,proto3" json:"account_uuid,omitempty"` // @gotags: yaml:"account_uuid"
|
||||
Resource *Resource `protobuf:"bytes,5,opt,name=resource,proto3" json:"resource,omitempty"` // @gotags: yaml:"resource"
|
||||
SettingId string `protobuf:"bytes,3,opt,name=setting_id,json=settingId,proto3" json:"setting_id,omitempty" yaml:"setting_id"` // @gotags: yaml:"setting_id"
|
||||
AccountUuid string `protobuf:"bytes,4,opt,name=account_uuid,json=accountUuid,proto3" json:"account_uuid,omitempty" yaml:"account_uuid"` // @gotags: yaml:"account_uuid"
|
||||
Resource *Resource `protobuf:"bytes,5,opt,name=resource,proto3" json:"resource,omitempty" yaml:"resource"` // @gotags: yaml:"resource"
|
||||
// Types that are assignable to Value:
|
||||
//
|
||||
// *Value_BoolValue
|
||||
@@ -1682,23 +1682,23 @@ type isValue_Value interface {
|
||||
}
|
||||
|
||||
type Value_BoolValue struct {
|
||||
BoolValue bool `protobuf:"varint,6,opt,name=bool_value,json=boolValue,proto3,oneof"` // @gotags: yaml:"bool_value"
|
||||
BoolValue bool `protobuf:"varint,6,opt,name=bool_value,json=boolValue,proto3,oneof" yaml:"bool_value"` // @gotags: yaml:"bool_value"
|
||||
}
|
||||
|
||||
type Value_IntValue struct {
|
||||
IntValue int64 `protobuf:"varint,7,opt,name=int_value,json=intValue,proto3,oneof"` // @gotags: yaml:"int_value"
|
||||
IntValue int64 `protobuf:"varint,7,opt,name=int_value,json=intValue,proto3,oneof" yaml:"int_value"` // @gotags: yaml:"int_value"
|
||||
}
|
||||
|
||||
type Value_StringValue struct {
|
||||
StringValue string `protobuf:"bytes,8,opt,name=string_value,json=stringValue,proto3,oneof"` // @gotags: yaml:"string_value"
|
||||
StringValue string `protobuf:"bytes,8,opt,name=string_value,json=stringValue,proto3,oneof" yaml:"string_value"` // @gotags: yaml:"string_value"
|
||||
}
|
||||
|
||||
type Value_ListValue struct {
|
||||
ListValue *ListValue `protobuf:"bytes,9,opt,name=list_value,json=listValue,proto3,oneof"` // @gotags: yaml:"list_value"
|
||||
ListValue *ListValue `protobuf:"bytes,9,opt,name=list_value,json=listValue,proto3,oneof" yaml:"list_value"` // @gotags: yaml:"list_value"
|
||||
}
|
||||
|
||||
type Value_CollectionValue struct {
|
||||
CollectionValue *CollectionValue `protobuf:"bytes,10,opt,name=collection_value,json=collectionValue,proto3,oneof"` // @gotags: yaml:"collection_value"
|
||||
CollectionValue *CollectionValue `protobuf:"bytes,10,opt,name=collection_value,json=collectionValue,proto3,oneof" yaml:"collection_value"` // @gotags: yaml:"collection_value"
|
||||
}
|
||||
|
||||
func (*Value_BoolValue) isValue_Value() {}
|
||||
@@ -1716,7 +1716,7 @@ type ListValue struct {
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Values []*ListOptionValue `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"` // @gotags: yaml:"values"
|
||||
Values []*ListOptionValue `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty" yaml:"values"` // @gotags: yaml:"values"
|
||||
}
|
||||
|
||||
func (x *ListValue) Reset() {
|
||||
@@ -1836,15 +1836,15 @@ type isListOptionValue_Option interface {
|
||||
}
|
||||
|
||||
type ListOptionValue_StringValue struct {
|
||||
StringValue string `protobuf:"bytes,1,opt,name=string_value,json=stringValue,proto3,oneof"` // @gotags: yaml:"string_value"
|
||||
StringValue string `protobuf:"bytes,1,opt,name=string_value,json=stringValue,proto3,oneof" yaml:"string_value"` // @gotags: yaml:"string_value"
|
||||
}
|
||||
|
||||
type ListOptionValue_IntValue struct {
|
||||
IntValue int64 `protobuf:"varint,2,opt,name=int_value,json=intValue,proto3,oneof"` // @gotags: yaml:"int_value"
|
||||
IntValue int64 `protobuf:"varint,2,opt,name=int_value,json=intValue,proto3,oneof" yaml:"int_value"` // @gotags: yaml:"int_value"
|
||||
}
|
||||
|
||||
type ListOptionValue_BoolValue struct {
|
||||
BoolValue bool `protobuf:"varint,3,opt,name=bool_value,json=boolValue,proto3,oneof"` // @gotags: yaml:"bool_value"
|
||||
BoolValue bool `protobuf:"varint,3,opt,name=bool_value,json=boolValue,proto3,oneof" yaml:"bool_value"` // @gotags: yaml:"bool_value"
|
||||
}
|
||||
|
||||
func (*ListOptionValue_StringValue) isListOptionValue_Option() {}
|
||||
@@ -1858,7 +1858,7 @@ type CollectionValue struct {
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Values []*CollectionOption `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"` // @gotags: yaml:"values"
|
||||
Values []*CollectionOption `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty" yaml:"values"` // @gotags: yaml:"values"
|
||||
}
|
||||
|
||||
func (x *CollectionValue) Reset() {
|
||||
@@ -1906,7 +1906,7 @@ type CollectionOption struct {
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// required
|
||||
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` // @gotags: yaml:"key"
|
||||
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty" yaml:"key"` // @gotags: yaml:"key"
|
||||
// Types that are assignable to Option:
|
||||
//
|
||||
// *CollectionOption_IntValue
|
||||
@@ -1987,15 +1987,15 @@ type isCollectionOption_Option interface {
|
||||
}
|
||||
|
||||
type CollectionOption_IntValue struct {
|
||||
IntValue int64 `protobuf:"varint,2,opt,name=int_value,json=intValue,proto3,oneof"` // @gotags: yaml:"int_value"
|
||||
IntValue int64 `protobuf:"varint,2,opt,name=int_value,json=intValue,proto3,oneof" yaml:"int_value"` // @gotags: yaml:"int_value"
|
||||
}
|
||||
|
||||
type CollectionOption_StringValue struct {
|
||||
StringValue string `protobuf:"bytes,3,opt,name=string_value,json=stringValue,proto3,oneof"` // @gotags: yaml:"string_value"
|
||||
StringValue string `protobuf:"bytes,3,opt,name=string_value,json=stringValue,proto3,oneof" yaml:"string_value"` // @gotags: yaml:"string_value"
|
||||
}
|
||||
|
||||
type CollectionOption_BoolValue struct {
|
||||
BoolValue bool `protobuf:"varint,4,opt,name=bool_value,json=boolValue,proto3,oneof"` // @gotags: yaml:"bool_value"
|
||||
BoolValue bool `protobuf:"varint,4,opt,name=bool_value,json=boolValue,proto3,oneof" yaml:"bool_value"` // @gotags: yaml:"bool_value"
|
||||
}
|
||||
|
||||
func (*CollectionOption_IntValue) isCollectionOption_Option() {}
|
||||
|
||||
@@ -282,6 +282,12 @@
|
||||
},
|
||||
"photo": {
|
||||
"$ref": "#/definitions/v0Photo"
|
||||
},
|
||||
"favorites": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -79,6 +79,7 @@ message Entity {
|
||||
ResourceID remote_item_id = 17;
|
||||
Image image = 18;
|
||||
Photo photo = 19;
|
||||
repeated string favorites = 20;
|
||||
}
|
||||
|
||||
message Match {
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode"
|
||||
revaCtx "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/events"
|
||||
)
|
||||
|
||||
// FollowDriveItem marks a drive item as favorite.
|
||||
func (g Graph) FollowDriveItem(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
itemID, err := parseIDParam(r, "itemID")
|
||||
if err != nil {
|
||||
g.logger.Debug().Err(err).Msg("could not parse itemID")
|
||||
return
|
||||
}
|
||||
|
||||
gatewayClient, err := g.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Msg("could not select next gateway client")
|
||||
errorcode.ServiceNotAvailable.Render(w, r, http.StatusServiceUnavailable, "could not select next gateway client")
|
||||
return
|
||||
}
|
||||
|
||||
ref := &provider.Reference{
|
||||
ResourceId: &itemID,
|
||||
}
|
||||
|
||||
u, ok := revactx.ContextGetUser(ctx)
|
||||
if !ok {
|
||||
errorcode.GeneralException.Render(w, r, http.StatusUnauthorized, "User not found in context")
|
||||
return
|
||||
}
|
||||
|
||||
statReq := &provider.StatRequest{
|
||||
Ref: ref,
|
||||
}
|
||||
statRes, err := gatewayClient.Stat(ctx, statReq)
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Msg("could not stat resource")
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "could not stat resource")
|
||||
return
|
||||
}
|
||||
switch statRes.GetStatus().GetCode() {
|
||||
case rpc.Code_CODE_OK:
|
||||
// continue
|
||||
case rpc.Code_CODE_NOT_FOUND:
|
||||
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "resource not found")
|
||||
return
|
||||
default:
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "could not stat resource")
|
||||
return
|
||||
}
|
||||
|
||||
req := &provider.AddFavoriteRequest{
|
||||
Ref: ref,
|
||||
UserId: u.Id,
|
||||
}
|
||||
|
||||
res, err := gatewayClient.AddFavorite(ctx, req)
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Msg("could not add favorite")
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "could not add favorite")
|
||||
return
|
||||
}
|
||||
|
||||
if res.Status.Code != rpc.Code_CODE_OK {
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "could not add favorite")
|
||||
return
|
||||
}
|
||||
|
||||
if g.eventsPublisher != nil {
|
||||
ev := events.FavoriteAdded{
|
||||
Ref: &provider.Reference{
|
||||
ResourceId: &itemID,
|
||||
Path: ".",
|
||||
},
|
||||
UserID: u.Id,
|
||||
Executant: revaCtx.ContextMustGetUser(r.Context()).Id,
|
||||
}
|
||||
if err := events.Publish(r.Context(), g.eventsPublisher, ev); err != nil {
|
||||
g.logger.Error().Err(err).Msg("Failed to publish FavoriteAdded event")
|
||||
}
|
||||
}
|
||||
|
||||
driveItem, err := cs3ResourceToDriveItem(g.logger, statRes.GetInfo())
|
||||
if err != nil {
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
render.Status(r, http.StatusCreated)
|
||||
render.JSON(w, r, &driveItem)
|
||||
}
|
||||
|
||||
// UnfollowDriveItem unmarks a drive item as favorite.
|
||||
func (g Graph) UnfollowDriveItem(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
itemID, err := parseIDParam(r, "itemID")
|
||||
if err != nil {
|
||||
g.logger.Debug().Err(err).Msg("could not parse itemID")
|
||||
return
|
||||
}
|
||||
|
||||
gatewayClient, err := g.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Msg("could not select next gateway client")
|
||||
errorcode.ServiceNotAvailable.Render(w, r, http.StatusServiceUnavailable, "could not select next gateway client")
|
||||
return
|
||||
}
|
||||
|
||||
ref := &provider.Reference{
|
||||
ResourceId: &itemID,
|
||||
}
|
||||
|
||||
u, ok := revactx.ContextGetUser(ctx)
|
||||
if !ok {
|
||||
errorcode.GeneralException.Render(w, r, http.StatusUnauthorized, "User not found in context")
|
||||
return
|
||||
}
|
||||
|
||||
req := &provider.RemoveFavoriteRequest{
|
||||
Ref: ref,
|
||||
UserId: u.Id,
|
||||
}
|
||||
|
||||
res, err := gatewayClient.RemoveFavorite(ctx, req)
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Msg("could not remove favorite")
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "could not remove favorite")
|
||||
return
|
||||
}
|
||||
|
||||
switch res.Status.Code {
|
||||
case rpc.Code_CODE_OK:
|
||||
// continue
|
||||
case rpc.Code_CODE_NOT_FOUND:
|
||||
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "favorite not found")
|
||||
return
|
||||
default:
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "could not remove favorite")
|
||||
return
|
||||
}
|
||||
|
||||
if g.eventsPublisher != nil {
|
||||
ev := events.FavoriteRemoved{
|
||||
Ref: &provider.Reference{
|
||||
ResourceId: &itemID,
|
||||
Path: ".",
|
||||
},
|
||||
UserID: u.Id,
|
||||
Executant: revaCtx.ContextMustGetUser(r.Context()).Id,
|
||||
}
|
||||
if err := events.Publish(r.Context(), g.eventsPublisher, ev); err != nil {
|
||||
g.logger.Error().Err(err).Msg("Failed to publish FavoriteRemoved event")
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
@@ -309,6 +309,8 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx
|
||||
r.Route("/drive", func(r chi.Router) {
|
||||
r.Get("/", svc.GetUserDrive)
|
||||
r.Get("/root/children", svc.GetRootDriveChildren)
|
||||
r.Post("/items/{itemID}/follow", svc.FollowDriveItem)
|
||||
r.Delete("/following/{itemID}", svc.UnfollowDriveItem)
|
||||
})
|
||||
r.Get("/drives", svc.GetDrives(APIVersion_1))
|
||||
r.Post("/changePassword", svc.ChangeOwnPassword)
|
||||
|
||||
@@ -134,6 +134,7 @@ func (b *Backend) Search(_ context.Context, sir *searchService.SearchIndexReques
|
||||
MimeType: getFieldValue[string](hit.Fields, "MimeType"),
|
||||
Deleted: getFieldValue[bool](hit.Fields, "Deleted"),
|
||||
Tags: getFieldSliceValue[string](hit.Fields, "Tags"),
|
||||
Favorites: getFieldSliceValue[string](hit.Fields, "Favorites"),
|
||||
Highlights: getFragmentValue(hit.Fragments, "Content", 0),
|
||||
Audio: getAudioValue[searchMessage.Audio](hit.Fields),
|
||||
Image: getImageValue[searchMessage.Image](hit.Fields),
|
||||
|
||||
@@ -186,17 +186,18 @@ func matchToResource(match *bleveSearch.DocumentMatch) *search.Resource {
|
||||
Type: uint64(getFieldValue[float64](match.Fields, "Type")),
|
||||
Deleted: getFieldValue[bool](match.Fields, "Deleted"),
|
||||
Document: content.Document{
|
||||
Name: getFieldValue[string](match.Fields, "Name"),
|
||||
Title: getFieldValue[string](match.Fields, "Title"),
|
||||
Size: uint64(getFieldValue[float64](match.Fields, "Size")),
|
||||
Mtime: getFieldValue[string](match.Fields, "Mtime"),
|
||||
MimeType: getFieldValue[string](match.Fields, "MimeType"),
|
||||
Content: getFieldValue[string](match.Fields, "Content"),
|
||||
Tags: getFieldSliceValue[string](match.Fields, "Tags"),
|
||||
Audio: getAudioValue[libregraph.Audio](match.Fields),
|
||||
Image: getImageValue[libregraph.Image](match.Fields),
|
||||
Location: getLocationValue[libregraph.GeoCoordinates](match.Fields),
|
||||
Photo: getPhotoValue[libregraph.Photo](match.Fields),
|
||||
Name: getFieldValue[string](match.Fields, "Name"),
|
||||
Title: getFieldValue[string](match.Fields, "Title"),
|
||||
Size: uint64(getFieldValue[float64](match.Fields, "Size")),
|
||||
Mtime: getFieldValue[string](match.Fields, "Mtime"),
|
||||
MimeType: getFieldValue[string](match.Fields, "MimeType"),
|
||||
Content: getFieldValue[string](match.Fields, "Content"),
|
||||
Tags: getFieldSliceValue[string](match.Fields, "Tags"),
|
||||
Favorites: getFieldSliceValue[string](match.Fields, "Favorites"),
|
||||
Audio: getAudioValue[libregraph.Audio](match.Fields),
|
||||
Image: getImageValue[libregraph.Image](match.Fields),
|
||||
Location: getLocationValue[libregraph.GeoCoordinates](match.Fields),
|
||||
Photo: getPhotoValue[libregraph.Photo](match.Fields),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ func NewMapping() (mapping.IndexMapping, error) {
|
||||
docMapping := bleve.NewDocumentMapping()
|
||||
docMapping.AddFieldMappingsAt("Name", nameMapping)
|
||||
docMapping.AddFieldMappingsAt("Tags", lowercaseMapping)
|
||||
docMapping.AddFieldMappingsAt("Favorites", lowercaseMapping)
|
||||
docMapping.AddFieldMappingsAt("Content", fulltextFieldMapping)
|
||||
|
||||
indexMapping := bleve.NewIndexMapping()
|
||||
|
||||
@@ -2,6 +2,7 @@ package content
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
storageProvider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
@@ -34,6 +35,24 @@ func (b Basic) Extract(_ context.Context, ri *storageProvider.ResourceInfo) (Doc
|
||||
}
|
||||
}
|
||||
|
||||
if m := ri.Opaque.GetMap(); m != nil && m["favorites"] != nil {
|
||||
favEntry := m["favorites"]
|
||||
|
||||
switch favEntry.Decoder {
|
||||
case "json":
|
||||
favorites := []string{}
|
||||
err := json.Unmarshal(favEntry.Value, &favorites)
|
||||
if err != nil {
|
||||
b.logger.Error().Err(err).Msg("failed to unmarshal favorites")
|
||||
break
|
||||
}
|
||||
|
||||
doc.Favorites = favorites
|
||||
default:
|
||||
b.logger.Error().Msgf("unsupported decoder for favorites: %s", favEntry.Decoder)
|
||||
}
|
||||
}
|
||||
|
||||
if ri.Mtime != nil {
|
||||
doc.Mtime = utils.TSToTime(ri.Mtime).UTC().Format(time.RFC3339Nano)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package content_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
storageProvider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
cs3Types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
@@ -86,5 +87,26 @@ var _ = Describe("Basic", func() {
|
||||
Expect(doc.Mtime).To(Equal(data.expect))
|
||||
}
|
||||
})
|
||||
|
||||
It("extracts favorites", func() {
|
||||
favorites := []string{"foo", "bar"}
|
||||
favBytes, _ := json.Marshal(favorites)
|
||||
|
||||
ri := &storageProvider.ResourceInfo{
|
||||
Opaque: &cs3Types.Opaque{
|
||||
Map: map[string]*cs3Types.OpaqueEntry{
|
||||
"favorites": {
|
||||
Decoder: "json",
|
||||
Value: favBytes,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
doc, err := basic.Extract(ctx, ri)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(doc).ToNot(BeNil())
|
||||
Expect(doc.Favorites).To(Equal(favorites))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -14,17 +14,18 @@ func init() {
|
||||
// Document wraps all resource meta fields,
|
||||
// it is used as a content extraction result.
|
||||
type Document struct {
|
||||
Title string
|
||||
Name string
|
||||
Content string
|
||||
Size uint64
|
||||
Mtime string
|
||||
MimeType string
|
||||
Tags []string
|
||||
Audio *libregraph.Audio `json:"audio,omitempty"`
|
||||
Image *libregraph.Image `json:"image,omitempty"`
|
||||
Location *libregraph.GeoCoordinates `json:"location,omitempty"`
|
||||
Photo *libregraph.Photo `json:"photo,omitempty"`
|
||||
Title string
|
||||
Name string
|
||||
Content string
|
||||
Size uint64
|
||||
Mtime string
|
||||
MimeType string
|
||||
Tags []string
|
||||
Favorites []string
|
||||
Audio *libregraph.Audio `json:"audio,omitempty"`
|
||||
Image *libregraph.Image `json:"image,omitempty"`
|
||||
Location *libregraph.GeoCoordinates `json:"location,omitempty"`
|
||||
Photo *libregraph.Photo `json:"photo,omitempty"`
|
||||
}
|
||||
|
||||
func CleanString(content, langCode string) string {
|
||||
|
||||
@@ -23,6 +23,7 @@ var _fields = map[string]string{
|
||||
"tags": "Tags",
|
||||
"content": "Content",
|
||||
"hidden": "Hidden",
|
||||
"favorite": "Favorites",
|
||||
}
|
||||
|
||||
// The following quoted string enumerates the characters which may be escaped: "+-=&|><!(){}[]^\"~*?:\\/ "
|
||||
|
||||
@@ -196,3 +196,17 @@ func ParseScope(query string) (string, string) {
|
||||
}
|
||||
return query, ""
|
||||
}
|
||||
|
||||
// ParseFlags extracts supported flags from the query string and returns the cleaned query and a map of flags
|
||||
func ParseFlags(query string) (string, []string) {
|
||||
supportedFlags := []string{"is:favorite"}
|
||||
flags := []string{}
|
||||
for _, flag := range supportedFlags {
|
||||
if strings.Contains(query, flag) {
|
||||
flags = append(flags, flag)
|
||||
query = strings.ReplaceAll(query, flag, "")
|
||||
}
|
||||
}
|
||||
|
||||
return strings.TrimSpace(query), flags
|
||||
}
|
||||
|
||||
@@ -116,8 +116,17 @@ func (s *Service) Search(ctx context.Context, req *searchsvc.SearchRequest) (*se
|
||||
}
|
||||
currentUser := revactx.ContextMustGetUser(ctx)
|
||||
|
||||
// Handle flags
|
||||
query, flags := ParseFlags(req.Query)
|
||||
for _, flag := range flags {
|
||||
switch flag {
|
||||
case "is:favorite":
|
||||
query += " Favorites:\"" + currentUser.GetId().GetOpaqueId() + "\""
|
||||
}
|
||||
}
|
||||
|
||||
// Extract scope from query if set
|
||||
query, scope := ParseScope(req.Query)
|
||||
query, scope := ParseScope(query)
|
||||
if query == "" {
|
||||
return nil, errtypes.BadRequest("empty query provided")
|
||||
}
|
||||
|
||||
@@ -61,6 +61,8 @@ func New(ctx context.Context, stream raw.Stream, logger log.Logger, tp trace.Tra
|
||||
events.TagsAdded{},
|
||||
events.TagsRemoved{},
|
||||
events.SpaceRenamed{},
|
||||
events.FavoriteAdded{},
|
||||
events.FavoriteRemoved{},
|
||||
},
|
||||
numConsumers: numConsumers,
|
||||
}
|
||||
@@ -198,6 +200,10 @@ func (s Service) processEvent(e raw.Event) error {
|
||||
s.indexSpaceDebouncer.Debounce(getSpaceID(ev.FileRef), e.Ack)
|
||||
case events.SpaceRenamed:
|
||||
s.indexSpaceDebouncer.Debounce(ev.ID, e.Ack)
|
||||
case events.FavoriteAdded:
|
||||
s.index.UpsertItem(ev.Ref)
|
||||
case events.FavoriteRemoved:
|
||||
s.index.UpsertItem(ev.Ref)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -288,6 +288,12 @@ _ocdav: api compatibility, return correct status code_
|
||||
- [coreApiFavorites/favoritesSharingToShares.feature:91](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/coreApiFavorites/favoritesSharingToShares.feature#L91)
|
||||
- [coreApiFavorites/favoritesSharingToShares.feature:92](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/coreApiFavorites/favoritesSharingToShares.feature#L92)
|
||||
- [coreApiFavorites/favoritesSharingToShares.feature:93](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/coreApiFavorites/favoritesSharingToShares.feature#L93)
|
||||
- [coreApiFavorites/favoritesSharingToShares.feature:112](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/coreApiFavorites/favoritesSharingToShares.feature#L112)
|
||||
- [coreApiFavorites/favoritesSharingToShares.feature:113](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/coreApiFavorites/favoritesSharingToShares.feature#L113)
|
||||
- [coreApiFavorites/favoritesSharingToShares.feature:114](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/coreApiFavorites/favoritesSharingToShares.feature#L114)
|
||||
- [apiSpacesShares/favorite.feature:56](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiSpacesShares/favorite.feature#L56)
|
||||
|
||||
|
||||
|
||||
#### [WWW-Authenticate header for unauthenticated requests is not clear](https://github.com/owncloud/ocis/issues/2285)
|
||||
|
||||
|
||||
+862
-839
File diff suppressed because it is too large
Load Diff
+78
@@ -89,6 +89,8 @@ const (
|
||||
GatewayAPI_ListStorageSpaces_FullMethodName = "/cs3.gateway.v1beta1.GatewayAPI/ListStorageSpaces"
|
||||
GatewayAPI_UpdateStorageSpace_FullMethodName = "/cs3.gateway.v1beta1.GatewayAPI/UpdateStorageSpace"
|
||||
GatewayAPI_DeleteStorageSpace_FullMethodName = "/cs3.gateway.v1beta1.GatewayAPI/DeleteStorageSpace"
|
||||
GatewayAPI_AddFavorite_FullMethodName = "/cs3.gateway.v1beta1.GatewayAPI/AddFavorite"
|
||||
GatewayAPI_RemoveFavorite_FullMethodName = "/cs3.gateway.v1beta1.GatewayAPI/RemoveFavorite"
|
||||
GatewayAPI_OpenInApp_FullMethodName = "/cs3.gateway.v1beta1.GatewayAPI/OpenInApp"
|
||||
GatewayAPI_CreateShare_FullMethodName = "/cs3.gateway.v1beta1.GatewayAPI/CreateShare"
|
||||
GatewayAPI_RemoveShare_FullMethodName = "/cs3.gateway.v1beta1.GatewayAPI/RemoveShare"
|
||||
@@ -289,6 +291,10 @@ type GatewayAPIClient interface {
|
||||
UpdateStorageSpace(ctx context.Context, in *v1beta11.UpdateStorageSpaceRequest, opts ...grpc.CallOption) (*v1beta11.UpdateStorageSpaceResponse, error)
|
||||
// Deletes a storage space.
|
||||
DeleteStorageSpace(ctx context.Context, in *v1beta11.DeleteStorageSpaceRequest, opts ...grpc.CallOption) (*v1beta11.DeleteStorageSpaceResponse, error)
|
||||
// Adds a resource as a favorite for a user.
|
||||
AddFavorite(ctx context.Context, in *v1beta11.AddFavoriteRequest, opts ...grpc.CallOption) (*v1beta11.AddFavoriteResponse, error)
|
||||
// Removes a resource from favorites for a user.
|
||||
RemoveFavorite(ctx context.Context, in *v1beta11.RemoveFavoriteRequest, opts ...grpc.CallOption) (*v1beta11.RemoveFavoriteResponse, error)
|
||||
// Returns the App URL and all necessary info to open a resource in an online editor.
|
||||
// MUST return CODE_NOT_FOUND if the resource does not exist.
|
||||
OpenInApp(ctx context.Context, in *OpenInAppRequest, opts ...grpc.CallOption) (*v1beta12.OpenInAppResponse, error)
|
||||
@@ -845,6 +851,24 @@ func (c *gatewayAPIClient) DeleteStorageSpace(ctx context.Context, in *v1beta11.
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *gatewayAPIClient) AddFavorite(ctx context.Context, in *v1beta11.AddFavoriteRequest, opts ...grpc.CallOption) (*v1beta11.AddFavoriteResponse, error) {
|
||||
out := new(v1beta11.AddFavoriteResponse)
|
||||
err := c.cc.Invoke(ctx, GatewayAPI_AddFavorite_FullMethodName, in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *gatewayAPIClient) RemoveFavorite(ctx context.Context, in *v1beta11.RemoveFavoriteRequest, opts ...grpc.CallOption) (*v1beta11.RemoveFavoriteResponse, error) {
|
||||
out := new(v1beta11.RemoveFavoriteResponse)
|
||||
err := c.cc.Invoke(ctx, GatewayAPI_RemoveFavorite_FullMethodName, in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *gatewayAPIClient) OpenInApp(ctx context.Context, in *OpenInAppRequest, opts ...grpc.CallOption) (*v1beta12.OpenInAppResponse, error) {
|
||||
out := new(v1beta12.OpenInAppResponse)
|
||||
err := c.cc.Invoke(ctx, GatewayAPI_OpenInApp_FullMethodName, in, out, opts...)
|
||||
@@ -1595,6 +1619,10 @@ type GatewayAPIServer interface {
|
||||
UpdateStorageSpace(context.Context, *v1beta11.UpdateStorageSpaceRequest) (*v1beta11.UpdateStorageSpaceResponse, error)
|
||||
// Deletes a storage space.
|
||||
DeleteStorageSpace(context.Context, *v1beta11.DeleteStorageSpaceRequest) (*v1beta11.DeleteStorageSpaceResponse, error)
|
||||
// Adds a resource as a favorite for a user.
|
||||
AddFavorite(context.Context, *v1beta11.AddFavoriteRequest) (*v1beta11.AddFavoriteResponse, error)
|
||||
// Removes a resource from favorites for a user.
|
||||
RemoveFavorite(context.Context, *v1beta11.RemoveFavoriteRequest) (*v1beta11.RemoveFavoriteResponse, error)
|
||||
// Returns the App URL and all necessary info to open a resource in an online editor.
|
||||
// MUST return CODE_NOT_FOUND if the resource does not exist.
|
||||
OpenInApp(context.Context, *OpenInAppRequest) (*v1beta12.OpenInAppResponse, error)
|
||||
@@ -1891,6 +1919,12 @@ func (UnimplementedGatewayAPIServer) UpdateStorageSpace(context.Context, *v1beta
|
||||
func (UnimplementedGatewayAPIServer) DeleteStorageSpace(context.Context, *v1beta11.DeleteStorageSpaceRequest) (*v1beta11.DeleteStorageSpaceResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DeleteStorageSpace not implemented")
|
||||
}
|
||||
func (UnimplementedGatewayAPIServer) AddFavorite(context.Context, *v1beta11.AddFavoriteRequest) (*v1beta11.AddFavoriteResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AddFavorite not implemented")
|
||||
}
|
||||
func (UnimplementedGatewayAPIServer) RemoveFavorite(context.Context, *v1beta11.RemoveFavoriteRequest) (*v1beta11.RemoveFavoriteResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RemoveFavorite not implemented")
|
||||
}
|
||||
func (UnimplementedGatewayAPIServer) OpenInApp(context.Context, *OpenInAppRequest) (*v1beta12.OpenInAppResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method OpenInApp not implemented")
|
||||
}
|
||||
@@ -2746,6 +2780,42 @@ func _GatewayAPI_DeleteStorageSpace_Handler(srv interface{}, ctx context.Context
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _GatewayAPI_AddFavorite_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(v1beta11.AddFavoriteRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(GatewayAPIServer).AddFavorite(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: GatewayAPI_AddFavorite_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(GatewayAPIServer).AddFavorite(ctx, req.(*v1beta11.AddFavoriteRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _GatewayAPI_RemoveFavorite_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(v1beta11.RemoveFavoriteRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(GatewayAPIServer).RemoveFavorite(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: GatewayAPI_RemoveFavorite_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(GatewayAPIServer).RemoveFavorite(ctx, req.(*v1beta11.RemoveFavoriteRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _GatewayAPI_OpenInApp_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(OpenInAppRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@@ -4127,6 +4197,14 @@ var GatewayAPI_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "DeleteStorageSpace",
|
||||
Handler: _GatewayAPI_DeleteStorageSpace_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "AddFavorite",
|
||||
Handler: _GatewayAPI_AddFavorite_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "RemoveFavorite",
|
||||
Handler: _GatewayAPI_RemoveFavorite_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "OpenInApp",
|
||||
Handler: _GatewayAPI_OpenInApp_Handler,
|
||||
|
||||
Generated
Vendored
+862
-484
File diff suppressed because it is too large
Load Diff
Generated
Vendored
+78
@@ -69,6 +69,8 @@ const (
|
||||
ProviderAPI_Unlock_FullMethodName = "/cs3.storage.provider.v1beta1.ProviderAPI/Unlock"
|
||||
ProviderAPI_CreateHome_FullMethodName = "/cs3.storage.provider.v1beta1.ProviderAPI/CreateHome"
|
||||
ProviderAPI_GetHome_FullMethodName = "/cs3.storage.provider.v1beta1.ProviderAPI/GetHome"
|
||||
ProviderAPI_AddFavorite_FullMethodName = "/cs3.storage.provider.v1beta1.ProviderAPI/AddFavorite"
|
||||
ProviderAPI_RemoveFavorite_FullMethodName = "/cs3.storage.provider.v1beta1.ProviderAPI/RemoveFavorite"
|
||||
)
|
||||
|
||||
// ProviderAPIClient is the client API for ProviderAPI service.
|
||||
@@ -204,6 +206,10 @@ type ProviderAPIClient interface {
|
||||
CreateHome(ctx context.Context, in *CreateHomeRequest, opts ...grpc.CallOption) (*CreateHomeResponse, error)
|
||||
// Gets the home path for the user.
|
||||
GetHome(ctx context.Context, in *GetHomeRequest, opts ...grpc.CallOption) (*GetHomeResponse, error)
|
||||
// Marks a resource as favorite for a user.
|
||||
AddFavorite(ctx context.Context, in *AddFavoriteRequest, opts ...grpc.CallOption) (*AddFavoriteResponse, error)
|
||||
// Unmarks a resource as favorite for a user.
|
||||
RemoveFavorite(ctx context.Context, in *RemoveFavoriteRequest, opts ...grpc.CallOption) (*RemoveFavoriteResponse, error)
|
||||
}
|
||||
|
||||
type providerAPIClient struct {
|
||||
@@ -548,6 +554,24 @@ func (c *providerAPIClient) GetHome(ctx context.Context, in *GetHomeRequest, opt
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *providerAPIClient) AddFavorite(ctx context.Context, in *AddFavoriteRequest, opts ...grpc.CallOption) (*AddFavoriteResponse, error) {
|
||||
out := new(AddFavoriteResponse)
|
||||
err := c.cc.Invoke(ctx, ProviderAPI_AddFavorite_FullMethodName, in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *providerAPIClient) RemoveFavorite(ctx context.Context, in *RemoveFavoriteRequest, opts ...grpc.CallOption) (*RemoveFavoriteResponse, error) {
|
||||
out := new(RemoveFavoriteResponse)
|
||||
err := c.cc.Invoke(ctx, ProviderAPI_RemoveFavorite_FullMethodName, in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ProviderAPIServer is the server API for ProviderAPI service.
|
||||
// All implementations should embed UnimplementedProviderAPIServer
|
||||
// for forward compatibility
|
||||
@@ -681,6 +705,10 @@ type ProviderAPIServer interface {
|
||||
CreateHome(context.Context, *CreateHomeRequest) (*CreateHomeResponse, error)
|
||||
// Gets the home path for the user.
|
||||
GetHome(context.Context, *GetHomeRequest) (*GetHomeResponse, error)
|
||||
// Marks a resource as favorite for a user.
|
||||
AddFavorite(context.Context, *AddFavoriteRequest) (*AddFavoriteResponse, error)
|
||||
// Unmarks a resource as favorite for a user.
|
||||
RemoveFavorite(context.Context, *RemoveFavoriteRequest) (*RemoveFavoriteResponse, error)
|
||||
}
|
||||
|
||||
// UnimplementedProviderAPIServer should be embedded to have forward compatible implementations.
|
||||
@@ -783,6 +811,12 @@ func (UnimplementedProviderAPIServer) CreateHome(context.Context, *CreateHomeReq
|
||||
func (UnimplementedProviderAPIServer) GetHome(context.Context, *GetHomeRequest) (*GetHomeResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetHome not implemented")
|
||||
}
|
||||
func (UnimplementedProviderAPIServer) AddFavorite(context.Context, *AddFavoriteRequest) (*AddFavoriteResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AddFavorite not implemented")
|
||||
}
|
||||
func (UnimplementedProviderAPIServer) RemoveFavorite(context.Context, *RemoveFavoriteRequest) (*RemoveFavoriteResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RemoveFavorite not implemented")
|
||||
}
|
||||
|
||||
// UnsafeProviderAPIServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to ProviderAPIServer will
|
||||
@@ -1377,6 +1411,42 @@ func _ProviderAPI_GetHome_Handler(srv interface{}, ctx context.Context, dec func
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _ProviderAPI_AddFavorite_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AddFavoriteRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ProviderAPIServer).AddFavorite(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: ProviderAPI_AddFavorite_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ProviderAPIServer).AddFavorite(ctx, req.(*AddFavoriteRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _ProviderAPI_RemoveFavorite_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(RemoveFavoriteRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ProviderAPIServer).RemoveFavorite(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: ProviderAPI_RemoveFavorite_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ProviderAPIServer).RemoveFavorite(ctx, req.(*RemoveFavoriteRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// ProviderAPI_ServiceDesc is the grpc.ServiceDesc for ProviderAPI service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@@ -1504,6 +1574,14 @@ var ProviderAPI_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "GetHome",
|
||||
Handler: _ProviderAPI_GetHome_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "AddFavorite",
|
||||
Handler: _ProviderAPI_AddFavorite_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "RemoveFavorite",
|
||||
Handler: _ProviderAPI_RemoveFavorite_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
|
||||
-177
@@ -1,177 +0,0 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
// Copyright 2014 Gary Burd
|
||||
//
|
||||
// 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.
|
||||
|
||||
package redis
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
connectionWatchState = 1 << iota
|
||||
connectionMultiState
|
||||
connectionSubscribeState
|
||||
connectionMonitorState
|
||||
)
|
||||
|
||||
type commandInfo struct {
|
||||
// Set or Clear these states on connection.
|
||||
Set, Clear int
|
||||
}
|
||||
|
||||
var commandInfos = map[string]commandInfo{
|
||||
"WATCH": {Set: connectionWatchState},
|
||||
"UNWATCH": {Clear: connectionWatchState},
|
||||
"MULTI": {Set: connectionMultiState},
|
||||
"EXEC": {Clear: connectionWatchState | connectionMultiState},
|
||||
"DISCARD": {Clear: connectionWatchState | connectionMultiState},
|
||||
"PSUBSCRIBE": {Set: connectionSubscribeState},
|
||||
"SUBSCRIBE": {Set: connectionSubscribeState},
|
||||
"MONITOR": {Set: connectionMonitorState},
|
||||
}
|
||||
|
||||
func init() {
|
||||
for n, ci := range commandInfos {
|
||||
commandInfos[strings.ToLower(n)] = ci
|
||||
}
|
||||
}
|
||||
|
||||
func lookupCommandInfo(commandName string) commandInfo {
|
||||
if ci, ok := commandInfos[commandName]; ok {
|
||||
return ci
|
||||
}
|
||||
return commandInfos[strings.ToUpper(commandName)]
|
||||
}
|
||||
-868
@@ -1,868 +0,0 @@
|
||||
// Copyright 2012 Gary Burd
|
||||
//
|
||||
// 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.
|
||||
|
||||
package redis
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
_ ConnWithTimeout = (*conn)(nil)
|
||||
)
|
||||
|
||||
// conn is the low-level implementation of Conn
|
||||
type conn struct {
|
||||
// Shared
|
||||
mu sync.Mutex
|
||||
pending int
|
||||
err error
|
||||
conn net.Conn
|
||||
|
||||
// Read
|
||||
readTimeout time.Duration
|
||||
br *bufio.Reader
|
||||
|
||||
// Write
|
||||
writeTimeout time.Duration
|
||||
bw *bufio.Writer
|
||||
|
||||
// Scratch space for formatting argument length.
|
||||
// '*' or '$', length, "\r\n"
|
||||
lenScratch [32]byte
|
||||
|
||||
// Scratch space for formatting integers and floats.
|
||||
numScratch [40]byte
|
||||
}
|
||||
|
||||
// DialTimeout acts like Dial but takes timeouts for establishing the
|
||||
// connection to the server, writing a command and reading a reply.
|
||||
//
|
||||
// Deprecated: Use Dial with options instead.
|
||||
func DialTimeout(network, address string, connectTimeout, readTimeout, writeTimeout time.Duration) (Conn, error) {
|
||||
return Dial(network, address,
|
||||
DialConnectTimeout(connectTimeout),
|
||||
DialReadTimeout(readTimeout),
|
||||
DialWriteTimeout(writeTimeout))
|
||||
}
|
||||
|
||||
// DialOption specifies an option for dialing a Redis server.
|
||||
type DialOption struct {
|
||||
f func(*dialOptions)
|
||||
}
|
||||
|
||||
type dialOptions struct {
|
||||
readTimeout time.Duration
|
||||
writeTimeout time.Duration
|
||||
tlsHandshakeTimeout time.Duration
|
||||
dialer *net.Dialer
|
||||
dialContext func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
db int
|
||||
username string
|
||||
password string
|
||||
clientName string
|
||||
useTLS bool
|
||||
skipVerify bool
|
||||
tlsConfig *tls.Config
|
||||
}
|
||||
|
||||
// DialTLSHandshakeTimeout specifies the maximum amount of time waiting to
|
||||
// wait for a TLS handshake. Zero means no timeout.
|
||||
// If no DialTLSHandshakeTimeout option is specified then the default is 30 seconds.
|
||||
func DialTLSHandshakeTimeout(d time.Duration) DialOption {
|
||||
return DialOption{func(do *dialOptions) {
|
||||
do.tlsHandshakeTimeout = d
|
||||
}}
|
||||
}
|
||||
|
||||
// DialReadTimeout specifies the timeout for reading a single command reply.
|
||||
func DialReadTimeout(d time.Duration) DialOption {
|
||||
return DialOption{func(do *dialOptions) {
|
||||
do.readTimeout = d
|
||||
}}
|
||||
}
|
||||
|
||||
// DialWriteTimeout specifies the timeout for writing a single command.
|
||||
func DialWriteTimeout(d time.Duration) DialOption {
|
||||
return DialOption{func(do *dialOptions) {
|
||||
do.writeTimeout = d
|
||||
}}
|
||||
}
|
||||
|
||||
// DialConnectTimeout specifies the timeout for connecting to the Redis server when
|
||||
// no DialNetDial option is specified.
|
||||
// If no DialConnectTimeout option is specified then the default is 30 seconds.
|
||||
func DialConnectTimeout(d time.Duration) DialOption {
|
||||
return DialOption{func(do *dialOptions) {
|
||||
do.dialer.Timeout = d
|
||||
}}
|
||||
}
|
||||
|
||||
// DialKeepAlive specifies the keep-alive period for TCP connections to the Redis server
|
||||
// when no DialNetDial option is specified.
|
||||
// If zero, keep-alives are not enabled. If no DialKeepAlive option is specified then
|
||||
// the default of 5 minutes is used to ensure that half-closed TCP sessions are detected.
|
||||
func DialKeepAlive(d time.Duration) DialOption {
|
||||
return DialOption{func(do *dialOptions) {
|
||||
do.dialer.KeepAlive = d
|
||||
}}
|
||||
}
|
||||
|
||||
// DialNetDial specifies a custom dial function for creating TCP
|
||||
// connections, otherwise a net.Dialer customized via the other options is used.
|
||||
// DialNetDial overrides DialConnectTimeout and DialKeepAlive.
|
||||
func DialNetDial(dial func(network, addr string) (net.Conn, error)) DialOption {
|
||||
return DialOption{func(do *dialOptions) {
|
||||
do.dialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return dial(network, addr)
|
||||
}
|
||||
}}
|
||||
}
|
||||
|
||||
// DialContextFunc specifies a custom dial function with context for creating TCP
|
||||
// connections, otherwise a net.Dialer customized via the other options is used.
|
||||
// DialContextFunc overrides DialConnectTimeout and DialKeepAlive.
|
||||
func DialContextFunc(f func(ctx context.Context, network, addr string) (net.Conn, error)) DialOption {
|
||||
return DialOption{func(do *dialOptions) {
|
||||
do.dialContext = f
|
||||
}}
|
||||
}
|
||||
|
||||
// DialDatabase specifies the database to select when dialing a connection.
|
||||
func DialDatabase(db int) DialOption {
|
||||
return DialOption{func(do *dialOptions) {
|
||||
do.db = db
|
||||
}}
|
||||
}
|
||||
|
||||
// DialPassword specifies the password to use when connecting to
|
||||
// the Redis server.
|
||||
func DialPassword(password string) DialOption {
|
||||
return DialOption{func(do *dialOptions) {
|
||||
do.password = password
|
||||
}}
|
||||
}
|
||||
|
||||
// DialUsername specifies the username to use when connecting to
|
||||
// the Redis server when Redis ACLs are used.
|
||||
// A DialPassword must also be passed otherwise this option will have no effect.
|
||||
func DialUsername(username string) DialOption {
|
||||
return DialOption{func(do *dialOptions) {
|
||||
do.username = username
|
||||
}}
|
||||
}
|
||||
|
||||
// DialClientName specifies a client name to be used
|
||||
// by the Redis server connection.
|
||||
func DialClientName(name string) DialOption {
|
||||
return DialOption{func(do *dialOptions) {
|
||||
do.clientName = name
|
||||
}}
|
||||
}
|
||||
|
||||
// DialTLSConfig specifies the config to use when a TLS connection is dialed.
|
||||
// Has no effect when not dialing a TLS connection.
|
||||
func DialTLSConfig(c *tls.Config) DialOption {
|
||||
return DialOption{func(do *dialOptions) {
|
||||
do.tlsConfig = c
|
||||
}}
|
||||
}
|
||||
|
||||
// DialTLSSkipVerify disables server name verification when connecting over
|
||||
// TLS. Has no effect when not dialing a TLS connection.
|
||||
func DialTLSSkipVerify(skip bool) DialOption {
|
||||
return DialOption{func(do *dialOptions) {
|
||||
do.skipVerify = skip
|
||||
}}
|
||||
}
|
||||
|
||||
// DialUseTLS specifies whether TLS should be used when connecting to the
|
||||
// server. This option is ignore by DialURL.
|
||||
func DialUseTLS(useTLS bool) DialOption {
|
||||
return DialOption{func(do *dialOptions) {
|
||||
do.useTLS = useTLS
|
||||
}}
|
||||
}
|
||||
|
||||
// Dial connects to the Redis server at the given network and
|
||||
// address using the specified options.
|
||||
func Dial(network, address string, options ...DialOption) (Conn, error) {
|
||||
return DialContext(context.Background(), network, address, options...)
|
||||
}
|
||||
|
||||
type tlsHandshakeTimeoutError struct{}
|
||||
|
||||
func (tlsHandshakeTimeoutError) Timeout() bool { return true }
|
||||
func (tlsHandshakeTimeoutError) Temporary() bool { return true }
|
||||
func (tlsHandshakeTimeoutError) Error() string { return "TLS handshake timeout" }
|
||||
|
||||
// DialContext connects to the Redis server at the given network and
|
||||
// address using the specified options and context.
|
||||
func DialContext(ctx context.Context, network, address string, options ...DialOption) (Conn, error) {
|
||||
do := dialOptions{
|
||||
dialer: &net.Dialer{
|
||||
Timeout: time.Second * 30,
|
||||
KeepAlive: time.Minute * 5,
|
||||
},
|
||||
tlsHandshakeTimeout: time.Second * 10,
|
||||
}
|
||||
for _, option := range options {
|
||||
option.f(&do)
|
||||
}
|
||||
if do.dialContext == nil {
|
||||
do.dialContext = do.dialer.DialContext
|
||||
}
|
||||
|
||||
netConn, err := do.dialContext(ctx, network, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if do.useTLS {
|
||||
var tlsConfig *tls.Config
|
||||
if do.tlsConfig == nil {
|
||||
tlsConfig = &tls.Config{InsecureSkipVerify: do.skipVerify}
|
||||
} else {
|
||||
tlsConfig = do.tlsConfig.Clone()
|
||||
}
|
||||
if tlsConfig.ServerName == "" {
|
||||
host, _, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
netConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
tlsConfig.ServerName = host
|
||||
}
|
||||
|
||||
tlsConn := tls.Client(netConn, tlsConfig)
|
||||
errc := make(chan error, 2) // buffered so we don't block timeout or Handshake
|
||||
if d := do.tlsHandshakeTimeout; d != 0 {
|
||||
timer := time.AfterFunc(d, func() {
|
||||
errc <- tlsHandshakeTimeoutError{}
|
||||
})
|
||||
defer timer.Stop()
|
||||
}
|
||||
go func() {
|
||||
errc <- tlsConn.Handshake()
|
||||
}()
|
||||
if err := <-errc; err != nil {
|
||||
// Timeout or Handshake error.
|
||||
netConn.Close() // nolint: errcheck
|
||||
return nil, err
|
||||
}
|
||||
|
||||
netConn = tlsConn
|
||||
}
|
||||
|
||||
c := &conn{
|
||||
conn: netConn,
|
||||
bw: bufio.NewWriter(netConn),
|
||||
br: bufio.NewReader(netConn),
|
||||
readTimeout: do.readTimeout,
|
||||
writeTimeout: do.writeTimeout,
|
||||
}
|
||||
|
||||
if do.password != "" {
|
||||
authArgs := make([]interface{}, 0, 2)
|
||||
if do.username != "" {
|
||||
authArgs = append(authArgs, do.username)
|
||||
}
|
||||
authArgs = append(authArgs, do.password)
|
||||
if _, err := c.DoContext(ctx, "AUTH", authArgs...); err != nil {
|
||||
netConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if do.clientName != "" {
|
||||
if _, err := c.DoContext(ctx, "CLIENT", "SETNAME", do.clientName); err != nil {
|
||||
netConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if do.db != 0 {
|
||||
if _, err := c.DoContext(ctx, "SELECT", do.db); err != nil {
|
||||
netConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
var pathDBRegexp = regexp.MustCompile(`/(\d*)\z`)
|
||||
|
||||
// DialURL wraps DialURLContext using context.Background.
|
||||
func DialURL(rawurl string, options ...DialOption) (Conn, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
return DialURLContext(ctx, rawurl, options...)
|
||||
}
|
||||
|
||||
// DialURLContext connects to a Redis server at the given URL using the Redis
|
||||
// URI scheme. URLs should follow the draft IANA specification for the
|
||||
// scheme (https://www.iana.org/assignments/uri-schemes/prov/redis).
|
||||
func DialURLContext(ctx context.Context, rawurl string, options ...DialOption) (Conn, error) {
|
||||
u, err := url.Parse(rawurl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "redis", "rediss", "valkey", "valkeys":
|
||||
// valid scheme
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid redis URL scheme: %s", u.Scheme)
|
||||
}
|
||||
|
||||
if u.Opaque != "" {
|
||||
return nil, fmt.Errorf("invalid redis URL, url is opaque: %s", rawurl)
|
||||
}
|
||||
|
||||
// As per the IANA draft spec, the host defaults to localhost and
|
||||
// the port defaults to 6379.
|
||||
host, port, err := net.SplitHostPort(u.Host)
|
||||
if err != nil {
|
||||
// assume port is missing
|
||||
host = u.Host
|
||||
port = "6379"
|
||||
}
|
||||
if host == "" {
|
||||
host = "localhost"
|
||||
}
|
||||
address := net.JoinHostPort(host, port)
|
||||
|
||||
if u.User != nil {
|
||||
password, isSet := u.User.Password()
|
||||
username := u.User.Username()
|
||||
if isSet {
|
||||
if username != "" {
|
||||
// ACL
|
||||
options = append(options, DialUsername(username), DialPassword(password))
|
||||
} else {
|
||||
// requirepass - user-info username:password with blank username
|
||||
options = append(options, DialPassword(password))
|
||||
}
|
||||
} else if username != "" {
|
||||
// requirepass - redis-cli compatibility which treats as single arg in user-info as a password
|
||||
options = append(options, DialPassword(username))
|
||||
}
|
||||
}
|
||||
|
||||
match := pathDBRegexp.FindStringSubmatch(u.Path)
|
||||
if len(match) == 2 {
|
||||
db := 0
|
||||
if len(match[1]) > 0 {
|
||||
db, err = strconv.Atoi(match[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid database: %s", u.Path[1:])
|
||||
}
|
||||
}
|
||||
if db != 0 {
|
||||
options = append(options, DialDatabase(db))
|
||||
}
|
||||
} else if u.Path != "" {
|
||||
return nil, fmt.Errorf("invalid database: %s", u.Path[1:])
|
||||
}
|
||||
|
||||
options = append(options, DialUseTLS(u.Scheme == "rediss" || u.Scheme == "valkeys"))
|
||||
|
||||
return DialContext(ctx, "tcp", address, options...)
|
||||
}
|
||||
|
||||
// NewConn returns a new Redigo connection for the given net connection.
|
||||
func NewConn(netConn net.Conn, readTimeout, writeTimeout time.Duration) Conn {
|
||||
return &conn{
|
||||
conn: netConn,
|
||||
bw: bufio.NewWriter(netConn),
|
||||
br: bufio.NewReader(netConn),
|
||||
readTimeout: readTimeout,
|
||||
writeTimeout: writeTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *conn) Close() error {
|
||||
c.mu.Lock()
|
||||
err := c.err
|
||||
if c.err == nil {
|
||||
c.err = errors.New("redigo: closed")
|
||||
err = c.conn.Close()
|
||||
}
|
||||
c.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *conn) fatal(err error) error {
|
||||
c.mu.Lock()
|
||||
if c.err == nil {
|
||||
c.err = err
|
||||
// Close connection to force errors on subsequent calls and to unblock
|
||||
// other reader or writer.
|
||||
c.conn.Close()
|
||||
}
|
||||
c.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *conn) Err() error {
|
||||
c.mu.Lock()
|
||||
err := c.err
|
||||
c.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *conn) writeLen(prefix byte, n int) error {
|
||||
c.lenScratch[len(c.lenScratch)-1] = '\n'
|
||||
c.lenScratch[len(c.lenScratch)-2] = '\r'
|
||||
i := len(c.lenScratch) - 3
|
||||
for {
|
||||
c.lenScratch[i] = byte('0' + n%10)
|
||||
i -= 1
|
||||
n = n / 10
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
c.lenScratch[i] = prefix
|
||||
_, err := c.bw.Write(c.lenScratch[i:])
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *conn) writeString(s string) error {
|
||||
if err := c.writeLen('$', len(s)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.bw.WriteString(s); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := c.bw.WriteString("\r\n")
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *conn) writeBytes(p []byte) error {
|
||||
if err := c.writeLen('$', len(p)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.bw.Write(p); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := c.bw.WriteString("\r\n")
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *conn) writeInt64(n int64) error {
|
||||
return c.writeBytes(strconv.AppendInt(c.numScratch[:0], n, 10))
|
||||
}
|
||||
|
||||
func (c *conn) writeFloat64(n float64) error {
|
||||
return c.writeBytes(strconv.AppendFloat(c.numScratch[:0], n, 'g', -1, 64))
|
||||
}
|
||||
|
||||
func (c *conn) writeCommand(cmd string, args []interface{}) error {
|
||||
if err := c.writeLen('*', 1+len(args)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.writeString(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, arg := range args {
|
||||
if err := c.writeArg(arg, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *conn) writeArg(arg interface{}, argumentTypeOK bool) (err error) {
|
||||
switch arg := arg.(type) {
|
||||
case string:
|
||||
return c.writeString(arg)
|
||||
case []byte:
|
||||
return c.writeBytes(arg)
|
||||
case int:
|
||||
return c.writeInt64(int64(arg))
|
||||
case int64:
|
||||
return c.writeInt64(arg)
|
||||
case float64:
|
||||
return c.writeFloat64(arg)
|
||||
case bool:
|
||||
if arg {
|
||||
return c.writeString("1")
|
||||
} else {
|
||||
return c.writeString("0")
|
||||
}
|
||||
case nil:
|
||||
return c.writeString("")
|
||||
case Argument:
|
||||
if argumentTypeOK {
|
||||
return c.writeArg(arg.RedisArg(), false)
|
||||
}
|
||||
// See comment in default clause below.
|
||||
var buf bytes.Buffer
|
||||
fmt.Fprint(&buf, arg)
|
||||
return c.writeBytes(buf.Bytes())
|
||||
default:
|
||||
// This default clause is intended to handle builtin numeric types.
|
||||
// The function should return an error for other types, but this is not
|
||||
// done for compatibility with previous versions of the package.
|
||||
var buf bytes.Buffer
|
||||
fmt.Fprint(&buf, arg)
|
||||
return c.writeBytes(buf.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
type protocolError string
|
||||
|
||||
func (pe protocolError) Error() string {
|
||||
return fmt.Sprintf("redigo: %s (possible server error or unsupported concurrent read by application)", string(pe))
|
||||
}
|
||||
|
||||
// readLine reads a line of input from the RESP stream.
|
||||
func (c *conn) readLine() ([]byte, error) {
|
||||
// To avoid allocations, attempt to read the line using ReadSlice. This
|
||||
// call typically succeeds. The known case where the call fails is when
|
||||
// reading the output from the MONITOR command.
|
||||
p, err := c.br.ReadSlice('\n')
|
||||
if err == bufio.ErrBufferFull {
|
||||
// The line does not fit in the bufio.Reader's buffer. Fall back to
|
||||
// allocating a buffer for the line.
|
||||
buf := append([]byte{}, p...)
|
||||
for err == bufio.ErrBufferFull {
|
||||
p, err = c.br.ReadSlice('\n')
|
||||
buf = append(buf, p...)
|
||||
}
|
||||
p = buf
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
i := len(p) - 2
|
||||
if i < 0 || p[i] != '\r' {
|
||||
return nil, protocolError("bad response line terminator")
|
||||
}
|
||||
return p[:i], nil
|
||||
}
|
||||
|
||||
// parseLen parses bulk string and array lengths.
|
||||
func parseLen(p []byte) (int, error) {
|
||||
if len(p) == 0 {
|
||||
return -1, protocolError("malformed length")
|
||||
}
|
||||
|
||||
if p[0] == '-' && len(p) == 2 && p[1] == '1' {
|
||||
// handle $-1 and $-1 null replies.
|
||||
return -1, nil
|
||||
}
|
||||
|
||||
var n int
|
||||
for _, b := range p {
|
||||
n *= 10
|
||||
if b < '0' || b > '9' {
|
||||
return -1, protocolError("illegal bytes in length")
|
||||
}
|
||||
n += int(b - '0')
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// parseInt parses an integer reply.
|
||||
func parseInt(p []byte) (interface{}, error) {
|
||||
if len(p) == 0 {
|
||||
return 0, protocolError("malformed integer")
|
||||
}
|
||||
|
||||
var negate bool
|
||||
if p[0] == '-' {
|
||||
negate = true
|
||||
p = p[1:]
|
||||
if len(p) == 0 {
|
||||
return 0, protocolError("malformed integer")
|
||||
}
|
||||
}
|
||||
|
||||
var n int64
|
||||
for _, b := range p {
|
||||
n *= 10
|
||||
if b < '0' || b > '9' {
|
||||
return 0, protocolError("illegal bytes in length")
|
||||
}
|
||||
n += int64(b - '0')
|
||||
}
|
||||
|
||||
if negate {
|
||||
n = -n
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
var (
|
||||
okReply interface{} = "OK"
|
||||
pongReply interface{} = "PONG"
|
||||
)
|
||||
|
||||
func (c *conn) readReply() (interface{}, error) {
|
||||
line, err := c.readLine()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) == 0 {
|
||||
return nil, protocolError("short response line")
|
||||
}
|
||||
switch line[0] {
|
||||
case '+':
|
||||
switch string(line[1:]) {
|
||||
case "OK":
|
||||
// Avoid allocation for frequent "+OK" response.
|
||||
return okReply, nil
|
||||
case "PONG":
|
||||
// Avoid allocation in PING command benchmarks :)
|
||||
return pongReply, nil
|
||||
default:
|
||||
return string(line[1:]), nil
|
||||
}
|
||||
case '-':
|
||||
return Error(line[1:]), nil
|
||||
case ':':
|
||||
return parseInt(line[1:])
|
||||
case '$':
|
||||
n, err := parseLen(line[1:])
|
||||
if n < 0 || err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p := make([]byte, n)
|
||||
_, err = io.ReadFull(c.br, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if line, err := c.readLine(); err != nil {
|
||||
return nil, err
|
||||
} else if len(line) != 0 {
|
||||
return nil, protocolError("bad bulk string format")
|
||||
}
|
||||
return p, nil
|
||||
case '*':
|
||||
n, err := parseLen(line[1:])
|
||||
if n < 0 || err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r := make([]interface{}, n)
|
||||
for i := range r {
|
||||
r[i], err = c.readReply()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
return nil, protocolError("unexpected response line")
|
||||
}
|
||||
|
||||
func (c *conn) Send(cmd string, args ...interface{}) error {
|
||||
c.mu.Lock()
|
||||
c.pending += 1
|
||||
c.mu.Unlock()
|
||||
if c.writeTimeout != 0 {
|
||||
if err := c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout)); err != nil {
|
||||
return c.fatal(err)
|
||||
}
|
||||
}
|
||||
if err := c.writeCommand(cmd, args); err != nil {
|
||||
return c.fatal(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *conn) Flush() error {
|
||||
if c.writeTimeout != 0 {
|
||||
if err := c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout)); err != nil {
|
||||
return c.fatal(err)
|
||||
}
|
||||
}
|
||||
if err := c.bw.Flush(); err != nil {
|
||||
return c.fatal(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *conn) Receive() (interface{}, error) {
|
||||
return c.ReceiveWithTimeout(c.readTimeout)
|
||||
}
|
||||
|
||||
func (c *conn) ReceiveContext(ctx context.Context) (interface{}, error) {
|
||||
var realTimeout time.Duration
|
||||
if dl, ok := ctx.Deadline(); ok {
|
||||
timeout := time.Until(dl)
|
||||
if timeout >= c.readTimeout && c.readTimeout != 0 {
|
||||
realTimeout = c.readTimeout
|
||||
} else if timeout <= 0 {
|
||||
return nil, c.fatal(context.DeadlineExceeded)
|
||||
} else {
|
||||
realTimeout = timeout
|
||||
}
|
||||
} else {
|
||||
realTimeout = c.readTimeout
|
||||
}
|
||||
endch := make(chan struct{})
|
||||
var r interface{}
|
||||
var e error
|
||||
go func() {
|
||||
defer close(endch)
|
||||
|
||||
r, e = c.ReceiveWithTimeout(realTimeout)
|
||||
}()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, c.fatal(ctx.Err())
|
||||
case <-endch:
|
||||
return r, e
|
||||
}
|
||||
}
|
||||
|
||||
func (c *conn) ReceiveWithTimeout(timeout time.Duration) (reply interface{}, err error) {
|
||||
var deadline time.Time
|
||||
if timeout != 0 {
|
||||
deadline = time.Now().Add(timeout)
|
||||
}
|
||||
if err := c.conn.SetReadDeadline(deadline); err != nil {
|
||||
return nil, c.fatal(err)
|
||||
}
|
||||
|
||||
if reply, err = c.readReply(); err != nil {
|
||||
return nil, c.fatal(err)
|
||||
}
|
||||
// When using pub/sub, the number of receives can be greater than the
|
||||
// number of sends. To enable normal use of the connection after
|
||||
// unsubscribing from all channels, we do not decrement pending to a
|
||||
// negative value.
|
||||
//
|
||||
// The pending field is decremented after the reply is read to handle the
|
||||
// case where Receive is called before Send.
|
||||
c.mu.Lock()
|
||||
if c.pending > 0 {
|
||||
c.pending -= 1
|
||||
}
|
||||
c.mu.Unlock()
|
||||
if err, ok := reply.(Error); ok {
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *conn) Do(cmd string, args ...interface{}) (interface{}, error) {
|
||||
return c.DoWithTimeout(c.readTimeout, cmd, args...)
|
||||
}
|
||||
|
||||
func (c *conn) DoContext(ctx context.Context, cmd string, args ...interface{}) (interface{}, error) {
|
||||
var realTimeout time.Duration
|
||||
if dl, ok := ctx.Deadline(); ok {
|
||||
timeout := time.Until(dl)
|
||||
if timeout >= c.readTimeout && c.readTimeout != 0 {
|
||||
realTimeout = c.readTimeout
|
||||
} else if timeout <= 0 {
|
||||
return nil, c.fatal(context.DeadlineExceeded)
|
||||
} else {
|
||||
realTimeout = timeout
|
||||
}
|
||||
} else {
|
||||
realTimeout = c.readTimeout
|
||||
}
|
||||
endch := make(chan struct{})
|
||||
var r interface{}
|
||||
var e error
|
||||
go func() {
|
||||
defer close(endch)
|
||||
|
||||
r, e = c.DoWithTimeout(realTimeout, cmd, args...)
|
||||
}()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, c.fatal(ctx.Err())
|
||||
case <-endch:
|
||||
return r, e
|
||||
}
|
||||
}
|
||||
|
||||
func (c *conn) DoWithTimeout(readTimeout time.Duration, cmd string, args ...interface{}) (interface{}, error) {
|
||||
c.mu.Lock()
|
||||
pending := c.pending
|
||||
c.pending = 0
|
||||
c.mu.Unlock()
|
||||
|
||||
if cmd == "" && pending == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if c.writeTimeout != 0 {
|
||||
if err := c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout)); err != nil {
|
||||
return nil, c.fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if cmd != "" {
|
||||
if err := c.writeCommand(cmd, args); err != nil {
|
||||
return nil, c.fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.bw.Flush(); err != nil {
|
||||
return nil, c.fatal(err)
|
||||
}
|
||||
|
||||
var deadline time.Time
|
||||
if readTimeout != 0 {
|
||||
deadline = time.Now().Add(readTimeout)
|
||||
}
|
||||
if err := c.conn.SetReadDeadline(deadline); err != nil {
|
||||
return nil, c.fatal(err)
|
||||
}
|
||||
|
||||
if cmd == "" {
|
||||
reply := make([]interface{}, pending)
|
||||
for i := range reply {
|
||||
r, e := c.readReply()
|
||||
if e != nil {
|
||||
return nil, c.fatal(e)
|
||||
}
|
||||
reply[i] = r
|
||||
}
|
||||
return reply, nil
|
||||
}
|
||||
|
||||
var err error
|
||||
var reply interface{}
|
||||
for i := 0; i <= pending; i++ {
|
||||
var e error
|
||||
if reply, e = c.readReply(); e != nil {
|
||||
return nil, c.fatal(e)
|
||||
}
|
||||
if e, ok := reply.(Error); ok && err == nil {
|
||||
err = e
|
||||
}
|
||||
}
|
||||
return reply, err
|
||||
}
|
||||
-177
@@ -1,177 +0,0 @@
|
||||
// Copyright 2012 Gary Burd
|
||||
//
|
||||
// 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.
|
||||
|
||||
// Package redis is a client for the Redis database.
|
||||
//
|
||||
// The Redigo FAQ (https://github.com/gomodule/redigo/wiki/FAQ) contains more
|
||||
// documentation about this package.
|
||||
//
|
||||
// Connections
|
||||
//
|
||||
// The Conn interface is the primary interface for working with Redis.
|
||||
// Applications create connections by calling the Dial, DialWithTimeout or
|
||||
// NewConn functions. In the future, functions will be added for creating
|
||||
// sharded and other types of connections.
|
||||
//
|
||||
// The application must call the connection Close method when the application
|
||||
// is done with the connection.
|
||||
//
|
||||
// Executing Commands
|
||||
//
|
||||
// The Conn interface has a generic method for executing Redis commands:
|
||||
//
|
||||
// Do(commandName string, args ...interface{}) (reply interface{}, err error)
|
||||
//
|
||||
// The Redis command reference (http://redis.io/commands) lists the available
|
||||
// commands. An example of using the Redis APPEND command is:
|
||||
//
|
||||
// n, err := conn.Do("APPEND", "key", "value")
|
||||
//
|
||||
// The Do method converts command arguments to bulk strings for transmission
|
||||
// to the server as follows:
|
||||
//
|
||||
// Go Type Conversion
|
||||
// []byte Sent as is
|
||||
// string Sent as is
|
||||
// int, int64 strconv.FormatInt(v)
|
||||
// float64 strconv.FormatFloat(v, 'g', -1, 64)
|
||||
// bool true -> "1", false -> "0"
|
||||
// nil ""
|
||||
// all other types fmt.Fprint(w, v)
|
||||
//
|
||||
// Redis command reply types are represented using the following Go types:
|
||||
//
|
||||
// Redis type Go type
|
||||
// error redis.Error
|
||||
// integer int64
|
||||
// simple string string
|
||||
// bulk string []byte or nil if value not present.
|
||||
// array []interface{} or nil if value not present.
|
||||
//
|
||||
// Use type assertions or the reply helper functions to convert from
|
||||
// interface{} to the specific Go type for the command result.
|
||||
//
|
||||
// Pipelining
|
||||
//
|
||||
// Connections support pipelining using the Send, Flush and Receive methods.
|
||||
//
|
||||
// Send(commandName string, args ...interface{}) error
|
||||
// Flush() error
|
||||
// Receive() (reply interface{}, err error)
|
||||
//
|
||||
// Send writes the command to the connection's output buffer. Flush flushes the
|
||||
// connection's output buffer to the server. Receive reads a single reply from
|
||||
// the server. The following example shows a simple pipeline.
|
||||
//
|
||||
// c.Send("SET", "foo", "bar")
|
||||
// c.Send("GET", "foo")
|
||||
// c.Flush()
|
||||
// c.Receive() // reply from SET
|
||||
// v, err = c.Receive() // reply from GET
|
||||
//
|
||||
// The Do method combines the functionality of the Send, Flush and Receive
|
||||
// methods. The Do method starts by writing the command and flushing the output
|
||||
// buffer. Next, the Do method receives all pending replies including the reply
|
||||
// for the command just sent by Do. If any of the received replies is an error,
|
||||
// then Do returns the error. If there are no errors, then Do returns the last
|
||||
// reply. If the command argument to the Do method is "", then the Do method
|
||||
// will flush the output buffer and receive pending replies without sending a
|
||||
// command.
|
||||
//
|
||||
// Use the Send and Do methods to implement pipelined transactions.
|
||||
//
|
||||
// c.Send("MULTI")
|
||||
// c.Send("INCR", "foo")
|
||||
// c.Send("INCR", "bar")
|
||||
// r, err := c.Do("EXEC")
|
||||
// fmt.Println(r) // prints [1, 1]
|
||||
//
|
||||
// Concurrency
|
||||
//
|
||||
// Connections support one concurrent caller to the Receive method and one
|
||||
// concurrent caller to the Send and Flush methods. No other concurrency is
|
||||
// supported including concurrent calls to the Do and Close methods.
|
||||
//
|
||||
// For full concurrent access to Redis, use the thread-safe Pool to get, use
|
||||
// and release a connection from within a goroutine. Connections returned from
|
||||
// a Pool have the concurrency restrictions described in the previous
|
||||
// paragraph.
|
||||
//
|
||||
// Publish and Subscribe
|
||||
//
|
||||
// Use the Send, Flush and Receive methods to implement Pub/Sub subscribers.
|
||||
//
|
||||
// c.Send("SUBSCRIBE", "example")
|
||||
// c.Flush()
|
||||
// for {
|
||||
// reply, err := c.Receive()
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// // process pushed message
|
||||
// }
|
||||
//
|
||||
// The PubSubConn type wraps a Conn with convenience methods for implementing
|
||||
// subscribers. The Subscribe, PSubscribe, Unsubscribe and PUnsubscribe methods
|
||||
// send and flush a subscription management command. The receive method
|
||||
// converts a pushed message to convenient types for use in a type switch.
|
||||
//
|
||||
// psc := redis.PubSubConn{Conn: c}
|
||||
// psc.Subscribe("example")
|
||||
// for {
|
||||
// switch v := psc.Receive().(type) {
|
||||
// case redis.Message:
|
||||
// fmt.Printf("%s: message: %s\n", v.Channel, v.Data)
|
||||
// case redis.Subscription:
|
||||
// fmt.Printf("%s: %s %d\n", v.Channel, v.Kind, v.Count)
|
||||
// case error:
|
||||
// return v
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Reply Helpers
|
||||
//
|
||||
// The Bool, Int, Bytes, String, Strings and Values functions convert a reply
|
||||
// to a value of a specific type. To allow convenient wrapping of calls to the
|
||||
// connection Do and Receive methods, the functions take a second argument of
|
||||
// type error. If the error is non-nil, then the helper function returns the
|
||||
// error. If the error is nil, the function converts the reply to the specified
|
||||
// type:
|
||||
//
|
||||
// exists, err := redis.Bool(c.Do("EXISTS", "foo"))
|
||||
// if err != nil {
|
||||
// // handle error return from c.Do or type conversion error.
|
||||
// }
|
||||
//
|
||||
// The Scan function converts elements of a array reply to Go types:
|
||||
//
|
||||
// var value1 int
|
||||
// var value2 string
|
||||
// reply, err := redis.Values(c.Do("MGET", "key1", "key2"))
|
||||
// if err != nil {
|
||||
// // handle error
|
||||
// }
|
||||
// if _, err := redis.Scan(reply, &value1, &value2); err != nil {
|
||||
// // handle error
|
||||
// }
|
||||
//
|
||||
// Errors
|
||||
//
|
||||
// Connection methods return error replies from the server as type redis.Error.
|
||||
//
|
||||
// Call the connection Err() method to determine if the connection encountered
|
||||
// non-recoverable error such as a network error or protocol parsing error. If
|
||||
// Err() returns a non-nil value, then the connection is not usable and should
|
||||
// be closed.
|
||||
package redis
|
||||
-159
@@ -1,159 +0,0 @@
|
||||
// Copyright 2012 Gary Burd
|
||||
//
|
||||
// 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.
|
||||
|
||||
package redis
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
_ ConnWithTimeout = (*loggingConn)(nil)
|
||||
)
|
||||
|
||||
// NewLoggingConn returns a logging wrapper around a connection.
|
||||
func NewLoggingConn(conn Conn, logger *log.Logger, prefix string) Conn {
|
||||
if prefix != "" {
|
||||
prefix = prefix + "."
|
||||
}
|
||||
return &loggingConn{conn, logger, prefix, nil}
|
||||
}
|
||||
|
||||
//NewLoggingConnFilter returns a logging wrapper around a connection and a filter function.
|
||||
func NewLoggingConnFilter(conn Conn, logger *log.Logger, prefix string, skip func(cmdName string) bool) Conn {
|
||||
if prefix != "" {
|
||||
prefix = prefix + "."
|
||||
}
|
||||
return &loggingConn{conn, logger, prefix, skip}
|
||||
}
|
||||
|
||||
type loggingConn struct {
|
||||
Conn
|
||||
logger *log.Logger
|
||||
prefix string
|
||||
skip func(cmdName string) bool
|
||||
}
|
||||
|
||||
func (c *loggingConn) Close() error {
|
||||
err := c.Conn.Close()
|
||||
var buf bytes.Buffer
|
||||
fmt.Fprintf(&buf, "%sClose() -> (%v)", c.prefix, err)
|
||||
c.logger.Output(2, buf.String()) // nolint: errcheck
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *loggingConn) printValue(buf *bytes.Buffer, v interface{}) {
|
||||
const chop = 32
|
||||
switch v := v.(type) {
|
||||
case []byte:
|
||||
if len(v) > chop {
|
||||
fmt.Fprintf(buf, "%q...", v[:chop])
|
||||
} else {
|
||||
fmt.Fprintf(buf, "%q", v)
|
||||
}
|
||||
case string:
|
||||
if len(v) > chop {
|
||||
fmt.Fprintf(buf, "%q...", v[:chop])
|
||||
} else {
|
||||
fmt.Fprintf(buf, "%q", v)
|
||||
}
|
||||
case []interface{}:
|
||||
if len(v) == 0 {
|
||||
buf.WriteString("[]")
|
||||
} else {
|
||||
sep := "["
|
||||
fin := "]"
|
||||
if len(v) > chop {
|
||||
v = v[:chop]
|
||||
fin = "...]"
|
||||
}
|
||||
for _, vv := range v {
|
||||
buf.WriteString(sep)
|
||||
c.printValue(buf, vv)
|
||||
sep = ", "
|
||||
}
|
||||
buf.WriteString(fin)
|
||||
}
|
||||
default:
|
||||
fmt.Fprint(buf, v)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *loggingConn) print(method, commandName string, args []interface{}, reply interface{}, err error) {
|
||||
if c.skip != nil && c.skip(commandName) {
|
||||
return
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
fmt.Fprintf(&buf, "%s%s(", c.prefix, method)
|
||||
if method != "Receive" {
|
||||
buf.WriteString(commandName)
|
||||
for _, arg := range args {
|
||||
buf.WriteString(", ")
|
||||
c.printValue(&buf, arg)
|
||||
}
|
||||
}
|
||||
buf.WriteString(") -> (")
|
||||
if method != "Send" {
|
||||
c.printValue(&buf, reply)
|
||||
buf.WriteString(", ")
|
||||
}
|
||||
fmt.Fprintf(&buf, "%v)", err)
|
||||
c.logger.Output(3, buf.String()) // nolint: errcheck
|
||||
}
|
||||
|
||||
func (c *loggingConn) Do(commandName string, args ...interface{}) (interface{}, error) {
|
||||
reply, err := c.Conn.Do(commandName, args...)
|
||||
c.print("Do", commandName, args, reply, err)
|
||||
return reply, err
|
||||
}
|
||||
|
||||
func (c *loggingConn) DoContext(ctx context.Context, commandName string, args ...interface{}) (interface{}, error) {
|
||||
reply, err := DoContext(c.Conn, ctx, commandName, args...)
|
||||
c.print("DoContext", commandName, args, reply, err)
|
||||
return reply, err
|
||||
}
|
||||
|
||||
func (c *loggingConn) DoWithTimeout(timeout time.Duration, commandName string, args ...interface{}) (interface{}, error) {
|
||||
reply, err := DoWithTimeout(c.Conn, timeout, commandName, args...)
|
||||
c.print("DoWithTimeout", commandName, args, reply, err)
|
||||
return reply, err
|
||||
}
|
||||
|
||||
func (c *loggingConn) Send(commandName string, args ...interface{}) error {
|
||||
err := c.Conn.Send(commandName, args...)
|
||||
c.print("Send", commandName, args, nil, err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *loggingConn) Receive() (interface{}, error) {
|
||||
reply, err := c.Conn.Receive()
|
||||
c.print("Receive", "", nil, reply, err)
|
||||
return reply, err
|
||||
}
|
||||
|
||||
func (c *loggingConn) ReceiveContext(ctx context.Context) (interface{}, error) {
|
||||
reply, err := ReceiveContext(c.Conn, ctx)
|
||||
c.print("ReceiveContext", "", nil, reply, err)
|
||||
return reply, err
|
||||
}
|
||||
|
||||
func (c *loggingConn) ReceiveWithTimeout(timeout time.Duration) (interface{}, error) {
|
||||
reply, err := ReceiveWithTimeout(c.Conn, timeout)
|
||||
c.print("ReceiveWithTimeout", "", nil, reply, err)
|
||||
return reply, err
|
||||
}
|
||||
-675
@@ -1,675 +0,0 @@
|
||||
// Copyright 2012 Gary Burd
|
||||
//
|
||||
// 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.
|
||||
|
||||
package redis
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha1"
|
||||
"errors"
|
||||
"io"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
_ ConnWithTimeout = (*activeConn)(nil)
|
||||
_ ConnWithTimeout = (*errorConn)(nil)
|
||||
)
|
||||
|
||||
var nowFunc = time.Now // for testing
|
||||
|
||||
// ErrPoolExhausted is returned from a pool connection method (Do, Send,
|
||||
// Receive, Flush, Err) when the maximum number of database connections in the
|
||||
// pool has been reached.
|
||||
var ErrPoolExhausted = errors.New("redigo: connection pool exhausted")
|
||||
|
||||
var (
|
||||
errConnClosed = errors.New("redigo: connection closed")
|
||||
)
|
||||
|
||||
// Pool maintains a pool of connections. The application calls the Get method
|
||||
// to get a connection from the pool and the connection's Close method to
|
||||
// return the connection's resources to the pool.
|
||||
//
|
||||
// The following example shows how to use a pool in a web application. The
|
||||
// application creates a pool at application startup and makes it available to
|
||||
// request handlers using a package level variable. The pool configuration used
|
||||
// here is an example, not a recommendation.
|
||||
//
|
||||
// func newPool(addr string) *redis.Pool {
|
||||
// return &redis.Pool{
|
||||
// MaxIdle: 3,
|
||||
// IdleTimeout: 240 * time.Second,
|
||||
// // Dial or DialContext must be set. When both are set, DialContext takes precedence over Dial.
|
||||
// Dial: func () (redis.Conn, error) { return redis.Dial("tcp", addr) },
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// var (
|
||||
// pool *redis.Pool
|
||||
// redisServer = flag.String("redisServer", ":6379", "")
|
||||
// )
|
||||
//
|
||||
// func main() {
|
||||
// flag.Parse()
|
||||
// pool = newPool(*redisServer)
|
||||
// ...
|
||||
// }
|
||||
//
|
||||
// A request handler gets a connection from the pool and closes the connection
|
||||
// when the handler is done:
|
||||
//
|
||||
// func serveHome(w http.ResponseWriter, r *http.Request) {
|
||||
// conn := pool.Get()
|
||||
// defer conn.Close()
|
||||
// ...
|
||||
// }
|
||||
//
|
||||
// Use the Dial function to authenticate connections with the AUTH command or
|
||||
// select a database with the SELECT command:
|
||||
//
|
||||
// pool := &redis.Pool{
|
||||
// // Other pool configuration not shown in this example.
|
||||
// Dial: func () (redis.Conn, error) {
|
||||
// c, err := redis.Dial("tcp", server)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// if _, err := c.Do("AUTH", password); err != nil {
|
||||
// c.Close()
|
||||
// return nil, err
|
||||
// }
|
||||
// if _, err := c.Do("SELECT", db); err != nil {
|
||||
// c.Close()
|
||||
// return nil, err
|
||||
// }
|
||||
// return c, nil
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// Use the TestOnBorrow function to check the health of an idle connection
|
||||
// before the connection is returned to the application. This example PINGs
|
||||
// connections that have been idle more than a minute:
|
||||
//
|
||||
// pool := &redis.Pool{
|
||||
// // Other pool configuration not shown in this example.
|
||||
// TestOnBorrow: func(c redis.Conn, t time.Time) error {
|
||||
// if time.Since(t) < time.Minute {
|
||||
// return nil
|
||||
// }
|
||||
// _, err := c.Do("PING")
|
||||
// return err
|
||||
// },
|
||||
// }
|
||||
//
|
||||
type Pool struct {
|
||||
// Dial is an application supplied function for creating and configuring a
|
||||
// connection.
|
||||
//
|
||||
// The connection returned from Dial must not be in a special state
|
||||
// (subscribed to pubsub channel, transaction started, ...).
|
||||
Dial func() (Conn, error)
|
||||
|
||||
// DialContext is an application supplied function for creating and configuring a
|
||||
// connection with the given context.
|
||||
//
|
||||
// The connection returned from DialContext must not be in a special state
|
||||
// (subscribed to pubsub channel, transaction started, ...).
|
||||
DialContext func(ctx context.Context) (Conn, error)
|
||||
|
||||
// TestOnBorrow is an optional application supplied function for checking
|
||||
// the health of an idle connection before the connection is used again by
|
||||
// the application. Argument lastUsed is the time when the connection was returned
|
||||
// to the pool. If the function returns an error, then the connection is
|
||||
// closed.
|
||||
TestOnBorrow func(c Conn, lastUsed time.Time) error
|
||||
|
||||
// TestOnBorrowContext is an optional application supplied function
|
||||
// for checking the health of an idle connection with the given context
|
||||
// before the connection is used again by the application.
|
||||
// Argument lastUsed is the time when the connection was returned
|
||||
// to the pool. If the function returns an error, then the connection is
|
||||
// closed.
|
||||
TestOnBorrowContext func(ctx context.Context, c Conn, lastUsed time.Time) error
|
||||
|
||||
// Maximum number of idle connections in the pool.
|
||||
MaxIdle int
|
||||
|
||||
// Maximum number of connections allocated by the pool at a given time.
|
||||
// When zero, there is no limit on the number of connections in the pool.
|
||||
MaxActive int
|
||||
|
||||
// Close connections after remaining idle for this duration. If the value
|
||||
// is zero, then idle connections are not closed. Applications should set
|
||||
// the timeout to a value less than the server's timeout.
|
||||
IdleTimeout time.Duration
|
||||
|
||||
// If Wait is true and the pool is at the MaxActive limit, then Get() waits
|
||||
// for a connection to be returned to the pool before returning.
|
||||
Wait bool
|
||||
|
||||
// Close connections older than this duration. If the value is zero, then
|
||||
// the pool does not close connections based on age.
|
||||
MaxConnLifetime time.Duration
|
||||
|
||||
mu sync.Mutex // mu protects the following fields
|
||||
closed bool // set to true when the pool is closed.
|
||||
active int // the number of open connections in the pool
|
||||
initOnce sync.Once // the init ch once func
|
||||
ch chan struct{} // limits open connections when p.Wait is true
|
||||
idle idleList // idle connections
|
||||
waitCount int64 // total number of connections waited for.
|
||||
waitDuration time.Duration // total time waited for new connections.
|
||||
}
|
||||
|
||||
// NewPool creates a new pool.
|
||||
//
|
||||
// Deprecated: Initialize the Pool directly as shown in the example.
|
||||
func NewPool(newFn func() (Conn, error), maxIdle int) *Pool {
|
||||
return &Pool{Dial: newFn, MaxIdle: maxIdle}
|
||||
}
|
||||
|
||||
// Get gets a connection. The application must close the returned connection.
|
||||
// This method always returns a valid connection so that applications can defer
|
||||
// error handling to the first use of the connection. If there is an error
|
||||
// getting an underlying connection, then the connection Err, Do, Send, Flush
|
||||
// and Receive methods return that error.
|
||||
func (p *Pool) Get() Conn {
|
||||
// GetContext returns errorConn in the first argument when an error occurs.
|
||||
c, _ := p.GetContext(context.Background())
|
||||
return c
|
||||
}
|
||||
|
||||
// GetContext gets a connection using the provided context.
|
||||
//
|
||||
// The provided Context must be non-nil. If the context expires before the
|
||||
// connection is complete, an error is returned. Any expiration on the context
|
||||
// will not affect the returned connection.
|
||||
//
|
||||
// If the function completes without error, then the application must close the
|
||||
// returned connection.
|
||||
func (p *Pool) GetContext(ctx context.Context) (Conn, error) {
|
||||
// Wait until there is a vacant connection in the pool.
|
||||
waited, err := p.waitVacantConn(ctx)
|
||||
if err != nil {
|
||||
return errorConn{err}, err
|
||||
}
|
||||
|
||||
p.mu.Lock()
|
||||
|
||||
if waited > 0 {
|
||||
p.waitCount++
|
||||
p.waitDuration += waited
|
||||
}
|
||||
|
||||
// Prune stale connections at the back of the idle list.
|
||||
if p.IdleTimeout > 0 {
|
||||
n := p.idle.count
|
||||
for i := 0; i < n && p.idle.back != nil && p.idle.back.t.Add(p.IdleTimeout).Before(nowFunc()); i++ {
|
||||
pc := p.idle.back
|
||||
p.idle.popBack()
|
||||
p.mu.Unlock()
|
||||
pc.c.Close()
|
||||
p.mu.Lock()
|
||||
p.active--
|
||||
}
|
||||
}
|
||||
|
||||
// Get idle connection from the front of idle list.
|
||||
for p.idle.front != nil {
|
||||
pc := p.idle.front
|
||||
p.idle.popFront()
|
||||
p.mu.Unlock()
|
||||
if (p.TestOnBorrow == nil || p.TestOnBorrow(pc.c, pc.t) == nil) &&
|
||||
(p.TestOnBorrowContext == nil || p.TestOnBorrowContext(ctx, pc.c, pc.t) == nil) &&
|
||||
(p.MaxConnLifetime == 0 || nowFunc().Sub(pc.created) < p.MaxConnLifetime) {
|
||||
return &activeConn{p: p, pc: pc}, nil
|
||||
}
|
||||
pc.c.Close()
|
||||
p.mu.Lock()
|
||||
p.active--
|
||||
}
|
||||
|
||||
// Check for pool closed before dialing a new connection.
|
||||
if p.closed {
|
||||
p.mu.Unlock()
|
||||
err := errors.New("redigo: get on closed pool")
|
||||
return errorConn{err}, err
|
||||
}
|
||||
|
||||
// Handle limit for p.Wait == false.
|
||||
if !p.Wait && p.MaxActive > 0 && p.active >= p.MaxActive {
|
||||
p.mu.Unlock()
|
||||
return errorConn{ErrPoolExhausted}, ErrPoolExhausted
|
||||
}
|
||||
|
||||
p.active++
|
||||
p.mu.Unlock()
|
||||
c, err := p.dial(ctx)
|
||||
if err != nil {
|
||||
p.mu.Lock()
|
||||
p.active--
|
||||
if p.ch != nil && !p.closed {
|
||||
p.ch <- struct{}{}
|
||||
}
|
||||
p.mu.Unlock()
|
||||
return errorConn{err}, err
|
||||
}
|
||||
return &activeConn{p: p, pc: &poolConn{c: c, created: nowFunc()}}, nil
|
||||
}
|
||||
|
||||
// PoolStats contains pool statistics.
|
||||
type PoolStats struct {
|
||||
// ActiveCount is the number of connections in the pool. The count includes
|
||||
// idle connections and connections in use.
|
||||
ActiveCount int
|
||||
// IdleCount is the number of idle connections in the pool.
|
||||
IdleCount int
|
||||
|
||||
// WaitCount is the total number of connections waited for.
|
||||
// This value is currently not guaranteed to be 100% accurate.
|
||||
WaitCount int64
|
||||
|
||||
// WaitDuration is the total time blocked waiting for a new connection.
|
||||
// This value is currently not guaranteed to be 100% accurate.
|
||||
WaitDuration time.Duration
|
||||
}
|
||||
|
||||
// Stats returns pool's statistics.
|
||||
func (p *Pool) Stats() PoolStats {
|
||||
p.mu.Lock()
|
||||
stats := PoolStats{
|
||||
ActiveCount: p.active,
|
||||
IdleCount: p.idle.count,
|
||||
WaitCount: p.waitCount,
|
||||
WaitDuration: p.waitDuration,
|
||||
}
|
||||
p.mu.Unlock()
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
// ActiveCount returns the number of connections in the pool. The count
|
||||
// includes idle connections and connections in use.
|
||||
func (p *Pool) ActiveCount() int {
|
||||
p.mu.Lock()
|
||||
active := p.active
|
||||
p.mu.Unlock()
|
||||
return active
|
||||
}
|
||||
|
||||
// IdleCount returns the number of idle connections in the pool.
|
||||
func (p *Pool) IdleCount() int {
|
||||
p.mu.Lock()
|
||||
idle := p.idle.count
|
||||
p.mu.Unlock()
|
||||
return idle
|
||||
}
|
||||
|
||||
// Close releases the resources used by the pool.
|
||||
func (p *Pool) Close() error {
|
||||
p.mu.Lock()
|
||||
if p.closed {
|
||||
p.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
p.closed = true
|
||||
p.active -= p.idle.count
|
||||
pc := p.idle.front
|
||||
p.idle.count = 0
|
||||
p.idle.front, p.idle.back = nil, nil
|
||||
if p.ch != nil {
|
||||
close(p.ch)
|
||||
}
|
||||
p.mu.Unlock()
|
||||
for ; pc != nil; pc = pc.next {
|
||||
pc.c.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Pool) lazyInit() {
|
||||
p.initOnce.Do(func() {
|
||||
p.ch = make(chan struct{}, p.MaxActive)
|
||||
if p.closed {
|
||||
close(p.ch)
|
||||
} else {
|
||||
for i := 0; i < p.MaxActive; i++ {
|
||||
p.ch <- struct{}{}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// waitVacantConn waits for a vacant connection in pool if waiting
|
||||
// is enabled and pool size is limited, otherwise returns instantly.
|
||||
// If ctx expires before that, an error is returned.
|
||||
//
|
||||
// If there were no vacant connection in the pool right away it returns the time spent waiting
|
||||
// for that connection to appear in the pool.
|
||||
func (p *Pool) waitVacantConn(ctx context.Context) (waited time.Duration, err error) {
|
||||
if !p.Wait || p.MaxActive <= 0 {
|
||||
// No wait or no connection limit.
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
p.lazyInit()
|
||||
|
||||
// wait indicates if we believe it will block so its not 100% accurate
|
||||
// however for stats it should be good enough.
|
||||
wait := len(p.ch) == 0
|
||||
var start time.Time
|
||||
if wait {
|
||||
start = time.Now()
|
||||
}
|
||||
|
||||
select {
|
||||
case <-p.ch:
|
||||
// Additionally check that context hasn't expired while we were waiting,
|
||||
// because `select` picks a random `case` if several of them are "ready".
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
p.ch <- struct{}{}
|
||||
return 0, ctx.Err()
|
||||
default:
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return 0, ctx.Err()
|
||||
}
|
||||
|
||||
if wait {
|
||||
return time.Since(start), nil
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (p *Pool) dial(ctx context.Context) (Conn, error) {
|
||||
if p.DialContext != nil {
|
||||
return p.DialContext(ctx)
|
||||
}
|
||||
if p.Dial != nil {
|
||||
return p.Dial()
|
||||
}
|
||||
return nil, errors.New("redigo: must pass Dial or DialContext to pool")
|
||||
}
|
||||
|
||||
func (p *Pool) put(pc *poolConn, forceClose bool) error {
|
||||
p.mu.Lock()
|
||||
if !p.closed && !forceClose {
|
||||
pc.t = nowFunc()
|
||||
p.idle.pushFront(pc)
|
||||
if p.idle.count > p.MaxIdle {
|
||||
pc = p.idle.back
|
||||
p.idle.popBack()
|
||||
} else {
|
||||
pc = nil
|
||||
}
|
||||
}
|
||||
|
||||
if pc != nil {
|
||||
p.mu.Unlock()
|
||||
pc.c.Close()
|
||||
p.mu.Lock()
|
||||
p.active--
|
||||
}
|
||||
|
||||
if p.ch != nil && !p.closed {
|
||||
p.ch <- struct{}{}
|
||||
}
|
||||
p.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
type activeConn struct {
|
||||
p *Pool
|
||||
pc *poolConn
|
||||
state int
|
||||
}
|
||||
|
||||
var (
|
||||
sentinel []byte
|
||||
sentinelOnce sync.Once
|
||||
)
|
||||
|
||||
func initSentinel() {
|
||||
p := make([]byte, 64)
|
||||
if _, err := rand.Read(p); err == nil {
|
||||
sentinel = p
|
||||
} else {
|
||||
h := sha1.New()
|
||||
io.WriteString(h, "Oops, rand failed. Use time instead.") // nolint: errcheck
|
||||
io.WriteString(h, strconv.FormatInt(time.Now().UnixNano(), 10)) // nolint: errcheck
|
||||
sentinel = h.Sum(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (ac *activeConn) firstError(errs ...error) error {
|
||||
for _, err := range errs[:len(errs)-1] {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return errs[len(errs)-1]
|
||||
}
|
||||
|
||||
func (ac *activeConn) Close() (err error) {
|
||||
pc := ac.pc
|
||||
if pc == nil {
|
||||
return nil
|
||||
}
|
||||
ac.pc = nil
|
||||
|
||||
if ac.state&connectionMultiState != 0 {
|
||||
err = pc.c.Send("DISCARD")
|
||||
ac.state &^= (connectionMultiState | connectionWatchState)
|
||||
} else if ac.state&connectionWatchState != 0 {
|
||||
err = pc.c.Send("UNWATCH")
|
||||
ac.state &^= connectionWatchState
|
||||
}
|
||||
if ac.state&connectionSubscribeState != 0 {
|
||||
err = ac.firstError(err,
|
||||
pc.c.Send("UNSUBSCRIBE"),
|
||||
pc.c.Send("PUNSUBSCRIBE"),
|
||||
)
|
||||
// To detect the end of the message stream, ask the server to echo
|
||||
// a sentinel value and read until we see that value.
|
||||
sentinelOnce.Do(initSentinel)
|
||||
err = ac.firstError(err,
|
||||
pc.c.Send("ECHO", sentinel),
|
||||
pc.c.Flush(),
|
||||
)
|
||||
for {
|
||||
p, err2 := pc.c.Receive()
|
||||
if err2 != nil {
|
||||
err = ac.firstError(err, err2)
|
||||
break
|
||||
}
|
||||
if p, ok := p.([]byte); ok && bytes.Equal(p, sentinel) {
|
||||
ac.state &^= connectionSubscribeState
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
_, err2 := pc.c.Do("")
|
||||
return ac.firstError(
|
||||
err,
|
||||
err2,
|
||||
ac.p.put(pc, ac.state != 0 || pc.c.Err() != nil),
|
||||
)
|
||||
}
|
||||
|
||||
func (ac *activeConn) Err() error {
|
||||
pc := ac.pc
|
||||
if pc == nil {
|
||||
return errConnClosed
|
||||
}
|
||||
return pc.c.Err()
|
||||
}
|
||||
|
||||
func (ac *activeConn) DoContext(ctx context.Context, commandName string, args ...interface{}) (reply interface{}, err error) {
|
||||
pc := ac.pc
|
||||
if pc == nil {
|
||||
return nil, errConnClosed
|
||||
}
|
||||
cwt, ok := pc.c.(ConnWithContext)
|
||||
if !ok {
|
||||
return nil, errContextNotSupported
|
||||
}
|
||||
ci := lookupCommandInfo(commandName)
|
||||
ac.state = (ac.state | ci.Set) &^ ci.Clear
|
||||
return cwt.DoContext(ctx, commandName, args...)
|
||||
}
|
||||
|
||||
func (ac *activeConn) Do(commandName string, args ...interface{}) (reply interface{}, err error) {
|
||||
pc := ac.pc
|
||||
if pc == nil {
|
||||
return nil, errConnClosed
|
||||
}
|
||||
ci := lookupCommandInfo(commandName)
|
||||
ac.state = (ac.state | ci.Set) &^ ci.Clear
|
||||
return pc.c.Do(commandName, args...)
|
||||
}
|
||||
|
||||
func (ac *activeConn) DoWithTimeout(timeout time.Duration, commandName string, args ...interface{}) (reply interface{}, err error) {
|
||||
pc := ac.pc
|
||||
if pc == nil {
|
||||
return nil, errConnClosed
|
||||
}
|
||||
cwt, ok := pc.c.(ConnWithTimeout)
|
||||
if !ok {
|
||||
return nil, errTimeoutNotSupported
|
||||
}
|
||||
ci := lookupCommandInfo(commandName)
|
||||
ac.state = (ac.state | ci.Set) &^ ci.Clear
|
||||
return cwt.DoWithTimeout(timeout, commandName, args...)
|
||||
}
|
||||
|
||||
func (ac *activeConn) Send(commandName string, args ...interface{}) error {
|
||||
pc := ac.pc
|
||||
if pc == nil {
|
||||
return errConnClosed
|
||||
}
|
||||
ci := lookupCommandInfo(commandName)
|
||||
ac.state = (ac.state | ci.Set) &^ ci.Clear
|
||||
return pc.c.Send(commandName, args...)
|
||||
}
|
||||
|
||||
func (ac *activeConn) Flush() error {
|
||||
pc := ac.pc
|
||||
if pc == nil {
|
||||
return errConnClosed
|
||||
}
|
||||
return pc.c.Flush()
|
||||
}
|
||||
|
||||
func (ac *activeConn) Receive() (reply interface{}, err error) {
|
||||
pc := ac.pc
|
||||
if pc == nil {
|
||||
return nil, errConnClosed
|
||||
}
|
||||
return pc.c.Receive()
|
||||
}
|
||||
|
||||
func (ac *activeConn) ReceiveContext(ctx context.Context) (reply interface{}, err error) {
|
||||
pc := ac.pc
|
||||
if pc == nil {
|
||||
return nil, errConnClosed
|
||||
}
|
||||
cwt, ok := pc.c.(ConnWithContext)
|
||||
if !ok {
|
||||
return nil, errContextNotSupported
|
||||
}
|
||||
return cwt.ReceiveContext(ctx)
|
||||
}
|
||||
|
||||
func (ac *activeConn) ReceiveWithTimeout(timeout time.Duration) (reply interface{}, err error) {
|
||||
pc := ac.pc
|
||||
if pc == nil {
|
||||
return nil, errConnClosed
|
||||
}
|
||||
cwt, ok := pc.c.(ConnWithTimeout)
|
||||
if !ok {
|
||||
return nil, errTimeoutNotSupported
|
||||
}
|
||||
return cwt.ReceiveWithTimeout(timeout)
|
||||
}
|
||||
|
||||
type errorConn struct{ err error }
|
||||
|
||||
func (ec errorConn) Do(string, ...interface{}) (interface{}, error) { return nil, ec.err }
|
||||
func (ec errorConn) DoContext(context.Context, string, ...interface{}) (interface{}, error) {
|
||||
return nil, ec.err
|
||||
}
|
||||
func (ec errorConn) DoWithTimeout(time.Duration, string, ...interface{}) (interface{}, error) {
|
||||
return nil, ec.err
|
||||
}
|
||||
func (ec errorConn) Send(string, ...interface{}) error { return ec.err }
|
||||
func (ec errorConn) Err() error { return ec.err }
|
||||
func (ec errorConn) Close() error { return nil }
|
||||
func (ec errorConn) Flush() error { return ec.err }
|
||||
func (ec errorConn) Receive() (interface{}, error) { return nil, ec.err }
|
||||
func (ec errorConn) ReceiveContext(context.Context) (interface{}, error) { return nil, ec.err }
|
||||
func (ec errorConn) ReceiveWithTimeout(time.Duration) (interface{}, error) { return nil, ec.err }
|
||||
|
||||
type idleList struct {
|
||||
count int
|
||||
front, back *poolConn
|
||||
}
|
||||
|
||||
type poolConn struct {
|
||||
c Conn
|
||||
t time.Time
|
||||
created time.Time
|
||||
next, prev *poolConn
|
||||
}
|
||||
|
||||
func (l *idleList) pushFront(pc *poolConn) {
|
||||
pc.next = l.front
|
||||
pc.prev = nil
|
||||
if l.count == 0 {
|
||||
l.back = pc
|
||||
} else {
|
||||
l.front.prev = pc
|
||||
}
|
||||
l.front = pc
|
||||
l.count++
|
||||
}
|
||||
|
||||
func (l *idleList) popFront() {
|
||||
pc := l.front
|
||||
l.count--
|
||||
if l.count == 0 {
|
||||
l.front, l.back = nil, nil
|
||||
} else {
|
||||
pc.next.prev = nil
|
||||
l.front = pc.next
|
||||
}
|
||||
pc.next, pc.prev = nil, nil
|
||||
}
|
||||
|
||||
func (l *idleList) popBack() {
|
||||
pc := l.back
|
||||
l.count--
|
||||
if l.count == 0 {
|
||||
l.front, l.back = nil, nil
|
||||
} else {
|
||||
pc.prev.next = nil
|
||||
l.back = pc.prev
|
||||
}
|
||||
pc.next, pc.prev = nil, nil
|
||||
}
|
||||
-166
@@ -1,166 +0,0 @@
|
||||
// Copyright 2012 Gary Burd
|
||||
//
|
||||
// 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.
|
||||
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Subscription represents a subscribe or unsubscribe notification.
|
||||
type Subscription struct {
|
||||
// Kind is "subscribe", "unsubscribe", "psubscribe" or "punsubscribe"
|
||||
Kind string
|
||||
|
||||
// The channel that was changed.
|
||||
Channel string
|
||||
|
||||
// The current number of subscriptions for connection.
|
||||
Count int
|
||||
}
|
||||
|
||||
// Message represents a message notification.
|
||||
type Message struct {
|
||||
// The originating channel.
|
||||
Channel string
|
||||
|
||||
// The matched pattern, if any
|
||||
Pattern string
|
||||
|
||||
// The message data.
|
||||
Data []byte
|
||||
}
|
||||
|
||||
// Pong represents a pubsub pong notification.
|
||||
type Pong struct {
|
||||
Data string
|
||||
}
|
||||
|
||||
// PubSubConn wraps a Conn with convenience methods for subscribers.
|
||||
type PubSubConn struct {
|
||||
Conn Conn
|
||||
}
|
||||
|
||||
// Close closes the connection.
|
||||
func (c PubSubConn) Close() error {
|
||||
return c.Conn.Close()
|
||||
}
|
||||
|
||||
// Subscribe subscribes the connection to the specified channels.
|
||||
func (c PubSubConn) Subscribe(channel ...interface{}) error {
|
||||
if err := c.Conn.Send("SUBSCRIBE", channel...); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Conn.Flush()
|
||||
}
|
||||
|
||||
// PSubscribe subscribes the connection to the given patterns.
|
||||
func (c PubSubConn) PSubscribe(channel ...interface{}) error {
|
||||
if err := c.Conn.Send("PSUBSCRIBE", channel...); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Conn.Flush()
|
||||
}
|
||||
|
||||
// Unsubscribe unsubscribes the connection from the given channels, or from all
|
||||
// of them if none is given.
|
||||
func (c PubSubConn) Unsubscribe(channel ...interface{}) error {
|
||||
if err := c.Conn.Send("UNSUBSCRIBE", channel...); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Conn.Flush()
|
||||
}
|
||||
|
||||
// PUnsubscribe unsubscribes the connection from the given patterns, or from all
|
||||
// of them if none is given.
|
||||
func (c PubSubConn) PUnsubscribe(channel ...interface{}) error {
|
||||
if err := c.Conn.Send("PUNSUBSCRIBE", channel...); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Conn.Flush()
|
||||
}
|
||||
|
||||
// Ping sends a PING to the server with the specified data.
|
||||
//
|
||||
// The connection must be subscribed to at least one channel or pattern when
|
||||
// calling this method.
|
||||
func (c PubSubConn) Ping(data string) error {
|
||||
if err := c.Conn.Send("PING", data); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Conn.Flush()
|
||||
}
|
||||
|
||||
// Receive returns a pushed message as a Subscription, Message, Pong or error.
|
||||
// The return value is intended to be used directly in a type switch as
|
||||
// illustrated in the PubSubConn example.
|
||||
func (c PubSubConn) Receive() interface{} {
|
||||
return c.receiveInternal(c.Conn.Receive())
|
||||
}
|
||||
|
||||
// ReceiveWithTimeout is like Receive, but it allows the application to
|
||||
// override the connection's default timeout.
|
||||
func (c PubSubConn) ReceiveWithTimeout(timeout time.Duration) interface{} {
|
||||
return c.receiveInternal(ReceiveWithTimeout(c.Conn, timeout))
|
||||
}
|
||||
|
||||
// ReceiveContext is like Receive, but it allows termination of the receive
|
||||
// via a Context. If the call returns due to closure of the context's Done
|
||||
// channel the underlying Conn will have been closed.
|
||||
func (c PubSubConn) ReceiveContext(ctx context.Context) interface{} {
|
||||
return c.receiveInternal(ReceiveContext(c.Conn, ctx))
|
||||
}
|
||||
|
||||
func (c PubSubConn) receiveInternal(replyArg interface{}, errArg error) interface{} {
|
||||
reply, err := Values(replyArg, errArg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var kind string
|
||||
reply, err = Scan(reply, &kind)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch kind {
|
||||
case "message":
|
||||
var m Message
|
||||
if _, err := Scan(reply, &m.Channel, &m.Data); err != nil {
|
||||
return err
|
||||
}
|
||||
return m
|
||||
case "pmessage":
|
||||
var m Message
|
||||
if _, err := Scan(reply, &m.Pattern, &m.Channel, &m.Data); err != nil {
|
||||
return err
|
||||
}
|
||||
return m
|
||||
case "subscribe", "psubscribe", "unsubscribe", "punsubscribe":
|
||||
s := Subscription{Kind: kind}
|
||||
if _, err := Scan(reply, &s.Channel, &s.Count); err != nil {
|
||||
return err
|
||||
}
|
||||
return s
|
||||
case "pong":
|
||||
var p Pong
|
||||
if _, err := Scan(reply, &p.Data); err != nil {
|
||||
return err
|
||||
}
|
||||
return p
|
||||
}
|
||||
return errors.New("redigo: unknown pubsub notification")
|
||||
}
|
||||
-213
@@ -1,213 +0,0 @@
|
||||
// Copyright 2012 Gary Burd
|
||||
//
|
||||
// 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.
|
||||
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Error represents an error returned in a command reply.
|
||||
type Error string
|
||||
|
||||
func (err Error) Error() string { return string(err) }
|
||||
|
||||
// Conn represents a connection to a Redis server.
|
||||
type Conn interface {
|
||||
// Close closes the connection.
|
||||
Close() error
|
||||
|
||||
// Err returns a non-nil value when the connection is not usable.
|
||||
Err() error
|
||||
|
||||
// Do sends a command to the server and returns the received reply.
|
||||
// This function will use the timeout which was set when the connection is created
|
||||
Do(commandName string, args ...interface{}) (reply interface{}, err error)
|
||||
|
||||
// Send writes the command to the client's output buffer.
|
||||
Send(commandName string, args ...interface{}) error
|
||||
|
||||
// Flush flushes the output buffer to the Redis server.
|
||||
Flush() error
|
||||
|
||||
// Receive receives a single reply from the Redis server
|
||||
Receive() (reply interface{}, err error)
|
||||
}
|
||||
|
||||
// Argument is the interface implemented by an object which wants to control how
|
||||
// the object is converted to Redis bulk strings.
|
||||
type Argument interface {
|
||||
// RedisArg returns a value to be encoded as a bulk string per the
|
||||
// conversions listed in the section 'Executing Commands'.
|
||||
// Implementations should typically return a []byte or string.
|
||||
RedisArg() interface{}
|
||||
}
|
||||
|
||||
// Scanner is implemented by an object which wants to control its value is
|
||||
// interpreted when read from Redis.
|
||||
type Scanner interface {
|
||||
// RedisScan assigns a value from a Redis value. The argument src is one of
|
||||
// the reply types listed in the section `Executing Commands`.
|
||||
//
|
||||
// An error should be returned if the value cannot be stored without
|
||||
// loss of information.
|
||||
RedisScan(src interface{}) error
|
||||
}
|
||||
|
||||
// ConnWithTimeout is an optional interface that allows the caller to override
|
||||
// a connection's default read timeout. This interface is useful for executing
|
||||
// the BLPOP, BRPOP, BRPOPLPUSH, XREAD and other commands that block at the
|
||||
// server.
|
||||
//
|
||||
// A connection's default read timeout is set with the DialReadTimeout dial
|
||||
// option. Applications should rely on the default timeout for commands that do
|
||||
// not block at the server.
|
||||
//
|
||||
// All of the Conn implementations in this package satisfy the ConnWithTimeout
|
||||
// interface.
|
||||
//
|
||||
// Use the DoWithTimeout and ReceiveWithTimeout helper functions to simplify
|
||||
// use of this interface.
|
||||
type ConnWithTimeout interface {
|
||||
Conn
|
||||
|
||||
// DoWithTimeout sends a command to the server and returns the received reply.
|
||||
// The timeout overrides the readtimeout set when dialing the connection.
|
||||
DoWithTimeout(timeout time.Duration, commandName string, args ...interface{}) (reply interface{}, err error)
|
||||
|
||||
// ReceiveWithTimeout receives a single reply from the Redis server.
|
||||
// The timeout overrides the readtimeout set when dialing the connection.
|
||||
ReceiveWithTimeout(timeout time.Duration) (reply interface{}, err error)
|
||||
}
|
||||
|
||||
// ConnWithContext is an optional interface that allows the caller to control the command's life with context.
|
||||
type ConnWithContext interface {
|
||||
Conn
|
||||
|
||||
// DoContext sends a command to server and returns the received reply.
|
||||
// min(ctx,DialReadTimeout()) will be used as the deadline.
|
||||
// The connection will be closed if DialReadTimeout() timeout or ctx timeout or ctx canceled when this function is running.
|
||||
// DialReadTimeout() timeout return err can be checked by errors.Is(err, os.ErrDeadlineExceeded).
|
||||
// ctx timeout return err context.DeadlineExceeded.
|
||||
// ctx canceled return err context.Canceled.
|
||||
DoContext(ctx context.Context, commandName string, args ...interface{}) (reply interface{}, err error)
|
||||
|
||||
// ReceiveContext receives a single reply from the Redis server.
|
||||
// min(ctx,DialReadTimeout()) will be used as the deadline.
|
||||
// The connection will be closed if DialReadTimeout() timeout or ctx timeout or ctx canceled when this function is running.
|
||||
// DialReadTimeout() timeout return err can be checked by errors.Is(err, os.ErrDeadlineExceeded).
|
||||
// ctx timeout return err context.DeadlineExceeded.
|
||||
// ctx canceled return err context.Canceled.
|
||||
ReceiveContext(ctx context.Context) (reply interface{}, err error)
|
||||
}
|
||||
|
||||
var errTimeoutNotSupported = errors.New("redis: connection does not support ConnWithTimeout")
|
||||
var errContextNotSupported = errors.New("redis: connection does not support ConnWithContext")
|
||||
|
||||
// DoContext sends a command to server and returns the received reply.
|
||||
// min(ctx,DialReadTimeout()) will be used as the deadline.
|
||||
// The connection will be closed if DialReadTimeout() timeout or ctx timeout or ctx canceled when this function is running.
|
||||
// DialReadTimeout() timeout return err can be checked by errors.Is(err, os.ErrDeadlineExceeded).
|
||||
// ctx timeout return err context.DeadlineExceeded.
|
||||
// ctx canceled return err context.Canceled.
|
||||
func DoContext(c Conn, ctx context.Context, cmd string, args ...interface{}) (interface{}, error) {
|
||||
cwt, ok := c.(ConnWithContext)
|
||||
if !ok {
|
||||
return nil, errContextNotSupported
|
||||
}
|
||||
return cwt.DoContext(ctx, cmd, args...)
|
||||
}
|
||||
|
||||
// DoWithTimeout executes a Redis command with the specified read timeout. If
|
||||
// the connection does not satisfy the ConnWithTimeout interface, then an error
|
||||
// is returned.
|
||||
func DoWithTimeout(c Conn, timeout time.Duration, cmd string, args ...interface{}) (interface{}, error) {
|
||||
cwt, ok := c.(ConnWithTimeout)
|
||||
if !ok {
|
||||
return nil, errTimeoutNotSupported
|
||||
}
|
||||
return cwt.DoWithTimeout(timeout, cmd, args...)
|
||||
}
|
||||
|
||||
// ReceiveContext receives a single reply from the Redis server.
|
||||
// min(ctx,DialReadTimeout()) will be used as the deadline.
|
||||
// The connection will be closed if DialReadTimeout() timeout or ctx timeout or ctx canceled when this function is running.
|
||||
// DialReadTimeout() timeout return err can be checked by strings.Contains(e.Error(), "io/timeout").
|
||||
// ctx timeout return err context.DeadlineExceeded.
|
||||
// ctx canceled return err context.Canceled.
|
||||
func ReceiveContext(c Conn, ctx context.Context) (interface{}, error) {
|
||||
cwt, ok := c.(ConnWithContext)
|
||||
if !ok {
|
||||
return nil, errContextNotSupported
|
||||
}
|
||||
return cwt.ReceiveContext(ctx)
|
||||
}
|
||||
|
||||
// ReceiveWithTimeout receives a reply with the specified read timeout. If the
|
||||
// connection does not satisfy the ConnWithTimeout interface, then an error is
|
||||
// returned.
|
||||
func ReceiveWithTimeout(c Conn, timeout time.Duration) (interface{}, error) {
|
||||
cwt, ok := c.(ConnWithTimeout)
|
||||
if !ok {
|
||||
return nil, errTimeoutNotSupported
|
||||
}
|
||||
return cwt.ReceiveWithTimeout(timeout)
|
||||
}
|
||||
|
||||
// SlowLog represents a redis SlowLog
|
||||
type SlowLog struct {
|
||||
// ID is a unique progressive identifier for every slow log entry.
|
||||
ID int64
|
||||
|
||||
// Time is the unix timestamp at which the logged command was processed.
|
||||
Time time.Time
|
||||
|
||||
// ExecutationTime is the amount of time needed for the command execution.
|
||||
ExecutionTime time.Duration
|
||||
|
||||
// Args is the command name and arguments
|
||||
Args []string
|
||||
|
||||
// ClientAddr is the client IP address (4.0 only).
|
||||
ClientAddr string
|
||||
|
||||
// ClientName is the name set via the CLIENT SETNAME command (4.0 only).
|
||||
ClientName string
|
||||
}
|
||||
|
||||
// Latency represents a redis LATENCY LATEST.
|
||||
type Latency struct {
|
||||
// Name of the latest latency spike event.
|
||||
Name string
|
||||
|
||||
// Time of the latest latency spike for the event.
|
||||
Time time.Time
|
||||
|
||||
// Latest is the latest recorded latency for the named event.
|
||||
Latest time.Duration
|
||||
|
||||
// Max is the maximum latency for the named event.
|
||||
Max time.Duration
|
||||
}
|
||||
|
||||
// LatencyHistory represents a redis LATENCY HISTORY.
|
||||
type LatencyHistory struct {
|
||||
// Time is the unix timestamp at which the event was processed.
|
||||
Time time.Time
|
||||
|
||||
// ExecutationTime is the amount of time needed for the command execution.
|
||||
ExecutionTime time.Duration
|
||||
}
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// methodName returns the name of the calling method,
|
||||
// assumed to be two stack frames above.
|
||||
func methodName() string {
|
||||
pc, _, _, _ := runtime.Caller(2)
|
||||
f := runtime.FuncForPC(pc)
|
||||
if f == nil {
|
||||
return "unknown method"
|
||||
}
|
||||
return f.Name()
|
||||
}
|
||||
|
||||
// mustBe panics if f's kind is not expected.
|
||||
func mustBe(v reflect.Value, expected reflect.Kind) {
|
||||
if v.Kind() != expected {
|
||||
panic(&reflect.ValueError{Method: methodName(), Kind: v.Kind()})
|
||||
}
|
||||
}
|
||||
|
||||
// fieldByIndexCreate returns the nested field corresponding
|
||||
// to index creating elements that are nil when stepping through.
|
||||
// It panics if v is not a struct.
|
||||
func fieldByIndexCreate(v reflect.Value, index []int) reflect.Value {
|
||||
if len(index) == 1 {
|
||||
return v.Field(index[0])
|
||||
}
|
||||
|
||||
mustBe(v, reflect.Struct)
|
||||
for i, x := range index {
|
||||
if i > 0 {
|
||||
if v.Kind() == reflect.Ptr && v.Type().Elem().Kind() == reflect.Struct {
|
||||
if v.IsNil() {
|
||||
v.Set(reflect.New(v.Type().Elem()))
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
}
|
||||
v = v.Field(x)
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
//go:build !go1.18
|
||||
// +build !go1.18
|
||||
|
||||
package redis
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// fieldByIndexErr returns the nested field corresponding to index.
|
||||
// It returns an error if evaluation requires stepping through a nil
|
||||
// pointer, but panics if it must step through a field that
|
||||
// is not a struct.
|
||||
func fieldByIndexErr(v reflect.Value, index []int) (reflect.Value, error) {
|
||||
if len(index) == 1 {
|
||||
return v.Field(index[0]), nil
|
||||
}
|
||||
|
||||
mustBe(v, reflect.Struct)
|
||||
for i, x := range index {
|
||||
if i > 0 {
|
||||
if v.Kind() == reflect.Ptr && v.Type().Elem().Kind() == reflect.Struct {
|
||||
if v.IsNil() {
|
||||
return reflect.Value{}, errors.New("reflect: indirection through nil pointer to embedded struct field " + v.Type().Elem().Name())
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
}
|
||||
v = v.Field(x)
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
//go:build go1.18
|
||||
// +build go1.18
|
||||
|
||||
package redis
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// fieldByIndexErr returns the nested field corresponding to index.
|
||||
// It returns an error if evaluation requires stepping through a nil
|
||||
// pointer, but panics if it must step through a field that
|
||||
// is not a struct.
|
||||
func fieldByIndexErr(v reflect.Value, index []int) (reflect.Value, error) {
|
||||
return v.FieldByIndexErr(index)
|
||||
}
|
||||
-735
@@ -1,735 +0,0 @@
|
||||
// Copyright 2012 Gary Burd
|
||||
//
|
||||
// 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.
|
||||
|
||||
package redis
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ErrNil indicates that a reply value is nil.
|
||||
var ErrNil = errors.New("redigo: nil returned")
|
||||
|
||||
// Int is a helper that converts a command reply to an integer. If err is not
|
||||
// equal to nil, then Int returns 0, err. Otherwise, Int converts the
|
||||
// reply to an int as follows:
|
||||
//
|
||||
// Reply type Result
|
||||
// integer int(reply), nil
|
||||
// bulk string parsed reply, nil
|
||||
// nil 0, ErrNil
|
||||
// other 0, error
|
||||
func Int(reply interface{}, err error) (int, error) {
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
switch reply := reply.(type) {
|
||||
case int64:
|
||||
x := int(reply)
|
||||
if int64(x) != reply {
|
||||
return 0, strconv.ErrRange
|
||||
}
|
||||
return x, nil
|
||||
case []byte:
|
||||
n, err := strconv.ParseInt(string(reply), 10, 0)
|
||||
return int(n), err
|
||||
case nil:
|
||||
return 0, ErrNil
|
||||
case Error:
|
||||
return 0, reply
|
||||
}
|
||||
return 0, fmt.Errorf("redigo: unexpected type for Int, got type %T", reply)
|
||||
}
|
||||
|
||||
// Int64 is a helper that converts a command reply to 64 bit integer. If err is
|
||||
// not equal to nil, then Int64 returns 0, err. Otherwise, Int64 converts the
|
||||
// reply to an int64 as follows:
|
||||
//
|
||||
// Reply type Result
|
||||
// integer reply, nil
|
||||
// bulk string parsed reply, nil
|
||||
// nil 0, ErrNil
|
||||
// other 0, error
|
||||
func Int64(reply interface{}, err error) (int64, error) {
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
switch reply := reply.(type) {
|
||||
case int64:
|
||||
return reply, nil
|
||||
case []byte:
|
||||
n, err := strconv.ParseInt(string(reply), 10, 64)
|
||||
return n, err
|
||||
case nil:
|
||||
return 0, ErrNil
|
||||
case Error:
|
||||
return 0, reply
|
||||
}
|
||||
return 0, fmt.Errorf("redigo: unexpected type for Int64, got type %T", reply)
|
||||
}
|
||||
|
||||
func errNegativeInt(v int64) error {
|
||||
return fmt.Errorf("redigo: unexpected negative value %v for Uint64", v)
|
||||
}
|
||||
|
||||
// Uint64 is a helper that converts a command reply to 64 bit unsigned integer.
|
||||
// If err is not equal to nil, then Uint64 returns 0, err. Otherwise, Uint64 converts the
|
||||
// reply to an uint64 as follows:
|
||||
//
|
||||
// Reply type Result
|
||||
// +integer reply, nil
|
||||
// bulk string parsed reply, nil
|
||||
// nil 0, ErrNil
|
||||
// other 0, error
|
||||
func Uint64(reply interface{}, err error) (uint64, error) {
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
switch reply := reply.(type) {
|
||||
case int64:
|
||||
if reply < 0 {
|
||||
return 0, errNegativeInt(reply)
|
||||
}
|
||||
return uint64(reply), nil
|
||||
case []byte:
|
||||
n, err := strconv.ParseUint(string(reply), 10, 64)
|
||||
return n, err
|
||||
case nil:
|
||||
return 0, ErrNil
|
||||
case Error:
|
||||
return 0, reply
|
||||
}
|
||||
return 0, fmt.Errorf("redigo: unexpected type for Uint64, got type %T", reply)
|
||||
}
|
||||
|
||||
// Float64 is a helper that converts a command reply to 64 bit float. If err is
|
||||
// not equal to nil, then Float64 returns 0, err. Otherwise, Float64 converts
|
||||
// the reply to a float64 as follows:
|
||||
//
|
||||
// Reply type Result
|
||||
// bulk string parsed reply, nil
|
||||
// nil 0, ErrNil
|
||||
// other 0, error
|
||||
func Float64(reply interface{}, err error) (float64, error) {
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
switch reply := reply.(type) {
|
||||
case []byte:
|
||||
n, err := strconv.ParseFloat(string(reply), 64)
|
||||
return n, err
|
||||
case nil:
|
||||
return 0, ErrNil
|
||||
case Error:
|
||||
return 0, reply
|
||||
}
|
||||
return 0, fmt.Errorf("redigo: unexpected type for Float64, got type %T", reply)
|
||||
}
|
||||
|
||||
// String is a helper that converts a command reply to a string. If err is not
|
||||
// equal to nil, then String returns "", err. Otherwise String converts the
|
||||
// reply to a string as follows:
|
||||
//
|
||||
// Reply type Result
|
||||
// bulk string string(reply), nil
|
||||
// simple string reply, nil
|
||||
// nil "", ErrNil
|
||||
// other "", error
|
||||
func String(reply interface{}, err error) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
switch reply := reply.(type) {
|
||||
case []byte:
|
||||
return string(reply), nil
|
||||
case string:
|
||||
return reply, nil
|
||||
case nil:
|
||||
return "", ErrNil
|
||||
case Error:
|
||||
return "", reply
|
||||
}
|
||||
return "", fmt.Errorf("redigo: unexpected type for String, got type %T", reply)
|
||||
}
|
||||
|
||||
// Bytes is a helper that converts a command reply to a slice of bytes. If err
|
||||
// is not equal to nil, then Bytes returns nil, err. Otherwise Bytes converts
|
||||
// the reply to a slice of bytes as follows:
|
||||
//
|
||||
// Reply type Result
|
||||
// bulk string reply, nil
|
||||
// simple string []byte(reply), nil
|
||||
// nil nil, ErrNil
|
||||
// other nil, error
|
||||
func Bytes(reply interface{}, err error) ([]byte, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch reply := reply.(type) {
|
||||
case []byte:
|
||||
return reply, nil
|
||||
case string:
|
||||
return []byte(reply), nil
|
||||
case nil:
|
||||
return nil, ErrNil
|
||||
case Error:
|
||||
return nil, reply
|
||||
}
|
||||
return nil, fmt.Errorf("redigo: unexpected type for Bytes, got type %T", reply)
|
||||
}
|
||||
|
||||
// Bool is a helper that converts a command reply to a boolean. If err is not
|
||||
// equal to nil, then Bool returns false, err. Otherwise Bool converts the
|
||||
// reply to boolean as follows:
|
||||
//
|
||||
// Reply type Result
|
||||
// integer value != 0, nil
|
||||
// bulk string strconv.ParseBool(reply)
|
||||
// nil false, ErrNil
|
||||
// other false, error
|
||||
func Bool(reply interface{}, err error) (bool, error) {
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
switch reply := reply.(type) {
|
||||
case int64:
|
||||
return reply != 0, nil
|
||||
case []byte:
|
||||
return strconv.ParseBool(string(reply))
|
||||
case nil:
|
||||
return false, ErrNil
|
||||
case Error:
|
||||
return false, reply
|
||||
}
|
||||
return false, fmt.Errorf("redigo: unexpected type for Bool, got type %T", reply)
|
||||
}
|
||||
|
||||
// MultiBulk is a helper that converts an array command reply to a []interface{}.
|
||||
//
|
||||
// Deprecated: Use Values instead.
|
||||
func MultiBulk(reply interface{}, err error) ([]interface{}, error) { return Values(reply, err) }
|
||||
|
||||
// Values is a helper that converts an array command reply to a []interface{}.
|
||||
// If err is not equal to nil, then Values returns nil, err. Otherwise, Values
|
||||
// converts the reply as follows:
|
||||
//
|
||||
// Reply type Result
|
||||
// array reply, nil
|
||||
// nil nil, ErrNil
|
||||
// other nil, error
|
||||
func Values(reply interface{}, err error) ([]interface{}, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch reply := reply.(type) {
|
||||
case []interface{}:
|
||||
return reply, nil
|
||||
case nil:
|
||||
return nil, ErrNil
|
||||
case Error:
|
||||
return nil, reply
|
||||
}
|
||||
return nil, fmt.Errorf("redigo: unexpected type for Values, got type %T", reply)
|
||||
}
|
||||
|
||||
func sliceHelper(reply interface{}, err error, name string, makeSlice func(int), assign func(int, interface{}) error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch reply := reply.(type) {
|
||||
case []interface{}:
|
||||
makeSlice(len(reply))
|
||||
for i := range reply {
|
||||
if reply[i] == nil {
|
||||
continue
|
||||
}
|
||||
if err := assign(i, reply[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
case nil:
|
||||
return ErrNil
|
||||
case Error:
|
||||
return reply
|
||||
}
|
||||
return fmt.Errorf("redigo: unexpected type for %s, got type %T", name, reply)
|
||||
}
|
||||
|
||||
// Float64s is a helper that converts an array command reply to a []float64. If
|
||||
// err is not equal to nil, then Float64s returns nil, err. Nil array items are
|
||||
// converted to 0 in the output slice. Floats64 returns an error if an array
|
||||
// item is not a bulk string or nil.
|
||||
func Float64s(reply interface{}, err error) ([]float64, error) {
|
||||
var result []float64
|
||||
err = sliceHelper(reply, err, "Float64s", func(n int) { result = make([]float64, n) }, func(i int, v interface{}) error {
|
||||
switch v := v.(type) {
|
||||
case []byte:
|
||||
f, err := strconv.ParseFloat(string(v), 64)
|
||||
result[i] = f
|
||||
return err
|
||||
case Error:
|
||||
return v
|
||||
default:
|
||||
return fmt.Errorf("redigo: unexpected element type for Float64s, got type %T", v)
|
||||
}
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Strings is a helper that converts an array command reply to a []string. If
|
||||
// err is not equal to nil, then Strings returns nil, err. Nil array items are
|
||||
// converted to "" in the output slice. Strings returns an error if an array
|
||||
// item is not a bulk string or nil.
|
||||
func Strings(reply interface{}, err error) ([]string, error) {
|
||||
var result []string
|
||||
err = sliceHelper(reply, err, "Strings", func(n int) { result = make([]string, n) }, func(i int, v interface{}) error {
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
result[i] = v
|
||||
return nil
|
||||
case []byte:
|
||||
result[i] = string(v)
|
||||
return nil
|
||||
case Error:
|
||||
return v
|
||||
default:
|
||||
return fmt.Errorf("redigo: unexpected element type for Strings, got type %T", v)
|
||||
}
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
// ByteSlices is a helper that converts an array command reply to a [][]byte.
|
||||
// If err is not equal to nil, then ByteSlices returns nil, err. Nil array
|
||||
// items are stay nil. ByteSlices returns an error if an array item is not a
|
||||
// bulk string or nil.
|
||||
func ByteSlices(reply interface{}, err error) ([][]byte, error) {
|
||||
var result [][]byte
|
||||
err = sliceHelper(reply, err, "ByteSlices", func(n int) { result = make([][]byte, n) }, func(i int, v interface{}) error {
|
||||
switch v := v.(type) {
|
||||
case []byte:
|
||||
result[i] = v
|
||||
return nil
|
||||
case Error:
|
||||
return v
|
||||
default:
|
||||
return fmt.Errorf("redigo: unexpected element type for ByteSlices, got type %T", v)
|
||||
}
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Int64s is a helper that converts an array command reply to a []int64.
|
||||
// If err is not equal to nil, then Int64s returns nil, err. Nil array
|
||||
// items are stay nil. Int64s returns an error if an array item is not a
|
||||
// bulk string or nil.
|
||||
func Int64s(reply interface{}, err error) ([]int64, error) {
|
||||
var result []int64
|
||||
err = sliceHelper(reply, err, "Int64s", func(n int) { result = make([]int64, n) }, func(i int, v interface{}) error {
|
||||
switch v := v.(type) {
|
||||
case int64:
|
||||
result[i] = v
|
||||
return nil
|
||||
case []byte:
|
||||
n, err := strconv.ParseInt(string(v), 10, 64)
|
||||
result[i] = n
|
||||
return err
|
||||
case Error:
|
||||
return v
|
||||
default:
|
||||
return fmt.Errorf("redigo: unexpected element type for Int64s, got type %T", v)
|
||||
}
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Ints is a helper that converts an array command reply to a []int.
|
||||
// If err is not equal to nil, then Ints returns nil, err. Nil array
|
||||
// items are stay nil. Ints returns an error if an array item is not a
|
||||
// bulk string or nil.
|
||||
func Ints(reply interface{}, err error) ([]int, error) {
|
||||
var result []int
|
||||
err = sliceHelper(reply, err, "Ints", func(n int) { result = make([]int, n) }, func(i int, v interface{}) error {
|
||||
switch v := v.(type) {
|
||||
case int64:
|
||||
n := int(v)
|
||||
if int64(n) != v {
|
||||
return strconv.ErrRange
|
||||
}
|
||||
result[i] = n
|
||||
return nil
|
||||
case []byte:
|
||||
n, err := strconv.Atoi(string(v))
|
||||
result[i] = n
|
||||
return err
|
||||
case Error:
|
||||
return v
|
||||
default:
|
||||
return fmt.Errorf("redigo: unexpected element type for Ints, got type %T", v)
|
||||
}
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
// mapHelper builds a map from the data in reply.
|
||||
func mapHelper(reply interface{}, err error, name string, makeMap func(int), assign func(key string, value interface{}) error) error {
|
||||
values, err := Values(reply, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(values)%2 != 0 {
|
||||
return fmt.Errorf("redigo: %s expects even number of values result, got %d", name, len(values))
|
||||
}
|
||||
|
||||
makeMap(len(values) / 2)
|
||||
for i := 0; i < len(values); i += 2 {
|
||||
key, ok := values[i].([]byte)
|
||||
if !ok {
|
||||
return fmt.Errorf("redigo: %s key[%d] not a bulk string value, got %T", name, i, values[i])
|
||||
}
|
||||
|
||||
if err := assign(string(key), values[i+1]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StringMap is a helper that converts an array of strings (alternating key, value)
|
||||
// into a map[string]string. The HGETALL and CONFIG GET commands return replies in this format.
|
||||
// Requires an even number of values in result.
|
||||
func StringMap(reply interface{}, err error) (map[string]string, error) {
|
||||
var result map[string]string
|
||||
err = mapHelper(reply, err, "StringMap",
|
||||
func(n int) {
|
||||
result = make(map[string]string, n)
|
||||
}, func(key string, v interface{}) error {
|
||||
value, ok := v.([]byte)
|
||||
if !ok {
|
||||
return fmt.Errorf("redigo: StringMap for %q not a bulk string value, got %T", key, v)
|
||||
}
|
||||
|
||||
result[key] = string(value)
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
// IntMap is a helper that converts an array of strings (alternating key, value)
|
||||
// into a map[string]int. The HGETALL commands return replies in this format.
|
||||
// Requires an even number of values in result.
|
||||
func IntMap(result interface{}, err error) (map[string]int, error) {
|
||||
var m map[string]int
|
||||
err = mapHelper(result, err, "IntMap",
|
||||
func(n int) {
|
||||
m = make(map[string]int, n)
|
||||
}, func(key string, v interface{}) error {
|
||||
value, err := Int(v, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m[key] = value
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
return m, err
|
||||
}
|
||||
|
||||
// Int64Map is a helper that converts an array of strings (alternating key, value)
|
||||
// into a map[string]int64. The HGETALL commands return replies in this format.
|
||||
// Requires an even number of values in result.
|
||||
func Int64Map(result interface{}, err error) (map[string]int64, error) {
|
||||
var m map[string]int64
|
||||
err = mapHelper(result, err, "Int64Map",
|
||||
func(n int) {
|
||||
m = make(map[string]int64, n)
|
||||
}, func(key string, v interface{}) error {
|
||||
value, err := Int64(v, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m[key] = value
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
return m, err
|
||||
}
|
||||
|
||||
// Float64Map is a helper that converts an array of strings (alternating key, value)
|
||||
// into a map[string]float64. The HGETALL commands return replies in this format.
|
||||
// Requires an even number of values in result.
|
||||
func Float64Map(result interface{}, err error) (map[string]float64, error) {
|
||||
var m map[string]float64
|
||||
err = mapHelper(result, err, "Float64Map",
|
||||
func(n int) {
|
||||
m = make(map[string]float64, n)
|
||||
}, func(key string, v interface{}) error {
|
||||
value, err := Float64(v, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m[key] = value
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
return m, err
|
||||
}
|
||||
|
||||
// Positions is a helper that converts an array of positions (lat, long)
|
||||
// into a [][2]float64. The GEOPOS command returns replies in this format.
|
||||
func Positions(result interface{}, err error) ([]*[2]float64, error) {
|
||||
values, err := Values(result, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
positions := make([]*[2]float64, len(values))
|
||||
for i := range values {
|
||||
if values[i] == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
p, ok := values[i].([]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("redigo: unexpected element type for interface slice, got type %T", values[i])
|
||||
}
|
||||
|
||||
if len(p) != 2 {
|
||||
return nil, fmt.Errorf("redigo: unexpected number of values for a member position, got %d", len(p))
|
||||
}
|
||||
|
||||
lat, err := Float64(p[0], nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
long, err := Float64(p[1], nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
positions[i] = &[2]float64{lat, long}
|
||||
}
|
||||
return positions, nil
|
||||
}
|
||||
|
||||
// Uint64s is a helper that converts an array command reply to a []uint64.
|
||||
// If err is not equal to nil, then Uint64s returns nil, err. Nil array
|
||||
// items are stay nil. Uint64s returns an error if an array item is not a
|
||||
// bulk string or nil.
|
||||
func Uint64s(reply interface{}, err error) ([]uint64, error) {
|
||||
var result []uint64
|
||||
err = sliceHelper(reply, err, "Uint64s", func(n int) { result = make([]uint64, n) }, func(i int, v interface{}) error {
|
||||
switch v := v.(type) {
|
||||
case uint64:
|
||||
result[i] = v
|
||||
return nil
|
||||
case []byte:
|
||||
n, err := strconv.ParseUint(string(v), 10, 64)
|
||||
result[i] = n
|
||||
return err
|
||||
case Error:
|
||||
return v
|
||||
default:
|
||||
return fmt.Errorf("redigo: unexpected element type for Uint64s, got type %T", v)
|
||||
}
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Uint64Map is a helper that converts an array of strings (alternating key, value)
|
||||
// into a map[string]uint64. The HGETALL commands return replies in this format.
|
||||
// Requires an even number of values in result.
|
||||
func Uint64Map(result interface{}, err error) (map[string]uint64, error) {
|
||||
var m map[string]uint64
|
||||
err = mapHelper(result, err, "Uint64Map",
|
||||
func(n int) {
|
||||
m = make(map[string]uint64, n)
|
||||
}, func(key string, v interface{}) error {
|
||||
value, err := Uint64(v, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m[key] = value
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
return m, err
|
||||
}
|
||||
|
||||
// SlowLogs is a helper that parse the SLOWLOG GET command output and
|
||||
// return the array of SlowLog
|
||||
func SlowLogs(result interface{}, err error) ([]SlowLog, error) {
|
||||
rawLogs, err := Values(result, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logs := make([]SlowLog, len(rawLogs))
|
||||
for i, e := range rawLogs {
|
||||
rawLog, ok := e.([]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("redigo: slowlog element is not an array, got %T", e)
|
||||
}
|
||||
|
||||
var log SlowLog
|
||||
if len(rawLog) < 4 {
|
||||
return nil, fmt.Errorf("redigo: slowlog element has %d elements, expected at least 4", len(rawLog))
|
||||
}
|
||||
|
||||
log.ID, ok = rawLog[0].(int64)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("redigo: slowlog element[0] not an int64, got %T", rawLog[0])
|
||||
}
|
||||
|
||||
timestamp, ok := rawLog[1].(int64)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("redigo: slowlog element[1] not an int64, got %T", rawLog[1])
|
||||
}
|
||||
|
||||
log.Time = time.Unix(timestamp, 0)
|
||||
duration, ok := rawLog[2].(int64)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("redigo: slowlog element[2] not an int64, got %T", rawLog[2])
|
||||
}
|
||||
|
||||
log.ExecutionTime = time.Duration(duration) * time.Microsecond
|
||||
|
||||
log.Args, err = Strings(rawLog[3], nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("redigo: slowlog element[3] is not array of strings: %w", err)
|
||||
}
|
||||
|
||||
if len(rawLog) >= 6 {
|
||||
log.ClientAddr, err = String(rawLog[4], nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("redigo: slowlog element[4] is not a string: %w", err)
|
||||
}
|
||||
|
||||
log.ClientName, err = String(rawLog[5], nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("redigo: slowlog element[5] is not a string: %w", err)
|
||||
}
|
||||
}
|
||||
logs[i] = log
|
||||
}
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
// Latencies is a helper that parses the LATENCY LATEST command output and
|
||||
// return the slice of Latency values.
|
||||
func Latencies(result interface{}, err error) ([]Latency, error) {
|
||||
rawLatencies, err := Values(result, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
latencies := make([]Latency, len(rawLatencies))
|
||||
for i, e := range rawLatencies {
|
||||
rawLatency, ok := e.([]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("redigo: latencies element is not slice, got %T", e)
|
||||
}
|
||||
|
||||
var event Latency
|
||||
if len(rawLatency) != 4 {
|
||||
return nil, fmt.Errorf("redigo: latencies element has %d elements, expected 4", len(rawLatency))
|
||||
}
|
||||
|
||||
event.Name, err = String(rawLatency[0], nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("redigo: latencies element[0] is not a string: %w", err)
|
||||
}
|
||||
|
||||
timestamp, ok := rawLatency[1].(int64)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("redigo: latencies element[1] not an int64, got %T", rawLatency[1])
|
||||
}
|
||||
|
||||
event.Time = time.Unix(timestamp, 0)
|
||||
|
||||
latestDuration, ok := rawLatency[2].(int64)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("redigo: latencies element[2] not an int64, got %T", rawLatency[2])
|
||||
}
|
||||
|
||||
event.Latest = time.Duration(latestDuration) * time.Millisecond
|
||||
|
||||
maxDuration, ok := rawLatency[3].(int64)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("redigo: latencies element[3] not an int64, got %T", rawLatency[3])
|
||||
}
|
||||
|
||||
event.Max = time.Duration(maxDuration) * time.Millisecond
|
||||
|
||||
latencies[i] = event
|
||||
}
|
||||
|
||||
return latencies, nil
|
||||
}
|
||||
|
||||
// LatencyHistories is a helper that parse the LATENCY HISTORY command output and
|
||||
// returns a LatencyHistory slice.
|
||||
func LatencyHistories(result interface{}, err error) ([]LatencyHistory, error) {
|
||||
rawLogs, err := Values(result, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
latencyHistories := make([]LatencyHistory, len(rawLogs))
|
||||
for i, e := range rawLogs {
|
||||
rawLog, ok := e.([]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("redigo: latency history element is not an slice, got %T", e)
|
||||
}
|
||||
|
||||
var event LatencyHistory
|
||||
timestamp, ok := rawLog[0].(int64)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("redigo: latency history element[0] not an int64, got %T", rawLog[0])
|
||||
}
|
||||
|
||||
event.Time = time.Unix(timestamp, 0)
|
||||
|
||||
duration, ok := rawLog[1].(int64)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("redigo: latency history element[1] not an int64, got %T", rawLog[1])
|
||||
}
|
||||
|
||||
event.ExecutionTime = time.Duration(duration) * time.Millisecond
|
||||
|
||||
latencyHistories[i] = event
|
||||
}
|
||||
|
||||
return latencyHistories, nil
|
||||
}
|
||||
-729
@@ -1,729 +0,0 @@
|
||||
// Copyright 2012 Gary Burd
|
||||
//
|
||||
// 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.
|
||||
|
||||
package redis
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
scannerType = reflect.TypeOf((*Scanner)(nil)).Elem()
|
||||
)
|
||||
|
||||
func ensureLen(d reflect.Value, n int) {
|
||||
if n > d.Cap() {
|
||||
d.Set(reflect.MakeSlice(d.Type(), n, n))
|
||||
} else {
|
||||
d.SetLen(n)
|
||||
}
|
||||
}
|
||||
|
||||
func cannotConvert(d reflect.Value, s interface{}) error {
|
||||
var sname string
|
||||
switch s.(type) {
|
||||
case string:
|
||||
sname = "Redis simple string"
|
||||
case Error:
|
||||
sname = "Redis error"
|
||||
case int64:
|
||||
sname = "Redis integer"
|
||||
case []byte:
|
||||
sname = "Redis bulk string"
|
||||
case []interface{}:
|
||||
sname = "Redis array"
|
||||
case nil:
|
||||
sname = "Redis nil"
|
||||
default:
|
||||
sname = reflect.TypeOf(s).String()
|
||||
}
|
||||
return fmt.Errorf("cannot convert from %s to %s", sname, d.Type())
|
||||
}
|
||||
|
||||
func convertAssignNil(d reflect.Value) (err error) {
|
||||
switch d.Type().Kind() {
|
||||
case reflect.Slice, reflect.Interface:
|
||||
d.Set(reflect.Zero(d.Type()))
|
||||
default:
|
||||
err = cannotConvert(d, nil)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func convertAssignError(d reflect.Value, s Error) (err error) {
|
||||
if d.Kind() == reflect.String {
|
||||
d.SetString(string(s))
|
||||
} else if d.Kind() == reflect.Slice && d.Type().Elem().Kind() == reflect.Uint8 {
|
||||
d.SetBytes([]byte(s))
|
||||
} else {
|
||||
err = cannotConvert(d, s)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func convertAssignString(d reflect.Value, s string) (err error) {
|
||||
switch d.Type().Kind() {
|
||||
case reflect.Float32, reflect.Float64:
|
||||
var x float64
|
||||
x, err = strconv.ParseFloat(s, d.Type().Bits())
|
||||
d.SetFloat(x)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
var x int64
|
||||
x, err = strconv.ParseInt(s, 10, d.Type().Bits())
|
||||
d.SetInt(x)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
var x uint64
|
||||
x, err = strconv.ParseUint(s, 10, d.Type().Bits())
|
||||
d.SetUint(x)
|
||||
case reflect.Bool:
|
||||
var x bool
|
||||
x, err = strconv.ParseBool(s)
|
||||
d.SetBool(x)
|
||||
case reflect.String:
|
||||
d.SetString(s)
|
||||
case reflect.Slice:
|
||||
if d.Type().Elem().Kind() == reflect.Uint8 {
|
||||
d.SetBytes([]byte(s))
|
||||
} else {
|
||||
err = cannotConvert(d, s)
|
||||
}
|
||||
case reflect.Ptr:
|
||||
err = convertAssignString(d.Elem(), s)
|
||||
default:
|
||||
err = cannotConvert(d, s)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func convertAssignBulkString(d reflect.Value, s []byte) (err error) {
|
||||
switch d.Type().Kind() {
|
||||
case reflect.Slice:
|
||||
// Handle []byte destination here to avoid unnecessary
|
||||
// []byte -> string -> []byte converion.
|
||||
if d.Type().Elem().Kind() == reflect.Uint8 {
|
||||
d.SetBytes(s)
|
||||
} else {
|
||||
err = cannotConvert(d, s)
|
||||
}
|
||||
case reflect.Ptr:
|
||||
if d.CanInterface() && d.CanSet() {
|
||||
if s == nil {
|
||||
if d.IsNil() {
|
||||
return nil
|
||||
}
|
||||
|
||||
d.Set(reflect.Zero(d.Type()))
|
||||
return nil
|
||||
}
|
||||
|
||||
if d.IsNil() {
|
||||
d.Set(reflect.New(d.Type().Elem()))
|
||||
}
|
||||
|
||||
if sc, ok := d.Interface().(Scanner); ok {
|
||||
return sc.RedisScan(s)
|
||||
}
|
||||
}
|
||||
err = convertAssignString(d, string(s))
|
||||
default:
|
||||
err = convertAssignString(d, string(s))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func convertAssignInt(d reflect.Value, s int64) (err error) {
|
||||
switch d.Type().Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
d.SetInt(s)
|
||||
if d.Int() != s {
|
||||
err = strconv.ErrRange
|
||||
d.SetInt(0)
|
||||
}
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
if s < 0 {
|
||||
err = strconv.ErrRange
|
||||
} else {
|
||||
x := uint64(s)
|
||||
d.SetUint(x)
|
||||
if d.Uint() != x {
|
||||
err = strconv.ErrRange
|
||||
d.SetUint(0)
|
||||
}
|
||||
}
|
||||
case reflect.Bool:
|
||||
d.SetBool(s != 0)
|
||||
default:
|
||||
err = cannotConvert(d, s)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func convertAssignValue(d reflect.Value, s interface{}) (err error) {
|
||||
if d.Kind() != reflect.Ptr {
|
||||
if d.CanAddr() {
|
||||
d2 := d.Addr()
|
||||
if d2.CanInterface() {
|
||||
if scanner, ok := d2.Interface().(Scanner); ok {
|
||||
return scanner.RedisScan(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if d.CanInterface() {
|
||||
// Already a reflect.Ptr
|
||||
if d.IsNil() {
|
||||
d.Set(reflect.New(d.Type().Elem()))
|
||||
}
|
||||
if scanner, ok := d.Interface().(Scanner); ok {
|
||||
return scanner.RedisScan(s)
|
||||
}
|
||||
}
|
||||
|
||||
switch s := s.(type) {
|
||||
case nil:
|
||||
err = convertAssignNil(d)
|
||||
case []byte:
|
||||
err = convertAssignBulkString(d, s)
|
||||
case int64:
|
||||
err = convertAssignInt(d, s)
|
||||
case string:
|
||||
err = convertAssignString(d, s)
|
||||
case Error:
|
||||
err = convertAssignError(d, s)
|
||||
default:
|
||||
err = cannotConvert(d, s)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func convertAssignArray(d reflect.Value, s []interface{}) error {
|
||||
if d.Type().Kind() != reflect.Slice {
|
||||
return cannotConvert(d, s)
|
||||
}
|
||||
ensureLen(d, len(s))
|
||||
for i := 0; i < len(s); i++ {
|
||||
if err := convertAssignValue(d.Index(i), s[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertAssign(d interface{}, s interface{}) (err error) {
|
||||
if scanner, ok := d.(Scanner); ok {
|
||||
return scanner.RedisScan(s)
|
||||
}
|
||||
|
||||
// Handle the most common destination types using type switches and
|
||||
// fall back to reflection for all other types.
|
||||
switch s := s.(type) {
|
||||
case nil:
|
||||
// ignore
|
||||
case []byte:
|
||||
switch d := d.(type) {
|
||||
case *string:
|
||||
*d = string(s)
|
||||
case *int:
|
||||
*d, err = strconv.Atoi(string(s))
|
||||
case *bool:
|
||||
*d, err = strconv.ParseBool(string(s))
|
||||
case *[]byte:
|
||||
*d = s
|
||||
case *interface{}:
|
||||
*d = s
|
||||
case nil:
|
||||
// skip value
|
||||
default:
|
||||
if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr {
|
||||
err = cannotConvert(d, s)
|
||||
} else {
|
||||
err = convertAssignBulkString(d.Elem(), s)
|
||||
}
|
||||
}
|
||||
case int64:
|
||||
switch d := d.(type) {
|
||||
case *int:
|
||||
x := int(s)
|
||||
if int64(x) != s {
|
||||
err = strconv.ErrRange
|
||||
x = 0
|
||||
}
|
||||
*d = x
|
||||
case *bool:
|
||||
*d = s != 0
|
||||
case *interface{}:
|
||||
*d = s
|
||||
case nil:
|
||||
// skip value
|
||||
default:
|
||||
if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr {
|
||||
err = cannotConvert(d, s)
|
||||
} else {
|
||||
err = convertAssignInt(d.Elem(), s)
|
||||
}
|
||||
}
|
||||
case string:
|
||||
switch d := d.(type) {
|
||||
case *string:
|
||||
*d = s
|
||||
case *interface{}:
|
||||
*d = s
|
||||
case nil:
|
||||
// skip value
|
||||
default:
|
||||
err = cannotConvert(reflect.ValueOf(d), s)
|
||||
}
|
||||
case []interface{}:
|
||||
switch d := d.(type) {
|
||||
case *[]interface{}:
|
||||
*d = s
|
||||
case *interface{}:
|
||||
*d = s
|
||||
case nil:
|
||||
// skip value
|
||||
default:
|
||||
if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr {
|
||||
err = cannotConvert(d, s)
|
||||
} else {
|
||||
err = convertAssignArray(d.Elem(), s)
|
||||
}
|
||||
}
|
||||
case Error:
|
||||
err = s
|
||||
default:
|
||||
err = cannotConvert(reflect.ValueOf(d), s)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Scan copies from src to the values pointed at by dest.
|
||||
//
|
||||
// Scan uses RedisScan if available otherwise:
|
||||
//
|
||||
// The values pointed at by dest must be an integer, float, boolean, string,
|
||||
// []byte, interface{} or slices of these types. Scan uses the standard strconv
|
||||
// package to convert bulk strings to numeric and boolean types.
|
||||
//
|
||||
// If a dest value is nil, then the corresponding src value is skipped.
|
||||
//
|
||||
// If a src element is nil, then the corresponding dest value is not modified.
|
||||
//
|
||||
// To enable easy use of Scan in a loop, Scan returns the slice of src
|
||||
// following the copied values.
|
||||
func Scan(src []interface{}, dest ...interface{}) ([]interface{}, error) {
|
||||
if len(src) < len(dest) {
|
||||
return nil, errors.New("redigo.Scan: array short")
|
||||
}
|
||||
var err error
|
||||
for i, d := range dest {
|
||||
err = convertAssign(d, src[i])
|
||||
if err != nil {
|
||||
err = fmt.Errorf("redigo.Scan: cannot assign to dest %d: %v", i, err)
|
||||
break
|
||||
}
|
||||
}
|
||||
return src[len(dest):], err
|
||||
}
|
||||
|
||||
type fieldSpec struct {
|
||||
name string
|
||||
index []int
|
||||
omitEmpty bool
|
||||
}
|
||||
|
||||
type structSpec struct {
|
||||
m map[string]*fieldSpec
|
||||
l []*fieldSpec
|
||||
}
|
||||
|
||||
func (ss *structSpec) fieldSpec(name []byte) *fieldSpec {
|
||||
return ss.m[string(name)]
|
||||
}
|
||||
|
||||
func compileStructSpec(t reflect.Type, depth map[string]int, index []int, ss *structSpec, seen map[reflect.Type]struct{}) error {
|
||||
if _, ok := seen[t]; ok {
|
||||
// Protect against infinite recursion.
|
||||
return fmt.Errorf("recursive struct definition for %v", t)
|
||||
}
|
||||
|
||||
seen[t] = struct{}{}
|
||||
LOOP:
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
f := t.Field(i)
|
||||
switch {
|
||||
case f.PkgPath != "" && !f.Anonymous:
|
||||
// Ignore unexported fields.
|
||||
case f.Anonymous:
|
||||
switch f.Type.Kind() {
|
||||
case reflect.Struct:
|
||||
if err := compileStructSpec(f.Type, depth, append(index, i), ss, seen); err != nil {
|
||||
return err
|
||||
}
|
||||
case reflect.Ptr:
|
||||
if f.Type.Elem().Kind() == reflect.Struct {
|
||||
if err := compileStructSpec(f.Type.Elem(), depth, append(index, i), ss, seen); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
fs := &fieldSpec{name: f.Name}
|
||||
tag := f.Tag.Get("redis")
|
||||
|
||||
var p string
|
||||
first := true
|
||||
for len(tag) > 0 {
|
||||
i := strings.IndexByte(tag, ',')
|
||||
if i < 0 {
|
||||
p, tag = tag, ""
|
||||
} else {
|
||||
p, tag = tag[:i], tag[i+1:]
|
||||
}
|
||||
if p == "-" {
|
||||
continue LOOP
|
||||
}
|
||||
if first && len(p) > 0 {
|
||||
fs.name = p
|
||||
first = false
|
||||
} else {
|
||||
switch p {
|
||||
case "omitempty":
|
||||
fs.omitEmpty = true
|
||||
default:
|
||||
panic(fmt.Errorf("redigo: unknown field tag %s for type %s", p, t.Name()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
d, found := depth[fs.name]
|
||||
if !found {
|
||||
d = 1 << 30
|
||||
}
|
||||
|
||||
switch {
|
||||
case len(index) == d:
|
||||
// At same depth, remove from result.
|
||||
delete(ss.m, fs.name)
|
||||
j := 0
|
||||
for i := 0; i < len(ss.l); i++ {
|
||||
if fs.name != ss.l[i].name {
|
||||
ss.l[j] = ss.l[i]
|
||||
j += 1
|
||||
}
|
||||
}
|
||||
ss.l = ss.l[:j]
|
||||
case len(index) < d:
|
||||
fs.index = make([]int, len(index)+1)
|
||||
copy(fs.index, index)
|
||||
fs.index[len(index)] = i
|
||||
depth[fs.name] = len(index)
|
||||
ss.m[fs.name] = fs
|
||||
ss.l = append(ss.l, fs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
structSpecMutex sync.RWMutex
|
||||
structSpecCache = make(map[reflect.Type]*structSpec)
|
||||
)
|
||||
|
||||
func structSpecForType(t reflect.Type) (*structSpec, error) {
|
||||
structSpecMutex.RLock()
|
||||
ss, found := structSpecCache[t]
|
||||
structSpecMutex.RUnlock()
|
||||
if found {
|
||||
return ss, nil
|
||||
}
|
||||
|
||||
structSpecMutex.Lock()
|
||||
defer structSpecMutex.Unlock()
|
||||
ss, found = structSpecCache[t]
|
||||
if found {
|
||||
return ss, nil
|
||||
}
|
||||
|
||||
ss = &structSpec{m: make(map[string]*fieldSpec)}
|
||||
if err := compileStructSpec(t, make(map[string]int), nil, ss, make(map[reflect.Type]struct{})); err != nil {
|
||||
return nil, fmt.Errorf("compile struct: %s: %w", t, err)
|
||||
}
|
||||
structSpecCache[t] = ss
|
||||
return ss, nil
|
||||
}
|
||||
|
||||
var errScanStructValue = errors.New("redigo.ScanStruct: value must be non-nil pointer to a struct")
|
||||
|
||||
// ScanStruct scans alternating names and values from src to a struct. The
|
||||
// HGETALL and CONFIG GET commands return replies in this format.
|
||||
//
|
||||
// ScanStruct uses exported field names to match values in the response. Use
|
||||
// 'redis' field tag to override the name:
|
||||
//
|
||||
// Field int `redis:"myName"`
|
||||
//
|
||||
// Fields with the tag redis:"-" are ignored.
|
||||
//
|
||||
// Each field uses RedisScan if available otherwise:
|
||||
// Integer, float, boolean, string and []byte fields are supported. Scan uses the
|
||||
// standard strconv package to convert bulk string values to numeric and
|
||||
// boolean types.
|
||||
//
|
||||
// If a src element is nil, then the corresponding field is not modified.
|
||||
func ScanStruct(src []interface{}, dest interface{}) error {
|
||||
d := reflect.ValueOf(dest)
|
||||
if d.Kind() != reflect.Ptr || d.IsNil() {
|
||||
return errScanStructValue
|
||||
}
|
||||
|
||||
d = d.Elem()
|
||||
if d.Kind() != reflect.Struct {
|
||||
return errScanStructValue
|
||||
}
|
||||
|
||||
if len(src)%2 != 0 {
|
||||
return errors.New("redigo.ScanStruct: number of values not a multiple of 2")
|
||||
}
|
||||
|
||||
ss, err := structSpecForType(d.Type())
|
||||
if err != nil {
|
||||
return fmt.Errorf("redigo.ScanStruct: %w", err)
|
||||
}
|
||||
|
||||
for i := 0; i < len(src); i += 2 {
|
||||
s := src[i+1]
|
||||
if s == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
name, ok := convertToBulk(src[i])
|
||||
if !ok {
|
||||
return fmt.Errorf("redigo.ScanStruct: key %d not a bulk string value got type: %T", i, src[i])
|
||||
}
|
||||
|
||||
fs := ss.fieldSpec(name)
|
||||
if fs == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := convertAssignValue(fieldByIndexCreate(d, fs.index), s); err != nil {
|
||||
return fmt.Errorf("redigo.ScanStruct: cannot assign field %s: %v", fs.name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// convertToBulk converts src to a []byte if src is a string or bulk string
|
||||
// and returns true. Otherwise nil and false is returned.
|
||||
func convertToBulk(src interface{}) ([]byte, bool) {
|
||||
switch v := src.(type) {
|
||||
case []byte:
|
||||
return v, true
|
||||
case string:
|
||||
return []byte(v), true
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
errScanSliceValue = errors.New("redigo.ScanSlice: dest must be non-nil pointer to a struct")
|
||||
)
|
||||
|
||||
// ScanSlice scans src to the slice pointed to by dest.
|
||||
//
|
||||
// If the target is a slice of types which implement Scanner then the custom
|
||||
// RedisScan method is used otherwise the following rules apply:
|
||||
//
|
||||
// The elements in the dest slice must be integer, float, boolean, string, struct
|
||||
// or pointer to struct values.
|
||||
//
|
||||
// Struct fields must be integer, float, boolean or string values. All struct
|
||||
// fields are used unless a subset is specified using fieldNames.
|
||||
func ScanSlice(src []interface{}, dest interface{}, fieldNames ...string) error {
|
||||
d := reflect.ValueOf(dest)
|
||||
if d.Kind() != reflect.Ptr || d.IsNil() {
|
||||
return errScanSliceValue
|
||||
}
|
||||
d = d.Elem()
|
||||
if d.Kind() != reflect.Slice {
|
||||
return errScanSliceValue
|
||||
}
|
||||
|
||||
isPtr := false
|
||||
t := d.Type().Elem()
|
||||
st := t
|
||||
if t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct {
|
||||
isPtr = true
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
if t.Kind() != reflect.Struct || st.Implements(scannerType) {
|
||||
ensureLen(d, len(src))
|
||||
for i, s := range src {
|
||||
if s == nil {
|
||||
continue
|
||||
}
|
||||
if err := convertAssignValue(d.Index(i), s); err != nil {
|
||||
return fmt.Errorf("redigo.ScanSlice: cannot assign element %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
ss, err := structSpecForType(t)
|
||||
if err != nil {
|
||||
return fmt.Errorf("redigo.ScanSlice: %w", err)
|
||||
}
|
||||
|
||||
fss := ss.l
|
||||
if len(fieldNames) > 0 {
|
||||
fss = make([]*fieldSpec, len(fieldNames))
|
||||
for i, name := range fieldNames {
|
||||
fss[i] = ss.m[name]
|
||||
if fss[i] == nil {
|
||||
return fmt.Errorf("redigo.ScanSlice: ScanSlice bad field name %s", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(fss) == 0 {
|
||||
return errors.New("redigo.ScanSlice: no struct fields")
|
||||
}
|
||||
|
||||
n := len(src) / len(fss)
|
||||
if n*len(fss) != len(src) {
|
||||
return errors.New("redigo.ScanSlice: length not a multiple of struct field count")
|
||||
}
|
||||
|
||||
ensureLen(d, n)
|
||||
for i := 0; i < n; i++ {
|
||||
d := d.Index(i)
|
||||
if isPtr {
|
||||
if d.IsNil() {
|
||||
d.Set(reflect.New(t))
|
||||
}
|
||||
d = d.Elem()
|
||||
}
|
||||
for j, fs := range fss {
|
||||
s := src[i*len(fss)+j]
|
||||
if s == nil {
|
||||
continue
|
||||
}
|
||||
if err := convertAssignValue(d.FieldByIndex(fs.index), s); err != nil {
|
||||
return fmt.Errorf("redigo.ScanSlice: cannot assign element %d to field %s: %v", i*len(fss)+j, fs.name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Args is a helper for constructing command arguments from structured values.
|
||||
type Args []interface{}
|
||||
|
||||
// Add returns the result of appending value to args.
|
||||
func (args Args) Add(value ...interface{}) Args {
|
||||
return append(args, value...)
|
||||
}
|
||||
|
||||
// AddFlat returns the result of appending the flattened value of v to args.
|
||||
//
|
||||
// Maps are flattened by appending the alternating keys and map values to args.
|
||||
//
|
||||
// Slices are flattened by appending the slice elements to args.
|
||||
//
|
||||
// Structs are flattened by appending the alternating names and values of
|
||||
// exported fields to args. If v is a nil struct pointer, then nothing is
|
||||
// appended. The 'redis' field tag overrides struct field names. See ScanStruct
|
||||
// for more information on the use of the 'redis' field tag.
|
||||
//
|
||||
// Other types are appended to args as is.
|
||||
// panics if v includes a recursive anonymous struct.
|
||||
func (args Args) AddFlat(v interface{}) Args {
|
||||
rv := reflect.ValueOf(v)
|
||||
switch rv.Kind() {
|
||||
case reflect.Struct:
|
||||
args = flattenStruct(args, rv)
|
||||
case reflect.Slice:
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
args = append(args, rv.Index(i).Interface())
|
||||
}
|
||||
case reflect.Map:
|
||||
for _, k := range rv.MapKeys() {
|
||||
args = append(args, k.Interface(), rv.MapIndex(k).Interface())
|
||||
}
|
||||
case reflect.Ptr:
|
||||
if rv.Type().Elem().Kind() == reflect.Struct {
|
||||
if !rv.IsNil() {
|
||||
args = flattenStruct(args, rv.Elem())
|
||||
}
|
||||
} else {
|
||||
args = append(args, v)
|
||||
}
|
||||
default:
|
||||
args = append(args, v)
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
func flattenStruct(args Args, v reflect.Value) Args {
|
||||
ss, err := structSpecForType(v.Type())
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("redigo.AddFlat: %w", err))
|
||||
}
|
||||
|
||||
for _, fs := range ss.l {
|
||||
fv, err := fieldByIndexErr(v, fs.index)
|
||||
if err != nil {
|
||||
// Nil item ignore.
|
||||
continue
|
||||
}
|
||||
if fs.omitEmpty {
|
||||
var empty = false
|
||||
switch fv.Kind() {
|
||||
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
|
||||
empty = fv.Len() == 0
|
||||
case reflect.Bool:
|
||||
empty = !fv.Bool()
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
empty = fv.Int() == 0
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
empty = fv.Uint() == 0
|
||||
case reflect.Float32, reflect.Float64:
|
||||
empty = fv.Float() == 0
|
||||
case reflect.Interface, reflect.Ptr:
|
||||
empty = fv.IsNil()
|
||||
}
|
||||
if empty {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if arg, ok := fv.Interface().(Argument); ok {
|
||||
args = append(args, fs.name, arg.RedisArg())
|
||||
} else if fv.Kind() == reflect.Ptr {
|
||||
if !fv.IsNil() {
|
||||
args = append(args, fs.name, fv.Elem().Interface())
|
||||
}
|
||||
} else {
|
||||
args = append(args, fs.name, fv.Interface())
|
||||
}
|
||||
}
|
||||
return args
|
||||
}
|
||||
-104
@@ -1,104 +0,0 @@
|
||||
// Copyright 2012 Gary Burd
|
||||
//
|
||||
// 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.
|
||||
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Script encapsulates the source, hash and key count for a Lua script. See
|
||||
// http://redis.io/commands/eval for information on scripts in Redis.
|
||||
type Script struct {
|
||||
keyCount int
|
||||
src string
|
||||
hash string
|
||||
}
|
||||
|
||||
// NewScript returns a new script object. If keyCount is greater than or equal
|
||||
// to zero, then the count is automatically inserted in the EVAL command
|
||||
// argument list. If keyCount is less than zero, then the application supplies
|
||||
// the count as the first value in the keysAndArgs argument to the Do, Send and
|
||||
// SendHash methods.
|
||||
func NewScript(keyCount int, src string) *Script {
|
||||
h := sha1.New()
|
||||
io.WriteString(h, src) // nolint: errcheck
|
||||
return &Script{keyCount, src, hex.EncodeToString(h.Sum(nil))}
|
||||
}
|
||||
|
||||
func (s *Script) args(spec string, keysAndArgs []interface{}) []interface{} {
|
||||
var args []interface{}
|
||||
if s.keyCount < 0 {
|
||||
args = make([]interface{}, 1+len(keysAndArgs))
|
||||
args[0] = spec
|
||||
copy(args[1:], keysAndArgs)
|
||||
} else {
|
||||
args = make([]interface{}, 2+len(keysAndArgs))
|
||||
args[0] = spec
|
||||
args[1] = s.keyCount
|
||||
copy(args[2:], keysAndArgs)
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
// Hash returns the script hash.
|
||||
func (s *Script) Hash() string {
|
||||
return s.hash
|
||||
}
|
||||
|
||||
func (s *Script) DoContext(ctx context.Context, c Conn, keysAndArgs ...interface{}) (interface{}, error) {
|
||||
cwt, ok := c.(ConnWithContext)
|
||||
if !ok {
|
||||
return nil, errContextNotSupported
|
||||
}
|
||||
v, err := cwt.DoContext(ctx, "EVALSHA", s.args(s.hash, keysAndArgs)...)
|
||||
if e, ok := err.(Error); ok && strings.HasPrefix(string(e), "NOSCRIPT ") {
|
||||
v, err = cwt.DoContext(ctx, "EVAL", s.args(s.src, keysAndArgs)...)
|
||||
}
|
||||
return v, err
|
||||
}
|
||||
|
||||
// Do evaluates the script. Under the covers, Do optimistically evaluates the
|
||||
// script using the EVALSHA command. If the command fails because the script is
|
||||
// not loaded, then Do evaluates the script using the EVAL command (thus
|
||||
// causing the script to load).
|
||||
func (s *Script) Do(c Conn, keysAndArgs ...interface{}) (interface{}, error) {
|
||||
v, err := c.Do("EVALSHA", s.args(s.hash, keysAndArgs)...)
|
||||
if err != nil && strings.HasPrefix(err.Error(), "NOSCRIPT ") {
|
||||
v, err = c.Do("EVAL", s.args(s.src, keysAndArgs)...)
|
||||
}
|
||||
return v, err
|
||||
}
|
||||
|
||||
// SendHash evaluates the script without waiting for the reply. The script is
|
||||
// evaluated with the EVALSHA command. The application must ensure that the
|
||||
// script is loaded by a previous call to Send, Do or Load methods.
|
||||
func (s *Script) SendHash(c Conn, keysAndArgs ...interface{}) error {
|
||||
return c.Send("EVALSHA", s.args(s.hash, keysAndArgs)...)
|
||||
}
|
||||
|
||||
// Send evaluates the script without waiting for the reply.
|
||||
func (s *Script) Send(c Conn, keysAndArgs ...interface{}) error {
|
||||
return c.Send("EVAL", s.args(s.src, keysAndArgs)...)
|
||||
}
|
||||
|
||||
// Load loads the script without evaluating it.
|
||||
func (s *Script) Load(c Conn) error {
|
||||
_, err := c.Do("SCRIPT", "LOAD", s.src)
|
||||
return err
|
||||
}
|
||||
+11
-1
@@ -13,7 +13,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package renameio
|
||||
|
||||
@@ -86,3 +85,14 @@ func WithReplaceOnClose() Option {
|
||||
c.renameOnClose = true
|
||||
})
|
||||
}
|
||||
|
||||
// WithRoot specifies a root directory to use when working with files.
|
||||
// See [os.Root] and https://go.dev/blog/osroot for more details.
|
||||
//
|
||||
// When WithRoot is used, WithTempDir (and the $TMPDIR environment variable) are
|
||||
// ignored, as temporary files must be created in the specified root directory.
|
||||
func WithRoot(root *os.Root) Option {
|
||||
return optionFunc(func(c *config) {
|
||||
c.root = root
|
||||
})
|
||||
}
|
||||
|
||||
+114
-16
@@ -13,13 +13,11 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package renameio
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"math/rand/v2"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@@ -29,10 +27,10 @@ import (
|
||||
const defaultPerm os.FileMode = 0o600
|
||||
|
||||
// nextrandom is a function generating a random number.
|
||||
var nextrandom = rand.Int63
|
||||
var nextrandom = rand.Int64
|
||||
|
||||
// openTempFile creates a randomly named file and returns an open handle. It is
|
||||
// similar to ioutil.TempFile except that the directory must be given, the file
|
||||
// similar to os.CreateTemp except that the directory must be given, the file
|
||||
// permissions can be controlled and patterns in the name are not supported.
|
||||
// The name is always suffixed with a random number.
|
||||
func openTempFile(dir, name string, perm os.FileMode) (*os.File, error) {
|
||||
@@ -58,6 +56,33 @@ func openTempFile(dir, name string, perm os.FileMode) (*os.File, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// openTempFileRoot creates a randomly named file in root and returns an open
|
||||
// handle. It is similar to os.CreateTemp except that the directory must be
|
||||
// given, the file permissions can be controlled and patterns in the name are
|
||||
// not supported. The name is always suffixed with a random number.
|
||||
func openTempFileRoot(root *os.Root, name string, perm os.FileMode) (string, *os.File, error) {
|
||||
prefix := name
|
||||
|
||||
for attempt := 0; ; {
|
||||
// Generate a reasonably random name which is unlikely to already
|
||||
// exist. O_EXCL ensures that existing files generate an error.
|
||||
name := prefix + strconv.FormatInt(nextrandom(), 10)
|
||||
|
||||
f, err := root.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, perm)
|
||||
if !os.IsExist(err) {
|
||||
return name, f, err
|
||||
}
|
||||
|
||||
if attempt++; attempt > 10000 {
|
||||
return "", nil, &os.PathError{
|
||||
Op: "tempfile",
|
||||
Path: name,
|
||||
Err: os.ErrExist,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TempDir checks whether os.TempDir() can be used as a temporary directory for
|
||||
// later atomically replacing files within dest. If no (os.TempDir() resides on
|
||||
// a different mount point), dest is returned.
|
||||
@@ -83,7 +108,7 @@ func tempDir(dir, dest string) string {
|
||||
// the TMPDIR environment variable.
|
||||
tmpdir := os.TempDir()
|
||||
|
||||
testsrc, err := ioutil.TempFile(tmpdir, "."+filepath.Base(dest))
|
||||
testsrc, err := os.CreateTemp(tmpdir, "."+filepath.Base(dest))
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
@@ -95,7 +120,7 @@ func tempDir(dir, dest string) string {
|
||||
}()
|
||||
testsrc.Close()
|
||||
|
||||
testdest, err := ioutil.TempFile(filepath.Dir(dest), "."+filepath.Base(dest))
|
||||
testdest, err := os.CreateTemp(filepath.Dir(dest), "."+filepath.Base(dest))
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
@@ -118,6 +143,8 @@ type PendingFile struct {
|
||||
done bool
|
||||
closed bool
|
||||
replaceOnClose bool
|
||||
root *os.Root
|
||||
tmpname string
|
||||
}
|
||||
|
||||
// Cleanup is a no-op if CloseAtomicallyReplace succeeded, and otherwise closes
|
||||
@@ -134,8 +161,14 @@ func (t *PendingFile) Cleanup() error {
|
||||
if !t.closed {
|
||||
closeErr = t.File.Close()
|
||||
}
|
||||
if err := os.Remove(t.Name()); err != nil {
|
||||
return err
|
||||
if t.root != nil {
|
||||
if err := t.root.Remove(t.tmpname); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := os.Remove(t.Name()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
t.done = true
|
||||
return closeErr
|
||||
@@ -163,8 +196,14 @@ func (t *PendingFile) CloseAtomicallyReplace() error {
|
||||
if err := t.File.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(t.Name(), t.path); err != nil {
|
||||
return err
|
||||
if t.root != nil {
|
||||
if err := t.root.Rename(t.tmpname, t.path); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := os.Rename(t.Name(), t.path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
t.done = true
|
||||
return nil
|
||||
@@ -200,6 +239,7 @@ type config struct {
|
||||
ignoreUmask bool
|
||||
chmod *os.FileMode
|
||||
renameOnClose bool
|
||||
root *os.Root
|
||||
}
|
||||
|
||||
// NewPendingFile creates a temporary file destined to atomically creating or
|
||||
@@ -227,8 +267,15 @@ func NewPendingFile(path string, opts ...Option) (*PendingFile, error) {
|
||||
}
|
||||
|
||||
if cfg.attemptPermCopy {
|
||||
var existing os.FileInfo
|
||||
var err error
|
||||
if cfg.root != nil {
|
||||
existing, err = cfg.root.Lstat(cfg.path)
|
||||
} else {
|
||||
existing, err = os.Lstat(cfg.path)
|
||||
}
|
||||
// Try to determine permissions from an existing file.
|
||||
if existing, err := os.Lstat(cfg.path); err == nil && existing.Mode().IsRegular() {
|
||||
if err == nil && existing.Mode().IsRegular() {
|
||||
perm := existing.Mode() & os.ModePerm
|
||||
cfg.chmod = &perm
|
||||
|
||||
@@ -240,7 +287,14 @@ func NewPendingFile(path string, opts ...Option) (*PendingFile, error) {
|
||||
}
|
||||
}
|
||||
|
||||
f, err := openTempFile(tempDir(cfg.dir, cfg.path), "."+filepath.Base(cfg.path), cfg.createPerm)
|
||||
var f *os.File
|
||||
var err error
|
||||
var tmpname string
|
||||
if cfg.root != nil {
|
||||
tmpname, f, err = openTempFileRoot(cfg.root, "."+filepath.Base(cfg.path), cfg.createPerm)
|
||||
} else {
|
||||
f, err = openTempFile(tempDir(cfg.dir, cfg.path), "."+filepath.Base(cfg.path), cfg.createPerm)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -255,7 +309,13 @@ func NewPendingFile(path string, opts ...Option) (*PendingFile, error) {
|
||||
}
|
||||
}
|
||||
|
||||
return &PendingFile{File: f, path: cfg.path, replaceOnClose: cfg.renameOnClose}, nil
|
||||
return &PendingFile{
|
||||
File: f,
|
||||
path: cfg.path,
|
||||
replaceOnClose: cfg.renameOnClose,
|
||||
root: cfg.root,
|
||||
tmpname: tmpname,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Symlink wraps os.Symlink, replacing an existing symlink with the same name
|
||||
@@ -267,9 +327,9 @@ func Symlink(oldname, newname string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// We need to use ioutil.TempDir, as we cannot overwrite a ioutil.TempFile,
|
||||
// We need to use os.MkdirTemp, as we cannot overwrite a os.CreateTemp file,
|
||||
// and removing+symlinking creates a TOCTOU race.
|
||||
d, err := ioutil.TempDir(filepath.Dir(newname), "."+filepath.Base(newname))
|
||||
d, err := os.MkdirTemp(filepath.Dir(newname), "."+filepath.Base(newname))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -292,3 +352,41 @@ func Symlink(oldname, newname string) error {
|
||||
cleanup = false
|
||||
return os.RemoveAll(d)
|
||||
}
|
||||
|
||||
// SymlinkRoot wraps os.Symlink, replacing an existing symlink with the same
|
||||
// name atomically (os.Symlink fails when newname already exists, at least on
|
||||
// Linux).
|
||||
func SymlinkRoot(root *os.Root, oldname, newname string) error {
|
||||
// Fast path: if newname does not exist yet, we can skip the whole dance
|
||||
// below.
|
||||
if err := root.Symlink(oldname, newname); err == nil || !os.IsExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
// We need to use os.MkdirTemp, as we cannot overwrite a os.CreateTemp file,
|
||||
// and removing+symlinking creates a TOCTOU race.
|
||||
//
|
||||
// There is no os.Root-compatible os.MkdirTemp, so we use the path directly.
|
||||
d, err := os.MkdirTemp(root.Name(), "."+filepath.Base(newname))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cleanup := true
|
||||
defer func() {
|
||||
if cleanup {
|
||||
os.RemoveAll(d)
|
||||
}
|
||||
}()
|
||||
|
||||
symlink := filepath.Join(filepath.Base(d), "tmp.symlink")
|
||||
if err := root.Symlink(oldname, symlink); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := root.Rename(symlink, newname); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cleanup = false
|
||||
return os.RemoveAll(d)
|
||||
}
|
||||
|
||||
+1
-2
@@ -13,13 +13,12 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package renameio
|
||||
|
||||
import "os"
|
||||
|
||||
// WriteFile mirrors ioutil.WriteFile, replacing an existing file with the same
|
||||
// WriteFile mirrors os.WriteFile, replacing an existing file with the same
|
||||
// name atomically.
|
||||
func WriteFile(filename string, data []byte, perm os.FileMode, opts ...Option) error {
|
||||
opts = append([]Option{
|
||||
|
||||
+2
-1
@@ -3,4 +3,5 @@
|
||||
validator
|
||||
golangci-lint
|
||||
functional_tests
|
||||
.idea
|
||||
.idea
|
||||
vendor/
|
||||
|
||||
+8
-1
@@ -458,7 +458,14 @@ func (o *Object) ReadAt(b []byte, offset int64) (n int, err error) {
|
||||
return 0, o.prevErr
|
||||
}
|
||||
|
||||
// Set the current offset to ReadAt offset, because the current offset will be shifted at the end of this method.
|
||||
// Save and restore currOffset so ReadAt doesn't affect sequential Read operations.
|
||||
// Per io.ReaderAt: "ReadAt should not affect nor be affected by the underlying seek offset."
|
||||
savedOffset := o.currOffset
|
||||
defer func() {
|
||||
o.currOffset = savedOffset
|
||||
o.seekData = true // Force next Read to re-establish stream at correct position
|
||||
}()
|
||||
|
||||
o.currOffset = offset
|
||||
|
||||
// Can only compare offsets to size when size has been set.
|
||||
|
||||
+1
-1
@@ -525,7 +525,7 @@ func (c *Client) putObjectMultipartStreamParallel(ctx context.Context, bucketNam
|
||||
break
|
||||
}
|
||||
|
||||
if rerr != nil && rerr != io.ErrUnexpectedEOF && err != io.EOF {
|
||||
if rerr != nil && rerr != io.ErrUnexpectedEOF && rerr != io.EOF {
|
||||
cancel()
|
||||
wg.Wait()
|
||||
return UploadInfo{}, rerr
|
||||
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* MinIO Go Library for Amazon S3 Compatible Cloud Storage
|
||||
* Copyright 2025-2026 MinIO, Inc.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package minio
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/minio/minio-go/v7/pkg/s3utils"
|
||||
)
|
||||
|
||||
// updateObjectEncryptionSSEKMS represents the SSE-KMS element in the request body.
|
||||
type updateObjectEncryptionSSEKMS struct {
|
||||
BucketKeyEnabled bool `xml:"BucketKeyEnabled,omitempty"`
|
||||
KMSKeyArn string `xml:"KMSKeyArn"`
|
||||
}
|
||||
|
||||
// updateObjectEncryptionRequest represents the XML request body for UpdateObjectEncryption.
|
||||
type updateObjectEncryptionRequest struct {
|
||||
XMLName xml.Name `xml:"ObjectEncryption"`
|
||||
XMLNS string `xml:"xmlns,attr"`
|
||||
SSEKMS *updateObjectEncryptionSSEKMS `xml:"SSE-KMS"`
|
||||
}
|
||||
|
||||
// UpdateObjectEncryptionOptions holds options for the UpdateObjectEncryption call.
|
||||
type UpdateObjectEncryptionOptions struct {
|
||||
// KMSKeyArn is the KMS key name or ARN to encrypt the object with.
|
||||
KMSKeyArn string
|
||||
|
||||
// BucketKeyEnabled enables S3 Bucket Key for KMS encryption.
|
||||
BucketKeyEnabled bool
|
||||
|
||||
// VersionID targets a specific object version.
|
||||
VersionID string
|
||||
}
|
||||
|
||||
// UpdateObjectEncryption changes the encryption configuration of an existing object in-place.
|
||||
// The object must already be encrypted with SSE-S3 or SSE-KMS. SSE-C objects are not supported.
|
||||
// This operation rotates the data encryption key envelope without re-reading/re-writing object data.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: Context for request cancellation and timeout
|
||||
// - bucketName: Name of the bucket
|
||||
// - objectName: Name of the object
|
||||
// - opts: Options including KMSKeyArn (required), optional BucketKeyEnabled, and optional VersionID
|
||||
//
|
||||
// Returns an error if the operation fails.
|
||||
func (c *Client) UpdateObjectEncryption(ctx context.Context, bucketName, objectName string, opts UpdateObjectEncryptionOptions) error {
|
||||
// Input validation.
|
||||
if err := s3utils.CheckValidBucketName(bucketName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s3utils.CheckValidObjectName(objectName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.KMSKeyArn == "" {
|
||||
return errInvalidArgument("KMSKeyArn is required for UpdateObjectEncryption.")
|
||||
}
|
||||
|
||||
// Get resources properly escaped and lined up before
|
||||
// using them in http request.
|
||||
urlValues := make(url.Values)
|
||||
urlValues.Set("encryption", "")
|
||||
|
||||
if opts.VersionID != "" {
|
||||
urlValues.Set("versionId", opts.VersionID)
|
||||
}
|
||||
|
||||
reqBody := updateObjectEncryptionRequest{
|
||||
XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/",
|
||||
SSEKMS: &updateObjectEncryptionSSEKMS{
|
||||
BucketKeyEnabled: opts.BucketKeyEnabled,
|
||||
KMSKeyArn: opts.KMSKeyArn,
|
||||
},
|
||||
}
|
||||
|
||||
bodyData, err := xml.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reqMetadata := requestMetadata{
|
||||
bucketName: bucketName,
|
||||
objectName: objectName,
|
||||
queryValues: urlValues,
|
||||
contentBody: bytes.NewReader(bodyData),
|
||||
contentLength: int64(len(bodyData)),
|
||||
contentMD5Base64: sumMD5Base64(bodyData),
|
||||
contentSHA256Hex: sum256Hex(bodyData),
|
||||
}
|
||||
|
||||
// Execute PUT Object Encryption.
|
||||
resp, err := c.executeMethod(ctx, http.MethodPut, reqMetadata)
|
||||
defer closeResponse(resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return httpRespToErrorResponse(resp, bucketName, objectName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+36
-1
@@ -2004,7 +2004,6 @@ func testPutObjectWithAutoChecksums() {
|
||||
// Save the data
|
||||
objectName := randString(60, rand.NewSource(time.Now().UnixNano()), "")
|
||||
args["objectName"] = objectName
|
||||
c.TraceOn(os.Stdout)
|
||||
|
||||
cmpChecksum := func(got, want string) {
|
||||
if want != got {
|
||||
@@ -5285,6 +5284,25 @@ func testGetObjectReadAtFunctional() {
|
||||
}
|
||||
offset += 512
|
||||
|
||||
readOffset := 0
|
||||
bufRead := make([]byte, 512)
|
||||
// Read (again) using the regular read function.
|
||||
// Should not have been affected by ReadAt.
|
||||
m, err = io.ReadFull(r, bufRead)
|
||||
if err != nil {
|
||||
logError(testName, function, args, startTime, "", "ReadFull failed", err)
|
||||
return
|
||||
}
|
||||
if m != len(bufRead) {
|
||||
logError(testName, function, args, startTime, "", "ReadFull read shorter bytes before reaching EOF, expected "+string(len(bufRead))+", got "+string(m), err)
|
||||
return
|
||||
}
|
||||
if !bytes.Equal(bufRead, buf[readOffset:readOffset+len(bufRead)]) {
|
||||
logError(testName, function, args, startTime, "", "Incorrect Read from offset", err)
|
||||
return
|
||||
}
|
||||
readOffset += len(bufRead)
|
||||
|
||||
st, err := r.Stat()
|
||||
if err != nil {
|
||||
logError(testName, function, args, startTime, "", "Stat failed", err)
|
||||
@@ -5310,6 +5328,23 @@ func testGetObjectReadAtFunctional() {
|
||||
return
|
||||
}
|
||||
|
||||
// Read (again) using the regular read function.
|
||||
// Should not have been affected by ReadAt.
|
||||
m, err = io.ReadFull(r, bufRead)
|
||||
if err != nil {
|
||||
logError(testName, function, args, startTime, "", "ReadFull (2) failed", err)
|
||||
return
|
||||
}
|
||||
if m != len(bufRead) {
|
||||
logError(testName, function, args, startTime, "", "ReadFull read shorter bytes before reaching EOF", err)
|
||||
return
|
||||
}
|
||||
if !bytes.Equal(bufRead, buf[readOffset:readOffset+len(bufRead)]) {
|
||||
logError(testName, function, args, startTime, "", "Incorrect Read from offset", err)
|
||||
return
|
||||
}
|
||||
readOffset += len(bufRead)
|
||||
|
||||
offset += 512
|
||||
m, err = r.ReadAt(buf3, offset)
|
||||
if err != nil {
|
||||
|
||||
+2
@@ -142,9 +142,11 @@ Class | Method | HTTP request | Description
|
||||
*GroupsApi* | [**CreateGroup**](docs/GroupsApi.md#creategroup) | **Post** /v1.0/groups | Add new entity to groups
|
||||
*GroupsApi* | [**ListGroups**](docs/GroupsApi.md#listgroups) | **Get** /v1.0/groups | Get entities from groups
|
||||
*MeChangepasswordApi* | [**ChangeOwnPassword**](docs/MeChangepasswordApi.md#changeownpassword) | **Post** /v1.0/me/changePassword | Change your own password
|
||||
*MeDriveApi* | [**FollowDriveItem**](docs/MeDriveApi.md#followdriveitem) | **Post** /v1.0/me/drive/items/{item-id}/follow | Follow a DriveItem
|
||||
*MeDriveApi* | [**GetHome**](docs/MeDriveApi.md#gethome) | **Get** /v1.0/me/drive | Get personal space for user
|
||||
*MeDriveApi* | [**ListSharedByMe**](docs/MeDriveApi.md#listsharedbyme) | **Get** /v1beta1/me/drive/sharedByMe | Get a list of driveItem objects shared by the current user.
|
||||
*MeDriveApi* | [**ListSharedWithMe**](docs/MeDriveApi.md#listsharedwithme) | **Get** /v1beta1/me/drive/sharedWithMe | Get a list of driveItem objects shared with the owner of a drive.
|
||||
*MeDriveApi* | [**UnfollowDriveItem**](docs/MeDriveApi.md#unfollowdriveitem) | **Delete** /v1.0/me/drive/following/{item-id} | Unfollow a DriveItem
|
||||
*MeDriveRootApi* | [**HomeGetRoot**](docs/MeDriveRootApi.md#homegetroot) | **Get** /v1.0/me/drive/root | Get root from personal space
|
||||
*MeDriveRootChildrenApi* | [**HomeGetChildren**](docs/MeDriveRootChildrenApi.md#homegetchildren) | **Get** /v1.0/me/drive/root/children | Get children from drive
|
||||
*MeDrivesApi* | [**ListMyDrives**](docs/MeDrivesApi.md#listmydrives) | **Get** /v1.0/me/drives | Get all drives where the current user is a regular member of
|
||||
|
||||
+17
@@ -994,6 +994,13 @@ func (a *EducationSchoolApiService) ListSchoolUsersExecute(r ApiListSchoolUsersR
|
||||
type ApiListSchoolsRequest struct {
|
||||
ctx context.Context
|
||||
ApiService *EducationSchoolApiService
|
||||
filter *string
|
||||
}
|
||||
|
||||
// Filter items by property values. Supports a subset of OData filter expressions. **Supported filters:** - By external ID: `externalId eq 'ext_12345'`
|
||||
func (r ApiListSchoolsRequest) Filter(filter string) ApiListSchoolsRequest {
|
||||
r.filter = &filter
|
||||
return r
|
||||
}
|
||||
|
||||
func (r ApiListSchoolsRequest) Execute() (*CollectionOfSchools, *http.Response, error) {
|
||||
@@ -1003,6 +1010,13 @@ func (r ApiListSchoolsRequest) Execute() (*CollectionOfSchools, *http.Response,
|
||||
/*
|
||||
ListSchools Get a list of schools and their properties
|
||||
|
||||
Retrieves a collection of education schools with optional filtering and ordering.
|
||||
|
||||
**Filtering by external ID:**
|
||||
Use `$filter` to query schools by their external identifier, for example:
|
||||
`$filter=externalId eq 'EX12345'`
|
||||
|
||||
|
||||
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
|
||||
@return ApiListSchoolsRequest
|
||||
*/
|
||||
@@ -1034,6 +1048,9 @@ func (a *EducationSchoolApiService) ListSchoolsExecute(r ApiListSchoolsRequest)
|
||||
localVarQueryParams := url.Values{}
|
||||
localVarFormParams := url.Values{}
|
||||
|
||||
if r.filter != nil {
|
||||
parameterAddToHeaderOrQuery(localVarQueryParams, "$filter", r.filter, "form", "")
|
||||
}
|
||||
// to determine the Content-Type header
|
||||
localVarHTTPContentTypes := []string{}
|
||||
|
||||
|
||||
+1
-1
@@ -165,7 +165,7 @@ See the [ListEducationUsers](#/educationUser/ListEducationUsers) operation for q
|
||||
|
||||
|
||||
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
|
||||
@param userId key: internal user id (UUID format) or username of user. **Note:** If you only have an external ID, first query the user with `GET /graph/v1.0/education/users?$filter=externalId eq '{value}'` to retrieve the internal ID.
|
||||
@param userId key: internal user id (UUID format) or username of user. **Note:** If you only have an external ID, first query the user with `GET /graph/v1.0/education/users?$filter=externalId eq '{value}'` to retrieve the internal ID.
|
||||
@return ApiDeleteEducationUserRequest
|
||||
*/
|
||||
func (a *EducationUserApiService) DeleteEducationUser(ctx context.Context, userId string) ApiDeleteEducationUserRequest {
|
||||
|
||||
+212
@@ -16,12 +16,124 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
||||
// MeDriveApiService MeDriveApi service
|
||||
type MeDriveApiService service
|
||||
|
||||
type ApiFollowDriveItemRequest struct {
|
||||
ctx context.Context
|
||||
ApiService *MeDriveApiService
|
||||
itemId string
|
||||
}
|
||||
|
||||
func (r ApiFollowDriveItemRequest) Execute() (*DriveItem, *http.Response, error) {
|
||||
return r.ApiService.FollowDriveItemExecute(r)
|
||||
}
|
||||
|
||||
/*
|
||||
FollowDriveItem Follow a DriveItem
|
||||
|
||||
Follow a DriveItem.
|
||||
|
||||
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
|
||||
@param itemId key: id of item
|
||||
@return ApiFollowDriveItemRequest
|
||||
*/
|
||||
func (a *MeDriveApiService) FollowDriveItem(ctx context.Context, itemId string) ApiFollowDriveItemRequest {
|
||||
return ApiFollowDriveItemRequest{
|
||||
ApiService: a,
|
||||
ctx: ctx,
|
||||
itemId: itemId,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute executes the request
|
||||
// @return DriveItem
|
||||
func (a *MeDriveApiService) FollowDriveItemExecute(r ApiFollowDriveItemRequest) (*DriveItem, *http.Response, error) {
|
||||
var (
|
||||
localVarHTTPMethod = http.MethodPost
|
||||
localVarPostBody interface{}
|
||||
formFiles []formFile
|
||||
localVarReturnValue *DriveItem
|
||||
)
|
||||
|
||||
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "MeDriveApiService.FollowDriveItem")
|
||||
if err != nil {
|
||||
return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()}
|
||||
}
|
||||
|
||||
localVarPath := localBasePath + "/v1.0/me/drive/items/{item-id}/follow"
|
||||
localVarPath = strings.Replace(localVarPath, "{"+"item-id"+"}", url.PathEscape(parameterValueToString(r.itemId, "itemId")), -1)
|
||||
|
||||
localVarHeaderParams := make(map[string]string)
|
||||
localVarQueryParams := url.Values{}
|
||||
localVarFormParams := url.Values{}
|
||||
|
||||
// to determine the Content-Type header
|
||||
localVarHTTPContentTypes := []string{}
|
||||
|
||||
// set Content-Type header
|
||||
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
|
||||
if localVarHTTPContentType != "" {
|
||||
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
|
||||
}
|
||||
|
||||
// to determine the Accept header
|
||||
localVarHTTPHeaderAccepts := []string{"application/json"}
|
||||
|
||||
// set Accept header
|
||||
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
|
||||
if localVarHTTPHeaderAccept != "" {
|
||||
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
|
||||
}
|
||||
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
|
||||
if err != nil {
|
||||
return localVarReturnValue, nil, err
|
||||
}
|
||||
|
||||
localVarHTTPResponse, err := a.client.callAPI(req)
|
||||
if err != nil || localVarHTTPResponse == nil {
|
||||
return localVarReturnValue, localVarHTTPResponse, err
|
||||
}
|
||||
|
||||
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
|
||||
localVarHTTPResponse.Body.Close()
|
||||
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
|
||||
if err != nil {
|
||||
return localVarReturnValue, localVarHTTPResponse, err
|
||||
}
|
||||
|
||||
if localVarHTTPResponse.StatusCode >= 300 {
|
||||
newErr := &GenericOpenAPIError{
|
||||
body: localVarBody,
|
||||
error: localVarHTTPResponse.Status,
|
||||
}
|
||||
var v OdataError
|
||||
err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
newErr.error = err.Error()
|
||||
return localVarReturnValue, localVarHTTPResponse, newErr
|
||||
}
|
||||
newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
|
||||
newErr.model = v
|
||||
return localVarReturnValue, localVarHTTPResponse, newErr
|
||||
}
|
||||
|
||||
err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
newErr := &GenericOpenAPIError{
|
||||
body: localVarBody,
|
||||
error: err.Error(),
|
||||
}
|
||||
return localVarReturnValue, localVarHTTPResponse, newErr
|
||||
}
|
||||
|
||||
return localVarReturnValue, localVarHTTPResponse, nil
|
||||
}
|
||||
|
||||
type ApiGetHomeRequest struct {
|
||||
ctx context.Context
|
||||
ApiService *MeDriveApiService
|
||||
@@ -362,3 +474,103 @@ func (a *MeDriveApiService) ListSharedWithMeExecute(r ApiListSharedWithMeRequest
|
||||
|
||||
return localVarReturnValue, localVarHTTPResponse, nil
|
||||
}
|
||||
|
||||
type ApiUnfollowDriveItemRequest struct {
|
||||
ctx context.Context
|
||||
ApiService *MeDriveApiService
|
||||
itemId string
|
||||
}
|
||||
|
||||
func (r ApiUnfollowDriveItemRequest) Execute() (*http.Response, error) {
|
||||
return r.ApiService.UnfollowDriveItemExecute(r)
|
||||
}
|
||||
|
||||
/*
|
||||
UnfollowDriveItem Unfollow a DriveItem
|
||||
|
||||
Unfollow a DriveItem.
|
||||
|
||||
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
|
||||
@param itemId key: id of item
|
||||
@return ApiUnfollowDriveItemRequest
|
||||
*/
|
||||
func (a *MeDriveApiService) UnfollowDriveItem(ctx context.Context, itemId string) ApiUnfollowDriveItemRequest {
|
||||
return ApiUnfollowDriveItemRequest{
|
||||
ApiService: a,
|
||||
ctx: ctx,
|
||||
itemId: itemId,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute executes the request
|
||||
func (a *MeDriveApiService) UnfollowDriveItemExecute(r ApiUnfollowDriveItemRequest) (*http.Response, error) {
|
||||
var (
|
||||
localVarHTTPMethod = http.MethodDelete
|
||||
localVarPostBody interface{}
|
||||
formFiles []formFile
|
||||
)
|
||||
|
||||
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "MeDriveApiService.UnfollowDriveItem")
|
||||
if err != nil {
|
||||
return nil, &GenericOpenAPIError{error: err.Error()}
|
||||
}
|
||||
|
||||
localVarPath := localBasePath + "/v1.0/me/drive/following/{item-id}"
|
||||
localVarPath = strings.Replace(localVarPath, "{"+"item-id"+"}", url.PathEscape(parameterValueToString(r.itemId, "itemId")), -1)
|
||||
|
||||
localVarHeaderParams := make(map[string]string)
|
||||
localVarQueryParams := url.Values{}
|
||||
localVarFormParams := url.Values{}
|
||||
|
||||
// to determine the Content-Type header
|
||||
localVarHTTPContentTypes := []string{}
|
||||
|
||||
// set Content-Type header
|
||||
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
|
||||
if localVarHTTPContentType != "" {
|
||||
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
|
||||
}
|
||||
|
||||
// to determine the Accept header
|
||||
localVarHTTPHeaderAccepts := []string{"application/json"}
|
||||
|
||||
// set Accept header
|
||||
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
|
||||
if localVarHTTPHeaderAccept != "" {
|
||||
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
|
||||
}
|
||||
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
localVarHTTPResponse, err := a.client.callAPI(req)
|
||||
if err != nil || localVarHTTPResponse == nil {
|
||||
return localVarHTTPResponse, err
|
||||
}
|
||||
|
||||
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
|
||||
localVarHTTPResponse.Body.Close()
|
||||
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
|
||||
if err != nil {
|
||||
return localVarHTTPResponse, err
|
||||
}
|
||||
|
||||
if localVarHTTPResponse.StatusCode >= 300 {
|
||||
newErr := &GenericOpenAPIError{
|
||||
body: localVarBody,
|
||||
error: localVarHTTPResponse.Status,
|
||||
}
|
||||
var v OdataError
|
||||
err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
newErr.error = err.Error()
|
||||
return localVarHTTPResponse, newErr
|
||||
}
|
||||
newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
|
||||
newErr.model = v
|
||||
return localVarHTTPResponse, newErr
|
||||
}
|
||||
|
||||
return localVarHTTPResponse, nil
|
||||
}
|
||||
|
||||
+4
-4
@@ -140,7 +140,7 @@ type ApiListPermissionRoleDefinitionsRequest struct {
|
||||
ApiService *RoleManagementApiService
|
||||
}
|
||||
|
||||
func (r ApiListPermissionRoleDefinitionsRequest) Execute() (*UnifiedRoleDefinition, *http.Response, error) {
|
||||
func (r ApiListPermissionRoleDefinitionsRequest) Execute() ([]UnifiedRoleDefinition, *http.Response, error) {
|
||||
return r.ApiService.ListPermissionRoleDefinitionsExecute(r)
|
||||
}
|
||||
|
||||
@@ -161,13 +161,13 @@ func (a *RoleManagementApiService) ListPermissionRoleDefinitions(ctx context.Con
|
||||
}
|
||||
|
||||
// Execute executes the request
|
||||
// @return UnifiedRoleDefinition
|
||||
func (a *RoleManagementApiService) ListPermissionRoleDefinitionsExecute(r ApiListPermissionRoleDefinitionsRequest) (*UnifiedRoleDefinition, *http.Response, error) {
|
||||
// @return []UnifiedRoleDefinition
|
||||
func (a *RoleManagementApiService) ListPermissionRoleDefinitionsExecute(r ApiListPermissionRoleDefinitionsRequest) ([]UnifiedRoleDefinition, *http.Response, error) {
|
||||
var (
|
||||
localVarHTTPMethod = http.MethodGet
|
||||
localVarPostBody interface{}
|
||||
formFiles []formFile
|
||||
localVarReturnValue *UnifiedRoleDefinition
|
||||
localVarReturnValue []UnifiedRoleDefinition
|
||||
)
|
||||
|
||||
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "RoleManagementApiService.ListPermissionRoleDefinitions")
|
||||
|
||||
-2
@@ -32,7 +32,6 @@ import (
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/appauth/manager/loader"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/loader"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/auth/registry/loader"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/cbox/loader"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/datatx/manager/loader"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/group/manager/loader"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/metrics/driver/loader"
|
||||
@@ -45,7 +44,6 @@ import (
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/rhttp/datatx/manager/loader"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/share/cache/warmup/loader"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/share/manager/loader"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/storage/favorite/loader"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/storage/fs/loader"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/storage/registry/loader"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/token/manager/loader"
|
||||
|
||||
Generated
Vendored
+40
@@ -648,6 +648,46 @@ func (s *svc) CreateContainer(ctx context.Context, req *provider.CreateContainer
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *svc) AddFavorite(ctx context.Context, req *provider.AddFavoriteRequest) (*provider.AddFavoriteResponse, error) {
|
||||
var c provider.ProviderAPIClient
|
||||
var err error
|
||||
c, _, req.Ref, err = s.findAndUnwrap(ctx, req.Ref)
|
||||
if err != nil {
|
||||
return &provider.AddFavoriteResponse{
|
||||
Status: status.NewStatusFromErrType(ctx, fmt.Sprintf("gateway could not find space for ref=%+v", req.Ref), err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
res, err := c.AddFavorite(ctx, req)
|
||||
if err != nil {
|
||||
return &provider.AddFavoriteResponse{
|
||||
Status: status.NewStatusFromErrType(ctx, "gateway could not call AddFavorite", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *svc) RemoveFavorite(ctx context.Context, req *provider.RemoveFavoriteRequest) (*provider.RemoveFavoriteResponse, error) {
|
||||
var c provider.ProviderAPIClient
|
||||
var err error
|
||||
c, _, req.Ref, err = s.findAndUnwrap(ctx, req.Ref)
|
||||
if err != nil {
|
||||
return &provider.RemoveFavoriteResponse{
|
||||
Status: status.NewStatusFromErrType(ctx, fmt.Sprintf("gateway could not find space for ref=%+v", req.Ref), err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
res, err := c.RemoveFavorite(ctx, req)
|
||||
if err != nil {
|
||||
return &provider.RemoveFavoriteResponse{
|
||||
Status: status.NewStatusFromErrType(ctx, "gateway could not call RemoveFavorite", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *svc) TouchFile(ctx context.Context, req *provider.TouchFileRequest) (*provider.TouchFileResponse, error) {
|
||||
var c provider.ProviderAPIClient
|
||||
var err error
|
||||
|
||||
Generated
Vendored
+6
@@ -264,3 +264,9 @@ func (c *cachedAPIClient) GetHome(ctx context.Context, in *provider.GetHomeReque
|
||||
func (c *cachedAPIClient) TouchFile(ctx context.Context, in *provider.TouchFileRequest, opts ...grpc.CallOption) (*provider.TouchFileResponse, error) {
|
||||
return c.c.TouchFile(ctx, in, opts...)
|
||||
}
|
||||
func (c *cachedAPIClient) AddFavorite(ctx context.Context, in *provider.AddFavoriteRequest, opts ...grpc.CallOption) (*provider.AddFavoriteResponse, error) {
|
||||
return c.c.AddFavorite(ctx, in, opts...)
|
||||
}
|
||||
func (c *cachedAPIClient) RemoveFavorite(ctx context.Context, in *provider.RemoveFavoriteRequest, opts ...grpc.CallOption) (*provider.RemoveFavoriteResponse, error) {
|
||||
return c.c.RemoveFavorite(ctx, in, opts...)
|
||||
}
|
||||
|
||||
Generated
Vendored
+8
@@ -929,6 +929,14 @@ func (s *service) GetQuota(ctx context.Context, req *provider.GetQuotaRequest) (
|
||||
return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented")
|
||||
}
|
||||
|
||||
func (s *service) AddFavorite(ctx context.Context, req *provider.AddFavoriteRequest) (*provider.AddFavoriteResponse, error) {
|
||||
return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented")
|
||||
}
|
||||
|
||||
func (s *service) RemoveFavorite(ctx context.Context, req *provider.RemoveFavoriteRequest) (*provider.RemoveFavoriteResponse, error) {
|
||||
return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented")
|
||||
}
|
||||
|
||||
// resolveToken returns the resource info for the publicly shared resource.
|
||||
func (s *service) resolveToken(ctx context.Context, share interface{}) (*provider.ResourceInfo, interface{}, error) {
|
||||
gatewayClient, err := s.gatewaySelector.Next()
|
||||
|
||||
Generated
Vendored
+8
@@ -1035,6 +1035,14 @@ func (s *service) GetQuota(ctx context.Context, req *provider.GetQuotaRequest) (
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *service) AddFavorite(ctx context.Context, req *provider.AddFavoriteRequest) (*provider.AddFavoriteResponse, error) {
|
||||
return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented")
|
||||
}
|
||||
|
||||
func (s *service) RemoveFavorite(ctx context.Context, req *provider.RemoveFavoriteRequest) (*provider.RemoveFavoriteResponse, error) {
|
||||
return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented")
|
||||
}
|
||||
|
||||
func (s *service) resolveAcceptedShare(ctx context.Context, ref *provider.Reference) (*collaboration.ReceivedShare, *rpc.Status, error) {
|
||||
// treat absolute id based references as relative ones
|
||||
if ref.Path == "" {
|
||||
|
||||
Generated
Vendored
+30
@@ -706,6 +706,36 @@ func (s *Service) TouchFile(ctx context.Context, req *provider.TouchFileRequest)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) AddFavorite(ctx context.Context, req *provider.AddFavoriteRequest) (*provider.AddFavoriteResponse, error) {
|
||||
appctx.GetLogger(ctx).Debug().Msg("AddFavorite")
|
||||
|
||||
err := s.Storage.AddFavorite(ctx, req.Ref, req.UserId)
|
||||
if err != nil {
|
||||
return &provider.AddFavoriteResponse{
|
||||
Status: status.NewStatusFromErrType(ctx, "add favorite", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &provider.AddFavoriteResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) RemoveFavorite(ctx context.Context, req *provider.RemoveFavoriteRequest) (*provider.RemoveFavoriteResponse, error) {
|
||||
appctx.GetLogger(ctx).Debug().Msg("RemoveFavorite")
|
||||
|
||||
err := s.Storage.RemoveFavorite(ctx, req.Ref, req.UserId)
|
||||
if err != nil {
|
||||
return &provider.RemoveFavoriteResponse{
|
||||
Status: status.NewStatusFromErrType(ctx, "remove favorite", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &provider.RemoveFavoriteResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) Delete(ctx context.Context, req *provider.DeleteRequest) (*provider.DeleteResponse, error) {
|
||||
if req.Ref.GetPath() == "/" {
|
||||
return &provider.DeleteResponse{
|
||||
|
||||
Generated
Vendored
+9
-15
@@ -21,17 +21,15 @@ type Config struct {
|
||||
Timeout int64 `mapstructure:"timeout"`
|
||||
Insecure bool `mapstructure:"insecure"`
|
||||
// If true, HTTP COPY will expect the HTTP-TPC (third-party copy) headers
|
||||
EnableHTTPTpc bool `mapstructure:"enable_http_tpc"`
|
||||
PublicURL string `mapstructure:"public_url"`
|
||||
FavoriteStorageDriver string `mapstructure:"favorite_storage_driver"`
|
||||
FavoriteStorageDrivers map[string]map[string]interface{} `mapstructure:"favorite_storage_drivers"`
|
||||
Version string `mapstructure:"version"`
|
||||
VersionString string `mapstructure:"version_string"`
|
||||
Edition string `mapstructure:"edition"`
|
||||
Product string `mapstructure:"product"`
|
||||
ProductName string `mapstructure:"product_name"`
|
||||
ProductVersion string `mapstructure:"product_version"`
|
||||
AllowPropfindDepthInfinitiy bool `mapstructure:"allow_depth_infinity"`
|
||||
EnableHTTPTpc bool `mapstructure:"enable_http_tpc"`
|
||||
PublicURL string `mapstructure:"public_url"`
|
||||
Version string `mapstructure:"version"`
|
||||
VersionString string `mapstructure:"version_string"`
|
||||
Edition string `mapstructure:"edition"`
|
||||
Product string `mapstructure:"product"`
|
||||
ProductName string `mapstructure:"product_name"`
|
||||
ProductVersion string `mapstructure:"product_version"`
|
||||
AllowPropfindDepthInfinitiy bool `mapstructure:"allow_depth_infinity"`
|
||||
|
||||
NameValidation NameValidation `mapstructure:"validation"`
|
||||
|
||||
@@ -52,10 +50,6 @@ func (c *Config) Init() {
|
||||
// note: default c.Prefix is an empty string
|
||||
c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc)
|
||||
|
||||
if c.FavoriteStorageDriver == "" {
|
||||
c.FavoriteStorageDriver = "memory"
|
||||
}
|
||||
|
||||
if c.Version == "" {
|
||||
c.Version = "10.0.11.5"
|
||||
}
|
||||
|
||||
Generated
Vendored
+7
-21
@@ -43,8 +43,6 @@ import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rhttp/router"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/signedurl"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/favorite"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/favorite/registry"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/templates"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/rs/zerolog"
|
||||
@@ -61,12 +59,11 @@ func init() {
|
||||
}
|
||||
|
||||
type svc struct {
|
||||
c *config.Config
|
||||
webDavHandler *WebDavHandler
|
||||
davHandler *DavHandler
|
||||
favoritesManager favorite.Manager
|
||||
client *http.Client
|
||||
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
|
||||
c *config.Config
|
||||
webDavHandler *WebDavHandler
|
||||
davHandler *DavHandler
|
||||
client *http.Client
|
||||
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
|
||||
// LockSystem is the lock management system.
|
||||
LockSystem LockSystem
|
||||
userIdentifierCache *ttlcache.Cache
|
||||
@@ -78,12 +75,6 @@ func (s *svc) Config() *config.Config {
|
||||
return s.c
|
||||
}
|
||||
|
||||
func getFavoritesManager(c *config.Config) (favorite.Manager, error) {
|
||||
if f, ok := registry.NewFuncs[c.FavoriteStorageDriver]; ok {
|
||||
return f(c.FavoriteStorageDrivers[c.FavoriteStorageDriver])
|
||||
}
|
||||
return nil, errtypes.NotFound("driver not found: " + c.FavoriteStorageDriver)
|
||||
}
|
||||
func getLockSystem(c *config.Config) (LockSystem, error) {
|
||||
// TODO in memory implementation
|
||||
selector, err := pool.GatewaySelector(c.GatewaySvc)
|
||||
@@ -102,20 +93,16 @@ func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error)
|
||||
|
||||
conf.Init()
|
||||
|
||||
fm, err := getFavoritesManager(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ls, err := getLockSystem(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewWith(conf, fm, ls, log, nil)
|
||||
return NewWith(conf, ls, log, nil)
|
||||
}
|
||||
|
||||
// NewWith returns a new ocdav service
|
||||
func NewWith(conf *config.Config, fm favorite.Manager, ls LockSystem, _ *zerolog.Logger, selector pool.Selectable[gateway.GatewayAPIClient]) (global.Service, error) {
|
||||
func NewWith(conf *config.Config, ls LockSystem, _ *zerolog.Logger, selector pool.Selectable[gateway.GatewayAPIClient]) (global.Service, error) {
|
||||
// be safe - init the conf again
|
||||
conf.Init()
|
||||
|
||||
@@ -137,7 +124,6 @@ func NewWith(conf *config.Config, fm favorite.Manager, ls LockSystem, _ *zerolog
|
||||
rhttp.Insecure(conf.Insecure),
|
||||
),
|
||||
gatewaySelector: selector,
|
||||
favoritesManager: fm,
|
||||
LockSystem: ls,
|
||||
userIdentifierCache: ttlcache.NewCache(),
|
||||
nameValidators: ValidatorsFromConfig(conf),
|
||||
|
||||
Generated
Vendored
-52
@@ -36,11 +36,8 @@ import (
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/propfind"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/spacelookup"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/permission"
|
||||
rstatus "github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
@@ -214,30 +211,6 @@ func (s *svc) handleProppatch(ctx context.Context, w http.ResponseWriter, r *htt
|
||||
errors.HandleWebdavError(&log, w, b, err)
|
||||
return nil, nil, false
|
||||
}
|
||||
if key == "http://owncloud.org/ns/favorite" {
|
||||
statRes, err := client.Stat(ctx, &provider.StatRequest{Ref: ref})
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil, nil, false
|
||||
}
|
||||
currentUser := ctxpkg.ContextMustGetUser(ctx)
|
||||
ok, err := utils.CheckPermission(ctx, permission.WriteFavorites, client)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error checking permission")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil, nil, false
|
||||
}
|
||||
if !ok {
|
||||
log.Info().Interface("user", currentUser).Msg("user not allowed to unset favorite")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return nil, nil, false
|
||||
}
|
||||
err = s.favoritesManager.UnsetFavorite(ctx, currentUser.Id, statRes.Info)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil, nil, false
|
||||
}
|
||||
}
|
||||
removedProps = append(removedProps, propNameXML)
|
||||
} else {
|
||||
sreq.ArbitraryMetadata.Metadata[key] = value
|
||||
@@ -282,31 +255,6 @@ func (s *svc) handleProppatch(ctx context.Context, w http.ResponseWriter, r *htt
|
||||
|
||||
acceptedProps = append(acceptedProps, propNameXML)
|
||||
delete(sreq.ArbitraryMetadata.Metadata, key)
|
||||
|
||||
if key == "http://owncloud.org/ns/favorite" {
|
||||
statRes, err := client.Stat(ctx, &provider.StatRequest{Ref: ref})
|
||||
if err != nil || statRes.Info == nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil, nil, false
|
||||
}
|
||||
currentUser := ctxpkg.ContextMustGetUser(ctx)
|
||||
ok, err := utils.CheckPermission(ctx, permission.WriteFavorites, client)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error checking permission")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil, nil, false
|
||||
}
|
||||
if !ok {
|
||||
log.Info().Interface("user", currentUser).Msg("user not allowed to set favorite")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return nil, nil, false
|
||||
}
|
||||
err = s.favoritesManager.SetFavorite(ctx, currentUser.Id, statRes.Info)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil, nil, false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// FIXME: in case of error, need to set all properties back to the original state,
|
||||
|
||||
Generated
Vendored
-80
@@ -23,14 +23,8 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/propfind"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/permission"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -54,13 +48,6 @@ func (s *svc) handleReport(w http.ResponseWriter, r *http.Request, ns string) {
|
||||
return
|
||||
}
|
||||
|
||||
if rep.FilterFiles != nil {
|
||||
s.doFilterFiles(w, r, rep.FilterFiles, ns)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(jfd): implement report
|
||||
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
@@ -68,73 +55,6 @@ func (s *svc) doSearchFiles(w http.ResponseWriter, r *http.Request, sf *reportSe
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
func (s *svc) doFilterFiles(w http.ResponseWriter, r *http.Request, ff *reportFilterFiles, namespace string) {
|
||||
ctx := r.Context()
|
||||
log := appctx.GetLogger(ctx)
|
||||
|
||||
if ff.Rules.Favorite {
|
||||
// List the users favorite resources.
|
||||
client, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error selecting next gateway client")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
currentUser := ctxpkg.ContextMustGetUser(ctx)
|
||||
ok, err := utils.CheckPermission(ctx, permission.ListFavorites, client)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error checking permission")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
log.Info().Interface("user", currentUser).Msg("user not allowed to list favorites")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
favorites, err := s.favoritesManager.ListFavorites(ctx, currentUser.Id)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error getting favorites")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
infos := make([]*providerv1beta1.ResourceInfo, 0, len(favorites))
|
||||
for i := range favorites {
|
||||
statRes, err := client.Stat(ctx, &providerv1beta1.StatRequest{Ref: &providerv1beta1.Reference{ResourceId: favorites[i]}})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error getting resource info")
|
||||
continue
|
||||
}
|
||||
if statRes.Status.Code != rpcv1beta1.Code_CODE_OK {
|
||||
log.Error().Interface("stat_response", statRes).Msg("error getting resource info")
|
||||
continue
|
||||
}
|
||||
infos = append(infos, statRes.Info)
|
||||
}
|
||||
|
||||
prefer := net.ParsePrefer(r.Header.Get("prefer"))
|
||||
returnMinimal := prefer[net.HeaderPreferReturn] == "minimal"
|
||||
|
||||
responsesXML, err := propfind.MultistatusResponse(ctx, &propfind.XML{Prop: ff.Prop}, infos, s.c.PublicURL, namespace, nil, returnMinimal, nil)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error formatting propfind")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set(net.HeaderDav, "1, 3, extended-mkcol")
|
||||
w.Header().Set(net.HeaderContentType, "application/xml; charset=utf-8")
|
||||
w.Header().Set(net.HeaderVary, net.HeaderPrefer)
|
||||
if returnMinimal {
|
||||
w.Header().Set(net.HeaderPreferenceApplied, "return=minimal")
|
||||
}
|
||||
w.WriteHeader(http.StatusMultiStatus)
|
||||
if _, err := w.Write(responsesXML); err != nil {
|
||||
log.Err(err).Msg("error writing response")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type report struct {
|
||||
SearchFiles *reportSearchFiles
|
||||
// FilterFiles TODO add this for tag based search
|
||||
|
||||
-139
@@ -1,139 +0,0 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package cbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/cbox/utils"
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/favorite"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/favorite/registry"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register("sql", New)
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
type mgr struct {
|
||||
c *config
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// New returns an instance of the cbox sql favorites manager.
|
||||
func New(m map[string]interface{}) (favorite.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 &mgr{
|
||||
c: c,
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *mgr) ListFavorites(ctx context.Context, userID *user.UserId) ([]*provider.ResourceId, error) {
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
infos := []*provider.ResourceId{}
|
||||
query := `SELECT fileid_prefix, fileid FROM cbox_metadata WHERE uid=? AND tag_key="fav"`
|
||||
rows, err := m.db.Query(query, user.Id.OpaqueId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var info provider.ResourceId
|
||||
if err := rows.Scan(&info.SpaceId, &info.OpaqueId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
infos = append(infos, &info)
|
||||
}
|
||||
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return infos, nil
|
||||
}
|
||||
|
||||
func (m *mgr) SetFavorite(ctx context.Context, userID *user.UserId, resourceInfo *provider.ResourceInfo) error {
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
spaceID := resourceInfo.Id.SpaceId
|
||||
|
||||
// The primary key is just the ID in the table, it should ideally be (uid, fileid_prefix, fileid, tag_key)
|
||||
// For the time being, just check if the favorite already exists. If it does, return early
|
||||
var id int
|
||||
query := `SELECT id FROM cbox_metadata WHERE uid=? AND fileid_prefix=? AND fileid=? AND tag_key="fav"`
|
||||
if err := m.db.QueryRow(query, user.Id.OpaqueId, spaceID, resourceInfo.Id.OpaqueId).Scan(&id); err == nil {
|
||||
// Favorite is already set, return
|
||||
return nil
|
||||
}
|
||||
|
||||
query = `INSERT INTO cbox_metadata SET item_type=?, uid=?, fileid_prefix=?, fileid=?, tag_key="fav"`
|
||||
vals := []interface{}{utils.ResourceTypeToItemInt(resourceInfo.Type), user.Id.OpaqueId, spaceID, resourceInfo.Id.OpaqueId}
|
||||
stmt, err := m.db.Prepare(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = stmt.Exec(vals...); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mgr) UnsetFavorite(ctx context.Context, userID *user.UserId, resourceInfo *provider.ResourceInfo) error {
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
spaceID := resourceInfo.Id.SpaceId
|
||||
|
||||
stmt, err := m.db.Prepare(`DELETE FROM cbox_metadata WHERE uid=? AND fileid_prefix=? AND fileid=? AND tag_key="fav"`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := stmt.Exec(user.Id.OpaqueId, spaceID, resourceInfo.Id.OpaqueId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
-221
@@ -1,221 +0,0 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package rest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"github.com/gomodule/redigo/redis"
|
||||
)
|
||||
|
||||
const (
|
||||
groupPrefix = "group:"
|
||||
idPrefix = "id:"
|
||||
namePrefix = "name:"
|
||||
gidPrefix = "gid:"
|
||||
groupMembersPrefix = "members:"
|
||||
groupInternalIDPrefix = "internal:"
|
||||
)
|
||||
|
||||
func initRedisPool(address, username, password string) *redis.Pool {
|
||||
return &redis.Pool{
|
||||
|
||||
MaxIdle: 50,
|
||||
MaxActive: 1000,
|
||||
IdleTimeout: 240 * time.Second,
|
||||
|
||||
Dial: func() (redis.Conn, error) {
|
||||
var c redis.Conn
|
||||
var err error
|
||||
switch {
|
||||
case username != "":
|
||||
c, err = redis.Dial("tcp", address,
|
||||
redis.DialUsername(username),
|
||||
redis.DialPassword(password),
|
||||
)
|
||||
case password != "":
|
||||
c, err = redis.Dial("tcp", address,
|
||||
redis.DialPassword(password),
|
||||
)
|
||||
default:
|
||||
c, err = redis.Dial("tcp", address)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, err
|
||||
},
|
||||
|
||||
TestOnBorrow: func(c redis.Conn, t time.Time) error {
|
||||
_, err := c.Do("PING")
|
||||
return err
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *manager) setVal(key, val string, expiration int) error {
|
||||
conn := m.redisPool.Get()
|
||||
defer conn.Close()
|
||||
if conn != nil {
|
||||
args := []interface{}{key, val}
|
||||
if expiration != -1 {
|
||||
args = append(args, "EX", expiration)
|
||||
}
|
||||
if _, err := conn.Do("SET", args...); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return errors.New("rest: unable to get connection from redis pool")
|
||||
}
|
||||
|
||||
func (m *manager) getVal(key string) (string, error) {
|
||||
conn := m.redisPool.Get()
|
||||
defer conn.Close()
|
||||
if conn != nil {
|
||||
val, err := redis.String(conn.Do("GET", key))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
return "", errors.New("rest: unable to get connection from redis pool")
|
||||
}
|
||||
|
||||
func (m *manager) fetchCachedInternalID(gid *grouppb.GroupId) (string, error) {
|
||||
return m.getVal(groupPrefix + groupInternalIDPrefix + gid.OpaqueId)
|
||||
}
|
||||
|
||||
func (m *manager) cacheInternalID(gid *grouppb.GroupId, internalID string) error {
|
||||
return m.setVal(groupPrefix+groupInternalIDPrefix+gid.OpaqueId, internalID, -1)
|
||||
}
|
||||
|
||||
func (m *manager) findCachedGroups(query string) ([]*grouppb.Group, error) {
|
||||
conn := m.redisPool.Get()
|
||||
defer conn.Close()
|
||||
if conn != nil {
|
||||
query = fmt.Sprintf("%s*%s*", groupPrefix, strings.ReplaceAll(strings.ToLower(query), " ", "_"))
|
||||
keys, err := redis.Strings(conn.Do("KEYS", query))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var args []interface{}
|
||||
for _, k := range keys {
|
||||
args = append(args, k)
|
||||
}
|
||||
|
||||
// Fetch the groups for all these keys
|
||||
groupStrings, err := redis.Strings(conn.Do("MGET", args...))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
groupMap := make(map[string]*grouppb.Group)
|
||||
for _, group := range groupStrings {
|
||||
g := grouppb.Group{}
|
||||
if err = json.Unmarshal([]byte(group), &g); err == nil {
|
||||
groupMap[g.Id.OpaqueId] = &g
|
||||
}
|
||||
}
|
||||
|
||||
var groups []*grouppb.Group
|
||||
for _, g := range groupMap {
|
||||
groups = append(groups, g)
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("rest: unable to get connection from redis pool")
|
||||
}
|
||||
|
||||
func (m *manager) fetchCachedGroupDetails(gid *grouppb.GroupId) (*grouppb.Group, error) {
|
||||
group, err := m.getVal(groupPrefix + idPrefix + gid.OpaqueId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
g := grouppb.Group{}
|
||||
if err = json.Unmarshal([]byte(group), &g); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &g, nil
|
||||
}
|
||||
|
||||
func (m *manager) cacheGroupDetails(g *grouppb.Group) error {
|
||||
encodedGroup, err := json.Marshal(&g)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = m.setVal(groupPrefix+idPrefix+strings.ToLower(g.Id.OpaqueId), string(encodedGroup), -1); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if g.GidNumber != 0 {
|
||||
if err = m.setVal(groupPrefix+gidPrefix+strconv.FormatInt(g.GidNumber, 10), g.Id.OpaqueId, -1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if g.DisplayName != "" {
|
||||
if err = m.setVal(groupPrefix+namePrefix+g.Id.OpaqueId+"_"+strings.ToLower(g.DisplayName), g.Id.OpaqueId, -1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *manager) fetchCachedGroupByParam(field, claim string) (*grouppb.Group, error) {
|
||||
group, err := m.getVal(groupPrefix + field + ":" + strings.ToLower(claim))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
g := grouppb.Group{}
|
||||
if err = json.Unmarshal([]byte(group), &g); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &g, nil
|
||||
}
|
||||
|
||||
func (m *manager) fetchCachedGroupMembers(gid *grouppb.GroupId) ([]*userpb.UserId, error) {
|
||||
members, err := m.getVal(groupPrefix + groupMembersPrefix + strings.ToLower(gid.OpaqueId))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u := []*userpb.UserId{}
|
||||
if err = json.Unmarshal([]byte(members), &u); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (m *manager) cacheGroupMembers(gid *grouppb.GroupId, members []*userpb.UserId) error {
|
||||
u, err := json.Marshal(&members)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.setVal(groupPrefix+groupMembersPrefix+strings.ToLower(gid.OpaqueId), string(u), m.conf.GroupMembersCacheExpiration*60)
|
||||
}
|
||||
-329
@@ -1,329 +0,0 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"github.com/gomodule/redigo/redis"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
utils "github.com/opencloud-eu/reva/v2/pkg/cbox/utils"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/group"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/group/manager/registry"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register("rest", New)
|
||||
}
|
||||
|
||||
type manager struct {
|
||||
conf *config
|
||||
redisPool *redis.Pool
|
||||
apiTokenManager *utils.APITokenManager
|
||||
}
|
||||
|
||||
type config struct {
|
||||
// The address at which the redis server is running
|
||||
RedisAddress string `mapstructure:"redis_address" docs:"localhost:6379"`
|
||||
// The username for connecting to the redis server
|
||||
RedisUsername string `mapstructure:"redis_username" docs:""`
|
||||
// The password for connecting to the redis server
|
||||
RedisPassword string `mapstructure:"redis_password" docs:""`
|
||||
// The time in minutes for which the members of a group would be cached
|
||||
GroupMembersCacheExpiration int `mapstructure:"group_members_cache_expiration" docs:"5"`
|
||||
// The OIDC Provider
|
||||
IDProvider string `mapstructure:"id_provider" docs:"http://cernbox.cern.ch"`
|
||||
// Base API Endpoint
|
||||
APIBaseURL string `mapstructure:"api_base_url" docs:"https://authorization-service-api-dev.web.cern.ch"`
|
||||
// Client ID needed to authenticate
|
||||
ClientID string `mapstructure:"client_id" docs:"-"`
|
||||
// Client Secret
|
||||
ClientSecret string `mapstructure:"client_secret" docs:"-"`
|
||||
|
||||
// Endpoint to generate token to access the API
|
||||
OIDCTokenEndpoint string `mapstructure:"oidc_token_endpoint" docs:"https://keycloak-dev.cern.ch/auth/realms/cern/api-access/token"`
|
||||
// The target application for which token needs to be generated
|
||||
TargetAPI string `mapstructure:"target_api" docs:"authorization-service-api"`
|
||||
// The time in seconds between bulk fetch of groups
|
||||
GroupFetchInterval int `mapstructure:"group_fetch_interval" docs:"3600"`
|
||||
}
|
||||
|
||||
func (c *config) init() {
|
||||
if c.GroupMembersCacheExpiration == 0 {
|
||||
c.GroupMembersCacheExpiration = 5
|
||||
}
|
||||
if c.RedisAddress == "" {
|
||||
c.RedisAddress = ":6379"
|
||||
}
|
||||
if c.APIBaseURL == "" {
|
||||
c.APIBaseURL = "https://authorization-service-api-dev.web.cern.ch"
|
||||
}
|
||||
if c.TargetAPI == "" {
|
||||
c.TargetAPI = "authorization-service-api"
|
||||
}
|
||||
if c.OIDCTokenEndpoint == "" {
|
||||
c.OIDCTokenEndpoint = "https://keycloak-dev.cern.ch/auth/realms/cern/api-access/token"
|
||||
}
|
||||
if c.IDProvider == "" {
|
||||
c.IDProvider = "http://cernbox.cern.ch"
|
||||
}
|
||||
if c.GroupFetchInterval == 0 {
|
||||
c.GroupFetchInterval = 3600
|
||||
}
|
||||
}
|
||||
|
||||
func parseConfig(m map[string]interface{}) (*config, error) {
|
||||
c := &config{}
|
||||
if err := mapstructure.Decode(m, c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// New returns a user manager implementation that makes calls to the GRAPPA API.
|
||||
func New(m map[string]interface{}) (group.Manager, error) {
|
||||
c, err := parseConfig(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.init()
|
||||
|
||||
redisPool := initRedisPool(c.RedisAddress, c.RedisUsername, c.RedisPassword)
|
||||
apiTokenManager := utils.InitAPITokenManager(c.TargetAPI, c.OIDCTokenEndpoint, c.ClientID, c.ClientSecret)
|
||||
|
||||
mgr := &manager{
|
||||
conf: c,
|
||||
redisPool: redisPool,
|
||||
apiTokenManager: apiTokenManager,
|
||||
}
|
||||
go mgr.fetchAllGroups()
|
||||
return mgr, nil
|
||||
}
|
||||
|
||||
func (m *manager) fetchAllGroups() {
|
||||
_ = m.fetchAllGroupAccounts()
|
||||
ticker := time.NewTicker(time.Duration(m.conf.GroupFetchInterval) * time.Second)
|
||||
work := make(chan os.Signal, 1)
|
||||
signal.Notify(work, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-work:
|
||||
return
|
||||
case <-ticker.C:
|
||||
_ = m.fetchAllGroupAccounts()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *manager) fetchAllGroupAccounts() error {
|
||||
ctx := context.Background()
|
||||
url := fmt.Sprintf("%s/api/v1.0/Group?field=groupIdentifier&field=displayName&field=gid", m.conf.APIBaseURL)
|
||||
|
||||
for url != "" {
|
||||
result, err := m.apiTokenManager.SendAPIGetRequest(ctx, url, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
responseData, ok := result["data"].([]interface{})
|
||||
if !ok {
|
||||
return errors.New("rest: error in type assertion")
|
||||
}
|
||||
for _, usr := range responseData {
|
||||
groupData, ok := usr.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = m.parseAndCacheGroup(ctx, groupData)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
url = ""
|
||||
if pagination, ok := result["pagination"].(map[string]interface{}); ok {
|
||||
if links, ok := pagination["links"].(map[string]interface{}); ok {
|
||||
if next, ok := links["next"].(string); ok {
|
||||
url = fmt.Sprintf("%s%s", m.conf.APIBaseURL, next)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *manager) parseAndCacheGroup(ctx context.Context, groupData map[string]interface{}) (*grouppb.Group, error) {
|
||||
id, ok := groupData["groupIdentifier"].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("rest: missing upn in user data")
|
||||
}
|
||||
|
||||
name, _ := groupData["displayName"].(string)
|
||||
groupID := &grouppb.GroupId{
|
||||
OpaqueId: id,
|
||||
Idp: m.conf.IDProvider,
|
||||
}
|
||||
gid, ok := groupData["gid"].(int64)
|
||||
if !ok {
|
||||
gid = 0
|
||||
}
|
||||
g := &grouppb.Group{
|
||||
Id: groupID,
|
||||
GroupName: id,
|
||||
Mail: id + "@cern.ch",
|
||||
DisplayName: name,
|
||||
GidNumber: gid,
|
||||
}
|
||||
|
||||
if err := m.cacheGroupDetails(g); err != nil {
|
||||
log.Error().Err(err).Msg("rest: error caching group details")
|
||||
}
|
||||
|
||||
if internalID, ok := groupData["id"].(string); ok {
|
||||
if err := m.cacheInternalID(groupID, internalID); err != nil {
|
||||
log.Error().Err(err).Msg("rest: error caching group details")
|
||||
}
|
||||
}
|
||||
|
||||
return g, nil
|
||||
|
||||
}
|
||||
|
||||
func (m *manager) GetGroup(ctx context.Context, gid *grouppb.GroupId, skipFetchingMembers bool) (*grouppb.Group, error) {
|
||||
g, err := m.fetchCachedGroupDetails(gid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !skipFetchingMembers {
|
||||
groupMembers, err := m.GetMembers(ctx, gid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
g.Members = groupMembers
|
||||
}
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func (m *manager) GetGroupByClaim(ctx context.Context, claim, value string, skipFetchingMembers bool) (*grouppb.Group, error) {
|
||||
if claim == "group_name" {
|
||||
return m.GetGroup(ctx, &grouppb.GroupId{OpaqueId: value}, skipFetchingMembers)
|
||||
}
|
||||
|
||||
g, err := m.fetchCachedGroupByParam(claim, value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !skipFetchingMembers {
|
||||
groupMembers, err := m.GetMembers(ctx, g.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
g.Members = groupMembers
|
||||
}
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func (m *manager) FindGroups(ctx context.Context, query string, skipFetchingMembers bool) ([]*grouppb.Group, error) {
|
||||
|
||||
// Look at namespaces filters. If the query starts with:
|
||||
// "a" or none => get egroups
|
||||
// other filters => get empty list
|
||||
|
||||
parts := strings.SplitN(query, ":", 2)
|
||||
|
||||
if len(parts) == 2 {
|
||||
if parts[0] == "a" {
|
||||
query = parts[1]
|
||||
} else {
|
||||
return []*grouppb.Group{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return m.findCachedGroups(query)
|
||||
}
|
||||
|
||||
func (m *manager) GetMembers(ctx context.Context, gid *grouppb.GroupId) ([]*userpb.UserId, error) {
|
||||
|
||||
users, err := m.fetchCachedGroupMembers(gid)
|
||||
if err == nil {
|
||||
return users, nil
|
||||
}
|
||||
|
||||
internalID, err := m.fetchCachedInternalID(gid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
url := fmt.Sprintf("%s/api/v1.0/Group/%s/memberidentities/precomputed", m.conf.APIBaseURL, internalID)
|
||||
result, err := m.apiTokenManager.SendAPIGetRequest(ctx, url, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userData := result["data"].([]interface{})
|
||||
users = []*userpb.UserId{}
|
||||
|
||||
for _, u := range userData {
|
||||
userInfo, ok := u.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, errors.New("rest: error in type assertion")
|
||||
}
|
||||
if id, ok := userInfo["upn"].(string); ok {
|
||||
users = append(users, &userpb.UserId{OpaqueId: id, Idp: m.conf.IDProvider})
|
||||
}
|
||||
}
|
||||
|
||||
if err = m.cacheGroupMembers(gid, users); err != nil {
|
||||
log := appctx.GetLogger(ctx)
|
||||
log.Error().Err(err).Msg("rest: error caching group members")
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (m *manager) HasMember(ctx context.Context, gid *grouppb.GroupId, uid *userpb.UserId) (bool, error) {
|
||||
groupMemers, err := m.GetMembers(ctx, gid)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, u := range groupMemers {
|
||||
if uid.OpaqueId == u.OpaqueId {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package loader
|
||||
|
||||
import (
|
||||
// Load cbox specific drivers.
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/cbox/favorite/sql"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/cbox/group/rest"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/cbox/preferences/sql"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/cbox/publicshare/sql"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/cbox/share/sql"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/cbox/storage/eoshomewrapper"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/cbox/storage/eoswrapper"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/cbox/user/rest"
|
||||
)
|
||||
-100
@@ -1,100 +0,0 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/preferences"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/preferences/registry"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register("sql", New)
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
type mgr struct {
|
||||
c *config
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// New returns an instance of the cbox sql preferences manager.
|
||||
func New(m map[string]interface{}) (preferences.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 &mgr{
|
||||
c: c,
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *mgr) SetKey(ctx context.Context, key, namespace, value string) error {
|
||||
user, ok := ctxpkg.ContextGetUser(ctx)
|
||||
if !ok {
|
||||
return errtypes.UserRequired("preferences: error getting user from ctx")
|
||||
}
|
||||
query := `INSERT INTO oc_preferences(userid, appid, configkey, configvalue) values(?, ?, ?, ?) ON DUPLICATE KEY UPDATE configvalue = ?`
|
||||
params := []interface{}{user.Id.OpaqueId, namespace, key, value, value}
|
||||
stmt, err := m.db.Prepare(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = stmt.Exec(params...); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mgr) GetKey(ctx context.Context, key, namespace string) (string, error) {
|
||||
user, ok := ctxpkg.ContextGetUser(ctx)
|
||||
if !ok {
|
||||
return "", errtypes.UserRequired("preferences: error getting user from ctx")
|
||||
}
|
||||
query := `SELECT configvalue FROM oc_preferences WHERE userid=? AND appid=? AND configkey=?`
|
||||
var val string
|
||||
if err := m.db.QueryRow(query, user.Id.OpaqueId, namespace, key).Scan(&val); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return "", errtypes.NotFound(namespace + ":" + key)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
-533
@@ -1,533 +0,0 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
link "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/mitchellh/mapstructure"
|
||||
conversions "github.com/opencloud-eu/reva/v2/pkg/cbox/utils"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/publicshare"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/publicshare/manager/registry"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const publicShareType = 3
|
||||
|
||||
func init() {
|
||||
registry.Register("sql", New)
|
||||
}
|
||||
|
||||
type config struct {
|
||||
SharePasswordHashCost int `mapstructure:"password_hash_cost"`
|
||||
JanitorRunInterval int `mapstructure:"janitor_run_interval"`
|
||||
EnableExpiredSharesCleanup bool `mapstructure:"enable_expired_shares_cleanup"`
|
||||
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 manager struct {
|
||||
c *config
|
||||
db *sql.DB
|
||||
client gatewayv1beta1.GatewayAPIClient
|
||||
}
|
||||
|
||||
func (c *config) init() {
|
||||
if c.SharePasswordHashCost == 0 {
|
||||
c.SharePasswordHashCost = 11
|
||||
}
|
||||
if c.JanitorRunInterval == 0 {
|
||||
c.JanitorRunInterval = 3600
|
||||
}
|
||||
}
|
||||
|
||||
func (m *manager) startJanitorRun() {
|
||||
if !m.c.EnableExpiredSharesCleanup {
|
||||
return
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(time.Duration(m.c.JanitorRunInterval) * time.Second)
|
||||
work := make(chan os.Signal, 1)
|
||||
signal.Notify(work, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-work:
|
||||
return
|
||||
case <-ticker.C:
|
||||
_ = m.cleanupExpiredShares()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// New returns a new public share manager.
|
||||
func New(m map[string]interface{}) (publicshare.Manager, error) {
|
||||
c := &config{}
|
||||
if err := mapstructure.Decode(m, c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.init()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
gw, err := pool.GetGatewayServiceClient(c.GatewaySvc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mgr := manager{
|
||||
c: c,
|
||||
db: db,
|
||||
client: gw,
|
||||
}
|
||||
go mgr.startJanitorRun()
|
||||
|
||||
return &mgr, nil
|
||||
}
|
||||
|
||||
func (m *manager) CreatePublicShare(ctx context.Context, u *user.User, rInfo *provider.ResourceInfo, g *link.Grant) (*link.PublicShare, error) {
|
||||
|
||||
tkn := utils.RandString(15)
|
||||
now := time.Now().Unix()
|
||||
|
||||
displayName, ok := rInfo.ArbitraryMetadata.Metadata["name"]
|
||||
if !ok {
|
||||
displayName = tkn
|
||||
}
|
||||
createdAt := &typespb.Timestamp{
|
||||
Seconds: uint64(now),
|
||||
}
|
||||
|
||||
creator := conversions.FormatUserID(u.Id)
|
||||
owner := conversions.FormatUserID(rInfo.Owner)
|
||||
permissions := conversions.SharePermToInt(g.Permissions.Permissions)
|
||||
itemType := conversions.ResourceTypeToItem(rInfo.Type)
|
||||
prefix := rInfo.Id.SpaceId
|
||||
itemSource := rInfo.Id.OpaqueId
|
||||
fileSource, err := strconv.ParseUint(itemSource, 10, 64)
|
||||
if err != nil {
|
||||
// it can be the case that the item source may be a character string
|
||||
// we leave fileSource blank in that case
|
||||
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=?"
|
||||
params := []interface{}{publicShareType, owner, creator, itemType, prefix, itemSource, fileSource, permissions, now, tkn, displayName}
|
||||
|
||||
var passwordProtected bool
|
||||
password := g.Password
|
||||
if password != "" {
|
||||
password, err = hashPassword(password, m.c.SharePasswordHashCost)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not hash share password")
|
||||
}
|
||||
passwordProtected = true
|
||||
|
||||
query += ",share_with=?"
|
||||
params = append(params, password)
|
||||
}
|
||||
|
||||
if g.Expiration != nil && g.Expiration.Seconds != 0 {
|
||||
t := time.Unix(int64(g.Expiration.Seconds), 0)
|
||||
query += ",expiration=?"
|
||||
params = append(params, t)
|
||||
}
|
||||
|
||||
stmt, err := m.db.Prepare(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, err := stmt.Exec(params...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lastID, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &link.PublicShare{
|
||||
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,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *manager) UpdatePublicShare(ctx context.Context, u *user.User, req *link.UpdatePublicShareRequest) (*link.PublicShare, error) {
|
||||
query := "update oc_share set "
|
||||
paramsMap := map[string]interface{}{}
|
||||
params := []interface{}{}
|
||||
|
||||
now := time.Now().Unix()
|
||||
uid := conversions.FormatUserID(u.Id)
|
||||
|
||||
switch req.GetUpdate().GetType() {
|
||||
case link.UpdatePublicShareRequest_Update_TYPE_DISPLAYNAME:
|
||||
paramsMap["share_name"] = req.Update.GetDisplayName()
|
||||
case link.UpdatePublicShareRequest_Update_TYPE_PERMISSIONS:
|
||||
paramsMap["permissions"] = conversions.SharePermToInt(req.Update.GetGrant().GetPermissions().Permissions)
|
||||
case link.UpdatePublicShareRequest_Update_TYPE_EXPIRATION:
|
||||
paramsMap["expiration"] = time.Unix(int64(req.Update.GetGrant().Expiration.Seconds), 0)
|
||||
case link.UpdatePublicShareRequest_Update_TYPE_PASSWORD:
|
||||
if req.Update.GetGrant().Password == "" {
|
||||
paramsMap["share_with"] = ""
|
||||
} else {
|
||||
h, err := hashPassword(req.Update.GetGrant().Password, m.c.SharePasswordHashCost)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not hash share password")
|
||||
}
|
||||
paramsMap["share_with"] = h
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid update type: %v", req.GetUpdate().GetType())
|
||||
}
|
||||
|
||||
for k, v := range paramsMap {
|
||||
query += k + "=?"
|
||||
params = append(params, v)
|
||||
}
|
||||
|
||||
switch {
|
||||
case req.Ref.GetId() != nil:
|
||||
query += ",stime=? where id=? AND (uid_owner=? or uid_initiator=?)"
|
||||
params = append(params, now, req.Ref.GetId().OpaqueId, uid, uid)
|
||||
case req.Ref.GetToken() != "":
|
||||
query += ",stime=? where token=? AND (uid_owner=? or uid_initiator=?)"
|
||||
params = append(params, now, req.Ref.GetToken(), uid, uid)
|
||||
default:
|
||||
return nil, errtypes.NotFound(req.Ref.String())
|
||||
}
|
||||
|
||||
stmt, err := m.db.Prepare(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = stmt.Exec(params...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m.GetPublicShare(ctx, u, req.Ref, false)
|
||||
}
|
||||
|
||||
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 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); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, "", errtypes.NotFound(token)
|
||||
}
|
||||
return nil, "", err
|
||||
}
|
||||
share, err := conversions.ConvertToCS3PublicShare(ctx, m.client, s)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return share, s.ShareWith, nil
|
||||
}
|
||||
|
||||
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 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); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, "", errtypes.NotFound(id.OpaqueId)
|
||||
}
|
||||
return nil, "", err
|
||||
}
|
||||
share, err := conversions.ConvertToCS3PublicShare(ctx, m.client, s)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return share, s.ShareWith, nil
|
||||
}
|
||||
|
||||
func (m *manager) GetPublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference, sign bool) (*link.PublicShare, error) {
|
||||
var s *link.PublicShare
|
||||
var pw string
|
||||
var err error
|
||||
switch {
|
||||
case ref.GetId() != nil:
|
||||
s, pw, err = m.getByID(ctx, ref.GetId(), u)
|
||||
case ref.GetToken() != "":
|
||||
s, pw, err = m.getByToken(ctx, ref.GetToken(), u)
|
||||
default:
|
||||
err = errtypes.NotFound(ref.String())
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if expired(s) {
|
||||
if err := m.cleanupExpiredShares(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, errtypes.NotFound(ref.String())
|
||||
}
|
||||
|
||||
if s.PasswordProtected && sign {
|
||||
if err := publicshare.AddSignature(s, pw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (m *manager) ListPublicShares(ctx context.Context, u *user.User, filters []*link.ListPublicSharesRequest_Filter, sign bool) ([]*link.PublicShare, error) {
|
||||
uid := conversions.FormatUserID(u.Id)
|
||||
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 FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND (uid_owner=? or uid_initiator=?) AND (share_type=?)"
|
||||
var resourceFilters, ownerFilters, creatorFilters string
|
||||
var resourceParams, ownerParams, creatorParams []interface{}
|
||||
params := []interface{}{uid, uid, publicShareType}
|
||||
|
||||
for _, f := range filters {
|
||||
switch f.Type {
|
||||
case link.ListPublicSharesRequest_Filter_TYPE_RESOURCE_ID:
|
||||
if len(resourceFilters) != 0 {
|
||||
resourceFilters += " OR "
|
||||
}
|
||||
resourceFilters += "(fileid_prefix=? AND item_source=?)"
|
||||
resourceParams = append(resourceParams, f.GetResourceId().SpaceId, f.GetResourceId().OpaqueId)
|
||||
case link.ListPublicSharesRequest_Filter_TYPE_OWNER:
|
||||
if len(ownerFilters) != 0 {
|
||||
ownerFilters += " OR "
|
||||
}
|
||||
ownerFilters += "(uid_owner=?)"
|
||||
ownerParams = append(ownerParams, conversions.FormatUserID(f.GetOwner()))
|
||||
case link.ListPublicSharesRequest_Filter_TYPE_CREATOR:
|
||||
if len(creatorFilters) != 0 {
|
||||
creatorFilters += " OR "
|
||||
}
|
||||
creatorFilters += "(uid_initiator=?)"
|
||||
creatorParams = append(creatorParams, conversions.FormatUserID(f.GetCreator()))
|
||||
}
|
||||
}
|
||||
if resourceFilters != "" {
|
||||
query = fmt.Sprintf("%s AND (%s)", query, resourceFilters)
|
||||
params = append(params, resourceParams...)
|
||||
}
|
||||
if ownerFilters != "" {
|
||||
query = fmt.Sprintf("%s AND (%s)", query, ownerFilters)
|
||||
params = append(params, ownerParams...)
|
||||
}
|
||||
if creatorFilters != "" {
|
||||
query = fmt.Sprintf("%s AND (%s)", query, creatorFilters)
|
||||
params = append(params, creatorParams...)
|
||||
}
|
||||
|
||||
rows, err := m.db.Query(query, params...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
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); err != nil {
|
||||
continue
|
||||
}
|
||||
cs3Share, err := conversions.ConvertToCS3PublicShare(ctx, m.client, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if expired(cs3Share) {
|
||||
_ = m.cleanupExpiredShares()
|
||||
} else {
|
||||
if cs3Share.PasswordProtected && sign {
|
||||
if err := publicshare.AddSignature(cs3Share, s.ShareWith); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
shares = append(shares, cs3Share)
|
||||
}
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return shares, nil
|
||||
}
|
||||
|
||||
func (m *manager) RevokePublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference) error {
|
||||
uid := conversions.FormatUserID(u.Id)
|
||||
query := "delete from oc_share where "
|
||||
params := []interface{}{}
|
||||
|
||||
switch {
|
||||
case ref.GetId() != nil && ref.GetId().OpaqueId != "":
|
||||
query += "id=? AND (uid_owner=? or uid_initiator=?)"
|
||||
params = append(params, ref.GetId().OpaqueId, uid, uid)
|
||||
case ref.GetToken() != "":
|
||||
query += "token=? AND (uid_owner=? or uid_initiator=?)"
|
||||
params = append(params, ref.GetToken(), uid, uid)
|
||||
default:
|
||||
return errtypes.NotFound(ref.String())
|
||||
}
|
||||
|
||||
stmt, err := m.db.Prepare(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := stmt.Exec(params...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowCnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowCnt == 0 {
|
||||
return errtypes.NotFound(ref.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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 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); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, errtypes.NotFound(token)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
cs3Share, err := conversions.ConvertToCS3PublicShare(ctx, m.client, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s.ShareWith != "" {
|
||||
if !authenticate(cs3Share, s.ShareWith, auth) {
|
||||
// if check := checkPasswordHash(auth.Password, s.ShareWith); !check {
|
||||
return nil, errtypes.InvalidCredentials(token)
|
||||
}
|
||||
|
||||
if sign {
|
||||
if err := publicshare.AddSignature(cs3Share, s.ShareWith); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if expired(cs3Share) {
|
||||
if err := m.cleanupExpiredShares(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, errtypes.NotFound(token)
|
||||
}
|
||||
|
||||
return cs3Share, nil
|
||||
}
|
||||
|
||||
func (m *manager) cleanupExpiredShares() error {
|
||||
if !m.c.EnableExpiredSharesCleanup {
|
||||
return nil
|
||||
}
|
||||
|
||||
query := "update oc_share set orphan = 1 where expiration IS NOT NULL AND expiration < ?"
|
||||
params := []interface{}{time.Now().Format("2006-01-02 03:04:05")}
|
||||
|
||||
stmt, err := m.db.Prepare(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = stmt.Exec(params...); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func expired(s *link.PublicShare) bool {
|
||||
if s.Expiration != nil {
|
||||
if t := time.Unix(int64(s.Expiration.GetSeconds()), int64(s.Expiration.GetNanos())); t.Before(time.Now()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hashPassword(password string, cost int) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), cost)
|
||||
return "1|" + string(bytes), err
|
||||
}
|
||||
|
||||
func checkPasswordHash(password, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(strings.TrimPrefix(hash, "1|")), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func authenticate(share *link.PublicShare, pw string, auth *link.PublicShareAuthentication) bool {
|
||||
switch {
|
||||
case auth.GetPassword() != "":
|
||||
return checkPasswordHash(auth.GetPassword(), pw)
|
||||
case auth.GetSignature() != nil:
|
||||
sig := auth.GetSignature()
|
||||
now := time.Now()
|
||||
expiration := time.Unix(int64(sig.GetSignatureExpiration().GetSeconds()), int64(sig.GetSignatureExpiration().GetNanos()))
|
||||
if now.After(expiration) {
|
||||
return false
|
||||
}
|
||||
s, err := publicshare.CreateSignature(share.Token, pw, expiration)
|
||||
if err != nil {
|
||||
// TODO(labkode): pass context to call to log err.
|
||||
// No we are blind
|
||||
return false
|
||||
}
|
||||
return sig.GetSignature() == s
|
||||
}
|
||||
return false
|
||||
}
|
||||
-592
@@ -1,592 +0,0 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
conversions "github.com/opencloud-eu/reva/v2/pkg/cbox/utils"
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share/manager/registry"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/genproto/protobuf/field_mask"
|
||||
|
||||
// Provides mysql drivers
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
const (
|
||||
shareTypeUser = 0
|
||||
shareTypeGroup = 1
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register("sql", New)
|
||||
}
|
||||
|
||||
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 {
|
||||
c *config
|
||||
db *sql.DB
|
||||
client gatewayv1beta1.GatewayAPIClient
|
||||
}
|
||||
|
||||
// New returns a new share manager.
|
||||
func New(m map[string]interface{}) (share.Manager, error) {
|
||||
c, err := parseConfig(m)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "error creating a new manager")
|
||||
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
|
||||
}
|
||||
|
||||
gw, err := pool.GetGatewayServiceClient(c.GatewaySvc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &mgr{
|
||||
c: c,
|
||||
db: db,
|
||||
client: gw,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseConfig(m map[string]interface{}) (*config, error) {
|
||||
c := &config{}
|
||||
if err := mapstructure.Decode(m, c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (m *mgr) Share(ctx context.Context, md *provider.ResourceInfo, g *collaboration.ShareGrant) (*collaboration.Share, error) {
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
|
||||
// do not allow share to myself or the owner if share is for a user
|
||||
// TODO(labkode): should not this be caught already at the gw level?
|
||||
if g.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_USER &&
|
||||
(utils.UserEqual(g.Grantee.GetUserId(), user.Id) || utils.UserEqual(g.Grantee.GetUserId(), md.Owner)) {
|
||||
return nil, errors.New("sql: owner/creator and grantee are the same")
|
||||
}
|
||||
|
||||
// check if share already exists.
|
||||
key := &collaboration.ShareKey{
|
||||
Owner: md.Owner,
|
||||
ResourceId: md.Id,
|
||||
Grantee: g.Grantee,
|
||||
}
|
||||
_, err := m.getByKey(ctx, key)
|
||||
|
||||
// share already exists
|
||||
if err == nil {
|
||||
return nil, errtypes.AlreadyExists(key.String())
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
ts := &typespb.Timestamp{
|
||||
Seconds: uint64(now),
|
||||
}
|
||||
|
||||
shareType, shareWith := conversions.FormatGrantee(g.Grantee)
|
||||
itemType := conversions.ResourceTypeToItem(md.Type)
|
||||
targetPath := path.Join("/", path.Base(md.Path))
|
||||
permissions := conversions.SharePermToInt(g.Permissions.Permissions)
|
||||
prefix := md.Id.SpaceId
|
||||
itemSource := md.Id.OpaqueId
|
||||
fileSource, err := strconv.ParseUint(itemSource, 10, 64)
|
||||
if err != nil {
|
||||
// it can be the case that the item source may be a character string
|
||||
// we leave fileSource blank in that case
|
||||
fileSource = 0
|
||||
}
|
||||
|
||||
stmtString := "insert into oc_share set share_type=?,uid_owner=?,uid_initiator=?,item_type=?,fileid_prefix=?,item_source=?,file_source=?,permissions=?,stime=?,share_with=?,file_target=?"
|
||||
stmtValues := []interface{}{shareType, conversions.FormatUserID(md.Owner), conversions.FormatUserID(user.Id), itemType, prefix, itemSource, fileSource, permissions, now, shareWith, targetPath}
|
||||
|
||||
stmt, err := m.db.Prepare(stmtString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, err := stmt.Exec(stmtValues...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lastID, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &collaboration.Share{
|
||||
Id: &collaboration.ShareId{
|
||||
OpaqueId: strconv.FormatInt(lastID, 10),
|
||||
},
|
||||
ResourceId: md.Id,
|
||||
Permissions: g.Permissions,
|
||||
Grantee: g.Grantee,
|
||||
Owner: md.Owner,
|
||||
Creator: user.Id,
|
||||
Ctime: ts,
|
||||
Mtime: ts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *mgr) getByID(ctx context.Context, id *collaboration.ShareId) (*collaboration.Share, error) {
|
||||
uid := conversions.FormatUserID(ctxpkg.ContextMustGetUser(ctx).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, stime, permissions, share_type FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND id=? AND (uid_owner=? or uid_initiator=?)"
|
||||
if err := m.db.QueryRow(query, id.OpaqueId, uid, uid).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.STime, &s.Permissions, &s.ShareType); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, errtypes.NotFound(id.OpaqueId)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
share, err := conversions.ConvertToCS3Share(ctx, m.client, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return share, nil
|
||||
}
|
||||
|
||||
func (m *mgr) getByKey(ctx context.Context, key *collaboration.ShareKey) (*collaboration.Share, error) {
|
||||
owner := conversions.FormatUserID(key.Owner)
|
||||
uid := conversions.FormatUserID(ctxpkg.ContextMustGetUser(ctx).Id)
|
||||
|
||||
s := conversions.DBShare{}
|
||||
shareType, shareWith := conversions.FormatGrantee(key.Grantee)
|
||||
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, id, stime, permissions, share_type FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND uid_owner=? AND fileid_prefix=? AND item_source=? AND share_type=? AND share_with=? AND (uid_owner=? or uid_initiator=?)"
|
||||
if err := m.db.QueryRow(query, owner, key.ResourceId.SpaceId, key.ResourceId.OpaqueId, shareType, shareWith, uid, uid).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.ID, &s.STime, &s.Permissions, &s.ShareType); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, errtypes.NotFound(key.String())
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
share, err := conversions.ConvertToCS3Share(ctx, m.client, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return share, nil
|
||||
}
|
||||
|
||||
func (m *mgr) GetShare(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.Share, error) {
|
||||
var s *collaboration.Share
|
||||
var err error
|
||||
switch {
|
||||
case ref.GetId() != nil:
|
||||
s, err = m.getByID(ctx, ref.GetId())
|
||||
case ref.GetKey() != nil:
|
||||
s, err = m.getByKey(ctx, ref.GetKey())
|
||||
default:
|
||||
err = errtypes.NotFound(ref.String())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (m *mgr) Unshare(ctx context.Context, ref *collaboration.ShareReference) error {
|
||||
uid := conversions.FormatUserID(ctxpkg.ContextMustGetUser(ctx).Id)
|
||||
var query string
|
||||
params := []interface{}{}
|
||||
switch {
|
||||
case ref.GetId() != nil:
|
||||
query = "delete from oc_share where id=? AND (uid_owner=? or uid_initiator=?)"
|
||||
params = append(params, ref.GetId().OpaqueId, uid, uid)
|
||||
case ref.GetKey() != nil:
|
||||
key := ref.GetKey()
|
||||
shareType, shareWith := conversions.FormatGrantee(key.Grantee)
|
||||
owner := conversions.FormatUserID(key.Owner)
|
||||
query = "delete from oc_share where uid_owner=? AND fileid_prefix=? AND item_source=? AND share_type=? AND share_with=? AND (uid_owner=? or uid_initiator=?)"
|
||||
params = append(params, owner, key.ResourceId.SpaceId, key.ResourceId.OpaqueId, shareType, shareWith, uid, uid)
|
||||
default:
|
||||
return errtypes.NotFound(ref.String())
|
||||
}
|
||||
|
||||
stmt, err := m.db.Prepare(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := stmt.Exec(params...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowCnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowCnt == 0 {
|
||||
return errtypes.NotFound(ref.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mgr) UpdateShare(ctx context.Context, ref *collaboration.ShareReference, p *collaboration.SharePermissions, updated *collaboration.Share, fieldMask *field_mask.FieldMask) (*collaboration.Share, error) {
|
||||
permissions := conversions.SharePermToInt(p.Permissions)
|
||||
uid := conversions.FormatUserID(ctxpkg.ContextMustGetUser(ctx).Id)
|
||||
|
||||
var query string
|
||||
params := []interface{}{}
|
||||
switch {
|
||||
case ref.GetId() != nil:
|
||||
query = "update oc_share set permissions=?,stime=? where id=? AND (uid_owner=? or uid_initiator=?)"
|
||||
params = append(params, permissions, time.Now().Unix(), ref.GetId().OpaqueId, uid, uid)
|
||||
case ref.GetKey() != nil:
|
||||
key := ref.GetKey()
|
||||
shareType, shareWith := conversions.FormatGrantee(key.Grantee)
|
||||
owner := conversions.FormatUserID(key.Owner)
|
||||
query = "update oc_share set permissions=?,stime=? where (uid_owner=? or uid_initiator=?) AND fileid_prefix=? AND item_source=? AND share_type=? AND share_with=? AND (uid_owner=? or uid_initiator=?)"
|
||||
params = append(params, permissions, time.Now().Unix(), owner, owner, key.ResourceId.SpaceId, key.ResourceId.OpaqueId, shareType, shareWith, uid, uid)
|
||||
default:
|
||||
return nil, errtypes.NotFound(ref.String())
|
||||
}
|
||||
|
||||
stmt, err := m.db.Prepare(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = stmt.Exec(params...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m.GetShare(ctx, ref)
|
||||
}
|
||||
|
||||
func (m *mgr) ListShares(ctx context.Context, filters []*collaboration.Filter) ([]*collaboration.Share, error) {
|
||||
uid := conversions.FormatUserID(ctxpkg.ContextMustGetUser(ctx).Id)
|
||||
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,
|
||||
id, stime, permissions, share_type
|
||||
FROM oc_share
|
||||
WHERE (orphan = 0 or orphan IS NULL) AND (uid_owner=? or uid_initiator=?) AND (share_type=? OR share_type=?)`
|
||||
params := []interface{}{uid, uid, shareTypeUser, shareTypeGroup}
|
||||
|
||||
if len(filters) > 0 {
|
||||
filterQuery, filterParams, err := translateFilters(filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
params = append(params, filterParams...)
|
||||
if filterQuery != "" {
|
||||
query = fmt.Sprintf("%s AND (%s)", query, filterQuery)
|
||||
}
|
||||
}
|
||||
|
||||
rows, err := m.db.Query(query, params...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var s conversions.DBShare
|
||||
shares := []*collaboration.Share{}
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.ID, &s.STime, &s.Permissions, &s.ShareType); err != nil {
|
||||
continue
|
||||
}
|
||||
share, err := conversions.ConvertToCS3Share(ctx, m.client, s)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
shares = append(shares, share)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return shares, nil
|
||||
}
|
||||
|
||||
// we list the shares that are targeted to the user in context or to the user groups.
|
||||
func (m *mgr) ListReceivedShares(ctx context.Context, filters []*collaboration.Filter, _ *userpb.UserId) ([]*collaboration.ReceivedShare, error) {
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
uid := conversions.FormatUserID(user.Id)
|
||||
|
||||
params := []interface{}{uid, uid, uid, uid}
|
||||
for _, v := range user.Groups {
|
||||
params = append(params, v)
|
||||
}
|
||||
|
||||
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(file_target, '') as file_target,
|
||||
ts.id, stime, permissions, share_type, coalesce(tr.state, 0) as state
|
||||
FROM oc_share ts LEFT JOIN oc_share_status tr ON (ts.id = tr.id AND tr.recipient = ?)
|
||||
WHERE (orphan = 0 or orphan IS NULL) AND (uid_owner != ? AND uid_initiator != ?)`
|
||||
if len(user.Groups) > 0 {
|
||||
query += " AND ((share_with=? AND share_type = 0) OR (share_type = 1 AND share_with in (?" + strings.Repeat(",?", len(user.Groups)-1) + ")))"
|
||||
} else {
|
||||
query += " AND (share_with=? AND share_type = 0)"
|
||||
}
|
||||
|
||||
filterQuery, filterParams, err := translateFilters(filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
params = append(params, filterParams...)
|
||||
|
||||
if filterQuery != "" {
|
||||
query = fmt.Sprintf("%s AND (%s)", query, filterQuery)
|
||||
}
|
||||
|
||||
rows, err := m.db.Query(query, params...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var s conversions.DBShare
|
||||
shares := []*collaboration.ReceivedShare{}
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.FileTarget, &s.ID, &s.STime, &s.Permissions, &s.ShareType, &s.State); err != nil {
|
||||
continue
|
||||
}
|
||||
share, err := conversions.ConvertToCS3ReceivedShare(ctx, m.client, s)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
shares = append(shares, share)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return shares, nil
|
||||
}
|
||||
|
||||
func (m *mgr) getReceivedByID(ctx context.Context, id *collaboration.ShareId) (*collaboration.ReceivedShare, error) {
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
uid := conversions.FormatUserID(user.Id)
|
||||
|
||||
params := []interface{}{uid, id.OpaqueId, uid} // nolint:prealloc
|
||||
for _, v := range user.Groups {
|
||||
params = append(params, v)
|
||||
}
|
||||
|
||||
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(file_target, '') as file_target,
|
||||
stime, permissions, share_type, coalesce(tr.state, 0) as state
|
||||
FROM oc_share ts LEFT JOIN oc_share_status tr ON (ts.id = tr.id AND tr.recipient = ?)
|
||||
WHERE (orphan = 0 or orphan IS NULL) AND ts.id=?`
|
||||
if len(user.Groups) > 0 {
|
||||
query += " AND ((share_with=? AND share_type = 0) OR (share_type = 1 AND share_with in (?" + strings.Repeat(",?", len(user.Groups)-1) + ")))"
|
||||
} else {
|
||||
query += " AND (share_with=? AND share_type = 0)"
|
||||
}
|
||||
if err := m.db.QueryRow(query, params...).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.FileTarget, &s.STime, &s.Permissions, &s.ShareType, &s.State); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, errtypes.NotFound(id.OpaqueId)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
share, err := conversions.ConvertToCS3ReceivedShare(ctx, m.client, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return share, nil
|
||||
}
|
||||
|
||||
func (m *mgr) getReceivedByKey(ctx context.Context, key *collaboration.ShareKey) (*collaboration.ReceivedShare, error) {
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
uid := conversions.FormatUserID(user.Id)
|
||||
|
||||
shareType, shareWith := conversions.FormatGrantee(key.Grantee)
|
||||
params := []interface{}{uid, conversions.FormatUserID(key.Owner), key.GetResourceId().SpaceId, key.ResourceId.OpaqueId, shareType, shareWith, shareWith} // nolint:prealloc
|
||||
for _, v := range user.Groups {
|
||||
params = append(params, v)
|
||||
}
|
||||
|
||||
s := conversions.DBShare{}
|
||||
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(file_target, '') as file_target,
|
||||
ts.id, stime, permissions, share_type, coalesce(tr.state, 0) as state
|
||||
FROM oc_share ts LEFT JOIN oc_share_status tr ON (ts.id = tr.id AND tr.recipient = ?)
|
||||
WHERE (orphan = 0 or orphan IS NULL) AND uid_owner=? AND fileid_prefix=? AND item_source=? AND share_type=? AND share_with=?`
|
||||
if len(user.Groups) > 0 {
|
||||
query += " AND ((share_with=? AND share_type = 0) OR (share_type = 1 AND share_with in (?" + strings.Repeat(",?", len(user.Groups)-1) + ")))"
|
||||
} else {
|
||||
query += " AND (share_with=? AND share_type = 0)"
|
||||
}
|
||||
|
||||
if err := m.db.QueryRow(query, params...).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.FileTarget, &s.ID, &s.STime, &s.Permissions, &s.ShareType, &s.State); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, errtypes.NotFound(key.String())
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
share, err := conversions.ConvertToCS3ReceivedShare(ctx, m.client, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return share, nil
|
||||
}
|
||||
|
||||
func (m *mgr) GetReceivedShare(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.ReceivedShare, error) {
|
||||
var s *collaboration.ReceivedShare
|
||||
var err error
|
||||
switch {
|
||||
case ref.GetId() != nil:
|
||||
s, err = m.getReceivedByID(ctx, ref.GetId())
|
||||
case ref.GetKey() != nil:
|
||||
s, err = m.getReceivedByKey(ctx, ref.GetKey())
|
||||
default:
|
||||
err = errtypes.NotFound(ref.String())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
|
||||
}
|
||||
|
||||
func (m *mgr) UpdateReceivedShare(ctx context.Context, share *collaboration.ReceivedShare, fieldMask *field_mask.FieldMask, _ *userpb.UserId) (*collaboration.ReceivedShare, error) {
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
|
||||
rs, err := m.GetReceivedShare(ctx, &collaboration.ShareReference{Spec: &collaboration.ShareReference_Id{Id: share.Share.Id}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := range fieldMask.Paths {
|
||||
switch fieldMask.Paths[i] {
|
||||
case "state":
|
||||
rs.State = share.State
|
||||
case "mount_point":
|
||||
rs.MountPoint = share.MountPoint
|
||||
default:
|
||||
return nil, errtypes.NotSupported("updating " + fieldMask.Paths[i] + " is not supported")
|
||||
}
|
||||
}
|
||||
|
||||
state := 0
|
||||
switch rs.GetState() {
|
||||
case collaboration.ShareState_SHARE_STATE_REJECTED:
|
||||
state = -1
|
||||
case collaboration.ShareState_SHARE_STATE_ACCEPTED:
|
||||
state = 1
|
||||
}
|
||||
|
||||
params := []interface{}{rs.Share.Id.OpaqueId, conversions.FormatUserID(user.Id), state, state}
|
||||
query := "insert into oc_share_status(id, recipient, state) values(?, ?, ?) ON DUPLICATE KEY UPDATE state = ?"
|
||||
|
||||
stmt, err := m.db.Prepare(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = stmt.Exec(params...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
func granteeTypeToShareType(granteeType provider.GranteeType) int {
|
||||
switch granteeType {
|
||||
case provider.GranteeType_GRANTEE_TYPE_USER:
|
||||
return shareTypeUser
|
||||
case provider.GranteeType_GRANTEE_TYPE_GROUP:
|
||||
return shareTypeGroup
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// translateFilters translates the filters to sql queries
|
||||
func translateFilters(filters []*collaboration.Filter) (string, []interface{}, error) {
|
||||
var (
|
||||
filterQuery string
|
||||
params []interface{}
|
||||
)
|
||||
|
||||
groupedFilters := share.GroupFiltersByType(filters)
|
||||
// If multiple filters of the same type are passed to this function, they need to be combined with the `OR` operator.
|
||||
// That is why the filters got grouped by type.
|
||||
// For every given filter type, iterate over the filters and if there are more than one combine them.
|
||||
// Combine the different filter types using `AND`
|
||||
var filterCounter = 0
|
||||
for filterType, filters := range groupedFilters {
|
||||
switch filterType {
|
||||
case collaboration.Filter_TYPE_RESOURCE_ID:
|
||||
filterQuery += "("
|
||||
for i, f := range filters {
|
||||
filterQuery += "(fileid_prefix =? AND item_source=?)"
|
||||
params = append(params, f.GetResourceId().SpaceId, f.GetResourceId().OpaqueId)
|
||||
|
||||
if i != len(filters)-1 {
|
||||
filterQuery += " OR "
|
||||
}
|
||||
}
|
||||
filterQuery += ")"
|
||||
case collaboration.Filter_TYPE_GRANTEE_TYPE:
|
||||
filterQuery += "("
|
||||
for i, f := range filters {
|
||||
filterQuery += "share_type=?"
|
||||
params = append(params, granteeTypeToShareType(f.GetGranteeType()))
|
||||
|
||||
if i != len(filters)-1 {
|
||||
filterQuery += " OR "
|
||||
}
|
||||
}
|
||||
filterQuery += ")"
|
||||
case collaboration.Filter_TYPE_EXCLUDE_DENIALS:
|
||||
// TODO this may change once the mapping of permission to share types is completed (cf. pkg/cbox/utils/conversions.go)
|
||||
filterQuery += "(permissions > 0)"
|
||||
default:
|
||||
return "", nil, fmt.Errorf("filter type is not supported")
|
||||
}
|
||||
if filterCounter != len(groupedFilters)-1 {
|
||||
filterQuery += " AND "
|
||||
}
|
||||
filterCounter++
|
||||
}
|
||||
return filterQuery, params, nil
|
||||
}
|
||||
Generated
Vendored
-129
@@ -1,129 +0,0 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package eoshomewrapper
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"text/template"
|
||||
|
||||
"github.com/Masterminds/sprig"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/events"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/registry"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/eosfs"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register("eoshomewrapper", New)
|
||||
}
|
||||
|
||||
type wrapper struct {
|
||||
storage.FS
|
||||
mountIDTemplate *template.Template
|
||||
}
|
||||
|
||||
func parseConfig(m map[string]interface{}) (*eosfs.Config, string, error) {
|
||||
c := &eosfs.Config{}
|
||||
if err := mapstructure.Decode(m, c); err != nil {
|
||||
err = errors.Wrap(err, "error decoding conf")
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// default to version invariance if not configured
|
||||
if _, ok := m["version_invariant"]; !ok {
|
||||
c.VersionInvariant = true
|
||||
}
|
||||
|
||||
t, ok := m["mount_id_template"].(string)
|
||||
if !ok || t == "" {
|
||||
t = "eoshome-{{substr 0 1 .Username}}"
|
||||
}
|
||||
|
||||
return c, t, nil
|
||||
}
|
||||
|
||||
// New returns an implementation of the storage.FS interface that forms a wrapper
|
||||
// around separate connections to EOS.
|
||||
func New(m map[string]interface{}, _ events.Stream, _ *zerolog.Logger) (storage.FS, error) {
|
||||
c, t, err := parseConfig(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.EnableHome = true
|
||||
|
||||
eos, err := eosfs.NewEOSFS(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mountIDTemplate, err := template.New("mountID").Funcs(sprig.TxtFuncMap()).Parse(t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &wrapper{FS: eos, mountIDTemplate: mountIDTemplate}, nil
|
||||
}
|
||||
|
||||
// We need to override the two methods, GetMD and ListFolder to fill the
|
||||
// StorageId in the ResourceInfo objects.
|
||||
|
||||
func (w *wrapper) GetMD(ctx context.Context, ref *provider.Reference, mdKeys []string, fieldMask []string) (*provider.ResourceInfo, error) {
|
||||
res, err := w.FS.GetMD(ctx, ref, mdKeys, fieldMask)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We need to extract the mount ID based on the mapping template.
|
||||
//
|
||||
// Take the first letter of the username of the logged-in user, as the home
|
||||
// storage provider restricts requests only to the home namespace.
|
||||
res.Id.StorageId = w.getMountID(ctx, res)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (w *wrapper) ListFolder(ctx context.Context, ref *provider.Reference, mdKeys, fieldMask []string) ([]*provider.ResourceInfo, error) {
|
||||
res, err := w.FS.ListFolder(ctx, ref, mdKeys, fieldMask)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range res {
|
||||
r.Id.StorageId = w.getMountID(ctx, r)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (w *wrapper) DenyGrant(ctx context.Context, ref *provider.Reference, g *provider.Grantee) error {
|
||||
return errtypes.NotSupported("eos: deny grant is only enabled for project spaces")
|
||||
}
|
||||
|
||||
func (w *wrapper) getMountID(ctx context.Context, r *provider.ResourceInfo) string {
|
||||
u := ctxpkg.ContextMustGetUser(ctx)
|
||||
b := bytes.Buffer{}
|
||||
if err := w.mountIDTemplate.Execute(&b, u); err != nil {
|
||||
return ""
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
-296
@@ -1,296 +0,0 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package eoswrapper
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"path"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/Masterminds/sprig"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/events"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/registry"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/eosfs"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register("eoswrapper", New)
|
||||
}
|
||||
|
||||
const (
|
||||
eosProjectsNamespace = "/eos/project"
|
||||
|
||||
// We can use a regex for these, but that might have inferior performance
|
||||
projectSpaceGroupsPrefix = "cernbox-project-"
|
||||
projectSpaceAdminGroupsSuffix = "-admins"
|
||||
)
|
||||
|
||||
type wrapper struct {
|
||||
storage.FS
|
||||
conf *eosfs.Config
|
||||
mountIDTemplate *template.Template
|
||||
}
|
||||
|
||||
func parseConfig(m map[string]interface{}) (*eosfs.Config, string, error) {
|
||||
c := &eosfs.Config{}
|
||||
if err := mapstructure.Decode(m, c); err != nil {
|
||||
err = errors.Wrap(err, "error decoding conf")
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// default to version invariance if not configured
|
||||
if _, ok := m["version_invariant"]; !ok {
|
||||
c.VersionInvariant = true
|
||||
}
|
||||
|
||||
// allow recycle operations for project spaces
|
||||
if !c.EnableHome && strings.HasPrefix(c.Namespace, eosProjectsNamespace) {
|
||||
c.AllowPathRecycleOperations = true
|
||||
c.ImpersonateOwnerforRevisions = true
|
||||
}
|
||||
|
||||
t, ok := m["mount_id_template"].(string)
|
||||
if !ok || t == "" {
|
||||
t = "eoshome-{{ trimAll \"/\" .Path | substr 0 1 }}"
|
||||
}
|
||||
|
||||
return c, t, nil
|
||||
}
|
||||
|
||||
// New returns an implementation of the storage.FS interface that forms a wrapper
|
||||
// around separate connections to EOS.
|
||||
func New(m map[string]interface{}, _ events.Stream, _ *zerolog.Logger) (storage.FS, error) {
|
||||
c, t, err := parseConfig(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
eos, err := eosfs.NewEOSFS(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mountIDTemplate, err := template.New("mountID").Funcs(sprig.TxtFuncMap()).Parse(t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &wrapper{FS: eos, conf: c, mountIDTemplate: mountIDTemplate}, nil
|
||||
}
|
||||
|
||||
// We need to override the methods, GetMD, GetPathByID and ListFolder to fill the
|
||||
// StorageId in the ResourceInfo objects.
|
||||
|
||||
func (w *wrapper) GetMD(ctx context.Context, ref *provider.Reference, mdKeys []string, fieldMask []string) (*provider.ResourceInfo, error) {
|
||||
res, err := w.FS.GetMD(ctx, ref, mdKeys, fieldMask)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We need to extract the mount ID based on the mapping template.
|
||||
//
|
||||
// Take the first letter of the resource path after the namespace has been removed.
|
||||
// If it's empty, leave it empty to be filled by storageprovider.
|
||||
res.Id.StorageId = w.getMountID(ctx, res)
|
||||
|
||||
if err = w.setProjectSharingPermissions(ctx, res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If the request contains a relative reference, we also need to return the base path instead of the full one
|
||||
if utils.IsRelativeReference(ref) {
|
||||
res.Path = path.Base(res.Path)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (w *wrapper) ListFolder(ctx context.Context, ref *provider.Reference, mdKeys, fieldMask []string) ([]*provider.ResourceInfo, error) {
|
||||
res, err := w.FS.ListFolder(ctx, ref, mdKeys, fieldMask)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range res {
|
||||
r.Id.StorageId = w.getMountID(ctx, r)
|
||||
|
||||
// If the request contains a relative reference, we also need to return the base path instead of the full one
|
||||
if utils.IsRelativeReference(ref) {
|
||||
r.Path = path.Base(r.Path)
|
||||
}
|
||||
|
||||
if err = w.setProjectSharingPermissions(ctx, r); err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (w *wrapper) ListRecycle(ctx context.Context, ref *provider.Reference, key, relativePath string) ([]*provider.RecycleItem, error) {
|
||||
res, err := w.FS.ListRecycle(ctx, ref, key, relativePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If the request contains a relative reference, we also need to return the base path instead of the full one
|
||||
if utils.IsRelativeReference(ref) {
|
||||
for _, info := range res {
|
||||
info.Ref.Path = path.Base(info.Ref.Path)
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
|
||||
}
|
||||
|
||||
func (w *wrapper) ListStorageSpaces(ctx context.Context, filter []*provider.ListStorageSpacesRequest_Filter, unrestricted bool) ([]*provider.StorageSpace, error) {
|
||||
res, err := w.FS.ListStorageSpaces(ctx, filter, unrestricted)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, r := range res {
|
||||
if mountID, _, _, _ := storagespace.SplitID(r.Id.OpaqueId); mountID == "" {
|
||||
mountID = w.getMountID(ctx, &provider.ResourceInfo{Path: r.Name})
|
||||
r.Root.StorageId = mountID
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
|
||||
}
|
||||
|
||||
func (w *wrapper) ListRevisions(ctx context.Context, ref *provider.Reference) ([]*provider.FileVersion, error) {
|
||||
if err := w.userIsProjectAdmin(ctx, ref); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return w.FS.ListRevisions(ctx, ref)
|
||||
}
|
||||
|
||||
func (w *wrapper) DownloadRevision(ctx context.Context, ref *provider.Reference, revisionKey string, openReaderfunc func(*provider.ResourceInfo) bool) (*provider.ResourceInfo, io.ReadCloser, error) {
|
||||
if err := w.userIsProjectAdmin(ctx, ref); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return w.FS.DownloadRevision(ctx, ref, revisionKey, openReaderfunc)
|
||||
}
|
||||
|
||||
func (w *wrapper) RestoreRevision(ctx context.Context, ref *provider.Reference, revisionKey string) error {
|
||||
if err := w.userIsProjectAdmin(ctx, ref); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return w.FS.RestoreRevision(ctx, ref, revisionKey)
|
||||
}
|
||||
|
||||
func (w *wrapper) DenyGrant(ctx context.Context, ref *provider.Reference, g *provider.Grantee) error {
|
||||
// This is only allowed for project space admins
|
||||
if strings.HasPrefix(w.conf.Namespace, eosProjectsNamespace) {
|
||||
if err := w.userIsProjectAdmin(ctx, ref); err != nil {
|
||||
return err
|
||||
}
|
||||
return w.FS.DenyGrant(ctx, ref, g)
|
||||
}
|
||||
|
||||
return errtypes.NotSupported("eos: deny grant is only enabled for project spaces")
|
||||
}
|
||||
|
||||
func (w *wrapper) getMountID(ctx context.Context, r *provider.ResourceInfo) string {
|
||||
if r == nil {
|
||||
return ""
|
||||
}
|
||||
r.Path = strings.TrimPrefix(r.Path, w.conf.MountPath)
|
||||
b := bytes.Buffer{}
|
||||
if err := w.mountIDTemplate.Execute(&b, r); err != nil {
|
||||
return ""
|
||||
}
|
||||
r.Path = path.Join(w.conf.MountPath, r.Path)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (w *wrapper) setProjectSharingPermissions(ctx context.Context, r *provider.ResourceInfo) error {
|
||||
// Check if this storage provider corresponds to a project spaces instance
|
||||
if strings.HasPrefix(w.conf.Namespace, eosProjectsNamespace) {
|
||||
|
||||
// Extract project name from the path resembling /c/cernbox or /c/cernbox/minutes/..
|
||||
parts := strings.SplitN(r.Path, "/", 4)
|
||||
if len(parts) != 4 && len(parts) != 3 {
|
||||
// The request might be for / or /$letter
|
||||
// Nothing to do in that case
|
||||
return nil
|
||||
}
|
||||
adminGroup := projectSpaceGroupsPrefix + parts[2] + projectSpaceAdminGroupsSuffix
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
|
||||
for _, g := range user.Groups {
|
||||
if g == adminGroup {
|
||||
r.PermissionSet.AddGrant = true
|
||||
r.PermissionSet.RemoveGrant = true
|
||||
r.PermissionSet.UpdateGrant = true
|
||||
r.PermissionSet.ListGrants = true
|
||||
r.PermissionSet.GetQuota = true
|
||||
r.PermissionSet.DenyGrant = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *wrapper) userIsProjectAdmin(ctx context.Context, ref *provider.Reference) error {
|
||||
// Check if this storage provider corresponds to a project spaces instance
|
||||
if !strings.HasPrefix(w.conf.Namespace, eosProjectsNamespace) {
|
||||
return nil
|
||||
}
|
||||
|
||||
res, err := w.FS.GetMD(ctx, ref, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Extract project name from the path resembling /c/cernbox or /c/cernbox/minutes/..
|
||||
parts := strings.SplitN(res.Path, "/", 4)
|
||||
if len(parts) != 4 && len(parts) != 3 {
|
||||
// The request might be for / or /$letter
|
||||
// Nothing to do in that case
|
||||
return nil
|
||||
}
|
||||
adminGroup := projectSpaceGroupsPrefix + parts[2] + projectSpaceAdminGroupsSuffix
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
|
||||
for _, g := range user.Groups {
|
||||
if g == adminGroup {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return errtypes.PermissionDenied("eosfs: project spaces revisions can only be accessed by admins")
|
||||
}
|
||||
-214
@@ -1,214 +0,0 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package rest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"github.com/gomodule/redigo/redis"
|
||||
)
|
||||
|
||||
const (
|
||||
userPrefix = "user:"
|
||||
usernamePrefix = "username:"
|
||||
userIDPrefix = "userid:"
|
||||
namePrefix = "name:"
|
||||
mailPrefix = "mail:"
|
||||
uidPrefix = "uid:"
|
||||
userGroupsPrefix = "groups:"
|
||||
)
|
||||
|
||||
func initRedisPool(address, username, password string) *redis.Pool {
|
||||
return &redis.Pool{
|
||||
|
||||
MaxIdle: 50,
|
||||
MaxActive: 1000,
|
||||
IdleTimeout: 240 * time.Second,
|
||||
|
||||
Dial: func() (redis.Conn, error) {
|
||||
var opts []redis.DialOption
|
||||
if username != "" {
|
||||
opts = append(opts, redis.DialUsername(username))
|
||||
}
|
||||
if password != "" {
|
||||
opts = append(opts, redis.DialPassword(password))
|
||||
}
|
||||
|
||||
c, err := redis.Dial("tcp", address, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, err
|
||||
},
|
||||
|
||||
TestOnBorrow: func(c redis.Conn, t time.Time) error {
|
||||
_, err := c.Do("PING")
|
||||
return err
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *manager) setVal(key, val string, expiration int) error {
|
||||
conn := m.redisPool.Get()
|
||||
defer conn.Close()
|
||||
if conn != nil {
|
||||
args := []interface{}{key, val}
|
||||
if expiration != -1 {
|
||||
args = append(args, "EX", expiration)
|
||||
}
|
||||
if _, err := conn.Do("SET", args...); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return errors.New("rest: unable to get connection from redis pool")
|
||||
}
|
||||
|
||||
func (m *manager) getVal(key string) (string, error) {
|
||||
conn := m.redisPool.Get()
|
||||
defer conn.Close()
|
||||
if conn != nil {
|
||||
val, err := redis.String(conn.Do("GET", key))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
return "", errors.New("rest: unable to get connection from redis pool")
|
||||
}
|
||||
|
||||
func (m *manager) findCachedUsers(query string) ([]*userpb.User, error) {
|
||||
conn := m.redisPool.Get()
|
||||
defer conn.Close()
|
||||
if conn != nil {
|
||||
query = fmt.Sprintf("%s*%s*", userPrefix, strings.ReplaceAll(strings.ToLower(query), " ", "_"))
|
||||
keys, err := redis.Strings(conn.Do("KEYS", query))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var args []interface{}
|
||||
for _, k := range keys {
|
||||
args = append(args, k)
|
||||
}
|
||||
|
||||
// Fetch the users for all these keys
|
||||
userStrings, err := redis.Strings(conn.Do("MGET", args...))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userMap := make(map[string]*userpb.User)
|
||||
for _, user := range userStrings {
|
||||
u := userpb.User{}
|
||||
if err = json.Unmarshal([]byte(user), &u); err == nil {
|
||||
userMap[u.Id.OpaqueId] = &u
|
||||
}
|
||||
}
|
||||
|
||||
var users []*userpb.User
|
||||
for _, u := range userMap {
|
||||
users = append(users, u)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("rest: unable to get connection from redis pool")
|
||||
}
|
||||
|
||||
func (m *manager) fetchCachedUserDetails(uid *userpb.UserId) (*userpb.User, error) {
|
||||
user, err := m.getVal(userPrefix + usernamePrefix + strings.ToLower(uid.OpaqueId))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u := userpb.User{}
|
||||
if err = json.Unmarshal([]byte(user), &u); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
func (m *manager) cacheUserDetails(u *userpb.User) error {
|
||||
encodedUser, err := json.Marshal(&u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = m.setVal(userPrefix+usernamePrefix+strings.ToLower(u.Id.OpaqueId), string(encodedUser), -1); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = m.setVal(userPrefix+userIDPrefix+strings.ToLower(u.Id.OpaqueId), string(encodedUser), -1); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if u.Mail != "" {
|
||||
if err = m.setVal(userPrefix+mailPrefix+strings.ToLower(u.Mail), string(encodedUser), -1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if u.DisplayName != "" {
|
||||
if err = m.setVal(userPrefix+namePrefix+u.Id.OpaqueId+"_"+strings.ReplaceAll(strings.ToLower(u.DisplayName), " ", "_"), string(encodedUser), -1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if u.UidNumber != 0 {
|
||||
if err = m.setVal(userPrefix+uidPrefix+strconv.FormatInt(u.UidNumber, 10), string(encodedUser), -1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *manager) fetchCachedUserByParam(field, claim string) (*userpb.User, error) {
|
||||
user, err := m.getVal(userPrefix + field + ":" + strings.ToLower(claim))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u := userpb.User{}
|
||||
if err = json.Unmarshal([]byte(user), &u); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
func (m *manager) fetchCachedUserGroups(uid *userpb.UserId) ([]string, error) {
|
||||
groups, err := m.getVal(userPrefix + userGroupsPrefix + strings.ToLower(uid.OpaqueId))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
g := []string{}
|
||||
if err = json.Unmarshal([]byte(groups), &g); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func (m *manager) cacheUserGroups(uid *userpb.UserId, groups []string) error {
|
||||
g, err := json.Marshal(&groups)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.setVal(userPrefix+userGroupsPrefix+strings.ToLower(uid.OpaqueId), string(g), m.conf.UserGroupsCacheExpiration*60)
|
||||
}
|
||||
-391
@@ -1,391 +0,0 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"github.com/gomodule/redigo/redis"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
utils "github.com/opencloud-eu/reva/v2/pkg/cbox/utils"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/user"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/user/manager/registry"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register("rest", New)
|
||||
}
|
||||
|
||||
type manager struct {
|
||||
conf *config
|
||||
redisPool *redis.Pool
|
||||
apiTokenManager *utils.APITokenManager
|
||||
}
|
||||
|
||||
type config struct {
|
||||
// The address at which the redis server is running
|
||||
RedisAddress string `mapstructure:"redis_address" docs:"localhost:6379"`
|
||||
// The username for connecting to the redis server
|
||||
RedisUsername string `mapstructure:"redis_username" docs:""`
|
||||
// The password for connecting to the redis server
|
||||
RedisPassword string `mapstructure:"redis_password" docs:""`
|
||||
// The time in minutes for which the groups to which a user belongs would be cached
|
||||
UserGroupsCacheExpiration int `mapstructure:"user_groups_cache_expiration" docs:"5"`
|
||||
// The OIDC Provider
|
||||
IDProvider string `mapstructure:"id_provider" docs:"http://cernbox.cern.ch"`
|
||||
// Base API Endpoint
|
||||
APIBaseURL string `mapstructure:"api_base_url" docs:"https://authorization-service-api-dev.web.cern.ch"`
|
||||
// Client ID needed to authenticate
|
||||
ClientID string `mapstructure:"client_id" docs:"-"`
|
||||
// Client Secret
|
||||
ClientSecret string `mapstructure:"client_secret" docs:"-"`
|
||||
|
||||
// Endpoint to generate token to access the API
|
||||
OIDCTokenEndpoint string `mapstructure:"oidc_token_endpoint" docs:"https://keycloak-dev.cern.ch/auth/realms/cern/api-access/token"`
|
||||
// The target application for which token needs to be generated
|
||||
TargetAPI string `mapstructure:"target_api" docs:"authorization-service-api"`
|
||||
// The time in seconds between bulk fetch of user accounts
|
||||
UserFetchInterval int `mapstructure:"user_fetch_interval" docs:"3600"`
|
||||
}
|
||||
|
||||
func (c *config) init() {
|
||||
if c.UserGroupsCacheExpiration == 0 {
|
||||
c.UserGroupsCacheExpiration = 5
|
||||
}
|
||||
if c.RedisAddress == "" {
|
||||
c.RedisAddress = ":6379"
|
||||
}
|
||||
if c.APIBaseURL == "" {
|
||||
c.APIBaseURL = "https://authorization-service-api-dev.web.cern.ch"
|
||||
}
|
||||
if c.TargetAPI == "" {
|
||||
c.TargetAPI = "authorization-service-api"
|
||||
}
|
||||
if c.OIDCTokenEndpoint == "" {
|
||||
c.OIDCTokenEndpoint = "https://keycloak-dev.cern.ch/auth/realms/cern/api-access/token"
|
||||
}
|
||||
if c.IDProvider == "" {
|
||||
c.IDProvider = "http://cernbox.cern.ch"
|
||||
}
|
||||
if c.UserFetchInterval == 0 {
|
||||
c.UserFetchInterval = 3600
|
||||
}
|
||||
}
|
||||
|
||||
func parseConfig(m map[string]interface{}) (*config, error) {
|
||||
c := &config{}
|
||||
if err := mapstructure.Decode(m, c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// New returns a user manager implementation that makes calls to the GRAPPA API.
|
||||
func New(m map[string]interface{}) (user.Manager, error) {
|
||||
mgr := &manager{}
|
||||
err := mgr.Configure(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mgr, err
|
||||
}
|
||||
|
||||
func (m *manager) Configure(ml map[string]interface{}) error {
|
||||
c, err := parseConfig(ml)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.init()
|
||||
redisPool := initRedisPool(c.RedisAddress, c.RedisUsername, c.RedisPassword)
|
||||
apiTokenManager := utils.InitAPITokenManager(c.TargetAPI, c.OIDCTokenEndpoint, c.ClientID, c.ClientSecret)
|
||||
m.conf = c
|
||||
m.redisPool = redisPool
|
||||
m.apiTokenManager = apiTokenManager
|
||||
|
||||
// Since we're starting a subroutine which would take some time to execute,
|
||||
// we can't wait to see if it works before returning the user.Manager object
|
||||
// TODO: return err if the fetch fails
|
||||
go m.fetchAllUsers()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *manager) fetchAllUsers() {
|
||||
_ = m.fetchAllUserAccounts()
|
||||
ticker := time.NewTicker(time.Duration(m.conf.UserFetchInterval) * time.Second)
|
||||
work := make(chan os.Signal, 1)
|
||||
signal.Notify(work, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-work:
|
||||
return
|
||||
case <-ticker.C:
|
||||
_ = m.fetchAllUserAccounts()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *manager) fetchAllUserAccounts() error {
|
||||
ctx := context.Background()
|
||||
url := fmt.Sprintf("%s/api/v1.0/Identity?field=upn&field=primaryAccountEmail&field=displayName&field=uid&field=gid&field=type", m.conf.APIBaseURL)
|
||||
|
||||
for url != "" {
|
||||
result, err := m.apiTokenManager.SendAPIGetRequest(ctx, url, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
responseData, ok := result["data"].([]interface{})
|
||||
if !ok {
|
||||
return errors.New("rest: error in type assertion")
|
||||
}
|
||||
for _, usr := range responseData {
|
||||
userData, ok := usr.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = m.parseAndCacheUser(ctx, userData)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
url = ""
|
||||
if pagination, ok := result["pagination"].(map[string]interface{}); ok {
|
||||
if links, ok := pagination["links"].(map[string]interface{}); ok {
|
||||
if next, ok := links["next"].(string); ok {
|
||||
url = fmt.Sprintf("%s%s", m.conf.APIBaseURL, next)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *manager) parseAndCacheUser(ctx context.Context, userData map[string]interface{}) (*userpb.User, error) {
|
||||
upn, ok := userData["upn"].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("rest: missing upn in user data")
|
||||
}
|
||||
mail, _ := userData["primaryAccountEmail"].(string)
|
||||
name, _ := userData["displayName"].(string)
|
||||
uidNumber, _ := userData["uid"].(float64)
|
||||
gidNumber, _ := userData["gid"].(float64)
|
||||
t, _ := userData["type"].(string)
|
||||
userType := getUserType(t, upn)
|
||||
|
||||
userID := &userpb.UserId{
|
||||
OpaqueId: upn,
|
||||
Idp: m.conf.IDProvider,
|
||||
Type: userType,
|
||||
}
|
||||
u := &userpb.User{
|
||||
Id: userID,
|
||||
Username: upn,
|
||||
Mail: mail,
|
||||
DisplayName: name,
|
||||
UidNumber: int64(uidNumber),
|
||||
GidNumber: int64(gidNumber),
|
||||
}
|
||||
|
||||
if err := m.cacheUserDetails(u); err != nil {
|
||||
log.Error().Err(err).Msg("rest: error caching user details")
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (m *manager) GetUser(ctx context.Context, uid *userpb.UserId, skipFetchingGroups bool) (*userpb.User, error) {
|
||||
if uid.GetTenantId() != "" {
|
||||
return nil, errtypes.NotSupported("tenant filter not supported in rest user manager")
|
||||
}
|
||||
u, err := m.fetchCachedUserDetails(uid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !skipFetchingGroups {
|
||||
userGroups, err := m.GetUserGroups(ctx, uid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u.Groups = userGroups
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (m *manager) GetUserByClaim(ctx context.Context, claim, value, tenantID string, skipFetchingGroups bool) (*userpb.User, error) {
|
||||
if tenantID != "" {
|
||||
return nil, errtypes.NotSupported("tenant filter not supported in rest user manager")
|
||||
}
|
||||
u, err := m.fetchCachedUserByParam(claim, value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !skipFetchingGroups {
|
||||
userGroups, err := m.GetUserGroups(ctx, u.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u.Groups = userGroups
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (m *manager) FindUsers(ctx context.Context, query, tenantID string, skipFetchingGroups bool) ([]*userpb.User, error) {
|
||||
if tenantID != "" {
|
||||
return nil, errtypes.NotSupported("tenant filter not supported in rest user manager")
|
||||
}
|
||||
|
||||
// Look at namespaces filters. If the query starts with:
|
||||
// "a" => look into primary/secondary/service accounts
|
||||
// "l" => look into lightweight/federated accounts
|
||||
// none => look into primary
|
||||
|
||||
parts := strings.SplitN(query, ":", 2)
|
||||
|
||||
var namespace string
|
||||
if len(parts) == 2 {
|
||||
// the query contains a namespace filter
|
||||
namespace, query = parts[0], parts[1]
|
||||
}
|
||||
|
||||
users, err := m.findCachedUsers(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userSlice := []*userpb.User{}
|
||||
|
||||
var accountsFilters []userpb.UserType
|
||||
switch namespace {
|
||||
case "":
|
||||
accountsFilters = []userpb.UserType{userpb.UserType_USER_TYPE_PRIMARY}
|
||||
case "a":
|
||||
accountsFilters = []userpb.UserType{userpb.UserType_USER_TYPE_PRIMARY, userpb.UserType_USER_TYPE_SECONDARY, userpb.UserType_USER_TYPE_SERVICE}
|
||||
case "l":
|
||||
accountsFilters = []userpb.UserType{userpb.UserType_USER_TYPE_LIGHTWEIGHT, userpb.UserType_USER_TYPE_FEDERATED}
|
||||
}
|
||||
|
||||
for _, u := range users {
|
||||
if isUserAnyType(u, accountsFilters) {
|
||||
userSlice = append(userSlice, u)
|
||||
}
|
||||
}
|
||||
|
||||
return userSlice, nil
|
||||
}
|
||||
|
||||
// isUserAnyType returns true if the user's type is one of types list
|
||||
func isUserAnyType(user *userpb.User, types []userpb.UserType) bool {
|
||||
for _, t := range types {
|
||||
if user.GetId().Type == t {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *manager) GetUserGroups(ctx context.Context, uid *userpb.UserId) ([]string, error) {
|
||||
groups, err := m.fetchCachedUserGroups(uid)
|
||||
if err == nil {
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/api/v1.0/Identity/%s/groups?recursive=true", m.conf.APIBaseURL, uid.OpaqueId)
|
||||
result, err := m.apiTokenManager.SendAPIGetRequest(ctx, url, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
groupData := result["data"].([]interface{})
|
||||
groups = []string{}
|
||||
|
||||
for _, g := range groupData {
|
||||
groupInfo, ok := g.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, errors.New("rest: error in type assertion")
|
||||
}
|
||||
name, ok := groupInfo["displayName"].(string)
|
||||
if ok {
|
||||
groups = append(groups, name)
|
||||
}
|
||||
}
|
||||
|
||||
if err = m.cacheUserGroups(uid, groups); err != nil {
|
||||
log := appctx.GetLogger(ctx)
|
||||
log.Error().Err(err).Msg("rest: error caching user groups")
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func (m *manager) IsInGroup(ctx context.Context, uid *userpb.UserId, group string) (bool, error) {
|
||||
userGroups, err := m.GetUserGroups(ctx, uid)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, g := range userGroups {
|
||||
if group == g {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func getUserType(userType, upn string) userpb.UserType {
|
||||
var t userpb.UserType
|
||||
switch userType {
|
||||
case "Application":
|
||||
t = userpb.UserType_USER_TYPE_APPLICATION
|
||||
case "Service":
|
||||
t = userpb.UserType_USER_TYPE_SERVICE
|
||||
case "Secondary":
|
||||
t = userpb.UserType_USER_TYPE_SECONDARY
|
||||
case "Person":
|
||||
switch {
|
||||
case strings.HasPrefix(upn, "guest"):
|
||||
t = userpb.UserType_USER_TYPE_LIGHTWEIGHT
|
||||
case strings.Contains(upn, "@"):
|
||||
t = userpb.UserType_USER_TYPE_FEDERATED
|
||||
default:
|
||||
t = userpb.UserType_USER_TYPE_PRIMARY
|
||||
}
|
||||
default:
|
||||
t = userpb.UserType_USER_TYPE_INVALID
|
||||
}
|
||||
return t
|
||||
|
||||
}
|
||||
-308
@@ -1,308 +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 utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
|
||||
link "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/opencloud-eu/reva/v2/pkg/conversions"
|
||||
)
|
||||
|
||||
// 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
|
||||
NotifyUploads bool
|
||||
NotifyUploadsExtraRecipients string
|
||||
}
|
||||
|
||||
// FormatGrantee formats a CS3API grantee to a string.
|
||||
func FormatGrantee(g *provider.Grantee) (int, string) {
|
||||
var granteeType int
|
||||
var formattedID string
|
||||
switch g.Type {
|
||||
case provider.GranteeType_GRANTEE_TYPE_USER:
|
||||
granteeType = 0
|
||||
formattedID = FormatUserID(g.GetUserId())
|
||||
case provider.GranteeType_GRANTEE_TYPE_GROUP:
|
||||
granteeType = 1
|
||||
formattedID = FormatGroupID(g.GetGroupId())
|
||||
default:
|
||||
granteeType = -1
|
||||
}
|
||||
return granteeType, formattedID
|
||||
}
|
||||
|
||||
// ExtractGrantee retrieves the CS3API grantee from a formatted string.
|
||||
func ExtractGrantee(ctx context.Context, gateway gatewayv1beta1.GatewayAPIClient, t int, g string) (*provider.Grantee, error) {
|
||||
var grantee provider.Grantee
|
||||
switch t {
|
||||
case 0:
|
||||
grantee.Type = provider.GranteeType_GRANTEE_TYPE_USER
|
||||
user, err := ExtractUserID(ctx, gateway, g)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
grantee.Id = &provider.Grantee_UserId{UserId: user}
|
||||
case 1:
|
||||
grantee.Type = provider.GranteeType_GRANTEE_TYPE_GROUP
|
||||
group, err := ExtractGroupID(ctx, gateway, g)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
grantee.Id = &provider.Grantee_GroupId{GroupId: group}
|
||||
default:
|
||||
grantee.Type = provider.GranteeType_GRANTEE_TYPE_INVALID
|
||||
}
|
||||
return &grantee, nil
|
||||
}
|
||||
|
||||
// ResourceTypeToItem maps a resource type to a string.
|
||||
func ResourceTypeToItem(r provider.ResourceType) string {
|
||||
switch r {
|
||||
case provider.ResourceType_RESOURCE_TYPE_FILE:
|
||||
return "file"
|
||||
case provider.ResourceType_RESOURCE_TYPE_CONTAINER:
|
||||
return "folder"
|
||||
case provider.ResourceType_RESOURCE_TYPE_REFERENCE:
|
||||
return "reference"
|
||||
case provider.ResourceType_RESOURCE_TYPE_SYMLINK:
|
||||
return "symlink"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// ResourceTypeToItemInt maps a resource type to an integer.
|
||||
func ResourceTypeToItemInt(r provider.ResourceType) int {
|
||||
switch r {
|
||||
case provider.ResourceType_RESOURCE_TYPE_CONTAINER:
|
||||
return 0
|
||||
case provider.ResourceType_RESOURCE_TYPE_FILE:
|
||||
return 1
|
||||
default:
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
// SharePermToInt maps read/write permissions to an integer.
|
||||
func SharePermToInt(p *provider.ResourcePermissions) int {
|
||||
var perm int
|
||||
switch {
|
||||
case p.InitiateFileUpload && !p.InitiateFileDownload:
|
||||
perm = 4
|
||||
case p.InitiateFileUpload:
|
||||
perm = 15
|
||||
case p.InitiateFileDownload:
|
||||
perm = 1
|
||||
}
|
||||
// TODO map denials and resharing; currently, denials are mapped to 0
|
||||
return perm
|
||||
}
|
||||
|
||||
// IntTosharePerm retrieves read/write permissions from an integer.
|
||||
func IntTosharePerm(p int, itemType string) *provider.ResourcePermissions {
|
||||
switch p {
|
||||
case 1:
|
||||
return conversions.NewViewerRole().CS3ResourcePermissions()
|
||||
case 15:
|
||||
if itemType == "folder" {
|
||||
return conversions.NewEditorRole().CS3ResourcePermissions()
|
||||
}
|
||||
return conversions.NewFileEditorRole().CS3ResourcePermissions()
|
||||
case 4:
|
||||
return conversions.NewUploaderRole().CS3ResourcePermissions()
|
||||
default:
|
||||
// TODO we may have other options, for now this is a denial
|
||||
return &provider.ResourcePermissions{}
|
||||
}
|
||||
}
|
||||
|
||||
// IntToShareState retrieves the received share state from an integer.
|
||||
func IntToShareState(g int) collaboration.ShareState {
|
||||
switch g {
|
||||
case 0:
|
||||
return collaboration.ShareState_SHARE_STATE_PENDING
|
||||
case 1:
|
||||
return collaboration.ShareState_SHARE_STATE_ACCEPTED
|
||||
case -1:
|
||||
return collaboration.ShareState_SHARE_STATE_REJECTED
|
||||
default:
|
||||
return collaboration.ShareState_SHARE_STATE_INVALID
|
||||
}
|
||||
}
|
||||
|
||||
// FormatUserID formats a CS3API user ID to a string.
|
||||
func FormatUserID(u *userpb.UserId) string {
|
||||
return u.OpaqueId
|
||||
}
|
||||
|
||||
// ExtractUserID retrieves a CS3API user ID from a string.
|
||||
func ExtractUserID(ctx context.Context, gateway gatewayv1beta1.GatewayAPIClient, u string) (*userpb.UserId, error) {
|
||||
userRes, err := gateway.GetUser(ctx, &userpb.GetUserRequest{
|
||||
UserId: &userpb.UserId{OpaqueId: u},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if userRes.Status.Code != rpcv1beta1.Code_CODE_OK {
|
||||
return nil, errors.New(userRes.Status.Message)
|
||||
}
|
||||
|
||||
return userRes.User.Id, nil
|
||||
}
|
||||
|
||||
// FormatGroupID formats a CS3API group ID to a string.
|
||||
func FormatGroupID(u *grouppb.GroupId) string {
|
||||
return u.OpaqueId
|
||||
}
|
||||
|
||||
// ExtractGroupID retrieves a CS3API group ID from a string.
|
||||
func ExtractGroupID(ctx context.Context, gateway gatewayv1beta1.GatewayAPIClient, u string) (*grouppb.GroupId, error) {
|
||||
groupRes, err := gateway.GetGroup(ctx, &grouppb.GetGroupRequest{
|
||||
GroupId: &grouppb.GroupId{OpaqueId: u},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if groupRes.Status.Code != rpcv1beta1.Code_CODE_OK {
|
||||
return nil, errors.New(groupRes.Status.Message)
|
||||
}
|
||||
return groupRes.Group.Id, nil
|
||||
}
|
||||
|
||||
// ConvertToCS3Share converts a DBShare to a CS3API collaboration share.
|
||||
func ConvertToCS3Share(ctx context.Context, gateway gatewayv1beta1.GatewayAPIClient, s DBShare) (*collaboration.Share, error) {
|
||||
ts := &typespb.Timestamp{
|
||||
Seconds: uint64(s.STime),
|
||||
}
|
||||
owner, err := ExtractUserID(ctx, gateway, s.UIDOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
creator, err := ExtractUserID(ctx, gateway, s.UIDInitiator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
grantee, err := ExtractGrantee(ctx, gateway, s.ShareType, s.ShareWith)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &collaboration.Share{
|
||||
Id: &collaboration.ShareId{
|
||||
OpaqueId: s.ID,
|
||||
},
|
||||
//ResourceId: &provider.Reference{StorageId: s.Prefix, NodeId: s.ItemSource},
|
||||
ResourceId: &provider.ResourceId{
|
||||
StorageId: s.Prefix,
|
||||
OpaqueId: s.ItemSource,
|
||||
},
|
||||
Permissions: &collaboration.SharePermissions{Permissions: IntTosharePerm(s.Permissions, s.ItemType)},
|
||||
Grantee: grantee,
|
||||
Owner: owner,
|
||||
Creator: creator,
|
||||
Ctime: ts,
|
||||
Mtime: ts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ConvertToCS3ReceivedShare converts a DBShare to a CS3API collaboration received share.
|
||||
func ConvertToCS3ReceivedShare(ctx context.Context, gateway gatewayv1beta1.GatewayAPIClient, s DBShare) (*collaboration.ReceivedShare, error) {
|
||||
share, err := ConvertToCS3Share(ctx, gateway, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &collaboration.ReceivedShare{
|
||||
Share: share,
|
||||
State: IntToShareState(s.State),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ConvertToCS3PublicShare converts a DBShare to a CS3API public share.
|
||||
func ConvertToCS3PublicShare(ctx context.Context, gateway gatewayv1beta1.GatewayAPIClient, s DBShare) (*link.PublicShare, error) {
|
||||
ts := &typespb.Timestamp{
|
||||
Seconds: uint64(s.STime),
|
||||
}
|
||||
pwd := s.ShareWith != ""
|
||||
var expires *typespb.Timestamp
|
||||
if s.Expiration != "" {
|
||||
t, err := time.Parse("2006-01-02 15:04:05", s.Expiration)
|
||||
if err == nil {
|
||||
expires = &typespb.Timestamp{
|
||||
Seconds: uint64(t.Unix()),
|
||||
}
|
||||
}
|
||||
}
|
||||
owner, err := ExtractUserID(ctx, gateway, s.UIDOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
creator, err := ExtractUserID(ctx, gateway, s.UIDInitiator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &link.PublicShare{
|
||||
Id: &link.PublicShareId{
|
||||
OpaqueId: s.ID,
|
||||
},
|
||||
ResourceId: &provider.ResourceId{
|
||||
StorageId: s.Prefix,
|
||||
OpaqueId: s.ItemSource,
|
||||
},
|
||||
Permissions: &link.PublicSharePermissions{Permissions: IntTosharePerm(s.Permissions, s.ItemType)},
|
||||
Owner: owner,
|
||||
Creator: creator,
|
||||
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,
|
||||
}, nil
|
||||
}
|
||||
-172
@@ -1,172 +0,0 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rhttp"
|
||||
)
|
||||
|
||||
// APITokenManager stores config related to api management
|
||||
type APITokenManager struct {
|
||||
oidcToken OIDCToken
|
||||
conf *config
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// OIDCToken stores the OIDC token used to authenticate requests to the REST API service
|
||||
type OIDCToken struct {
|
||||
sync.Mutex // concurrent access to apiToken and tokenExpirationTime
|
||||
apiToken string
|
||||
tokenExpirationTime time.Time
|
||||
}
|
||||
|
||||
type config struct {
|
||||
TargetAPI string
|
||||
OIDCTokenEndpoint string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
}
|
||||
|
||||
// InitAPITokenManager initializes a new APITokenManager
|
||||
func InitAPITokenManager(targetAPI, oidcTokenEndpoint, clientID, clientSecret string) *APITokenManager {
|
||||
return &APITokenManager{
|
||||
conf: &config{
|
||||
TargetAPI: targetAPI,
|
||||
OIDCTokenEndpoint: oidcTokenEndpoint,
|
||||
ClientID: clientID,
|
||||
ClientSecret: clientSecret,
|
||||
},
|
||||
client: rhttp.GetHTTPClient(
|
||||
rhttp.Timeout(10*time.Second),
|
||||
rhttp.Insecure(true),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *APITokenManager) renewAPIToken(ctx context.Context, forceRenewal bool) error {
|
||||
// Received tokens have an expiration time of 20 minutes.
|
||||
// Take a couple of seconds as buffer time for the API call to complete
|
||||
if forceRenewal || a.oidcToken.tokenExpirationTime.Before(time.Now().Add(time.Second*time.Duration(2))) {
|
||||
token, expiration, err := a.getAPIToken(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.oidcToken.Lock()
|
||||
defer a.oidcToken.Unlock()
|
||||
|
||||
a.oidcToken.apiToken = token
|
||||
a.oidcToken.tokenExpirationTime = expiration
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *APITokenManager) getAPIToken(ctx context.Context) (string, time.Time, error) {
|
||||
|
||||
params := url.Values{
|
||||
"grant_type": {"client_credentials"},
|
||||
"audience": {a.conf.TargetAPI},
|
||||
}
|
||||
|
||||
httpReq, err := http.NewRequest("POST", a.conf.OIDCTokenEndpoint, strings.NewReader(params.Encode()))
|
||||
if err != nil {
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
httpReq.SetBasicAuth(a.conf.ClientID, a.conf.ClientSecret)
|
||||
httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value")
|
||||
|
||||
httpRes, err := a.client.Do(httpReq)
|
||||
if err != nil {
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
defer httpRes.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(httpRes.Body)
|
||||
if err != nil {
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
if httpRes.StatusCode < 200 || httpRes.StatusCode > 299 {
|
||||
return "", time.Time{}, errors.New("rest: get token endpoint returned " + httpRes.Status)
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
err = json.Unmarshal(body, &result)
|
||||
if err != nil {
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
|
||||
expirationSecs := result["expires_in"].(float64)
|
||||
expirationTime := time.Now().Add(time.Second * time.Duration(expirationSecs))
|
||||
return result["access_token"].(string), expirationTime, nil
|
||||
}
|
||||
|
||||
// SendAPIGetRequest makes an API GET Request to the passed URL
|
||||
func (a *APITokenManager) SendAPIGetRequest(ctx context.Context, url string, forceRenewal bool) (map[string]interface{}, error) {
|
||||
err := a.renewAPIToken(ctx, forceRenewal)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpReq, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We don't need to take the lock when reading apiToken, because if we reach here,
|
||||
// the token is valid at least for a couple of seconds. Even if another request modifies
|
||||
// the token and expiration time while this request is in progress, the current token will still be valid.
|
||||
httpReq.Header.Set("Authorization", "Bearer "+a.oidcToken.apiToken)
|
||||
|
||||
httpRes, err := a.client.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer httpRes.Body.Close()
|
||||
|
||||
if httpRes.StatusCode == http.StatusUnauthorized {
|
||||
// The token is no longer valid, try renewing it
|
||||
return a.SendAPIGetRequest(ctx, url, true)
|
||||
}
|
||||
if httpRes.StatusCode < 200 || httpRes.StatusCode > 299 {
|
||||
return nil, errors.New("rest: API request returned " + httpRes.Status)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(httpRes.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
err = json.Unmarshal(body, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
// Copyright 2026 OpenCloud GmbH <mail@opencloud.eu>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package events
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
)
|
||||
|
||||
// FavoriteAdded is emitted when a user added a resource to their favorites
|
||||
type FavoriteAdded struct {
|
||||
Ref *provider.Reference
|
||||
Executant *user.UserId
|
||||
UserID *user.UserId
|
||||
Timestamp *types.Timestamp
|
||||
}
|
||||
|
||||
// Unmarshal to fulfill umarshaller interface
|
||||
func (FavoriteAdded) Unmarshal(v []byte) (interface{}, error) {
|
||||
e := FavoriteAdded{}
|
||||
err := json.Unmarshal(v, &e)
|
||||
return e, err
|
||||
}
|
||||
|
||||
// FavoriteRemoved is emitted when a user removed a resource from their favorites
|
||||
type FavoriteRemoved struct {
|
||||
Ref *provider.Reference
|
||||
Executant *user.UserId
|
||||
UserID *user.UserId
|
||||
Timestamp *types.Timestamp
|
||||
}
|
||||
|
||||
// Unmarshal to fulfill umarshaller interface
|
||||
func (FavoriteRemoved) Unmarshal(v []byte) (interface{}, error) {
|
||||
e := FavoriteRemoved{}
|
||||
err := json.Unmarshal(v, &e)
|
||||
return e, err
|
||||
}
|
||||
Generated
Vendored
-1
@@ -22,6 +22,5 @@ import (
|
||||
// Load core share manager drivers.
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/ocm/invite/repository/json"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/ocm/invite/repository/memory"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/ocm/invite/repository/sql"
|
||||
// Add your own here.
|
||||
)
|
||||
|
||||
-251
@@ -1,251 +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 sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1"
|
||||
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/go-sql-driver/mysql"
|
||||
conversions "github.com/opencloud-eu/reva/v2/pkg/cbox/utils"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/ocm/invite"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils/cfg"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/ocm/invite/repository/registry"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// This module implement the invite.Repository interface as a mysql driver.
|
||||
//
|
||||
// The OCM Invitation tokens are saved in the table:
|
||||
// ocm_tokens(*token*, initiator, expiration, description)
|
||||
//
|
||||
// The OCM remote user are saved in the table:
|
||||
// ocm_remote_users(*initiator*, *opaque_user_id*, *idp*, email, display_name)
|
||||
|
||||
func init() {
|
||||
registry.Register("sql", New)
|
||||
}
|
||||
|
||||
type mgr struct {
|
||||
c *config
|
||||
db *sql.DB
|
||||
client gatewayv1beta1.GatewayAPIClient
|
||||
}
|
||||
|
||||
type config struct {
|
||||
DBUsername string `mapstructure:"db_username"`
|
||||
DBPassword string `mapstructure:"db_password"`
|
||||
DBAddress string `mapstructure:"db_address"`
|
||||
DBName string `mapstructure:"db_name"`
|
||||
GatewaySvc string `mapstructure:"gatewaysvc"`
|
||||
}
|
||||
|
||||
func (c *config) ApplyDefaults() {
|
||||
c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc)
|
||||
}
|
||||
|
||||
// New creates a sql repository for ocm tokens and users.
|
||||
func New(m map[string]interface{}) (invite.Repository, error) {
|
||||
var c config
|
||||
if err := cfg.Decode(m, &c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s?parseTime=true", c.DBUsername, c.DBPassword, c.DBAddress, c.DBName))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "sql: error opening connection to mysql database")
|
||||
}
|
||||
|
||||
gw, err := pool.GetGatewayServiceClient(c.GatewaySvc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mgr := mgr{
|
||||
c: &c,
|
||||
db: db,
|
||||
client: gw,
|
||||
}
|
||||
return &mgr, nil
|
||||
}
|
||||
|
||||
// AddToken stores the token in the repository.
|
||||
func (m *mgr) AddToken(ctx context.Context, token *invitepb.InviteToken) error {
|
||||
query := "INSERT INTO ocm_tokens SET token=?,initiator=?,expiration=?,description=?"
|
||||
_, err := m.db.ExecContext(ctx, query, token.Token, conversions.FormatUserID(token.UserId), timestampToTime(token.Expiration), token.Description)
|
||||
return err
|
||||
}
|
||||
|
||||
func timestampToTime(t *types.Timestamp) time.Time {
|
||||
return time.Unix(int64(t.Seconds), int64(t.Nanos))
|
||||
}
|
||||
|
||||
type dbToken struct {
|
||||
Token string
|
||||
Initiator string
|
||||
Expiration time.Time
|
||||
Description string
|
||||
}
|
||||
|
||||
// GetToken gets the token from the repository.
|
||||
func (m *mgr) GetToken(ctx context.Context, token string) (*invitepb.InviteToken, error) {
|
||||
query := "SELECT token, initiator, expiration, description FROM ocm_tokens where token=?"
|
||||
|
||||
var tkn dbToken
|
||||
if err := m.db.QueryRowContext(ctx, query, token).Scan(&tkn.Token, &tkn.Initiator, &tkn.Expiration, &tkn.Description); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, invite.ErrTokenNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return m.convertToInviteToken(ctx, tkn)
|
||||
}
|
||||
|
||||
func (m *mgr) convertToInviteToken(ctx context.Context, tkn dbToken) (*invitepb.InviteToken, error) {
|
||||
user, err := conversions.ExtractUserID(ctx, m.client, tkn.Initiator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &invitepb.InviteToken{
|
||||
Token: tkn.Token,
|
||||
UserId: user,
|
||||
Expiration: &types.Timestamp{
|
||||
Seconds: uint64(tkn.Expiration.Unix()),
|
||||
},
|
||||
Description: tkn.Description,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *mgr) ListTokens(ctx context.Context, initiator *userpb.UserId) ([]*invitepb.InviteToken, error) {
|
||||
query := "SELECT token, initiator, expiration, description FROM ocm_tokens WHERE initiator=? AND expiration > NOW()"
|
||||
|
||||
tokens := []*invitepb.InviteToken{}
|
||||
rows, err := m.db.QueryContext(ctx, query, conversions.FormatUserID(initiator))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tkn dbToken
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(&tkn.Token, &tkn.Initiator, &tkn.Expiration, &tkn.Description); err != nil {
|
||||
continue
|
||||
}
|
||||
token, err := m.convertToInviteToken(ctx, tkn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tokens = append(tokens, token)
|
||||
}
|
||||
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
// AddRemoteUser stores the remote user.
|
||||
func (m *mgr) AddRemoteUser(ctx context.Context, initiator *userpb.UserId, remoteUser *userpb.User) error {
|
||||
query := "INSERT INTO ocm_remote_users SET initiator=?, opaque_user_id=?, idp=?, email=?, display_name=?"
|
||||
if _, err := m.db.ExecContext(ctx, query, conversions.FormatUserID(initiator), conversions.FormatUserID(remoteUser.Id), remoteUser.Id.Idp, remoteUser.Mail, remoteUser.DisplayName); err != nil {
|
||||
// check if the user already exist in the db
|
||||
// https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html#error_er_dup_entry
|
||||
var e *mysql.MySQLError
|
||||
if errors.As(err, &e) && e.Number == 1062 {
|
||||
return invite.ErrUserAlreadyAccepted
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type dbOCMUser struct {
|
||||
OpaqueUserID string
|
||||
Idp string
|
||||
Email string
|
||||
DisplayName string
|
||||
}
|
||||
|
||||
// GetRemoteUser retrieves details about a remote user who has accepted an invite to share.
|
||||
func (m *mgr) GetRemoteUser(ctx context.Context, initiator *userpb.UserId, remoteUserID *userpb.UserId) (*userpb.User, error) {
|
||||
query := "SELECT opaque_user_id, idp, email, display_name FROM ocm_remote_users WHERE initiator=? AND opaque_user_id=? AND idp=?"
|
||||
|
||||
var user dbOCMUser
|
||||
if err := m.db.QueryRowContext(ctx, query, conversions.FormatUserID(initiator), conversions.FormatUserID(remoteUserID), remoteUserID.Idp).
|
||||
Scan(&user.OpaqueUserID, &user.Idp, &user.Email, &user.DisplayName); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, errtypes.NotFound(remoteUserID.OpaqueId)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return user.toCS3User(), nil
|
||||
}
|
||||
|
||||
func (u *dbOCMUser) toCS3User() *userpb.User {
|
||||
return &userpb.User{
|
||||
Id: &userpb.UserId{
|
||||
Idp: u.Idp,
|
||||
OpaqueId: u.OpaqueUserID,
|
||||
Type: userpb.UserType_USER_TYPE_FEDERATED,
|
||||
},
|
||||
Mail: u.Email,
|
||||
DisplayName: u.DisplayName,
|
||||
}
|
||||
}
|
||||
|
||||
// FindRemoteUsers finds remote users who have accepted invites based on their attributes.
|
||||
func (m *mgr) FindRemoteUsers(ctx context.Context, initiator *userpb.UserId, attr string) ([]*userpb.User, error) {
|
||||
// TODO: (gdelmont) this query can get really slow in case the number of rows is too high.
|
||||
// For the time being this is not expected, but if in future this happens, consider to add
|
||||
// a fulltext index.
|
||||
query := "SELECT opaque_user_id, idp, email, display_name FROM ocm_remote_users WHERE initiator=? AND (opaque_user_id LIKE ? OR idp LIKE ? OR email LIKE ? OR display_name LIKE ?)"
|
||||
s := "%" + attr + "%"
|
||||
params := []any{conversions.FormatUserID(initiator), s, s, s, s}
|
||||
|
||||
rows, err := m.db.QueryContext(ctx, query, params...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var u dbOCMUser
|
||||
var users []*userpb.User
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(&u.OpaqueUserID, &u.Idp, &u.Email, &u.DisplayName); err != nil {
|
||||
continue
|
||||
}
|
||||
users = append(users, u.toCS3User())
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (m *mgr) DeleteRemoteUser(ctx context.Context, initiator *userpb.UserId, remoteUser *userpb.UserId) error {
|
||||
query := "DELETE FROM ocm_remote_users WHERE initiator=? AND opaque_user_id=? AND idp=?"
|
||||
_, err := m.db.ExecContext(ctx, query, conversions.FormatUserID(initiator), conversions.FormatUserID(remoteUser), remoteUser.Idp)
|
||||
return err
|
||||
}
|
||||
+8
@@ -609,3 +609,11 @@ func (d *driver) UpdateStorageSpace(ctx context.Context, req *provider.UpdateSto
|
||||
func (d *driver) DeleteStorageSpace(ctx context.Context, req *provider.DeleteStorageSpaceRequest) error {
|
||||
return errtypes.NotSupported("operation not supported")
|
||||
}
|
||||
|
||||
func (d *driver) AddFavorite(ctx context.Context, ref *provider.Reference, userID *userpb.UserId) error {
|
||||
return errtypes.NotSupported("AddFavorite not implemented")
|
||||
}
|
||||
|
||||
func (d *driver) RemoveFavorite(ctx context.Context, ref *provider.Reference, userID *userpb.UserId) error {
|
||||
return errtypes.NotSupported("RemoveFavorite not implemented")
|
||||
}
|
||||
|
||||
-1
@@ -22,6 +22,5 @@ import (
|
||||
// Load core share manager drivers.
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/publicshare/manager/json"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/publicshare/manager/memory"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/publicshare/manager/owncloudsql"
|
||||
// Add your own here
|
||||
)
|
||||
|
||||
Generated
Vendored
-223
@@ -1,223 +0,0 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package owncloudsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
link "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/jellydator/ttlcache/v2"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/conversions"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
)
|
||||
|
||||
// DBShare stores information about user and public shares.
|
||||
type DBShare struct {
|
||||
ID string
|
||||
UIDOwner string
|
||||
UIDInitiator string
|
||||
ItemStorage string
|
||||
FileSource string
|
||||
ItemType string // 'file' or 'folder'
|
||||
ShareWith string
|
||||
Token string
|
||||
Expiration string
|
||||
Permissions int
|
||||
ShareType int
|
||||
ShareName string
|
||||
STime int
|
||||
FileTarget string
|
||||
RejectedBy string
|
||||
State int
|
||||
Parent int
|
||||
}
|
||||
|
||||
// UserConverter describes an interface for converting user ids to names and back
|
||||
type UserConverter interface {
|
||||
UserNameToUserID(ctx context.Context, username string) (*userpb.UserId, error)
|
||||
UserIDToUserName(ctx context.Context, userid *userpb.UserId) (string, error)
|
||||
}
|
||||
|
||||
// GatewayUserConverter converts usernames and ids using the gateway
|
||||
type GatewayUserConverter struct {
|
||||
gwAddr string
|
||||
|
||||
IDCache *ttlcache.Cache
|
||||
NameCache *ttlcache.Cache
|
||||
}
|
||||
|
||||
// NewGatewayUserConverter returns a instance of GatewayUserConverter
|
||||
func NewGatewayUserConverter(gwAddr string) *GatewayUserConverter {
|
||||
IDCache := ttlcache.NewCache()
|
||||
_ = IDCache.SetTTL(30 * time.Second)
|
||||
IDCache.SkipTTLExtensionOnHit(true)
|
||||
NameCache := ttlcache.NewCache()
|
||||
_ = NameCache.SetTTL(30 * time.Second)
|
||||
NameCache.SkipTTLExtensionOnHit(true)
|
||||
|
||||
return &GatewayUserConverter{
|
||||
gwAddr: gwAddr,
|
||||
IDCache: IDCache,
|
||||
NameCache: NameCache,
|
||||
}
|
||||
}
|
||||
|
||||
// UserIDToUserName converts a user ID to an username
|
||||
func (c *GatewayUserConverter) UserIDToUserName(ctx context.Context, userid *userpb.UserId) (string, error) {
|
||||
username, err := c.NameCache.Get(userid.String())
|
||||
if err == nil {
|
||||
return username.(string), nil
|
||||
}
|
||||
|
||||
gwConn, err := pool.GetGatewayServiceClient(c.gwAddr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
getUserResponse, err := gwConn.GetUser(ctx, &userpb.GetUserRequest{
|
||||
UserId: userid,
|
||||
SkipFetchingUserGroups: true,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if getUserResponse.Status.Code != rpc.Code_CODE_OK {
|
||||
return "", status.NewErrorFromCode(getUserResponse.Status.Code, "gateway")
|
||||
}
|
||||
_ = c.NameCache.Set(userid.String(), getUserResponse.User.Username)
|
||||
return getUserResponse.User.Username, nil
|
||||
}
|
||||
|
||||
// UserNameToUserID converts a username to an user ID
|
||||
func (c *GatewayUserConverter) UserNameToUserID(ctx context.Context, username string) (*userpb.UserId, error) {
|
||||
id, err := c.IDCache.Get(username)
|
||||
if err == nil {
|
||||
return id.(*userpb.UserId), nil
|
||||
}
|
||||
|
||||
gwConn, err := pool.GetGatewayServiceClient(c.gwAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
getUserResponse, err := gwConn.GetUserByClaim(ctx, &userpb.GetUserByClaimRequest{
|
||||
Claim: "username",
|
||||
Value: username,
|
||||
SkipFetchingUserGroups: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if getUserResponse.Status.Code != rpc.Code_CODE_OK {
|
||||
return nil, status.NewErrorFromCode(getUserResponse.Status.Code, "gateway")
|
||||
}
|
||||
_ = c.IDCache.Set(username, getUserResponse.User.Id)
|
||||
return getUserResponse.User.Id, nil
|
||||
}
|
||||
|
||||
func resourceTypeToItem(r provider.ResourceType) string {
|
||||
switch r {
|
||||
case provider.ResourceType_RESOURCE_TYPE_FILE:
|
||||
return "file"
|
||||
case provider.ResourceType_RESOURCE_TYPE_CONTAINER:
|
||||
return "folder"
|
||||
case provider.ResourceType_RESOURCE_TYPE_REFERENCE:
|
||||
return "reference"
|
||||
case provider.ResourceType_RESOURCE_TYPE_SYMLINK:
|
||||
return "symlink"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func sharePermToInt(p *provider.ResourcePermissions) int {
|
||||
return int(conversions.RoleFromResourcePermissions(p, true).OCSPermissions())
|
||||
}
|
||||
|
||||
func intTosharePerm(p int) (*provider.ResourcePermissions, error) {
|
||||
perms, err := conversions.NewPermissions(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conversions.RoleFromOCSPermissions(perms, nil).CS3ResourcePermissions(), nil
|
||||
}
|
||||
|
||||
func formatUserID(u *userpb.UserId) string {
|
||||
return u.OpaqueId
|
||||
}
|
||||
|
||||
// ConvertToCS3PublicShare converts a DBShare to a CS3API public share
|
||||
func (m *mgr) ConvertToCS3PublicShare(ctx context.Context, s DBShare) (*link.PublicShare, error) {
|
||||
ts := &typespb.Timestamp{
|
||||
Seconds: uint64(s.STime),
|
||||
}
|
||||
permissions, err := intTosharePerm(s.Permissions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
owner, err := m.userConverter.UserNameToUserID(ctx, s.UIDOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var creator *userpb.UserId
|
||||
if s.UIDOwner == s.UIDInitiator {
|
||||
creator = owner
|
||||
} else {
|
||||
creator, err = m.userConverter.UserNameToUserID(ctx, s.UIDOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
pwd := s.ShareWith != ""
|
||||
var expires *typespb.Timestamp
|
||||
if s.Expiration != "" {
|
||||
t, err := time.Parse("2006-01-02 15:04:05", s.Expiration)
|
||||
if err != nil {
|
||||
t, err = time.Parse("2006-01-02 15:04:05-07:00", s.Expiration)
|
||||
}
|
||||
if err == nil {
|
||||
expires = &typespb.Timestamp{
|
||||
Seconds: uint64(t.Unix()),
|
||||
}
|
||||
}
|
||||
}
|
||||
return &link.PublicShare{
|
||||
Id: &link.PublicShareId{
|
||||
OpaqueId: s.ID,
|
||||
},
|
||||
ResourceId: &provider.ResourceId{
|
||||
SpaceId: s.ItemStorage,
|
||||
OpaqueId: s.FileSource,
|
||||
},
|
||||
Permissions: &link.PublicSharePermissions{Permissions: permissions},
|
||||
Owner: owner,
|
||||
Creator: creator,
|
||||
Token: s.Token,
|
||||
DisplayName: s.ShareName,
|
||||
PasswordProtected: pwd,
|
||||
Expiration: expires,
|
||||
Ctime: ts,
|
||||
Mtime: ts,
|
||||
}, nil
|
||||
}
|
||||
Generated
Vendored
-533
@@ -1,533 +0,0 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
// Package owncloudsql implements a publiclink share manager backed by an existing ownCloud 10 database
|
||||
//
|
||||
// The SQL queries use `coalesce({column_identifier}, ”) as {column_identifier}` to read an emptystring
|
||||
// instead of null values, which better fits the golang default values.
|
||||
package owncloudsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
link "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/mitchellh/mapstructure"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/publicshare"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/publicshare/manager/registry"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
// Provides mysql drivers
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
const (
|
||||
publicShareType = 3
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register("owncloudsql", NewMysql)
|
||||
}
|
||||
|
||||
// Config configures an owncloudsql publicshare manager
|
||||
type Config struct {
|
||||
GatewayAddr string `mapstructure:"gateway_addr"`
|
||||
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"`
|
||||
EnableExpiredSharesCleanup bool `mapstructure:"enable_expired_shares_cleanup"`
|
||||
SharePasswordHashCost int `mapstructure:"password_hash_cost"`
|
||||
}
|
||||
|
||||
type mgr struct {
|
||||
driver string
|
||||
db *sql.DB
|
||||
c Config
|
||||
userConverter UserConverter
|
||||
}
|
||||
|
||||
// NewMysql returns a new publicshare manager connection to a mysql database
|
||||
func NewMysql(m map[string]interface{}) (publicshare.Manager, error) {
|
||||
c, err := parseConfig(m)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "error creating a new manager")
|
||||
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
|
||||
}
|
||||
|
||||
userConverter := NewGatewayUserConverter(sharedconf.GetGatewaySVC(c.GatewayAddr))
|
||||
|
||||
return New("mysql", db, *c, userConverter)
|
||||
}
|
||||
|
||||
// New returns a new Cache instance connecting to the given sql.DB
|
||||
func New(driver string, db *sql.DB, c Config, userConverter UserConverter) (publicshare.Manager, error) {
|
||||
if c.SharePasswordHashCost == 0 {
|
||||
c.SharePasswordHashCost = bcrypt.DefaultCost
|
||||
}
|
||||
return &mgr{
|
||||
driver: driver,
|
||||
db: db,
|
||||
c: c,
|
||||
userConverter: userConverter,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseConfig(m map[string]interface{}) (*Config, error) {
|
||||
c := &Config{}
|
||||
if err := mapstructure.Decode(m, c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (m *mgr) CreatePublicShare(ctx context.Context, u *user.User, rInfo *provider.ResourceInfo, g *link.Grant) (*link.PublicShare, error) {
|
||||
|
||||
tkn := utils.RandString(15)
|
||||
now := time.Now().Unix()
|
||||
|
||||
displayName := tkn
|
||||
if rInfo.ArbitraryMetadata != nil && rInfo.ArbitraryMetadata.Metadata["name"] != "" {
|
||||
displayName = rInfo.ArbitraryMetadata.Metadata["name"]
|
||||
}
|
||||
createdAt := &typespb.Timestamp{
|
||||
Seconds: uint64(now),
|
||||
}
|
||||
|
||||
creator := u.Username
|
||||
owner, err := m.userConverter.UserIDToUserName(ctx, rInfo.Owner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
permissions := sharePermToInt(g.Permissions.Permissions)
|
||||
itemType := resourceTypeToItem(rInfo.Type)
|
||||
|
||||
itemSource := rInfo.Id.OpaqueId
|
||||
fileSource, err := strconv.ParseUint(itemSource, 10, 64)
|
||||
if err != nil {
|
||||
// it can be the case that the item source may be a character string
|
||||
// we leave fileSource blank in that case
|
||||
fileSource = 0
|
||||
}
|
||||
|
||||
columns := "share_type,uid_owner,uid_initiator,item_type,item_source,file_source,permissions,stime,token,share_name"
|
||||
placeholders := "?,?,?,?,?,?,?,?,?,?"
|
||||
params := []interface{}{publicShareType, owner, creator, itemType, itemSource, fileSource, permissions, now, tkn, displayName}
|
||||
|
||||
var passwordProtected bool
|
||||
password := g.Password
|
||||
if password != "" {
|
||||
password, err = hashPassword(password, m.c.SharePasswordHashCost)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not hash share password")
|
||||
}
|
||||
passwordProtected = true
|
||||
|
||||
columns += ",share_with"
|
||||
placeholders += ",?"
|
||||
params = append(params, password)
|
||||
}
|
||||
|
||||
if g.Expiration != nil && g.Expiration.Seconds != 0 {
|
||||
t := time.Unix(int64(g.Expiration.Seconds), 0)
|
||||
columns += ",expiration"
|
||||
placeholders += ",?"
|
||||
params = append(params, t)
|
||||
}
|
||||
|
||||
query := "INSERT INTO oc_share (" + columns + ") VALUES (" + placeholders + ")"
|
||||
stmt, err := m.db.Prepare(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, err := stmt.Exec(params...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lastID, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &link.PublicShare{
|
||||
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,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// owncloud 10 prefixes the hash with `1|`
|
||||
func hashPassword(password string, cost int) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), cost)
|
||||
return "1|" + string(bytes), err
|
||||
}
|
||||
|
||||
// UpdatePublicShare updates the expiration date, permissions and Mtime
|
||||
func (m *mgr) UpdatePublicShare(ctx context.Context, u *user.User, req *link.UpdatePublicShareRequest) (*link.PublicShare, error) {
|
||||
query := "update oc_share set "
|
||||
paramsMap := map[string]interface{}{}
|
||||
params := []interface{}{}
|
||||
|
||||
now := time.Now().Unix()
|
||||
uid := u.Username
|
||||
|
||||
switch req.GetUpdate().GetType() {
|
||||
case link.UpdatePublicShareRequest_Update_TYPE_DISPLAYNAME:
|
||||
paramsMap["share_name"] = req.Update.GetDisplayName()
|
||||
case link.UpdatePublicShareRequest_Update_TYPE_PERMISSIONS:
|
||||
paramsMap["permissions"] = sharePermToInt(req.Update.GetGrant().GetPermissions().Permissions)
|
||||
case link.UpdatePublicShareRequest_Update_TYPE_EXPIRATION:
|
||||
paramsMap["expiration"] = time.Unix(int64(req.Update.GetGrant().Expiration.Seconds), 0)
|
||||
case link.UpdatePublicShareRequest_Update_TYPE_PASSWORD:
|
||||
if req.Update.GetGrant().Password == "" {
|
||||
paramsMap["share_with"] = ""
|
||||
} else {
|
||||
h, err := hashPassword(req.Update.GetGrant().Password, m.c.SharePasswordHashCost)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not hash share password")
|
||||
}
|
||||
paramsMap["share_with"] = h
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid update type: %v", req.GetUpdate().GetType())
|
||||
}
|
||||
|
||||
for k, v := range paramsMap {
|
||||
query += k + "=?"
|
||||
params = append(params, v)
|
||||
}
|
||||
|
||||
switch {
|
||||
case req.Ref.GetId() != nil:
|
||||
query += ",stime=? where id=? AND (uid_owner=? or uid_initiator=?)"
|
||||
params = append(params, now, req.Ref.GetId().OpaqueId, uid, uid)
|
||||
case req.Ref.GetToken() != "":
|
||||
query += ",stime=? where token=? AND (uid_owner=? or uid_initiator=?)"
|
||||
params = append(params, now, req.Ref.GetToken(), uid, uid)
|
||||
default:
|
||||
return nil, errtypes.NotFound(req.Ref.String())
|
||||
}
|
||||
|
||||
stmt, err := m.db.Prepare(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = stmt.Exec(params...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m.GetPublicShare(ctx, u, req.Ref, false)
|
||||
}
|
||||
|
||||
func (m *mgr) GetPublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference, sign bool) (share *link.PublicShare, err error) {
|
||||
|
||||
ps, err := m.getWithPassword(ctx, ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if publicshare.IsExpired(&ps.PublicShare) {
|
||||
if err := m.cleanupExpiredShares(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, errtypes.NotFound("public share has expired")
|
||||
}
|
||||
|
||||
if ps.PublicShare.PasswordProtected && sign {
|
||||
err = publicshare.AddSignature(&ps.PublicShare, ps.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &ps.PublicShare, nil
|
||||
}
|
||||
|
||||
func (m *mgr) getWithPassword(ctx context.Context, ref *link.PublicShareReference) (*publicshare.WithPassword, error) {
|
||||
switch {
|
||||
case ref.GetToken() != "":
|
||||
return m.getByToken(ctx, ref.GetToken())
|
||||
case ref.GetId().GetOpaqueId() != "":
|
||||
return m.getByID(ctx, ref.GetId().GetOpaqueId())
|
||||
default:
|
||||
return nil, errtypes.BadRequest("neither id nor token given")
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mgr) getByToken(ctx context.Context, token string) (*publicshare.WithPassword, error) {
|
||||
s, err := getByToken(m.db, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ps, err := m.ConvertToCS3PublicShare(ctx, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := &publicshare.WithPassword{
|
||||
Password: strings.TrimPrefix(s.ShareWith, "1|"),
|
||||
}
|
||||
proto.Merge(&ret.PublicShare, ps)
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func getByToken(db *sql.DB, token string) (DBShare, error) {
|
||||
s := DBShare{Token: token}
|
||||
query := `SELECT
|
||||
coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator,
|
||||
coalesce(share_with, '') as share_with, coalesce(file_source, '') as file_source,
|
||||
coalesce(item_type, '') as item_type,
|
||||
coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name,
|
||||
s.id, s.stime, s.permissions, fc.storage as storage
|
||||
FROM oc_share s
|
||||
LEFT JOIN oc_filecache fc ON fc.fileid = file_source
|
||||
WHERE share_type=? AND token=?`
|
||||
if err := db.QueryRow(query, publicShareType, token).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.ItemType, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions, &s.ItemStorage); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return s, errtypes.NotFound(token)
|
||||
}
|
||||
return s, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (m *mgr) getByID(ctx context.Context, id string) (*publicshare.WithPassword, error) {
|
||||
s := DBShare{ID: id}
|
||||
query := `SELECT
|
||||
coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator,
|
||||
coalesce(share_with, '') as share_with, coalesce(file_source, '') as file_source,
|
||||
coalesce(item_type, '') as item_type, coalesce(token,'') as token,
|
||||
coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name,
|
||||
s.stime, s.permissions, fc.storage as storage
|
||||
FROM oc_share s
|
||||
LEFT JOIN oc_filecache fc ON fc.fileid = file_source
|
||||
WHERE share_type=? AND id=?`
|
||||
if err := m.db.QueryRow(query, publicShareType, id).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.ItemType, &s.Token, &s.Expiration, &s.ShareName, &s.STime, &s.Permissions, &s.ItemStorage); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, errtypes.NotFound(id)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
ps, err := m.ConvertToCS3PublicShare(ctx, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := &publicshare.WithPassword{
|
||||
Password: strings.TrimPrefix(s.ShareWith, "1|"),
|
||||
}
|
||||
proto.Merge(&ret.PublicShare, ps)
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (m *mgr) ListPublicShares(ctx context.Context, u *user.User, filters []*link.ListPublicSharesRequest_Filter, sign bool) ([]*link.PublicShare, error) {
|
||||
uid := u.Username
|
||||
// FIXME instead of joining we may want to have to do a stat call ... if we want to store shares from other providers? or just Dump()? and be done with migration?
|
||||
query := `SELECT
|
||||
coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator,
|
||||
coalesce(share_with, '') as share_with, coalesce(file_source, '') as file_source,
|
||||
coalesce(item_type, '') as item_type, coalesce(token,'') as token,
|
||||
coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name,
|
||||
s.id, s.stime, s.permissions, fc.storage as storage
|
||||
FROM oc_share s
|
||||
LEFT JOIN oc_filecache fc ON fc.fileid = file_source
|
||||
WHERE (uid_owner=? or uid_initiator=?)
|
||||
AND (share_type=?)`
|
||||
var resourceFilters, ownerFilters, creatorFilters, storageFilters string
|
||||
var resourceParams, ownerParams, creatorParams, storageParams []interface{}
|
||||
params := []interface{}{uid, uid, publicShareType}
|
||||
|
||||
for _, f := range filters {
|
||||
switch f.Type {
|
||||
case link.ListPublicSharesRequest_Filter_TYPE_RESOURCE_ID:
|
||||
if len(resourceFilters) != 0 {
|
||||
resourceFilters += " OR "
|
||||
}
|
||||
resourceFilters += "item_source=?"
|
||||
resourceParams = append(resourceParams, f.GetResourceId().GetOpaqueId())
|
||||
case link.ListPublicSharesRequest_Filter_TYPE_OWNER:
|
||||
if len(ownerFilters) != 0 {
|
||||
ownerFilters += " OR "
|
||||
}
|
||||
ownerFilters += "(uid_owner=?)"
|
||||
ownerParams = append(ownerParams, formatUserID(f.GetOwner()))
|
||||
case link.ListPublicSharesRequest_Filter_TYPE_CREATOR:
|
||||
if len(creatorFilters) != 0 {
|
||||
creatorFilters += " OR "
|
||||
}
|
||||
creatorFilters += "(uid_initiator=?)"
|
||||
creatorParams = append(creatorParams, formatUserID(f.GetCreator()))
|
||||
case publicshare.StorageIDFilterType:
|
||||
if len(storageFilters) != 0 {
|
||||
storageFilters += " OR "
|
||||
}
|
||||
storageFilters += "(storage=?)"
|
||||
storageParams = append(storageParams, f.GetResourceId().GetStorageId())
|
||||
}
|
||||
}
|
||||
if resourceFilters != "" {
|
||||
query = fmt.Sprintf("%s AND (%s)", query, resourceFilters)
|
||||
params = append(params, resourceParams...)
|
||||
}
|
||||
if ownerFilters != "" {
|
||||
query = fmt.Sprintf("%s AND (%s)", query, ownerFilters)
|
||||
params = append(params, ownerParams...)
|
||||
}
|
||||
if creatorFilters != "" {
|
||||
query = fmt.Sprintf("%s AND (%s)", query, creatorFilters)
|
||||
params = append(params, creatorParams...)
|
||||
}
|
||||
if storageFilters != "" {
|
||||
query = fmt.Sprintf("%s AND (%s)", query, storageFilters)
|
||||
params = append(params, storageParams...)
|
||||
}
|
||||
|
||||
rows, err := m.db.Query(query, params...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var s DBShare
|
||||
shares := []*link.PublicShare{}
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.ItemType, &s.Token, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions, &s.ItemStorage); err != nil {
|
||||
continue
|
||||
}
|
||||
var cs3Share *link.PublicShare
|
||||
if cs3Share, err = m.ConvertToCS3PublicShare(ctx, s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if publicshare.IsExpired(cs3Share) {
|
||||
_ = m.cleanupExpiredShares()
|
||||
} else {
|
||||
if cs3Share.PasswordProtected && sign {
|
||||
if err := publicshare.AddSignature(cs3Share, strings.TrimPrefix(s.ShareWith, "1|")); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
shares = append(shares, cs3Share)
|
||||
}
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return shares, nil
|
||||
}
|
||||
|
||||
func (m *mgr) RevokePublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference) error {
|
||||
uid := u.Username
|
||||
query := "delete from oc_share where "
|
||||
params := []interface{}{}
|
||||
|
||||
switch {
|
||||
case ref.GetId() != nil && ref.GetId().OpaqueId != "":
|
||||
query += "id=? AND (uid_owner=? or uid_initiator=?)"
|
||||
params = append(params, ref.GetId().OpaqueId, uid, uid)
|
||||
case ref.GetToken() != "":
|
||||
query += "token=? AND (uid_owner=? or uid_initiator=?)"
|
||||
params = append(params, ref.GetToken(), uid, uid)
|
||||
default:
|
||||
return errtypes.NotFound(ref.String())
|
||||
}
|
||||
|
||||
stmt, err := m.db.Prepare(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := stmt.Exec(params...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowCnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowCnt == 0 {
|
||||
return errtypes.NotFound(ref.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mgr) GetPublicShareByToken(ctx context.Context, token string, auth *link.PublicShareAuthentication, sign bool) (*link.PublicShare, error) {
|
||||
ps, err := m.getByToken(ctx, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if publicshare.IsExpired(&ps.PublicShare) {
|
||||
if err := m.cleanupExpiredShares(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, errtypes.NotFound("public share has expired")
|
||||
}
|
||||
|
||||
if ps.PublicShare.PasswordProtected {
|
||||
if !publicshare.Authenticate(&ps.PublicShare, ps.Password, auth) {
|
||||
return nil, errtypes.InvalidCredentials("access denied")
|
||||
}
|
||||
}
|
||||
|
||||
return &ps.PublicShare, nil
|
||||
}
|
||||
|
||||
func (m *mgr) cleanupExpiredShares() error {
|
||||
if !m.c.EnableExpiredSharesCleanup {
|
||||
return nil
|
||||
}
|
||||
|
||||
query := "DELETE FROM oc_share WHERE expiration IS NOT NULL AND expiration < ?"
|
||||
params := []interface{}{time.Now().Format("2006-01-02 03:04:05")}
|
||||
|
||||
stmt, err := m.db.Prepare(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = stmt.Exec(params...); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Generated
Vendored
BIN
Binary file not shown.
-155
@@ -1,155 +0,0 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package cbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/auth/scope"
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share/cache"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share/cache/warmup/registry"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/token/manager/jwt"
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/grpc/metadata"
|
||||
|
||||
// Provides mysql drivers
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register("cbox", New)
|
||||
}
|
||||
|
||||
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"`
|
||||
EOSNamespace string `mapstructure:"namespace"`
|
||||
GatewaySvc string `mapstructure:"gatewaysvc"`
|
||||
JWTSecret string `mapstructure:"jwt_secret"`
|
||||
}
|
||||
|
||||
type manager struct {
|
||||
conf *config
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func parseConfig(m map[string]interface{}) (*config, error) {
|
||||
c := &config{}
|
||||
if err := mapstructure.Decode(m, c); err != nil {
|
||||
err = errors.Wrap(err, "error decoding conf")
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// New returns an implementation of cache warmup that connects to the cbox share db and stats resources on EOS
|
||||
func New(m map[string]interface{}) (cache.Warmup, error) {
|
||||
c, err := parseConfig(m)
|
||||
if 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 &manager{
|
||||
conf: c,
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *manager) GetResourceInfos() ([]*provider.ResourceInfo, error) {
|
||||
query := "select coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source FROM oc_share WHERE (orphan = 0 or orphan IS NULL)"
|
||||
rows, err := m.db.Query(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
tokenManager, err := jwt.New(map[string]interface{}{
|
||||
"secret": m.conf.JWTSecret,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u := &userpb.User{
|
||||
Id: &userpb.UserId{
|
||||
OpaqueId: "root",
|
||||
},
|
||||
UidNumber: 0,
|
||||
GidNumber: 0,
|
||||
}
|
||||
scope, err := scope.AddOwnerScope(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tkn, err := tokenManager.MintToken(context.Background(), u, scope)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx := metadata.AppendToOutgoingContext(context.Background(), ctxpkg.TokenHeader, tkn)
|
||||
|
||||
client, err := pool.GetGatewayServiceClient(m.conf.GatewaySvc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
infos := []*provider.ResourceInfo{}
|
||||
for rows.Next() {
|
||||
var spaceID, nodeID string
|
||||
if err := rows.Scan(&spaceID, &nodeID); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
statReq := provider.StatRequest{Ref: &provider.Reference{
|
||||
ResourceId: &provider.ResourceId{
|
||||
SpaceId: spaceID,
|
||||
OpaqueId: nodeID,
|
||||
},
|
||||
}}
|
||||
|
||||
statRes, err := client.Stat(ctx, &statReq)
|
||||
if err != nil || statRes.Status.Code != rpc.Code_CODE_OK {
|
||||
continue
|
||||
}
|
||||
|
||||
infos = append(infos, statRes.Info)
|
||||
}
|
||||
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return infos, nil
|
||||
|
||||
}
|
||||
+2
-5
@@ -18,8 +18,5 @@
|
||||
|
||||
package loader
|
||||
|
||||
import (
|
||||
// Load share cache drivers.
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/share/cache/warmup/cbox"
|
||||
// Add your own here
|
||||
)
|
||||
// Load share cache drivers.
|
||||
// Add your own here
|
||||
|
||||
-617
@@ -1,617 +0,0 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package json
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
rpcv1beta1 "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"
|
||||
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/golang/protobuf/proto" // nolint:staticcheck // we need the legacy package to convert V1 to V2 messages
|
||||
"github.com/google/uuid"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share"
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/genproto/protobuf/field_mask"
|
||||
"google.golang.org/protobuf/encoding/prototext"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share/manager/registry"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register("json", New)
|
||||
}
|
||||
|
||||
// New returns a new mgr.
|
||||
func New(m map[string]interface{}) (share.Manager, error) {
|
||||
c, err := parseConfig(m)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "error creating a new manager")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c.GatewayAddr == "" {
|
||||
return nil, errors.New("share manager config is missing gateway address")
|
||||
}
|
||||
|
||||
c.init()
|
||||
|
||||
// load or create file
|
||||
model, err := loadOrCreate(c.File)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "error loading the file containing the shares")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &mgr{
|
||||
c: c,
|
||||
model: model,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func loadOrCreate(file string) (*shareModel, error) {
|
||||
if info, err := os.Stat(file); errors.Is(err, fs.ErrNotExist) || info.Size() == 0 {
|
||||
if err := os.WriteFile(file, []byte("{}"), 0700); err != nil {
|
||||
err = errors.Wrap(err, "error opening/creating the file: "+file)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
fd, err := os.OpenFile(file, os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "error opening/creating the 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
|
||||
}
|
||||
|
||||
j := &jsonEncoding{}
|
||||
if err := json.Unmarshal(data, j); err != nil {
|
||||
err = errors.Wrap(err, "error decoding data from json")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m := &shareModel{State: j.State, MountPoint: j.MountPoint}
|
||||
for _, s := range j.Shares {
|
||||
var decShare collaboration.Share
|
||||
if err = utils.UnmarshalJSONToProtoV1([]byte(s), &decShare); err != nil {
|
||||
return nil, errors.Wrap(err, "error decoding share from json")
|
||||
}
|
||||
m.Shares = append(m.Shares, &decShare)
|
||||
}
|
||||
|
||||
if m.State == nil {
|
||||
m.State = map[string]map[string]collaboration.ShareState{}
|
||||
}
|
||||
if m.MountPoint == nil {
|
||||
m.MountPoint = map[string]map[string]*provider.Reference{}
|
||||
}
|
||||
|
||||
m.file = file
|
||||
return m, nil
|
||||
}
|
||||
|
||||
type shareModel struct {
|
||||
file string
|
||||
State map[string]map[string]collaboration.ShareState `json:"state"` // map[username]map[share_id]ShareState
|
||||
MountPoint map[string]map[string]*provider.Reference `json:"mount_point"` // map[username]map[share_id]MountPoint
|
||||
Shares []*collaboration.Share `json:"shares"`
|
||||
}
|
||||
|
||||
type jsonEncoding struct {
|
||||
State map[string]map[string]collaboration.ShareState `json:"state"` // map[username]map[share_id]ShareState
|
||||
MountPoint map[string]map[string]*provider.Reference `json:"mount_point"` // map[username]map[share_id]MountPoint
|
||||
Shares []string `json:"shares"`
|
||||
}
|
||||
|
||||
func (m *shareModel) Save() error {
|
||||
j := &jsonEncoding{State: m.State, MountPoint: m.MountPoint}
|
||||
for _, s := range m.Shares {
|
||||
encShare, err := utils.MarshalProtoV1ToJSON(s)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error encoding to json")
|
||||
}
|
||||
j.Shares = append(j.Shares, string(encShare))
|
||||
}
|
||||
|
||||
data, err := json.Marshal(j)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "error encoding to json")
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.WriteFile(m.file, data, 0644); err != nil {
|
||||
err = errors.Wrap(err, "error writing to file: "+m.file)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type mgr struct {
|
||||
c *config
|
||||
sync.Mutex // concurrent access to the file
|
||||
model *shareModel
|
||||
}
|
||||
|
||||
type config struct {
|
||||
File string `mapstructure:"file"`
|
||||
GatewayAddr string `mapstructure:"gateway_addr"`
|
||||
}
|
||||
|
||||
func (c *config) init() {
|
||||
if c.File == "" {
|
||||
c.File = "/var/tmp/reva/shares.json"
|
||||
}
|
||||
}
|
||||
|
||||
func parseConfig(m map[string]interface{}) (*config, error) {
|
||||
c := &config{}
|
||||
if err := mapstructure.Decode(m, c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Dump exports shares and received shares to channels (e.g. during migration)
|
||||
func (m *mgr) Dump(ctx context.Context, shareChan chan<- *collaboration.Share, receivedShareChan chan<- share.ReceivedShareWithUser) error {
|
||||
log := appctx.GetLogger(ctx)
|
||||
for _, s := range m.model.Shares {
|
||||
shareChan <- s
|
||||
}
|
||||
|
||||
for userIDString, states := range m.model.State {
|
||||
userMountPoints := m.model.MountPoint[userIDString]
|
||||
id := &userv1beta1.UserId{}
|
||||
mV2 := proto.MessageV2(id)
|
||||
if err := prototext.Unmarshal([]byte(userIDString), mV2); err != nil {
|
||||
log.Error().Err(err).Msg("error unmarshalling the user id")
|
||||
continue
|
||||
}
|
||||
|
||||
for shareIDString, state := range states {
|
||||
sid := &collaboration.ShareId{}
|
||||
mV2 := proto.MessageV2(sid)
|
||||
if err := prototext.Unmarshal([]byte(shareIDString), mV2); err != nil {
|
||||
log.Error().Err(err).Msg("error unmarshalling the user id")
|
||||
continue
|
||||
}
|
||||
|
||||
var s *collaboration.Share
|
||||
for _, is := range m.model.Shares {
|
||||
if is.Id.OpaqueId == sid.OpaqueId {
|
||||
s = is
|
||||
break
|
||||
}
|
||||
}
|
||||
if s == nil {
|
||||
log.Warn().Str("share id", sid.OpaqueId).Msg("Share not found")
|
||||
continue
|
||||
}
|
||||
|
||||
var mp *provider.Reference
|
||||
if userMountPoints != nil {
|
||||
mp = userMountPoints[shareIDString]
|
||||
}
|
||||
|
||||
receivedShareChan <- share.ReceivedShareWithUser{
|
||||
UserID: id,
|
||||
ReceivedShare: &collaboration.ReceivedShare{
|
||||
Share: s,
|
||||
State: state,
|
||||
MountPoint: mp,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mgr) Share(ctx context.Context, md *provider.ResourceInfo, g *collaboration.ShareGrant) (*collaboration.Share, error) {
|
||||
id := uuid.NewString()
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
now := time.Now().UnixNano()
|
||||
ts := &typespb.Timestamp{
|
||||
Seconds: uint64(now / int64(time.Second)),
|
||||
Nanos: uint32(now % int64(time.Second)),
|
||||
}
|
||||
|
||||
// do not allow share to myself or the owner if share is for a user
|
||||
// TODO(labkode): should not this be caught already at the gw level?
|
||||
if g.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_USER &&
|
||||
(utils.UserEqual(g.Grantee.GetUserId(), user.Id) || utils.UserEqual(g.Grantee.GetUserId(), md.Owner)) {
|
||||
return nil, errtypes.BadRequest("json: owner/creator and grantee are the same")
|
||||
}
|
||||
|
||||
// check if share already exists.
|
||||
key := &collaboration.ShareKey{
|
||||
Owner: md.Owner,
|
||||
ResourceId: md.Id,
|
||||
Grantee: g.Grantee,
|
||||
}
|
||||
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
_, _, err := m.getByKey(key)
|
||||
if err == nil {
|
||||
// share already exists
|
||||
return nil, errtypes.AlreadyExists(key.String())
|
||||
}
|
||||
|
||||
s := &collaboration.Share{
|
||||
Id: &collaboration.ShareId{
|
||||
OpaqueId: id,
|
||||
},
|
||||
ResourceId: md.Id,
|
||||
Permissions: g.Permissions,
|
||||
Grantee: g.Grantee,
|
||||
Owner: md.Owner,
|
||||
Creator: user.Id,
|
||||
Ctime: ts,
|
||||
Mtime: ts,
|
||||
}
|
||||
|
||||
m.model.Shares = append(m.model.Shares, s)
|
||||
if err := m.model.Save(); err != nil {
|
||||
err = errors.Wrap(err, "error saving model")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// getByID must be called in a lock-controlled block.
|
||||
func (m *mgr) getByID(id *collaboration.ShareId) (int, *collaboration.Share, error) {
|
||||
for i, s := range m.model.Shares {
|
||||
if s.GetId().OpaqueId == id.OpaqueId {
|
||||
return i, s, nil
|
||||
}
|
||||
}
|
||||
return -1, nil, errtypes.NotFound(id.String())
|
||||
}
|
||||
|
||||
// getByKey must be called in a lock-controlled block.
|
||||
func (m *mgr) getByKey(key *collaboration.ShareKey) (int, *collaboration.Share, error) {
|
||||
for i, s := range m.model.Shares {
|
||||
if (utils.UserEqual(key.Owner, s.Owner) || utils.UserEqual(key.Owner, s.Creator)) &&
|
||||
utils.ResourceIDEqual(key.ResourceId, s.ResourceId) && utils.GranteeEqual(key.Grantee, s.Grantee) {
|
||||
return i, s, nil
|
||||
}
|
||||
}
|
||||
return -1, nil, errtypes.NotFound(key.String())
|
||||
}
|
||||
|
||||
// get must be called in a lock-controlled block.
|
||||
func (m *mgr) get(ref *collaboration.ShareReference) (idx int, s *collaboration.Share, err error) {
|
||||
switch {
|
||||
case ref.GetId() != nil:
|
||||
idx, s, err = m.getByID(ref.GetId())
|
||||
case ref.GetKey() != nil:
|
||||
idx, s, err = m.getByKey(ref.GetKey())
|
||||
default:
|
||||
err = errtypes.NotFound(ref.String())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (m *mgr) GetShare(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.Share, error) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
_, s, err := m.get(ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// check if we are the owner or the grantee
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
if share.IsCreatedByUser(s, user) || share.IsGrantedToUser(s, user) {
|
||||
return s, nil
|
||||
}
|
||||
// we return not found to not disclose information
|
||||
return nil, errtypes.NotFound(ref.String())
|
||||
}
|
||||
|
||||
func (m *mgr) Unshare(ctx context.Context, ref *collaboration.ShareReference) error {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
|
||||
idx, s, err := m.get(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !share.IsCreatedByUser(s, user) {
|
||||
return errtypes.NotFound(ref.String())
|
||||
}
|
||||
|
||||
last := len(m.model.Shares) - 1
|
||||
m.model.Shares[idx] = m.model.Shares[last]
|
||||
// explicitly nil the reference to prevent memory leaks
|
||||
// https://github.com/golang/go/wiki/SliceTricks#delete-without-preserving-order
|
||||
m.model.Shares[last] = nil
|
||||
m.model.Shares = m.model.Shares[:last]
|
||||
if err := m.model.Save(); err != nil {
|
||||
err = errors.Wrap(err, "error saving model")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mgr) UpdateShare(ctx context.Context, ref *collaboration.ShareReference, p *collaboration.SharePermissions, updated *collaboration.Share, fieldMask *field_mask.FieldMask) (*collaboration.Share, error) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
var (
|
||||
idx int
|
||||
toUpdate *collaboration.Share
|
||||
)
|
||||
|
||||
if ref != nil {
|
||||
var err error
|
||||
idx, toUpdate, err = m.get(ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if updated != nil {
|
||||
var err error
|
||||
idx, toUpdate, err = m.getByID(updated.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if fieldMask != nil {
|
||||
for i := range fieldMask.Paths {
|
||||
switch fieldMask.Paths[i] {
|
||||
case "permissions":
|
||||
m.model.Shares[idx].Permissions = updated.Permissions
|
||||
case "expiration":
|
||||
m.model.Shares[idx].Expiration = updated.Expiration
|
||||
case "hidden":
|
||||
continue
|
||||
default:
|
||||
return nil, errtypes.NotSupported("updating " + fieldMask.Paths[i] + " is not supported")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
if !share.IsCreatedByUser(toUpdate, user) {
|
||||
return nil, errtypes.NotFound(ref.String())
|
||||
}
|
||||
|
||||
now := time.Now().UnixNano()
|
||||
if p != nil {
|
||||
m.model.Shares[idx].Permissions = p
|
||||
}
|
||||
m.model.Shares[idx].Mtime = &typespb.Timestamp{
|
||||
Seconds: uint64(now / int64(time.Second)),
|
||||
Nanos: uint32(now % int64(time.Second)),
|
||||
}
|
||||
|
||||
if err := m.model.Save(); err != nil {
|
||||
err = errors.Wrap(err, "error saving model")
|
||||
return nil, err
|
||||
}
|
||||
return m.model.Shares[idx], nil
|
||||
}
|
||||
|
||||
func (m *mgr) ListShares(ctx context.Context, filters []*collaboration.Filter) ([]*collaboration.Share, error) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
log := appctx.GetLogger(ctx)
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
|
||||
client, err := pool.GetGatewayServiceClient(m.c.GatewayAddr)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to list shares")
|
||||
}
|
||||
cache := make(map[string]struct{})
|
||||
var ss []*collaboration.Share
|
||||
for _, s := range m.model.Shares {
|
||||
if share.MatchesFilters(s, filters) {
|
||||
// Only add the share if the share was created by the user or if
|
||||
// the user has ListGrants permissions on the shared resource.
|
||||
// The ListGrants check is necessary when a space member wants
|
||||
// to list shares in a space.
|
||||
// We are using a cache here so that we don't have to stat a
|
||||
// resource multiple times.
|
||||
key := strings.Join([]string{s.ResourceId.StorageId, s.ResourceId.OpaqueId}, "!")
|
||||
if _, hit := cache[key]; !hit && !share.IsCreatedByUser(s, user) {
|
||||
sRes, err := client.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ResourceId: s.ResourceId}})
|
||||
if err != nil || sRes.Status.Code != rpcv1beta1.Code_CODE_OK {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Interface("status", sRes.Status).
|
||||
Interface("resource_id", s.ResourceId).
|
||||
Msg("ListShares: could not stat resource")
|
||||
continue
|
||||
}
|
||||
if !sRes.Info.PermissionSet.ListGrants {
|
||||
continue
|
||||
}
|
||||
cache[key] = struct{}{}
|
||||
}
|
||||
ss = append(ss, s)
|
||||
}
|
||||
}
|
||||
return ss, nil
|
||||
}
|
||||
|
||||
// we list the shares that are targeted to the user in context or to the user groups.
|
||||
func (m *mgr) ListReceivedShares(ctx context.Context, filters []*collaboration.Filter, forUser *userv1beta1.UserId) ([]*collaboration.ReceivedShare, error) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
if user.GetId().GetType() == userv1beta1.UserType_USER_TYPE_SERVICE {
|
||||
gwc, err := pool.GetGatewayServiceClient(m.c.GatewayAddr)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to list shares")
|
||||
}
|
||||
u, err := utils.GetUser(ctx, forUser, gwc)
|
||||
if err != nil {
|
||||
return nil, errtypes.BadRequest("user not found")
|
||||
}
|
||||
user = u
|
||||
}
|
||||
mem := make(map[string]int)
|
||||
var rss []*collaboration.ReceivedShare
|
||||
for _, s := range m.model.Shares {
|
||||
if !share.IsCreatedByUser(s, user) &&
|
||||
share.IsGrantedToUser(s, user) &&
|
||||
share.MatchesFilters(s, filters) {
|
||||
|
||||
rs := m.convert(user.Id, s)
|
||||
idx, seen := mem[s.ResourceId.OpaqueId]
|
||||
if !seen {
|
||||
rss = append(rss, rs)
|
||||
mem[s.ResourceId.OpaqueId] = len(rss) - 1
|
||||
continue
|
||||
}
|
||||
|
||||
// When we arrive here there was already a share for this resource.
|
||||
// if there is a mix-up of shares of type group and shares of type user we need to deduplicate them, since it points
|
||||
// to the same resource. Leave the more explicit and hide the less explicit. In this case we hide the group shares
|
||||
// and return the user share to the user.
|
||||
other := rss[idx]
|
||||
if other.Share.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_GROUP && s.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_USER {
|
||||
if other.State == rs.State {
|
||||
rss[idx] = rs
|
||||
} else {
|
||||
rss = append(rss, rs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rss, nil
|
||||
}
|
||||
|
||||
// convert must be called in a lock-controlled block.
|
||||
func (m *mgr) convert(currentUser *userv1beta1.UserId, s *collaboration.Share) *collaboration.ReceivedShare {
|
||||
rs := &collaboration.ReceivedShare{
|
||||
Share: s,
|
||||
State: collaboration.ShareState_SHARE_STATE_PENDING,
|
||||
}
|
||||
if v, ok := m.model.State[currentUser.String()]; ok {
|
||||
if state, ok := v[s.Id.String()]; ok {
|
||||
rs.State = state
|
||||
}
|
||||
}
|
||||
if v, ok := m.model.MountPoint[currentUser.String()]; ok {
|
||||
if mp, ok := v[s.Id.String()]; ok {
|
||||
rs.MountPoint = mp
|
||||
}
|
||||
}
|
||||
return rs
|
||||
}
|
||||
|
||||
func (m *mgr) GetReceivedShare(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.ReceivedShare, error) {
|
||||
return m.getReceived(ctx, ref)
|
||||
}
|
||||
|
||||
func (m *mgr) getReceived(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.ReceivedShare, error) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
_, s, err := m.get(ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
if user.GetId().GetType() != userv1beta1.UserType_USER_TYPE_SERVICE && !share.IsGrantedToUser(s, user) {
|
||||
return nil, errtypes.NotFound(ref.String())
|
||||
}
|
||||
return m.convert(user.Id, s), nil
|
||||
}
|
||||
|
||||
func (m *mgr) UpdateReceivedShare(ctx context.Context, receivedShare *collaboration.ReceivedShare, fieldMask *field_mask.FieldMask, forUser *userv1beta1.UserId) (*collaboration.ReceivedShare, error) {
|
||||
rs, err := m.getReceived(ctx, &collaboration.ShareReference{Spec: &collaboration.ShareReference_Id{Id: receivedShare.Share.Id}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
for i := range fieldMask.Paths {
|
||||
switch fieldMask.Paths[i] {
|
||||
case "state":
|
||||
rs.State = receivedShare.State
|
||||
case "mount_point":
|
||||
rs.MountPoint = receivedShare.MountPoint
|
||||
default:
|
||||
return nil, errtypes.NotSupported("updating " + fieldMask.Paths[i] + " is not supported")
|
||||
}
|
||||
}
|
||||
|
||||
u := ctxpkg.ContextMustGetUser(ctx)
|
||||
uid := u.GetId().String()
|
||||
if u.GetId().GetType() == userv1beta1.UserType_USER_TYPE_SERVICE {
|
||||
uid = forUser.String()
|
||||
}
|
||||
// Persist state
|
||||
if v, ok := m.model.State[uid]; ok {
|
||||
v[rs.Share.Id.String()] = rs.State
|
||||
m.model.State[uid] = v
|
||||
} else {
|
||||
a := map[string]collaboration.ShareState{
|
||||
rs.Share.Id.String(): rs.State,
|
||||
}
|
||||
m.model.State[uid] = a
|
||||
}
|
||||
|
||||
// Persist mount point
|
||||
if v, ok := m.model.MountPoint[uid]; ok {
|
||||
v[rs.Share.Id.String()] = rs.MountPoint
|
||||
m.model.MountPoint[uid] = v
|
||||
} else {
|
||||
a := map[string]*provider.Reference{
|
||||
rs.Share.Id.String(): rs.MountPoint,
|
||||
}
|
||||
m.model.MountPoint[uid] = a
|
||||
}
|
||||
|
||||
if err := m.model.Save(); err != nil {
|
||||
err = errors.Wrap(err, "error saving model")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rs, nil
|
||||
}
|
||||
-2
@@ -20,9 +20,7 @@ package loader
|
||||
|
||||
import (
|
||||
// Load core share manager drivers.
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/share/manager/json"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/share/manager/jsoncs3"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/share/manager/memory"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/share/manager/owncloudsql"
|
||||
// Add your own here
|
||||
)
|
||||
|
||||
Generated
Vendored
-300
@@ -1,300 +0,0 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package owncloudsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
|
||||
userpb "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"
|
||||
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/jellydator/ttlcache/v2"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/conversions"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
)
|
||||
|
||||
// DBShare stores information about user and public shares.
|
||||
type DBShare struct {
|
||||
ID string
|
||||
UIDOwner string
|
||||
UIDInitiator string
|
||||
ItemStorage string
|
||||
FileSource string
|
||||
ShareWith string
|
||||
Token string
|
||||
Expiration string
|
||||
Permissions int
|
||||
ShareType int
|
||||
ShareName string
|
||||
STime int
|
||||
FileTarget string
|
||||
RejectedBy string
|
||||
State int
|
||||
Parent int
|
||||
}
|
||||
|
||||
// UserConverter describes an interface for converting user ids to names and back
|
||||
type UserConverter interface {
|
||||
UserNameToUserID(ctx context.Context, username string) (*userpb.UserId, error)
|
||||
UserIDToUserName(ctx context.Context, userid *userpb.UserId) (string, error)
|
||||
GetUser(ctx context.Context, userid *userpb.UserId) (*userpb.User, error)
|
||||
}
|
||||
|
||||
// GatewayUserConverter converts usernames and ids using the gateway
|
||||
type GatewayUserConverter struct {
|
||||
gwAddr string
|
||||
|
||||
IDCache *ttlcache.Cache
|
||||
NameCache *ttlcache.Cache
|
||||
}
|
||||
|
||||
// NewGatewayUserConverter returns a instance of GatewayUserConverter
|
||||
func NewGatewayUserConverter(gwAddr string) *GatewayUserConverter {
|
||||
IDCache := ttlcache.NewCache()
|
||||
_ = IDCache.SetTTL(30 * time.Second)
|
||||
IDCache.SkipTTLExtensionOnHit(true)
|
||||
NameCache := ttlcache.NewCache()
|
||||
_ = NameCache.SetTTL(30 * time.Second)
|
||||
NameCache.SkipTTLExtensionOnHit(true)
|
||||
|
||||
return &GatewayUserConverter{
|
||||
gwAddr: gwAddr,
|
||||
IDCache: IDCache,
|
||||
NameCache: NameCache,
|
||||
}
|
||||
}
|
||||
|
||||
// UserIDToUserName converts a user ID to an username
|
||||
func (c *GatewayUserConverter) UserIDToUserName(ctx context.Context, userid *userpb.UserId) (string, error) {
|
||||
username, err := c.NameCache.Get(userid.String())
|
||||
if err == nil {
|
||||
return username.(string), nil
|
||||
}
|
||||
|
||||
gwConn, err := pool.GetGatewayServiceClient(c.gwAddr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
getUserResponse, err := gwConn.GetUser(ctx, &userpb.GetUserRequest{
|
||||
UserId: userid,
|
||||
SkipFetchingUserGroups: true,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if getUserResponse.Status.Code != rpc.Code_CODE_OK {
|
||||
return "", status.NewErrorFromCode(getUserResponse.Status.Code, "gateway")
|
||||
}
|
||||
_ = c.NameCache.Set(userid.String(), getUserResponse.User.Username)
|
||||
return getUserResponse.User.Username, nil
|
||||
}
|
||||
|
||||
// UserNameToUserID converts a username to an user ID
|
||||
func (c *GatewayUserConverter) UserNameToUserID(ctx context.Context, username string) (*userpb.UserId, error) {
|
||||
id, err := c.IDCache.Get(username)
|
||||
if err == nil {
|
||||
return id.(*userpb.UserId), nil
|
||||
}
|
||||
|
||||
gwConn, err := pool.GetGatewayServiceClient(c.gwAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
getUserResponse, err := gwConn.GetUserByClaim(ctx, &userpb.GetUserByClaimRequest{
|
||||
Claim: "username",
|
||||
Value: username,
|
||||
SkipFetchingUserGroups: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if getUserResponse.Status.Code != rpc.Code_CODE_OK {
|
||||
return nil, status.NewErrorFromCode(getUserResponse.Status.Code, "gateway")
|
||||
}
|
||||
_ = c.IDCache.Set(username, getUserResponse.User.Id)
|
||||
return getUserResponse.User.Id, nil
|
||||
}
|
||||
|
||||
// GetUser gets the user
|
||||
func (c *GatewayUserConverter) GetUser(ctx context.Context, userid *userpb.UserId) (*userpb.User, error) {
|
||||
gwc, err := pool.GetGatewayServiceClient(c.gwAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return utils.GetUser(ctx, userid, gwc)
|
||||
}
|
||||
|
||||
func (m *mgr) formatGrantee(ctx context.Context, g *provider.Grantee) (int, string, error) {
|
||||
var granteeType int
|
||||
var formattedID string
|
||||
switch g.Type {
|
||||
case provider.GranteeType_GRANTEE_TYPE_USER:
|
||||
granteeType = 0
|
||||
var err error
|
||||
formattedID, err = m.userConverter.UserIDToUserName(ctx, g.GetUserId())
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
case provider.GranteeType_GRANTEE_TYPE_GROUP:
|
||||
granteeType = 1
|
||||
formattedID = formatGroupID(g.GetGroupId())
|
||||
default:
|
||||
granteeType = -1
|
||||
}
|
||||
return granteeType, formattedID, nil
|
||||
}
|
||||
|
||||
func (m *mgr) extractGrantee(ctx context.Context, t int, g string) (*provider.Grantee, error) {
|
||||
var grantee provider.Grantee
|
||||
switch t {
|
||||
case 0:
|
||||
userid, err := m.userConverter.UserNameToUserID(ctx, g)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
grantee.Type = provider.GranteeType_GRANTEE_TYPE_USER
|
||||
grantee.Id = &provider.Grantee_UserId{UserId: userid}
|
||||
case 1, 2:
|
||||
grantee.Type = provider.GranteeType_GRANTEE_TYPE_GROUP
|
||||
grantee.Id = &provider.Grantee_GroupId{GroupId: extractGroupID(g)}
|
||||
default:
|
||||
grantee.Type = provider.GranteeType_GRANTEE_TYPE_INVALID
|
||||
}
|
||||
return &grantee, nil
|
||||
}
|
||||
|
||||
func resourceTypeToItem(r provider.ResourceType) string {
|
||||
switch r {
|
||||
case provider.ResourceType_RESOURCE_TYPE_FILE:
|
||||
return "file"
|
||||
case provider.ResourceType_RESOURCE_TYPE_CONTAINER:
|
||||
return "folder"
|
||||
case provider.ResourceType_RESOURCE_TYPE_REFERENCE:
|
||||
return "reference"
|
||||
case provider.ResourceType_RESOURCE_TYPE_SYMLINK:
|
||||
return "symlink"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func sharePermToInt(p *provider.ResourcePermissions) int {
|
||||
return int(conversions.RoleFromResourcePermissions(p, false).OCSPermissions())
|
||||
}
|
||||
|
||||
func intTosharePerm(p int) (*provider.ResourcePermissions, error) {
|
||||
perms, err := conversions.NewPermissions(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conversions.RoleFromOCSPermissions(perms, nil).CS3ResourcePermissions(), nil
|
||||
}
|
||||
|
||||
func intToShareState(g int) collaboration.ShareState {
|
||||
switch g {
|
||||
case 0:
|
||||
return collaboration.ShareState_SHARE_STATE_ACCEPTED
|
||||
case 1:
|
||||
return collaboration.ShareState_SHARE_STATE_PENDING
|
||||
case 2:
|
||||
return collaboration.ShareState_SHARE_STATE_REJECTED
|
||||
default:
|
||||
return collaboration.ShareState_SHARE_STATE_INVALID
|
||||
}
|
||||
}
|
||||
|
||||
func formatUserID(u *userpb.UserId) string {
|
||||
return u.OpaqueId
|
||||
}
|
||||
|
||||
func formatGroupID(u *grouppb.GroupId) string {
|
||||
return u.OpaqueId
|
||||
}
|
||||
|
||||
func extractGroupID(u string) *grouppb.GroupId {
|
||||
return &grouppb.GroupId{OpaqueId: u}
|
||||
}
|
||||
|
||||
func (m *mgr) convertToCS3Share(ctx context.Context, s DBShare, storageMountID string) (*collaboration.Share, error) {
|
||||
ts := &typespb.Timestamp{
|
||||
Seconds: uint64(s.STime),
|
||||
}
|
||||
permissions, err := intTosharePerm(s.Permissions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
grantee, err := m.extractGrantee(ctx, s.ShareType, s.ShareWith)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
owner, err := m.userConverter.UserNameToUserID(ctx, s.UIDOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var creator *userpb.UserId
|
||||
if s.UIDOwner == s.UIDInitiator {
|
||||
creator = owner
|
||||
} else {
|
||||
creator, err = m.userConverter.UserNameToUserID(ctx, s.UIDOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &collaboration.Share{
|
||||
Id: &collaboration.ShareId{
|
||||
OpaqueId: s.ID,
|
||||
},
|
||||
ResourceId: &provider.ResourceId{
|
||||
SpaceId: s.ItemStorage,
|
||||
OpaqueId: s.FileSource,
|
||||
},
|
||||
Permissions: &collaboration.SharePermissions{Permissions: permissions},
|
||||
Grantee: grantee,
|
||||
Owner: owner,
|
||||
Creator: creator,
|
||||
Ctime: ts,
|
||||
Mtime: ts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *mgr) convertToCS3ReceivedShare(ctx context.Context, s DBShare, storageMountID string) (*collaboration.ReceivedShare, error) {
|
||||
share, err := m.convertToCS3Share(ctx, s, storageMountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var state collaboration.ShareState
|
||||
if s.RejectedBy != "" {
|
||||
state = collaboration.ShareState_SHARE_STATE_REJECTED
|
||||
} else {
|
||||
state = intToShareState(s.State)
|
||||
}
|
||||
return &collaboration.ReceivedShare{
|
||||
Share: share,
|
||||
State: state,
|
||||
MountPoint: &provider.Reference{Path: strings.TrimLeft(s.FileTarget, "/")},
|
||||
}, nil
|
||||
}
|
||||
Generated
Vendored
-674
@@ -1,674 +0,0 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package owncloudsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share/manager/registry"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/genproto/protobuf/field_mask"
|
||||
|
||||
// Provides mysql drivers
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
const (
|
||||
shareTypeUser = 0
|
||||
shareTypeGroup = 1
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register("owncloudsql", NewMysql)
|
||||
}
|
||||
|
||||
type config struct {
|
||||
GatewayAddr string `mapstructure:"gateway_addr"`
|
||||
StorageMountID string `mapstructure:"storage_mount_id"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type mgr struct {
|
||||
driver string
|
||||
db *sql.DB
|
||||
storageMountID string
|
||||
userConverter UserConverter
|
||||
}
|
||||
|
||||
// NewMysql returns a new share manager connection to a mysql database
|
||||
func NewMysql(m map[string]interface{}) (share.Manager, error) {
|
||||
c, err := parseConfig(m)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "error creating a new manager")
|
||||
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
|
||||
}
|
||||
|
||||
userConverter := NewGatewayUserConverter(c.GatewayAddr)
|
||||
|
||||
return New("mysql", db, c.StorageMountID, userConverter)
|
||||
}
|
||||
|
||||
// New returns a new Cache instance connecting to the given sql.DB
|
||||
func New(driver string, db *sql.DB, storageMountID string, userConverter UserConverter) (share.Manager, error) {
|
||||
return &mgr{
|
||||
driver: driver,
|
||||
db: db,
|
||||
storageMountID: storageMountID,
|
||||
userConverter: userConverter,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseConfig(m map[string]interface{}) (*config, error) {
|
||||
c := &config{}
|
||||
if err := mapstructure.Decode(m, c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (m *mgr) Share(ctx context.Context, md *provider.ResourceInfo, g *collaboration.ShareGrant) (*collaboration.Share, error) {
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
|
||||
// do not allow share to myself or the owner if share is for a user
|
||||
// TODO(labkode): should not this be caught already at the gw level?
|
||||
if g.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_USER &&
|
||||
(utils.UserEqual(g.Grantee.GetUserId(), user.Id) || utils.UserEqual(g.Grantee.GetUserId(), md.Owner)) {
|
||||
return nil, errtypes.BadRequest("owncloudsql: owner/creator and grantee are the same")
|
||||
}
|
||||
|
||||
// check if share already exists.
|
||||
key := &collaboration.ShareKey{
|
||||
Owner: md.Owner,
|
||||
ResourceId: md.Id,
|
||||
Grantee: g.Grantee,
|
||||
}
|
||||
_, err := m.getByKey(ctx, key)
|
||||
|
||||
// share already exists
|
||||
if err == nil {
|
||||
return nil, errtypes.AlreadyExists(key.String())
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
ts := &typespb.Timestamp{
|
||||
Seconds: uint64(now),
|
||||
}
|
||||
|
||||
owner, err := m.userConverter.UserIDToUserName(ctx, md.Owner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
shareType, shareWith, err := m.formatGrantee(ctx, g.Grantee)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
itemType := resourceTypeToItem(md.Type)
|
||||
targetPath := path.Join("/", path.Base(md.Path))
|
||||
permissions := sharePermToInt(g.Permissions.Permissions)
|
||||
itemSource := md.Id.OpaqueId
|
||||
fileSource, err := strconv.ParseUint(itemSource, 10, 64)
|
||||
if err != nil {
|
||||
// it can be the case that the item source may be a character string
|
||||
// we leave fileSource blank in that case
|
||||
fileSource = 0
|
||||
}
|
||||
|
||||
stmtString := "INSERT INTO oc_share (share_type,uid_owner,uid_initiator,item_type,item_source,file_source,permissions,stime,share_with,file_target) VALUES (?,?,?,?,?,?,?,?,?,?)"
|
||||
stmtValues := []interface{}{shareType, owner, user.Username, itemType, itemSource, fileSource, permissions, now, shareWith, targetPath}
|
||||
|
||||
stmt, err := m.db.Prepare(stmtString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, err := stmt.ExecContext(ctx, stmtValues...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lastID, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &collaboration.Share{
|
||||
Id: &collaboration.ShareId{
|
||||
OpaqueId: strconv.FormatInt(lastID, 10),
|
||||
},
|
||||
ResourceId: md.Id,
|
||||
Permissions: g.Permissions,
|
||||
Grantee: g.Grantee,
|
||||
Owner: md.Owner,
|
||||
Creator: user.Id,
|
||||
Ctime: ts,
|
||||
Mtime: ts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *mgr) GetShare(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.Share, error) {
|
||||
var s *collaboration.Share
|
||||
var err error
|
||||
switch {
|
||||
case ref.GetId() != nil:
|
||||
s, err = m.getByID(ctx, ref.GetId())
|
||||
case ref.GetKey() != nil:
|
||||
s, err = m.getByKey(ctx, ref.GetKey())
|
||||
default:
|
||||
err = errtypes.NotFound(ref.String())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (m *mgr) Unshare(ctx context.Context, ref *collaboration.ShareReference) error {
|
||||
uid := ctxpkg.ContextMustGetUser(ctx).Username
|
||||
var query string
|
||||
params := []interface{}{}
|
||||
switch {
|
||||
case ref.GetId() != nil:
|
||||
query = "DELETE FROM oc_share where id=? AND (uid_owner=? or uid_initiator=?)"
|
||||
params = append(params, ref.GetId().OpaqueId, uid, uid)
|
||||
case ref.GetKey() != nil:
|
||||
key := ref.GetKey()
|
||||
shareType, shareWith, err := m.formatGrantee(ctx, key.Grantee)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
owner := formatUserID(key.Owner)
|
||||
query = "DELETE FROM oc_share WHERE uid_owner=? AND file_source=? AND share_type=? AND share_with=? AND (uid_owner=? or uid_initiator=?)"
|
||||
params = append(params, owner, key.ResourceId.OpaqueId, shareType, shareWith, uid, uid)
|
||||
default:
|
||||
return errtypes.NotFound(ref.String())
|
||||
}
|
||||
|
||||
stmt, err := m.db.Prepare(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := stmt.ExecContext(ctx, params...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowCnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowCnt == 0 {
|
||||
return errtypes.NotFound(ref.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mgr) UpdateShare(ctx context.Context, ref *collaboration.ShareReference, p *collaboration.SharePermissions, updated *collaboration.Share, fieldMask *field_mask.FieldMask) (*collaboration.Share, error) {
|
||||
permissions := sharePermToInt(p.Permissions)
|
||||
uid := ctxpkg.ContextMustGetUser(ctx).Username
|
||||
|
||||
var query string
|
||||
params := []interface{}{}
|
||||
switch {
|
||||
case ref.GetId() != nil:
|
||||
query = "update oc_share set permissions=?,stime=? where id=? AND (uid_owner=? or uid_initiator=?)"
|
||||
params = append(params, permissions, time.Now().Unix(), ref.GetId().OpaqueId, uid, uid)
|
||||
case ref.GetKey() != nil:
|
||||
key := ref.GetKey()
|
||||
shareType, shareWith, err := m.formatGrantee(ctx, key.Grantee)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
owner := formatUserID(key.Owner)
|
||||
query = "update oc_share set permissions=?,stime=? where (uid_owner=? or uid_initiator=?) AND file_source=? AND share_type=? AND share_with=? AND (uid_owner=? or uid_initiator=?)"
|
||||
params = append(params, permissions, time.Now().Unix(), owner, owner, key.ResourceId.OpaqueId, shareType, shareWith, uid, uid)
|
||||
default:
|
||||
return nil, errtypes.NotFound(ref.String())
|
||||
}
|
||||
|
||||
stmt, err := m.db.Prepare(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = stmt.ExecContext(ctx, params...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m.GetShare(ctx, ref)
|
||||
}
|
||||
|
||||
func (m *mgr) ListShares(ctx context.Context, filters []*collaboration.Filter) ([]*collaboration.Share, error) {
|
||||
uid := ctxpkg.ContextMustGetUser(ctx).Username
|
||||
query := `
|
||||
SELECT
|
||||
coalesce(s.uid_owner, '') as uid_owner, coalesce(s.uid_initiator, '') as uid_initiator,
|
||||
coalesce(s.share_with, '') as share_with, coalesce(s.file_source, '') as file_source,
|
||||
s.file_target, s.id, s.stime, s.permissions, s.share_type, fc.storage as storage
|
||||
FROM oc_share s
|
||||
LEFT JOIN oc_filecache fc ON fc.fileid = file_source
|
||||
WHERE (uid_owner=? or uid_initiator=?)
|
||||
`
|
||||
params := []interface{}{uid, uid}
|
||||
|
||||
var (
|
||||
filterQuery string
|
||||
filterParams []interface{}
|
||||
err error
|
||||
)
|
||||
if len(filters) == 0 {
|
||||
filterQuery += "(share_type=? OR share_type=?)"
|
||||
params = append(params, shareTypeUser)
|
||||
params = append(params, shareTypeGroup)
|
||||
} else {
|
||||
filterQuery, filterParams, err = translateFilters(filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
params = append(params, filterParams...)
|
||||
}
|
||||
|
||||
if filterQuery != "" {
|
||||
query = fmt.Sprintf("%s AND (%s)", query, filterQuery)
|
||||
}
|
||||
|
||||
rows, err := m.db.QueryContext(ctx, query, params...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var s DBShare
|
||||
shares := []*collaboration.Share{}
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.FileTarget, &s.ID, &s.STime, &s.Permissions, &s.ShareType, &s.ItemStorage); err != nil {
|
||||
continue
|
||||
}
|
||||
share, err := m.convertToCS3Share(ctx, s, m.storageMountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
shares = append(shares, share)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return shares, nil
|
||||
}
|
||||
|
||||
// we list the shares that are targeted to the user in context or to the user groups.
|
||||
func (m *mgr) ListReceivedShares(ctx context.Context, filters []*collaboration.Filter, forUser *userpb.UserId) ([]*collaboration.ReceivedShare, error) {
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
if user.GetId().GetType() == userpb.UserType_USER_TYPE_SERVICE {
|
||||
u, err := m.userConverter.GetUser(ctx, forUser)
|
||||
if err != nil {
|
||||
return nil, errtypes.BadRequest("user not found")
|
||||
}
|
||||
user = u
|
||||
}
|
||||
uid := user.Username
|
||||
|
||||
params := []interface{}{uid, uid, uid}
|
||||
for _, v := range user.Groups {
|
||||
params = append(params, v)
|
||||
}
|
||||
|
||||
homeConcat := ""
|
||||
if m.driver == "mysql" { // mysql concat
|
||||
homeConcat = "storages.id = CONCAT('home::', s.uid_owner)"
|
||||
} else { // sqlite3 concat
|
||||
homeConcat = "storages.id = 'home::' || s.uid_owner"
|
||||
}
|
||||
userSelect := ""
|
||||
if len(user.Groups) > 0 {
|
||||
userSelect = "AND ((share_type != 1 AND share_with=?) OR (share_type = 1 AND share_with in (?" + strings.Repeat(",?", len(user.Groups)-1) + ")))"
|
||||
} else {
|
||||
userSelect = "AND (share_type != 1 AND share_with=?)"
|
||||
}
|
||||
query := `
|
||||
WITH results AS
|
||||
(
|
||||
SELECT s.*, storages.numeric_id FROM oc_share s
|
||||
LEFT JOIN oc_storages storages ON ` + homeConcat + `
|
||||
WHERE (uid_owner != ? AND uid_initiator != ?) ` + userSelect + `
|
||||
)
|
||||
SELECT COALESCE(r.uid_owner, '') AS uid_owner, COALESCE(r.uid_initiator, '') AS uid_initiator, COALESCE(r.share_with, '')
|
||||
AS share_with, COALESCE(r.file_source, '') AS file_source, COALESCE(r2.file_target, r.file_target), r.id, r.stime, r.permissions, r.share_type, COALESCE(r2.accepted, r.accepted),
|
||||
r.numeric_id, COALESCE(r.parent, -1) AS parent FROM results r LEFT JOIN results r2 ON r.id = r2.parent WHERE r.parent IS NULL`
|
||||
|
||||
filterQuery, filterParams, err := translateFilters(filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
params = append(params, filterParams...)
|
||||
|
||||
if filterQuery != "" {
|
||||
query = fmt.Sprintf("%s AND (%s)", query, filterQuery)
|
||||
}
|
||||
query += ";"
|
||||
rows, err := m.db.QueryContext(ctx, query, params...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var s DBShare
|
||||
shares := []*collaboration.ReceivedShare{}
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.FileTarget, &s.ID, &s.STime, &s.Permissions, &s.ShareType, &s.State, &s.ItemStorage, &s.Parent); err != nil {
|
||||
continue
|
||||
}
|
||||
share, err := m.convertToCS3ReceivedShare(ctx, s, m.storageMountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
shares = append(shares, share)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return shares, nil
|
||||
}
|
||||
|
||||
func (m *mgr) GetReceivedShare(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.ReceivedShare, error) {
|
||||
var s *collaboration.ReceivedShare
|
||||
var err error
|
||||
switch {
|
||||
case ref.GetId() != nil:
|
||||
s, err = m.getReceivedByID(ctx, ref.GetId())
|
||||
case ref.GetKey() != nil:
|
||||
s, err = m.getReceivedByKey(ctx, ref.GetKey())
|
||||
default:
|
||||
err = errtypes.NotFound(ref.String())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
|
||||
}
|
||||
|
||||
func (m *mgr) UpdateReceivedShare(ctx context.Context, receivedShare *collaboration.ReceivedShare, fieldMask *field_mask.FieldMask, _ *userpb.UserId) (*collaboration.ReceivedShare, error) {
|
||||
// TODO: How to inject the uid when a UserId is set? override it in the ctx? Add parameter to GetReceivedShare?
|
||||
rs, err := m.GetReceivedShare(ctx, &collaboration.ShareReference{Spec: &collaboration.ShareReference_Id{Id: receivedShare.Share.Id}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fields := []string{}
|
||||
params := []interface{}{}
|
||||
for i := range fieldMask.Paths {
|
||||
switch fieldMask.Paths[i] {
|
||||
case "state":
|
||||
rs.State = receivedShare.State
|
||||
fields = append(fields, "accepted=?")
|
||||
switch rs.State {
|
||||
case collaboration.ShareState_SHARE_STATE_REJECTED:
|
||||
params = append(params, 2)
|
||||
case collaboration.ShareState_SHARE_STATE_ACCEPTED:
|
||||
params = append(params, 0)
|
||||
}
|
||||
case "mount_point":
|
||||
fields = append(fields, "file_target=?")
|
||||
rs.MountPoint = receivedShare.MountPoint
|
||||
params = append(params, rs.MountPoint.Path)
|
||||
case "hidden":
|
||||
continue
|
||||
default:
|
||||
return nil, errtypes.NotSupported("updating " + fieldMask.Paths[i] + " is not supported")
|
||||
}
|
||||
}
|
||||
|
||||
if len(fields) == 0 {
|
||||
return nil, fmt.Errorf("no valid field provided in the fieldmask")
|
||||
}
|
||||
|
||||
updateReceivedShare := func(column string) error {
|
||||
query := "update oc_share set "
|
||||
query += strings.Join(fields, ",")
|
||||
query += fmt.Sprintf(" where %s=?", column)
|
||||
params := append(params, rs.Share.Id.OpaqueId)
|
||||
|
||||
stmt, err := m.db.Prepare(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := stmt.ExecContext(ctx, params...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
affected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if affected < 1 {
|
||||
return fmt.Errorf("no rows updated")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
err = updateReceivedShare("parent") // Try to update the child state in case of group shares first
|
||||
if err != nil {
|
||||
err = updateReceivedShare("id")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
func (m *mgr) getByID(ctx context.Context, id *collaboration.ShareId) (*collaboration.Share, error) {
|
||||
uid := ctxpkg.ContextMustGetUser(ctx).Username
|
||||
s := 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(file_source, '') as file_source, file_target, stime, permissions, share_type FROM oc_share WHERE id=? AND (uid_owner=? or uid_initiator=?)"
|
||||
if err := m.db.QueryRowContext(ctx, query, id.OpaqueId, uid, uid).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.FileTarget, &s.STime, &s.Permissions, &s.ShareType); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, errtypes.NotFound(id.OpaqueId)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return m.convertToCS3Share(ctx, s, m.storageMountID)
|
||||
}
|
||||
|
||||
func (m *mgr) getByKey(ctx context.Context, key *collaboration.ShareKey) (*collaboration.Share, error) {
|
||||
owner, err := m.userConverter.UserIDToUserName(ctx, key.Owner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uid := ctxpkg.ContextMustGetUser(ctx).Username
|
||||
|
||||
s := DBShare{}
|
||||
shareType, shareWith, err := m.formatGrantee(ctx, key.Grantee)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(file_source, '') as file_source, file_target, id, stime, permissions, share_type FROM oc_share WHERE uid_owner=? AND file_source=? AND share_type=? AND share_with=? AND (uid_owner=? or uid_initiator=?)"
|
||||
if err = m.db.QueryRowContext(ctx, query, owner, key.ResourceId.StorageId, shareType, shareWith, uid, uid).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.FileTarget, &s.ID, &s.STime, &s.Permissions, &s.ShareType); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, errtypes.NotFound(key.String())
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return m.convertToCS3Share(ctx, s, m.storageMountID)
|
||||
}
|
||||
|
||||
func (m *mgr) getReceivedByID(ctx context.Context, id *collaboration.ShareId) (*collaboration.ReceivedShare, error) {
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
uid := user.Username
|
||||
|
||||
params := []interface{}{id.OpaqueId, id.OpaqueId, uid} //nolint:prealloc
|
||||
for _, v := range user.Groups {
|
||||
params = append(params, v)
|
||||
}
|
||||
|
||||
homeConcat := ""
|
||||
if m.driver == "mysql" { // mysql concat
|
||||
homeConcat = "storages.id = CONCAT('home::', s.uid_owner)"
|
||||
} else { // sqlite3 concat
|
||||
homeConcat = "storages.id = 'home::' || s.uid_owner"
|
||||
}
|
||||
userSelect := ""
|
||||
if len(user.Groups) > 0 {
|
||||
userSelect = "AND ((share_type != 1 AND share_with=?) OR (share_type = 1 AND share_with in (?" + strings.Repeat(",?", len(user.Groups)-1) + ")))"
|
||||
} else {
|
||||
userSelect = "AND (share_type != 1 AND share_with=?)"
|
||||
}
|
||||
|
||||
query := `
|
||||
WITH results AS
|
||||
(
|
||||
SELECT s.*, storages.numeric_id
|
||||
FROM oc_share s
|
||||
LEFT JOIN oc_storages storages ON ` + homeConcat + `
|
||||
WHERE s.id=? OR s.parent=? ` + userSelect + `
|
||||
)
|
||||
SELECT COALESCE(r.uid_owner, '') AS uid_owner, COALESCE(r.uid_initiator, '') AS uid_initiator, COALESCE(r.share_with, '')
|
||||
AS share_with, COALESCE(r.file_source, '') AS file_source, COALESCE(r2.file_target, r.file_target), r.id, r.stime, r.permissions, r.share_type, COALESCE(r2.accepted, r.accepted),
|
||||
r.numeric_id, COALESCE(r.parent, -1) AS parent
|
||||
FROM results r
|
||||
LEFT JOIN results r2 ON r.id = r2.parent
|
||||
WHERE r.parent IS NULL;
|
||||
`
|
||||
|
||||
s := DBShare{}
|
||||
if err := m.db.QueryRowContext(ctx, query, params...).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.FileTarget, &s.ID, &s.STime, &s.Permissions, &s.ShareType, &s.State, &s.ItemStorage, &s.Parent); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, errtypes.NotFound(id.OpaqueId)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return m.convertToCS3ReceivedShare(ctx, s, m.storageMountID)
|
||||
}
|
||||
|
||||
func (m *mgr) getReceivedByKey(ctx context.Context, key *collaboration.ShareKey) (*collaboration.ReceivedShare, error) {
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
uid := user.Username
|
||||
|
||||
shareType, shareWith, err := m.formatGrantee(ctx, key.Grantee)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
params := []interface{}{uid, formatUserID(key.Owner), key.ResourceId.StorageId, key.ResourceId.OpaqueId, shareType, shareWith, shareWith}
|
||||
for _, v := range user.Groups {
|
||||
params = append(params, v)
|
||||
}
|
||||
|
||||
s := DBShare{}
|
||||
query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(file_source, '') as file_source, file_target, ts.id, stime, permissions, share_type, accepted FROM oc_share ts WHERE uid_owner=? AND file_source=? AND share_type=? AND share_with=? "
|
||||
if len(user.Groups) > 0 {
|
||||
query += "AND (share_with=? OR share_with in (?" + strings.Repeat(",?", len(user.Groups)-1) + "))"
|
||||
} else {
|
||||
query += "AND (share_with=?)"
|
||||
}
|
||||
|
||||
if err := m.db.QueryRowContext(ctx, query, params...).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.FileTarget, &s.ID, &s.STime, &s.Permissions, &s.ShareType, &s.State); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, errtypes.NotFound(key.String())
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return m.convertToCS3ReceivedShare(ctx, s, m.storageMountID)
|
||||
}
|
||||
|
||||
func granteeTypeToShareType(granteeType provider.GranteeType) int {
|
||||
switch granteeType {
|
||||
case provider.GranteeType_GRANTEE_TYPE_USER:
|
||||
return shareTypeUser
|
||||
case provider.GranteeType_GRANTEE_TYPE_GROUP:
|
||||
return shareTypeGroup
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// translateFilters translates the filters to sql queries
|
||||
func translateFilters(filters []*collaboration.Filter) (string, []interface{}, error) {
|
||||
var (
|
||||
filterQuery string
|
||||
params []interface{}
|
||||
)
|
||||
|
||||
groupedFilters := share.GroupFiltersByType(filters)
|
||||
// If multiple filters of the same type are passed to this function, they need to be combined with the `OR` operator.
|
||||
// That is why the filters got grouped by type.
|
||||
// For every given filter type, iterate over the filters and if there are more than one combine them.
|
||||
// Combine the different filter types using `AND`
|
||||
var filterCounter = 0
|
||||
for filterType, filters := range groupedFilters {
|
||||
switch filterType {
|
||||
case collaboration.Filter_TYPE_RESOURCE_ID:
|
||||
filterQuery += "("
|
||||
for i, f := range filters {
|
||||
filterQuery += "file_source=?"
|
||||
params = append(params, f.GetResourceId().OpaqueId)
|
||||
|
||||
if i != len(filters)-1 {
|
||||
filterQuery += " OR "
|
||||
}
|
||||
}
|
||||
filterQuery += ")"
|
||||
case collaboration.Filter_TYPE_GRANTEE_TYPE:
|
||||
filterQuery += "("
|
||||
for i, f := range filters {
|
||||
filterQuery += "r.share_type=?"
|
||||
params = append(params, granteeTypeToShareType(f.GetGranteeType()))
|
||||
|
||||
if i != len(filters)-1 {
|
||||
filterQuery += " OR "
|
||||
}
|
||||
}
|
||||
filterQuery += ")"
|
||||
case collaboration.Filter_TYPE_EXCLUDE_DENIALS:
|
||||
// TODO this may change once the mapping of permission to share types is completed (cf. pkg/cbox/utils/conversions.go)
|
||||
filterQuery += "r.permissions > 0"
|
||||
default:
|
||||
return "", nil, fmt.Errorf("filter type is not supported")
|
||||
}
|
||||
if filterCounter != len(groupedFilters)-1 {
|
||||
filterQuery += " AND "
|
||||
}
|
||||
filterCounter++
|
||||
}
|
||||
return filterQuery, params, nil
|
||||
}
|
||||
BIN
Binary file not shown.
-36
@@ -1,36 +0,0 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package favorite
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
)
|
||||
|
||||
// Manager defines an interface for a favorites manager.
|
||||
type Manager interface {
|
||||
// ListFavorites returns all resources that were favorited by a user.
|
||||
ListFavorites(ctx context.Context, userID *user.UserId) ([]*provider.ResourceId, error)
|
||||
// SetFavorite marks a resource as favorited by a user.
|
||||
SetFavorite(ctx context.Context, userID *user.UserId, resourceInfo *provider.ResourceInfo) error
|
||||
// UnsetFavorite unmarks a resource as favorited by a user.
|
||||
UnsetFavorite(ctx context.Context, userID *user.UserId, resourceInfo *provider.ResourceInfo) error
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package loader
|
||||
|
||||
import (
|
||||
// Load share cache drivers.
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/storage/favorite/memory"
|
||||
// Add your own here
|
||||
)
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/favorite"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/favorite/registry"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register("memory", New)
|
||||
}
|
||||
|
||||
type mgr struct {
|
||||
sync.RWMutex
|
||||
favorites map[string]map[string]*provider.ResourceId
|
||||
}
|
||||
|
||||
// New returns an instance of the in-memory favorites manager.
|
||||
func New(m map[string]interface{}) (favorite.Manager, error) {
|
||||
return &mgr{favorites: make(map[string]map[string]*provider.ResourceId)}, nil
|
||||
}
|
||||
|
||||
func (m *mgr) ListFavorites(_ context.Context, userID *user.UserId) ([]*provider.ResourceId, error) {
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
favorites := make([]*provider.ResourceId, 0, len(m.favorites[userID.OpaqueId]))
|
||||
for _, id := range m.favorites[userID.OpaqueId] {
|
||||
favorites = append(favorites, id)
|
||||
}
|
||||
return favorites, nil
|
||||
}
|
||||
|
||||
func (m *mgr) SetFavorite(_ context.Context, userID *user.UserId, resourceInfo *provider.ResourceInfo) error {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
if m.favorites[userID.OpaqueId] == nil {
|
||||
m.favorites[userID.OpaqueId] = make(map[string]*provider.ResourceId)
|
||||
}
|
||||
m.favorites[userID.OpaqueId][resourceInfo.Id.OpaqueId] = resourceInfo.Id
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mgr) UnsetFavorite(_ context.Context, userID *user.UserId, resourceInfo *provider.ResourceInfo) error {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
delete(m.favorites[userID.OpaqueId], resourceInfo.Id.OpaqueId)
|
||||
return nil
|
||||
}
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package registry
|
||||
|
||||
import "github.com/opencloud-eu/reva/v2/pkg/storage/favorite"
|
||||
|
||||
// NewFunc is the function that favorite storage implementations
|
||||
// should register at init time.
|
||||
type NewFunc func(map[string]interface{}) (favorite.Manager, error)
|
||||
|
||||
// NewFuncs is a map containing all the registered favorite storage implementations.
|
||||
var NewFuncs = map[string]NewFunc{}
|
||||
|
||||
// Register registers a new favorite storage function.
|
||||
// Not safe for concurrent use. Safe for use from package init.
|
||||
func Register(name string, f NewFunc) {
|
||||
NewFuncs[name] = f
|
||||
}
|
||||
+9
@@ -32,6 +32,7 @@ import (
|
||||
"strings"
|
||||
|
||||
cephfs2 "github.com/ceph/go-ceph/cephfs"
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/pkg/errors"
|
||||
@@ -602,6 +603,14 @@ func (fs *cephfs) UnsetArbitraryMetadata(ctx context.Context, ref *provider.Refe
|
||||
return getRevaError(err)
|
||||
}
|
||||
|
||||
func (fs *cephfs) AddFavorite(ctx context.Context, ref *provider.Reference, userID *user.UserId) error {
|
||||
return errtypes.NotSupported("AddFavorite not implemented")
|
||||
}
|
||||
|
||||
func (fs *cephfs) RemoveFavorite(ctx context.Context, ref *provider.Reference, userID *user.UserId) error {
|
||||
return errtypes.NotSupported("RemoveFavorite not implemented")
|
||||
}
|
||||
|
||||
func (fs *cephfs) EmptyRecycle(ctx context.Context, ref *provider.Reference) error {
|
||||
return errtypes.NotSupported("cephfs: empty recycle not supported")
|
||||
}
|
||||
|
||||
+9
@@ -23,6 +23,7 @@ import (
|
||||
"io"
|
||||
"net/url"
|
||||
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
@@ -124,6 +125,14 @@ func (fs *hellofs) UnsetArbitraryMetadata(ctx context.Context, ref *provider.Ref
|
||||
return errtypes.NotSupported("unimplemented")
|
||||
}
|
||||
|
||||
func (fs *hellofs) AddFavorite(ctx context.Context, ref *provider.Reference, userID *user.UserId) error {
|
||||
return errtypes.NotSupported("AddFavorite not implemented")
|
||||
}
|
||||
|
||||
func (fs *hellofs) RemoveFavorite(ctx context.Context, ref *provider.Reference, userID *user.UserId) error {
|
||||
return errtypes.NotSupported("RemoveFavorite not implemented")
|
||||
}
|
||||
|
||||
// locks
|
||||
|
||||
// GetLock returns an existing lock on the given reference
|
||||
|
||||
+10
@@ -862,6 +862,16 @@ func (nc *StorageDriver) Unlock(ctx context.Context, ref *provider.Reference, lo
|
||||
return errtypes.NotSupported("unimplemented")
|
||||
}
|
||||
|
||||
// AddFavorite adds a favourite to a resource
|
||||
func (nc *StorageDriver) AddFavorite(ctx context.Context, ref *provider.Reference, userID *user.UserId) error {
|
||||
return errtypes.NotSupported("AddFavorite not implemented")
|
||||
}
|
||||
|
||||
// RemoveFavorite removes a favourite from a resource
|
||||
func (nc *StorageDriver) RemoveFavorite(ctx context.Context, ref *provider.Reference, userID *user.UserId) error {
|
||||
return errtypes.NotSupported("RemoveFavorite not implemented")
|
||||
}
|
||||
|
||||
// ListStorageSpaces as defined in the storage.FS interface
|
||||
func (nc *StorageDriver) ListStorageSpaces(ctx context.Context, f []*provider.ListStorageSpacesRequest_Filter, unrestricted bool) ([]*provider.StorageSpace, error) {
|
||||
bodyStr, _ := json.Marshal(f)
|
||||
|
||||
+8
@@ -1137,6 +1137,14 @@ func (fs *owncloudsqlfs) UnsetArbitraryMetadata(ctx context.Context, ref *provid
|
||||
}
|
||||
}
|
||||
|
||||
func (fs *owncloudsqlfs) AddFavorite(ctx context.Context, ref *provider.Reference, userID *userpb.UserId) error {
|
||||
return errtypes.NotSupported("AddFavorite not implemented")
|
||||
}
|
||||
|
||||
func (fs *owncloudsqlfs) RemoveFavorite(ctx context.Context, ref *provider.Reference, userID *userpb.UserId) error {
|
||||
return errtypes.NotSupported("RemoveFavorite not implemented")
|
||||
}
|
||||
|
||||
// GetLock returns an existing lock on the given reference
|
||||
func (fs *owncloudsqlfs) GetLock(ctx context.Context, ref *provider.Reference) (*provider.Lock, error) {
|
||||
return nil, errtypes.NotSupported("unimplemented")
|
||||
|
||||
+9
@@ -38,6 +38,7 @@ import (
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
@@ -806,3 +807,11 @@ func isLockFile(path string) bool {
|
||||
func isTrash(path string) bool {
|
||||
return strings.HasSuffix(path, ".trashinfo") || strings.HasSuffix(path, ".trashitem") || strings.Contains(path, ".Trash")
|
||||
}
|
||||
|
||||
func (t *Tree) AddFavorite(ctx context.Context, ref *provider.Reference, userID *user.UserId) error {
|
||||
return errtypes.NotSupported("AddFavorite not implemented")
|
||||
}
|
||||
|
||||
func (t *Tree) RemoveFavorite(ctx context.Context, ref *provider.Reference, userID *user.UserId) error {
|
||||
return errtypes.NotSupported("RemoveFavorite not implemented")
|
||||
}
|
||||
|
||||
Generated
Vendored
+46
@@ -1035,6 +1035,52 @@ func (fs *Decomposedfs) ListFolder(ctx context.Context, ref *provider.Reference,
|
||||
return finfos, nil
|
||||
}
|
||||
|
||||
// AddFavorite adds a favorite
|
||||
func (fs *Decomposedfs) AddFavorite(ctx context.Context, ref *provider.Reference, uid *user.UserId) error {
|
||||
ctx, span := tracer.Start(ctx, "AddFavorite")
|
||||
defer span.End()
|
||||
n, err := fs.lu.NodeFromResource(ctx, ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !n.Exists {
|
||||
return errtypes.NotFound(filepath.Join(n.ParentID, n.Name))
|
||||
}
|
||||
|
||||
rp, err := fs.p.AssemblePermissions(ctx, n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !rp.Stat {
|
||||
return errtypes.PermissionDenied("stat")
|
||||
}
|
||||
|
||||
return n.SetFavorite(ctx, uid)
|
||||
}
|
||||
|
||||
// RemoveFavorite removes a favorite
|
||||
func (fs *Decomposedfs) RemoveFavorite(ctx context.Context, ref *provider.Reference, uid *user.UserId) error {
|
||||
ctx, span := tracer.Start(ctx, "RemoveFavorite")
|
||||
defer span.End()
|
||||
n, err := fs.lu.NodeFromResource(ctx, ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !n.Exists {
|
||||
return errtypes.NotFound(filepath.Join(n.ParentID, n.Name))
|
||||
}
|
||||
|
||||
rp, err := fs.p.AssemblePermissions(ctx, n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !rp.Stat {
|
||||
return errtypes.PermissionDenied("stat")
|
||||
}
|
||||
|
||||
return n.UnsetFavorite(ctx, uid)
|
||||
}
|
||||
|
||||
// Delete deletes the specified resource
|
||||
func (fs *Decomposedfs) Delete(ctx context.Context, ref *provider.Reference) (err error) {
|
||||
ctx, span := tracer.Start(ctx, "Delete")
|
||||
|
||||
+7
-65
@@ -20,19 +20,14 @@ package decomposedfs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata/prefixes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/node"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@@ -44,8 +39,6 @@ func (fs *Decomposedfs) SetArbitraryMetadata(ctx context.Context, ref *provider.
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Decomposedfs: error resolving ref")
|
||||
}
|
||||
sublog := appctx.GetLogger(ctx).With().Str("spaceid", n.SpaceID).Str("nodeid", n.ID).Logger()
|
||||
|
||||
if !n.Exists {
|
||||
err = errtypes.NotFound(filepath.Join(n.ParentID, n.Name))
|
||||
return err
|
||||
@@ -91,25 +84,6 @@ func (fs *Decomposedfs) SetArbitraryMetadata(ctx context.Context, ref *provider.
|
||||
errs = append(errs, errors.Wrap(err, "could not set etag"))
|
||||
}
|
||||
}
|
||||
if val, ok := md.Metadata[node.FavoriteKey]; ok {
|
||||
delete(md.Metadata, node.FavoriteKey)
|
||||
if u, ok := ctxpkg.ContextGetUser(ctx); ok {
|
||||
if uid := u.GetId(); uid != nil {
|
||||
if err := n.SetFavorite(ctx, uid, val); err != nil {
|
||||
sublog.Error().Err(err).
|
||||
Interface("user", u).
|
||||
Msg("could not set favorite flag")
|
||||
errs = append(errs, errors.Wrap(err, "could not set favorite flag"))
|
||||
}
|
||||
} else {
|
||||
sublog.Error().Interface("user", u).Msg("user has no id")
|
||||
errs = append(errs, errors.Wrap(errtypes.UserRequired("userrequired"), "user has no id"))
|
||||
}
|
||||
} else {
|
||||
sublog.Error().Interface("user", u).Msg("error getting user from ctx")
|
||||
errs = append(errs, errors.Wrap(errtypes.UserRequired("userrequired"), "error getting user from ctx"))
|
||||
}
|
||||
}
|
||||
}
|
||||
for k, v := range md.Metadata {
|
||||
attrName := prefixes.MetadataPrefix + k
|
||||
@@ -168,46 +142,14 @@ func (fs *Decomposedfs) UnsetArbitraryMetadata(ctx context.Context, ref *provide
|
||||
|
||||
errs := []error{}
|
||||
for _, k := range keys {
|
||||
switch k {
|
||||
case node.FavoriteKey:
|
||||
// the favorite flag is specific to the user, so we need to incorporate the userid
|
||||
u, ok := ctxpkg.ContextGetUser(ctx)
|
||||
if !ok {
|
||||
sublog.Error().
|
||||
Interface("user", u).
|
||||
Msg("error getting user from ctx")
|
||||
errs = append(errs, errors.Wrap(errtypes.UserRequired("userrequired"), "error getting user from ctx"))
|
||||
continue
|
||||
}
|
||||
var uid *userpb.UserId
|
||||
if uid = u.GetId(); uid == nil || uid.OpaqueId == "" {
|
||||
sublog.Error().
|
||||
Interface("user", u).
|
||||
Msg("user has no id")
|
||||
errs = append(errs, errors.Wrap(errtypes.UserRequired("userrequired"), "user has no id"))
|
||||
continue
|
||||
}
|
||||
fa := fmt.Sprintf("%s:%s:%s@%s", prefixes.FavPrefix, utils.UserTypeToString(uid.GetType()), uid.GetOpaqueId(), uid.GetIdp())
|
||||
if err := n.RemoveXattr(ctx, fa, true); err != nil {
|
||||
if metadata.IsAttrUnset(err) {
|
||||
continue // already gone, ignore
|
||||
}
|
||||
sublog.Error().Err(err).
|
||||
Interface("user", u).
|
||||
Str("key", fa).
|
||||
Msg("could not unset favorite flag")
|
||||
errs = append(errs, errors.Wrap(err, "could not unset favorite flag"))
|
||||
}
|
||||
default:
|
||||
if err = n.RemoveXattr(ctx, prefixes.MetadataPrefix+k, true); err != nil {
|
||||
if metadata.IsAttrUnset(err) {
|
||||
continue // already gone, ignore
|
||||
}
|
||||
sublog.Error().Err(err).
|
||||
Str("key", k).
|
||||
Msg("could not unset metadata")
|
||||
errs = append(errs, errors.Wrap(err, "could not unset metadata"))
|
||||
if err = n.RemoveXattr(ctx, prefixes.MetadataPrefix+k, true); err != nil {
|
||||
if metadata.IsAttrUnset(err) {
|
||||
continue // already gone, ignore
|
||||
}
|
||||
sublog.Error().Err(err).
|
||||
Str("key", k).
|
||||
Msg("could not unset metadata")
|
||||
errs = append(errs, errors.Wrap(err, "could not unset metadata"))
|
||||
}
|
||||
}
|
||||
switch len(errs) {
|
||||
|
||||
Generated
Vendored
+9
@@ -18,6 +18,10 @@
|
||||
|
||||
package prefixes
|
||||
|
||||
import (
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
)
|
||||
|
||||
// Declare a list of xattr keys
|
||||
|
||||
// Currently,extended file attributes have four separated
|
||||
@@ -102,3 +106,8 @@ const (
|
||||
UserAcePrefix string = "u:"
|
||||
GroupAcePrefix string = "g:"
|
||||
)
|
||||
|
||||
func FavoriteKey(uid *userpb.UserId) string {
|
||||
// the favorite flag is specific to the user, so we need to incorporate the userid
|
||||
return FavPrefix + uid.OpaqueId
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user