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 }