diff --git a/go.mod b/go.mod index 41c36aa9361..c6a9a6385c6 100644 --- a/go.mod +++ b/go.mod @@ -52,4 +52,4 @@ go 1.13 replace github.com/eventials/go-tus => github.com/andrewmostello/go-tus v0.0.0-20200314041820-904a9904af9a -//replace google.golang.org/grpc => google.golang.org/grpc v1.26.0 // temporary downgrade +replace google.golang.org/grpc => google.golang.org/grpc v1.26.0 // temporary downgrade diff --git a/go.sum b/go.sum index 1bc7f295b92..1630d01d738 100644 --- a/go.sum +++ b/go.sum @@ -329,14 +329,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/term v1.1.0 h1:xIAAdCMh3QIAy+5FrE8Ad8XoDhEU4ufwbaSozViP9kk= github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= -<<<<<<< Updated upstream github.com/pkg/xattr v0.4.2 h1:fbVxr9lvkToTGgPljVszvFsOdcbSv5BmGABneyxRgZM= github.com/pkg/xattr v0.4.2/go.mod h1:sBD3RAqlr8Q+RC3FutZcikpT8nyDrIEEBw2J744gVWs= -======= -github.com/pkg/xattr v0.4.1 h1:dhclzL6EqOXNaPDWqoeb9tIxATfBSmjqL0b4DpSjwRw= -github.com/pkg/xattr v0.4.1/go.mod h1:W2cGD0TBEus7MkUgv0tNZ9JutLtVO3cXu+IBRuHqnFs= ->>>>>>> Stashed changes -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU= github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= @@ -363,8 +357,6 @@ github.com/prometheus/procfs v0.0.6/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx github.com/prometheus/statsd_exporter v0.15.0 h1:UiwC1L5HkxEPeapXdm2Ye0u1vUJfTj7uwT5yydYpa1E= github.com/prometheus/statsd_exporter v0.15.0/go.mod h1:Dv8HnkoLQkeEjkIE4/2ndAA7WL1zHKK7WMqFQqu72rw= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/ramya-rao-a/go-outline v0.0.0-20200117021646-2a048b4510eb h1:ilZSL4VaIq4Hsi+lH928xQKnSWymFug6r2gJomUBpW8= -github.com/ramya-rao-a/go-outline v0.0.0-20200117021646-2a048b4510eb/go.mod h1:1WL5IqM+CnRCAbXetRnL1YVoS9KtU2zMhOi/5oAVPo4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= @@ -378,27 +370,19 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/afero v1.3.2 h1:GDarE4TJQI52kYSbSAmLiId1Elfj+xgSDqrUZxFhxlU= github.com/spf13/afero v1.3.2/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -408,14 +392,11 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/studio-b12/gowebdav v0.0.0-20200303150724-9380631c29a1 h1:TPyHV/OgChqNcnYqCoCvIFjR9TU60gFXXBKnhOBzVEI= github.com/studio-b12/gowebdav v0.0.0-20200303150724-9380631c29a1/go.mod h1:gCcfDlA1Y7GqOaeEKw5l9dOGx1VLdc/HuQSlQAaZ30s= -github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= -github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tus/tusd v1.1.0/go.mod h1:3DWPOdeCnjBwKtv98y5dSws3itPqfce5TVa0s59LRiA= @@ -430,9 +411,6 @@ github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1: github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/zmb3/gogetdoc v0.0.0-20190228002656-b37376c5da6a h1:00UFliGZl2UciXe8o/2iuEsRQ9u7z0rzDTVzuj6EYY0= -github.com/zmb3/gogetdoc v0.0.0-20190228002656-b37376c5da6a/go.mod h1:ofmGw6LrMypycsiWcyug6516EXpIxSbZ+uI9ppGypfY= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.mongodb.org/mongo-driver v1.0.3 h1:GKoji1ld3tw2aC+GX1wbr/J2fX13yNacEYoJ8Nhr0yU= go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= @@ -479,7 +457,6 @@ golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= @@ -488,7 +465,6 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -515,9 +491,6 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -571,7 +544,6 @@ golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200918174421-af09f7315aff h1:1CPUrky56AcgSpxz/KfgzQWzfG09u5YOL8MvPYBlrL8= golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201101102859-da207088b7d1 h1:a/mKvvZr9Jcc8oKfcmgzyp7OwF73JPWsQLvH1z2Kxck= golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -586,7 +558,6 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181207195948-8634b1ecd393/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -619,17 +590,10 @@ golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200721223218-6123e77877b2 h1:kxDWg8KNMtpGjI/XVKGgOtSljTnVg/PrjhS8+0pxjLE= golang.org/x/tools v0.0.0-20200721223218-6123e77877b2/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20201019175715-b894a3290fff h1:HiwHyqQ9ttqCHuTa++R4wNxOg6MY1hduSDT8j2aXoMM= -golang.org/x/tools v0.0.0-20201019175715-b894a3290fff/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0 h1:KKgc1aqhV8wDPbDzlDtpvyjZFY3vjz85FP7p4wcQUyI= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.6.0/go.mod h1:btoxGiFvQNVUZQ8W08zLtrVS08CNpINPEfxXxgJL1Q4= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= @@ -644,11 +608,9 @@ google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/ google.golang.org/api v0.29.0 h1:BaiDisFir8O4IJxvAabCGGkQ6yCJegNQqSVoYUNAnbk= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -686,10 +648,6 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.32.0 h1:zWTV+LMdc3kaiJMSTOFz2UgSBgx8RNQoTGiZu3fR9S0= -google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1 h1:DGeFlSan2f+WEtCERJ4J9GJWk15TxUi8QGagfI87Xyc= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2 h1:EQyQC3sa8M+p6Ulc8yy9SWSS2GVwyRc83gAbG8lrl4o= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -709,13 +667,11 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/h2non/gock.v1 v1.0.14/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww= gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= @@ -728,7 +684,6 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c h1:grhR+C34yXImVGp7EzNk+DTIk+323eIUWOmEevy6bDo= gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/pkg/eosclient/eosbinary/eosbinary.go b/pkg/eosclient/eosbinary/eosbinary.go new file mode 100644 index 00000000000..7787fe8eee1 --- /dev/null +++ b/pkg/eosclient/eosbinary/eosbinary.go @@ -0,0 +1,944 @@ +// Copyright 2018-2020 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 eosbinary + +import ( + "bytes" + "context" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path" + "strconv" + "strings" + "syscall" + + "github.com/cs3org/reva/pkg/appctx" + "github.com/cs3org/reva/pkg/eosclient" + "github.com/cs3org/reva/pkg/errtypes" + "github.com/cs3org/reva/pkg/storage/utils/acl" + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "go.opencensus.io/trace" +) + +type eosVersion string + +const ( + versionPrefix = ".sys.v#." + + versionAquamarine = eosVersion("aquamarine") + versionCitrine = eosVersion("citrine") +) + +const ( + // SystemAttr is the system extended attribute. + SystemAttr eosclient.AttrType = iota + // UserAttr is the user extended attribute. + UserAttr +) + +func serializeAttribute(a *eosclient.Attribute) string { + return fmt.Sprintf("%d.%s=%s", a.Type, a.Key, a.Val) +} + +func isValidAttribute(a *eosclient.Attribute) bool { + // validate that an attribute is correct. + if (a.Type != SystemAttr && a.Type != UserAttr) || a.Key == "" { + return false + } + return true +} + +// Options to configure the Client. +type Options struct { + + // ForceSingleUserMode forces all connections to use only one user. + // This is the case when access to EOS is done from FUSE under apache or www-data. + ForceSingleUserMode bool + + // UseKeyTabAuth changes will authenticate requests by using an EOS keytab. + UseKeytab bool + + // Whether to maintain the same inode across various versions of a file. + // Requires extra metadata operations if set to true + VersionInvariant bool + + // SingleUsername is the username to use when connecting to EOS. + // Defaults to apache + SingleUsername string + + // Location of the eos binary. + // Default is /usr/bin/eos. + EosBinary string + + // Location of the xrdcopy binary. + // Default is /usr/bin/xrdcopy. + XrdcopyBinary string + + // URL of the EOS MGM. + // Default is root://eos-example.org + URL string + + // Location on the local fs where to store reads. + // Defaults to os.TempDir() + CacheDirectory string + + // Keytab is the location of the EOS keytab file. + Keytab string + + // SecProtocol is the comma separated list of security protocols used by xrootd. + // For example: "sss, unix" + SecProtocol string +} + +func (opt *Options) init() { + if opt.ForceSingleUserMode && opt.SingleUsername != "" { + opt.SingleUsername = "apache" + } + + if opt.EosBinary == "" { + opt.EosBinary = "/usr/bin/eos" + } + + if opt.XrdcopyBinary == "" { + opt.XrdcopyBinary = "/usr/bin/xrdcopy" + } + + if opt.URL == "" { + opt.URL = "root://eos-example.org" + } + + if opt.CacheDirectory == "" { + opt.CacheDirectory = os.TempDir() + } +} + +// Client performs actions against a EOS management node (MGM). +// It requires the eos-client and xrootd-client packages installed to work. +type Client struct { + opt *Options +} + +// New creates a new client with the given options. +func New(opt *Options) *Client { + opt.init() + c := new(Client) + c.opt = opt + return c +} + +// exec executes the command and returns the stdout, stderr and return code +func (c *Client) execute(ctx context.Context, cmd *exec.Cmd) (string, string, error) { + log := appctx.GetLogger(ctx) + + outBuf := &bytes.Buffer{} + errBuf := &bytes.Buffer{} + cmd.Stdout = outBuf + cmd.Stderr = errBuf + cmd.Env = []string{ + "EOS_MGM_URL=" + c.opt.URL, + } + + if c.opt.UseKeytab { + cmd.Env = append(cmd.Env, "XrdSecPROTOCOL="+c.opt.SecProtocol) + cmd.Env = append(cmd.Env, "XrdSecSSSKT="+c.opt.Keytab) + } + + err := cmd.Run() + + var exitStatus int + if exiterr, ok := err.(*exec.ExitError); ok { + // The program has exited with an exit code != 0 + // This works on both Unix and Windows. Although package + // syscall is generally platform dependent, WaitStatus is + // defined for both Unix and Windows and in both cases has + // an ExitStatus() method with the same signature. + if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { + + exitStatus = status.ExitStatus() + switch exitStatus { + case 0: + err = nil + case int(syscall.ENOENT): + err = errtypes.NotFound(errBuf.String()) + } + } + } + + args := fmt.Sprintf("%s", cmd.Args) + env := fmt.Sprintf("%s", cmd.Env) + log.Info().Str("args", args).Str("env", env).Int("exit", exitStatus).Msg("eos cmd") + + if err != nil && exitStatus != int(syscall.ENOENT) { // don't wrap the errtypes.NotFoundError + err = errors.Wrap(err, "eosclient: error while executing command") + } + + return outBuf.String(), errBuf.String(), err +} + +// exec executes only EOS commands the command and returns the stdout, stderr and return code. +// execute() executes arbitrary commands. +func (c *Client) executeEOS(ctx context.Context, cmd *exec.Cmd) (string, string, error) { + log := appctx.GetLogger(ctx) + + outBuf := &bytes.Buffer{} + errBuf := &bytes.Buffer{} + cmd.Stdout = outBuf + cmd.Stderr = errBuf + cmd.Env = []string{ + "EOS_MGM_URL=" + c.opt.URL, + } + if c.opt.UseKeytab { + cmd.Env = append(cmd.Env, "XrdSecPROTOCOL="+c.opt.SecProtocol) + cmd.Env = append(cmd.Env, "XrdSecSSSKT="+c.opt.Keytab) + } + + trace := trace.FromContext(ctx).SpanContext().TraceID.String() + cmd.Args = append(cmd.Args, "--comment", trace) + + err := cmd.Run() + + var exitStatus int + if exiterr, ok := err.(*exec.ExitError); ok { + // The program has exited with an exit code != 0 + // This works on both Unix and Windows. Although package + // syscall is generally platform dependent, WaitStatus is + // defined for both Unix and Windows and in both cases has + // an ExitStatus() method with the same signature. + if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { + exitStatus = status.ExitStatus() + switch exitStatus { + case 0: + err = nil + case int(syscall.ENOENT): + err = errtypes.NotFound(errBuf.String()) + case int(syscall.EPERM), int(syscall.E2BIG), int(syscall.EINVAL): + // eos reports back error code 1 (EPERM) when ? + // eos reports back error code 7 (E2BIG) when the user is not allowed to read the directory + // eos reports back error code 22 (EINVAL) when the user is not allowed to enter the instance + err = errtypes.PermissionDenied(errBuf.String()) + } + } + } + + args := fmt.Sprintf("%s", cmd.Args) + env := fmt.Sprintf("%s", cmd.Env) + log.Info().Str("args", args).Str("env", env).Int("exit", exitStatus).Str("err", errBuf.String()).Msg("eos cmd") + + if err != nil && exitStatus != int(syscall.ENOENT) { // don't wrap the errtypes.NotFoundError + err = errors.Wrap(err, "eosclient: error while executing command") + } + + return outBuf.String(), errBuf.String(), err +} + +func (c *Client) getVersion(ctx context.Context, rootUID, rootGID string) (eosVersion, error) { + cmd := exec.CommandContext(ctx, c.opt.EosBinary, "-r", rootUID, rootGID, "version") + stdout, _, err := c.executeEOS(ctx, cmd) + if err != nil { + return "", err + } + return c.parseVersion(ctx, stdout), nil +} + +func (c *Client) parseVersion(ctx context.Context, raw string) eosVersion { + var serverVersion string + rawLines := strings.Split(raw, "\n") + for _, rl := range rawLines { + if rl == "" { + continue + } + if strings.HasPrefix(rl, "EOS_SERVER_VERSION") { + serverVersion = strings.Split(strings.Split(rl, " ")[0], "=")[1] + break + } + } + + if strings.HasPrefix(serverVersion, "4.") { + return versionCitrine + } + return versionAquamarine +} + +// AddACL adds an new acl to EOS with the given aclType. +func (c *Client) AddACL(ctx context.Context, uid, gid, rootUID, rootGID, path string, a *acl.Entry) error { + version, err := c.getVersion(ctx, rootUID, rootGID) + if err != nil { + return err + } + + var cmd *exec.Cmd + if version == versionCitrine { + sysACL := a.CitrineSerialize() + cmd = exec.CommandContext(ctx, c.opt.EosBinary, "-r", rootUID, rootGID, "acl", "--sys", "--recursive", sysACL, path) + } else { + acls, err := c.getACLForPath(ctx, uid, gid, path) + if err != nil { + return err + } + + err = acls.SetEntry(a.Type, a.Qualifier, a.Permissions) + if err != nil { + return err + } + sysACL := acls.Serialize() + cmd = exec.CommandContext(ctx, c.opt.EosBinary, "-r", rootUID, rootGID, "attr", "-r", "set", fmt.Sprintf("sys.acl=%s", sysACL), path) + } + + _, _, err = c.executeEOS(ctx, cmd) + return err + +} + +// RemoveACL removes the acl from EOS. +func (c *Client) RemoveACL(ctx context.Context, uid, gid, rootUID, rootGID, path string, a *acl.Entry) error { + version, err := c.getVersion(ctx, rootUID, rootGID) + if err != nil { + return err + } + + var cmd *exec.Cmd + if version == versionCitrine { + sysACL := a.CitrineSerialize() + cmd = exec.CommandContext(ctx, c.opt.EosBinary, "-r", rootUID, rootGID, "acl", "--sys", "--recursive", sysACL, path) + } else { + acls, err := c.getACLForPath(ctx, uid, gid, path) + if err != nil { + return err + } + + acls.DeleteEntry(a.Type, a.Qualifier) + sysACL := acls.Serialize() + cmd = exec.CommandContext(ctx, c.opt.EosBinary, "-r", rootUID, rootGID, "attr", "-r", "set", fmt.Sprintf("sys.acl=%s", sysACL), path) + } + + _, _, err = c.executeEOS(ctx, cmd) + return err + +} + +// UpdateACL updates the EOS acl. +func (c *Client) UpdateACL(ctx context.Context, uid, gid, rootUID, rootGID, path string, a *acl.Entry) error { + return c.AddACL(ctx, uid, gid, rootUID, rootGID, path, a) +} + +// GetACL for a file +func (c *Client) GetACL(ctx context.Context, uid, gid, path, aclType, target string) (*acl.Entry, error) { + acls, err := c.ListACLs(ctx, uid, gid, path) + if err != nil { + return nil, err + } + for _, a := range acls { + if a.Type == aclType && a.Qualifier == target { + return a, nil + } + } + return nil, errtypes.NotFound(fmt.Sprintf("%s:%s", aclType, target)) + +} + +// ListACLs returns the list of ACLs present under the given path. +// EOS returns uids/gid for Citrine version and usernames for older versions. +// For Citire we need to convert back the uid back to username. +func (c *Client) ListACLs(ctx context.Context, uid, gid, path string) ([]*acl.Entry, error) { + + parsedACLs, err := c.getACLForPath(ctx, uid, gid, path) + if err != nil { + return nil, err + } + + // EOS Citrine ACLs are stored with uid. The UID will be resolved to the + // user opaque ID at the eosfs level. + return parsedACLs.Entries, nil +} + +func (c *Client) getACLForPath(ctx context.Context, uid, gid, path string) (*acl.ACLs, error) { + finfo, err := c.GetFileInfoByPath(ctx, uid, gid, path) + if err != nil { + return nil, err + } + + return acl.Parse(finfo.SysACL, acl.ShortTextForm) +} + +// GetFileInfoByInode returns the FileInfo by the given inode +func (c *Client) GetFileInfoByInode(ctx context.Context, uid, gid string, inode uint64) (*eosclient.FileInfo, error) { + cmd := exec.CommandContext(ctx, c.opt.EosBinary, "-r", uid, gid, "file", "info", fmt.Sprintf("inode:%d", inode), "-m") + stdout, _, err := c.executeEOS(ctx, cmd) + if err != nil { + return nil, err + } + info, err := c.parseFileInfo(stdout) + if err != nil { + return nil, err + } + + if c.opt.VersionInvariant && isVersionFolder(info.File) { + info, err = c.getFileInfoFromVersion(ctx, uid, gid, info.File) + if err != nil { + return nil, err + } + info.Inode = inode + } + + return info, nil +} + +// GetFileInfoByFXID returns the FileInfo by the given file id in hexadecimal +func (c *Client) GetFileInfoByFXID(ctx context.Context, uid, gid string, fxid string) (*eosclient.FileInfo, error) { + cmd := exec.CommandContext(ctx, c.opt.EosBinary, "-r", uid, gid, "file", "info", fmt.Sprintf("fxid:%s", fxid), "-m") + stdout, _, err := c.executeEOS(ctx, cmd) + if err != nil { + return nil, err + } + return c.parseFileInfo(stdout) +} + +// SetAttr sets an extended attributes on a path. +func (c *Client) SetAttr(ctx context.Context, uid, gid string, attr *eosclient.Attribute, recursive bool, path string) error { + if !isValidAttribute(attr) { + return errors.New("eos: attr is invalid: " + serializeAttribute(attr)) + } + var cmd *exec.Cmd + if recursive { + cmd = exec.CommandContext(ctx, "/usr/bin/eos", "-r", uid, gid, "attr", "-r", "set", serializeAttribute(attr), path) + } else { + cmd = exec.CommandContext(ctx, "/usr/bin/eos", "-r", uid, gid, "attr", "set", serializeAttribute(attr), path) + } + + _, _, err := c.executeEOS(ctx, cmd) + if err != nil { + return err + } + return nil +} + +// UnsetAttr unsets an extended attribute on a path. +func (c *Client) UnsetAttr(ctx context.Context, uid, gid string, attr *eosclient.Attribute, path string) error { + if !isValidAttribute(attr) { + return errors.New("eos: attr is invalid: " + serializeAttribute(attr)) + } + cmd := exec.CommandContext(ctx, "/usr/bin/eos", "-r", uid, gid, "attr", "-r", "rm", fmt.Sprintf("%d.%s", attr.Type, attr.Key), path) + _, _, err := c.executeEOS(ctx, cmd) + if err != nil { + return err + } + return nil +} + +// GetFileInfoByPath returns the FilInfo at the given path +func (c *Client) GetFileInfoByPath(ctx context.Context, uid, gid, path string) (*eosclient.FileInfo, error) { + cmd := exec.CommandContext(ctx, c.opt.EosBinary, "-r", uid, gid, "file", "info", path, "-m") + stdout, _, err := c.executeEOS(ctx, cmd) + if err != nil { + return nil, err + } + info, err := c.parseFileInfo(stdout) + if err != nil { + return nil, err + } + + if c.opt.VersionInvariant && !isVersionFolder(path) && !info.IsDir { + inode, err := c.getVersionFolderInode(ctx, uid, gid, path) + if err != nil { + return nil, err + } + info.Inode = inode + } + + return info, nil +} + +// GetQuota gets the quota of a user on the quota node defined by path +func (c *Client) GetQuota(ctx context.Context, username, rootUID, rootGID, path string) (*eosclient.QuotaInfo, error) { + cmd := exec.CommandContext(ctx, c.opt.EosBinary, "-r", rootUID, rootGID, "quota", "ls", "-u", username, "-m") + stdout, _, err := c.executeEOS(ctx, cmd) + if err != nil { + return nil, err + } + return c.parseQuota(path, stdout) +} + +// Touch creates a 0-size,0-replica file in the EOS namespace. +func (c *Client) Touch(ctx context.Context, uid, gid, path string) error { + cmd := exec.CommandContext(ctx, "/usr/bin/eos", "-r", uid, gid, "file", "touch", path) + _, _, err := c.executeEOS(ctx, cmd) + return err +} + +// Chown given path +func (c *Client) Chown(ctx context.Context, uid, gid, chownUID, chownGID, path string) error { + cmd := exec.CommandContext(ctx, c.opt.EosBinary, "-r", uid, gid, "chown", chownUID+":"+chownGID, path) + _, _, err := c.executeEOS(ctx, cmd) + return err +} + +// Chmod given path +func (c *Client) Chmod(ctx context.Context, uid, gid, mode, path string) error { + cmd := exec.CommandContext(ctx, c.opt.EosBinary, "-r", uid, gid, "chmod", mode, path) + _, _, err := c.executeEOS(ctx, cmd) + return err +} + +// CreateDir creates a directory at the given path +func (c *Client) CreateDir(ctx context.Context, uid, gid, path string) error { + cmd := exec.CommandContext(ctx, c.opt.EosBinary, "-r", uid, gid, "mkdir", "-p", path) + _, _, err := c.executeEOS(ctx, cmd) + return err +} + +// Remove removes the resource at the given path +func (c *Client) Remove(ctx context.Context, uid, gid, path string) error { + cmd := exec.CommandContext(ctx, c.opt.EosBinary, "-r", uid, gid, "rm", "-r", path) + _, _, err := c.executeEOS(ctx, cmd) + return err +} + +// Rename renames the resource referenced by oldPath to newPath +func (c *Client) Rename(ctx context.Context, uid, gid, oldPath, newPath string) error { + cmd := exec.CommandContext(ctx, c.opt.EosBinary, "-r", uid, gid, "file", "rename", oldPath, newPath) + _, _, err := c.executeEOS(ctx, cmd) + return err +} + +// List the contents of the directory given by path +func (c *Client) List(ctx context.Context, uid, gid, path string) ([]*eosclient.FileInfo, error) { + cmd := exec.CommandContext(ctx, c.opt.EosBinary, "-r", uid, gid, "find", "--fileinfo", "--maxdepth", "1", path) + stdout, _, err := c.executeEOS(ctx, cmd) + if err != nil { + return nil, errors.Wrapf(err, "eosclient: error listing fn=%s", path) + } + return c.parseFind(path, stdout) +} + +// Read reads a file from the mgm +func (c *Client) Read(ctx context.Context, uid, gid, path string) (io.ReadCloser, error) { + rand := "eosread-" + uuid.Must(uuid.NewV4()).String() + localTarget := fmt.Sprintf("%s/%s", c.opt.CacheDirectory, rand) + defer os.RemoveAll(localTarget) + + xrdPath := fmt.Sprintf("%s//%s", c.opt.URL, path) + cmd := exec.CommandContext(ctx, c.opt.XrdcopyBinary, "--nopbar", "--silent", "-f", xrdPath, localTarget, fmt.Sprintf("-OSeos.ruid=%s&eos.rgid=%s", uid, gid)) + _, _, err := c.execute(ctx, cmd) + if err != nil { + return nil, err + } + return os.Open(localTarget) +} + +// Write writes a stream to the mgm +func (c *Client) Write(ctx context.Context, uid, gid, path string, stream io.ReadCloser) error { + fd, err := ioutil.TempFile(c.opt.CacheDirectory, "eoswrite-") + if err != nil { + return err + } + defer fd.Close() + defer os.RemoveAll(fd.Name()) + + // copy stream to local temp file + _, err = io.Copy(fd, stream) + if err != nil { + return err + } + + return c.WriteFile(ctx, uid, gid, path, fd.Name()) +} + +// WriteFile writes an existing file to the mgm +func (c *Client) WriteFile(ctx context.Context, uid, gid, path, source string) error { + xrdPath := fmt.Sprintf("%s//%s", c.opt.URL, path) + cmd := exec.CommandContext(ctx, c.opt.XrdcopyBinary, "--nopbar", "--silent", "-f", source, xrdPath, fmt.Sprintf("-ODeos.ruid=%s&eos.rgid=%s", uid, gid)) + _, _, err := c.execute(ctx, cmd) + return err +} + +// ListDeletedEntries returns a list of the deleted entries. +func (c *Client) ListDeletedEntries(ctx context.Context, uid, gid string) ([]*eosclient.DeletedEntry, error) { + // TODO(labkode): add protection if slave is configured and alive to count how many files are in the trashbin before + // triggering the recycle ls call that could break the instance because of unavailable memory. + cmd := exec.CommandContext(ctx, c.opt.EosBinary, "-r", uid, gid, "recycle", "ls", "-m") + stdout, _, err := c.executeEOS(ctx, cmd) + if err != nil { + return nil, err + } + return parseRecycleList(stdout) +} + +// RestoreDeletedEntry restores a deleted entry. +func (c *Client) RestoreDeletedEntry(ctx context.Context, uid, gid, key string) error { + cmd := exec.CommandContext(ctx, c.opt.EosBinary, "-r", uid, gid, "recycle", "restore", key) + _, _, err := c.executeEOS(ctx, cmd) + return err +} + +// PurgeDeletedEntries purges all entries from the recycle bin. +func (c *Client) PurgeDeletedEntries(ctx context.Context, uid, gid string) error { + cmd := exec.CommandContext(ctx, c.opt.EosBinary, "-r", uid, gid, "recycle", "purge") + _, _, err := c.executeEOS(ctx, cmd) + return err +} + +// ListVersions list all the versions for a given file. +func (c *Client) ListVersions(ctx context.Context, uid, gid, p string) ([]*eosclient.FileInfo, error) { + versionFolder := getVersionFolder(p) + finfos, err := c.List(ctx, uid, gid, versionFolder) + if err != nil { + // we send back an empty list + return []*eosclient.FileInfo{}, nil + } + return finfos, nil +} + +// RollbackToVersion rollbacks a file to a previous version. +func (c *Client) RollbackToVersion(ctx context.Context, uid, gid, path, version string) error { + cmd := exec.CommandContext(ctx, c.opt.EosBinary, "-r", uid, gid, "file", "versions", path, version) + _, _, err := c.executeEOS(ctx, cmd) + return err +} + +// ReadVersion reads the version for the given file. +func (c *Client) ReadVersion(ctx context.Context, uid, gid, p, version string) (io.ReadCloser, error) { + versionFile := path.Join(getVersionFolder(p), version) + return c.Read(ctx, uid, gid, versionFile) +} + +func (c *Client) getVersionFolderInode(ctx context.Context, uid, gid, p string) (uint64, error) { + versionFolder := getVersionFolder(p) + md, err := c.GetFileInfoByPath(ctx, uid, gid, versionFolder) + if err != nil { + if err = c.CreateDir(ctx, uid, gid, versionFolder); err != nil { + return 0, err + } + md, err = c.GetFileInfoByPath(ctx, uid, gid, versionFolder) + if err != nil { + return 0, err + } + } + return md.Inode, nil +} + +func (c *Client) getFileInfoFromVersion(ctx context.Context, uid, gid, p string) (*eosclient.FileInfo, error) { + file := getFileFromVersionFolder(p) + md, err := c.GetFileInfoByPath(ctx, uid, gid, file) + if err != nil { + return nil, err + } + return md, nil +} + +func isVersionFolder(p string) bool { + return strings.HasPrefix(path.Base(p), versionPrefix) +} + +func getVersionFolder(p string) string { + return path.Join(path.Dir(p), versionPrefix+path.Base(p)) +} + +func getFileFromVersionFolder(p string) string { + return path.Join(path.Dir(p), strings.TrimPrefix(path.Base(p), versionPrefix)) +} + +func parseRecycleList(raw string) ([]*eosclient.DeletedEntry, error) { + entries := []*eosclient.DeletedEntry{} + rawLines := strings.FieldsFunc(raw, func(c rune) bool { + return c == '\n' + }) + for _, rl := range rawLines { + if rl == "" { + continue + } + entry, err := parseRecycleEntry(rl) + if err != nil { + return nil, err + } + entries = append(entries, entry) + } + return entries, nil +} + +// parse entries like these: +// recycle=ls recycle-bin=/eos/backup/proc/recycle/ uid=gonzalhu gid=it size=0 deletion-time=1510823151 type=recursive-dir keylength.restore-path=45 restore-path=/eos/scratch/user/g/gonzalhu/.sys.v#.app.ico/ restore-key=0000000000a35100 +// recycle=ls recycle-bin=/eos/backup/proc/recycle/ uid=gonzalhu gid=it size=381038 deletion-time=1510823151 type=file keylength.restore-path=36 restore-path=/eos/scratch/user/g/gonzalhu/app.ico restore-key=000000002544fdb3 +func parseRecycleEntry(raw string) (*eosclient.DeletedEntry, error) { + partsBySpace := strings.FieldsFunc(raw, func(c rune) bool { + return c == ' ' + }) + restoreKeyPair, partsBySpace := partsBySpace[len(partsBySpace)-1], partsBySpace[:len(partsBySpace)-1] + restorePathPair := strings.Join(partsBySpace[8:], " ") + + partsBySpace = partsBySpace[:8] + partsBySpace = append(partsBySpace, restorePathPair) + partsBySpace = append(partsBySpace, restoreKeyPair) + + kv := getMap(partsBySpace) + size, err := strconv.ParseUint(kv["size"], 10, 64) + if err != nil { + return nil, err + } + isDir := false + if kv["type"] == "recursive-dir" { + isDir = true + } + deletionMTime, err := strconv.ParseUint(strings.Split(kv["deletion-time"], ".")[0], 10, 64) + if err != nil { + return nil, err + } + entry := &eosclient.DeletedEntry{ + RestorePath: kv["restore-path"], + RestoreKey: kv["restore-key"], + Size: size, + DeletionMTime: deletionMTime, + IsDir: isDir, + } + return entry, nil +} + +func getMap(partsBySpace []string) map[string]string { + kv := map[string]string{} + for _, pair := range partsBySpace { + parts := strings.Split(pair, "=") + if len(parts) > 1 { + kv[parts[0]] = parts[1] + } + + } + return kv +} + +func (c *Client) parseFind(dirPath, raw string) ([]*eosclient.FileInfo, error) { + finfos := []*eosclient.FileInfo{} + rawLines := strings.FieldsFunc(raw, func(c rune) bool { + return c == '\n' + }) + for _, rl := range rawLines { + if rl == "" { + continue + } + fi, err := c.parseFileInfo(rl) + if err != nil { + return nil, err + } + // dirs in eos end with a slash, like /eos/user/g/gonzalhu/ + // we skip the current directory as eos find will return the directory we + // ask to find + if fi.File == path.Clean(dirPath) { + continue + } + finfos = append(finfos, fi) + } + return finfos, nil +} + +func (c Client) parseQuotaLine(line string) map[string]string { + partsBySpace := strings.FieldsFunc(line, func(c rune) bool { + return c == ' ' + }) + m := getMap(partsBySpace) + return m +} +func (c *Client) parseQuota(path, raw string) (*eosclient.QuotaInfo, error) { + rawLines := strings.FieldsFunc(raw, func(c rune) bool { + return c == '\n' + }) + for _, rl := range rawLines { + if rl == "" { + continue + } + + m := c.parseQuotaLine(rl) + // map[maxbytes:2000000000000 maxlogicalbytes:1000000000000 percentageusedbytes:0.49 quota:node uid:gonzalhu space:/eos/scratch/user/ usedbytes:9829986500 usedlogicalbytes:4914993250 statusfiles:ok usedfiles:334 maxfiles:1000000 statusbytes:ok] + + space := m["space"] + if strings.HasPrefix(path, space) { + maxBytesString := m["maxlogicalbytes"] + usedBytesString := m["usedlogicalbytes"] + maxBytes, _ := strconv.ParseInt(maxBytesString, 10, 64) + usedBytes, _ := strconv.ParseInt(usedBytesString, 10, 64) + + maxInodesString := m["maxfiles"] + usedInodesString := m["usedfiles"] + maxInodes, _ := strconv.ParseInt(maxInodesString, 10, 64) + usedInodes, _ := strconv.ParseInt(usedInodesString, 10, 64) + + qi := &eosclient.QuotaInfo{ + AvailableBytes: int(maxBytes), + UsedBytes: int(usedBytes), + AvailableInodes: int(maxInodes), + UsedInodes: int(usedInodes), + } + return qi, nil + } + } + return &eosclient.QuotaInfo{}, nil +} + +// TODO(labkode): better API to access extended attributes. +func (c *Client) parseFileInfo(raw string) (*eosclient.FileInfo, error) { + + line := raw[15:] + index := strings.Index(line, " file=/") + lengthString := line[0:index] + length, err := strconv.ParseUint(lengthString, 10, 64) + if err != nil { + return nil, err + } + + line = line[index+6:] // skip ' file=' + name := line[0:length] + + kv := make(map[string]string) + // strip trailing slash + kv["file"] = strings.TrimSuffix(name, "/") + + line = line[length+1:] + partsBySpace := strings.FieldsFunc(line, func(c rune) bool { // we have [size=45 container=3 ...} + return c == ' ' + }) + var previousXAttr = "" + for _, p := range partsBySpace { + partsByEqual := strings.Split(p, "=") // we have kv pairs like [size 14] + if len(partsByEqual) == 2 { + // handle xattrn and xattrv special cases + switch { + case partsByEqual[0] == "xattrn": + previousXAttr = partsByEqual[1] + case partsByEqual[0] == "xattrv": + kv[previousXAttr] = partsByEqual[1] + previousXAttr = "" + default: + kv[partsByEqual[0]] = partsByEqual[1] + + } + } + } + + fi, err := c.mapToFileInfo(kv) + if err != nil { + return nil, err + } + return fi, nil +} + +// mapToFileInfo converts the dictionary to an usable structure. +// The kv has format: +// map[sys.forced.space:default files:0 mode:42555 ino:5 sys.forced.blocksize:4k sys.forced.layout:replica uid:0 fid:5 sys.forced.blockchecksum:crc32c sys.recycle:/eos/backup/proc/recycle/ fxid:00000005 pid:1 etag:5:0.000 keylength.file:4 file:/eos treesize:1931593933849913 container:3 gid:0 mtime:1498571294.108614409 ctime:1460121992.294326762 pxid:00000001 sys.forced.checksum:adler sys.forced.nstripes:2] +func (c *Client) mapToFileInfo(kv map[string]string) (*eosclient.FileInfo, error) { + inode, err := strconv.ParseUint(kv["ino"], 10, 64) + if err != nil { + return nil, err + } + fid, err := strconv.ParseUint(kv["fid"], 10, 64) + if err != nil { + return nil, err + } + uid, err := strconv.ParseUint(kv["uid"], 10, 64) + if err != nil { + return nil, err + } + gid, err := strconv.ParseUint(kv["gid"], 10, 64) + if err != nil { + return nil, err + } + + var treeSize uint64 + // treeSize is only for containers, so we check + if val, ok := kv["treesize"]; ok { + treeSize, err = strconv.ParseUint(val, 10, 64) + if err != nil { + return nil, err + } + } + var fileCounter uint64 + // fileCounter is only for containers + if val, ok := kv["files"]; ok { + fileCounter, err = strconv.ParseUint(val, 10, 64) + if err != nil { + return nil, err + } + } + var dirCounter uint64 + // dirCounter is only for containers + if val, ok := kv["container"]; ok { + dirCounter, err = strconv.ParseUint(val, 10, 64) + if err != nil { + return nil, err + } + } + + // treeCount is the number of entries under the tree + treeCount := fileCounter + dirCounter + + var size uint64 + if val, ok := kv["size"]; ok { + size, err = strconv.ParseUint(val, 10, 64) + if err != nil { + return nil, err + } + } + + // look for the stime first as mtime is not updated for parent dirs; if that isn't set, we use mtime + var mtimesec, mtimenanos uint64 + var mtimeSet bool + if val, ok := kv["stime"]; ok && val != "" { + stimeSplit := strings.Split(val, ".") + if mtimesec, err = strconv.ParseUint(stimeSplit[0], 10, 64); err == nil { + mtimeSet = true + } + if mtimenanos, err = strconv.ParseUint(stimeSplit[1], 10, 32); err != nil { + mtimeSet = false + } + } + if !mtimeSet { + mtimeSplit := strings.Split(kv["mtime"], ".") + if mtimesec, err = strconv.ParseUint(mtimeSplit[0], 10, 64); err != nil { + return nil, err + } + if mtimenanos, err = strconv.ParseUint(mtimeSplit[1], 10, 32); err != nil { + return nil, err + } + } + + isDir := false + if _, ok := kv["files"]; ok { + isDir = true + } + + fi := &eosclient.FileInfo{ + File: kv["file"], + Inode: inode, + FID: fid, + UID: uid, + GID: gid, + ETag: kv["etag"], + Size: size, + TreeSize: treeSize, + MTimeSec: mtimesec, + MTimeNanos: uint32(mtimenanos), + IsDir: isDir, + Instance: c.opt.URL, + SysACL: kv["sys.acl"], + TreeCount: treeCount, + Attrs: kv, + } + + return fi, nil +} diff --git a/pkg/eosclient/eosclient.go b/pkg/eosclient/eosclient.go index ba4daf1108e..370b70ce733 100644 --- a/pkg/eosclient/eosclient.go +++ b/pkg/eosclient/eosclient.go @@ -19,950 +19,53 @@ package eosclient import ( - "bytes" "context" - "fmt" "io" - "io/ioutil" - "os" - "os/exec" - "path" - "strconv" - "strings" - "syscall" - "github.com/cs3org/reva/pkg/appctx" - "github.com/cs3org/reva/pkg/errtypes" "github.com/cs3org/reva/pkg/storage/utils/acl" - "github.com/gofrs/uuid" - "github.com/pkg/errors" - "go.opencensus.io/trace" ) -const ( - versionPrefix = ".sys.v#." - - versionAquamarine = eosVersion("aquamarine") - versionCitrine = eosVersion("citrine") -) +// EOSClient is the interface which enables access to EOS instances through various interfaces. +type EOSClient interface { + AddACL(ctx context.Context, uid, gid, rootUID, rootGID, path string, a *acl.Entry) error + RemoveACL(ctx context.Context, uid, gid, rootUID, rootGID, path string, a *acl.Entry) error + UpdateACL(ctx context.Context, uid, gid, rootUID, rootGID, path string, a *acl.Entry) error + GetACL(ctx context.Context, uid, gid, path, aclType, target string) (*acl.Entry, error) + ListACLs(ctx context.Context, uid, gid, path string) ([]*acl.Entry, error) + GetFileInfoByInode(ctx context.Context, uid, gid string, inode uint64) (*FileInfo, error) + GetFileInfoByFXID(ctx context.Context, uid, gid string, fxid string) (*FileInfo, error) + GetFileInfoByPath(ctx context.Context, uid, gid, path string) (*FileInfo, error) + SetAttr(ctx context.Context, uid, gid string, attr *Attribute, recursive bool, path string) error + UnsetAttr(ctx context.Context, uid, gid string, attr *Attribute, path string) error + GetQuota(ctx context.Context, username, rootUID, rootGID, path string) (*QuotaInfo, error) + Touch(ctx context.Context, uid, gid, path string) error + Chown(ctx context.Context, uid, gid, chownUID, chownGID, path string) error + Chmod(ctx context.Context, uid, gid, mode, path string) error + CreateDir(ctx context.Context, uid, gid, path string) error + Remove(ctx context.Context, uid, gid, path string) error + Rename(ctx context.Context, uid, gid, oldPath, newPath string) error + List(ctx context.Context, uid, gid, path string) ([]*FileInfo, error) + Read(ctx context.Context, uid, gid, path string) (io.ReadCloser, error) + Write(ctx context.Context, uid, gid, path string, stream io.ReadCloser) error + WriteFile(ctx context.Context, uid, gid, path, source string) error + ListDeletedEntries(ctx context.Context, uid, gid string) ([]*DeletedEntry, error) + RestoreDeletedEntry(ctx context.Context, uid, gid, key string) error + PurgeDeletedEntries(ctx context.Context, uid, gid string) error + ListVersions(ctx context.Context, uid, gid, p string) ([]*FileInfo, error) + RollbackToVersion(ctx context.Context, uid, gid, path, version string) error + ReadVersion(ctx context.Context, uid, gid, p, version string) (io.ReadCloser, error) +} // AttrType is the type of extended attribute, // either system (sys) or user (user). type AttrType uint32 -type eosVersion string - -const ( - // SystemAttr is the system extended attribute. - SystemAttr AttrType = iota - // UserAttr is the user extended attribute. - UserAttr -) - -func (at AttrType) String() string { - switch at { - case SystemAttr: - return "sys" - case UserAttr: - return "user" - default: - return "invalid" - } -} - // Attribute represents an EOS extended attribute. type Attribute struct { Type AttrType Key, Val string } -func (a *Attribute) serialize() string { - return fmt.Sprintf("%s.%s=%s", a.Type, a.Key, a.Val) -} - -func (a *Attribute) isValid() bool { - // validate that an attribute is correct. - if (a.Type != SystemAttr && a.Type != UserAttr) || a.Key == "" { - return false - } - return true -} - -// Options to configure the Client. -type Options struct { - - // ForceSingleUserMode forces all connections to use only one user. - // This is the case when access to EOS is done from FUSE under apache or www-data. - ForceSingleUserMode bool - - // UseKeyTabAuth changes will authenticate requests by using an EOS keytab. - UseKeytab bool - - // Whether to maintain the same inode across various versions of a file. - // Requires extra metadata operations if set to true - VersionInvariant bool - - // SingleUsername is the username to use when connecting to EOS. - // Defaults to apache - SingleUsername string - - // Location of the eos binary. - // Default is /usr/bin/eos. - EosBinary string - - // Location of the xrdcopy binary. - // Default is /usr/bin/xrdcopy. - XrdcopyBinary string - - // URL of the EOS MGM. - // Default is root://eos-example.org - URL string - - // Location on the local fs where to store reads. - // Defaults to os.TempDir() - CacheDirectory string - - // Keytab is the location of the EOS keytab file. - Keytab string - - // SecProtocol is the comma separated list of security protocols used by xrootd. - // For example: "sss, unix" - SecProtocol string -} - -func (opt *Options) init() { - if opt.ForceSingleUserMode && opt.SingleUsername != "" { - opt.SingleUsername = "apache" - } - - if opt.EosBinary == "" { - opt.EosBinary = "/usr/bin/eos" - } - - if opt.XrdcopyBinary == "" { - opt.XrdcopyBinary = "/usr/bin/xrdcopy" - } - - if opt.URL == "" { - opt.URL = "root://eos-example.org" - } - - if opt.CacheDirectory == "" { - opt.CacheDirectory = os.TempDir() - } -} - -// Client performs actions against a EOS management node (MGM). -// It requires the eos-client and xrootd-client packages installed to work. -type Client struct { - opt *Options -} - -// New creates a new client with the given options. -func New(opt *Options) *Client { - opt.init() - c := new(Client) - c.opt = opt - return c -} - -// exec executes the command and returns the stdout, stderr and return code -func (c *Client) execute(ctx context.Context, cmd *exec.Cmd) (string, string, error) { - log := appctx.GetLogger(ctx) - - outBuf := &bytes.Buffer{} - errBuf := &bytes.Buffer{} - cmd.Stdout = outBuf - cmd.Stderr = errBuf - cmd.Env = []string{ - "EOS_MGM_URL=" + c.opt.URL, - } - - if c.opt.UseKeytab { - cmd.Env = append(cmd.Env, "XrdSecPROTOCOL="+c.opt.SecProtocol) - cmd.Env = append(cmd.Env, "XrdSecSSSKT="+c.opt.Keytab) - } - - err := cmd.Run() - - var exitStatus int - if exiterr, ok := err.(*exec.ExitError); ok { - // The program has exited with an exit code != 0 - // This works on both Unix and Windows. Although package - // syscall is generally platform dependent, WaitStatus is - // defined for both Unix and Windows and in both cases has - // an ExitStatus() method with the same signature. - if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { - - exitStatus = status.ExitStatus() - switch exitStatus { - case 0: - err = nil - case int(syscall.ENOENT): - err = errtypes.NotFound(errBuf.String()) - } - } - } - - args := fmt.Sprintf("%s", cmd.Args) - env := fmt.Sprintf("%s", cmd.Env) - log.Info().Str("args", args).Str("env", env).Int("exit", exitStatus).Msg("eos cmd") - - if err != nil && exitStatus != int(syscall.ENOENT) { // don't wrap the errtypes.NotFoundError - err = errors.Wrap(err, "eosclient: error while executing command") - } - - return outBuf.String(), errBuf.String(), err -} - -// exec executes only EOS commands the command and returns the stdout, stderr and return code. -// execute() executes arbitrary commands. -func (c *Client) executeEOS(ctx context.Context, cmd *exec.Cmd) (string, string, error) { - log := appctx.GetLogger(ctx) - - outBuf := &bytes.Buffer{} - errBuf := &bytes.Buffer{} - cmd.Stdout = outBuf - cmd.Stderr = errBuf - cmd.Env = []string{ - "EOS_MGM_URL=" + c.opt.URL, - } - if c.opt.UseKeytab { - cmd.Env = append(cmd.Env, "XrdSecPROTOCOL="+c.opt.SecProtocol) - cmd.Env = append(cmd.Env, "XrdSecSSSKT="+c.opt.Keytab) - } - - trace := trace.FromContext(ctx).SpanContext().TraceID.String() - cmd.Args = append(cmd.Args, "--comment", trace) - - err := cmd.Run() - - var exitStatus int - if exiterr, ok := err.(*exec.ExitError); ok { - // The program has exited with an exit code != 0 - // This works on both Unix and Windows. Although package - // syscall is generally platform dependent, WaitStatus is - // defined for both Unix and Windows and in both cases has - // an ExitStatus() method with the same signature. - if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { - exitStatus = status.ExitStatus() - switch exitStatus { - case 0: - err = nil - case int(syscall.ENOENT): - err = errtypes.NotFound(errBuf.String()) - case int(syscall.EPERM), int(syscall.E2BIG), int(syscall.EINVAL): - // eos reports back error code 1 (EPERM) when ? - // eos reports back error code 7 (E2BIG) when the user is not allowed to read the directory - // eos reports back error code 22 (EINVAL) when the user is not allowed to enter the instance - err = errtypes.PermissionDenied(errBuf.String()) - } - } - } - - args := fmt.Sprintf("%s", cmd.Args) - env := fmt.Sprintf("%s", cmd.Env) - log.Info().Str("args", args).Str("env", env).Int("exit", exitStatus).Str("err", errBuf.String()).Msg("eos cmd") - - if err != nil && exitStatus != int(syscall.ENOENT) { // don't wrap the errtypes.NotFoundError - err = errors.Wrap(err, "eosclient: error while executing command") - } - - return outBuf.String(), errBuf.String(), err -} - -func (c *Client) getVersion(ctx context.Context, rootUID, rootGID string) (eosVersion, error) { - cmd := exec.CommandContext(ctx, c.opt.EosBinary, "-r", rootUID, rootGID, "version") - stdout, _, err := c.executeEOS(ctx, cmd) - if err != nil { - return "", err - } - return c.parseVersion(ctx, stdout), nil -} - -func (c *Client) parseVersion(ctx context.Context, raw string) eosVersion { - var serverVersion string - rawLines := strings.Split(raw, "\n") - for _, rl := range rawLines { - if rl == "" { - continue - } - if strings.HasPrefix(rl, "EOS_SERVER_VERSION") { - serverVersion = strings.Split(strings.Split(rl, " ")[0], "=")[1] - break - } - } - - if strings.HasPrefix(serverVersion, "4.") { - return versionCitrine - } - return versionAquamarine -} - -// AddACL adds an new acl to EOS with the given aclType. -func (c *Client) AddACL(ctx context.Context, uid, gid, rootUID, rootGID, path string, a *acl.Entry) error { - version, err := c.getVersion(ctx, rootUID, rootGID) - if err != nil { - return err - } - - var cmd *exec.Cmd - if version == versionCitrine { - sysACL := a.CitrineSerialize() - cmd = exec.CommandContext(ctx, c.opt.EosBinary, "-r", rootUID, rootGID, "acl", "--sys", "--recursive", sysACL, path) - } else { - acls, err := c.getACLForPath(ctx, uid, gid, path) - if err != nil { - return err - } - - err = acls.SetEntry(a.Type, a.Qualifier, a.Permissions) - if err != nil { - return err - } - sysACL := acls.Serialize() - cmd = exec.CommandContext(ctx, c.opt.EosBinary, "-r", rootUID, rootGID, "attr", "-r", "set", fmt.Sprintf("sys.acl=%s", sysACL), path) - } - - _, _, err = c.executeEOS(ctx, cmd) - return err - -} - -// RemoveACL removes the acl from EOS. -func (c *Client) RemoveACL(ctx context.Context, uid, gid, rootUID, rootGID, path string, a *acl.Entry) error { - version, err := c.getVersion(ctx, rootUID, rootGID) - if err != nil { - return err - } - - var cmd *exec.Cmd - if version == versionCitrine { - sysACL := a.CitrineSerialize() - cmd = exec.CommandContext(ctx, c.opt.EosBinary, "-r", rootUID, rootGID, "acl", "--sys", "--recursive", sysACL, path) - } else { - acls, err := c.getACLForPath(ctx, uid, gid, path) - if err != nil { - return err - } - - acls.DeleteEntry(a.Type, a.Qualifier) - sysACL := acls.Serialize() - cmd = exec.CommandContext(ctx, c.opt.EosBinary, "-r", rootUID, rootGID, "attr", "-r", "set", fmt.Sprintf("sys.acl=%s", sysACL), path) - } - - _, _, err = c.executeEOS(ctx, cmd) - return err - -} - -// UpdateACL updates the EOS acl. -func (c *Client) UpdateACL(ctx context.Context, uid, gid, rootUID, rootGID, path string, a *acl.Entry) error { - return c.AddACL(ctx, uid, gid, rootUID, rootGID, path, a) -} - -// GetACL for a file -func (c *Client) GetACL(ctx context.Context, uid, gid, path, aclType, target string) (*acl.Entry, error) { - acls, err := c.ListACLs(ctx, uid, gid, path) - if err != nil { - return nil, err - } - for _, a := range acls { - if a.Type == aclType && a.Qualifier == target { - return a, nil - } - } - return nil, errtypes.NotFound(fmt.Sprintf("%s:%s", aclType, target)) - -} - -// ListACLs returns the list of ACLs present under the given path. -// EOS returns uids/gid for Citrine version and usernames for older versions. -// For Citire we need to convert back the uid back to username. -func (c *Client) ListACLs(ctx context.Context, uid, gid, path string) ([]*acl.Entry, error) { - - parsedACLs, err := c.getACLForPath(ctx, uid, gid, path) - if err != nil { - return nil, err - } - - // EOS Citrine ACLs are stored with uid. The UID will be resolved to the - // user opaque ID at the eosfs level. - return parsedACLs.Entries, nil -} - -func (c *Client) getACLForPath(ctx context.Context, uid, gid, path string) (*acl.ACLs, error) { - finfo, err := c.GetFileInfoByPath(ctx, uid, gid, path) - if err != nil { - return nil, err - } - - return acl.Parse(finfo.SysACL, acl.ShortTextForm) -} - -// GetFileInfoByInode returns the FileInfo by the given inode -func (c *Client) GetFileInfoByInode(ctx context.Context, uid, gid string, inode uint64) (*FileInfo, error) { - cmd := exec.CommandContext(ctx, c.opt.EosBinary, "-r", uid, gid, "file", "info", fmt.Sprintf("inode:%d", inode), "-m") - stdout, _, err := c.executeEOS(ctx, cmd) - if err != nil { - return nil, err - } - info, err := c.parseFileInfo(stdout) - if err != nil { - return nil, err - } - - if c.opt.VersionInvariant && isVersionFolder(info.File) { - info, err = c.getFileInfoFromVersion(ctx, uid, gid, info.File) - if err != nil { - return nil, err - } - info.Inode = inode - } - - return info, nil -} - -// GetFileInfoByFXID returns the FileInfo by the given file id in hexadecimal -func (c *Client) GetFileInfoByFXID(ctx context.Context, uid, gid string, fxid string) (*FileInfo, error) { - cmd := exec.CommandContext(ctx, c.opt.EosBinary, "-r", uid, gid, "file", "info", fmt.Sprintf("fxid:%s", fxid), "-m") - stdout, _, err := c.executeEOS(ctx, cmd) - if err != nil { - return nil, err - } - return c.parseFileInfo(stdout) -} - -// SetAttr sets an extended attributes on a path. -func (c *Client) SetAttr(ctx context.Context, uid, gid string, attr *Attribute, recursive bool, path string) error { - if !attr.isValid() { - return errors.New("eos: attr is invalid: " + attr.serialize()) - } - var cmd *exec.Cmd - if recursive { - cmd = exec.CommandContext(ctx, "/usr/bin/eos", "-r", uid, gid, "attr", "-r", "set", attr.serialize(), path) - } else { - cmd = exec.CommandContext(ctx, "/usr/bin/eos", "-r", uid, gid, "attr", "set", attr.serialize(), path) - } - - _, _, err := c.executeEOS(ctx, cmd) - if err != nil { - return err - } - return nil -} - -// UnsetAttr unsets an extended attribute on a path. -func (c *Client) UnsetAttr(ctx context.Context, uid, gid string, attr *Attribute, path string) error { - if !attr.isValid() { - return errors.New("eos: attr is invalid: " + attr.serialize()) - } - cmd := exec.CommandContext(ctx, "/usr/bin/eos", "-r", uid, gid, "attr", "-r", "rm", fmt.Sprintf("%s.%s", attr.Type, attr.Key), path) - _, _, err := c.executeEOS(ctx, cmd) - if err != nil { - return err - } - return nil -} - -// GetFileInfoByPath returns the FilInfo at the given path -func (c *Client) GetFileInfoByPath(ctx context.Context, uid, gid, path string) (*FileInfo, error) { - cmd := exec.CommandContext(ctx, c.opt.EosBinary, "-r", uid, gid, "file", "info", path, "-m") - stdout, _, err := c.executeEOS(ctx, cmd) - if err != nil { - return nil, err - } - info, err := c.parseFileInfo(stdout) - if err != nil { - return nil, err - } - - if c.opt.VersionInvariant && !isVersionFolder(path) && !info.IsDir { - inode, err := c.getVersionFolderInode(ctx, uid, gid, path) - if err != nil { - return nil, err - } - info.Inode = inode - } - - return info, nil -} - -// GetQuota gets the quota of a user on the quota node defined by path -func (c *Client) GetQuota(ctx context.Context, username, rootUID, rootGID, path string) (*QuotaInfo, error) { - cmd := exec.CommandContext(ctx, c.opt.EosBinary, "-r", rootUID, rootGID, "quota", "ls", "-u", username, "-m") - stdout, _, err := c.executeEOS(ctx, cmd) - if err != nil { - return nil, err - } - return c.parseQuota(path, stdout) -} - -// Touch creates a 0-size,0-replica file in the EOS namespace. -func (c *Client) Touch(ctx context.Context, uid, gid, path string) error { - cmd := exec.CommandContext(ctx, "/usr/bin/eos", "-r", uid, gid, "file", "touch", path) - _, _, err := c.executeEOS(ctx, cmd) - return err -} - -// Chown given path -func (c *Client) Chown(ctx context.Context, uid, gid, chownUID, chownGID, path string) error { - cmd := exec.CommandContext(ctx, c.opt.EosBinary, "-r", uid, gid, "chown", chownUID+":"+chownGID, path) - _, _, err := c.executeEOS(ctx, cmd) - return err -} - -// Chmod given path -func (c *Client) Chmod(ctx context.Context, uid, gid, mode, path string) error { - cmd := exec.CommandContext(ctx, c.opt.EosBinary, "-r", uid, gid, "chmod", mode, path) - _, _, err := c.executeEOS(ctx, cmd) - return err -} - -// CreateDir creates a directory at the given path -func (c *Client) CreateDir(ctx context.Context, uid, gid, path string) error { - cmd := exec.CommandContext(ctx, c.opt.EosBinary, "-r", uid, gid, "mkdir", "-p", path) - _, _, err := c.executeEOS(ctx, cmd) - return err -} - -// Remove removes the resource at the given path -func (c *Client) Remove(ctx context.Context, uid, gid, path string) error { - cmd := exec.CommandContext(ctx, c.opt.EosBinary, "-r", uid, gid, "rm", "-r", path) - _, _, err := c.executeEOS(ctx, cmd) - return err -} - -// Rename renames the resource referenced by oldPath to newPath -func (c *Client) Rename(ctx context.Context, uid, gid, oldPath, newPath string) error { - cmd := exec.CommandContext(ctx, c.opt.EosBinary, "-r", uid, gid, "file", "rename", oldPath, newPath) - _, _, err := c.executeEOS(ctx, cmd) - return err -} - -// List the contents of the directory given by path -func (c *Client) List(ctx context.Context, uid, gid, path string) ([]*FileInfo, error) { - cmd := exec.CommandContext(ctx, c.opt.EosBinary, "-r", uid, gid, "find", "--fileinfo", "--maxdepth", "1", path) - stdout, _, err := c.executeEOS(ctx, cmd) - if err != nil { - return nil, errors.Wrapf(err, "eosclient: error listing fn=%s", path) - } - return c.parseFind(path, stdout) -} - -// Read reads a file from the mgm -func (c *Client) Read(ctx context.Context, uid, gid, path string) (io.ReadCloser, error) { - rand := "eosread-" + uuid.Must(uuid.NewV4()).String() - localTarget := fmt.Sprintf("%s/%s", c.opt.CacheDirectory, rand) - defer os.RemoveAll(localTarget) - - xrdPath := fmt.Sprintf("%s//%s", c.opt.URL, path) - cmd := exec.CommandContext(ctx, c.opt.XrdcopyBinary, "--nopbar", "--silent", "-f", xrdPath, localTarget, fmt.Sprintf("-OSeos.ruid=%s&eos.rgid=%s", uid, gid)) - _, _, err := c.execute(ctx, cmd) - if err != nil { - return nil, err - } - return os.Open(localTarget) -} - -// Write writes a stream to the mgm -func (c *Client) Write(ctx context.Context, uid, gid, path string, stream io.ReadCloser) error { - fd, err := ioutil.TempFile(c.opt.CacheDirectory, "eoswrite-") - if err != nil { - return err - } - defer fd.Close() - defer os.RemoveAll(fd.Name()) - - // copy stream to local temp file - _, err = io.Copy(fd, stream) - if err != nil { - return err - } - - return c.WriteFile(ctx, uid, gid, path, fd.Name()) -} - -// WriteFile writes an existing file to the mgm -func (c *Client) WriteFile(ctx context.Context, uid, gid, path, source string) error { - xrdPath := fmt.Sprintf("%s//%s", c.opt.URL, path) - cmd := exec.CommandContext(ctx, c.opt.XrdcopyBinary, "--nopbar", "--silent", "-f", source, xrdPath, fmt.Sprintf("-ODeos.ruid=%s&eos.rgid=%s", uid, gid)) - _, _, err := c.execute(ctx, cmd) - return err -} - -// ListDeletedEntries returns a list of the deleted entries. -func (c *Client) ListDeletedEntries(ctx context.Context, uid, gid string) ([]*DeletedEntry, error) { - // TODO(labkode): add protection if slave is configured and alive to count how many files are in the trashbin before - // triggering the recycle ls call that could break the instance because of unavailable memory. - cmd := exec.CommandContext(ctx, c.opt.EosBinary, "-r", uid, gid, "recycle", "ls", "-m") - stdout, _, err := c.executeEOS(ctx, cmd) - if err != nil { - return nil, err - } - return parseRecycleList(stdout) -} - -// RestoreDeletedEntry restores a deleted entry. -func (c *Client) RestoreDeletedEntry(ctx context.Context, uid, gid, key string) error { - cmd := exec.CommandContext(ctx, c.opt.EosBinary, "-r", uid, gid, "recycle", "restore", key) - _, _, err := c.executeEOS(ctx, cmd) - return err -} - -// PurgeDeletedEntries purges all entries from the recycle bin. -func (c *Client) PurgeDeletedEntries(ctx context.Context, uid, gid string) error { - cmd := exec.CommandContext(ctx, c.opt.EosBinary, "-r", uid, gid, "recycle", "purge") - _, _, err := c.executeEOS(ctx, cmd) - return err -} - -// ListVersions list all the versions for a given file. -func (c *Client) ListVersions(ctx context.Context, uid, gid, p string) ([]*FileInfo, error) { - versionFolder := getVersionFolder(p) - finfos, err := c.List(ctx, uid, gid, versionFolder) - if err != nil { - // we send back an empty list - return []*FileInfo{}, nil - } - return finfos, nil -} - -// RollbackToVersion rollbacks a file to a previous version. -func (c *Client) RollbackToVersion(ctx context.Context, uid, gid, path, version string) error { - cmd := exec.CommandContext(ctx, c.opt.EosBinary, "-r", uid, gid, "file", "versions", path, version) - _, _, err := c.executeEOS(ctx, cmd) - return err -} - -// ReadVersion reads the version for the given file. -func (c *Client) ReadVersion(ctx context.Context, uid, gid, p, version string) (io.ReadCloser, error) { - versionFile := path.Join(getVersionFolder(p), version) - return c.Read(ctx, uid, gid, versionFile) -} - -func (c *Client) getVersionFolderInode(ctx context.Context, uid, gid, p string) (uint64, error) { - versionFolder := getVersionFolder(p) - md, err := c.GetFileInfoByPath(ctx, uid, gid, versionFolder) - if err != nil { - if err = c.CreateDir(ctx, uid, gid, versionFolder); err != nil { - return 0, err - } - md, err = c.GetFileInfoByPath(ctx, uid, gid, versionFolder) - if err != nil { - return 0, err - } - } - return md.Inode, nil -} - -func (c *Client) getFileInfoFromVersion(ctx context.Context, uid, gid, p string) (*FileInfo, error) { - file := getFileFromVersionFolder(p) - md, err := c.GetFileInfoByPath(ctx, uid, gid, file) - if err != nil { - return nil, err - } - return md, nil -} - -func isVersionFolder(p string) bool { - return strings.HasPrefix(path.Base(p), versionPrefix) -} - -func getVersionFolder(p string) string { - return path.Join(path.Dir(p), versionPrefix+path.Base(p)) -} - -func getFileFromVersionFolder(p string) string { - return path.Join(path.Dir(p), strings.TrimPrefix(path.Base(p), versionPrefix)) -} - -func parseRecycleList(raw string) ([]*DeletedEntry, error) { - entries := []*DeletedEntry{} - rawLines := strings.FieldsFunc(raw, func(c rune) bool { - return c == '\n' - }) - for _, rl := range rawLines { - if rl == "" { - continue - } - entry, err := parseRecycleEntry(rl) - if err != nil { - return nil, err - } - entries = append(entries, entry) - } - return entries, nil -} - -// parse entries like these: -// recycle=ls recycle-bin=/eos/backup/proc/recycle/ uid=gonzalhu gid=it size=0 deletion-time=1510823151 type=recursive-dir keylength.restore-path=45 restore-path=/eos/scratch/user/g/gonzalhu/.sys.v#.app.ico/ restore-key=0000000000a35100 -// recycle=ls recycle-bin=/eos/backup/proc/recycle/ uid=gonzalhu gid=it size=381038 deletion-time=1510823151 type=file keylength.restore-path=36 restore-path=/eos/scratch/user/g/gonzalhu/app.ico restore-key=000000002544fdb3 -func parseRecycleEntry(raw string) (*DeletedEntry, error) { - partsBySpace := strings.FieldsFunc(raw, func(c rune) bool { - return c == ' ' - }) - restoreKeyPair, partsBySpace := partsBySpace[len(partsBySpace)-1], partsBySpace[:len(partsBySpace)-1] - restorePathPair := strings.Join(partsBySpace[8:], " ") - - partsBySpace = partsBySpace[:8] - partsBySpace = append(partsBySpace, restorePathPair) - partsBySpace = append(partsBySpace, restoreKeyPair) - - kv := getMap(partsBySpace) - size, err := strconv.ParseUint(kv["size"], 10, 64) - if err != nil { - return nil, err - } - isDir := false - if kv["type"] == "recursive-dir" { - isDir = true - } - deletionMTime, err := strconv.ParseUint(strings.Split(kv["deletion-time"], ".")[0], 10, 64) - if err != nil { - return nil, err - } - entry := &DeletedEntry{ - RestorePath: kv["restore-path"], - RestoreKey: kv["restore-key"], - Size: size, - DeletionMTime: deletionMTime, - IsDir: isDir, - } - return entry, nil -} - -func getMap(partsBySpace []string) map[string]string { - kv := map[string]string{} - for _, pair := range partsBySpace { - parts := strings.Split(pair, "=") - if len(parts) > 1 { - kv[parts[0]] = parts[1] - } - - } - return kv -} - -func (c *Client) parseFind(dirPath, raw string) ([]*FileInfo, error) { - finfos := []*FileInfo{} - rawLines := strings.FieldsFunc(raw, func(c rune) bool { - return c == '\n' - }) - for _, rl := range rawLines { - if rl == "" { - continue - } - fi, err := c.parseFileInfo(rl) - if err != nil { - return nil, err - } - // dirs in eos end with a slash, like /eos/user/g/gonzalhu/ - // we skip the current directory as eos find will return the directory we - // ask to find - if fi.File == path.Clean(dirPath) { - continue - } - finfos = append(finfos, fi) - } - return finfos, nil -} - -func (c Client) parseQuotaLine(line string) map[string]string { - partsBySpace := strings.FieldsFunc(line, func(c rune) bool { - return c == ' ' - }) - m := getMap(partsBySpace) - return m -} -func (c *Client) parseQuota(path, raw string) (*QuotaInfo, error) { - rawLines := strings.FieldsFunc(raw, func(c rune) bool { - return c == '\n' - }) - for _, rl := range rawLines { - if rl == "" { - continue - } - - m := c.parseQuotaLine(rl) - // map[maxbytes:2000000000000 maxlogicalbytes:1000000000000 percentageusedbytes:0.49 quota:node uid:gonzalhu space:/eos/scratch/user/ usedbytes:9829986500 usedlogicalbytes:4914993250 statusfiles:ok usedfiles:334 maxfiles:1000000 statusbytes:ok] - - space := m["space"] - if strings.HasPrefix(path, space) { - maxBytesString := m["maxlogicalbytes"] - usedBytesString := m["usedlogicalbytes"] - maxBytes, _ := strconv.ParseInt(maxBytesString, 10, 64) - usedBytes, _ := strconv.ParseInt(usedBytesString, 10, 64) - - maxInodesString := m["maxfiles"] - usedInodesString := m["usedfiles"] - maxInodes, _ := strconv.ParseInt(maxInodesString, 10, 64) - usedInodes, _ := strconv.ParseInt(usedInodesString, 10, 64) - - qi := &QuotaInfo{ - AvailableBytes: int(maxBytes), - UsedBytes: int(usedBytes), - AvailableInodes: int(maxInodes), - UsedInodes: int(usedInodes), - } - return qi, nil - } - } - return &QuotaInfo{}, nil -} - -// TODO(labkode): better API to access extended attributes. -func (c *Client) parseFileInfo(raw string) (*FileInfo, error) { - - line := raw[15:] - index := strings.Index(line, " file=/") - lengthString := line[0:index] - length, err := strconv.ParseUint(lengthString, 10, 64) - if err != nil { - return nil, err - } - - line = line[index+6:] // skip ' file=' - name := line[0:length] - - kv := make(map[string]string) - // strip trailing slash - kv["file"] = strings.TrimSuffix(name, "/") - - line = line[length+1:] - partsBySpace := strings.FieldsFunc(line, func(c rune) bool { // we have [size=45 container=3 ...} - return c == ' ' - }) - var previousXAttr = "" - for _, p := range partsBySpace { - partsByEqual := strings.Split(p, "=") // we have kv pairs like [size 14] - if len(partsByEqual) == 2 { - // handle xattrn and xattrv special cases - switch { - case partsByEqual[0] == "xattrn": - previousXAttr = partsByEqual[1] - case partsByEqual[0] == "xattrv": - kv[previousXAttr] = partsByEqual[1] - previousXAttr = "" - default: - kv[partsByEqual[0]] = partsByEqual[1] - - } - } - } - - fi, err := c.mapToFileInfo(kv) - if err != nil { - return nil, err - } - return fi, nil -} - -// mapToFileInfo converts the dictionary to an usable structure. -// The kv has format: -// map[sys.forced.space:default files:0 mode:42555 ino:5 sys.forced.blocksize:4k sys.forced.layout:replica uid:0 fid:5 sys.forced.blockchecksum:crc32c sys.recycle:/eos/backup/proc/recycle/ fxid:00000005 pid:1 etag:5:0.000 keylength.file:4 file:/eos treesize:1931593933849913 container:3 gid:0 mtime:1498571294.108614409 ctime:1460121992.294326762 pxid:00000001 sys.forced.checksum:adler sys.forced.nstripes:2] -func (c *Client) mapToFileInfo(kv map[string]string) (*FileInfo, error) { - inode, err := strconv.ParseUint(kv["ino"], 10, 64) - if err != nil { - return nil, err - } - fid, err := strconv.ParseUint(kv["fid"], 10, 64) - if err != nil { - return nil, err - } - uid, err := strconv.ParseUint(kv["uid"], 10, 64) - if err != nil { - return nil, err - } - gid, err := strconv.ParseUint(kv["gid"], 10, 64) - if err != nil { - return nil, err - } - - var treeSize uint64 - // treeSize is only for containers, so we check - if val, ok := kv["treesize"]; ok { - treeSize, err = strconv.ParseUint(val, 10, 64) - if err != nil { - return nil, err - } - } - var fileCounter uint64 - // fileCounter is only for containers - if val, ok := kv["files"]; ok { - fileCounter, err = strconv.ParseUint(val, 10, 64) - if err != nil { - return nil, err - } - } - var dirCounter uint64 - // dirCounter is only for containers - if val, ok := kv["container"]; ok { - dirCounter, err = strconv.ParseUint(val, 10, 64) - if err != nil { - return nil, err - } - } - - // treeCount is the number of entries under the tree - treeCount := fileCounter + dirCounter - - var size uint64 - if val, ok := kv["size"]; ok { - size, err = strconv.ParseUint(val, 10, 64) - if err != nil { - return nil, err - } - } - - // look for the stime first as mtime is not updated for parent dirs; if that isn't set, we use mtime - var mtimesec, mtimenanos uint64 - var mtimeSet bool - if val, ok := kv["stime"]; ok && val != "" { - stimeSplit := strings.Split(val, ".") - if mtimesec, err = strconv.ParseUint(stimeSplit[0], 10, 64); err == nil { - mtimeSet = true - } - if mtimenanos, err = strconv.ParseUint(stimeSplit[1], 10, 32); err != nil { - mtimeSet = false - } - } - if !mtimeSet { - mtimeSplit := strings.Split(kv["mtime"], ".") - if mtimesec, err = strconv.ParseUint(mtimeSplit[0], 10, 64); err != nil { - return nil, err - } - if mtimenanos, err = strconv.ParseUint(mtimeSplit[1], 10, 32); err != nil { - return nil, err - } - } - - isDir := false - if _, ok := kv["files"]; ok { - isDir = true - } - - fi := &FileInfo{ - File: kv["file"], - Inode: inode, - FID: fid, - UID: uid, - GID: gid, - ETag: kv["etag"], - Size: size, - TreeSize: treeSize, - MTimeSec: mtimesec, - MTimeNanos: uint32(mtimenanos), - IsDir: isDir, - Instance: c.opt.URL, - SysACL: kv["sys.acl"], - TreeCount: treeCount, - Attrs: kv, - } - - return fi, nil -} - // FileInfo represents the metadata information returned by querying the EOS namespace. type FileInfo struct { IsDir bool diff --git a/pkg/eosclientgrpc/eos_grpc/eos_grpc.pb.go b/pkg/eosclient/eosgrpc/eos_grpc/eos_grpc.pb.go similarity index 100% rename from pkg/eosclientgrpc/eos_grpc/eos_grpc.pb.go rename to pkg/eosclient/eosgrpc/eos_grpc/eos_grpc.pb.go diff --git a/pkg/eosclientgrpc/eos_grpc/eos_grpc.proto b/pkg/eosclient/eosgrpc/eos_grpc/eos_grpc.proto similarity index 100% rename from pkg/eosclientgrpc/eos_grpc/eos_grpc.proto rename to pkg/eosclient/eosgrpc/eos_grpc/eos_grpc.proto diff --git a/pkg/eosclientgrpc/eosclientgrpc.go b/pkg/eosclient/eosgrpc/eosgrpc.go similarity index 60% rename from pkg/eosclientgrpc/eosclientgrpc.go rename to pkg/eosclient/eosgrpc/eosgrpc.go index 075f03fc7a7..74523eae89d 100644 --- a/pkg/eosclientgrpc/eosclientgrpc.go +++ b/pkg/eosclient/eosgrpc/eosgrpc.go @@ -16,7 +16,7 @@ // granted to it by virtue of its status as an Intergovernmental Organization // or submit itself to any jurisdiction. -package eosclientgrpc +package eosgrpc import ( "bytes" @@ -26,14 +26,15 @@ import ( "io/ioutil" "os" "os/exec" - gouser "os/user" "path" "path/filepath" "strconv" + "strings" "syscall" "github.com/cs3org/reva/pkg/appctx" - erpc "github.com/cs3org/reva/pkg/eosclientgrpc/eos_grpc" + "github.com/cs3org/reva/pkg/eosclient" + erpc "github.com/cs3org/reva/pkg/eosclient/eosgrpc/eos_grpc" "github.com/cs3org/reva/pkg/errtypes" "github.com/cs3org/reva/pkg/storage/utils/acl" "github.com/gofrs/uuid" @@ -48,34 +49,13 @@ const ( versionPrefix = ".sys.v#." ) -// AttrType is the type of extended attribute, -// either system (sys) or user (user). -type AttrType uint32 - const ( // SystemAttr is the system extended attribute. - SystemAttr AttrType = iota + SystemAttr eosclient.AttrType = iota // UserAttr is the user extended attribute. UserAttr ) -func (at AttrType) String() string { - switch at { - case SystemAttr: - return "sys" - case UserAttr: - return "user" - default: - return "invalid" - } -} - -// Attribute represents an EOS extended attribute. -type Attribute struct { - Type AttrType - Key, Val string -} - // Options to configure the Client. type Options struct { @@ -86,6 +66,10 @@ type Options struct { // UseKeyTabAuth changes will authenticate requests by using an EOS keytab. UseKeytab bool + // Whether to maintain the same inode across various versions of a file. + // Requires extra metadata operations if set to true + VersionInvariant bool + // SingleUsername is the username to use when connecting to EOS. // Defaults to apache SingleUsername string @@ -177,138 +161,78 @@ func newgrpc(ctx context.Context, opt *Options) (erpc.EosClient, error) { // New creates a new client with the given options. func New(opt *Options) *Client { tlog := logger.New().With().Int("pid", os.Getpid()).Logger() - tlog.Debug().Str("Creating new eosgrpc client. opt: ", "'"+fmt.Sprintf("%#v", opt)+"' ").Msg("") opt.init() c := new(Client) c.opt = opt - tctx := appctx.WithLogger(context.Background(), &tlog) - // Let's be successful if the ping was ok. This is an initialization phase // and we enforce the server to be up // This will likely improve as soon as the behaviour of grpc is understood // in the case of server restarts or failures + tctx := appctx.WithLogger(context.Background(), &tlog) ccl, err := newgrpc(tctx, opt) if err != nil { return nil } c.cl = ccl - // Some connection tests, useful for logging in this dev phase - tlog.Debug().Str("Connection tests to: ", "'"+opt.GrpcURI+"' ").Msg("") - - tlog.Debug().Str("Going to stat", "/eos").Msg("") - frep, err := c.GetFileInfoByPath(tctx, "furano", "/eos") - if err != nil { - tlog.Error().Str("GetFileInfoByPath /eos to ", "'"+opt.GrpcURI+"' ").Str("err:", err.Error()).Msg("") - // return nil - } else { - tlog.Info().Str("GetFileInfoByPath /eos to ", "'"+opt.GrpcURI+"' ").Str("resp:", frep.File).Msg("") - } - - tlog.Debug().Str("Going to stat", "/eos-idonotexist").Msg("") - frep1, err := c.GetFileInfoByPath(tctx, "furano", "/eos-idonotexist") - if err != nil { - tlog.Info().Str("GetFileInfoByPath /eos-idonotexist to ", "'"+opt.GrpcURI+"' ").Str("err:", err.Error()).Msg("") - - // return nil - } else { - tlog.Error().Str("GetFileInfoByPath /eos-idonotexist to ", "'"+opt.GrpcURI+"' ").Str("wrong resp:", frep1.File).Msg("") - } - - tlog.Debug().Str("Going to list", "/eos").Msg("") - lrep, err := c.List(context.Background(), "furano", "/eos") - if err != nil { - tlog.Error().Str("List /eos to ", "'"+opt.GrpcURI+"' ").Str("err:", err.Error()).Msg("") - // return nil - } else { - tlog.Info().Str("List /eos to ", "'"+opt.GrpcURI+"' ").Int("nentries:", len(lrep)).Msg("") - } - return c } // Common code to create and initialize a NSRequest -func (c *Client) initNSRequest(username string) (*erpc.NSRequest, error) { +func (c *Client) initNSRequest(uid, gid string) (*erpc.NSRequest, error) { // Stuff filename, uid, gid into the MDRequest type rq := new(erpc.NSRequest) - - // setting of the sys.acl is only possible from root user - unixUser, err := c.getUnixUser(username) - if err != nil { - return nil, err - } rq.Role = new(erpc.RoleId) - uid, err := strconv.ParseUint(unixUser.Uid, 10, 64) + uidInt, err := strconv.ParseUint(uid, 10, 64) if err != nil { return nil, err } - rq.Role.Uid = uid - gid, err := strconv.ParseUint(unixUser.Gid, 10, 64) + gidInt, err := strconv.ParseUint(gid, 10, 64) if err != nil { return nil, err } - rq.Role.Gid = gid - + rq.Role.Uid = uidInt + rq.Role.Gid = gidInt rq.Authkey = c.opt.Authkey return rq, nil } // Common code to create and initialize a NSRequest -func (c *Client) initMDRequest(username string) (*erpc.MDRequest, error) { +func (c *Client) initMDRequest(uid, gid string) (*erpc.MDRequest, error) { // Stuff filename, uid, gid into the MDRequest type mdrq := new(erpc.MDRequest) - - unixUser, err := c.getUnixUser(username) - if err != nil { - return nil, err - } mdrq.Role = new(erpc.RoleId) - uid, err := strconv.ParseUint(unixUser.Uid, 10, 64) + uidInt, err := strconv.ParseUint(uid, 10, 64) if err != nil { return nil, err } - mdrq.Role.Uid = uid - gid, err := strconv.ParseUint(unixUser.Gid, 10, 64) + gidInt, err := strconv.ParseUint(gid, 10, 64) if err != nil { return nil, err } - mdrq.Role.Gid = gid + mdrq.Role.Uid = uidInt + mdrq.Role.Gid = gidInt mdrq.Authkey = c.opt.Authkey return mdrq, nil } -func (c *Client) getUnixUser(username string) (*gouser.User, error) { - if c.opt.ForceSingleUserMode { - username = c.opt.SingleUsername - } - return gouser.Lookup(username) -} - // AddACL adds an new acl to EOS with the given aclType. -func (c *Client) AddACL(ctx context.Context, username, path string, a *acl.Entry) error { +func (c *Client) AddACL(ctx context.Context, uid, gid, rootUID, rootGID, path string, a *acl.Entry) error { log := appctx.GetLogger(ctx) - acls, err := c.getACLForPath(ctx, username, path) + acls, err := c.getACLForPath(ctx, uid, gid, path) if err != nil { return err } - // since EOS Citrine ACLs are is stored with uid, we need to convert username to uid - // only for users. - if a.Type == acl.TypeUser { - a.Qualifier, err = getUID(a.Qualifier) - if err != nil { - return err - } - } err = acls.SetEntry(a.Type, a.Qualifier, a.Permissions) if err != nil { return err @@ -316,7 +240,7 @@ func (c *Client) AddACL(ctx context.Context, username, path string, a *acl.Entry sysACL := acls.Serialize() // Init a new NSRequest - rq, err := c.initNSRequest(username) + rq, err := c.initNSRequest(uid, gid) if err != nil { return err } @@ -349,29 +273,19 @@ func (c *Client) AddACL(ctx context.Context, username, path string, a *acl.Entry } // RemoveACL removes the acl from EOS. -func (c *Client) RemoveACL(ctx context.Context, username, path string, aclType string, recipient string) error { +func (c *Client) RemoveACL(ctx context.Context, uid, gid, rootUID, rootGID, path string, a *acl.Entry) error { log := appctx.GetLogger(ctx) - acls, err := c.getACLForPath(ctx, username, path) + acls, err := c.getACLForPath(ctx, uid, gid, path) if err != nil { return err } - // since EOS Citrine ACLs are is stored with uid, we need to convert username to uid - // only for users. - - // since EOS Citrine ACLs are stored with uid, we need to convert username to uid - if aclType == acl.TypeUser { - recipient, err = getUID(recipient) - if err != nil { - return err - } - } - acls.DeleteEntry(aclType, recipient) + acls.DeleteEntry(a.Type, a.Qualifier) sysACL := acls.Serialize() // Init a new NSRequest - rq, err := c.initNSRequest(username) + rq, err := c.initNSRequest(uid, gid) if err != nil { return err } @@ -404,13 +318,13 @@ func (c *Client) RemoveACL(ctx context.Context, username, path string, aclType s } // UpdateACL updates the EOS acl. -func (c *Client) UpdateACL(ctx context.Context, username, path string, a *acl.Entry) error { - return c.AddACL(ctx, username, path, a) +func (c *Client) UpdateACL(ctx context.Context, uid, gid, rootUID, rootGID, path string, a *acl.Entry) error { + return c.AddACL(ctx, uid, gid, path, rootUID, rootGID, a) } // GetACL for a file -func (c *Client) GetACL(ctx context.Context, username, path, aclType, target string) (*acl.Entry, error) { - acls, err := c.ListACLs(ctx, username, path) +func (c *Client) GetACL(ctx context.Context, uid, gid, path, aclType, target string) (*acl.Entry, error) { + acls, err := c.ListACLs(ctx, uid, gid, path) if err != nil { return nil, err } @@ -423,52 +337,25 @@ func (c *Client) GetACL(ctx context.Context, username, path, aclType, target str } -func getUsername(uid string) (string, error) { - user, err := gouser.LookupId(uid) - if err != nil { - return "", err - } - return user.Username, nil -} - -func getUID(username string) (string, error) { - user, err := gouser.Lookup(username) - if err != nil { - return "", err - } - return user.Uid, nil -} - // ListACLs returns the list of ACLs present under the given path. // EOS returns uids/gid for Citrine version and usernames for older versions. // For Citire we need to convert back the uid back to username. -func (c *Client) ListACLs(ctx context.Context, username, path string) ([]*acl.Entry, error) { - log := appctx.GetLogger(ctx) - - parsedACLs, err := c.getACLForPath(ctx, username, path) +func (c *Client) ListACLs(ctx context.Context, uid, gid, path string) ([]*acl.Entry, error) { + parsedACLs, err := c.getACLForPath(ctx, uid, gid, path) if err != nil { return nil, err } - acls := []*acl.Entry{} - for _, acl := range parsedACLs.Entries { - // since EOS Citrine ACLs are is stored with uid, we need to convert uid to username - // TODO map group names as well if acl.Type == "g" ... - acl.Qualifier, err = getUsername(acl.Qualifier) - if err != nil { - log.Warn().Err(err).Str("path", path).Str("username", username).Str("qualifier", acl.Qualifier).Msg("cannot map qualifier to name") - continue - } - acls = append(acls, acl) - } - return acls, nil + // EOS Citrine ACLs are stored with uid. The UID will be resolved to the + // user opaque ID at the eosfs level. + return parsedACLs.Entries, nil } -func (c *Client) getACLForPath(ctx context.Context, username, path string) (*acl.ACLs, error) { +func (c *Client) getACLForPath(ctx context.Context, uid, gid, path string) (*acl.ACLs, error) { log := appctx.GetLogger(ctx) // Initialize the common fields of the NSReq - rq, err := c.initNSRequest(username) + rq, err := c.initNSRequest(uid, gid) if err != nil { return nil, err } @@ -487,22 +374,22 @@ func (c *Client) getACLForPath(ctx context.Context, username, path string) (*acl resp, err := c.cl.Exec(context.Background(), rq) if err != nil { - log.Error().Err(err).Str("username", username).Str("path", path).Str("err", err.Error()) + log.Error().Err(err).Str("path", path).Str("err", err.Error()) return nil, err } if resp == nil { - return nil, errtypes.InternalError(fmt.Sprintf("nil response for username: '%s' path: '%s'", username, path)) + return nil, errtypes.InternalError(fmt.Sprintf("nil response for uid: '%s' path: '%s'", uid, path)) } log.Debug().Str("Exec ", "'"+path+"' ").Str("resp:", fmt.Sprintf("%#v", resp)).Msg("") if resp.Acl == nil { - return nil, errtypes.InternalError(fmt.Sprintf("nil acl for username: '%s' path: '%s'", username, path)) + return nil, errtypes.InternalError(fmt.Sprintf("nil acl for uid: '%s' path: '%s'", uid, path)) } if resp.GetError() != nil { - log.Info().Str("username", username).Str("path", path).Int64("errcode", resp.GetError().Code).Str("errmsg", resp.GetError().Msg).Msg("grpc response") + log.Info().Str("uid", uid).Str("path", path).Int64("errcode", resp.GetError().Code).Str("errmsg", resp.GetError().Msg).Msg("grpc response") } aclret, err := acl.Parse(resp.Acl.Rule, acl.ShortTextForm) @@ -513,11 +400,11 @@ func (c *Client) getACLForPath(ctx context.Context, username, path string) (*acl } // GetFileInfoByInode returns the FileInfo by the given inode -func (c *Client) GetFileInfoByInode(ctx context.Context, username string, inode uint64) (*FileInfo, error) { +func (c *Client) GetFileInfoByInode(ctx context.Context, uid, gid string, inode uint64) (*eosclient.FileInfo, error) { log := appctx.GetLogger(ctx) // Initialize the common fields of the MDReq - mdrq, err := c.initMDRequest(username) + mdrq, err := c.initMDRequest(uid, gid) if err != nil { return nil, err } @@ -546,15 +433,28 @@ func (c *Client) GetFileInfoByInode(ctx context.Context, username string, inode log.Info().Uint64("inode", inode).Str("rsp:", fmt.Sprintf("%#v", rsp)).Msg("grpc response") - return c.grpcMDResponseToFileInfo(rsp, "") + info, err := c.grpcMDResponseToFileInfo(rsp, "") + if err != nil { + return nil, err + } + + if c.opt.VersionInvariant && isVersionFolder(info.File) { + info, err = c.getFileInfoFromVersion(ctx, uid, gid, info.File) + if err != nil { + return nil, err + } + info.Inode = inode + } + + return info, nil } // SetAttr sets an extended attributes on a path. -func (c *Client) SetAttr(ctx context.Context, username string, attr *Attribute, recursive bool, path string) error { +func (c *Client) SetAttr(ctx context.Context, uid, gid string, attr *eosclient.Attribute, recursive bool, path string) error { log := appctx.GetLogger(ctx) // Initialize the common fields of the NSReq - rq, err := c.initNSRequest(username) + rq, err := c.initNSRequest(uid, gid) if err != nil { return err } @@ -573,26 +473,26 @@ func (c *Client) SetAttr(ctx context.Context, username string, attr *Attribute, // Now send the req and see what happens resp, err := c.cl.Exec(ctx, rq) if err != nil { - log.Warn().Err(err).Str("username", username).Str("path", path).Str("err", err.Error()) + log.Warn().Err(err).Str("path", path).Str("err", err.Error()) return err } if resp == nil { - return errtypes.InternalError(fmt.Sprintf("nil response for username: '%s' path: '%s'", username, path)) + return errtypes.InternalError(fmt.Sprintf("nil response for uid: '%s' gid: '%s' path: '%s'", uid, gid, path)) } - log.Info().Str("username", username).Str("path", path).Int64("errcode", resp.GetError().Code).Str("errmsg", resp.GetError().Msg).Msg("grpc response") + log.Info().Str("path", path).Int64("errcode", resp.GetError().Code).Str("errmsg", resp.GetError().Msg).Msg("grpc response") return err } // UnsetAttr unsets an extended attribute on a path. -func (c *Client) UnsetAttr(ctx context.Context, username string, attr *Attribute, path string) error { +func (c *Client) UnsetAttr(ctx context.Context, uid, gid string, attr *eosclient.Attribute, path string) error { log := appctx.GetLogger(ctx) // Initialize the common fields of the NSReq - rq, err := c.initNSRequest(username) + rq, err := c.initNSRequest(uid, gid) if err != nil { return err } @@ -610,26 +510,26 @@ func (c *Client) UnsetAttr(ctx context.Context, username string, attr *Attribute // Now send the req and see what happens resp, err := c.cl.Exec(ctx, rq) if err != nil { - log.Error().Err(err).Str("username", username).Str("path", path).Str("err", err.Error()) + log.Error().Err(err).Str("path", path).Str("err", err.Error()) return err } if resp == nil { - return errtypes.InternalError(fmt.Sprintf("nil response for username: '%s' path: '%s'", username, path)) + return errtypes.InternalError(fmt.Sprintf("nil response for uid: '%s' gid: '%s' path: '%s'", uid, gid, path)) } - log.Info().Str("username", username).Str("path", path).Int64("errcode", resp.GetError().Code).Str("errmsg", resp.GetError().Msg).Msg("grpc response") + log.Info().Str("path", path).Int64("errcode", resp.GetError().Code).Str("errmsg", resp.GetError().Msg).Msg("grpc response") return err } // GetFileInfoByPath returns the FilInfo at the given path -func (c *Client) GetFileInfoByPath(ctx context.Context, username, path string) (*FileInfo, error) { +func (c *Client) GetFileInfoByPath(ctx context.Context, uid, gid, path string) (*eosclient.FileInfo, error) { log := appctx.GetLogger(ctx) // Initialize the common fields of the MDReq - mdrq, err := c.initMDRequest(username) + mdrq, err := c.initMDRequest(uid, gid) if err != nil { return nil, err } @@ -641,13 +541,13 @@ func (c *Client) GetFileInfoByPath(ctx context.Context, username, path string) ( // Now send the req and see what happens resp, err := c.cl.MD(ctx, mdrq) if err != nil { - log.Error().Err(err).Str("username", username).Str("path", path).Str("err", err.Error()) + log.Error().Err(err).Str("path", path).Str("err", err.Error()) return nil, err } rsp, err := resp.Recv() if err != nil { - log.Error().Err(err).Str("FIXME username", username).Str("path", path).Str("err", err.Error()) + log.Error().Err(err).Str("path", path).Str("err", err.Error()) // FIXME: this is very very bad and poisonous for the project!!!!!!! // Apparently here we have to assume that an error in Recv() means "file not found" @@ -661,23 +561,39 @@ func (c *Client) GetFileInfoByPath(ctx context.Context, username, path string) ( return nil, errtypes.NotFound(fmt.Sprintf("%s:%s", "acltype", path)) } - log.Info().Str("username", username).Str("path", path).Str("rsp:", fmt.Sprintf("%#v", rsp)).Msg("grpc response") + log.Info().Str("path", path).Str("rsp:", fmt.Sprintf("%#v", rsp)).Msg("grpc response") + + info, err := c.grpcMDResponseToFileInfo(rsp, filepath.Dir(path)) + if err != nil { + return nil, err + } - return c.grpcMDResponseToFileInfo(rsp, filepath.Dir(path)) + if c.opt.VersionInvariant && !isVersionFolder(path) && !info.IsDir { + inode, err := c.getVersionFolderInode(ctx, uid, gid, path) + if err != nil { + return nil, err + } + info.Inode = inode + } + return info, nil +} +// GetFileInfoByFXID returns the FileInfo by the given file id in hexadecimal +func (c *Client) GetFileInfoByFXID(ctx context.Context, uid, gid string, fxid string) (*eosclient.FileInfo, error) { + return nil, errtypes.NotSupported("eosgrpc: GetFileInfoByFXID not implemented") } // GetQuota gets the quota of a user on the quota node defined by path -func (c *Client) GetQuota(ctx context.Context, username, path string) (int, int, error) { - return 0, 0, errtypes.NotSupported(fmt.Sprintf("%s:%s", "acltype", path)) +func (c *Client) GetQuota(ctx context.Context, username, rootUID, rootGID, path string) (*eosclient.QuotaInfo, error) { + return nil, errtypes.NotSupported("eosgrpc: GetQuota not implemented") } // Touch creates a 0-size,0-replica file in the EOS namespace. -func (c *Client) Touch(ctx context.Context, username, path string) error { +func (c *Client) Touch(ctx context.Context, uid, gid, path string) error { log := appctx.GetLogger(ctx) // Initialize the common fields of the NSReq - rq, err := c.initNSRequest(username) + rq, err := c.initNSRequest(uid, gid) if err != nil { return err } @@ -692,39 +608,33 @@ func (c *Client) Touch(ctx context.Context, username, path string) error { // Now send the req and see what happens resp, err := c.cl.Exec(ctx, rq) if err != nil { - log.Warn().Err(err).Str("username", username).Str("path", path).Str("err", err.Error()) + log.Warn().Err(err).Str("path", path).Str("err", err.Error()) return err } if resp == nil { - return errtypes.InternalError(fmt.Sprintf("nil response for username: '%s' path: '%s'", username, path)) + return errtypes.InternalError(fmt.Sprintf("nil response for uid: '%s' path: '%s'", uid, path)) } - log.Info().Str("username", username).Str("path", path).Str("resp:", fmt.Sprintf("%#v", resp)).Msg("grpc response") + log.Info().Str("path", path).Str("resp:", fmt.Sprintf("%#v", resp)).Msg("grpc response") return err } // Chown given path -func (c *Client) Chown(ctx context.Context, username, chownUser, path string) error { +func (c *Client) Chown(ctx context.Context, uid, gid, chownUID, chownGID, path string) error { log := appctx.GetLogger(ctx) // Initialize the common fields of the NSReq - rq, err := c.initNSRequest(username) + rq, err := c.initNSRequest(uid, gid) if err != nil { return err } msg := new(erpc.NSRequest_ChownRequest) msg.Owner = new(erpc.RoleId) - - chownunixUser, err := c.getUnixUser(chownUser) - if err != nil { - return err - } - - msg.Owner.Uid, err = strconv.ParseUint(chownunixUser.Uid, 10, 64) + msg.Owner.Uid, err = strconv.ParseUint(chownUID, 10, 64) if err != nil { return err } @@ -737,26 +647,26 @@ func (c *Client) Chown(ctx context.Context, username, chownUser, path string) er // Now send the req and see what happens resp, err := c.cl.Exec(ctx, rq) if err != nil { - log.Error().Err(err).Str("username", username).Str("chownuser", chownUser).Str("path", path).Str("err", err.Error()) + log.Error().Err(err).Str("path", path).Str("err", err.Error()) return err } if resp == nil { - return errtypes.InternalError(fmt.Sprintf("nil response for username: '%s' chownuser: '%s' path: '%s'", username, chownUser, path)) + return errtypes.InternalError(fmt.Sprintf("nil response for uid: '%s' chownuid: '%s' path: '%s'", uid, chownUID, path)) } - log.Info().Str("username", username).Str("path", path).Str("resp:", fmt.Sprintf("%#v", resp)).Msg("grpc response") + log.Info().Str("path", path).Str("resp:", fmt.Sprintf("%#v", resp)).Msg("grpc response") return err } // Chmod given path -func (c *Client) Chmod(ctx context.Context, username, mode, path string) error { +func (c *Client) Chmod(ctx context.Context, uid, gid, mode, path string) error { log := appctx.GetLogger(ctx) // Initialize the common fields of the NSReq - rq, err := c.initNSRequest(username) + rq, err := c.initNSRequest(uid, gid) if err != nil { return err } @@ -777,26 +687,26 @@ func (c *Client) Chmod(ctx context.Context, username, mode, path string) error { // Now send the req and see what happens resp, err := c.cl.Exec(ctx, rq) if err != nil { - log.Warn().Err(err).Str("username", username).Str("mode", mode).Str("path", path).Str("err", err.Error()) + log.Warn().Err(err).Str("mode", mode).Str("path", path).Str("err", err.Error()) return err } if resp == nil { - return errtypes.InternalError(fmt.Sprintf("nil response for username: '%s' mode: '%s' path: '%s'", username, mode, path)) + return errtypes.InternalError(fmt.Sprintf("nil response for uid: '%s' mode: '%s' path: '%s'", uid, mode, path)) } - log.Info().Str("username", username).Str("path", path).Str("resp:", fmt.Sprintf("%#v", resp)).Msg("grpc response") + log.Info().Str("path", path).Str("resp:", fmt.Sprintf("%#v", resp)).Msg("grpc response") return err } // CreateDir creates a directory at the given path -func (c *Client) CreateDir(ctx context.Context, username, path string) error { +func (c *Client) CreateDir(ctx context.Context, uid, gid, path string) error { log := appctx.GetLogger(ctx) // Initialize the common fields of the NSReq - rq, err := c.initNSRequest(username) + rq, err := c.initNSRequest(uid, gid) if err != nil { return err } @@ -818,25 +728,25 @@ func (c *Client) CreateDir(ctx context.Context, username, path string) error { // Now send the req and see what happens resp, err := c.cl.Exec(ctx, rq) if err != nil { - log.Warn().Err(err).Str("username", username).Str("path", path).Str("err", err.Error()) + log.Warn().Err(err).Str("path", path).Str("err", err.Error()) return err } if resp == nil { - return errtypes.InternalError(fmt.Sprintf("nil response for username: '%s' path: '%s'", username, path)) + return errtypes.InternalError(fmt.Sprintf("nil response for uid: '%s' path: '%s'", uid, path)) } - log.Info().Str("username", username).Str("path", path).Str("resp:", fmt.Sprintf("%#v", resp)).Msg("grpc response") + log.Info().Str("path", path).Str("resp:", fmt.Sprintf("%#v", resp)).Msg("grpc response") return err } -func (c *Client) rm(ctx context.Context, username, path string) error { +func (c *Client) rm(ctx context.Context, uid, gid, path string) error { log := appctx.GetLogger(ctx) // Initialize the common fields of the NSReq - rq, err := c.initNSRequest(username) + rq, err := c.initNSRequest(uid, gid) if err != nil { return err } @@ -851,25 +761,25 @@ func (c *Client) rm(ctx context.Context, username, path string) error { // Now send the req and see what happens resp, err := c.cl.Exec(ctx, rq) if err != nil { - log.Warn().Err(err).Str("username", username).Str("path", path).Str("err", err.Error()) + log.Warn().Err(err).Str("path", path).Str("err", err.Error()) return err } if resp == nil { - return errtypes.InternalError(fmt.Sprintf("nil response for username: '%s' path: '%s'", username, path)) + return errtypes.InternalError(fmt.Sprintf("nil response for uid: '%s' path: '%s'", uid, path)) } - log.Info().Str("username", username).Str("path", path).Str("resp:", fmt.Sprintf("%#v", resp)).Msg("grpc response") + log.Info().Str("path", path).Str("resp:", fmt.Sprintf("%#v", resp)).Msg("grpc response") return err } -func (c *Client) rmdir(ctx context.Context, username, path string) error { +func (c *Client) rmdir(ctx context.Context, uid, gid, path string) error { log := appctx.GetLogger(ctx) // Initialize the common fields of the NSReq - rq, err := c.initNSRequest(username) + rq, err := c.initNSRequest(uid, gid) if err != nil { return err } @@ -884,43 +794,43 @@ func (c *Client) rmdir(ctx context.Context, username, path string) error { // Now send the req and see what happens resp, err := c.cl.Exec(ctx, rq) if err != nil { - log.Warn().Err(err).Str("username", username).Str("path", path).Str("err", err.Error()) + log.Warn().Err(err).Str("path", path).Str("err", err.Error()) return err } if resp == nil { - return errtypes.InternalError(fmt.Sprintf("nil response for username: '%s' path: '%s'", username, path)) + return errtypes.InternalError(fmt.Sprintf("nil response for uid: '%s' path: '%s'", uid, path)) } - log.Info().Str("username", username).Str("path", path).Str("resp:", fmt.Sprintf("%#v", resp)).Msg("grpc response") + log.Info().Str("path", path).Str("resp:", fmt.Sprintf("%#v", resp)).Msg("grpc response") return err } // Remove removes the resource at the given path -func (c *Client) Remove(ctx context.Context, username, path string) error { +func (c *Client) Remove(ctx context.Context, uid, gid, path string) error { log := appctx.GetLogger(ctx) - nfo, err := c.GetFileInfoByPath(ctx, username, path) + nfo, err := c.GetFileInfoByPath(ctx, uid, gid, path) if err != nil { - log.Warn().Err(err).Str("username", username).Str("path", path).Str("err", err.Error()) + log.Warn().Err(err).Str("path", path).Str("err", err.Error()) return err } if nfo.IsDir { - return c.rmdir(ctx, username, path) + return c.rmdir(ctx, uid, gid, path) } - return c.rm(ctx, username, path) + return c.rm(ctx, uid, gid, path) } // Rename renames the resource referenced by oldPath to newPath -func (c *Client) Rename(ctx context.Context, username, oldPath, newPath string) error { - return errtypes.NotFound(fmt.Sprintf("%s:%s", "acltype", newPath)) +func (c *Client) Rename(ctx context.Context, uid, gid, oldPath, newPath string) error { + return errtypes.NotSupported("eosgrpc: Rename not implemented") } // List the contents of the directory given by path -func (c *Client) List(ctx context.Context, username, dpath string) ([]*FileInfo, error) { +func (c *Client) List(ctx context.Context, uid, gid, dpath string) ([]*eosclient.FileInfo, error) { log := appctx.GetLogger(ctx) // Stuff filename, uid, gid into the FindRequest type @@ -930,34 +840,30 @@ func (c *Client) List(ctx context.Context, username, dpath string) ([]*FileInfo, fdrq.Id = new(erpc.MDId) fdrq.Id.Path = []byte(dpath) - unixUser, err := c.getUnixUser(username) - if err != nil { - return nil, err - } fdrq.Role = new(erpc.RoleId) - uid, err := strconv.ParseUint(unixUser.Uid, 10, 64) + uidInt, err := strconv.ParseUint(uid, 10, 64) if err != nil { return nil, err } - fdrq.Role.Uid = uid - gid, err := strconv.ParseUint(unixUser.Gid, 10, 64) + gidInt, err := strconv.ParseUint(gid, 10, 64) if err != nil { return nil, err } - fdrq.Role.Gid = gid + fdrq.Role.Uid = uidInt + fdrq.Role.Gid = gidInt fdrq.Authkey = c.opt.Authkey // Now send the req and see what happens resp, err := c.cl.Find(context.Background(), fdrq) if err != nil { - log.Error().Err(err).Str("username", username).Str("path", dpath).Str("err", err.Error()) + log.Error().Err(err).Str("path", dpath).Str("err", err.Error()) return nil, err } - var mylst []*FileInfo + var mylst []*eosclient.FileInfo i := 0 for { rsp, err := resp.Recv() @@ -966,21 +872,21 @@ func (c *Client) List(ctx context.Context, username, dpath string) ([]*FileInfo, return mylst, nil } - log.Warn().Err(err).Str("username", username).Str("path", dpath).Str("err", err.Error()) + log.Warn().Err(err).Str("path", dpath).Str("err", err.Error()) return nil, err } if rsp == nil { - log.Warn().Err(err).Str("username", username).Str("path", dpath).Str("err", "rsp is nil") + log.Warn().Err(err).Str("path", dpath).Str("err", "rsp is nil") return nil, errtypes.NotFound(dpath) } - log.Debug().Str("username", username).Str("path", dpath).Str("item resp:", fmt.Sprintf("%#v", rsp)).Msg("grpc response") + log.Debug().Str("path", dpath).Str("item resp:", fmt.Sprintf("%#v", rsp)).Msg("grpc response") myitem, err := c.grpcMDResponseToFileInfo(rsp, dpath) if err != nil { - log.Warn().Err(err).Str("username", username).Str("path", dpath).Str("could not convert item:", fmt.Sprintf("%#v", rsp)).Str("err:", err.Error()).Msg("") + log.Warn().Err(err).Str("path", dpath).Str("could not convert item:", fmt.Sprintf("%#v", rsp)).Str("err:", err.Error()).Msg("") return nil, err } @@ -994,30 +900,21 @@ func (c *Client) List(ctx context.Context, username, dpath string) ([]*FileInfo, } // Read reads a file from the mgm -func (c *Client) Read(ctx context.Context, username, path string) (io.ReadCloser, error) { - unixUser, err := c.getUnixUser(username) - if err != nil { - return nil, err - } +func (c *Client) Read(ctx context.Context, uid, gid, path string) (io.ReadCloser, error) { rand := "eosread-" + uuid.Must(uuid.NewV4()).String() localTarget := fmt.Sprintf("%s/%s", c.opt.CacheDirectory, rand) defer os.RemoveAll(localTarget) xrdPath := fmt.Sprintf("%s//%s", c.opt.URL, path) - cmd := exec.CommandContext(ctx, c.opt.XrdcopyBinary, "--nopbar", "--silent", "-f", xrdPath, localTarget, fmt.Sprintf("-OSeos.ruid=%s&eos.rgid=%s", unixUser.Uid, unixUser.Gid)) - _, _, err = c.execute(ctx, cmd) - if err != nil { + cmd := exec.CommandContext(ctx, c.opt.XrdcopyBinary, "--nopbar", "--silent", "-f", xrdPath, localTarget, fmt.Sprintf("-OSeos.ruid=%s&eos.rgid=%s", uid, gid)) + if _, _, err := c.execute(ctx, cmd); err != nil { return nil, err } return os.Open(localTarget) } // Write writes a file to the mgm -func (c *Client) Write(ctx context.Context, username, path string, stream io.ReadCloser) error { - unixUser, err := c.getUnixUser(username) - if err != nil { - return err - } +func (c *Client) Write(ctx context.Context, uid, gid, path string, stream io.ReadCloser) error { fd, err := ioutil.TempFile(c.opt.CacheDirectory, "eoswrite-") if err != nil { return err @@ -1030,33 +927,25 @@ func (c *Client) Write(ctx context.Context, username, path string, stream io.Rea if err != nil { return err } - xrdPath := fmt.Sprintf("%s//%s", c.opt.URL, path) - cmd := exec.CommandContext(ctx, c.opt.XrdcopyBinary, "--nopbar", "--silent", "-f", fd.Name(), xrdPath, fmt.Sprintf("-ODeos.ruid=%s&eos.rgid=%s", unixUser.Uid, unixUser.Gid)) - _, _, err = c.execute(ctx, cmd) - return err + + return c.WriteFile(ctx, uid, gid, path, fd.Name()) } // WriteFile writes an existing file to the mgm -func (c *Client) WriteFile(ctx context.Context, username, path, source string) error { - - unixUser, err := c.getUnixUser(username) - if err != nil { - return err - } - +func (c *Client) WriteFile(ctx context.Context, uid, gid, path, source string) error { xrdPath := fmt.Sprintf("%s//%s", c.opt.URL, path) - cmd := exec.CommandContext(ctx, c.opt.XrdcopyBinary, "--nopbar", "--silent", "-f", source, xrdPath, fmt.Sprintf("-ODeos.ruid=%s&eos.rgid=%s", unixUser.Uid, unixUser.Gid)) - _, _, err = c.execute(ctx, cmd) + cmd := exec.CommandContext(ctx, c.opt.XrdcopyBinary, "--nopbar", "--silent", "-f", source, xrdPath, fmt.Sprintf("-ODeos.ruid=%s&eos.rgid=%s", uid, gid)) + _, _, err := c.execute(ctx, cmd) return err } // ListDeletedEntries returns a list of the deleted entries. -func (c *Client) ListDeletedEntries(ctx context.Context, username string) ([]*DeletedEntry, error) { +func (c *Client) ListDeletedEntries(ctx context.Context, uid, gid string) ([]*eosclient.DeletedEntry, error) { log := appctx.GetLogger(ctx) // Initialize the common fields of the NSReq - rq, err := c.initNSRequest(username) + rq, err := c.initNSRequest(uid, gid) if err != nil { return nil, err } @@ -1069,29 +958,29 @@ func (c *Client) ListDeletedEntries(ctx context.Context, username string) ([]*De // Now send the req and see what happens resp, err := c.cl.Exec(context.Background(), rq) if err != nil { - log.Warn().Err(err).Str("username", username).Str("err", err.Error()) + log.Warn().Err(err).Str("err", err.Error()) return nil, err } if resp == nil { - return nil, errtypes.InternalError(fmt.Sprintf("nil response for username: '%s'", username)) + return nil, errtypes.InternalError(fmt.Sprintf("nil response for uid: '%s'", uid)) } - log.Info().Str("username", username).Int64("errcode", resp.GetError().Code).Str("errmsg", resp.GetError().Msg).Msg("grpc response") + log.Info().Int64("errcode", resp.GetError().Code).Str("errmsg", resp.GetError().Msg).Msg("grpc response") // TODO(labkode): add protection if slave is configured and alive to count how many files are in the trashbin before // triggering the recycle ls call that could break the instance because of unavailable memory. // FF: I agree with labkode, if we think we may have memory problems then the semantics of the grpc call`and // the semantics if this func will have to change. For now this is not foreseen - ret := make([]*DeletedEntry, 0) + ret := make([]*eosclient.DeletedEntry, 0) for _, f := range resp.Recycle.Recycles { if f == nil { - log.Info().Str("username", username).Msg("nil item in response") + log.Info().Msg("nil item in response") continue } - entry := &DeletedEntry{ + entry := &eosclient.DeletedEntry{ RestorePath: string(f.Id.Path), RestoreKey: f.Key, Size: f.Size, @@ -1106,11 +995,11 @@ func (c *Client) ListDeletedEntries(ctx context.Context, username string) ([]*De } // RestoreDeletedEntry restores a deleted entry. -func (c *Client) RestoreDeletedEntry(ctx context.Context, username, key string) error { +func (c *Client) RestoreDeletedEntry(ctx context.Context, uid, gid, key string) error { log := appctx.GetLogger(ctx) // Initialize the common fields of the NSReq - rq, err := c.initNSRequest(username) + rq, err := c.initNSRequest(uid, gid) if err != nil { return err } @@ -1125,25 +1014,25 @@ func (c *Client) RestoreDeletedEntry(ctx context.Context, username, key string) // Now send the req and see what happens resp, err := c.cl.Exec(context.Background(), rq) if err != nil { - log.Warn().Err(err).Str("username", username).Str("key", key).Str("err", err.Error()) + log.Warn().Err(err).Str("key", key).Str("err", err.Error()) return err } if resp == nil { - return errtypes.InternalError(fmt.Sprintf("nil response for username: '%s' key: '%s'", username, key)) + return errtypes.InternalError(fmt.Sprintf("nil response for uid: '%s' key: '%s'", uid, key)) } - log.Info().Str("username", username).Str("key", key).Int64("errcode", resp.GetError().Code).Str("errmsg", resp.GetError().Msg).Msg("grpc response") + log.Info().Str("key", key).Int64("errcode", resp.GetError().Code).Str("errmsg", resp.GetError().Msg).Msg("grpc response") return err } // PurgeDeletedEntries purges all entries from the recycle bin. -func (c *Client) PurgeDeletedEntries(ctx context.Context, username string) error { +func (c *Client) PurgeDeletedEntries(ctx context.Context, uid, gid string) error { log := appctx.GetLogger(ctx) // Initialize the common fields of the NSReq - rq, err := c.initNSRequest(username) + rq, err := c.initNSRequest(uid, gid) if err != nil { return err } @@ -1156,40 +1045,35 @@ func (c *Client) PurgeDeletedEntries(ctx context.Context, username string) error // Now send the req and see what happens resp, err := c.cl.Exec(context.Background(), rq) if err != nil { - log.Warn().Err(err).Str("username", username).Str("err", err.Error()) + log.Warn().Err(err).Str("err", err.Error()) return err } if resp == nil { - return errtypes.InternalError(fmt.Sprintf("nil response for username: '%s' ", username)) + return errtypes.InternalError(fmt.Sprintf("nil response for uid: '%s' ", uid)) } - log.Info().Str("username", username).Int64("errcode", resp.GetError().Code).Str("errmsg", resp.GetError().Msg).Msg("grpc response") + log.Info().Int64("errcode", resp.GetError().Code).Str("errmsg", resp.GetError().Msg).Msg("grpc response") return err } // ListVersions list all the versions for a given file. -func (c *Client) ListVersions(ctx context.Context, username, p string) ([]*FileInfo, error) { - basename := path.Base(p) - versionFolder := path.Join(path.Dir(p), versionPrefix+basename) - finfos, err := c.List(ctx, username, versionFolder) +func (c *Client) ListVersions(ctx context.Context, uid, gid, p string) ([]*eosclient.FileInfo, error) { + versionFolder := getVersionFolder(p) + finfos, err := c.List(ctx, uid, gid, versionFolder) if err != nil { // we send back an empty list - return []*FileInfo{}, nil + return []*eosclient.FileInfo{}, nil } return finfos, nil } // RollbackToVersion rollbacks a file to a previous version. -func (c *Client) RollbackToVersion(ctx context.Context, username, path, version string) error { +func (c *Client) RollbackToVersion(ctx context.Context, uid, gid, path, version string) error { // TODO(ffurano): /* - unixUser, err := c.getUnixUser(username) - if err != nil { - return err - } - cmd := exec.CommandContext(ctx, c.opt.EosBinary, "-r", unixUser.Uid, unixUser.Gid, "file", "versions", path, version) + cmd := exec.CommandContext(ctx, c.opt.EosBinary, "-r", uid, gid, "file", "versions", path, version) _, _, err = c.executeEOS(ctx, cmd) return err */ @@ -1197,17 +1081,52 @@ func (c *Client) RollbackToVersion(ctx context.Context, username, path, version } // ReadVersion reads the version for the given file. -func (c *Client) ReadVersion(ctx context.Context, username, p, version string) (io.ReadCloser, error) { - basename := path.Base(p) - versionFile := path.Join(path.Dir(p), versionPrefix+basename, version) - return c.Read(ctx, username, versionFile) +func (c *Client) ReadVersion(ctx context.Context, uid, gid, p, version string) (io.ReadCloser, error) { + versionFile := path.Join(getVersionFolder(p), version) + return c.Read(ctx, uid, gid, versionFile) +} + +func (c *Client) getVersionFolderInode(ctx context.Context, uid, gid, p string) (uint64, error) { + versionFolder := getVersionFolder(p) + md, err := c.GetFileInfoByPath(ctx, uid, gid, versionFolder) + if err != nil { + if err = c.CreateDir(ctx, uid, gid, versionFolder); err != nil { + return 0, err + } + md, err = c.GetFileInfoByPath(ctx, uid, gid, versionFolder) + if err != nil { + return 0, err + } + } + return md.Inode, nil } -func (c *Client) grpcMDResponseToFileInfo(st *erpc.MDResponse, namepfx string) (*FileInfo, error) { +func (c *Client) getFileInfoFromVersion(ctx context.Context, uid, gid, p string) (*eosclient.FileInfo, error) { + file := getFileFromVersionFolder(p) + md, err := c.GetFileInfoByPath(ctx, uid, gid, file) + if err != nil { + return nil, err + } + return md, nil +} + +func isVersionFolder(p string) bool { + return strings.HasPrefix(path.Base(p), versionPrefix) +} + +func getVersionFolder(p string) string { + return path.Join(path.Dir(p), versionPrefix+path.Base(p)) +} + +func getFileFromVersionFolder(p string) string { + return path.Join(path.Dir(p), strings.TrimPrefix(path.Base(p), versionPrefix)) +} + +func (c *Client) grpcMDResponseToFileInfo(st *erpc.MDResponse, namepfx string) (*eosclient.FileInfo, error) { if st.Cmd == nil && st.Fmd == nil { return nil, errors.Wrap(errtypes.NotSupported(""), "Invalid response (st.Cmd and st.Fmd are nil)") } - fi := new(FileInfo) + fi := new(eosclient.FileInfo) if st.Type != 0 { fi.IsDir = true @@ -1263,34 +1182,6 @@ func (c *Client) grpcMDResponseToFileInfo(st *erpc.MDResponse, namepfx string) ( return fi, nil } -// FileInfo represents the metadata information returned by querying the EOS namespace. -type FileInfo struct { - IsDir bool - MTimeNanos uint32 - Inode uint64 `json:"inode"` - FID uint64 `json:"fid"` - UID uint64 `json:"uid"` - GID uint64 `json:"gid"` - TreeSize uint64 `json:"tree_size"` - MTimeSec uint64 `json:"mtime_sec"` - Size uint64 `json:"size"` - TreeCount uint64 `json:"tree_count"` - File string `json:"eos_file"` - ETag string `json:"etag"` - Instance string `json:"instance"` - SysACL string `json:"sys_acl"` - Attrs map[string]string `json:"attrs"` -} - -// DeletedEntry represents an entry from the trashbin. -type DeletedEntry struct { - RestorePath string - RestoreKey string - Size uint64 - DeletionMTime uint64 - IsDir bool -} - // exec executes the command and returns the stdout, stderr and return code func (c *Client) execute(ctx context.Context, cmd *exec.Cmd) (string, string, error) { log := appctx.GetLogger(ctx) diff --git a/pkg/storage/fs/eos/eos.go b/pkg/storage/fs/eos/eos.go index cfe18e6cd0e..be20be98925 100644 --- a/pkg/storage/fs/eos/eos.go +++ b/pkg/storage/fs/eos/eos.go @@ -19,9 +19,6 @@ package eos import ( - "bytes" - "encoding/gob" - "github.com/cs3org/reva/pkg/storage" "github.com/cs3org/reva/pkg/storage/fs/registry" "github.com/cs3org/reva/pkg/storage/utils/eosfs" @@ -33,73 +30,8 @@ func init() { registry.Register("eos", New) } -type config struct { - // Namespace for metadata operations - Namespace string `mapstructure:"namespace" docs:"/"` - - // ShadowNamespace for storing shadow data - ShadowNamespace string `mapstructure:"shadow_namespace" docs:"/.shadow"` - - // UploadsNamespace for storing upload data - UploadsNamespace string `mapstructure:"uploads_namespace" docs:"/.uploads"` - - // ShareFolder defines the name of the folder in the - // shadowed namespace. Ex: /eos/user/.shadow/h/hugo/MyShares - ShareFolder string `mapstructure:"share_folder" docs:"/MyShares"` - - // Location of the eos binary. - // Default is /usr/bin/eos. - EosBinary string `mapstructure:"eos_binary" docs:"/usr/bin/eos"` - - // Location of the xrdcopy binary. - // Default is /usr/bin/xrdcopy. - XrdcopyBinary string `mapstructure:"xrdcopy_binary" docs:"/usr/bin/xrdcopy"` - - // URL of the Master EOS MGM. - // Default is root://eos-example.org - MasterURL string `mapstructure:"master_url" docs:"root://eos-example.org"` - - // URL of the Slave EOS MGM. - // Default is root://eos-example.org - SlaveURL string `mapstructure:"slave_url" docs:"root://eos-example.org"` - - // Location on the local fs where to store reads. - // Defaults to os.TempDir() - CacheDirectory string `mapstructure:"cache_directory" docs:"/var/tmp/"` - - // SecProtocol specifies the xrootd security protocol to use between the server and EOS. - SecProtocol string `mapstructure:"sec_protocol" docs:"-"` - - // Keytab specifies the location of the keytab to use to authenticate to EOS. - Keytab string `mapstructure:"keytab" docs:"-"` - - // SingleUsername is the username to use when SingleUserMode is enabled - SingleUsername string `mapstructure:"single_username" docs:"-"` - - // Enables logging of the commands executed - // Defaults to false - EnableLogging bool `mapstructure:"enable_logging" docs:"false"` - - // ShowHiddenSysFiles shows internal EOS files like - // .sys.v# and .sys.a# files. - ShowHiddenSysFiles bool `mapstructure:"show_hidden_sys_files" docs:"false"` - - // ForceSingleUserMode will force connections to EOS to use SingleUsername - ForceSingleUserMode bool `mapstructure:"force_single_user_mode" docs:"false"` - - // UseKeyTabAuth changes will authenticate requests by using an EOS keytab. - UseKeytab bool `mapstructure:"use_keytab" docs:"false"` - - // Whether to maintain the same inode across various versions of a file. - // Requires extra metadata operations if set to true - VersionInvariant bool `mapstructure:"version_invariant" docs:"true"` - - // GatewaySvc stores the endpoint at which the GRPC gateway is exposed. - GatewaySvc string `mapstructure:"gatewaysvc" docs:"0.0.0.0:19000"` -} - -func parseConfig(m map[string]interface{}) (*config, error) { - c := &config{} +func parseConfig(m map[string]interface{}) (*eosfs.Config, error) { + c := &eosfs.Config{} if err := mapstructure.Decode(m, c); err != nil { err = errors.Wrap(err, "error decoding conf") return nil, err @@ -120,16 +52,5 @@ func New(m map[string]interface{}) (storage.FS, error) { return nil, err } - var buf bytes.Buffer - err = gob.NewEncoder(&buf).Encode(&c) - if err != nil { - return nil, err - } - var conf eosfs.Config - err = gob.NewDecoder(&buf).Decode(&conf) - if err != nil { - return nil, err - } - - return eosfs.NewEOSFS(&conf) + return eosfs.NewEOSFS(c) } diff --git a/pkg/storage/fs/eosgrpc/eosgrpc.go b/pkg/storage/fs/eosgrpc/eosgrpc.go index 279273d3b43..0b8b0302887 100644 --- a/pkg/storage/fs/eosgrpc/eosgrpc.go +++ b/pkg/storage/fs/eosgrpc/eosgrpc.go @@ -19,1565 +19,39 @@ package eosgrpc import ( - "context" - "encoding/json" - "fmt" - "io" - "net/url" - "os" - gouser "os/user" - "path" - "regexp" - "strconv" - "strings" - - "github.com/cs3org/reva/pkg/appctx" - "github.com/cs3org/reva/pkg/eosclientgrpc" - "github.com/cs3org/reva/pkg/mime" "github.com/cs3org/reva/pkg/storage" "github.com/cs3org/reva/pkg/storage/fs/registry" - "github.com/cs3org/reva/pkg/storage/utils/acl" - "github.com/cs3org/reva/pkg/storage/utils/templates" - "github.com/cs3org/reva/pkg/user" + "github.com/cs3org/reva/pkg/storage/utils/eosfs" "github.com/mitchellh/mapstructure" "github.com/pkg/errors" - - userpb "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" - "github.com/cs3org/reva/pkg/errtypes" -) - -const ( - refTargetAttrKey = "reva.target" ) func init() { - registry.Register("eosgrpc", New) } -var hiddenReg = regexp.MustCompile(`\.sys\..#.`) - -type eosfs struct { - c *eosclientgrpc.Client - conf *config -} - -func parseConfig(m map[string]interface{}) (*config, error) { - c := &config{} +func parseConfig(m map[string]interface{}) (*eosfs.Config, error) { + c := &eosfs.Config{} if err := mapstructure.Decode(m, c); err != nil { + err = errors.Wrap(err, "error decoding conf") return nil, err } - return c, nil -} - -// Options are the configuration options to pass to the New function. -type config struct { - // Namespace for metadata operations - Namespace string `mapstructure:"namespace" docs:"/"` - - // ShadowNamespace for storing shadow data - ShadowNamespace string `mapstructure:"shadow_namespace" docs:"/.shadow"` - - // ShareFolder defines the name of the folder in the - // shadowed namespace. Ex: /eos/user/.shadow/h/hugo/MyShares - ShareFolder string `mapstructure:"share_folder" docs:"/MyShares"` - - // Location of the eos binary. - // Default is /usr/bin/eos. - EosBinary string `mapstructure:"eos_binary" docs:"/usr/bin/eos"` - - // Location of the xrdcopy binary. - // Default is /usr/bin/xrdcopy. - XrdcopyBinary string `mapstructure:"xrdcopy_binary" docs:"/usr/bin/xrdcopy"` - - // URL of the Master EOS MGM. - // Default is root://eos-example.org - MasterURL string `mapstructure:"master_url" docs:"root://eos-example.org"` - - // URI of the EOS MGM grpc server - // Default is empty - GrpcURI string `mapstructure:"master_grpc_uri" docs:"root://eos-grpc-example.org"` - - // URL of the Slave EOS MGM. - // Default is root://eos-example.org - SlaveURL string `mapstructure:"slave_url" docs:"root://eos-example.org"` - - // Location on the local fs where to store reads. - // Defaults to os.TempDir() - CacheDirectory string `mapstructure:"cache_directory" docs:"/var/tmp/"` - - // SecProtocol specifies the xrootd security protocol to use between the server and EOS. - SecProtocol string `mapstructure:"sec_protocol" docs:"-"` - - // Keytab specifies the location of the keytab to use to authenticate to EOS. - Keytab string `mapstructure:"keytab"` - - // SingleUsername is the username to use when SingleUserMode is enabled - SingleUsername string `mapstructure:"single_username"` - - // UserLayout wraps the internal path with user information. - // Example: if conf.Namespace is /eos/user and received path is /docs - // and the UserLayout is {{.Username}} the internal path will be: - // /eos/user//docs - UserLayout string `mapstructure:"user_layout" docs:"-"` - - // Enables logging of the commands executed - // Defaults to false - EnableLogging bool `mapstructure:"enable_logging" docs:"false"` - - // ShowHiddenSysFiles shows internal EOS files like - // .sys.v# and .sys.a# files. - ShowHiddenSysFiles bool `mapstructure:"show_hidden_sys_files" docs:"-"` - - // ForceSingleUserMode will force connections to EOS to use SingleUsername - ForceSingleUserMode bool `mapstructure:"force_single_user_mode" docs:"false"` - - // UseKeyTabAuth changes will authenticate requests by using an EOS keytab. - UseKeytab bool `mapstrucuture:"use_keytab" docs:"false"` - - // EnableHome enables the creation of home directories. - EnableHome bool `mapstructure:"enable_home" docs:"false"` - - // Authkey is the key that authorizes this client to connect to the GRPC service - // It's unclear whether this will be the final solution - Authkey string `mapstructure:"authkey" docs:"-"` -} - -func getUser(ctx context.Context) (*userpb.User, error) { - u, ok := user.ContextGetUser(ctx) - if !ok { - err := errors.Wrap(errtypes.UserRequired(""), "eos: error getting user from ctx") - return nil, err - } - return u, nil -} - -func (c *config) init() { - fmt.Printf("-- Initialising eosgrpc\n") - - c.Namespace = path.Clean(c.Namespace) - if !strings.HasPrefix(c.Namespace, "/") { - c.Namespace = "/" - } - - if c.ShadowNamespace == "" { - c.ShadowNamespace = path.Join(c.Namespace, ".shadow") - } - - if c.ShareFolder == "" { - c.ShareFolder = "/MyShares" - } - // ensure share folder always starts with slash - c.ShareFolder = path.Join("/", c.ShareFolder) - - if c.EosBinary == "" { - c.EosBinary = "/usr/bin/eos" - } - - if c.XrdcopyBinary == "" { - c.XrdcopyBinary = "/usr/bin/xrdcopy" - } - - if c.MasterURL == "" { - c.MasterURL = "root://eos-example.org" - } - - if c.SlaveURL == "" { - c.SlaveURL = c.MasterURL - } - if c.CacheDirectory == "" { - c.CacheDirectory = os.TempDir() + // default to version invariance if not configured + if _, ok := m["version_invariant"]; !ok { + c.VersionInvariant = true } - if c.UserLayout == "" { - c.UserLayout = "{{.Username}}" // TODO set better layout - } + return c, nil } // New returns a new implementation of the storage.FS interface that connects to EOS. func New(m map[string]interface{}) (storage.FS, error) { - c, err := parseConfig(m) if err != nil { return nil, err } - c.init() - - // bail out if keytab is not found. - if c.UseKeytab { - if _, err := os.Stat(c.Keytab); err != nil { - err = errors.Wrapf(err, "eos: keytab not accessible at location: %s", err) - return nil, err - } - } - - eosClientOpts := &eosclientgrpc.Options{ - XrdcopyBinary: c.XrdcopyBinary, - URL: c.MasterURL, - GrpcURI: c.GrpcURI, - CacheDirectory: c.CacheDirectory, - ForceSingleUserMode: c.ForceSingleUserMode, - SingleUsername: c.SingleUsername, - UseKeytab: c.UseKeytab, - Keytab: c.Keytab, - Authkey: c.Authkey, - SecProtocol: c.SecProtocol, - } - - eosClient := eosclientgrpc.New(eosClientOpts) - - eosfs := &eosfs{ - c: eosClient, - conf: c, - } - - return eosfs, nil -} - -// InitiateUpload returns an upload id that can be used for uploads with tus -//func (fs *eosfs) InitiateUpload(ctx context.Context, ref *provider.Reference, uploadLength int64, metadata map[string]string) (uploadID string, err error) { -// return "", errtypes.NotSupported("op not supported") -//} - -func (fs *eosfs) Shutdown(ctx context.Context) error { - // TODO(labkode): in a grpc implementation we can close connections. - return nil -} - -func (fs *eosfs) wrapShadow(ctx context.Context, fn string) (internal string) { - if fs.conf.EnableHome { - u, err := getUser(ctx) - if err != nil { - err = errors.Wrap(err, "eos: wrap: no user in ctx and home is enabled") - panic(err) - } - layout := templates.WithUser(u, fs.conf.UserLayout) - internal = path.Join(fs.conf.ShadowNamespace, layout, fn) - } else { - internal = path.Join(fs.conf.ShadowNamespace, fn) - } - return -} - -func (fs *eosfs) wrap(ctx context.Context, fn string) (internal string) { - if fs.conf.EnableHome { - layout, err := fs.GetHome(ctx) - if err != nil { - panic(err) - } - internal = path.Join(fs.conf.Namespace, layout, fn) - } else { - internal = path.Join(fs.conf.Namespace, fn) - } - log := appctx.GetLogger(ctx) - log.Debug().Msg("eos: wrap external=" + fn + " internal=" + internal) - return -} - -func (fs *eosfs) unwrap(ctx context.Context, internal string) (external string) { - log := appctx.GetLogger(ctx) - layout := fs.getLayout(ctx) - nss := []string{fs.conf.Namespace, fs.conf.ShadowNamespace} - log.Debug().Msgf("eos: unwrap: internal=%s external=%s", internal, nss) - ns := fs.getNsMatch(internal, nss) - external = fs.unwrapInternal(ctx, ns, internal, layout) - log.Debug().Msgf("eos: unwrap: internal=%s external=%s", internal, external) - return -} - -func (fs *eosfs) getLayout(ctx context.Context) (layout string) { - if fs.conf.EnableHome { - u, err := getUser(ctx) - if err != nil { - panic(err) - } - layout = templates.WithUser(u, fs.conf.UserLayout) - } - return -} - -func (fs *eosfs) getNsMatch(internal string, nss []string) string { - var match string - - for _, ns := range nss { - - if strings.HasPrefix(internal, ns) && len(ns) > len(match) { - match = ns - } - } - - if match == "" { - panic(fmt.Sprintf("eos: path is outside namespaces: path=%s namespaces=%+v", internal, nss)) - } - - return match -} - -func (fs *eosfs) unwrapInternal(ctx context.Context, ns, np, layout string) (external string) { - log := appctx.GetLogger(ctx) - trim := path.Join(ns, layout) - - if !strings.HasPrefix(np, trim) { - panic("eos: resource is outside the directory of the logged-in user: internal=" + np + " trim=" + trim + " namespace=" + ns) - } - - external = strings.TrimPrefix(np, trim) - - if external == "" { - external = "/" - } - - log.Debug().Msgf("eos: unwrapInternal: trim=%s external=%s ns=%s np=%s", trim, external, ns, np) - - return -} - -func (fs *eosfs) GetPathByID(ctx context.Context, id *provider.ResourceId) (string, error) { - u, err := getUser(ctx) - if err != nil { - return "", errors.Wrap(err, "eos: no user in ctx") - } - - // parts[0] = 868317, parts[1] = photos, ... - parts := strings.Split(id.OpaqueId, "/") - fileID, err := strconv.ParseUint(parts[0], 10, 64) - if err != nil { - return "", errors.Wrap(err, "eos: error parsing fileid string") - } - - eosFileInfo, err := fs.c.GetFileInfoByInode(ctx, u.Username, fileID) - if err != nil { - return "", errors.Wrap(err, "eos: error getting file info by inode") - } - - fi := fs.convertToResourceInfo(ctx, eosFileInfo) - return fi.Path, nil -} - -// resolve takes in a request path or request id and returns the unwrapNominalped path. -func (fs *eosfs) resolve(ctx context.Context, u *userpb.User, ref *provider.Reference) (string, error) { - if ref.GetPath() != "" { - return ref.GetPath(), nil - } - - if ref.GetId() != nil { - p, err := fs.getPath(ctx, u, ref.GetId()) - if err != nil { - return "", err - } - - return p, nil - } - - // reference is invalid - return "", fmt.Errorf("invalid reference %+v. id and path are missing", ref) -} - -func (fs *eosfs) getPath(ctx context.Context, u *userpb.User, id *provider.ResourceId) (string, error) { - fid, err := strconv.ParseUint(id.OpaqueId, 10, 64) - if err != nil { - return "", fmt.Errorf("error converting string to int for eos fileid: %s", id.OpaqueId) - } - - eosFileInfo, err := fs.c.GetFileInfoByInode(ctx, u.Username, fid) - if err != nil { - return "", errors.Wrap(err, "eos: error getting file info by inode") - } - - return fs.unwrap(ctx, eosFileInfo.File), nil -} - -func (fs *eosfs) AddGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error { - u, err := getUser(ctx) - if err != nil { - return errors.Wrap(err, "eos: no user in ctx") - } - - p, err := fs.resolve(ctx, u, ref) - if err != nil { - return errors.Wrap(err, "eos: error resolving reference") - } - - fn := fs.wrap(ctx, p) - - eosACL, err := fs.getEosACL(g) - if err != nil { - return err - } - - err = fs.c.AddACL(ctx, u.Username, fn, eosACL) - if err != nil { - return errors.Wrap(err, "eos: error adding acl") - } - - return nil -} - -func getEosACLType(gt provider.GranteeType) (string, error) { - switch gt { - case provider.GranteeType_GRANTEE_TYPE_USER: - return "u", nil - case provider.GranteeType_GRANTEE_TYPE_GROUP: - return "g", nil - default: - return "", errors.New("no eos acl for grantee type: " + gt.String()) - } -} - -// TODO(labkode): fine grained permission controls. -func getEosACLPerm(set *provider.ResourcePermissions) (string, error) { - var b strings.Builder - - if set.Stat || set.InitiateFileDownload { - b.WriteString("r") - } - if set.CreateContainer || set.InitiateFileUpload || set.Delete || set.Move { - b.WriteString("w") - } - if set.ListContainer { - b.WriteString("x") - } - - if set.Delete { - b.WriteString("+d") - } else { - b.WriteString("!d") - } - - // TODO sharing - // TODO trash - // TODO versions - return b.String(), nil -} - -func (fs *eosfs) getEosACL(g *provider.Grant) (*acl.Entry, error) { - permissions, err := getEosACLPerm(g.Permissions) - if err != nil { - return nil, err - } - t, err := getEosACLType(g.Grantee.Type) - if err != nil { - return nil, err - } - eosACL := &acl.Entry{ - Qualifier: g.Grantee.Id.OpaqueId, - Permissions: permissions, - Type: t, - } - return eosACL, nil -} - -func (fs *eosfs) SetArbitraryMetadata(ctx context.Context, ref *provider.Reference, md *provider.ArbitraryMetadata) error { - u, err := getUser(ctx) - if err != nil { - return err - } - - log := appctx.GetLogger(ctx) - log.Info().Msg("eos: set arbitrary md for ref:" + ref.String()) - - p, err := fs.resolve(ctx, u, ref) - if err != nil { - return errors.Wrap(err, "eos: error resolving reference") - } - - fn := fs.wrap(ctx, p) - - // Loop over the keyvalues of md.metadata and set them individually - for k, v := range md.Metadata { - var attr eosclientgrpc.Attribute - attr.Type = eosclientgrpc.SystemAttr - attr.Key = k - attr.Val = v - err = fs.c.SetAttr(ctx, u.Username, &attr, false, fn) - if err != nil { - return err - } - - } - return nil -} - -func (fs *eosfs) UnsetArbitraryMetadata(ctx context.Context, ref *provider.Reference, keys []string) error { - - u, err := getUser(ctx) - if err != nil { - return err - } - - log := appctx.GetLogger(ctx) - log.Info().Msg("eos: set arbitrary md for ref:" + ref.String()) - - p, err := fs.resolve(ctx, u, ref) - if err != nil { - return errors.Wrap(err, "eos: error resolving reference") - } - - fn := fs.wrap(ctx, p) - - // Loop over the keys and unset them individually - for _, k := range keys { - var attr eosclientgrpc.Attribute - attr.Key = k - - err = fs.c.UnsetAttr(ctx, u.Username, &attr, fn) - if err != nil { - return err - } - - } - return nil -} - -func (fs *eosfs) RemoveGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error { - u, err := getUser(ctx) - if err != nil { - return errors.Wrap(err, "eos: no user in ctx") - } - - eosACLType, err := getEosACLType(g.Grantee.Type) - if err != nil { - return err - } - - p, err := fs.resolve(ctx, u, ref) - if err != nil { - return errors.Wrap(err, "eos: error resolving reference") - } - - fn := fs.wrap(ctx, p) - - err = fs.c.RemoveACL(ctx, u.Username, fn, eosACLType, g.Grantee.Id.OpaqueId) - if err != nil { - return errors.Wrap(err, "eos: error removing acl") - } - return nil -} - -func (fs *eosfs) UpdateGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error { - u, err := getUser(ctx) - if err != nil { - return errors.Wrap(err, "eos: no user in ctx") - } - - eosACL, err := fs.getEosACL(g) - if err != nil { - return errors.Wrap(err, "eos: error mapping acl") - } - - p, err := fs.resolve(ctx, u, ref) - if err != nil { - return errors.Wrap(err, "eos: error resolving reference") - } - fn := fs.wrap(ctx, p) - - err = fs.c.AddACL(ctx, u.Username, fn, eosACL) - if err != nil { - return errors.Wrap(err, "eos: error updating acl") - } - return nil -} - -func (fs *eosfs) ListGrants(ctx context.Context, ref *provider.Reference) ([]*provider.Grant, error) { - u, err := getUser(ctx) - if err != nil { - return nil, err - } - - p, err := fs.resolve(ctx, u, ref) - if err != nil { - return nil, errors.Wrap(err, "eos: error resolving reference") - } - fn := fs.wrap(ctx, p) - - acls, err := fs.c.ListACLs(ctx, u.Username, fn) - if err != nil { - return nil, err - } - - grants := []*provider.Grant{} - for _, a := range acls { - grantee := &provider.Grantee{ - Id: &userpb.UserId{OpaqueId: a.Qualifier}, - Type: fs.getGranteeType(a.Type), - } - grants = append(grants, &provider.Grant{ - Grantee: grantee, - Permissions: fs.getGrantPermissionSet(a.Permissions), - }) - } - - return grants, nil -} - -func (fs *eosfs) getGranteeType(aclType string) provider.GranteeType { - switch aclType { - case "u": - return provider.GranteeType_GRANTEE_TYPE_USER - case "g": - return provider.GranteeType_GRANTEE_TYPE_GROUP - default: - return provider.GranteeType_GRANTEE_TYPE_INVALID - } -} - -// TODO(labkode): add more fine grained controls. -// EOS acls are a mix of ACLs and POSIX permissions. More details can be found in -// https://github.com/cern-eos/eos/blob/master/doc/configuration/permission.rst -// TODO we need to evaluate all acls in the list at once to properly forbid (!) and overwrite (+) permissions -// This is ugly, because those are actually negative permissions ... -func (fs *eosfs) getGrantPermissionSet(mode string) *provider.ResourcePermissions { - - // TODO also check unix permissions for read access - p := &provider.ResourcePermissions{} - // r - if strings.Contains(mode, "r") { - p.Stat = true - p.InitiateFileDownload = true - } - // w - if strings.Contains(mode, "w") { - p.CreateContainer = true - p.InitiateFileUpload = true - p.Delete = true - if p.InitiateFileDownload { - p.Move = true - } - } - if strings.Contains(mode, "wo") { - p.CreateContainer = true - // p.InitiateFileUpload = false // TODO only when the file exists - p.Delete = false - } - if strings.Contains(mode, "!d") { - p.Delete = false - } else if strings.Contains(mode, "+d") { - p.Delete = true - } - // x - if strings.Contains(mode, "x") { - p.ListContainer = true - } - - // sharing - // TODO AddGrant - // TODO ListGrants - // TODO RemoveGrant - // TODO UpdateGrant - - // trash - // TODO ListRecycle - // TODO RestoreRecycleItem - // TODO PurgeRecycle - - // versions - // TODO ListFileVersions - // TODO RestoreFileVersion - - // ? - // TODO GetPath - // TODO GetQuota - return p -} - -func (fs *eosfs) GetMD(ctx context.Context, ref *provider.Reference, mdKeys []string) (*provider.ResourceInfo, error) { - u, err := getUser(ctx) - if err != nil { - return nil, err - } - - log := appctx.GetLogger(ctx) - log.Info().Msg("eos: get md for ref:" + ref.String()) - - p, err := fs.resolve(ctx, u, ref) - if err != nil { - return nil, errors.Wrap(err, "eos: error resolving reference") - } - - // if path is home we need to add in the response any shadow folder in the shadow homedirectory. - if fs.conf.EnableHome { - if fs.isShareFolder(ctx, p) { - return fs.getMDShareFolder(ctx, p, mdKeys) - } - } - - fn := fs.wrap(ctx, p) - - eosFileInfo, err := fs.c.GetFileInfoByPath(ctx, u.Username, fn) - if err != nil { - return nil, err - } + c.UseGRPC = true - fi := fs.convertToResourceInfo(ctx, eosFileInfo) - return fi, nil + return eosfs.NewEOSFS(c) } - -func (fs *eosfs) getMDShareFolder(ctx context.Context, p string, mdKeys []string) (*provider.ResourceInfo, error) { - u, err := getUser(ctx) - if err != nil { - return nil, err - } - - fn := fs.wrapShadow(ctx, p) - eosFileInfo, err := fs.c.GetFileInfoByPath(ctx, u.Username, fn) - if err != nil { - return nil, err - } - // TODO(labkode): diff between root (dir) and children (ref) - - if fs.isShareFolderRoot(ctx, p) { - return fs.convertToResourceInfo(ctx, eosFileInfo), nil - } - return fs.convertToFileReference(ctx, eosFileInfo), nil -} - -func (fs *eosfs) ListFolder(ctx context.Context, ref *provider.Reference, mdKeys []string) ([]*provider.ResourceInfo, error) { - u, err := getUser(ctx) - if err != nil { - return nil, errors.Wrap(err, "eos: no user in ctx") - } - - p, err := fs.resolve(ctx, u, ref) - if err != nil { - return nil, errors.Wrap(err, "eos: error resolving reference") - } - - // if path is home we need to add in the response any shadow folder in the shadown homedirectory. - if fs.conf.EnableHome { - home, err := fs.GetHome(ctx) - if err != nil { - err = errors.Wrap(err, "eos: error getting home") - return nil, err - } - - if strings.HasPrefix(p, home) { - return fs.listWithHome(ctx, home, p) - } - } - - return fs.listWithNominalHome(ctx, p) -} - -func (fs *eosfs) listWithNominalHome(ctx context.Context, p string) (finfos []*provider.ResourceInfo, err error) { - log := appctx.GetLogger(ctx) - - u, err := getUser(ctx) - if err != nil { - return nil, errors.Wrap(err, "eos: no user in ctx") - } - - fn := fs.wrap(ctx, p) - - eosFileInfos, err := fs.c.List(ctx, u.Username, fn) - if err != nil { - return nil, errors.Wrap(err, "eos: error listing") - } - - for _, eosFileInfo := range eosFileInfos { - // filter out sys files - if !fs.conf.ShowHiddenSysFiles { - base := path.Base(eosFileInfo.File) - if hiddenReg.MatchString(base) { - log.Debug().Msgf("eos: path is filtered because is considered hidden: path=%s hiddenReg=%s", base, hiddenReg) - continue - } - } - - finfos = append(finfos, fs.convertToResourceInfo(ctx, eosFileInfo)) - } - - return finfos, nil -} - -func (fs *eosfs) listWithHome(ctx context.Context, home, p string) ([]*provider.ResourceInfo, error) { - if p == home { - return fs.listHome(ctx, home) - } - - if fs.isShareFolderRoot(ctx, p) { - return fs.listShareFolderRoot(ctx, p) - } - - if fs.isShareFolderChild(ctx, p) { - return nil, errtypes.PermissionDenied("eos: error listing folders inside the shared folder, only file references are stored inside") - } - - // path points to a resource in the nominal home - return fs.listWithNominalHome(ctx, p) -} - -func (fs *eosfs) listHome(ctx context.Context, home string) ([]*provider.ResourceInfo, error) { - u, err := getUser(ctx) - if err != nil { - return nil, errors.Wrap(err, "eos: no user in ctx") - } - - fns := []string{fs.wrap(ctx, home), fs.wrapShadow(ctx, home)} - - finfos := []*provider.ResourceInfo{} - for _, fn := range fns { - eosFileInfos, err := fs.c.List(ctx, u.Username, fn) - if err != nil { - return nil, errors.Wrap(err, "eos: error listing") - } - - for _, eosFileInfo := range eosFileInfos { - // filter out sys files - if !fs.conf.ShowHiddenSysFiles { - base := path.Base(eosFileInfo.File) - if hiddenReg.MatchString(base) { - continue - } - - } - finfos = append(finfos, fs.convertToResourceInfo(ctx, eosFileInfo)) - } - - } - return finfos, nil -} - -func (fs *eosfs) listShareFolderRoot(ctx context.Context, p string) (finfos []*provider.ResourceInfo, err error) { - u, err := getUser(ctx) - if err != nil { - return nil, errors.Wrap(err, "eos: no user in ctx") - } - - fn := fs.wrapShadow(ctx, p) - - eosFileInfos, err := fs.c.List(ctx, u.Username, fn) - if err != nil { - return nil, errors.Wrap(err, "eos: error listing") - } - - for _, eosFileInfo := range eosFileInfos { - // filter out sys files - if !fs.conf.ShowHiddenSysFiles { - base := path.Base(eosFileInfo.File) - if hiddenReg.MatchString(base) { - continue - } - } - - finfo := fs.convertToFileReference(ctx, eosFileInfo) - finfos = append(finfos, finfo) - } - - return finfos, nil -} - -func (fs *eosfs) GetQuota(ctx context.Context) (int, int, error) { - u, err := getUser(ctx) - if err != nil { - return 0, 0, errors.Wrap(err, "eos: no user in ctx") - } - return fs.c.GetQuota(ctx, u.Username, fs.conf.Namespace) -} - -func (fs *eosfs) GetHome(ctx context.Context) (string, error) { - if !fs.conf.EnableHome { - return "", errtypes.NotSupported("eos: get home not supported") - } - - u, err := getUser(ctx) - if err != nil { - err = errors.Wrap(err, "local: wrap: no user in ctx and home is enabled") - return "", err - } - relativeHome := templates.WithUser(u, fs.conf.UserLayout) - - return relativeHome, nil -} - -func (fs *eosfs) createShadowHome(ctx context.Context) error { - u, err := getUser(ctx) - if err != nil { - return errors.Wrap(err, "eos: no user in ctx") - } - - home := fs.wrapShadow(ctx, "/") - _, err = fs.c.GetFileInfoByPath(ctx, "root", home) - if err == nil { // home already exists - return nil - } - - // TODO(labkode): abort on any error that is not found - if _, ok := err.(errtypes.IsNotFound); !ok { - return errors.Wrap(err, "eos: error verifying if user home directory exists") - } - - // TODO(labkode): only trigger creation on not found, copy from CERNBox logic. - err = fs.c.CreateDir(ctx, "root", home) - if err != nil { - // EOS will return success on mkdir over an existing directory. - return errors.Wrap(err, "eos: error creating dir") - } - - err = fs.c.Chown(ctx, "root", u.Username, home) - if err != nil { - return errors.Wrap(err, "eos: error chowning directory") - } - - err = fs.c.Chmod(ctx, "root", "770", home) - if err != nil { - return errors.Wrap(err, "eos: error chmoding directory") - } - - attrs := []*eosclientgrpc.Attribute{ - &eosclientgrpc.Attribute{ - Type: eosclientgrpc.SystemAttr, - Key: "mask", - Val: "700", - }, - &eosclientgrpc.Attribute{ - Type: eosclientgrpc.SystemAttr, - Key: "allow.oc.sync", - Val: "1", - }, - &eosclientgrpc.Attribute{ - Type: eosclientgrpc.SystemAttr, - Key: "mtime.propagation", - Val: "1", - }, - &eosclientgrpc.Attribute{ - Type: eosclientgrpc.SystemAttr, - Key: "forced.atomic", - Val: "1", - }, - } - - for _, attr := range attrs { - err = fs.c.SetAttr(ctx, "root", attr, true, home) - if err != nil { - return errors.Wrap(err, "eos: error setting attribute") - } - - } - - // create shadow folders - shadowFolders := []string{fs.conf.ShareFolder} - for _, sf := range shadowFolders { - sf = path.Join(home, sf) - err = fs.c.CreateDir(ctx, "root", sf) - if err != nil { - // EOS will return success on mkdir over an existing directory. - return errors.Wrap(err, "eos: error creating dir") - } - - } - - return nil -} - -func (fs *eosfs) createNominalHome(ctx context.Context) error { - u, err := getUser(ctx) - if err != nil { - return errors.Wrap(err, "eos: no user in ctx") - } - - home := fs.wrap(ctx, "/") - _, err = fs.c.GetFileInfoByPath(ctx, "root", home) - if err == nil { // home already exists - return nil - } - - // TODO(labkode): abort on any error that is not found - if _, ok := err.(errtypes.IsNotFound); !ok { - return errors.Wrap(err, "eos: error verifying if user home directory exists") - } - - // TODO(labkode): only trigger creation on not found, copy from CERNBox logic. - err = fs.c.CreateDir(ctx, "root", home) - if err != nil { - // EOS will return success on mkdir over an existing directory. - return errors.Wrap(err, "eos: error creating dir") - } - err = fs.c.Chown(ctx, "root", u.Username, home) - if err != nil { - return errors.Wrap(err, "eos: error chowning directory") - } - - err = fs.c.Chmod(ctx, "root", "770", home) - if err != nil { - return errors.Wrap(err, "eos: error chmoding directory") - } - - attrs := []*eosclientgrpc.Attribute{ - &eosclientgrpc.Attribute{ - Type: eosclientgrpc.SystemAttr, - Key: "mask", - Val: "700", - }, - &eosclientgrpc.Attribute{ - Type: eosclientgrpc.SystemAttr, - Key: "allow.oc.sync", - Val: "1", - }, - &eosclientgrpc.Attribute{ - Type: eosclientgrpc.SystemAttr, - Key: "mtime.propagation", - Val: "1", - }, - &eosclientgrpc.Attribute{ - Type: eosclientgrpc.SystemAttr, - Key: "forced.atomic", - Val: "1", - }, - } - - for _, attr := range attrs { - err = fs.c.SetAttr(ctx, "root", attr, true, home) - if err != nil { - return errors.Wrap(err, "eos: error setting attribute") - } - - } - return nil -} - -func (fs *eosfs) CreateHome(ctx context.Context) error { - if !fs.conf.EnableHome { - return errtypes.NotSupported("eos: create home not supported") - } - - if err := fs.createNominalHome(ctx); err != nil { - return errors.Wrap(err, "eos: error creating nominal home") - } - - if err := fs.createShadowHome(ctx); err != nil { - return errors.Wrap(err, "eos: error creating shadow home") - } - - return nil -} - -func (fs *eosfs) CreateDir(ctx context.Context, p string) error { - log := appctx.GetLogger(ctx) - u, err := getUser(ctx) - if err != nil { - return errors.Wrap(err, "eos: no user in ctx") - } - - log.Info().Msgf("eos: createdir: path=%s", p) - - if fs.isShareFolder(ctx, p) { - return errtypes.PermissionDenied("eos: cannot create folder under the share folder") - } - - fn := fs.wrap(ctx, p) - return fs.c.CreateDir(ctx, u.Username, fn) -} - -func (fs *eosfs) isShareFolder(ctx context.Context, p string) bool { - return strings.HasPrefix(p, fs.conf.ShareFolder) -} - -func (fs *eosfs) isShareFolderRoot(ctx context.Context, p string) bool { - return path.Clean(p) == fs.conf.ShareFolder -} - -func (fs *eosfs) isShareFolderChild(ctx context.Context, p string) bool { - p = path.Clean(p) - vals := strings.Split(p, fs.conf.ShareFolder+"/") - return len(vals) > 1 && vals[1] != "" -} - -func (fs *eosfs) CreateReference(ctx context.Context, p string, targetURI *url.URL) error { - // TODO(labkode): for the time being we only allow to create references - // on the virtual share folder to not pollute the nominal user tree. - - if !fs.isShareFolder(ctx, p) { - return errtypes.PermissionDenied("eos: cannot create references outside the share folder: share_folder=" + fs.conf.ShareFolder + " path=" + p) - } - - fn := fs.wrapShadow(ctx, p) - - // TODO(labkode): with grpc we can create a file touching with xattrs. - // Current mechanism is: touch to hidden dir, set xattr, rename. - dir, base := path.Split(fn) - tmp := path.Join(dir, fmt.Sprintf(".sys.reva#.%s", base)) - if err := fs.c.CreateDir(ctx, "root", tmp); err != nil { - err = errors.Wrapf(err, "eos: error creating temporary ref file") - return err - } - - // set xattr on ref - attr := &eosclientgrpc.Attribute{ - Type: eosclientgrpc.UserAttr, - Key: refTargetAttrKey, - Val: targetURI.String(), - } - - if err := fs.c.SetAttr(ctx, "root", attr, false, tmp); err != nil { - err = errors.Wrapf(err, "eos: error setting reva.ref attr on file: %q", tmp) - return err - } - - // rename to have the file visible in user space. - if err := fs.c.Rename(ctx, "root", tmp, fn); err != nil { - err = errors.Wrapf(err, "eos: error renaming from: %q to %q", tmp, fn) - return err - } - - return nil -} - -func (fs *eosfs) Delete(ctx context.Context, ref *provider.Reference) error { - u, err := getUser(ctx) - if err != nil { - return errors.Wrap(err, "eos: no user in ctx") - } - - p, err := fs.resolve(ctx, u, ref) - if err != nil { - return errors.Wrap(err, "eos: error resolving reference") - } - - if fs.isShareFolder(ctx, p) { - return fs.deleteShadow(ctx, p) - } - - fn := fs.wrap(ctx, p) - - return fs.c.Remove(ctx, u.Username, fn) -} - -func (fs *eosfs) deleteShadow(ctx context.Context, p string) error { - if fs.isShareFolderRoot(ctx, p) { - return errtypes.PermissionDenied("eos: cannot delete the virtual share folder") - } - - if fs.isShareFolderChild(ctx, p) { - u, err := getUser(ctx) - if err != nil { - return errors.Wrap(err, "eos: no user in ctx") - } - fn := fs.wrapShadow(ctx, p) - return fs.c.Remove(ctx, u.Username, fn) - } - - panic("eos: shadow delete of share folder that is neither root nor child. path=" + p) -} - -func (fs *eosfs) Move(ctx context.Context, oldRef, newRef *provider.Reference) error { - u, err := getUser(ctx) - if err != nil { - return errors.Wrap(err, "eos: no user in ctx") - } - - oldPath, err := fs.resolve(ctx, u, oldRef) - if err != nil { - return errors.Wrap(err, "eos: error resolving reference") - } - - newPath, err := fs.resolve(ctx, u, newRef) - if err != nil { - return errors.Wrap(err, "eos: error resolving reference") - } - - if fs.isShareFolder(ctx, oldPath) || fs.isShareFolder(ctx, newPath) { - return fs.moveShadow(ctx, oldPath, newPath) - } - - oldFn := fs.wrap(ctx, oldPath) - newFn := fs.wrap(ctx, newPath) - return fs.c.Rename(ctx, u.Username, oldFn, newFn) -} - -func (fs *eosfs) moveShadow(ctx context.Context, oldPath, newPath string) error { - u, err := getUser(ctx) - if err != nil { - return errors.Wrap(err, "eos: no user in ctx") - } - - if fs.isShareFolderRoot(ctx, oldPath) || fs.isShareFolderRoot(ctx, newPath) { - return errtypes.PermissionDenied("eos: cannot move/rename the virtual share folder") - } - - // only rename of the reference is allowed, hence having the same basedir - bold, _ := path.Split(oldPath) - bnew, _ := path.Split(newPath) - - if bold != bnew { - return errtypes.PermissionDenied("eos: cannot move references under the virtual share folder") - } - - oldfn := fs.wrapShadow(ctx, oldPath) - newfn := fs.wrapShadow(ctx, newPath) - return fs.c.Rename(ctx, u.Username, oldfn, newfn) -} - -func (fs *eosfs) Download(ctx context.Context, ref *provider.Reference) (io.ReadCloser, error) { - u, err := getUser(ctx) - if err != nil { - return nil, errors.Wrap(err, "eos: no user in ctx") - } - - p, err := fs.resolve(ctx, u, ref) - if err != nil { - return nil, errors.Wrap(err, "eos: error resolving reference") - } - - if fs.isShareFolder(ctx, p) { - return nil, errtypes.PermissionDenied("eos: cannot download under the virtual share folder") - } - - fn := fs.wrap(ctx, p) - - return fs.c.Read(ctx, u.Username, fn) -} - -//func (fs *eosfs) Upload(ctx context.Context, ref *provider.Reference, r io.ReadCloser) error { -// u, err := getUser(ctx) -// if err != nil { -// return errors.Wrap(err, "eos: no user in ctx") -// } -// -// p, err := fs.resolve(ctx, u, ref) -// if err != nil { -// return errors.Wrap(err, "eos: error resolving reference") -// } -// -// if fs.isShareFolder(ctx, p) { -// return errtypes.PermissionDenied("eos: cannot download under the virtual share folder") -// } -// -// fn := fs.wrap(ctx, p) -// -// return fs.c.Write(ctx, u.Username, fn, r) -//} - -func (fs *eosfs) ListRevisions(ctx context.Context, ref *provider.Reference) ([]*provider.FileVersion, error) { - u, err := getUser(ctx) - if err != nil { - return nil, errors.Wrap(err, "eos: no user in ctx") - } - - p, err := fs.resolve(ctx, u, ref) - if err != nil { - return nil, errors.Wrap(err, "eos: error resolving reference") - } - - if fs.isShareFolder(ctx, p) { - return nil, errtypes.PermissionDenied("eos: cannot list revisions under the virtual share folder") - } - - fn := fs.wrap(ctx, p) - - eosRevisions, err := fs.c.ListVersions(ctx, u.Username, fn) - if err != nil { - return nil, errors.Wrap(err, "eos: error listing versions") - } - revisions := []*provider.FileVersion{} - for _, eosRev := range eosRevisions { - rev := fs.convertToRevision(ctx, eosRev) - revisions = append(revisions, rev) - } - return revisions, nil -} - -func (fs *eosfs) DownloadRevision(ctx context.Context, ref *provider.Reference, revisionKey string) (io.ReadCloser, error) { - u, err := getUser(ctx) - if err != nil { - return nil, errors.Wrap(err, "eos: no user in ctx") - } - - p, err := fs.resolve(ctx, u, ref) - if err != nil { - return nil, errors.Wrap(err, "eos: error resolving reference") - } - - if fs.isShareFolder(ctx, p) { - return nil, errtypes.PermissionDenied("eos: cannot download revision under the virtual share folder") - } - - fn := fs.wrap(ctx, p) - - fn = fs.wrap(ctx, fn) - return fs.c.ReadVersion(ctx, u.Username, fn, revisionKey) -} - -func (fs *eosfs) RestoreRevision(ctx context.Context, ref *provider.Reference, revisionKey string) error { - u, err := getUser(ctx) - if err != nil { - return errors.Wrap(err, "eos: no user in ctx") - } - - p, err := fs.resolve(ctx, u, ref) - if err != nil { - return errors.Wrap(err, "eos: error resolving reference") - } - - if fs.isShareFolder(ctx, p) { - return errtypes.PermissionDenied("eos: cannot restore revision under the virtual share folder") - } - - fn := fs.wrap(ctx, p) - - return fs.c.RollbackToVersion(ctx, u.Username, fn, revisionKey) -} - -func (fs *eosfs) PurgeRecycleItem(ctx context.Context, key string) error { - u, err := getUser(ctx) - if err != nil { - return errors.Wrap(err, "storage_eos: no user in ctx") - } - return fs.c.RestoreDeletedEntry(ctx, u.Username, key) -} - -func (fs *eosfs) EmptyRecycle(ctx context.Context) error { - u, err := getUser(ctx) - if err != nil { - return errors.Wrap(err, "eos: no user in ctx") - } - return fs.c.PurgeDeletedEntries(ctx, u.Username) -} - -func (fs *eosfs) ListRecycle(ctx context.Context) ([]*provider.RecycleItem, error) { - u, err := getUser(ctx) - if err != nil { - return nil, errors.Wrap(err, "eos: no user in ctx") - } - eosDeletedEntries, err := fs.c.ListDeletedEntries(ctx, u.Username) - if err != nil { - return nil, errors.Wrap(err, "eos: error listing deleted entries") - } - recycleEntries := []*provider.RecycleItem{} - for _, entry := range eosDeletedEntries { - if !fs.conf.ShowHiddenSysFiles { - base := path.Base(entry.RestorePath) - if hiddenReg.MatchString(base) { - continue - } - - } - recycleItem := fs.convertToRecycleItem(ctx, entry) - recycleEntries = append(recycleEntries, recycleItem) - } - return recycleEntries, nil -} - -func (fs *eosfs) RestoreRecycleItem(ctx context.Context, key string) error { - u, err := getUser(ctx) - if err != nil { - return errors.Wrap(err, "eos: no user in ctx") - } - return fs.c.RestoreDeletedEntry(ctx, u.Username, key) -} - -func (fs *eosfs) convertToRecycleItem(ctx context.Context, eosDeletedItem *eosclientgrpc.DeletedEntry) *provider.RecycleItem { - path := fs.unwrap(ctx, eosDeletedItem.RestorePath) - recycleItem := &provider.RecycleItem{ - Path: path, - Key: eosDeletedItem.RestoreKey, - Size: eosDeletedItem.Size, - DeletionTime: &types.Timestamp{Seconds: eosDeletedItem.DeletionMTime / 1000}, // TODO(labkode): check if eos time is millis or nanos - } - if eosDeletedItem.IsDir { - recycleItem.Type = provider.ResourceType_RESOURCE_TYPE_CONTAINER - } else { - // TODO(labkode): if eos returns more types oin the future we need to map them. - recycleItem.Type = provider.ResourceType_RESOURCE_TYPE_FILE - } - return recycleItem -} - -func (fs *eosfs) convertToRevision(ctx context.Context, eosFileInfo *eosclientgrpc.FileInfo) *provider.FileVersion { - md := fs.convertToResourceInfo(ctx, eosFileInfo) - revision := &provider.FileVersion{ - Key: path.Base(md.Path), - Size: md.Size, - Mtime: md.Mtime.Seconds, // TODO do we need nanos here? - } - return revision -} - -func (fs *eosfs) convertToResourceInfo(ctx context.Context, eosFileInfo *eosclientgrpc.FileInfo) *provider.ResourceInfo { - return fs.convert(ctx, eosFileInfo) -} - -func (fs *eosfs) convertToFileReference(ctx context.Context, eosFileInfo *eosclientgrpc.FileInfo) *provider.ResourceInfo { - info := fs.convert(ctx, eosFileInfo) - info.Type = provider.ResourceType_RESOURCE_TYPE_REFERENCE - val, ok := eosFileInfo.Attrs["user.reva.target"] - if !ok || val == "" { - panic("eos: reference does not contain target: target=" + val + " file=" + eosFileInfo.File) - } - info.Target = val - return info -} - -func (fs *eosfs) convert(ctx context.Context, eosFileInfo *eosclientgrpc.FileInfo) *provider.ResourceInfo { - path := fs.unwrap(ctx, eosFileInfo.File) - - size := eosFileInfo.Size - if eosFileInfo.IsDir { - size = eosFileInfo.TreeSize - } - - username, err := getUsername(eosFileInfo.UID) - if err != nil { - log := appctx.GetLogger(ctx) - log.Warn().Uint64("uid", eosFileInfo.UID).Msg("could not lookup userid, leaving empty") - username = "" // TODO(labkode): should we abort here? - } - - info := &provider.ResourceInfo{ - Id: &provider.ResourceId{OpaqueId: fmt.Sprintf("%d", eosFileInfo.Inode)}, - Path: path, - Owner: &userpb.UserId{OpaqueId: username}, - Etag: fmt.Sprintf("\"%s\"", strings.Trim(eosFileInfo.ETag, "\"")), - MimeType: mime.Detect(eosFileInfo.IsDir, path), - Size: size, - PermissionSet: &provider.ResourcePermissions{ListContainer: true, CreateContainer: true}, - Mtime: &types.Timestamp{ - Seconds: eosFileInfo.MTimeSec, - Nanos: eosFileInfo.MTimeNanos, - }, - Opaque: &types.Opaque{ - Map: map[string]*types.OpaqueEntry{ - "eosgrpc": &types.OpaqueEntry{ - Decoder: "json", - Value: fs.getEosMetadata(eosFileInfo), - }, - }, - }, - } - - info.Type = getResourceType(eosFileInfo.IsDir) - return info -} - -func getResourceType(isDir bool) provider.ResourceType { - if isDir { - return provider.ResourceType_RESOURCE_TYPE_CONTAINER - } - return provider.ResourceType_RESOURCE_TYPE_FILE -} - -func getUsername(uid uint64) (string, error) { - s := strconv.FormatUint(uid, 10) - user, err := gouser.LookupId(s) - if err != nil { - return "", err - } - return user.Username, nil -} - -type eosSysMetadata struct { - TreeSize uint64 `json:"tree_size"` - TreeCount uint64 `json:"tree_count"` - File string `json:"file"` - Instance string `json:"instance"` -} - -func (fs *eosfs) getEosMetadata(finfo *eosclientgrpc.FileInfo) []byte { - sys := &eosSysMetadata{ - File: finfo.File, - Instance: finfo.Instance, - } - - if finfo.IsDir { - sys.TreeCount = finfo.TreeCount - sys.TreeSize = finfo.TreeSize - } - - v, _ := json.Marshal(sys) - return v -} - -/* - Merge shadow on requests for /home ? - - No - GetHome(ctx context.Context) (string, error) - No -CreateHome(ctx context.Context) error - No - CreateDir(ctx context.Context, fn string) error - No -Delete(ctx context.Context, ref *provider.Reference) error - No -Move(ctx context.Context, oldRef, newRef *provider.Reference) error - No -GetMD(ctx context.Context, ref *provider.Reference, mdKeys []string) (*provider.ResourceInfo, error) - Yes -ListFolder(ctx context.Context, ref *provider.Reference) ([]*provider.ResourceInfo, error) - No -Upload(ctx context.Context, ref *provider.Reference, r io.ReadCloser) error - No -Download(ctx context.Context, ref *provider.Reference) (io.ReadCloser, error) - No -ListRevisions(ctx context.Context, ref *provider.Reference) ([]*provider.FileVersion, error) - No -DownloadRevision(ctx context.Context, ref *provider.Reference, key string) (io.ReadCloser, error) - No -RestoreRevision(ctx context.Context, ref *provider.Reference, key string) error - No ListRecycle(ctx context.Context) ([]*provider.RecycleItem, error) - No RestoreRecycleItem(ctx context.Context, key string) error - No PurgeRecycleItem(ctx context.Context, key string) error - No EmptyRecycle(ctx context.Context) error - ? GetPathByID(ctx context.Context, id *provider.ResourceId) (string, error) - No AddGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error - No RemoveGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error - No UpdateGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error - No ListGrants(ctx context.Context, ref *provider.Reference) ([]*provider.Grant, error) - No GetQuota(ctx context.Context) (int, int, error) - No CreateReference(ctx context.Context, path string, targetURI *url.URL) error - No Shutdown(ctx context.Context) error - No SetArbitraryMetadata(ctx context.Context, ref *provider.Reference, md *provider.ArbitraryMetadata) error - No UnsetArbitraryMetadata(ctx context.Context, ref *provider.Reference, keys []string) error -*/ - -/* - Merge shadow on requests for /home/MyShares ? - - No - GetHome(ctx context.Context) (string, error) - No -CreateHome(ctx context.Context) error - No - CreateDir(ctx context.Context, fn string) error - Maybe -Delete(ctx context.Context, ref *provider.Reference) error - No -Move(ctx context.Context, oldRef, newRef *provider.Reference) error - Yes -GetMD(ctx context.Context, ref *provider.Reference, mdKeys []string) (*provider.ResourceInfo, error) - Yes -ListFolder(ctx context.Context, ref *provider.Reference) ([]*provider.ResourceInfo, error) - No -Upload(ctx context.Context, ref *provider.Reference, r io.ReadCloser) error - No -Download(ctx context.Context, ref *provider.Reference) (io.ReadCloser, error) - No -ListRevisions(ctx context.Context, ref *provider.Reference) ([]*provider.FileVersion, error) - No -DownloadRevision(ctx context.Context, ref *provider.Reference, key string) (io.ReadCloser, error) - No -RestoreRevision(ctx context.Context, ref *provider.Reference, key string) error - No ListRecycle(ctx context.Context) ([]*provider.RecycleItem, error) - No RestoreRecycleItem(ctx context.Context, key string) error - No PurgeRecycleItem(ctx context.Context, key string) error - No EmptyRecycle(ctx context.Context) error - ? GetPathByID(ctx context.Context, id *provider.ResourceId) (string, error) - No AddGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error - No RemoveGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error - No UpdateGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error - No ListGrants(ctx context.Context, ref *provider.Reference) ([]*provider.Grant, error) - No GetQuota(ctx context.Context) (int, int, error) - No CreateReference(ctx context.Context, path string, targetURI *url.URL) error - No Shutdown(ctx context.Context) error - No SetArbitraryMetadata(ctx context.Context, ref *provider.Reference, md *provider.ArbitraryMetadata) error - No UnsetArbitraryMetadata(ctx context.Context, ref *provider.Reference, keys []string) error -*/ - -/* - Merge shadow on requests for /home/MyShares/file-reference ? - - No - GetHome(ctx context.Context) (string, error) - No -CreateHome(ctx context.Context) error - No - CreateDir(ctx context.Context, fn string) error - Maybe -Delete(ctx context.Context, ref *provider.Reference) error - Yes -Move(ctx context.Context, oldRef, newRef *provider.Reference) error - Yes -GetMD(ctx context.Context, ref *provider.Reference, mdKeys []string) (*provider.ResourceInfo, error) - No -ListFolder(ctx context.Context, ref *provider.Reference) ([]*provider.ResourceInfo, error) - No -Upload(ctx context.Context, ref *provider.Reference, r io.ReadCloser) error - No -Download(ctx context.Context, ref *provider.Reference) (io.ReadCloser, error) - No -ListRevisions(ctx context.Context, ref *provider.Reference) ([]*provider.FileVersion, error) - No -DownloadRevision(ctx context.Context, ref *provider.Reference, key string) (io.ReadCloser, error) - No -RestoreRevision(ctx context.Context, ref *provider.Reference, key string) error - No ListRecycle(ctx context.Context) ([]*provider.RecycleItem, error) - No RestoreRecycleItem(ctx context.Context, key string) error - No PurgeRecycleItem(ctx context.Context, key string) error - No EmptyRecycle(ctx context.Context) error - ? GetPathByID(ctx context.Context, id *provider.ResourceId) (string, error) - No AddGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error - No RemoveGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error - No UpdateGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error - No ListGrants(ctx context.Context, ref *provider.Reference) ([]*provider.Grant, error) - No GetQuota(ctx context.Context) (int, int, error) - No CreateReference(ctx context.Context, path string, targetURI *url.URL) error - No Shutdown(ctx context.Context) error - Maybe SetArbitraryMetadata(ctx context.Context, ref *provider.Reference, md *provider.ArbitraryMetadata) error - Maybe UnsetArbitraryMetadata(ctx context.Context, ref *provider.Reference, keys []string) error -*/ diff --git a/pkg/storage/fs/eosgrpc/upload.go b/pkg/storage/fs/eosgrpc/upload.go deleted file mode 100644 index 847a1bff9c1..00000000000 --- a/pkg/storage/fs/eosgrpc/upload.go +++ /dev/null @@ -1,337 +0,0 @@ -// Copyright 2018-2020 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 eosgrpc - -import ( - "context" - "encoding/json" - "io" - "io/ioutil" - "os" - "path/filepath" - "strings" - - provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" - "github.com/cs3org/reva/pkg/appctx" - "github.com/cs3org/reva/pkg/errtypes" - "github.com/google/uuid" - "github.com/pkg/errors" - tusd "github.com/tus/tusd/pkg/handler" -) - -var defaultFilePerm = os.FileMode(0664) - -// TODO deprecated ... use tus -func (fs *eosfs) Upload(ctx context.Context, ref *provider.Reference, r io.ReadCloser) error { - u, err := getUser(ctx) - if err != nil { - return errors.Wrap(err, "eos: no user in ctx") - } - - p, err := fs.resolve(ctx, u, ref) - if err != nil { - return errors.Wrap(err, "eos: error resolving reference") - } - - if fs.isShareFolder(ctx, p) { - return errtypes.PermissionDenied("eos: cannot upload under the virtual share folder") - } - - fn := fs.wrap(ctx, p) - - return fs.c.Write(ctx, u.Username, fn, r) -} - -// InitiateUpload returns an upload id that can be used for uploads with tus -// TODO read optional content for small files in this request -func (fs *eosfs) InitiateUpload(ctx context.Context, ref *provider.Reference, uploadLength int64, metadata map[string]string) (uploadID string, err error) { - u, err := getUser(ctx) - if err != nil { - return "", errors.Wrap(err, "eos: no user in ctx") - } - - np, err := fs.resolve(ctx, u, ref) - if err != nil { - return "", errors.Wrap(err, "eos: error resolving reference") - } - - p := fs.wrap(ctx, np) - - info := tusd.FileInfo{ - MetaData: tusd.MetaData{ - "filename": filepath.Base(p), - "dir": filepath.Dir(p), - }, - Size: uploadLength, - } - - if metadata != nil && metadata["mtime"] != "" { - info.MetaData["mtime"] = metadata["mtime"] - } - - upload, err := fs.NewUpload(ctx, info) - if err != nil { - return "", err - } - - info, _ = upload.GetInfo(ctx) - - return info.ID, nil -} - -// UseIn tells the tus upload middleware which extensions it supports. -func (fs *eosfs) UseIn(composer *tusd.StoreComposer) { - composer.UseCore(fs) - composer.UseTerminater(fs) -} - -// NewUpload creates a new upload using the size as the file's length. To determine where to write the binary data -// the Fileinfo metadata must contain a dir and a filename. -// returns a unique id which is used to identify the upload. The properties Size and MetaData will be filled. -func (fs *eosfs) NewUpload(ctx context.Context, info tusd.FileInfo) (upload tusd.Upload, err error) { - - log := appctx.GetLogger(ctx) - log.Debug().Interface("info", info).Msg("eos: NewUpload") - - fn := info.MetaData["filename"] - if fn == "" { - return nil, errors.New("eos: missing filename in metadata") - } - info.MetaData["filename"] = filepath.Clean(info.MetaData["filename"]) - - dir := info.MetaData["dir"] - if dir == "" { - return nil, errors.New("eos: missing dir in metadata") - } - info.MetaData["dir"] = filepath.Clean(info.MetaData["dir"]) - - log.Debug().Interface("info", info).Msg("eos: resolved filename") - - info.ID = uuid.New().String() - - binPath, err := fs.getUploadPath(ctx, info.ID) - if err != nil { - return nil, errors.Wrap(err, "eos: error resolving upload path") - } - user, err := getUser(ctx) - if err != nil { - return nil, errors.Wrap(err, "eos: no user in ctx") - } - info.Storage = map[string]string{ - "Type": "EOSStore", - "Username": user.Username, - } - // Create binary file with no content - - file, err := os.OpenFile(binPath, os.O_CREATE|os.O_WRONLY, defaultFilePerm) - if err != nil { - return nil, err - } - defer file.Close() - - u := &fileUpload{ - info: info, - binPath: binPath, - infoPath: binPath + ".info", - fs: fs, - } - - if !info.SizeIsDeferred && info.Size == 0 { - log.Debug().Interface("info", info).Msg("eos: finishing upload for empty file") - // no need to create info file and finish directly - err := u.FinishUpload(ctx) - if err != nil { - return nil, err - } - return u, nil - } - - // writeInfo creates the file by itself if necessary - err = u.writeInfo() - if err != nil { - return nil, err - } - - return u, nil -} - -// TODO use a subdirectory in the shadow tree -func (fs *eosfs) getUploadPath(ctx context.Context, uploadID string) (string, error) { - return filepath.Join(fs.conf.CacheDirectory, uploadID), nil -} - -// GetUpload returns the Upload for the given upload id -func (fs *eosfs) GetUpload(ctx context.Context, id string) (tusd.Upload, error) { - binPath, err := fs.getUploadPath(ctx, id) - if err != nil { - return nil, err - } - infoPath := binPath + ".info" - info := tusd.FileInfo{} - data, err := ioutil.ReadFile(infoPath) - if err != nil { - return nil, err - } - if err := json.Unmarshal(data, &info); err != nil { - return nil, err - } - - stat, err := os.Stat(binPath) - if err != nil { - return nil, err - } - - info.Offset = stat.Size() - - return &fileUpload{ - info: info, - binPath: binPath, - infoPath: infoPath, - fs: fs, - }, nil -} - -type fileUpload struct { - // info stores the current information about the upload - info tusd.FileInfo - // infoPath is the path to the .info file - infoPath string - // binPath is the path to the binary file (which has no extension) - binPath string - // only fs knows how to handle metadata and versions - fs *eosfs -} - -// GetInfo returns the FileInfo -func (upload *fileUpload) GetInfo(ctx context.Context) (tusd.FileInfo, error) { - return upload.info, nil -} - -// GetReader returns an io.Reader for the upload -func (upload *fileUpload) GetReader(ctx context.Context) (io.Reader, error) { - return os.Open(upload.binPath) -} - -// WriteChunk writes the stream from the reader to the given offset of the upload -// TODO use the grpc api to directly stream to a temporary uploads location in the eos shadow tree -func (upload *fileUpload) WriteChunk(ctx context.Context, offset int64, src io.Reader) (int64, error) { - file, err := os.OpenFile(upload.binPath, os.O_WRONLY|os.O_APPEND, defaultFilePerm) - if err != nil { - return 0, err - } - defer file.Close() - - n, err := io.Copy(file, src) - - // If the HTTP PATCH request gets interrupted in the middle (e.g. because - // the user wants to pause the upload), Go's net/http returns an io.ErrUnexpectedEOF. - // However, for OwnCloudStore it's not important whether the stream has ended - // on purpose or accidentally. - if err != nil { - if err != io.ErrUnexpectedEOF { - return n, err - } - } - - upload.info.Offset += n - err = upload.writeInfo() - - return n, err -} - -// writeInfo updates the entire information. Everything will be overwritten. -func (upload *fileUpload) writeInfo() error { - data, err := json.Marshal(upload.info) - if err != nil { - return err - } - return ioutil.WriteFile(upload.infoPath, data, defaultFilePerm) -} - -// FinishUpload finishes an upload and moves the file to the internal destination -func (upload *fileUpload) FinishUpload(ctx context.Context) error { - - checksum := upload.info.MetaData["checksum"] - if checksum != "" { - // check checksum - s := strings.SplitN(checksum, " ", 2) - if len(s) == 2 { - alg, hash := s[0], s[1] - - log := appctx.GetLogger(ctx) - log.Debug(). - Interface("info", upload.info). - Str("alg", alg). - Str("hash", hash). - Msg("eos: TODO check checksum") // TODO this is done by eos if we write chunks to it directly - - } - } - np := filepath.Join(upload.info.MetaData["dir"], upload.info.MetaData["filename"]) - - err := upload.fs.c.WriteFile(ctx, upload.info.Storage["Username"], np, upload.binPath) - - // only delete the upload if it was successfully written to eos - if err == nil { - // cleanup in the background, delete might take a while and we don't need to wait for it to finish - go func() { - if err := os.Remove(upload.infoPath); err != nil { - if !os.IsNotExist(err) { - log := appctx.GetLogger(ctx) - log.Err(err).Interface("info", upload.info).Msg("eos: could not delete upload info") - } - } - if err := os.Remove(upload.binPath); err != nil { - if !os.IsNotExist(err) { - log := appctx.GetLogger(ctx) - log.Err(err).Interface("info", upload.info).Msg("eos: could not delete upload binary") - } - } - }() - } - - // TODO: set mtime if specified in metadata - - // metadata propagation is left to the storage implementation - return err -} - -// To implement the termination extension as specified in https://tus.io/protocols/resumable-upload.html#termination -// - the storage needs to implement AsTerminatableUpload -// - the upload needs to implement Terminate - -// AsTerminatableUpload returns a TerminatableUpload -func (fs *eosfs) AsTerminatableUpload(upload tusd.Upload) tusd.TerminatableUpload { - return upload.(*fileUpload) -} - -// Terminate terminates the upload -func (upload *fileUpload) Terminate(ctx context.Context) error { - if err := os.Remove(upload.infoPath); err != nil { - if !os.IsNotExist(err) { - return err - } - } - if err := os.Remove(upload.binPath); err != nil { - if !os.IsNotExist(err) { - return err - } - } - return nil -} diff --git a/pkg/storage/fs/eosgrpchome/eosgrpchome.go b/pkg/storage/fs/eosgrpchome/eosgrpchome.go new file mode 100644 index 00000000000..79cc953b771 --- /dev/null +++ b/pkg/storage/fs/eosgrpchome/eosgrpchome.go @@ -0,0 +1,58 @@ +// Copyright 2018-2020 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 eosgrpchome + +import ( + "github.com/cs3org/reva/pkg/storage" + "github.com/cs3org/reva/pkg/storage/fs/registry" + "github.com/cs3org/reva/pkg/storage/utils/eosfs" + "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" +) + +func init() { + registry.Register("eosgrpchome", New) +} + +func parseConfig(m map[string]interface{}) (*eosfs.Config, 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 + } + + return c, nil +} + +// New returns a new implementation of the storage.FS interface that connects to EOS. +func New(m map[string]interface{}) (storage.FS, error) { + c, err := parseConfig(m) + if err != nil { + return nil, err + } + c.UseGRPC = true + c.EnableHome = true + + return eosfs.NewEOSFS(c) +} diff --git a/pkg/storage/fs/eoshome/eoshome.go b/pkg/storage/fs/eoshome/eoshome.go index b72f184b773..717d5ae3ad9 100644 --- a/pkg/storage/fs/eoshome/eoshome.go +++ b/pkg/storage/fs/eoshome/eoshome.go @@ -19,9 +19,6 @@ package eoshome import ( - "bytes" - "encoding/gob" - "github.com/cs3org/reva/pkg/storage" "github.com/cs3org/reva/pkg/storage/fs/registry" "github.com/cs3org/reva/pkg/storage/utils/eosfs" @@ -33,79 +30,8 @@ func init() { registry.Register("eoshome", New) } -type config struct { - // Namespace for metadata operations - Namespace string `mapstructure:"namespace" docs:"/"` - - // ShadowNamespace for storing shadow data - ShadowNamespace string `mapstructure:"shadow_namespace" docs:"/.shadow"` - - // UploadsNamespace for storing upload data - UploadsNamespace string `mapstructure:"uploads_namespace" docs:"/.uploads"` - - // ShareFolder defines the name of the folder in the - // shadowed namespace. Ex: /eos/user/.shadow/h/hugo/MyShares - ShareFolder string `mapstructure:"share_folder" docs:"/MyShares"` - - // Location of the eos binary. - // Default is /usr/bin/eos. - EosBinary string `mapstructure:"eos_binary" docs:"/usr/bin/eos"` - - // Location of the xrdcopy binary. - // Default is /usr/bin/xrdcopy. - XrdcopyBinary string `mapstructure:"xrdcopy_binary" docs:"/usr/bin/xrdcopy"` - - // URL of the Master EOS MGM. - // Default is root://eos-example.org - MasterURL string `mapstructure:"master_url" docs:"root://eos-example.org"` - - // URL of the Slave EOS MGM. - // Default is root://eos-example.org - SlaveURL string `mapstructure:"slave_url" docs:"root://eos-example.org"` - - // Location on the local fs where to store reads. - // Defaults to os.TempDir() - CacheDirectory string `mapstructure:"cache_directory" docs:"/var/tmp/"` - - // SecProtocol specifies the xrootd security protocol to use between the server and EOS. - SecProtocol string `mapstructure:"sec_protocol" docs:"-"` - - // Keytab specifies the location of the keytab to use to authenticate to EOS. - Keytab string `mapstructure:"keytab" docs:"-"` - - // SingleUsername is the username to use when SingleUserMode is enabled - SingleUsername string `mapstructure:"single_username" docs:"-"` - - // UserLayout wraps the internal path with user information. - // Example: if conf.Namespace is /eos/user and received path is /docs - // and the UserLayout is {{.Username}} the internal path will be: - // /eos/user//docs - UserLayout string `mapstructure:"user_layout" docs:"{{.Username}}"` - - // Enables logging of the commands executed - // Defaults to false - EnableLogging bool `mapstructure:"enable_logging" docs:"false"` - - // ShowHiddenSysFiles shows internal EOS files like - // .sys.v# and .sys.a# files. - ShowHiddenSysFiles bool `mapstructure:"show_hidden_sys_files" docs:"false"` - - // ForceSingleUserMode will force connections to EOS to use SingleUsername - ForceSingleUserMode bool `mapstructure:"force_single_user_mode" docs:"false"` - - // UseKeyTabAuth changes will authenticate requests by using an EOS keytab. - UseKeytab bool `mapstructure:"use_keytab" docs:"false"` - - // Whether to maintain the same inode across various versions of a file. - // Requires extra metadata operations if set to true - VersionInvariant bool `mapstructure:"version_invariant" docs:"true"` - - // GatewaySvc stores the endpoint at which the GRPC gateway is exposed. - GatewaySvc string `mapstructure:"gatewaysvc" docs:"0.0.0.0:19000"` -} - -func parseConfig(m map[string]interface{}) (*config, error) { - c := &config{} +func parseConfig(m map[string]interface{}) (*eosfs.Config, error) { + c := &eosfs.Config{} if err := mapstructure.Decode(m, c); err != nil { err = errors.Wrap(err, "error decoding conf") return nil, err @@ -125,18 +51,7 @@ func New(m map[string]interface{}) (storage.FS, error) { if err != nil { return nil, err } + c.EnableHome = true - var buf bytes.Buffer - err = gob.NewEncoder(&buf).Encode(&c) - if err != nil { - return nil, err - } - var conf eosfs.Config - err = gob.NewDecoder(&buf).Decode(&conf) - if err != nil { - return nil, err - } - conf.EnableHome = true - - return eosfs.NewEOSFS(&conf) + return eosfs.NewEOSFS(c) } diff --git a/pkg/storage/fs/loader/loader.go b/pkg/storage/fs/loader/loader.go index 744265bc8ff..0077ee5c598 100644 --- a/pkg/storage/fs/loader/loader.go +++ b/pkg/storage/fs/loader/loader.go @@ -22,6 +22,7 @@ import ( // Load core storage filesystem backends. _ "github.com/cs3org/reva/pkg/storage/fs/eos" _ "github.com/cs3org/reva/pkg/storage/fs/eosgrpc" + _ "github.com/cs3org/reva/pkg/storage/fs/eosgrpchome" _ "github.com/cs3org/reva/pkg/storage/fs/eoshome" _ "github.com/cs3org/reva/pkg/storage/fs/local" _ "github.com/cs3org/reva/pkg/storage/fs/localhome" diff --git a/pkg/storage/utils/eosfs/config.go b/pkg/storage/utils/eosfs/config.go new file mode 100644 index 00000000000..f06e35cf25e --- /dev/null +++ b/pkg/storage/utils/eosfs/config.go @@ -0,0 +1,105 @@ +// Copyright 2018-2020 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 eosfs + +// Config holds the configuration details for the EOS fs. +type Config struct { + // Namespace for metadata operations + Namespace string `mapstructure:"namespace"` + + // ShadowNamespace for storing shadow data + ShadowNamespace string `mapstructure:"shadow_namespace"` + + // UploadsNamespace for storing upload data + UploadsNamespace string `mapstructure:"uploads_namespace"` + + // ShareFolder defines the name of the folder in the + // shadowed namespace. Ex: /eos/user/.shadow/h/hugo/MyShares + ShareFolder string `mapstructure:"share_folder"` + + // Location of the eos binary. + // Default is /usr/bin/eos. + EosBinary string `mapstructure:"eos_binary"` + + // Location of the xrdcopy binary. + // Default is /usr/bin/xrdcopy. + XrdcopyBinary string `mapstructure:"xrdcopy_binary"` + + // URL of the Master EOS MGM. + // Default is root://eos-example.org + MasterURL string `mapstructure:"master_url"` + + // URL of the Slave EOS MGM. + // Default is root://eos-example.org + SlaveURL string `mapstructure:"slave_url"` + + // Location on the local fs where to store reads. + // Defaults to os.TempDir() + CacheDirectory string `mapstructure:"cache_directory"` + + // SecProtocol specifies the xrootd security protocol to use between the server and EOS. + SecProtocol string `mapstructure:"sec_protocol"` + + // Keytab specifies the location of the keytab to use to authenticate to EOS. + Keytab string `mapstructure:"keytab"` + + // SingleUsername is the username to use when SingleUserMode is enabled + SingleUsername string `mapstructure:"single_username"` + + // UserLayout wraps the internal path with user information. + // Example: if conf.Namespace is /eos/user and received path is /docs + // and the UserLayout is {{.Username}} the internal path will be: + // /eos/user//docs + UserLayout string `mapstructure:"user_layout"` + + // Enables logging of the commands executed + // Defaults to false + EnableLogging bool `mapstructure:"enable_logging"` + + // ShowHiddenSysFiles shows internal EOS files like + // .sys.v# and .sys.a# files. + ShowHiddenSysFiles bool `mapstructure:"show_hidden_sys_files"` + + // ForceSingleUserMode will force connections to EOS to use SingleUsername + ForceSingleUserMode bool `mapstructure:"force_single_user_mode"` + + // UseKeyTabAuth changes will authenticate requests by using an EOS keytab. + UseKeytab bool `mapstructure:"use_keytab"` + + // EnableHome enables the creation of home directories. + EnableHome bool `mapstructure:"enable_home"` + + // Whether to maintain the same inode across various versions of a file. + // Requires extra metadata operations if set to true + VersionInvariant bool `mapstructure:"version_invariant"` + + // UseGRPC controls whether we spawn eosclient processes or use GRPC to connect to EOS. + UseGRPC bool `mapstructure:"use_grpc"` + + // GatewaySvc stores the endpoint at which the GRPC gateway is exposed. + GatewaySvc string `mapstructure:"gatewaysvc"` + + // GRPCAuthkey is the key that authorizes this client to connect to the GRPC service + // It's unclear whether this will be the final solution + GRPCAuthkey string `mapstructure:"grpc_auth_key"` + + // URI of the EOS MGM grpc server + // Default is empty + GrpcURI string `mapstructure:"master_grpc_uri"` +} diff --git a/pkg/storage/utils/eosfs/eosfs.go b/pkg/storage/utils/eosfs/eosfs.go index 6f47e3a93b5..c8138a98ad3 100644 --- a/pkg/storage/utils/eosfs/eosfs.go +++ b/pkg/storage/utils/eosfs/eosfs.go @@ -37,6 +37,8 @@ import ( types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/cs3org/reva/pkg/appctx" "github.com/cs3org/reva/pkg/eosclient" + "github.com/cs3org/reva/pkg/eosclient/eosbinary" + "github.com/cs3org/reva/pkg/eosclient/eosgrpc" "github.com/cs3org/reva/pkg/errtypes" "github.com/cs3org/reva/pkg/mime" "github.com/cs3org/reva/pkg/rgrpc/todo/pool" @@ -54,82 +56,14 @@ const ( refTargetAttrKey = "reva.target" ) -var hiddenReg = regexp.MustCompile(`\.sys\..#.`) - -// Config holds the configuration details for the EOS fs. -type Config struct { - // Namespace for metadata operations - Namespace string `mapstructure:"namespace"` - - // ShadowNamespace for storing shadow data - ShadowNamespace string `mapstructure:"shadow_namespace"` - - // UploadsNamespace for storing upload data - UploadsNamespace string `mapstructure:"uploads_namespace"` - - // ShareFolder defines the name of the folder in the - // shadowed namespace. Ex: /eos/user/.shadow/h/hugo/MyShares - ShareFolder string `mapstructure:"share_folder"` - - // Location of the eos binary. - // Default is /usr/bin/eos. - EosBinary string `mapstructure:"eos_binary"` - - // Location of the xrdcopy binary. - // Default is /usr/bin/xrdcopy. - XrdcopyBinary string `mapstructure:"xrdcopy_binary"` - - // URL of the Master EOS MGM. - // Default is root://eos-example.org - MasterURL string `mapstructure:"master_url"` - - // URL of the Slave EOS MGM. - // Default is root://eos-example.org - SlaveURL string `mapstructure:"slave_url"` - - // Location on the local fs where to store reads. - // Defaults to os.TempDir() - CacheDirectory string `mapstructure:"cache_directory"` - - // SecProtocol specifies the xrootd security protocol to use between the server and EOS. - SecProtocol string `mapstructure:"sec_protocol"` - - // Keytab specifies the location of the keytab to use to authenticate to EOS. - Keytab string `mapstructure:"keytab"` - - // SingleUsername is the username to use when SingleUserMode is enabled - SingleUsername string `mapstructure:"single_username"` - - // UserLayout wraps the internal path with user information. - // Example: if conf.Namespace is /eos/user and received path is /docs - // and the UserLayout is {{.Username}} the internal path will be: - // /eos/user//docs - UserLayout string `mapstructure:"user_layout"` - - // Enables logging of the commands executed - // Defaults to false - EnableLogging bool `mapstructure:"enable_logging"` - - // ShowHiddenSysFiles shows internal EOS files like - // .sys.v# and .sys.a# files. - ShowHiddenSysFiles bool `mapstructure:"show_hidden_sys_files"` - - // ForceSingleUserMode will force connections to EOS to use SingleUsername - ForceSingleUserMode bool `mapstructure:"force_single_user_mode"` - - // UseKeyTabAuth changes will authenticate requests by using an EOS keytab. - UseKeytab bool `mapstructure:"use_keytab"` - - // EnableHome enables the creation of home directories. - EnableHome bool `mapstructure:"enable_home"` - - // Whether to maintain the same inode across various versions of a file. - // Requires extra metadata operations if set to true - VersionInvariant bool `mapstructure:"version_invariant"` +const ( + // SystemAttr is the system extended attribute. + SystemAttr eosclient.AttrType = iota + // UserAttr is the user extended attribute. + UserAttr +) - // GatewaySvc stores the endpoint at which the GRPC gateway is exposed. - GatewaySvc string `mapstructure:"gatewaysvc"` -} +var hiddenReg = regexp.MustCompile(`\.sys\..#.`) func (c *Config) init() { c.Namespace = path.Clean(c.Namespace) @@ -175,7 +109,7 @@ func (c *Config) init() { } type eosfs struct { - c *eosclient.Client + c eosclient.EOSClient conf *Config chunkHandler *chunking.ChunkHandler singleUserUID string @@ -183,8 +117,7 @@ type eosfs struct { userIDCache sync.Map } -// NewEOSFS returns a storage.FS interface implementation that connects to an -// EOS instance +// NewEOSFS returns a storage.FS interface implementation that connects to an EOS instance func NewEOSFS(c *Config) (storage.FS, error) { c.init() @@ -196,21 +129,38 @@ func NewEOSFS(c *Config) (storage.FS, error) { } } - eosClientOpts := &eosclient.Options{ - XrdcopyBinary: c.XrdcopyBinary, - URL: c.MasterURL, - EosBinary: c.EosBinary, - CacheDirectory: c.CacheDirectory, - ForceSingleUserMode: c.ForceSingleUserMode, - SingleUsername: c.SingleUsername, - UseKeytab: c.UseKeytab, - Keytab: c.Keytab, - SecProtocol: c.SecProtocol, - VersionInvariant: c.VersionInvariant, + var eosClient eosclient.EOSClient + if c.UseGRPC { + eosClientOpts := &eosgrpc.Options{ + XrdcopyBinary: c.XrdcopyBinary, + URL: c.MasterURL, + GrpcURI: c.GrpcURI, + CacheDirectory: c.CacheDirectory, + ForceSingleUserMode: c.ForceSingleUserMode, + SingleUsername: c.SingleUsername, + UseKeytab: c.UseKeytab, + Keytab: c.Keytab, + Authkey: c.GRPCAuthkey, + SecProtocol: c.SecProtocol, + VersionInvariant: c.VersionInvariant, + } + eosClient = eosgrpc.New(eosClientOpts) + } else { + eosClientOpts := &eosbinary.Options{ + XrdcopyBinary: c.XrdcopyBinary, + URL: c.MasterURL, + EosBinary: c.EosBinary, + CacheDirectory: c.CacheDirectory, + ForceSingleUserMode: c.ForceSingleUserMode, + SingleUsername: c.SingleUsername, + UseKeytab: c.UseKeytab, + Keytab: c.Keytab, + SecProtocol: c.SecProtocol, + VersionInvariant: c.VersionInvariant, + } + eosClient = eosbinary.New(eosClientOpts) } - eosClient := eosclient.New(eosClientOpts) - eosfs := &eosfs{ c: eosClient, conf: c, @@ -646,8 +596,6 @@ func (fs *eosfs) ListFolder(ctx context.Context, ref *provider.Reference, mdKeys return nil, errors.Wrap(err, "eos: error resolving reference") } - log.Debug().Msg("internal: " + p) - // if path is home we need to add in the response any shadow folder in the shadow homedirectory. if fs.conf.EnableHome { log.Debug().Msg("home enabled") @@ -656,7 +604,6 @@ func (fs *eosfs) ListFolder(ctx context.Context, ref *provider.Reference, mdKeys } } - log.Debug().Msg("list with nominal home") return fs.listWithNominalHome(ctx, p) } @@ -699,14 +646,11 @@ func (fs *eosfs) listWithNominalHome(ctx context.Context, p string) (finfos []*p } func (fs *eosfs) listWithHome(ctx context.Context, home, p string) ([]*provider.ResourceInfo, error) { - log := appctx.GetLogger(ctx) if p == home { - log.Debug().Msg("listing home") return fs.listHome(ctx, home) } if fs.isShareFolderRoot(ctx, p) { - log.Debug().Msg("listing share root folder") return fs.listShareFolderRoot(ctx, p) } @@ -715,7 +659,6 @@ func (fs *eosfs) listWithHome(ctx context.Context, home, p string) ([]*provider. } // path points to a resource in the nominal home - log.Debug().Msg("listing nominal home") return fs.listWithNominalHome(ctx, p) } @@ -848,22 +791,21 @@ func (fs *eosfs) createShadowHome(ctx context.Context) error { return nil } _, err = fs.c.GetFileInfoByPath(ctx, uid, gid, home) - if err == nil { // home already exists - return nil - } + if err != nil { // home already exists + // TODO(labkode): abort on any error that is not found + if _, ok := err.(errtypes.IsNotFound); !ok { + return errors.Wrap(err, "eos: error verifying if user home directory exists") + } - // TODO(labkode): abort on any error that is not found - if _, ok := err.(errtypes.IsNotFound); !ok { - return errors.Wrap(err, "eos: error verifying if user home directory exists") + err = fs.createUserDir(ctx, u, home) + if err != nil { + return err + } } - err = fs.createUserDir(ctx, u, home) - if err != nil { - return err - } shadowFolders := []string{fs.conf.ShareFolder} for _, sf := range shadowFolders { - err = fs.createUserDir(ctx, u, path.Join(home, sf)) + err = fs.c.CreateDir(ctx, uid, gid, path.Join(home, sf)) if err != nil { return err } @@ -942,22 +884,22 @@ func (fs *eosfs) createUserDir(ctx context.Context, u *userpb.User, path string) attrs := []*eosclient.Attribute{ &eosclient.Attribute{ - Type: eosclient.SystemAttr, + Type: SystemAttr, Key: "mask", Val: "700", }, &eosclient.Attribute{ - Type: eosclient.SystemAttr, + Type: SystemAttr, Key: "allow.oc.sync", Val: "1", }, &eosclient.Attribute{ - Type: eosclient.SystemAttr, + Type: SystemAttr, Key: "mtime.propagation", Val: "1", }, &eosclient.Attribute{ - Type: eosclient.SystemAttr, + Type: SystemAttr, Key: "forced.atomic", Val: "1", }, @@ -1019,7 +961,7 @@ func (fs *eosfs) CreateReference(ctx context.Context, p string, targetURI *url.U // set xattr on ref attr := &eosclient.Attribute{ - Type: eosclient.UserAttr, + Type: UserAttr, Key: refTargetAttrKey, Val: targetURI.String(), } @@ -1083,7 +1025,7 @@ func (fs *eosfs) deleteShadow(ctx context.Context, p string) error { return fs.c.Remove(ctx, uid, gid, fn) } - panic("eos: shadow delete of share folder that is neither root nor child. path=" + p) + return errors.New("eos: shadow delete of share folder that is neither root nor child. path=" + p) } func (fs *eosfs) Move(ctx context.Context, oldRef, newRef *provider.Reference) error { @@ -1165,7 +1107,6 @@ func (fs *eosfs) Download(ctx context.Context, ref *provider.Reference) (io.Read } fn := fs.wrap(ctx, p) - return fs.c.Read(ctx, uid, gid, fn) } @@ -1507,6 +1448,17 @@ func (fs *eosfs) getRootUIDAndGID(ctx context.Context) (string, string, error) { return "0", "0", nil } +func attrTypeToString(at eosclient.AttrType) string { + switch at { + case SystemAttr: + return "sys" + case UserAttr: + return "user" + default: + return "invalid" + } +} + type eosSysMetadata struct { TreeSize uint64 `json:"tree_size"` TreeCount uint64 `json:"tree_count"`