github.com/cs3org/reva/v2@v2.27.7/pkg/storage/fs/owncloudsql/upload.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 owncloudsql
    20  
    21  import (
    22  	"context"
    23  	"encoding/json"
    24  	"fmt"
    25  	"io"
    26  	iofs "io/fs"
    27  	"os"
    28  	"path/filepath"
    29  	"strconv"
    30  	"time"
    31  
    32  	userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
    33  	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
    34  	"github.com/cs3org/reva/v2/pkg/appctx"
    35  	"github.com/cs3org/reva/v2/pkg/conversions"
    36  	ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
    37  	"github.com/cs3org/reva/v2/pkg/errtypes"
    38  	"github.com/cs3org/reva/v2/pkg/logger"
    39  	"github.com/cs3org/reva/v2/pkg/mime"
    40  	"github.com/cs3org/reva/v2/pkg/storage"
    41  	"github.com/cs3org/reva/v2/pkg/storage/utils/chunking"
    42  	"github.com/cs3org/reva/v2/pkg/storage/utils/templates"
    43  	"github.com/cs3org/reva/v2/pkg/utils"
    44  	"github.com/google/uuid"
    45  	"github.com/pkg/errors"
    46  	"github.com/rs/zerolog/log"
    47  	tusd "github.com/tus/tusd/v2/pkg/handler"
    48  )
    49  
    50  var defaultFilePerm = os.FileMode(0664)
    51  
    52  func (fs *owncloudsqlfs) Upload(ctx context.Context, req storage.UploadRequest, uff storage.UploadFinishedFunc) (*provider.ResourceInfo, error) {
    53  	upload, err := fs.GetUpload(ctx, req.Ref.GetPath())
    54  	if err != nil {
    55  		return &provider.ResourceInfo{}, errors.Wrap(err, "owncloudsql: error retrieving upload")
    56  	}
    57  
    58  	uploadInfo := upload.(*fileUpload)
    59  
    60  	p := uploadInfo.info.Storage["InternalDestination"]
    61  	if chunking.IsChunked(p) {
    62  		var assembledFile string
    63  		p, assembledFile, err = fs.chunkHandler.WriteChunk(p, req.Body)
    64  		if err != nil {
    65  			return &provider.ResourceInfo{}, err
    66  		}
    67  		if p == "" {
    68  			if err = uploadInfo.Terminate(ctx); err != nil {
    69  				return &provider.ResourceInfo{}, errors.Wrap(err, "owncloudsql: error removing auxiliary files")
    70  			}
    71  			return &provider.ResourceInfo{}, errtypes.PartialContent(req.Ref.String())
    72  		}
    73  		uploadInfo.info.Storage["InternalDestination"] = p
    74  		fd, err := os.Open(assembledFile)
    75  		if err != nil {
    76  			return &provider.ResourceInfo{}, errors.Wrap(err, "owncloudsql: error opening assembled file")
    77  		}
    78  		defer fd.Close()
    79  		defer os.RemoveAll(assembledFile)
    80  		req.Body = fd
    81  	}
    82  
    83  	if _, err := uploadInfo.WriteChunk(ctx, 0, req.Body); err != nil {
    84  		return &provider.ResourceInfo{}, errors.Wrap(err, "owncloudsql: error writing to binary file")
    85  	}
    86  
    87  	if err := uploadInfo.FinishUpload(ctx); err != nil {
    88  		return &provider.ResourceInfo{}, err
    89  	}
    90  
    91  	if uff != nil {
    92  		info := uploadInfo.info
    93  		uploadRef := &provider.Reference{
    94  			ResourceId: &provider.ResourceId{
    95  				StorageId: info.MetaData["providerID"],
    96  				SpaceId:   info.Storage["SpaceRoot"],
    97  				OpaqueId:  info.Storage["SpaceRoot"],
    98  			},
    99  			Path: utils.MakeRelativePath(filepath.Join(info.MetaData["dir"], info.MetaData["filename"])),
   100  		}
   101  		owner, ok := ctxpkg.ContextGetUser(uploadInfo.ctx)
   102  		if !ok {
   103  			return &provider.ResourceInfo{}, errtypes.PreconditionFailed("error getting user from uploadinfo context")
   104  		}
   105  		// spaces support in localfs needs to be revisited:
   106  		// * info.Storage["SpaceRoot"] is never set
   107  		// * there is no space owner or manager that could be passed to the UploadFinishedFunc
   108  		uff(owner.Id, owner.Id, uploadRef)
   109  	}
   110  
   111  	ri := &provider.ResourceInfo{
   112  		// fill with at least fileid, mtime and etag
   113  		Id: &provider.ResourceId{
   114  			StorageId: uploadInfo.info.MetaData["providerID"],
   115  			SpaceId:   uploadInfo.info.Storage["StorageId"],
   116  			OpaqueId:  uploadInfo.info.Storage["fileid"],
   117  		},
   118  		Etag: uploadInfo.info.MetaData["etag"],
   119  	}
   120  
   121  	if mtime, err := utils.MTimeToTS(uploadInfo.info.MetaData["mtime"]); err == nil {
   122  		ri.Mtime = &mtime
   123  	}
   124  
   125  	return ri, nil
   126  }
   127  
   128  // InitiateUpload returns upload ids corresponding to different protocols it supports
   129  // TODO read optional content for small files in this request
   130  func (fs *owncloudsqlfs) InitiateUpload(ctx context.Context, ref *provider.Reference, uploadLength int64, metadata map[string]string) (map[string]string, error) {
   131  	ip, err := fs.resolve(ctx, ref)
   132  	if err != nil {
   133  		return nil, errors.Wrap(err, "owncloudsql: error resolving reference")
   134  	}
   135  
   136  	// permissions are checked in NewUpload below
   137  
   138  	p := fs.toStoragePath(ctx, ip)
   139  
   140  	info := tusd.FileInfo{
   141  		MetaData: tusd.MetaData{
   142  			"filename": filepath.Base(p),
   143  			"dir":      filepath.Dir(p),
   144  			"mtime":    strconv.FormatInt(time.Now().Unix(), 10),
   145  		},
   146  		Size: uploadLength,
   147  	}
   148  
   149  	if metadata != nil {
   150  		info.MetaData["providerID"] = metadata["providerID"]
   151  		if metadata["mtime"] != "" {
   152  			info.MetaData["mtime"] = metadata["mtime"]
   153  		}
   154  		if _, ok := metadata["sizedeferred"]; ok {
   155  			info.SizeIsDeferred = true
   156  		}
   157  	}
   158  
   159  	upload, err := fs.NewUpload(ctx, info)
   160  	if err != nil {
   161  		return nil, err
   162  	}
   163  
   164  	info, _ = upload.GetInfo(ctx)
   165  
   166  	return map[string]string{
   167  		"simple": info.ID,
   168  		"tus":    info.ID,
   169  	}, nil
   170  }
   171  
   172  // UseIn tells the tus upload middleware which extensions it supports.
   173  func (fs *owncloudsqlfs) UseIn(composer *tusd.StoreComposer) {
   174  	composer.UseCore(fs)
   175  	composer.UseTerminater(fs)
   176  	composer.UseConcater(fs)
   177  	composer.UseLengthDeferrer(fs)
   178  }
   179  
   180  // To implement the core tus.io protocol as specified in https://tus.io/protocols/resumable-upload.html#core-protocol
   181  // - the storage needs to implement NewUpload and GetUpload
   182  // - the upload needs to implement the tusd.Upload interface: WriteChunk, GetInfo, GetReader and FinishUpload
   183  
   184  func (fs *owncloudsqlfs) NewUpload(ctx context.Context, info tusd.FileInfo) (upload tusd.Upload, err error) {
   185  
   186  	log := appctx.GetLogger(ctx)
   187  	log.Debug().Interface("info", info).Msg("owncloudsql: NewUpload")
   188  
   189  	if info.MetaData["filename"] == "" {
   190  		return nil, errors.New("owncloudsql: missing filename in metadata")
   191  	}
   192  	info.MetaData["filename"] = filepath.Clean(info.MetaData["filename"])
   193  
   194  	dir := info.MetaData["dir"]
   195  	if dir == "" {
   196  		return nil, errors.New("owncloudsql: missing dir in metadata")
   197  	}
   198  	info.MetaData["dir"] = filepath.Clean(info.MetaData["dir"])
   199  
   200  	ip := fs.toInternalPath(ctx, filepath.Join(info.MetaData["dir"], info.MetaData["filename"]))
   201  
   202  	// check permissions
   203  	var perm *provider.ResourcePermissions
   204  	var perr error
   205  	var fsInfo iofs.FileInfo
   206  	// if destination exists
   207  	if fsInfo, err = os.Stat(ip); err == nil {
   208  		// check permissions of file to be overwritten
   209  		perm, perr = fs.readPermissions(ctx, ip)
   210  	} else {
   211  		// check permissions of parent folder
   212  		perm, perr = fs.readPermissions(ctx, filepath.Dir(ip))
   213  	}
   214  	if perr == nil {
   215  		if !perm.InitiateFileUpload {
   216  			return nil, errtypes.PermissionDenied("")
   217  		}
   218  	} else {
   219  		if os.IsNotExist(err) {
   220  			return nil, errtypes.NotFound(fs.toStoragePath(ctx, filepath.Dir(ip)))
   221  		}
   222  		return nil, errors.Wrap(err, "owncloudsql: error reading permissions")
   223  	}
   224  
   225  	// if we are trying to overwriting a folder with a file
   226  	if fsInfo != nil && fsInfo.IsDir() {
   227  		return nil, errtypes.PreconditionFailed("resource is not a file")
   228  	}
   229  
   230  	log.Debug().Interface("info", info).Msg("owncloudsql: resolved filename")
   231  
   232  	info.ID = uuid.New().String()
   233  
   234  	binPath, err := fs.getUploadPath(ctx, info.ID)
   235  	if err != nil {
   236  		return nil, errors.Wrap(err, "owncloudsql: error resolving upload path")
   237  	}
   238  	usr := ctxpkg.ContextMustGetUser(ctx)
   239  	storageID, err := fs.getStorage(ctx, ip)
   240  	if err != nil {
   241  		return nil, err
   242  	}
   243  	info.Storage = map[string]string{
   244  		"Type":                "OwnCloudStore",
   245  		"BinPath":             binPath,
   246  		"InternalDestination": ip,
   247  		"Permissions":         strconv.Itoa((int)(conversions.RoleFromResourcePermissions(perm, false).OCSPermissions())),
   248  
   249  		"Idp":      usr.Id.Idp,
   250  		"UserId":   usr.Id.OpaqueId,
   251  		"UserName": usr.Username,
   252  
   253  		"LogLevel": log.GetLevel().String(),
   254  
   255  		"StorageId": strconv.Itoa(storageID),
   256  	}
   257  	// Create binary file in the upload folder with no content
   258  	log.Debug().Interface("info", info).Msg("owncloudsql: built storage info")
   259  	file, err := os.OpenFile(binPath, os.O_CREATE|os.O_WRONLY, defaultFilePerm)
   260  	if err != nil {
   261  		return nil, err
   262  	}
   263  	defer file.Close()
   264  
   265  	u := &fileUpload{
   266  		info:     info,
   267  		binPath:  binPath,
   268  		infoPath: filepath.Join(fs.c.UploadInfoDir, info.ID+".info"),
   269  		fs:       fs,
   270  		ctx:      ctx,
   271  	}
   272  
   273  	// writeInfo creates the file by itself if necessary
   274  	err = u.writeInfo()
   275  	if err != nil {
   276  		return nil, err
   277  	}
   278  
   279  	return u, nil
   280  }
   281  
   282  func (fs *owncloudsqlfs) getUploadPath(ctx context.Context, uploadID string) (string, error) {
   283  	u, ok := ctxpkg.ContextGetUser(ctx)
   284  	if !ok {
   285  		err := errors.Wrap(errtypes.UserRequired("userrequired"), "error getting user from ctx")
   286  		return "", err
   287  	}
   288  	layout := templates.WithUser(u, fs.c.UserLayout)
   289  	return filepath.Join(fs.c.DataDirectory, layout, "uploads", uploadID), nil
   290  }
   291  
   292  // GetUpload returns the Upload for the given upload id
   293  func (fs *owncloudsqlfs) GetUpload(ctx context.Context, id string) (tusd.Upload, error) {
   294  	infoPath := filepath.Join(fs.c.UploadInfoDir, id+".info")
   295  
   296  	info := tusd.FileInfo{}
   297  	data, err := os.ReadFile(infoPath)
   298  	if err != nil {
   299  		if os.IsNotExist(err) {
   300  			// Interpret os.ErrNotExist as 404 Not Found
   301  			err = tusd.ErrNotFound
   302  		}
   303  		return nil, err
   304  	}
   305  	if err := json.Unmarshal(data, &info); err != nil {
   306  		return nil, err
   307  	}
   308  
   309  	stat, err := os.Stat(info.Storage["BinPath"])
   310  	if err != nil {
   311  		return nil, err
   312  	}
   313  
   314  	info.Offset = stat.Size()
   315  
   316  	u := &userpb.User{
   317  		Id: &userpb.UserId{
   318  			Idp:      info.Storage["Idp"],
   319  			OpaqueId: info.Storage["UserId"],
   320  		},
   321  		Username: info.Storage["UserName"],
   322  	}
   323  
   324  	ctx = ctxpkg.ContextSetUser(ctx, u)
   325  	// TODO configure the logger the same way ... store and add traceid in file info
   326  
   327  	var opts []logger.Option
   328  	opts = append(opts, logger.WithLevel(info.Storage["LogLevel"]))
   329  	opts = append(opts, logger.WithWriter(os.Stderr, logger.ConsoleMode))
   330  	l := logger.New(opts...)
   331  
   332  	sub := l.With().Int("pid", os.Getpid()).Logger()
   333  
   334  	ctx = appctx.WithLogger(ctx, &sub)
   335  
   336  	return &fileUpload{
   337  		info:     info,
   338  		binPath:  info.Storage["BinPath"],
   339  		infoPath: infoPath,
   340  		fs:       fs,
   341  		ctx:      ctx,
   342  	}, nil
   343  }
   344  
   345  type fileUpload struct {
   346  	// info stores the current information about the upload
   347  	info tusd.FileInfo
   348  	// infoPath is the path to the .info file
   349  	infoPath string
   350  	// binPath is the path to the binary file (which has no extension)
   351  	binPath string
   352  	// only fs knows how to handle metadata and versions
   353  	fs *owncloudsqlfs
   354  	// a context with a user
   355  	// TODO add logger as well?
   356  	ctx context.Context
   357  }
   358  
   359  // GetInfo returns the FileInfo
   360  func (upload *fileUpload) GetInfo(ctx context.Context) (tusd.FileInfo, error) {
   361  	return upload.info, nil
   362  }
   363  
   364  // WriteChunk writes the stream from the reader to the given offset of the upload
   365  func (upload *fileUpload) WriteChunk(ctx context.Context, offset int64, src io.Reader) (int64, error) {
   366  	file, err := os.OpenFile(upload.binPath, os.O_WRONLY|os.O_APPEND, defaultFilePerm)
   367  	if err != nil {
   368  		return 0, err
   369  	}
   370  	defer file.Close()
   371  
   372  	n, err := io.Copy(file, src)
   373  
   374  	// If the HTTP PATCH request gets interrupted in the middle (e.g. because
   375  	// the user wants to pause the upload), Go's net/http returns an io.ErrUnexpectedEOF.
   376  	// However, for OwnCloudStore it's not important whether the stream has ended
   377  	// on purpose or accidentally.
   378  	if err != nil {
   379  		if err != io.ErrUnexpectedEOF {
   380  			return n, err
   381  		}
   382  	}
   383  
   384  	upload.info.Offset += n
   385  	err = upload.writeInfo() // TODO info is written here ... we need to truncate in DiscardChunk
   386  
   387  	return n, err
   388  }
   389  
   390  // GetReader returns an io.Reader for the upload
   391  func (upload *fileUpload) GetReader(ctx context.Context) (io.ReadCloser, error) {
   392  	return os.Open(upload.binPath)
   393  }
   394  
   395  // writeInfo updates the entire information. Everything will be overwritten.
   396  func (upload *fileUpload) writeInfo() error {
   397  	log.Debug().Str("path", upload.infoPath).Msg("Writing info file")
   398  	data, err := json.Marshal(upload.info)
   399  	if err != nil {
   400  		return err
   401  	}
   402  	return os.WriteFile(upload.infoPath, data, defaultFilePerm)
   403  }
   404  
   405  // FinishUpload finishes an upload and moves the file to the internal destination
   406  func (upload *fileUpload) FinishUpload(ctx context.Context) error {
   407  
   408  	ip := upload.info.Storage["InternalDestination"]
   409  
   410  	// if destination exists
   411  	// TODO check etag with If-Match header
   412  	if _, err := os.Stat(ip); err == nil {
   413  		// create revision
   414  		if err := upload.fs.archiveRevision(upload.ctx, upload.fs.getVersionsPath(upload.ctx, ip), ip); err != nil {
   415  			return err
   416  		}
   417  	}
   418  
   419  	sha1h, md5h, adler32h, err := upload.fs.HashFile(upload.binPath)
   420  	if err != nil {
   421  		log.Err(err).Msg("owncloudsql: could not open file for checksumming")
   422  	}
   423  
   424  	err = os.Rename(upload.binPath, ip)
   425  	if err != nil {
   426  		log.Err(err).Interface("info", upload.info).
   427  			Str("binPath", upload.binPath).
   428  			Str("ipath", ip).
   429  			Msg("owncloudsql: could not rename")
   430  		return err
   431  	}
   432  
   433  	var fi os.FileInfo
   434  	fi, err = os.Stat(ip)
   435  	if err != nil {
   436  		return err
   437  	}
   438  
   439  	perms, err := strconv.Atoi(upload.info.Storage["Permissions"])
   440  	if err != nil {
   441  		return err
   442  	}
   443  
   444  	if upload.info.MetaData["mtime"] == "" {
   445  		upload.info.MetaData["mtime"] = fmt.Sprintf("%d", fi.ModTime().Unix())
   446  	}
   447  	if upload.info.MetaData["etag"] == "" {
   448  		upload.info.MetaData["etag"] = calcEtag(upload.ctx, fi)
   449  	}
   450  
   451  	data := map[string]interface{}{
   452  		"path":          upload.fs.toDatabasePath(ip),
   453  		"checksum":      fmt.Sprintf("SHA1:%032x MD5:%032x ADLER32:%032x", sha1h, md5h, adler32h),
   454  		"etag":          upload.info.MetaData["etag"],
   455  		"size":          upload.info.Size,
   456  		"mimetype":      mime.Detect(false, ip),
   457  		"permissions":   perms,
   458  		"mtime":         upload.info.MetaData["mtime"],
   459  		"storage_mtime": upload.info.MetaData["mtime"],
   460  	}
   461  	var fileid int
   462  	fileid, err = upload.fs.filecache.InsertOrUpdate(ctx, upload.info.Storage["StorageId"], data, false)
   463  	if err != nil {
   464  		return err
   465  	}
   466  	upload.info.Storage["fileid"] = fmt.Sprintf("%d", fileid)
   467  
   468  	// only delete the upload if it was successfully written to the storage
   469  	if err := os.Remove(upload.infoPath); err != nil {
   470  		if !os.IsNotExist(err) {
   471  			log.Err(err).Interface("info", upload.info).Msg("owncloudsql: could not delete upload info")
   472  			return err
   473  		}
   474  	}
   475  
   476  	return upload.fs.propagate(upload.ctx, ip)
   477  }
   478  
   479  // To implement the termination extension as specified in https://tus.io/protocols/resumable-upload.html#termination
   480  // - the storage needs to implement AsTerminatableUpload
   481  // - the upload needs to implement Terminate
   482  
   483  // AsTerminatableUpload returns a TerminatableUpload
   484  func (fs *owncloudsqlfs) AsTerminatableUpload(upload tusd.Upload) tusd.TerminatableUpload {
   485  	return upload.(*fileUpload)
   486  }
   487  
   488  // Terminate terminates the upload
   489  func (upload *fileUpload) Terminate(ctx context.Context) error {
   490  	if err := os.Remove(upload.infoPath); err != nil {
   491  		if !os.IsNotExist(err) {
   492  			return err
   493  		}
   494  	}
   495  	if err := os.Remove(upload.binPath); err != nil {
   496  		if !os.IsNotExist(err) {
   497  			return err
   498  		}
   499  	}
   500  	return nil
   501  }
   502  
   503  // To implement the creation-defer-length extension as specified in https://tus.io/protocols/resumable-upload.html#creation
   504  // - the storage needs to implement AsLengthDeclarableUpload
   505  // - the upload needs to implement DeclareLength
   506  
   507  // AsLengthDeclarableUpload returns a LengthDeclarableUpload
   508  func (fs *owncloudsqlfs) AsLengthDeclarableUpload(upload tusd.Upload) tusd.LengthDeclarableUpload {
   509  	return upload.(*fileUpload)
   510  }
   511  
   512  // DeclareLength updates the upload length information
   513  func (upload *fileUpload) DeclareLength(ctx context.Context, length int64) error {
   514  	upload.info.Size = length
   515  	upload.info.SizeIsDeferred = false
   516  	return upload.writeInfo()
   517  }
   518  
   519  // To implement the concatenation extension as specified in https://tus.io/protocols/resumable-upload.html#concatenation
   520  // - the storage needs to implement AsConcatableUpload
   521  // - the upload needs to implement ConcatUploads
   522  
   523  // AsConcatableUpload returns a ConcatableUpload
   524  func (fs *owncloudsqlfs) AsConcatableUpload(upload tusd.Upload) tusd.ConcatableUpload {
   525  	return upload.(*fileUpload)
   526  }
   527  
   528  // ConcatUploads concatenates multiple uploads
   529  func (upload *fileUpload) ConcatUploads(ctx context.Context, uploads []tusd.Upload) (err error) {
   530  	file, err := os.OpenFile(upload.binPath, os.O_WRONLY|os.O_APPEND, defaultFilePerm)
   531  	if err != nil {
   532  		return err
   533  	}
   534  	defer file.Close()
   535  
   536  	for _, partialUpload := range uploads {
   537  		fileUpload := partialUpload.(*fileUpload)
   538  
   539  		src, err := os.Open(fileUpload.binPath)
   540  		if err != nil {
   541  			return err
   542  		}
   543  
   544  		if _, err := io.Copy(file, src); err != nil {
   545  			return err
   546  		}
   547  	}
   548  
   549  	return
   550  }