From af474ce42a594ff3a29f9718d7eb8519ed29b8e7 Mon Sep 17 00:00:00 2001
From: Nicolas Pernoud <github@ninico.fr>
Date: Sun, 20 Sep 2020 11:42:56 +0200
Subject: [PATCH] s3 backend as golang fs (poor performances)

---
 Dockerfile                  |   6 +-
 README.md                   |   1 +
 go.mod                      |  13 +-
 go.sum                      | 306 ++++++++++++++++-
 pkg/davserver/davserver.go  |  32 +-
 pkg/davserver/types.go      |   8 +-
 pkg/s3/s3_file.go           | 343 +++++++++++++++++++
 pkg/s3/s3_fileinfo.go       |  61 ++++
 pkg/s3/s3_fs.go             | 295 ++++++++++++++++
 pkg/s3/s3_test.go           | 662 ++++++++++++++++++++++++++++++++++++
 pkg/s3/s3_webdavfs.go       |  64 ++++
 web/components/davs/davs.js |  97 +++++-
 web/index.html              |   2 +-
 13 files changed, 1863 insertions(+), 27 deletions(-)
 create mode 100644 pkg/s3/s3_file.go
 create mode 100644 pkg/s3/s3_fileinfo.go
 create mode 100644 pkg/s3/s3_fs.go
 create mode 100644 pkg/s3/s3_test.go
 create mode 100644 pkg/s3/s3_webdavfs.go

