github.com/cs3org/reva/v2@v2.27.7/pkg/rhttp/datatx/manager/tus/tus.go (about) 1 // Copyright 2018-2021 CERN 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 // 15 // In applying this license, CERN does not waive the privileges and immunities 16 // granted to it by virtue of its status as an Intergovernmental Organization 17 // or submit itself to any jurisdiction. 18 19 package tus 20 21 import ( 22 "context" 23 "net/http" 24 "path" 25 "regexp" 26 27 provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" 28 "github.com/mitchellh/mapstructure" 29 "github.com/pkg/errors" 30 "github.com/rs/zerolog" 31 tusd "github.com/tus/tusd/v2/pkg/handler" 32 "golang.org/x/exp/slog" 33 34 "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/net" 35 "github.com/cs3org/reva/v2/pkg/appctx" 36 "github.com/cs3org/reva/v2/pkg/errtypes" 37 "github.com/cs3org/reva/v2/pkg/events" 38 "github.com/cs3org/reva/v2/pkg/rhttp/datatx" 39 "github.com/cs3org/reva/v2/pkg/rhttp/datatx/manager/registry" 40 "github.com/cs3org/reva/v2/pkg/rhttp/datatx/metrics" 41 "github.com/cs3org/reva/v2/pkg/storage" 42 "github.com/cs3org/reva/v2/pkg/storagespace" 43 ) 44 45 func init() { 46 registry.Register("tus", New) 47 } 48 49 type TusConfig struct { 50 CorsEnabled bool `mapstructure:"cors_enabled"` 51 CorsAllowOrigin string `mapstructure:"cors_allow_origin"` 52 CorsAllowCredentials bool `mapstructure:"cors_allow_credentials"` 53 CorsAllowMethods string `mapstructure:"cors_allow_methods"` 54 CorsAllowHeaders string `mapstructure:"cors_allow_headers"` 55 CorsMaxAge string `mapstructure:"cors_max_age"` 56 CorsExposeHeaders string `mapstructure:"cors_expose_headers"` 57 } 58 59 type manager struct { 60 conf *TusConfig 61 publisher events.Publisher 62 log *zerolog.Logger 63 } 64 65 func parseConfig(m map[string]interface{}) (*TusConfig, error) { 66 c := &TusConfig{} 67 if err := mapstructure.Decode(m, c); err != nil { 68 err = errors.Wrap(err, "error decoding conf") 69 return nil, err 70 } 71 return c, nil 72 } 73 74 // New returns a datatx manager implementation that relies on HTTP PUT/GET. 75 func New(m map[string]interface{}, publisher events.Publisher, log *zerolog.Logger) (datatx.DataTX, error) { 76 c, err := parseConfig(m) 77 if err != nil { 78 return nil, err 79 } 80 81 l := log.With().Str("datatx", "tus").Logger() 82 83 return &manager{ 84 conf: c, 85 publisher: publisher, 86 log: &l, 87 }, nil 88 } 89 90 func (m *manager) Handler(fs storage.FS) (http.Handler, error) { 91 composable, ok := fs.(storage.ComposableFS) 92 if !ok { 93 return nil, errtypes.NotSupported("file system does not support the tus protocol") 94 } 95 96 // A storage backend for tusd may consist of multiple different parts which 97 // handle upload creation, locking, termination and so on. The composer is a 98 // place where all those separated pieces are joined together. In this example 99 // we only use the file store but you may plug in multiple. 100 composer := tusd.NewStoreComposer() 101 102 // let the composable storage tell tus which extensions it supports 103 composable.UseIn(composer) 104 105 config := tusd.Config{ 106 StoreComposer: composer, 107 NotifyCompleteUploads: true, 108 Logger: slog.New(tusdLogger{log: m.log}), 109 } 110 111 if m.conf.CorsEnabled { 112 allowOrigin, err := regexp.Compile(m.conf.CorsAllowOrigin) 113 if m.conf.CorsAllowOrigin != "" && err != nil { 114 return nil, err 115 } 116 117 config.Cors = &tusd.CorsConfig{ 118 Disable: false, 119 AllowOrigin: allowOrigin, 120 AllowCredentials: m.conf.CorsAllowCredentials, 121 AllowMethods: m.conf.CorsAllowMethods, 122 AllowHeaders: m.conf.CorsAllowHeaders, 123 MaxAge: m.conf.CorsMaxAge, 124 ExposeHeaders: m.conf.CorsExposeHeaders, 125 } 126 } 127 128 handler, err := tusd.NewUnroutedHandler(config) 129 if err != nil { 130 return nil, err 131 } 132 133 if usl, ok := fs.(storage.UploadSessionLister); ok { 134 // We can currently only send updates if the fs is decomposedfs as we read very specific keys from the storage map of the tus info 135 go func() { 136 for { 137 ev := <-handler.CompleteUploads 138 // We should be able to get the upload progress with fs.GetUploadProgress, but currently tus will erase the info files 139 // so we create a Progress instance here that is used to read the correct properties 140 ups, err := usl.ListUploadSessions(context.Background(), storage.UploadSessionFilter{ID: &ev.Upload.ID}) 141 if err != nil { 142 appctx.GetLogger(context.Background()).Error().Err(err).Str("session", ev.Upload.ID).Msg("failed to list upload session") 143 } else { 144 if len(ups) < 1 { 145 appctx.GetLogger(context.Background()).Error().Str("session", ev.Upload.ID).Msg("upload session not found") 146 continue 147 } 148 up := ups[0] 149 executant := up.Executant() 150 ref := up.Reference() 151 if m.publisher != nil { 152 if err := datatx.EmitFileUploadedEvent(up.SpaceOwner(), &executant, &ref, m.publisher); err != nil { 153 appctx.GetLogger(context.Background()).Error().Err(err).Msg("failed to publish FileUploaded event") 154 } 155 } 156 } 157 } 158 }() 159 } 160 161 h := handler.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 162 sublog := m.log.With().Str("uploadid", r.URL.Path).Logger() 163 r = r.WithContext(appctx.WithLogger(r.Context(), &sublog)) 164 method := r.Method 165 // https://github.com/tus/tus-resumable-upload-protocol/blob/master/protocol.md#x-http-method-override 166 if r.Header.Get("X-HTTP-Method-Override") != "" { 167 method = r.Header.Get("X-HTTP-Method-Override") 168 } 169 170 switch method { 171 case "POST": 172 metrics.UploadsActive.Add(1) 173 defer func() { 174 metrics.UploadsActive.Sub(1) 175 }() 176 // set etag, mtime and file id 177 setHeaders(fs, w, r) 178 handler.PostFile(w, r) 179 case "HEAD": 180 handler.HeadFile(w, r) 181 case "PATCH": 182 metrics.UploadsActive.Add(1) 183 defer func() { 184 metrics.UploadsActive.Sub(1) 185 }() 186 // set etag, mtime and file id 187 setHeaders(fs, w, r) 188 handler.PatchFile(w, r) 189 case "DELETE": 190 handler.DelFile(w, r) 191 case "GET": 192 metrics.DownloadsActive.Add(1) 193 defer func() { 194 metrics.DownloadsActive.Sub(1) 195 }() 196 // NOTE: this is breaking change - allthought it does not seem to be used 197 // We can make a switch here depending on some header value if that is needed 198 // download.GetOrHeadFile(w, r, fs, "") 199 handler.GetFile(w, r) 200 default: 201 w.WriteHeader(http.StatusNotImplemented) 202 } 203 })) 204 205 return h, nil 206 } 207 208 func setHeaders(fs storage.FS, w http.ResponseWriter, r *http.Request) { 209 ctx := r.Context() 210 id := path.Base(r.URL.Path) 211 datastore, ok := fs.(tusd.DataStore) 212 if !ok { 213 appctx.GetLogger(ctx).Error().Interface("fs", fs).Msg("storage is not a tus datastore") 214 return 215 } 216 upload, err := datastore.GetUpload(ctx, id) 217 if err != nil { 218 appctx.GetLogger(ctx).Error().Err(err).Msg("could not get upload from storage") 219 return 220 } 221 info, err := upload.GetInfo(ctx) 222 if err != nil { 223 appctx.GetLogger(ctx).Error().Err(err).Msg("could not get upload info for upload") 224 return 225 } 226 expires := info.MetaData["expires"] 227 if expires != "" { 228 w.Header().Set(net.HeaderTusUploadExpires, expires) 229 } 230 resourceid := &provider.ResourceId{ 231 StorageId: info.MetaData["providerID"], 232 SpaceId: info.Storage["SpaceRoot"], 233 OpaqueId: info.Storage["NodeId"], 234 } 235 w.Header().Set(net.HeaderOCFileID, storagespace.FormatResourceID(resourceid)) 236 } 237 238 // tusdLogger is a logger implementation (slog) for tusd that uses zerolog. 239 type tusdLogger struct { 240 log *zerolog.Logger 241 } 242 243 // Handle handles the record 244 func (l tusdLogger) Handle(_ context.Context, r slog.Record) error { 245 switch r.Level { 246 case slog.LevelDebug: 247 l.log.Debug().Msg(r.Message) 248 case slog.LevelInfo: 249 l.log.Info().Msg(r.Message) 250 case slog.LevelWarn: 251 l.log.Warn().Msg(r.Message) 252 case slog.LevelError: 253 l.log.Error().Msg(r.Message) 254 } 255 return nil 256 } 257 258 // Enabled returns true 259 func (l tusdLogger) Enabled(_ context.Context, _ slog.Level) bool { return true } 260 261 // WithAttrs creates a new logger with the given attributes 262 func (l tusdLogger) WithAttrs(attr []slog.Attr) slog.Handler { 263 fields := make(map[string]interface{}, len(attr)) 264 for _, a := range attr { 265 fields[a.Key] = a.Value 266 } 267 c := l.log.With().Fields(fields).Logger() 268 sLog := tusdLogger{log: &c} 269 return sLog 270 } 271 272 // WithGroup is not implemented 273 func (l tusdLogger) WithGroup(name string) slog.Handler { return l }