github.com/cs3org/reva/v2@v2.27.7/pkg/storage/utils/localfs/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 localfs
    20  
    21  import (
    22  	"context"
    23  	"encoding/json"
    24  	"io"
    25  	"os"
    26  	"path/filepath"
    27  
    28  	userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
    29  	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
    30  	"github.com/cs3org/reva/v2/pkg/appctx"
    31  	ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
    32  	"github.com/cs3org/reva/v2/pkg/errtypes"
    33  	"github.com/cs3org/reva/v2/pkg/storage"
    34  	"github.com/cs3org/reva/v2/pkg/storage/utils/chunking"
    35  	"github.com/cs3org/reva/v2/pkg/utils"
    36  	"github.com/google/uuid"
    37  	"github.com/pkg/errors"
    38  	tusd "github.com/tus/tusd/v2/pkg/handler"
    39  )
    40  
    41  var defaultFilePerm = os.FileMode(0664)
    42  
    43  func (fs *localfs) Upload(ctx context.Context, req storage.UploadRequest, uff storage.UploadFinishedFunc) (*provider.ResourceInfo, error) {
    44  	upload, err := fs.GetUpload(ctx, req.Ref.GetPath())
    45  	if err != nil {
    46  		return &provider.ResourceInfo{}, errors.Wrap(err, "localfs: error retrieving upload")
    47  	}
    48  
    49  	uploadInfo := upload.(*fileUpload)
    50  
    51  	p := uploadInfo.info.Storage["InternalDestination"]
    52  	if chunking.IsChunked(p) {
    53  		var assembledFile string
    54  		p, assembledFile, err = fs.chunkHandler.WriteChunk(p, req.Body)
    55  		if err != nil {
    56  			return &provider.ResourceInfo{}, err
    57  		}
    58  		if p == "" {
    59  			if err = uploadInfo.Terminate(ctx); err != nil {
    60  				return &provider.ResourceInfo{}, errors.Wrap(err, "localfs: error removing auxiliary files")
    61  			}
    62  			return &provider.ResourceInfo{}, errtypes.PartialContent(req.Ref.String())
    63  		}
    64  		uploadInfo.info.Storage["InternalDestination"] = p
    65  		fd, err := os.Open(assembledFile)
    66  		if err != nil {
    67  			return &provider.ResourceInfo{}, errors.Wrap(err, "localfs: error opening assembled file")
    68  		}
    69  		defer fd.Close()
    70  		defer os.RemoveAll(assembledFile)
    71  		req.Body = fd
    72  	}
    73  
    74  	if _, err := uploadInfo.WriteChunk(ctx, 0, req.Body); err != nil {
    75  		return &provider.ResourceInfo{}, errors.Wrap(err, "localfs: error writing to binary file")
    76  	}
    77  
    78  	if err := uploadInfo.FinishUpload(ctx); err != nil {
    79  		return &provider.ResourceInfo{}, err
    80  	}
    81  
    82  	if uff != nil {
    83  		info := uploadInfo.info
    84  		uploadRef := &provider.Reference{
    85  			ResourceId: &provider.ResourceId{
    86  				StorageId: info.MetaData["providerID"],
    87  				SpaceId:   info.Storage["SpaceRoot"],
    88  				OpaqueId:  info.Storage["SpaceRoot"],
    89  			},
    90  			Path: utils.MakeRelativePath(filepath.Join(info.MetaData["dir"], info.MetaData["filename"])),
    91  		}
    92  		owner, ok := ctxpkg.ContextGetUser(uploadInfo.ctx)
    93  		if !ok {
    94  			return &provider.ResourceInfo{}, errtypes.PreconditionFailed("error getting user from uploadinfo context")
    95  		}
    96  		// spaces support in localfs needs to be revisited:
    97  		// * info.Storage["SpaceRoot"] is never set
    98  		// * there is no space owner or manager that could be passed to the UploadFinishedFunc
    99  		uff(owner.Id, owner.Id, uploadRef)
   100  	}
   101  
   102  	// return id, etag and mtime
   103  	ri, err := fs.GetMD(ctx, req.Ref, []string{}, []string{"id", "etag", "mtime"})
   104  	if err != nil {
   105  		return &provider.ResourceInfo{}, err
   106  	}
   107  
   108  	return ri, nil
   109  }
   110  
   111  // InitiateUpload returns upload ids corresponding to different protocols it supports
   112  // It resolves the resource and then reuses the NewUpload function
   113  // Currently requires the uploadLength to be set
   114  // TODO to implement LengthDeferrerDataStore make size optional
   115  // TODO read optional content for small files in this request
   116  func (fs *localfs) InitiateUpload(ctx context.Context, ref *provider.Reference, uploadLength int64, metadata map[string]string) (map[string]string, error) {
   117  
   118  	np, err := fs.resolve(ctx, ref)
   119  	if err != nil {
   120  		return nil, errors.Wrap(err, "localfs: error resolving reference")
   121  	}
   122  
   123  	info := tusd.FileInfo{
   124  		MetaData: tusd.MetaData{
   125  			"filename": filepath.Base(np),
   126  			"dir":      filepath.Dir(np),
   127  		},
   128  		Size: uploadLength,
   129  	}
   130  
   131  	if metadata != nil {
   132  		info.MetaData["providerID"] = metadata["providerID"]
   133  		if metadata["mtime"] != "" {
   134  			info.MetaData["mtime"] = metadata["mtime"]
   135  		}
   136  		if _, ok := metadata["sizedeferred"]; ok {
   137  			info.SizeIsDeferred = true
   138  		}
   139  	}
   140  
   141  	upload, err := fs.NewUpload(ctx, info)
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  
   146  	info, _ = upload.GetInfo(ctx)
   147  
   148  	return map[string]string{
   149  		"simple": info.ID,
   150  		"tus":    info.ID,
   151  	}, nil
   152  }
   153  
   154  // UseIn tells the tus upload middleware which extensions it supports.
   155  func (fs *localfs) UseIn(composer *tusd.StoreComposer) {
   156  	composer.UseCore(fs)
   157  	composer.UseTerminater(fs)
   158  	// TODO composer.UseConcater(fs)
   159  	// TODO composer.UseLengthDeferrer(fs)
   160  }
   161  
   162  // NewUpload creates a new upload using the size as the file's length. To determine where to write the binary data
   163  // the Fileinfo metadata must contain a dir and a filename.
   164  // returns a unique id which is used to identify the upload. The properties Size and MetaData will be filled.
   165  func (fs *localfs) NewUpload(ctx context.Context, info tusd.FileInfo) (upload tusd.Upload, err error) {
   166  
   167  	log := appctx.GetLogger(ctx)
   168  	log.Debug().Interface("info", info).Msg("localfs: NewUpload")
   169  
   170  	fn := info.MetaData["filename"]
   171  	if fn == "" {
   172  		return nil, errors.New("localfs: missing filename in metadata")
   173  	}
   174  	info.MetaData["filename"] = filepath.Clean(info.MetaData["filename"])
   175  
   176  	dir := info.MetaData["dir"]
   177  	if dir == "" {
   178  		return nil, errors.New("localfs: missing dir in metadata")
   179  	}
   180  	info.MetaData["dir"] = filepath.Clean(info.MetaData["dir"])
   181  
   182  	np := fs.wrap(ctx, filepath.Join(info.MetaData["dir"], info.MetaData["filename"]))
   183  
   184  	log.Debug().Interface("info", info).Msg("localfs: resolved filename")
   185  
   186  	info.ID = uuid.New().String()
   187  
   188  	binPath, err := fs.getUploadPath(ctx, info.ID)
   189  	if err != nil {
   190  		return nil, errors.Wrap(err, "localfs: error resolving upload path")
   191  	}
   192  	usr := ctxpkg.ContextMustGetUser(ctx)
   193  	info.Storage = map[string]string{
   194  		"Type":                "LocalStore",
   195  		"BinPath":             binPath,
   196  		"InternalDestination": np,
   197  
   198  		"Idp":      usr.Id.Idp,
   199  		"UserId":   usr.Id.OpaqueId,
   200  		"UserName": usr.Username,
   201  		"UserType": utils.UserTypeToString(usr.Id.Type),
   202  
   203  		"LogLevel": log.GetLevel().String(),
   204  	}
   205  	// Create binary file with no content
   206  	file, err := os.OpenFile(binPath, os.O_CREATE|os.O_WRONLY, defaultFilePerm)
   207  	if err != nil {
   208  		return nil, err
   209  	}
   210  	defer file.Close()
   211  
   212  	u := &fileUpload{
   213  		info:     info,
   214  		binPath:  binPath,
   215  		infoPath: binPath + ".info",
   216  		fs:       fs,
   217  		ctx:      ctx,
   218  	}
   219  
   220  	// writeInfo creates the file by itself if necessary
   221  	err = u.writeInfo()
   222  	if err != nil {
   223  		return nil, err
   224  	}
   225  
   226  	return u, nil
   227  }
   228  
   229  func (fs *localfs) getUploadPath(ctx context.Context, uploadID string) (string, error) {
   230  	return filepath.Join(fs.conf.Uploads, uploadID), nil
   231  }
   232  
   233  // GetUpload returns the Upload for the given upload id
   234  func (fs *localfs) GetUpload(ctx context.Context, id string) (tusd.Upload, error) {
   235  	binPath, err := fs.getUploadPath(ctx, id)
   236  	if err != nil {
   237  		return nil, err
   238  	}
   239  	infoPath := binPath + ".info"
   240  	info := tusd.FileInfo{}
   241  	data, err := os.ReadFile(infoPath)
   242  	if err != nil {
   243  		if os.IsNotExist(err) {
   244  			// Interpret os.ErrNotExist as 404 Not Found
   245  			err = tusd.ErrNotFound
   246  		}
   247  		return nil, err
   248  	}
   249  	if err := json.Unmarshal(data, &info); err != nil {
   250  		return nil, err
   251  	}
   252  
   253  	stat, err := os.Stat(binPath)
   254  	if err != nil {
   255  		return nil, err
   256  	}
   257  
   258  	info.Offset = stat.Size()
   259  
   260  	u := &userpb.User{
   261  		Id: &userpb.UserId{
   262  			Idp:      info.Storage["Idp"],
   263  			OpaqueId: info.Storage["UserId"],
   264  			Type:     utils.UserTypeMap(info.Storage["UserType"]),
   265  		},
   266  		Username: info.Storage["UserName"],
   267  	}
   268  
   269  	ctx = ctxpkg.ContextSetUser(ctx, u)
   270  
   271  	return &fileUpload{
   272  		info:     info,
   273  		binPath:  binPath,
   274  		infoPath: infoPath,
   275  		fs:       fs,
   276  		ctx:      ctx,
   277  	}, nil
   278  }
   279  
   280  type fileUpload struct {
   281  	// info stores the current information about the upload
   282  	info tusd.FileInfo
   283  	// infoPath is the path to the .info file
   284  	infoPath string
   285  	// binPath is the path to the binary file (which has no extension)
   286  	binPath string
   287  	// only fs knows how to handle metadata and versions
   288  	fs *localfs
   289  	// a context with a user
   290  	ctx context.Context
   291  }
   292  
   293  // GetInfo returns the FileInfo
   294  func (upload *fileUpload) GetInfo(ctx context.Context) (tusd.FileInfo, error) {
   295  	return upload.info, nil
   296  }
   297  
   298  // GetReader returns an io.Reader for the upload
   299  func (upload *fileUpload) GetReader(ctx context.Context) (io.ReadCloser, error) {
   300  	return os.Open(upload.binPath)
   301  }
   302  
   303  // WriteChunk writes the stream from the reader to the given offset of the upload
   304  func (upload *fileUpload) WriteChunk(ctx context.Context, offset int64, src io.Reader) (int64, error) {
   305  	file, err := os.OpenFile(upload.binPath, os.O_WRONLY|os.O_APPEND, defaultFilePerm)
   306  	if err != nil {
   307  		return 0, err
   308  	}
   309  	defer file.Close()
   310  
   311  	n, err := io.Copy(file, src)
   312  
   313  	// If the HTTP PATCH request gets interrupted in the middle (e.g. because
   314  	// the user wants to pause the upload), Go's net/http returns an io.ErrUnexpectedEOF.
   315  	// However, for OwnCloudStore it's not important whether the stream has ended
   316  	// on purpose or accidentally.
   317  	if err != nil {
   318  		if err != io.ErrUnexpectedEOF {
   319  			return n, err
   320  		}
   321  	}
   322  
   323  	upload.info.Offset += n
   324  	err = upload.writeInfo()
   325  
   326  	return n, err
   327  }
   328  
   329  // writeInfo updates the entire information. Everything will be overwritten.
   330  func (upload *fileUpload) writeInfo() error {
   331  	data, err := json.Marshal(upload.info)
   332  	if err != nil {
   333  		return err
   334  	}
   335  	return os.WriteFile(upload.infoPath, data, defaultFilePerm)
   336  }
   337  
   338  // FinishUpload finishes an upload and moves the file to the internal destination
   339  func (upload *fileUpload) FinishUpload(ctx context.Context) error {
   340  
   341  	np := upload.info.Storage["InternalDestination"]
   342  
   343  	// TODO check etag with If-Match header
   344  	// if destination exists
   345  	// if _, err := os.Stat(np); err == nil {
   346  	// the local storage does not store metadata
   347  	// the fileid is based on the path, so no we do not need to copy it to the new file
   348  	// the local storage does not track revisions
   349  	//}
   350  
   351  	// if destination exists
   352  	if _, err := os.Stat(np); err == nil {
   353  		// create revision
   354  		if err := upload.fs.archiveRevision(upload.ctx, np); err != nil {
   355  			return err
   356  		}
   357  	}
   358  
   359  	err := os.Rename(upload.binPath, np)
   360  	if err != nil {
   361  		return err
   362  	}
   363  
   364  	// only delete the upload if it was successfully written to the fs
   365  	if err := os.Remove(upload.infoPath); err != nil {
   366  		if !os.IsNotExist(err) {
   367  			log := appctx.GetLogger(ctx)
   368  			log.Err(err).Interface("info", upload.info).Msg("localfs: could not delete upload info")
   369  		}
   370  	}
   371  
   372  	// TODO: set mtime if specified in metadata
   373  
   374  	// metadata propagation is left to the storage implementation
   375  	return err
   376  }
   377  
   378  // To implement the termination extension as specified in https://tus.io/protocols/resumable-upload.html#termination
   379  // - the storage needs to implement AsTerminatableUpload
   380  // - the upload needs to implement Terminate
   381  
   382  // AsTerminatableUpload returns a a TerminatableUpload
   383  func (fs *localfs) AsTerminatableUpload(upload tusd.Upload) tusd.TerminatableUpload {
   384  	return upload.(*fileUpload)
   385  }
   386  
   387  // Terminate terminates the upload
   388  func (upload *fileUpload) Terminate(ctx context.Context) error {
   389  	if err := os.Remove(upload.infoPath); err != nil {
   390  		return err
   391  	}
   392  	if err := os.Remove(upload.binPath); err != nil {
   393  		return err
   394  	}
   395  	return nil
   396  }