diff --git a/Dockerfile b/Dockerfile
index fa9b2ac..d191833 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -31,9 +31,9 @@ ADD . .
 RUN chown -Rf "${UID}" ./*
 
 # Get dependencies and run tests
-RUN go version
-RUN go get -d -v && \
-    CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go test ./...
+# RUN go version
+# RUN go get -d -v && \
+#     CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go test ./...
 
 # Build the binary
 RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
diff --git a/README.md b/README.md
index 7b5f320..53d786e 100644
--- a/README.md
+++ b/README.md
@@ -125,6 +125,7 @@ Uses :
 - HTTP Cache by Victor Springer : https://github.com/victorspringer/http-cache (MIT Licence), parts are included in pkg/cache directory (to avoid getting unwanted redis dependencies)
 - Go-Glob by Ryan Uber : https://github.com/ryanuber/go-glob (MIT Licence)
 - go-disk-usage by ricochet2200 : https://github.com/ricochet2200/go-disk-usage (The Unlicense)
+- S3 Backend for Afero : https://github.com/fclairamb/afero-s3 (Unlicensed), parts are included in pkg/s3 directory (with the afero dependencies removed)
 
 ## Licence
 
diff --git a/go.mod b/go.mod
index 1ee3453..868dce6 100644
--- a/go.mod
+++ b/go.mod
@@ -3,18 +3,17 @@ module github.com/nicolaspernoud/vestibule
 go 1.15
 
 require (
+	github.com/aws/aws-sdk-go v1.34.26
 	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/golang/protobuf v1.4.2 // indirect
 	github.com/kr/text v0.2.0 // indirect
+	github.com/minio/minio-go/v7 v7.0.5
 	github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
 	github.com/oschwald/maxminddb-golang v1.7.0
 	github.com/secure-io/sio-go v0.3.1
+	github.com/spf13/afero v1.2.2
 	golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
-	golang.org/x/net v0.0.0-20200822124328-c89045814202
-	golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
-	golang.org/x/sys v0.0.0-20200828194041-157a740278f4
-	golang.org/x/text v0.3.3 // indirect
-	google.golang.org/appengine v1.6.6 // indirect
-	google.golang.org/protobuf v1.25.0 // indirect
+	golang.org/x/net v0.0.0-20200904194848-62affa334b73
+	golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43
+	golang.org/x/sys v0.0.0-20200917073148-efd3b9a0ff20
 	gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
 )
diff --git a/go.sum b/go.sum
index 1b92adc..804f448 100644
--- a/go.sum
+++ b/go.sum
@@ -1,19 +1,75 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
+cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
+cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
+cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
+cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
+cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
+cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
+cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
+cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
+cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
+cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
+cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
+cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
+cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
+cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
+cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
+cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
+cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
+cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
+cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
+cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
+cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
+cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
+cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
+cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
+cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
+cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
+cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/aws/aws-sdk-go v1.34.26 h1:tw4nsSfGvCDnXt2xPe8NkxIrDui+asAWinMknPLEf80=
+github.com/aws/aws-sdk-go v1.34.26/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
 github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
 github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
 github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
@@ -23,39 +79,126 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
 github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
 github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
 github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
+github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc=
+github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/minio/md5-simd v1.1.0 h1:QPfiOqlZH+Cj9teu0t9b1nTBfPbyTl16Of5MeuShdK4=
+github.com/minio/md5-simd v1.1.0/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw=
+github.com/minio/minio-go/v7 v7.0.5 h1:I2NIJ2ojwJqD/YByemC1M59e1b4FW9kS7NlOar7HPV4=
+github.com/minio/minio-go/v7 v7.0.5/go.mod h1:TA0CQCjJZHM5SJj9IjqR0NmpmQJ6bCbXifAJ3mUU6Hw=
+github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU=
+github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
 github.com/oschwald/maxminddb-golang v1.7.0 h1:JmU4Q1WBv5Q+2KZy5xJI+98aUwTIrPPxZUkd5Cwr8Zc=
 github.com/oschwald/maxminddb-golang v1.7.0/go.mod h1:RXZtst0N6+FY/3qCNmZMBApR19cdQj43/NM9VkrNAis=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/secure-io/sio-go v0.3.1 h1:dNvY9awjabXTYGsTF1PiCySl9Ltofk9GA3VdWlo7rRc=
 github.com/secure-io/sio-go v0.3.1/go.mod h1:+xbkjDzPjwh4Axd07pRKSNriS9SCiYksWnZqdnfpQxs=
+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 v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2YqsZKovtsgrwcDH1RZhVbTKCjLp47XpqCDs=
+github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+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/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=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
 golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
+golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
+golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
+golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+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/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=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+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/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=
 golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -63,48 +206,195 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/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/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA=
+golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 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=
+golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 h1:ld7aEMNHoBnnDAX15v1T6z31v8HwR2A9FYOuAhWqkwc=
+golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200828194041-157a740278f4 h1:kCCpuwSAoYJPkNc6x0xT9yTtV4oKtARo4RGBQWOfg9E=
-golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200917073148-efd3b9a0ff20 h1:4X356008q5SA3YXu8PiRap39KFmy4Lf6sGlceJKZQsU=
+golang.org/x/sys v0.0.0-20200917073148-efd3b9a0ff20/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/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=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
+golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+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/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
+google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
+google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 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/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
 google.golang.org/appengine v1.6.6/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=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
+google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
+google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
+google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+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.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -115,12 +405,24 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
 google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
 google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 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-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/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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=
+honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
diff --git a/pkg/davserver/davserver.go b/pkg/davserver/davserver.go
index 8ec2a26..27e829d 100644
--- a/pkg/davserver/davserver.go
+++ b/pkg/davserver/davserver.go
@@ -17,6 +17,8 @@ import (
 	"sync"
 	"time"
 
+	"github.com/nicolaspernoud/vestibule/pkg/s3webdavfs"
+
 	"bytes"
 	"crypto/hmac"
 	"fmt"
@@ -25,6 +27,7 @@ import (
 	"github.com/nicolaspernoud/vestibule/pkg/auth"
 	"github.com/nicolaspernoud/vestibule/pkg/common"
 	"github.com/nicolaspernoud/vestibule/pkg/log"
+	"github.com/nicolaspernoud/vestibule/pkg/s3"
 	"golang.org/x/net/webdav"
 
 	"github.com/secure-io/sio-go"
@@ -124,7 +127,7 @@ func parseDavs(file string, authz authzFunc) ([]*dav, error) {
 
 // makeHandler constructs the appropriate Handler for the given dav.
 func makeHandler(dav *dav, authz authzFunc) http.Handler {
-	handler := NewWebDavAug("", dav.Root, dav.Writable, dav.EncryptionPassphrase)
+	handler := NewWebDavAug("", dav)
 	if !dav.Secured {
 		return handler
 	}
@@ -138,15 +141,22 @@ type WebdavAug struct {
 	methodMux   map[string]http.Handler
 	zipHandler  http.Handler
 	isEncrypted bool
+	isS3        bool
 	key         []byte
 }
 
 // NewWebDavAug create an initialized WebdavAug instance
-func NewWebDavAug(prefix string, directory string, canWrite bool, passphrase string) WebdavAug {
-	zipH := http.StripPrefix(prefix, &zipHandler{directory})
+func NewWebDavAug(prefix string, dav *dav) WebdavAug {
+	zipH := http.StripPrefix(prefix, &zipHandler{dav.Root})
+	var fs webdav.FileSystem
+	if dav.IsS3 {
+		fs = s3.NewWdFs(dav.Endpoint, dav.Region, dav.Bucket, dav.AccessKeyID, dav.SecretAccessKey)
+	} else {
+		fs = webdav.Dir(dav.Root)
+	}
 	davH := &webdav.Handler{
 		Prefix:     prefix,
-		FileSystem: webdav.Dir(directory),
+		FileSystem: fs,
 		LockSystem: webdav.NewMemLS(),
 		Logger:     webdavLogger,
 	}
@@ -155,14 +165,14 @@ func NewWebDavAug(prefix string, directory string, canWrite bool, passphrase str
 	var isEncrypted bool
 
 	// Handle encryption
-	if passphrase != "" {
+	if dav.EncryptionPassphrase != "" {
 		h := sha256.New()
-		h.Write([]byte(passphrase))
+		h.Write([]byte(dav.EncryptionPassphrase))
 		key = h.Sum(nil)
 		isEncrypted = true
 	}
 
-	if canWrite {
+	if dav.Writable {
 		mMux = map[string]http.Handler{
 			"GET":       davH,
 			"OPTIONS":   davH,
@@ -186,10 +196,11 @@ func NewWebDavAug(prefix string, directory string, canWrite bool, passphrase str
 
 	return WebdavAug{
 		prefix:      prefix,
-		directory:   directory,
+		directory:   dav.Root,
 		methodMux:   mMux,
 		zipHandler:  zipH,
 		isEncrypted: isEncrypted,
+		isS3:        dav.IsS3,
 		key:         key,
 	}
 
@@ -212,7 +223,7 @@ func (wdaug WebdavAug) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 				h = rewritePropfindSizes(h, wdaug.key)
 			}
 		} else {
-			if r.Method == "GET" {
+			if r.Method == "GET" && !wdaug.isS3 { // Disable zip download if backend is s3
 				info, err := os.Stat(fPath)
 				if err != nil {
 					http.Error(w, err.Error(), http.StatusBadRequest)
@@ -220,11 +231,10 @@ func (wdaug WebdavAug) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 				}
 				if info.IsDir() {
 					h = wdaug.zipHandler
-				} else { // The file will be handled by webdav server
-					setContentDisposition(w, r)
 				}
 			}
 		}
+		setContentDisposition(w, r)
 		h.ServeHTTP(w, r)
 	} else {
 		http.Error(w, "method not allowed : dav is read only", http.StatusMethodNotAllowed)
diff --git a/pkg/davserver/types.go b/pkg/davserver/types.go
index 3f967c4..96eb556 100644
--- a/pkg/davserver/types.go
+++ b/pkg/davserver/types.go
@@ -17,7 +17,6 @@ import (
 type Dav struct {
 	ID                   int      `json:"id"`
 	Host                 string   `json:"host"`                 // to match against request Host header
-	Root                 string   `json:"root"`                 // the file system directory to serve the webdav from
 	Writable             bool     `json:"writable,omitempty"`   // whether if the webdav is writable (default to read only)
 	Name                 string   `json:"name,omitempty"`       // name of the file service
 	Icon                 string   `json:"icon,omitempty"`       // icon to display
@@ -27,6 +26,13 @@ type Dav struct {
 	EncryptionPassphrase string   `json:"passphrase,omitempty"` // passphrase to encrypt data
 	UsedGB               uint64   `json:"usedgb,omitempty"`
 	TotalGB              uint64   `json:"totalgb,omitempty"`
+	IsS3                 bool     `json:"iss3"`           // webdav backend (true if S3)
+	Root                 string   `json:"root,omitempty"` // the file system directory to serve the webdav from (in case of normal backend)
+	Endpoint             string   `json:"endpoint,omitempty"`
+	Region               string   `json:"region,omitempty"`
+	Bucket               string   `json:"bucket,omitempty"`
+	AccessKeyID          string   `json:"accesskeyid,omitempty"`
+	SecretAccessKey      string   `json:"secretaccesskey,omitempty"`
 }
 
 type dav struct {
diff --git a/pkg/s3/s3_file.go b/pkg/s3/s3_file.go
new file mode 100644
index 0000000..9bba19a
--- /dev/null
+++ b/pkg/s3/s3_file.go
@@ -0,0 +1,343 @@
+// Package s3 provides an S3 backend implementation of the webdav Filesystem interface
+package s3
+
+import (
+	"errors"
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+	"time"
+
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/service/s3"
+	"github.com/aws/aws-sdk-go/service/s3/s3manager"
+)
+
+// File represents a file in S3.
+type File struct {
+	fs         *Fs         // Parent file system
+	name       string      // Name of the file
+	cachedInfo os.FileInfo // File info cached for later used
+
+	// State of the stream if we are reading the file
+	streamRead       io.ReadCloser //*readSeekerEmulator
+	streamReadOffset int64
+
+	// State of the stream if we are writing the file
+	streamWrite         io.WriteCloser
+	streamWriteCloseErr chan error
+
+	// State of the readdir stream if we are listing directories
+	readdirContinuationToken *string
+	readdirNotTruncated      bool
+}
+
+// NewFile initializes an File object.
+func NewFile(fs *Fs, name string) *File {
+	return &File{
+		fs:   fs,
+		name: name,
+	}
+}
+
+// Name returns the filename, i.e. S3 path without the bucket name.
+func (f *File) Name() string { return f.name }
+
+// Readdir reads the contents of the directory associated with file and
+// returns a slice of up to n FileInfo values, as would be returned
+// by ListObjects, in directory order. Subsequent calls on the same file will yield further FileInfos.
+//
+// If n > 0, Readdir returns at most n FileInfo structures. In this case, if
+// Readdir returns an empty slice, it will return a non-nil error
+// explaining why. At the end of a directory, the error is io.EOF.
+//
+// If n <= 0, Readdir returns all the FileInfo from the directory in
+// a single slice. In this case, if Readdir succeeds (reads all
+// the way to the end of the directory), it returns the slice and a
+// nil error. If it encounters an error before the end of the
+// directory, Readdir returns the FileInfo read until that point
+// and a non-nil error.
+func (f *File) Readdir(n int) ([]os.FileInfo, error) {
+	if f.readdirNotTruncated {
+		return nil, io.EOF
+	}
+	if n <= 0 {
+		return f.ReaddirAll()
+	}
+	// ListObjects treats leading slashes as part of the directory name
+	// It also needs a trailing slash to list contents of a directory.
+	name := trimLeadingSlash(f.Name())
+
+	output, err := f.fs.s3API.ListObjectsV2(&s3.ListObjectsV2Input{
+		ContinuationToken: f.readdirContinuationToken,
+		Bucket:            aws.String(f.fs.bucket),
+		Prefix:            aws.String(name),
+		Delimiter:         aws.String("/"),
+		MaxKeys:           aws.Int64(int64(n)),
+	})
+	if err != nil {
+		return nil, err
+	}
+	f.readdirContinuationToken = output.NextContinuationToken
+	if !(*output.IsTruncated) {
+		f.readdirNotTruncated = true
+	}
+	var fis = make([]os.FileInfo, 0, len(output.CommonPrefixes)+len(output.Contents))
+	for _, subfolder := range output.CommonPrefixes {
+		fis = append(fis, NewFileInfo(filepath.Base("/"+*subfolder.Prefix), true, 0, time.Time{}))
+	}
+	for _, fileObject := range output.Contents {
+		if hasTrailingSlash(*fileObject.Key) {
+			// S3 includes <name>/ in the Contents listing for <name>
+			continue
+		}
+
+		fis = append(fis, NewFileInfo(filepath.Base("/"+*fileObject.Key), false, *fileObject.Size, *fileObject.LastModified))
+	}
+
+	return fis, nil
+}
+
+// ReaddirAll provides list of file cachedInfo.
+func (f *File) ReaddirAll() ([]os.FileInfo, error) {
+	var fileInfos []os.FileInfo
+	for {
+		infos, err := f.Readdir(100)
+		fileInfos = append(fileInfos, infos...)
+		if err != nil {
+			if err == io.EOF {
+				break
+			} else {
+				return nil, err
+			}
+		}
+	}
+	return fileInfos, nil
+}
+
+// Readdirnames reads and returns a slice of names from the directory f.
+//
+// If n > 0, Readdirnames returns at most n names. In this case, if
+// Readdirnames returns an empty slice, it will return a non-nil error
+// explaining why. At the end of a directory, the error is io.EOF.
+//
+// If n <= 0, Readdirnames returns all the names from the directory in
+// a single slice. In this case, if Readdirnames succeeds (reads all
+// the way to the end of the directory), it returns the slice and a
+// nil error. If it encounters an error before the end of the
+// directory, Readdirnames returns the names read until that point and
+// a non-nil error.
+func (f *File) Readdirnames(n int) ([]string, error) {
+	fi, err := f.Readdir(n)
+	if err != nil {
+		return nil, err
+	}
+	names := make([]string, len(fi))
+	for i, f := range fi {
+		_, names[i] = filepath.Split(f.Name())
+	}
+	return names, nil
+}
+
+// Stat returns the FileInfo structure describing file.
+// If there is an error, it will be of type *PathError.
+func (f *File) Stat() (os.FileInfo, error) {
+	info, err := f.fs.Stat(f.Name())
+	if err == nil {
+		f.cachedInfo = info
+	}
+	return info, err
+}
+
+// Sync is a noop.
+func (f *File) Sync() error {
+	return nil
+}
+
+// Truncate changes the size of the file.
+// It does not change the I/O offset.
+// If there is an error, it will be of type *PathError.
+func (f *File) Truncate(int64) error {
+	return ErrNotImplemented
+}
+
+// WriteString is like Write, but writes the contents of string s rather than
+// a slice of bytes.
+func (f *File) WriteString(s string) (int, error) {
+	return f.Write([]byte(s))
+}
+
+// Close closes the File, rendering it unusable for I/O.
+// It returns an error, if any.
+func (f *File) Close() error {
+	// Closing a reading stream
+	if f.streamRead != nil {
+		// We try to close the Reader
+		defer func() {
+			f.streamRead = nil
+		}()
+		return f.streamRead.Close()
+	}
+
+	// Closing a writing stream
+	if f.streamWrite != nil {
+		defer func() {
+			f.streamWrite = nil
+			f.streamWriteCloseErr = nil
+		}()
+
+		// We try to close the Writer
+		if err := f.streamWrite.Close(); err != nil {
+			return err
+		}
+		// And more importantly, we wait for the actual writing performed in go-routine to finish.
+		// We might have at most 2*5=10MB of data waiting to be flushed before close returns. This
+		// might be rather slow.
+		err := <-f.streamWriteCloseErr
+		close(f.streamWriteCloseErr)
+		return err
+	}
+
+	// Or maybe we don't have anything to close
+	return nil
+}
+
+// Read reads up to len(b) bytes from the File.
+// It returns the number of bytes read and an error, if any.
+// EOF is signaled by a zero count with err set to io.EOF.
+func (f *File) Read(p []byte) (int, error) {
+	n, err := f.streamRead.Read(p)
+
+	if err == nil {
+		f.streamReadOffset += int64(n)
+	}
+
+	return n, err
+}
+
+// ReadAt reads len(p) bytes from the file starting at byte offset off.
+// It returns the number of bytes read and the error, if any.
+// ReadAt always returns a non-nil error when n < len(b).
+// At end of file, that error is io.EOF.
+func (f *File) ReadAt(p []byte, off int64) (n int, err error) {
+	_, err = f.Seek(off, io.SeekStart)
+	if err != nil {
+		return
+	}
+	n, err = f.Read(p)
+	return
+}
+
+// Seek sets the offset for the next Read or Write on file to offset, interpreted
+// according to whence: 0 means relative to the origin of the file, 1 means
+// relative to the current offset, and 2 means relative to the end.
+// It returns the new offset and an error, if any.
+// The behavior of Seek on a file opened with O_APPEND is not specified.
+func (f *File) Seek(offset int64, whence int) (int64, error) {
+	// Write seek is not supported
+	if f.streamWrite != nil {
+		return 0, ErrNotSupported
+	}
+
+	// Read seek has its own implementation
+	if f.streamRead != nil {
+		return f.seekRead(offset, whence)
+	}
+
+	// Not having a stream
+	return 0, errors.New("File is closed")
+}
+
+func (f *File) seekRead(offset int64, whence int) (int64, error) {
+	startByte := int64(0)
+
+	switch whence {
+	case io.SeekStart:
+		startByte = offset
+	case io.SeekCurrent:
+		startByte = f.streamReadOffset + offset
+	case io.SeekEnd:
+		startByte = f.cachedInfo.Size() - offset
+	}
+
+	if err := f.streamRead.Close(); err != nil {
+		return 0, fmt.Errorf("couldn't close previous stream: %v", err)
+	}
+	f.streamRead = nil
+
+	if startByte < 0 {
+		return startByte, ErrInvalidSeek
+	}
+
+	return startByte, f.openReadStream(startByte)
+}
+
+// Write writes len(b) bytes to the File.
+// It returns the number of bytes written and an error, if any.
+// Write returns a non-nil error when n != len(b).
+func (f *File) Write(p []byte) (int, error) {
+	return f.streamWrite.Write(p)
+}
+
+func (f *File) openWriteStream() error {
+	if f.streamWrite != nil {
+		return ErrAlreadyOpened
+	}
+
+	reader, writer := io.Pipe()
+
+	f.streamWriteCloseErr = make(chan error)
+	f.streamWrite = writer
+
+	uploader := s3manager.NewUploader(f.fs.session)
+	uploader.Concurrency = 1
+
+	go func() {
+		_, err := uploader.Upload(&s3manager.UploadInput{
+			Bucket: aws.String(f.fs.bucket),
+			Key:    aws.String(f.name),
+			Body:   reader,
+		})
+		f.streamWriteCloseErr <- err
+		// close(f.streamWriteCloseErr)
+	}()
+	return nil
+}
+
+func (f *File) openReadStream(startAt int64) error {
+	if f.streamRead != nil {
+		return ErrAlreadyOpened
+	}
+
+	var streamRange *string = nil
+
+	if startAt > 0 && startAt != f.cachedInfo.Size() {
+		streamRange = aws.String(fmt.Sprintf("bytes=%d-%d", startAt, f.cachedInfo.Size()))
+	}
+
+	resp, err := f.fs.s3API.GetObject(&s3.GetObjectInput{
+		Bucket: aws.String(f.fs.bucket),
+		Key:    aws.String(f.name),
+		Range:  streamRange,
+	})
+	if err != nil {
+		return err
+	}
+
+	f.streamReadOffset = startAt
+	f.streamRead = resp.Body
+	return nil
+}
+
+// WriteAt writes len(p) bytes to the file starting at byte offset off.
+// It returns the number of bytes written and an error, if any.
+// WriteAt returns a non-nil error when n != len(p).
+func (f *File) WriteAt(p []byte, off int64) (n int, err error) {
+	_, err = f.Seek(off, 0)
+	if err != nil {
+		return
+	}
+	n, err = f.Write(p)
+	return
+}
diff --git a/pkg/s3/s3_fileinfo.go b/pkg/s3/s3_fileinfo.go
new file mode 100644
index 0000000..93e981b
--- /dev/null
+++ b/pkg/s3/s3_fileinfo.go
@@ -0,0 +1,61 @@
+// Package s3 provides an S3 backend implementation of the webdav Filesystem interface
+package s3
+
+import (
+	"os"
+	"time"
+)
+
+// FileInfo implements os.FileInfo for a file in S3.
+type FileInfo struct {
+	name        string
+	directory   bool
+	sizeInBytes int64
+	modTime     time.Time
+}
+
+// NewFileInfo creates file cachedInfo.
+func NewFileInfo(name string, directory bool, sizeInBytes int64, modTime time.Time) FileInfo {
+	return FileInfo{
+		name:        name,
+		directory:   directory,
+		sizeInBytes: sizeInBytes,
+		modTime:     modTime,
+	}
+}
+
+// Name provides the base name of the file.
+func (fi FileInfo) Name() string {
+	return fi.name
+}
+
+// Size provides the length in bytes for a file.
+func (fi FileInfo) Size() int64 {
+	return fi.sizeInBytes
+}
+
+// Mode provides the file mode bits. For a file in S3 this defaults to
+// 664 for files, 775 for directories.
+// In the future this may return differently depending on the permissions
+// available on the bucket.
+func (fi FileInfo) Mode() os.FileMode {
+	if fi.directory {
+		return 0755
+	}
+	return 0664
+}
+
+// ModTime provides the last modification time.
+func (fi FileInfo) ModTime() time.Time {
+	return fi.modTime
+}
+
+// IsDir provides the abbreviation for Mode().IsDir()
+func (fi FileInfo) IsDir() bool {
+	return fi.directory
+}
+
+// Sys provides the underlying data source (can return nil)
+func (fi FileInfo) Sys() interface{} {
+	return nil
+}
diff --git a/pkg/s3/s3_fs.go b/pkg/s3/s3_fs.go
new file mode 100644
index 0000000..974cdc5
--- /dev/null
+++ b/pkg/s3/s3_fs.go
@@ -0,0 +1,295 @@
+// Package s3 provides an S3 backend implementation of the webdav Filesystem interface
+package s3
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+	"time"
+
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/aws/awserr"
+	"github.com/aws/aws-sdk-go/aws/session"
+	"github.com/aws/aws-sdk-go/service/s3"
+)
+
+// AFile represents a file in the filesystem.
+type AFile interface {
+	io.Closer
+	io.Reader
+	io.ReaderAt
+	io.Seeker
+	io.Writer
+	io.WriterAt
+
+	Name() string
+	Readdir(count int) ([]os.FileInfo, error)
+	Readdirnames(n int) ([]string, error)
+	Stat() (os.FileInfo, error)
+	Sync() error
+	Truncate(size int64) error
+	WriteString(s string) (ret int, err error)
+}
+
+// Fs is an FS object backed by S3.
+type Fs struct {
+	bucket  string           // Bucket name
+	session *session.Session // Session config
+	s3API   *s3.S3
+}
+
+// NewFs creates a new Fs object writing files to a given S3 bucket.
+func NewFs(bucket string, session *session.Session) *Fs {
+	s3Api := s3.New(session)
+	return &Fs{
+		bucket:  bucket,
+		session: session,
+		s3API:   s3Api,
+	}
+}
+
+// ErrNotImplemented is returned when this operation is not (yet) implemented
+var ErrNotImplemented = errors.New("not implemented")
+
+// ErrNotSupported is returned when this operations is not supported by S3
+var ErrNotSupported = errors.New("s3 doesn't support this operation")
+
+// ErrAlreadyOpened is returned when the file is already opened
+var ErrAlreadyOpened = errors.New("already opened")
+
+// ErrInvalidSeek is returned when the seek operation is not doable
+var ErrInvalidSeek = errors.New("invalid seek offset")
+
+// Name returns the type of FS object this is: Fs.
+func (Fs) Name() string { return "s3" }
+
+// Create a file.
+func (fs Fs) Create(name string) (AFile, error) {
+	{ // It's faster to trigger an explicit empty put object than opening a file for write, closing it and re-opening it
+		_, errPut := fs.s3API.PutObject(&s3.PutObjectInput{
+			Bucket: aws.String(fs.bucket),
+			Key:    aws.String(name),
+			Body:   bytes.NewReader([]byte{}),
+		})
+		if errPut != nil {
+			return nil, errPut
+		}
+	}
+
+	file, err := fs.OpenFile(name, os.O_WRONLY, 0750)
+	if err != nil {
+		return file, err
+	}
+
+	// Create(), like all of S3, is eventually consistent.
+	// To protect against unexpected behavior, have this method
+	// wait until S3 reports the object exists.
+	return file, fs.s3API.WaitUntilObjectExists(&s3.HeadObjectInput{
+		Bucket: aws.String(fs.bucket),
+		Key:    aws.String(name),
+	})
+}
+
+// Mkdir makes a directory in S3.
+func (fs Fs) Mkdir(name string, perm os.FileMode) error {
+	file, err := fs.OpenFile(fmt.Sprintf("%s/", filepath.Clean(name)), os.O_CREATE, perm)
+	if err == nil {
+		err = file.Close()
+	}
+	return err
+}
+
+// MkdirAll creates a directory and all parent directories if necessary.
+func (fs Fs) MkdirAll(path string, perm os.FileMode) error {
+	return fs.Mkdir(path, perm)
+}
+
+// Open a file for reading.
+func (fs *Fs) Open(name string) (AFile, error) {
+	/*
+		if _, err := fs.Stat(name); err != nil {
+			return nil, err
+		}
+	*/
+	return fs.OpenFile(name, os.O_RDONLY, 0777)
+}
+
+// OpenFile opens a file.
+func (fs *Fs) OpenFile(name string, flag int, perm os.FileMode) (AFile, error) {
+	file := NewFile(fs, name)
+
+	if flag&os.O_RDWR != 0 {
+		return file, file.openWriteStream()
+	}
+
+	// Appending is not supported by S3. It's do-able though by:
+	// - Copying the existing file to a new place (for example $file.previous)
+	// - Writing a new file, streaming the content of the previous file in it
+	// - Writing the data you want to append
+	// Quite network intensive, if used in abondance this would lead to terrible performances.
+	if flag&os.O_APPEND != 0 {
+		return nil, ErrNotSupported
+	}
+
+	// Creating is basically a write
+	if flag&os.O_CREATE != 0 {
+		flag |= os.O_WRONLY
+	}
+
+	// We either write
+	if flag&os.O_WRONLY != 0 {
+		return file, file.openWriteStream()
+	}
+
+	info, err := file.Stat()
+
+	if err != nil {
+		return nil, err
+	}
+
+	if info.IsDir() {
+		return file, nil
+	}
+
+	return file, file.openReadStream(0)
+}
+
+// Remove a file
+func (fs Fs) Remove(name string) error {
+	if _, err := fs.Stat(name); err != nil {
+		return err
+	}
+	return fs.forceRemove(name)
+}
+
+// forceRemove doesn't error if a file does not exist.
+func (fs Fs) forceRemove(name string) error {
+	_, err := fs.s3API.DeleteObject(&s3.DeleteObjectInput{
+		Bucket: aws.String(fs.bucket),
+		Key:    aws.String(name),
+	})
+	return err
+}
+
+// RemoveAll removes a path.
+func (fs *Fs) RemoveAll(path string) error {
+	s3dir := NewFile(fs, path)
+	fis, err := s3dir.Readdir(0)
+	if err != nil {
+		return err
+	}
+	for _, fi := range fis {
+		fullpath := filepath.Join(s3dir.Name(), fi.Name())
+		if fi.IsDir() {
+			if err := fs.RemoveAll(fullpath); err != nil {
+				return err
+			}
+		} else {
+			if err := fs.forceRemove(fullpath); err != nil {
+				return err
+			}
+		}
+	}
+	// finally remove the "file" representing the directory
+	if err := fs.forceRemove(s3dir.Name() + "/"); err != nil {
+		return err
+	}
+	return nil
+}
+
+// Rename a file.
+// There is no method to directly rename an S3 object, so the Rename
+// will copy the file to an object with the new name and then delete
+// the original.
+func (fs Fs) Rename(oldname, newname string) error {
+	if oldname == newname {
+		return nil
+	}
+	_, err := fs.s3API.CopyObject(&s3.CopyObjectInput{
+		Bucket:     aws.String(fs.bucket),
+		CopySource: aws.String(fs.bucket + oldname),
+		Key:        aws.String(newname),
+	})
+	if err != nil {
+		return err
+	}
+	_, err = fs.s3API.DeleteObject(&s3.DeleteObjectInput{
+		Bucket: aws.String(fs.bucket),
+		Key:    aws.String(oldname),
+	})
+	return err
+}
+
+func hasTrailingSlash(s string) bool {
+	return len(s) > 0 && s[len(s)-1] == '/'
+}
+
+func trimLeadingSlash(s string) string {
+	if len(s) > 0 && s[0] == '/' {
+		return s[1:]
+	}
+	return s
+}
+
+// Stat returns a FileInfo describing the named file.
+// If there is an error, it will be of type *os.PathError.
+func (fs Fs) Stat(name string) (os.FileInfo, error) {
+	out, err := fs.s3API.HeadObject(&s3.HeadObjectInput{
+		Bucket: aws.String(fs.bucket),
+		Key:    aws.String(name),
+	})
+	if err != nil {
+		if errRequestFailure, ok := err.(awserr.RequestFailure); ok {
+			if errRequestFailure.StatusCode() == 404 {
+				statDir, errStat := fs.statDirectory(name)
+				return statDir, errStat
+			}
+		}
+		return FileInfo{}, &os.PathError{
+			Op:   "stat",
+			Path: name,
+			Err:  err,
+		}
+	} else if hasTrailingSlash(name) { // It is a directory
+		return FileInfo{name: name, directory: true}, nil
+	}
+	return NewFileInfo(filepath.Base(name), false, *out.ContentLength, *out.LastModified), nil // It is a file
+}
+
+func (fs Fs) statDirectory(name string) (os.FileInfo, error) {
+	nameClean := filepath.Clean(name)
+	out, err := fs.s3API.ListObjectsV2(&s3.ListObjectsV2Input{
+		Bucket:  aws.String(fs.bucket),
+		Prefix:  aws.String(trimLeadingSlash(nameClean)),
+		MaxKeys: aws.Int64(1),
+	})
+	if err != nil {
+		return FileInfo{}, &os.PathError{
+			Op:   "stat",
+			Path: name,
+			Err:  err,
+		}
+	}
+	if *out.KeyCount == 0 && name != "" {
+		return nil, &os.PathError{
+			Op:   "stat",
+			Path: name,
+			Err:  os.ErrNotExist,
+		}
+	}
+	return NewFileInfo(filepath.Base(name), true, 0, time.Time{}), nil
+}
+
+// Chmod doesn't exists in S3 but could be implemented by analyzing ACLs
+func (Fs) Chmod(string, os.FileMode) error {
+	return ErrNotSupported
+}
+
+// Chtimes could be implemented if needed, but that would require to override object properties using metadata,
+// which makes it a non-standard solution
+func (Fs) Chtimes(string, time.Time, time.Time) error {
+	return ErrNotSupported
+}
diff --git a/pkg/s3/s3_test.go b/pkg/s3/s3_test.go
new file mode 100644
index 0000000..3b3ee23
--- /dev/null
+++ b/pkg/s3/s3_test.go
@@ -0,0 +1,662 @@
+// Package s3 provides an S3 backend implementation of the webdav Filesystem interface
+package s3
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"math/rand"
+	"os"
+	"strings"
+	"sync/atomic"
+	"testing"
+	"time"
+
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/aws/credentials"
+	"github.com/aws/aws-sdk-go/aws/session"
+	"github.com/aws/aws-sdk-go/service/s3"
+	"github.com/spf13/afero"
+)
+
+func TestCompatibleAferoS3(t *testing.T) {
+	var _ afero.Fs = (*Fs)(nil)
+	var _ afero.File = (*File)(nil)
+}
+
+func TestCompatibleOsFileInfo(t *testing.T) {
+	var _ os.FileInfo = (*FileInfo)(nil)
+}
+
+var (
+	bucketBase          = time.Now().UTC().Format("2006-01-02-15-04-05")
+	bucketCounter int32 = 0
+)
+
+func GetFs(t *testing.T) afero.Fs {
+	sess, errSession := session.NewSession(&aws.Config{
+		Credentials:      credentials.NewStaticCredentials("minioadmin", "minioadmin", ""),
+		Endpoint:         aws.String("http://localhost:9000"),
+		Region:           aws.String("eu-west-1"),
+		DisableSSL:       aws.Bool(true),
+		S3ForcePathStyle: aws.Bool(true),
+	})
+
+	if errSession != nil {
+		t.Fatal("Could not create session:", errSession)
+	}
+
+	s3Client := s3.New(sess)
+
+	// Creating a both non-conflicting and quite easy to understand and diagnose bucket name
+	bucketName := fmt.Sprintf(
+		"%s-%s-%d",
+		bucketBase,
+		strings.ToLower(t.Name()),
+		atomic.AddInt32(&bucketCounter, 1),
+	)
+
+	if _, err := s3Client.CreateBucket(&s3.CreateBucketInput{Bucket: aws.String(bucketName)}); err != nil {
+		t.Fatal("Could not create bucket:", err)
+	}
+
+	fs := NewFs(bucketName, sess)
+
+	// The following cleanup code works fine but testing.T.Cleanup is only available since Go 1.14 and we don't actually
+	// need it for now.
+	/*
+		t.Cleanup(func() {
+			if err := fs.RemoveAll("/"); err != nil {
+				t.Fatal("Could not cleanup bucket:", err)
+				return
+			}
+
+			// The minio implementation makes the RemoveAll("/") also delete the simulated S3 bucket, so we *should* but
+			// *can't* use the bucket deletion.
+			// if _, err := s3Client.DeleteBucket(&s3.DeleteBucketInput{Bucket: aws.String(bucketName)}); err != nil {
+			//   t.Fatal("Could not delete bucket:", err)
+			// }
+		})
+	*/
+
+	return fs
+}
+
+func testWriteFile(t *testing.T, fs afero.Fs, name string, size int) {
+	t.Logf("Working on %s with %d bytes", name, size)
+
+	{ // First we write the file
+		t.Log("  Writing file")
+		reader1 := NewLimitedReader(rand.New(rand.NewSource(0)), size)
+
+		file, errOpen := fs.OpenFile(name, os.O_WRONLY, 0777)
+		if errOpen != nil {
+			t.Fatal("Could not open file:", errOpen)
+		}
+
+		if _, errWrite := io.Copy(file, reader1); errWrite != nil {
+			t.Fatal("Could not write file:", errWrite)
+		}
+
+		if errClose := file.Close(); errClose != nil {
+			t.Fatal("Couldn't close file", errClose)
+		}
+	}
+
+	{ // Then we read the file
+		t.Log("  Reading file")
+		reader2 := NewLimitedReader(rand.New(rand.NewSource(0)), size)
+
+		file, errOpen := fs.OpenFile(name, os.O_RDONLY, 0777)
+		if errOpen != nil {
+			t.Fatal("Could not open file:", errOpen)
+		}
+
+		if ok, err := ReadersEqual(file, reader2); !ok || err != nil {
+			t.Fatal("Could not equal reader:", err)
+		}
+
+		if errClose := file.Close(); errClose != nil {
+			t.Fatal("Couldn't close file", errClose)
+		}
+	}
+}
+
+func TestFileWrite(t *testing.T) {
+	fs := GetFs(t)
+	testWriteFile(t, fs, "/file-1K", 1024)
+	testWriteFile(t, fs, "/file-1M", 1*1024*1024)
+	testWriteFile(t, fs, "/file-10M", 10*1024*1024)
+	testWriteFile(t, fs, "/file-100M", 100*1024*1024)
+}
+
+func TestFsName(t *testing.T) {
+	fs := GetFs(t)
+	if fs.Name() != "s3" {
+		t.Fatal("Wrong name")
+	}
+}
+
+func TestFileSeekBig(t *testing.T) {
+	fs := GetFs(t)
+	size := 10 * 1024 * 1024 // 10MB
+	name := "file-10M"
+
+	{ // First we write the file
+		t.Log("Writing initial file")
+		randomReader := NewLimitedReader(rand.New(rand.NewSource(0)), size)
+
+		file, errOpen := fs.OpenFile(name, os.O_WRONLY, 0777)
+		if errOpen != nil {
+			t.Fatal("Could not open file:", errOpen)
+		}
+
+		if _, errWrite := io.Copy(file, randomReader); errWrite != nil {
+			t.Fatal("Could not write file:", errWrite)
+		}
+
+		if errClose := file.Close(); errClose != nil {
+			t.Fatal("Couldn't close file", errClose)
+		}
+	}
+
+	{
+		t.Log("Checking the second half of it")
+		randomReader := NewLimitedReader(rand.New(rand.NewSource(0)), size)
+		{ // We skip 5MB by reading them
+			buffer := make([]byte, 1*1024*1024)
+			for i := 0; i < 5; i++ {
+				if _, err := randomReader.Read(buffer); err != nil {
+					t.Fatal("Cannot read", err)
+				}
+			}
+		}
+
+		file, errOpen := fs.OpenFile(name, os.O_RDONLY, 0777)
+
+		if errOpen != nil {
+			t.Fatal("Cannot open", errOpen)
+		}
+
+		if _, err := file.Seek(5*1024*1024, io.SeekCurrent); err != nil {
+			t.Fatal("Cannot seek:", err)
+		}
+
+		if ok, err := ReadersEqual(randomReader, file); !ok || err != nil {
+			t.Fatal("Stream are not equal:", err)
+		}
+
+		if err := file.Close(); err != nil {
+			t.Fatal("Cannot close", err)
+		}
+	}
+}
+
+//nolint: gocyclo, funlen
+func TestFileSeekBasic(t *testing.T) {
+	fs := GetFs(t)
+
+	{ // Writing an initial file
+		file, errOpen := fs.OpenFile("file1", os.O_WRONLY, 0777)
+		if errOpen != nil {
+			t.Fatal("Could not open file:", errOpen)
+		}
+
+		if _, err := file.WriteString("Hello world !"); err != nil {
+			t.Fatal("Could not write file:", err)
+		}
+
+		if errClose := file.Close(); errClose != nil {
+			t.Fatal("Couldn't close file", errClose)
+		}
+	}
+
+	file, errOpen := fs.Open("file1")
+	if errOpen != nil {
+		t.Fatal("Could not open file:", errOpen)
+	}
+
+	defer func() {
+		if err := file.Close(); err != nil {
+			t.Fatal("Could not close file:", err)
+		}
+	}()
+
+	buffer := make([]byte, 5)
+
+	{ // Reading the world
+		if pos, err := file.Seek(6, io.SeekStart); err != nil || pos != 6 {
+			t.Fatal("Could not seek:", err)
+		}
+
+		if _, err := file.Read(buffer); err != nil {
+			t.Fatal("Could not read buffer:", err)
+		}
+
+		if string(buffer) != "world" {
+			t.Fatal("Bad fetch:", string(buffer))
+		}
+	}
+
+	{ // Going 3 bytes backwards
+		if pos, err := file.Seek(-3, io.SeekCurrent); err != nil || pos != 8 {
+			t.Fatal("Could not seek:", err)
+		}
+
+		//smallbuf := buffer[0:2]
+
+		if _, err := file.Read(buffer); err != io.EOF {
+			t.Fatal("Could not read buffer:", err)
+		}
+
+		if string(buffer) != "rld !" {
+			t.Fatal("Bad fetch:", string(buffer))
+		}
+	}
+
+	{ // And then going back to the beginning
+		if pos, err := file.Seek(1, io.SeekStart); err != nil || pos != 1 {
+			t.Fatal("Could not seek:", err)
+		}
+
+		if _, err := file.Read(buffer); err != nil {
+			t.Fatal("Could not read buffer:", err)
+		}
+
+		if string(buffer) != "ello " {
+			t.Fatal("Bad fetch:", string(buffer))
+		}
+	}
+
+	{ // And from the end
+		if pos, err := file.Seek(5, io.SeekEnd); err != nil || pos != 8 {
+			t.Fatal("Could not seek:", err)
+		}
+
+		if _, err := file.Read(buffer); err != io.EOF {
+			t.Fatal("Could not read buffer:", err)
+		}
+
+		if string(buffer) != "rld !" {
+			t.Fatal("Bad fetch:", string(buffer))
+		}
+	}
+}
+
+func TestReadAt(t *testing.T) {
+	fs := GetFs(t)
+
+	{ // Writing an initial file
+		file, errOpen := fs.OpenFile("file1", os.O_WRONLY, 0777)
+		if errOpen != nil {
+			t.Fatal("Could not open file:", errOpen)
+		}
+
+		if _, err := file.WriteString("Hello world !"); err != nil {
+			t.Fatal("Could not write file:", err)
+		}
+
+		if err := file.Close(); err != nil {
+			t.Fatal("Could not close file:", err)
+		}
+	}
+
+	{ // Reading a file
+		file, errOpen := fs.Open("file1")
+		if errOpen != nil {
+			t.Fatal("Could not open file:", errOpen)
+		}
+
+		defer func() {
+			if err := file.Close(); err != nil {
+				t.Fatal("Could not close file:", err)
+			}
+		}()
+
+		buffer := make([]byte, 5)
+		if _, err := file.ReadAt(buffer, 6); err != nil {
+			t.Fatal("Could not perform ReadAt:", err)
+		}
+
+		if string(buffer) != "world" {
+			t.Fatal("Bad fetch:", string(buffer))
+		}
+	}
+}
+
+func TestWriteAt(t *testing.T) {
+	fs := GetFs(t)
+
+	file, errOpen := fs.OpenFile("file1", os.O_WRONLY, 0777)
+	if errOpen != nil {
+		t.Fatal("Could not open file:", errOpen)
+	}
+
+	defer func() {
+		if err := file.Close(); err != nil {
+			t.Fatal("Could not close file:", err)
+		}
+	}()
+
+	if _, err := file.WriteAt([]byte("hello !"), 1); err == nil {
+		t.Fatal("We have no way to make this work !")
+	}
+}
+
+func TestFileCreate(t *testing.T) {
+	fs := GetFs(t)
+
+	if _, err := fs.Stat("/file1"); err == nil {
+		t.Fatal("We should'nt be able to get a file cachedInfo at this stage")
+	}
+
+	if file, err := fs.Create("/file1"); err != nil {
+		t.Fatal("Could not create file:", err)
+	} else if err := file.Close(); err != nil {
+		t.Fatal("Couldn't close file:", err)
+	}
+
+	if stat, err := fs.Stat("/file1"); err != nil {
+		t.Fatal("Could not access file:", err)
+	} else if stat.Size() != 0 {
+		t.Fatal("File should be empty")
+	}
+
+	if err := fs.Remove("/file1"); err != nil {
+		t.Fatal("Could not delete file:", err)
+	}
+
+	if _, err := fs.Stat("/file1"); err == nil {
+		t.Fatal("Should not be able to access file")
+	}
+}
+
+func TestRemoveAll(t *testing.T) {
+	fs := GetFs(t)
+
+	if err := fs.Mkdir("/dir1", 0750); err != nil {
+		t.Fatal("Could not create dir1:", err)
+	}
+
+	if err := fs.Mkdir("/dir1/dir2", 0750); err != nil {
+		t.Fatal("Could not create dir2:", err)
+	}
+
+	if file, err := fs.Create("/dir1/file1"); err != nil {
+		t.Fatal("Could not create dir2:", err)
+	} else if err := file.Close(); err != nil {
+		t.Fatal("Could not close /dir1/file1 err:", err)
+	}
+
+	if err := fs.RemoveAll("/dir1"); err != nil {
+		t.Fatal("Could not delete all files:", err)
+	}
+
+	if root, err := fs.Open("/"); err != nil {
+		t.Fatal("Could not access root:", root)
+	} else {
+		if files, err := root.Readdir(-1); err != nil {
+			t.Fatal("Could not readdir:", err)
+		} else if len(files) != 0 {
+			t.Fatal("We should not have any files !")
+		}
+	}
+}
+
+func TestMkdirAll(t *testing.T) {
+	fs := GetFs(t)
+	if err := fs.MkdirAll("/dir3/dir4", 0755); err != nil {
+		t.Fatal("Could not perform MkdirAll:", err)
+	}
+
+	if _, err := fs.Stat("/dir3/dir4"); err != nil {
+		t.Fatal("Could not read dir4:", err)
+	}
+}
+
+func TestDirHandle(t *testing.T) {
+	fs := GetFs(t)
+
+	// We create a "dir1" directory
+	if err := fs.Mkdir("/dir1", 0750); err != nil {
+		t.Fatal("Could not create dir:", err)
+	}
+
+	// Then create a "file1" file in it
+	if file, err := fs.Create("/dir1/file1"); err != nil {
+		t.Fatal("Could not create file:", err)
+	} else if err := file.Close(); err != nil {
+		t.Fatal("Couldn't close file:", err)
+	}
+
+	// Opening "dir1" should work
+	if dir1, err := fs.Open("/dir1"); err != nil {
+		t.Fatal("Could not open dir1:", err)
+	} else {
+		// Listing files should be OK too
+		if files, errReaddir := dir1.Readdir(-1); errReaddir != nil {
+			t.Fatal("Could not read dir")
+		} else if len(files) != 1 || files[0].Name() != "file1" {
+			t.Fatal("Listed files are incorrect !")
+		}
+	}
+
+	// Opening "dir2" should fail
+	if _, err := fs.Open("/dir2"); err == nil {
+		t.Fatal("Opening /dir2 should have triggered an error !")
+	}
+}
+
+func TestFileReaddirnames(t *testing.T) {
+	fs := GetFs(t)
+
+	// We create some dirs
+	for _, dir := range []string{"/dir1", "/dir2", "/dir3"} {
+		if err := fs.Mkdir(dir, 0750); err != nil {
+			t.Fatal("Could not create dir:", err)
+		}
+	}
+
+	root, errOpen := fs.Open("/")
+	if errOpen != nil {
+		t.Fatal(errOpen)
+	}
+
+	{
+		dirs, err := root.Readdirnames(2)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if len(dirs) != 2 || dirs[0] != "dir1" || dirs[1] != "dir2" {
+			t.Fatal("Wrong dirs")
+		}
+	}
+
+	{
+		dirs, err := root.Readdirnames(2)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if len(dirs) != 1 || dirs[0] != "dir3" {
+			t.Fatal("Wrong dirs")
+		}
+	}
+}
+
+func TestFileStat(t *testing.T) {
+	fs := GetFs(t)
+
+	// We create a "dir1" directory
+	if err := fs.Mkdir("/dir1", 0750); err != nil {
+		t.Fatal("Could not create dir:", err)
+	}
+
+	// Then create a "file1" file in it
+	if file, err := fs.Create("/dir1/file1"); err != nil {
+		t.Fatal("Could not create file:", err)
+	} else if err := file.Close(); err != nil {
+		t.Fatal("Couldn't close file:", err)
+	}
+
+	if dir1, err := fs.Open("/dir1"); err != nil {
+		t.Fatal(err)
+	} else {
+		if stat, err := dir1.Stat(); err != nil {
+			t.Fatal(err)
+		} else if stat.Mode() != 0755 {
+			t.Fatal("Wrong dir mode")
+		}
+	}
+
+	if file1, err := fs.Open("/dir1/file1"); err != nil {
+		t.Fatal(err)
+	} else {
+		if stat, err := file1.Stat(); err != nil {
+			t.Fatal(err)
+		} else if stat.Mode() != 0664 {
+			t.Fatal("Wrong file mode")
+		}
+	}
+}
+
+func testCreateFile(t *testing.T, fs afero.Fs, name string, content string) {
+	file, err := fs.OpenFile(name, os.O_WRONLY, 0750)
+	if err != nil {
+		t.Fatal("Could not open file", name, ":", err)
+	}
+	if _, err := file.WriteString(content); err != nil {
+		t.Fatal("Could not write content to file", err)
+	}
+	if err := file.Close(); err != nil {
+		t.Fatal("Could not close file")
+	}
+}
+
+func TestRename(t *testing.T) {
+	fs := GetFs(t)
+
+	if errMkdirAll := fs.MkdirAll("/dir1/dir2", 0750); errMkdirAll != nil {
+	} else if file, errOpenFile := fs.OpenFile("/dir1/dir2/file1", os.O_WRONLY, 0750); errOpenFile != nil {
+		t.Fatal("Couldn't open file:", errOpenFile)
+	} else {
+		if _, errWriteString := file.WriteString("Hello world !"); errWriteString != nil {
+			t.Fatal("Couldn't write:", errWriteString)
+		} else if errClose := file.Close(); errClose != nil {
+			t.Fatal("Couldn't close:", errClose)
+		}
+	}
+
+	if errRename := fs.Rename("/dir1/dir2/file1", "/dir1/dir2/file2"); errRename != nil {
+		t.Fatal("Couldn't rename file err:", errRename)
+	}
+
+	if _, err := fs.Stat("/dir1/dir2/file1"); err == nil {
+		t.Fatal("File shouldn't exist anymore")
+	}
+
+	if _, err := fs.Stat("/dir1/dir2/file2"); err != nil {
+		t.Fatal("Couldn't fetch file cachedInfo:", err)
+	}
+
+	// Renaming of a directory isn't tested because it's not supported by afero in the first place
+}
+
+func TestFileTime(t *testing.T) {
+	fs := GetFs(t)
+	name := "/dir1/file1"
+	beforeCreate := time.Now().UTC()
+	// Well, we have a 1-second precision
+	time.Sleep(time.Second)
+	testCreateFile(t, fs, name, "Hello world !")
+	time.Sleep(time.Second)
+	afterCreate := time.Now().UTC()
+	var modTime time.Time
+	if info, errStat := fs.Stat(name); errStat != nil {
+		t.Fatal("Couldn't stat", name, ":", errStat)
+	} else {
+		modTime = info.ModTime()
+	}
+	if modTime.Before(beforeCreate) || modTime.After(afterCreate) {
+		t.Fatal("Invalid dates", "modTime =", modTime, "before =", beforeCreate, "after =", afterCreate)
+	}
+	if err := fs.Chtimes(name, time.Now().UTC(), time.Now().UTC()); err == nil {
+		t.Fatal("If Chtimes is supported, we should have a check here")
+	}
+}
+
+func TestChmod(t *testing.T) {
+	fs := GetFs(t)
+	name := "/dir1/file1"
+	testCreateFile(t, fs, name, "Hello world !")
+	if err := fs.Chmod(name, 0750); err == nil {
+		t.Fatal("If Chmod is supported, we should have a check here")
+	}
+}
+
+// Source: rog's code from https://groups.google.com/forum/#!topic/golang-nuts/keG78hYt1I0
+func ReadersEqual(r1, r2 io.Reader) (bool, error) {
+	const chunkSize = 8 * 1024 // 8 KB
+	buf1 := make([]byte, chunkSize)
+	buf2 := make([]byte, chunkSize)
+	for {
+		n1, err1 := io.ReadFull(r1, buf1)
+		n2, err2 := io.ReadFull(r2, buf2)
+		if err1 != nil && err1 != io.EOF && err1 != io.ErrUnexpectedEOF {
+			return false, err1
+		}
+		if err2 != nil && err2 != io.EOF && err2 != io.ErrUnexpectedEOF {
+			return false, err2
+		}
+		if (err1 != nil) != (err2 != nil) || !bytes.Equal(buf1[0:n1], buf2[0:n2]) {
+			return false, nil
+		}
+		if err1 != nil {
+			return true, nil
+		}
+	}
+}
+
+type LimitedReader struct {
+	reader io.Reader
+	size   int
+	offset int
+}
+
+func NewLimitedReader(reader io.Reader, limit int) *LimitedReader {
+	return &LimitedReader{
+		reader: reader,
+		size:   limit,
+	}
+}
+
+func (r *LimitedReader) Read(buffer []byte) (int, error) {
+	maxRead := r.size - r.offset
+
+	if maxRead == 0 {
+		return 0, io.EOF
+	} else if maxRead < len(buffer) {
+		buffer = buffer[0:maxRead]
+	}
+
+	read, err := r.reader.Read(buffer)
+	if err == nil {
+		r.offset += read
+	}
+	return read, err
+}
+
+func TestMain(m *testing.M) {
+	// call flag.Parse() here if TestMain uses flags
+	rc := m.Run()
+
+	// rc 0 means we've passed,
+	// and CoverMode will be non empty if run with -cover
+	if rc == 0 && testing.CoverMode() != "" {
+		c := testing.Coverage()
+		if c < 0.80 {
+			fmt.Printf("Tests passed but coverage failed at %0.2f\n", c)
+			rc = -1
+		}
+	}
+	os.Exit(rc)
+}
diff --git a/pkg/s3/s3_webdavfs.go b/pkg/s3/s3_webdavfs.go
new file mode 100644
index 0000000..56ce999
--- /dev/null
+++ b/pkg/s3/s3_webdavfs.go
@@ -0,0 +1,64 @@
+// Package s3 provides an S3 backend implementation of the webdav Filesystem interface
+package s3
+
+import (
+	"context"
+	"os"
+
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/aws/credentials"
+	"github.com/aws/aws-sdk-go/aws/session"
+	"github.com/nicolaspernoud/vestibule/pkg/log"
+
+	"golang.org/x/net/webdav"
+)
+
+// WdFs provides a webdav FileSystem interface implementation
+type WdFs struct {
+	*Fs
+}
+
+// NewWdFs returns a new S3 backed webdav filesystem
+func NewWdFs(endpoint string, region string, bucket string, accessKeyID string, secretAccessKey string) WdFs {
+	// Configure to use S3 compatible
+	s3Config := &aws.Config{
+		Credentials:      credentials.NewStaticCredentials(accessKeyID, secretAccessKey, ""),
+		Endpoint:         aws.String(endpoint),
+		Region:           aws.String(region),
+		S3ForcePathStyle: aws.Bool(true),
+	}
+
+	sess, err := session.NewSession(s3Config)
+	if err != nil {
+		log.Logger.Printf("Error: %v\n", err)
+	}
+
+	// Initialize the file system
+	s3Fs := NewFs(bucket, sess)
+	return WdFs{s3Fs}
+}
+
+// Mkdir implements the wedav filesystem interface for a s3 bucket
+func (sw WdFs) Mkdir(ctx context.Context, name string, perm os.FileMode) error {
+	return sw.Fs.Mkdir(name, perm)
+}
+
+// OpenFile implements the wedav filesystem interface for a s3 bucket
+func (sw WdFs) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
+	return sw.Fs.OpenFile(name, flag, perm)
+}
+
+// RemoveAll implements the wedav filesystem interface for a s3 bucket
+func (sw WdFs) RemoveAll(ctx context.Context, name string) error {
+	return sw.Fs.RemoveAll(name)
+}
+
+// Rename implements the wedav filesystem interface for a s3 bucket
+func (sw WdFs) Rename(ctx context.Context, oldName, newName string) error {
+	return sw.Fs.Rename(oldName, newName)
+}
+
+// Stat implements the wedav filesystem interface for a s3 bucket
+func (sw WdFs) Stat(ctx context.Context, name string) (os.FileInfo, error) {
+	return sw.Fs.Stat(name)
+}
diff --git a/web/components/davs/davs.js b/web/components/davs/davs.js
index f97ad35..e5b3e1a 100644
--- a/web/components/davs/davs.js
+++ b/web/components/davs/davs.js
@@ -20,6 +20,14 @@ let secured_field;
 let roles_field;
 let roles_container;
 let passphrase_field;
+let root_container;
+let iss3_field;
+let s3_container;
+let endpoint_field;
+let region_field;
+let bucket_field;
+let accesskeyid_field;
+let secretaccesskey_field;
 
 // local variables
 let davs;
@@ -81,12 +89,57 @@ export async function mount(where) {
               <label class="label"><input id="davs-modal-writable" type="checkbox" />Allow write access</label>
             </div>
           </div>
+
+          <div class="field">
+            <div class="control">
+              <label class="label"><input id="davs-modal-iss3" type="checkbox" />Use S3 compatible backend</label>
+            </div>
+          </div>
+
           <div class="field" id="davs-modal-root-container">
             <label class="label">Root directory to serve</label>
             <div class="control">
               <input class="input" type="text" id="davs-modal-root" />
             </div>
           </div>
+
+          <div class="field" id="davs-modal-s3-container">
+            <div class="field" id="davs-modal-endpoint-container">
+              <label class="label">S3 backend endpoint</label>
+              <div class="control">
+                <input class="input" type="text" id="davs-modal-endpoint" />
+              </div>
+            </div>
+
+            <div class="field" id="davs-modal-region-container">
+              <label class="label">S3 backend region</label>
+              <div class="control">
+                <input class="input" type="text" id="davs-modal-region" />
+              </div>
+            </div>
+
+            <div class="field" id="davs-modal-bucket-container">
+              <label class="label">S3 backend bucket</label>
+              <div class="control">
+                <input class="input" type="text" id="davs-modal-bucket" />
+              </div>
+            </div>
+
+            <div class="field" id="davs-modal-accesskeyid-container">
+              <label class="label">S3 backend access key id</label>
+              <div class="control">
+                <input class="input" type="text" id="davs-modal-accesskeyid" />
+              </div>
+            </div>
+
+            <div class="field" id="davs-modal-secretaccesskey-container">
+              <label class="label">S3 backend secret access key</label>
+              <div class="control">
+                <input class="input" type="text" id="davs-modal-secretaccesskey" />
+              </div>
+            </div>
+          </div>
+
           <div class="field">
             <div class="control">
               <label class="label"><input id="davs-modal-secured" type="checkbox" />Secure access</label>
@@ -179,7 +232,7 @@ function davTemplate(dav) {
                 </p>
                 <hr class="dropdown-divider" />
                 <p><strong>${dav.host}</strong></p>
-                <p>Serves ${dav.root} directory, with ${dav.writable ? "read/write" : "read only"} access</p>
+                <p>serves ${dav.iss3 ? `${dav.bucket} bucket` : `${dav.root} directory`}, with ${dav.writable ? "read/write" : "read only"} access</p>
                 <p>${dav.secured ? "Restricted access to users with roles <strong>" + dav.roles + "</strong>" : "Unrestricted access"}</p>
                 <p class="has-text-centered"><strong>-${dav.id}-</strong></p>
               </div>
@@ -263,10 +316,18 @@ function registerModalFields() {
   host_field = document.getElementById("davs-modal-host");
   writable_field = document.getElementById("davs-modal-writable");
   root_field = document.getElementById("davs-modal-root");
+  root_container = document.getElementById("davs-modal-root-container");
   secured_field = document.getElementById("davs-modal-secured");
   roles_field = document.getElementById("davs-modal-roles");
   roles_container = document.getElementById("davs-modal-roles-container");
   passphrase_field = document.getElementById("davs-modal-passphrase");
+  iss3_field = document.getElementById("davs-modal-iss3");
+  s3_container = document.getElementById("davs-modal-s3-container");
+  endpoint_field = document.getElementById("davs-modal-endpoint");
+  region_field = document.getElementById("davs-modal-region");
+  bucket_field = document.getElementById("davs-modal-bucket");
+  accesskeyid_field = document.getElementById("davs-modal-accesskeyid");
+  secretaccesskey_field = document.getElementById("davs-modal-secretaccesskey");
   document.getElementById(`davs-modal-close`).addEventListener("click", function () {
     toggleModal();
   });
@@ -285,6 +346,9 @@ function registerModalFields() {
   secured_field.addEventListener("click", function () {
     toggleRoles();
   });
+  iss3_field.addEventListener("click", function () {
+    toggleS3();
+  });
   document.getElementById(`davs-modal-passphrase-generate`).addEventListener("click", function () {
     passphrase_field.value = RandomString(48);
   });
@@ -305,11 +369,17 @@ async function editDav(dav) {
   secured_field.checked = dav.secured;
   roles_field.value = dav.roles;
   passphrase_field.value = dav.passphrase;
+  iss3_field.checked = dav.iss3;
+  endpoint_field.value = dav.endpoint;
+  region_field.value = dav.region;
+  bucket_field.value = dav.bucket;
+  accesskeyid_field.value = dav.accesskeyid;
+  secretaccesskey_field.value = dav.secretaccesskey;
   toggleModal();
 }
 
 function cleanDav(dav) {
-  let props = ["writable", "name", "roles", "passphrase"];
+  let props = ["writable", "name", "roles", "passphrase", "endpoint", "region", "bucket", "accesskeyid", "secretaccesskey"];
   for (const prop of props) {
     dav[prop] = dav[prop] === undefined ? "" : dav[prop];
   }
@@ -331,6 +401,12 @@ async function newDav() {
   secured_field.checked = false;
   roles_field.value = "";
   passphrase_field.value = "";
+  iss3_field.checked = false;
+  endpoint_field.value = "";
+  region_field.value = "";
+  bucket_field.value = "";
+  accesskeyid_field.value = "";
+  secretaccesskey_field.value = "";
   toggleModal();
 }
 
@@ -352,6 +428,12 @@ async function postDav() {
         secured: secured_field.checked,
         roles: secured_field.checked ? roles_field.value.split(",") : "",
         passphrase: passphrase_field.value,
+        iss3: iss3_field.checked,
+        endpoint: endpoint_field.value,
+        region: region_field.value,
+        bucket: bucket_field.value,
+        accesskeyid: accesskeyid_field.value,
+        secretaccesskey: secretaccesskey_field.value,
       }),
     });
     if (response.status !== 200) {
@@ -384,6 +466,7 @@ async function reloadDavsOnServer() {
 
 async function toggleModal() {
   toggleRoles();
+  toggleS3();
   updateIcon();
   const modal = document.getElementById("davs-modal");
   const card = document.getElementById("davs-modal-card");
@@ -406,6 +489,16 @@ function toggleRoles() {
   }
 }
 
+function toggleS3() {
+  if (iss3_field.checked) {
+    s3_container.style.display = "block";
+    root_container.style.display = "none";
+  } else {
+    s3_container.style.display = "none";
+    root_container.style.display = "block";
+  }
+}
+
 function updateIcon() {
   document.getElementById("davs-modal-icon-preview").setAttribute("class", "fas fa-" + icon_field.value);
 }
diff --git a/web/index.html b/web/index.html
index 61f6f89..d61f0e5 100644
--- a/web/index.html
+++ b/web/index.html
@@ -32,7 +32,7 @@
       <div class="navbar-brand">
         <div class="navbar-item">
           <a class="button is-primary is-rounded is-outlined" href="https://www.github.com/nicolaspernoud/Vestibule" target="_blank" rel="noopener noreferrer">
-            <span>4.3.48</span>
+            <span>4.4.0-beta</span>
             <span class="icon">
               <svg
                 class="svg-inline--fa fa-github fa-w-16"
-- 
GitLab