github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/files/files.go (about)

     1  // Package files is the HTTP frontend of the vfs package. It exposes
     2  // an HTTP api to manipulate the filesystem and offer all the
     3  // possibilities given by the vfs.
     4  package files
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/base64"
     9  	"encoding/json"
    10  	"errors"
    11  	"fmt"
    12  	"io"
    13  	"math"
    14  	"net/http"
    15  	"net/url"
    16  	"os"
    17  	"path"
    18  	"path/filepath"
    19  	"strconv"
    20  	"strings"
    21  	"time"
    22  
    23  	"github.com/cozy/cozy-stack/model/instance"
    24  	"github.com/cozy/cozy-stack/model/job"
    25  	"github.com/cozy/cozy-stack/model/note"
    26  	"github.com/cozy/cozy-stack/model/oauth"
    27  	"github.com/cozy/cozy-stack/model/permission"
    28  	"github.com/cozy/cozy-stack/model/sharing"
    29  	"github.com/cozy/cozy-stack/model/vfs"
    30  	"github.com/cozy/cozy-stack/pkg/assets/statik"
    31  	"github.com/cozy/cozy-stack/pkg/config/config"
    32  	"github.com/cozy/cozy-stack/pkg/consts"
    33  	"github.com/cozy/cozy-stack/pkg/couchdb"
    34  	"github.com/cozy/cozy-stack/pkg/jsonapi"
    35  	"github.com/cozy/cozy-stack/pkg/limits"
    36  	"github.com/cozy/cozy-stack/pkg/logger"
    37  	"github.com/cozy/cozy-stack/pkg/metadata"
    38  	"github.com/cozy/cozy-stack/pkg/utils"
    39  	"github.com/cozy/cozy-stack/web/middlewares"
    40  	"github.com/cozy/cozy-stack/worker/thumbnail"
    41  	"github.com/labstack/echo/v4"
    42  	"github.com/ncw/swift/v2"
    43  )
    44  
    45  type docPatch struct {
    46  	docID   string
    47  	docPath string
    48  
    49  	Trash  bool `json:"move_to_trash,omitempty"`
    50  	Delete bool `json:"permanent_delete,omitempty"`
    51  	vfs.DocPatch
    52  }
    53  
    54  // TagSeparator is the character separating tags
    55  const TagSeparator = ","
    56  
    57  // ErrDocTypeInvalid is used when the document type sent is not
    58  // recognized
    59  var ErrDocTypeInvalid = errors.New("Invalid document type")
    60  
    61  // SharedDrivesCreationHandler is the handler for POST /files/drives. It
    62  // creates the directory where shared and external drives are saved if it
    63  // doesn't exist, and return information about this directory.
    64  func SharedDrivesCreationHandler(c echo.Context) error {
    65  	inst := middlewares.GetInstance(c)
    66  	if err := middlewares.AllowWholeType(c, permission.POST, consts.Files); err != nil {
    67  		return err
    68  	}
    69  	doc, err := inst.EnsureSharedDrivesDir()
    70  	if err != nil {
    71  		return wrapVfsError(err)
    72  	}
    73  	return jsonapi.Data(c, http.StatusOK, newDir(doc), nil)
    74  }
    75  
    76  // CreationHandler handle all POST requests on /files/:file-id
    77  // aiming at creating a new document in the FS. Given the Type
    78  // parameter of the request, it will either upload a new file or
    79  // create a new directory.
    80  func CreationHandler(c echo.Context) error {
    81  	instance := middlewares.GetInstance(c)
    82  	var doc jsonapi.Object
    83  	var err error
    84  	switch c.QueryParam("Type") {
    85  	case consts.FileType:
    86  		doc, err = createFileHandler(c, instance.VFS())
    87  	case consts.DirType:
    88  		doc, err = createDirHandler(c, instance.VFS())
    89  	default:
    90  		err = ErrDocTypeInvalid
    91  	}
    92  
    93  	if err != nil {
    94  		return WrapVfsError(err)
    95  	}
    96  
    97  	return jsonapi.Data(c, http.StatusCreated, doc, nil)
    98  }
    99  
   100  func createFileHandler(c echo.Context, fs vfs.VFS) (*file, error) {
   101  	inst := middlewares.GetInstance(c)
   102  	dirID := c.Param("file-id")
   103  	name := c.QueryParam("Name")
   104  	doc, err := FileDocFromReq(c, name, dirID)
   105  	if err != nil {
   106  		return nil, err
   107  	}
   108  
   109  	if created := c.QueryParam("CreatedAt"); created != "" {
   110  		if at, err2 := time.Parse(time.RFC3339, created); err2 == nil {
   111  			doc.CreatedAt = at
   112  		}
   113  	}
   114  	if updated := c.QueryParam("UpdatedAt"); updated != "" {
   115  		if at, err3 := time.Parse(time.RFC3339, updated); err3 == nil {
   116  			doc.UpdatedAt = at
   117  		}
   118  	}
   119  	doc.CozyMetadata, _ = CozyMetadataFromClaims(c, true)
   120  
   121  	err = checkPerm(c, "POST", nil, doc)
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  
   126  	if filepath.Ext(doc.DocName) == ".cozy-note" {
   127  		err := note.ImportFile(inst, doc, nil, c.Request().Body)
   128  		if err != nil {
   129  			inst.Logger().WithNamespace("files").
   130  				Infof("Cannot import note: %s", err)
   131  			return nil, WrapVfsError(err)
   132  		}
   133  		return NewFile(doc, inst), nil
   134  	}
   135  
   136  	file, err := fs.CreateFile(doc, nil)
   137  	if err != nil {
   138  		return nil, err
   139  	}
   140  
   141  	n, err := io.Copy(file, c.Request().Body)
   142  	if err != nil {
   143  		inst.Logger().WithNamespace("files").
   144  			Warnf("Error on uploading file (copy): %s (%d bytes written - expected %d)", err, n, doc.ByteSize)
   145  	}
   146  	if cerr := file.Close(); cerr != nil && (err == nil || errors.Is(err, io.ErrUnexpectedEOF)) {
   147  		err = cerr
   148  		inst.Logger().WithNamespace("files").
   149  			Warnf("Error on uploading file (close): %s", err)
   150  	}
   151  	if err != nil {
   152  		return nil, wrapVfsError(err)
   153  	}
   154  	return NewFile(doc, inst), nil
   155  }
   156  
   157  func createDirHandler(c echo.Context, fs vfs.VFS) (*dir, error) {
   158  	path := c.QueryParam("Path")
   159  	tags := utils.SplitTrimString(c.QueryParam("Tags"), TagSeparator)
   160  
   161  	var doc *vfs.DirDoc
   162  	var err error
   163  	if path != "" {
   164  		if c.QueryParam("Recursive") == "true" {
   165  			doc, err = vfs.MkdirAll(fs, path)
   166  		} else {
   167  			doc, err = vfs.Mkdir(fs, path, tags)
   168  		}
   169  		if err != nil {
   170  			return nil, err
   171  		}
   172  		return newDir(doc), nil
   173  	}
   174  
   175  	dirID := c.Param("file-id")
   176  	name := c.QueryParam("Name")
   177  	doc, err = vfs.NewDirDoc(fs, name, dirID, tags)
   178  	if err != nil {
   179  		return nil, err
   180  	}
   181  	if date := c.Request().Header.Get("Date"); date != "" {
   182  		if t, err2 := time.Parse(time.RFC1123, date); err2 == nil {
   183  			doc.CreatedAt = t
   184  			doc.UpdatedAt = t
   185  		}
   186  	}
   187  	if created := c.QueryParam("CreatedAt"); created != "" {
   188  		if at, err2 := time.Parse(time.RFC3339, created); err2 == nil {
   189  			doc.CreatedAt = at
   190  		}
   191  	}
   192  
   193  	if updated := c.QueryParam("UpdatedAt"); updated != "" {
   194  		if at, err3 := time.Parse(time.RFC3339, updated); err3 == nil {
   195  			doc.UpdatedAt = at
   196  		}
   197  	}
   198  
   199  	if secret := c.QueryParam("MetadataID"); secret != "" {
   200  		instance := middlewares.GetInstance(c)
   201  		meta, err := vfs.GetStore().GetMetadata(instance, secret)
   202  		if err != nil {
   203  			return nil, err
   204  		}
   205  		doc.Metadata = *meta
   206  	}
   207  
   208  	if len(doc.Metadata) > 0 {
   209  		if _, ok := doc.Metadata[consts.CarbonCopyKey]; ok {
   210  			if err := middlewares.AllowWholeType(c, permission.POST, consts.CertifiedCarbonCopy); err != nil {
   211  				delete(doc.Metadata, consts.CarbonCopyKey)
   212  			}
   213  		}
   214  		if _, ok := doc.Metadata[consts.ElectronicSafeKey]; ok {
   215  			if err := middlewares.AllowWholeType(c, permission.POST, consts.CertifiedElectronicSafe); err != nil {
   216  				delete(doc.Metadata, consts.ElectronicSafeKey)
   217  			}
   218  		}
   219  	}
   220  
   221  	doc.CozyMetadata, _ = CozyMetadataFromClaims(c, false)
   222  
   223  	err = checkPerm(c, "POST", doc, nil)
   224  	if err != nil {
   225  		return nil, err
   226  	}
   227  
   228  	if err = fs.CreateDir(doc); err != nil {
   229  		return nil, err
   230  	}
   231  
   232  	return newDir(doc), nil
   233  }
   234  
   235  // OverwriteFileContentHandler handles PUT requests on /files/:file-id
   236  // to overwrite the content of a file given its identifier.
   237  func OverwriteFileContentHandler(c echo.Context) error {
   238  	instance := middlewares.GetInstance(c)
   239  
   240  	fileID := c.Param("file-id")
   241  	if fileID == "" {
   242  		fileID = c.Param("docid") // Used by sharings.updateDocument
   243  	}
   244  
   245  	olddoc, err := instance.VFS().FileByID(fileID)
   246  	if err != nil {
   247  		return WrapVfsError(err)
   248  	}
   249  
   250  	newdoc, err := FileDocFromReq(c, olddoc.DocName, olddoc.DirID)
   251  	if err != nil {
   252  		return WrapVfsError(err)
   253  	}
   254  
   255  	if updated := c.QueryParam("UpdatedAt"); updated != "" {
   256  		if at, err2 := time.Parse(time.RFC3339, updated); err2 == nil {
   257  			newdoc.UpdatedAt = at
   258  		}
   259  	}
   260  
   261  	newdoc.ReferencedBy = olddoc.ReferencedBy
   262  
   263  	if err := CheckIfMatch(c, olddoc.Rev()); err != nil {
   264  		return WrapVfsError(err)
   265  	}
   266  
   267  	if olddoc.CozyMetadata != nil {
   268  		newdoc.CozyMetadata = olddoc.CozyMetadata.Clone()
   269  	}
   270  	updateFileCozyMetadata(c, newdoc, true)
   271  
   272  	err = checkPerm(c, permission.PUT, nil, olddoc)
   273  	if err != nil {
   274  		return err
   275  	}
   276  
   277  	newdoc.SetID(olddoc.ID()) // The ID can be useful to check permissions
   278  	err = checkPerm(c, permission.PUT, nil, newdoc)
   279  	if err != nil {
   280  		return err
   281  	}
   282  
   283  	if filepath.Ext(newdoc.DocName) == ".cozy-note" {
   284  		err := note.ImportFile(instance, newdoc, olddoc, c.Request().Body)
   285  		if err != nil {
   286  			instance.Logger().WithNamespace("files").
   287  				Infof("Cannot import note: %s", err)
   288  			return WrapVfsError(err)
   289  		}
   290  		return FileData(c, http.StatusOK, newdoc, true, nil)
   291  	}
   292  
   293  	file, err := instance.VFS().CreateFile(newdoc, olddoc)
   294  	if err != nil {
   295  		return WrapVfsError(err)
   296  	}
   297  	_, err = io.Copy(file, c.Request().Body)
   298  	if cerr := file.Close(); cerr != nil && err == nil {
   299  		err = cerr
   300  	}
   301  	if err != nil {
   302  		return WrapVfsError(err)
   303  	}
   304  	return FileData(c, http.StatusOK, newdoc, true, nil)
   305  }
   306  
   307  // UploadMetadataHandler accepts a metadata objet and persist it, so that it
   308  // can be used in a future file upload.
   309  func UploadMetadataHandler(c echo.Context) error {
   310  	if err := checkPerm(c, permission.POST, nil, &vfs.FileDoc{}); err != nil {
   311  		return err
   312  	}
   313  
   314  	meta := &vfs.Metadata{}
   315  	if _, err := jsonapi.Bind(c.Request().Body, meta); err != nil {
   316  		return err
   317  	}
   318  
   319  	instance := middlewares.GetInstance(c)
   320  	secret, err := vfs.GetStore().AddMetadata(instance, meta)
   321  	if err != nil {
   322  		return WrapVfsError(err)
   323  	}
   324  
   325  	m := apiMetadata{
   326  		Metadata: meta,
   327  		secret:   secret,
   328  	}
   329  	return jsonapi.Data(c, http.StatusCreated, &m, nil)
   330  }
   331  
   332  // FileCopyHandler handles POST requests on /files/:file-id/copy
   333  //
   334  // It is used to duplicate the given file and its metadata except for
   335  // relationships.
   336  func FileCopyHandler(c echo.Context) error {
   337  	inst := middlewares.GetInstance(c)
   338  	fs := inst.VFS()
   339  
   340  	fileID := c.Param("file-id")
   341  	olddoc, err := inst.VFS().FileByID(fileID)
   342  	if err != nil {
   343  		return WrapVfsError(err)
   344  	}
   345  
   346  	newDirID := c.QueryParam("DirID")
   347  	copyName := c.QueryParam("Name")
   348  	if copyName == "" {
   349  		copyName = fileCopyName(inst, olddoc.DocName)
   350  	}
   351  	newdoc := vfs.CreateFileDocCopy(olddoc, newDirID, copyName)
   352  
   353  	err = checkPerm(c, permission.POST, nil, newdoc)
   354  	if err != nil {
   355  		return err
   356  	}
   357  
   358  	exists, err := fs.GetIndexer().DirChildExists(newdoc.DirID, newdoc.DocName)
   359  	if err != nil {
   360  		return WrapVfsError(err)
   361  	}
   362  	if exists {
   363  		newdoc.DocName = vfs.ConflictName(fs, newdoc.DirID, newdoc.DocName, true)
   364  		exists, err = fs.GetIndexer().DirChildExists(newdoc.DirID, newdoc.DocName)
   365  		if err != nil {
   366  			return WrapVfsError(err)
   367  		}
   368  		if exists {
   369  			return WrapVfsError(os.ErrExist)
   370  		}
   371  	}
   372  	newdoc.ResetFullpath()
   373  	updateFileCozyMetadata(c, newdoc, true)
   374  
   375  	if olddoc.Mime == consts.NoteMimeType {
   376  		// We need a special copy for notes because of their images
   377  		err = note.CopyFile(inst, olddoc, newdoc)
   378  	} else {
   379  		err = fs.CopyFile(olddoc, newdoc)
   380  	}
   381  	if err != nil {
   382  		return WrapVfsError(err)
   383  	}
   384  
   385  	return FileData(c, http.StatusCreated, newdoc, false, nil)
   386  }
   387  
   388  // ModifyMetadataByIDHandler handles PATCH requests on /files/:file-id
   389  //
   390  // It can be used to modify the file or directory metadata, as well as
   391  // moving and renaming it in the filesystem.
   392  func ModifyMetadataByIDHandler(c echo.Context) error {
   393  	patch, err := getPatch(c, c.Param("file-id"), "")
   394  	if err != nil {
   395  		return WrapVfsError(err)
   396  	}
   397  	i := middlewares.GetInstance(c)
   398  	if err = applyPatch(c, i.VFS(), patch); err != nil {
   399  		return WrapVfsError(err)
   400  	}
   401  	return nil
   402  }
   403  
   404  // ModifyMetadataByIDInBatchHandler handles PATCH requests on /files/.
   405  //
   406  // It can be used to modify many files or directories metadata, as well as
   407  // moving and renaming it in the filesystem, in batch.
   408  func ModifyMetadataByIDInBatchHandler(c echo.Context) error {
   409  	patches, err := getPatches(c)
   410  	if err != nil {
   411  		return WrapVfsError(err)
   412  	}
   413  	i := middlewares.GetInstance(c)
   414  	patchErrors, err := applyPatches(c, i.VFS(), patches)
   415  	if err != nil {
   416  		return err
   417  	}
   418  	if len(patchErrors) > 0 {
   419  		return jsonapi.DataErrorList(c, patchErrors...)
   420  	}
   421  	return c.NoContent(http.StatusNoContent)
   422  }
   423  
   424  // ModifyMetadataByPathHandler handles PATCH requests on /files/metadata
   425  //
   426  // It can be used to modify the file or directory metadata, as well as
   427  // moving and renaming it in the filesystem.
   428  func ModifyMetadataByPathHandler(c echo.Context) error {
   429  	patch, err := getPatch(c, "", c.QueryParam("Path"))
   430  	if err != nil {
   431  		return WrapVfsError(err)
   432  	}
   433  	i := middlewares.GetInstance(c)
   434  	if err = applyPatch(c, i.VFS(), patch); err != nil {
   435  		return WrapVfsError(err)
   436  	}
   437  	return nil
   438  }
   439  
   440  // ModifyFileVersionMetadata handles PATCH requests on /files/:file-id/:version-id
   441  //
   442  // It can be used to modify tags on an old version of a file.
   443  func ModifyFileVersionMetadata(c echo.Context) error {
   444  	inst := middlewares.GetInstance(c)
   445  	fileID := c.Param("file-id")
   446  	_, file, err := inst.VFS().DirOrFileByID(fileID)
   447  	if err != nil {
   448  		return WrapVfsError(err)
   449  	}
   450  	if file == nil {
   451  		return WrapVfsError(vfs.ErrConflict)
   452  	}
   453  	if err = checkPerm(c, permission.PATCH, nil, file); err != nil {
   454  		return WrapVfsError(err)
   455  	}
   456  	docID := fileID + "/" + c.Param("version-id")
   457  	version, err := vfs.FindVersion(inst, docID)
   458  	if err != nil {
   459  		return WrapVfsError(err)
   460  	}
   461  	var patch vfs.DocPatch
   462  	if _, err = jsonapi.Bind(c.Request().Body, &patch); err != nil || patch.Tags == nil {
   463  		return jsonapi.BadJSON()
   464  	}
   465  	version.Tags = *patch.Tags
   466  	version.CozyMetadata.UpdatedAt = time.Now()
   467  	if err = couchdb.UpdateDoc(inst, version); err != nil {
   468  		return WrapVfsError(err)
   469  	}
   470  	return jsonapi.Data(c, http.StatusOK, version, nil)
   471  }
   472  
   473  // DeleteFileVersionMetadata handles DELETE requests on /files/:file-id/:version-id
   474  //
   475  // It can be used to delete an old version of a file.
   476  func DeleteFileVersionMetadata(c echo.Context) error {
   477  	inst := middlewares.GetInstance(c)
   478  	fs := inst.VFS()
   479  	fileID := c.Param("file-id")
   480  	_, file, err := fs.DirOrFileByID(fileID)
   481  	if err != nil {
   482  		return WrapVfsError(err)
   483  	}
   484  	if file == nil {
   485  		return WrapVfsError(vfs.ErrConflict)
   486  	}
   487  	if err = checkPerm(c, permission.DELETE, nil, file); err != nil {
   488  		return WrapVfsError(err)
   489  	}
   490  	docID := fileID + "/" + c.Param("version-id")
   491  	version, err := vfs.FindVersion(inst, docID)
   492  	if err != nil {
   493  		return WrapVfsError(err)
   494  	}
   495  	if err := fs.CleanOldVersion(fileID, version); err != nil {
   496  		return WrapVfsError(err)
   497  	}
   498  	return c.NoContent(http.StatusNoContent)
   499  }
   500  
   501  // CopyVersionHandler handles POST requests on /files/:file-id/versions.
   502  //
   503  // It can be used to create a new version of a file, with the same content but
   504  // new metadata.
   505  func CopyVersionHandler(c echo.Context) error {
   506  	inst := middlewares.GetInstance(c)
   507  	fs := inst.VFS()
   508  	fileID := c.Param("file-id")
   509  	olddoc, err := fs.FileByID(fileID)
   510  	if err != nil {
   511  		return WrapVfsError(err)
   512  	}
   513  	if olddoc == nil {
   514  		return WrapVfsError(vfs.ErrConflict)
   515  	}
   516  	if err = checkPerm(c, permission.PUT, nil, olddoc); err != nil {
   517  		return WrapVfsError(err)
   518  	}
   519  
   520  	meta := vfs.Metadata{}
   521  	if _, err := jsonapi.Bind(c.Request().Body, &meta); err != nil {
   522  		return err
   523  	}
   524  
   525  	// Manage the special cases of carbonCopy & electronicSafe
   526  	if len(meta) > 0 {
   527  		if _, ok := meta[consts.CarbonCopyKey]; ok {
   528  			if err := middlewares.AllowWholeType(c, permission.POST, consts.CertifiedCarbonCopy); err != nil {
   529  				delete(meta, consts.CarbonCopyKey)
   530  			}
   531  		}
   532  		if _, ok := meta[consts.ElectronicSafeKey]; ok {
   533  			if err := middlewares.AllowWholeType(c, permission.POST, consts.CertifiedElectronicSafe); err != nil {
   534  				delete(meta, consts.ElectronicSafeKey)
   535  			}
   536  		}
   537  	}
   538  	keepCarbonCopy := true
   539  	keepElectronicSafe := true
   540  	for key := range meta {
   541  		switch key {
   542  		case consts.CarbonCopyKey:
   543  			keepCarbonCopy = false
   544  		case consts.ElectronicSafeKey:
   545  			keepElectronicSafe = false
   546  		case "qualification":
   547  			//
   548  		default:
   549  			keepCarbonCopy = false
   550  			keepElectronicSafe = false
   551  		}
   552  	}
   553  	if value, ok := olddoc.Metadata[consts.CarbonCopyKey]; ok && keepCarbonCopy {
   554  		meta[consts.CarbonCopyKey] = value
   555  	}
   556  	if value, ok := olddoc.Metadata[consts.ElectronicSafeKey]; ok && keepElectronicSafe {
   557  		meta[consts.ElectronicSafeKey] = value
   558  	}
   559  
   560  	// For notes, preserve the prosemirror metadata
   561  	if olddoc.Mime == consts.NoteMimeType {
   562  		for _, name := range []string{"title", "content", "schema", "version"} {
   563  			if meta[name] == nil {
   564  				meta[name] = olddoc.Metadata[name]
   565  			}
   566  		}
   567  	}
   568  
   569  	newdoc := olddoc.Clone().(*vfs.FileDoc)
   570  	newdoc.Metadata = meta
   571  	newdoc.Tags = utils.SplitTrimString(c.QueryParam("Tags"), TagSeparator)
   572  	updateFileCozyMetadata(c, newdoc, true)
   573  
   574  	content, err := fs.OpenFile(olddoc)
   575  	if err != nil {
   576  		return WrapVfsError(err)
   577  	}
   578  	defer content.Close()
   579  
   580  	file, err := fs.CreateFile(newdoc, olddoc)
   581  	if err != nil {
   582  		return WrapVfsError(err)
   583  	}
   584  
   585  	defer func() {
   586  		if cerr := file.Close(); cerr != nil && err == nil {
   587  			err = cerr
   588  		}
   589  		if err != nil {
   590  			err = WrapVfsError(err)
   591  			return
   592  		}
   593  		err = FileData(c, http.StatusOK, newdoc, true, nil)
   594  	}()
   595  
   596  	_, err = io.Copy(file, content)
   597  	return err
   598  }
   599  
   600  // ClearOldVersions is the handler for DELETE /files/versions.
   601  // It deletes all the old versions of all files to make space for new files.
   602  func ClearOldVersions(c echo.Context) error {
   603  	if err := middlewares.AllowWholeType(c, permission.DELETE, consts.Files); err != nil {
   604  		return err
   605  	}
   606  
   607  	fs := middlewares.GetInstance(c).VFS()
   608  	if err := fs.ClearOldVersions(); err != nil {
   609  		return WrapVfsError(err)
   610  	}
   611  
   612  	return c.NoContent(204)
   613  }
   614  
   615  func getPatch(c echo.Context, docID, docPath string) (*docPatch, error) {
   616  	var patch docPatch
   617  	obj, err := jsonapi.Bind(c.Request().Body, &patch)
   618  	if err != nil {
   619  		return nil, jsonapi.BadJSON()
   620  	}
   621  	patch.docID = docID
   622  	patch.docPath = docPath
   623  	patch.RestorePath = nil
   624  	if rel, ok := obj.GetRelationship("parent"); ok {
   625  		rid, ok := rel.ResourceIdentifier()
   626  		if !ok {
   627  			return nil, jsonapi.BadJSON()
   628  		}
   629  		patch.DirID = &rid.ID
   630  	}
   631  	return &patch, nil
   632  }
   633  
   634  func getPatches(c echo.Context) ([]*docPatch, error) {
   635  	req := c.Request()
   636  	objs, err := jsonapi.BindCompound(req.Body)
   637  	if err != nil {
   638  		return nil, jsonapi.BadJSON()
   639  	}
   640  	patches := make([]*docPatch, len(objs))
   641  	for i, obj := range objs {
   642  		var patch docPatch
   643  		if obj.Attributes == nil {
   644  			return nil, jsonapi.BadJSON()
   645  		}
   646  		if err = json.Unmarshal(*obj.Attributes, &patch); err != nil {
   647  			return nil, err
   648  		}
   649  		patch.docID = obj.ID
   650  		patch.docPath = ""
   651  		patch.RestorePath = nil
   652  		if rel, ok := obj.GetRelationship("parent"); ok {
   653  			rid, ok := rel.ResourceIdentifier()
   654  			if !ok {
   655  				return nil, jsonapi.BadJSON()
   656  			}
   657  			patch.DirID = &rid.ID
   658  		}
   659  		patches[i] = &patch
   660  	}
   661  	return patches, nil
   662  }
   663  
   664  func applyPatch(c echo.Context, fs vfs.VFS, patch *docPatch) (err error) {
   665  	var file *vfs.FileDoc
   666  	var dir *vfs.DirDoc
   667  	if patch.docID != "" {
   668  		dir, file, err = fs.DirOrFileByID(patch.docID)
   669  	} else {
   670  		dir, file, err = fs.DirOrFileByPath(patch.docPath)
   671  	}
   672  	if err != nil {
   673  		return err
   674  	}
   675  
   676  	var rev string
   677  	if dir != nil {
   678  		rev = dir.Rev()
   679  	} else {
   680  		rev = file.Rev()
   681  	}
   682  
   683  	if err = CheckIfMatch(c, rev); err != nil {
   684  		return err
   685  	}
   686  
   687  	if err = checkPerm(c, permission.PATCH, dir, file); err != nil {
   688  		return err
   689  	}
   690  
   691  	if patch.Delete {
   692  		if dir != nil {
   693  			inst := middlewares.GetInstance(c)
   694  			err = fs.DestroyDirAndContent(dir, pushTrashJob(inst))
   695  		} else {
   696  			err = fs.DestroyFile(file)
   697  		}
   698  	} else if patch.Trash {
   699  		if dir != nil {
   700  			updateDirCozyMetadata(c, dir)
   701  			dir, err = vfs.TrashDir(fs, dir)
   702  		} else {
   703  			updateFileCozyMetadata(c, file, false)
   704  			file, err = vfs.TrashFile(fs, file)
   705  		}
   706  	} else {
   707  		if dir != nil {
   708  			updateDirCozyMetadata(c, dir)
   709  			dir, err = vfs.ModifyDirMetadata(fs, dir, &patch.DocPatch)
   710  		} else {
   711  			updateFileCozyMetadata(c, file, false)
   712  			file, err = vfs.ModifyFileMetadata(fs, file, &patch.DocPatch)
   713  		}
   714  	}
   715  	if err != nil {
   716  		return err
   717  	}
   718  
   719  	if dir != nil {
   720  		return dirData(c, http.StatusOK, dir)
   721  	}
   722  	return FileData(c, http.StatusOK, file, false, nil)
   723  }
   724  
   725  func applyPatches(c echo.Context, fs vfs.VFS, patches []*docPatch) (errors []*jsonapi.Error, err error) {
   726  	for _, patch := range patches {
   727  		dir, file, errf := fs.DirOrFileByID(patch.docID)
   728  		if errf != nil {
   729  			jsonapiError := wrapVfsErrorJSONAPI(errf)
   730  			jsonapiError.Source.Parameter = "_id"
   731  			jsonapiError.Source.Pointer = patch.docID
   732  			errors = append(errors, jsonapiError)
   733  			continue
   734  		}
   735  		if err = checkPerm(c, permission.PATCH, dir, file); err != nil {
   736  			return
   737  		}
   738  		var errp error
   739  		if patch.Delete {
   740  			if dir != nil {
   741  				inst := middlewares.GetInstance(c)
   742  				errp = fs.DestroyDirAndContent(dir, pushTrashJob(inst))
   743  			} else if file != nil {
   744  				errp = fs.DestroyFile(file)
   745  			}
   746  		} else if patch.Trash {
   747  			if dir != nil {
   748  				updateDirCozyMetadata(c, dir)
   749  				_, errp = vfs.TrashDir(fs, dir)
   750  			} else if file != nil {
   751  				updateFileCozyMetadata(c, file, false)
   752  				_, errp = vfs.TrashFile(fs, file)
   753  			}
   754  		} else if dir != nil {
   755  			updateDirCozyMetadata(c, dir)
   756  			_, errp = vfs.ModifyDirMetadata(fs, dir, &patch.DocPatch)
   757  		} else if file != nil {
   758  			updateFileCozyMetadata(c, file, false)
   759  			_, errp = vfs.ModifyFileMetadata(fs, file, &patch.DocPatch)
   760  		}
   761  		if errp != nil {
   762  			jsonapiError := wrapVfsErrorJSONAPI(errp)
   763  			jsonapiError.Source.Parameter = "_id"
   764  			jsonapiError.Source.Pointer = patch.docID
   765  			errors = append(errors, jsonapiError)
   766  		}
   767  	}
   768  
   769  	return
   770  }
   771  
   772  // ReadMetadataFromIDHandler handles all GET requests on /files/:file-
   773  // id aiming at getting file metadata from its id.
   774  func ReadMetadataFromIDHandler(c echo.Context) error {
   775  	instance := middlewares.GetInstance(c)
   776  	perm, err := middlewares.GetPermission(c)
   777  	if err != nil {
   778  		return err
   779  	}
   780  
   781  	fileID := c.Param("file-id")
   782  
   783  	dir, file, err := instance.VFS().DirOrFileByID(fileID)
   784  	if err != nil {
   785  		return WrapVfsError(err)
   786  	}
   787  
   788  	if err := checkPerm(c, permission.GET, dir, file); err != nil {
   789  		return err
   790  	}
   791  
   792  	// Limiting the number of public share link consultations
   793  	if perm.Type == permission.TypeShareByLink {
   794  		err = config.GetRateLimiter().CheckRateLimitKey(fileID, limits.SharingPublicLinkType)
   795  		if limits.IsLimitReachedOrExceeded(err) {
   796  			return err
   797  		}
   798  	}
   799  
   800  	if dir != nil {
   801  		return dirData(c, http.StatusOK, dir)
   802  	}
   803  	return FileData(c, http.StatusOK, file, true, nil)
   804  }
   805  
   806  // GetChildrenHandler returns a list of children of a folder
   807  func GetChildrenHandler(c echo.Context) error {
   808  	instance := middlewares.GetInstance(c)
   809  
   810  	fileID := c.Param("file-id")
   811  
   812  	dir, file, err := instance.VFS().DirOrFileByID(fileID)
   813  	if err != nil {
   814  		return WrapVfsError(err)
   815  	}
   816  
   817  	if err := checkPerm(c, permission.GET, dir, file); err != nil {
   818  		return err
   819  	}
   820  
   821  	if file != nil {
   822  		return jsonapi.Errorf(http.StatusBadRequest, "cant read children of file %v", fileID)
   823  	}
   824  
   825  	return dirDataList(c, http.StatusOK, dir)
   826  }
   827  
   828  type apiDiskSize struct {
   829  	DocID string `json:"id,omitempty"`
   830  	Size  int64  `json:"size,string"`
   831  }
   832  
   833  func (d *apiDiskSize) ID() string                             { return d.DocID }
   834  func (d *apiDiskSize) Rev() string                            { return "" }
   835  func (d *apiDiskSize) DocType() string                        { return consts.DirSizes }
   836  func (d *apiDiskSize) Clone() couchdb.Doc                     { return d }
   837  func (d *apiDiskSize) SetID(id string)                        { d.DocID = id }
   838  func (d *apiDiskSize) SetRev(_ string)                        {}
   839  func (d *apiDiskSize) Relationships() jsonapi.RelationshipMap { return nil }
   840  func (d *apiDiskSize) Included() []jsonapi.Object             { return nil }
   841  func (d *apiDiskSize) Links() *jsonapi.LinksList              { return nil }
   842  
   843  // GetDirSize returns the size of a directory (the sum of the size of the files
   844  // in this directory, including those in subdirectories).
   845  func GetDirSize(c echo.Context) error {
   846  	fs := middlewares.GetInstance(c).VFS()
   847  	fileID := c.Param("file-id")
   848  
   849  	dir, err := fs.DirByID(fileID)
   850  	if err != nil {
   851  		return WrapVfsError(err)
   852  	}
   853  	if err := checkPerm(c, permission.GET, dir, nil); err != nil {
   854  		return err
   855  	}
   856  
   857  	size, err := fs.DirSize(dir)
   858  	if err != nil {
   859  		return WrapVfsError(err)
   860  	}
   861  
   862  	result := apiDiskSize{DocID: fileID, Size: size}
   863  	return jsonapi.Data(c, http.StatusOK, &result, nil)
   864  }
   865  
   866  // ReadMetadataFromPathHandler handles all GET requests on
   867  // /files/metadata aiming at getting file metadata from its path.
   868  func ReadMetadataFromPathHandler(c echo.Context) error {
   869  	var err error
   870  
   871  	instance := middlewares.GetInstance(c)
   872  
   873  	dir, file, err := instance.VFS().DirOrFileByPath(c.QueryParam("Path"))
   874  	if err != nil {
   875  		return WrapVfsError(err)
   876  	}
   877  
   878  	if err := checkPerm(c, permission.GET, dir, file); err != nil {
   879  		return err
   880  	}
   881  
   882  	if dir != nil {
   883  		return dirData(c, http.StatusOK, dir)
   884  	}
   885  	return FileData(c, http.StatusOK, file, true, nil)
   886  }
   887  
   888  // ReadFileContentFromIDHandler handles all GET requests on /files/:file-id
   889  // aiming at downloading a file given its ID. It serves the file in inline
   890  // mode.
   891  func ReadFileContentFromIDHandler(c echo.Context) error {
   892  	instance := middlewares.GetInstance(c)
   893  
   894  	doc, err := instance.VFS().FileByID(c.Param("file-id"))
   895  	if err != nil {
   896  		return WrapVfsError(err)
   897  	}
   898  
   899  	err = checkPerm(c, permission.GET, nil, doc)
   900  	if err != nil {
   901  		return err
   902  	}
   903  
   904  	disposition := "inline"
   905  	if c.QueryParam("Dl") == "1" {
   906  		disposition = "attachment"
   907  	}
   908  	err = vfs.ServeFileContent(instance.VFS(), doc, nil, "", disposition, c.Request(), c.Response())
   909  	if err != nil {
   910  		return WrapVfsError(err)
   911  	}
   912  
   913  	return nil
   914  }
   915  
   916  // ReadFileContentFromVersion handles the download of an old version of the
   917  // file content.
   918  func ReadFileContentFromVersion(c echo.Context) error {
   919  	instance := middlewares.GetInstance(c)
   920  
   921  	doc, err := instance.VFS().FileByID(c.Param("file-id"))
   922  	if err != nil {
   923  		return WrapVfsError(err)
   924  	}
   925  
   926  	err = checkPerm(c, permission.GET, nil, doc)
   927  	if err != nil {
   928  		return err
   929  	}
   930  
   931  	version, err := vfs.FindVersion(instance, doc.DocID+"/"+c.Param("version-id"))
   932  	if err != nil {
   933  		return WrapVfsError(err)
   934  	}
   935  
   936  	disposition := "inline"
   937  	if c.QueryParam("Dl") == "1" {
   938  		disposition = "attachment"
   939  	}
   940  	err = vfs.ServeFileContent(instance.VFS(), doc, version, "", disposition, c.Request(), c.Response())
   941  	if err != nil {
   942  		return WrapVfsError(err)
   943  	}
   944  
   945  	return nil
   946  }
   947  
   948  // RevertFileVersion restores an old version of the file content.
   949  func RevertFileVersion(c echo.Context) error {
   950  	inst := middlewares.GetInstance(c)
   951  
   952  	doc, err := inst.VFS().FileByID(c.Param("file-id"))
   953  	if err != nil {
   954  		return WrapVfsError(err)
   955  	}
   956  
   957  	if err = checkPerm(c, permission.POST, nil, doc); err != nil {
   958  		return err
   959  	}
   960  
   961  	version, err := vfs.FindVersion(inst, doc.DocID+"/"+c.Param("version-id"))
   962  	if err != nil {
   963  		return WrapVfsError(err)
   964  	}
   965  
   966  	if err = inst.VFS().RevertFileVersion(doc, version); err != nil {
   967  		return WrapVfsError(err)
   968  	}
   969  
   970  	return FileData(c, http.StatusOK, doc, true, nil)
   971  }
   972  
   973  // HeadDirOrFile handles HEAD requests on directory or file to check their
   974  // existence
   975  func HeadDirOrFile(c echo.Context) error {
   976  	instance := middlewares.GetInstance(c)
   977  
   978  	dir, file, err := instance.VFS().DirOrFileByID(c.Param("file-id"))
   979  	if err != nil {
   980  		return WrapVfsError(err)
   981  	}
   982  
   983  	if dir != nil {
   984  		err = checkPerm(c, permission.GET, dir, nil)
   985  	} else {
   986  		err = checkPerm(c, permission.GET, nil, file)
   987  	}
   988  	if err != nil {
   989  		return err
   990  	}
   991  
   992  	return nil
   993  }
   994  
   995  // IconHandler serves icon for the PDFs.
   996  func IconHandler(c echo.Context) error {
   997  	instance := middlewares.GetInstance(c)
   998  
   999  	secret := c.Param("secret")
  1000  	fileID, err := vfs.GetStore().GetThumb(instance, secret)
  1001  	if err != nil {
  1002  		return WrapVfsError(err)
  1003  	}
  1004  	if c.Param("file-id") != fileID {
  1005  		return jsonapi.NewError(http.StatusBadRequest, "Wrong download token")
  1006  	}
  1007  
  1008  	doc, err := instance.VFS().FileByID(fileID)
  1009  	if err != nil {
  1010  		return WrapVfsError(err)
  1011  	}
  1012  
  1013  	return vfs.ServePDFIcon(c.Response(), c.Request(), instance.VFS(), doc)
  1014  }
  1015  
  1016  // PreviewHandler serves preview images for the PDFs.
  1017  func PreviewHandler(c echo.Context) error {
  1018  	instance := middlewares.GetInstance(c)
  1019  
  1020  	secret := c.Param("secret")
  1021  	fileID, err := vfs.GetStore().GetThumb(instance, secret)
  1022  	if err != nil {
  1023  		return WrapVfsError(err)
  1024  	}
  1025  	if c.Param("file-id") != fileID {
  1026  		return jsonapi.NewError(http.StatusBadRequest, "Wrong download token")
  1027  	}
  1028  
  1029  	doc, err := instance.VFS().FileByID(fileID)
  1030  	if err != nil {
  1031  		return WrapVfsError(err)
  1032  	}
  1033  
  1034  	return vfs.ServePDFPreview(c.Response(), c.Request(), instance.VFS(), doc)
  1035  }
  1036  
  1037  // ThumbnailHandler serves thumbnails of the images/photos
  1038  func ThumbnailHandler(c echo.Context) error {
  1039  	instance := middlewares.GetInstance(c)
  1040  
  1041  	secret := c.Param("secret")
  1042  	fileID, err := vfs.GetStore().GetThumb(instance, secret)
  1043  	if err != nil {
  1044  		return WrapVfsError(err)
  1045  	}
  1046  	if c.Param("file-id") != fileID {
  1047  		return jsonapi.NewError(http.StatusBadRequest, "Wrong download token")
  1048  	}
  1049  
  1050  	doc, err := instance.VFS().FileByID(fileID)
  1051  	if err != nil {
  1052  		return WrapVfsError(err)
  1053  	}
  1054  
  1055  	fs := instance.ThumbsFS()
  1056  	format := c.Param("format")
  1057  	err = fs.ServeThumbContent(c.Response(), c.Request(), doc, format)
  1058  	if err != nil {
  1059  		if !errors.Is(err, os.ErrInvalid) {
  1060  			msg, _ := job.NewMessage(thumbnail.ImageMessage{
  1061  				File:   doc,
  1062  				Format: format,
  1063  			})
  1064  			_, _ = job.System().PushJob(instance, &job.JobRequest{
  1065  				WorkerType: "thumbnail",
  1066  				Message:    msg,
  1067  			})
  1068  		}
  1069  		return serveThumbnailPlaceholder(c.Response(), c.Request(), doc, format)
  1070  	}
  1071  	return nil
  1072  }
  1073  
  1074  func serveThumbnailPlaceholder(res http.ResponseWriter, req *http.Request, doc *vfs.FileDoc, format string) error {
  1075  	if !utils.IsInArray(format, vfs.ThumbnailFormatNames) {
  1076  		return echo.NewHTTPError(http.StatusNotFound, "Format does not exist")
  1077  	}
  1078  	f := statik.GetAsset("/placeholders/thumbnail-" + format + ".png")
  1079  	if f == nil {
  1080  		return os.ErrNotExist
  1081  	}
  1082  	etag := f.Etag
  1083  	if utils.CheckPreconditions(res, req, etag) {
  1084  		return nil
  1085  	}
  1086  	res.Header().Set("Etag", etag)
  1087  	res.WriteHeader(http.StatusNotFound)
  1088  	_, err := io.Copy(res, f.Reader())
  1089  	return err
  1090  }
  1091  
  1092  func sendFileFromPath(c echo.Context, path string, checkPermission bool) error {
  1093  	instance := middlewares.GetInstance(c)
  1094  
  1095  	doc, err := instance.VFS().FileByPath(path)
  1096  	if err != nil {
  1097  		return WrapVfsError(err)
  1098  	}
  1099  
  1100  	if checkPermission {
  1101  		err = middlewares.Allow(c, permission.GET, doc)
  1102  		if err != nil {
  1103  			return err
  1104  		}
  1105  	}
  1106  
  1107  	// Forbid extracting autofilled passwords on an HTML page hosted in the Cozy
  1108  	if !config.GetConfig().CSPDisabled {
  1109  		middlewares.AppendCSPRule(c, "form-action", "'none'")
  1110  	}
  1111  
  1112  	disposition := "inline"
  1113  	if c.QueryParam("Dl") == "1" {
  1114  		disposition = "attachment"
  1115  	} else if !checkPermission {
  1116  		addCSPRuleForDirectLink(c, doc.Class, doc.Mime)
  1117  	}
  1118  	err = vfs.ServeFileContent(instance.VFS(), doc, nil, "", disposition, c.Request(), c.Response())
  1119  	if err != nil {
  1120  		return WrapVfsError(err)
  1121  	}
  1122  
  1123  	return nil
  1124  }
  1125  
  1126  func addCSPRuleForDirectLink(c echo.Context, class, mime string) {
  1127  	if config.GetConfig().CSPDisabled {
  1128  		return
  1129  	}
  1130  	// Allow some files to be displayed by the browser in the client-side apps
  1131  	if mime == "text/plain" || class == "image" || class == "audio" || class == "video" || mime == "application/pdf" {
  1132  		middlewares.AppendCSPRule(c, "frame-ancestors", "*")
  1133  	}
  1134  }
  1135  
  1136  // ReadFileContentFromPathHandler handles all GET request on /files/download
  1137  // aiming at downloading a file given its path. It serves the file in in
  1138  // attachment mode.
  1139  func ReadFileContentFromPathHandler(c echo.Context) error {
  1140  	return sendFileFromPath(c, c.QueryParam("Path"), true)
  1141  }
  1142  
  1143  // ArchiveDownloadCreateHandler handles requests to /files/archive and stores the
  1144  // paremeters with a secret to be used in download handler below.s
  1145  func ArchiveDownloadCreateHandler(c echo.Context) error {
  1146  	archive := &vfs.Archive{}
  1147  	if _, err := jsonapi.Bind(c.Request().Body, archive); err != nil {
  1148  		return err
  1149  	}
  1150  	if len(archive.Files) == 0 && len(archive.IDs) == 0 {
  1151  		return c.JSON(http.StatusBadRequest, "Can't create an archive with no files")
  1152  	}
  1153  	if strings.Contains(archive.Name, "/") {
  1154  		return c.JSON(http.StatusBadRequest, "The archive filename can't contain a /")
  1155  	}
  1156  	if archive.Name == "" {
  1157  		archive.Name = "archive"
  1158  	}
  1159  	instance := middlewares.GetInstance(c)
  1160  
  1161  	entries, err := archive.GetEntries(instance.VFS())
  1162  	if err != nil {
  1163  		return WrapVfsError(err)
  1164  	}
  1165  
  1166  	for _, e := range entries {
  1167  		err = checkPerm(c, permission.GET, e.Dir, e.File)
  1168  		if err != nil {
  1169  			return err
  1170  		}
  1171  	}
  1172  
  1173  	// if accept header is application/zip, send the archive immediately
  1174  	if c.Request().Header.Get(echo.HeaderAccept) == "application/zip" {
  1175  		return archive.Serve(instance.VFS(), c.Response())
  1176  	}
  1177  
  1178  	secret, err := vfs.GetStore().AddArchive(instance, archive)
  1179  	if err != nil {
  1180  		return WrapVfsError(err)
  1181  	}
  1182  	archive.Secret = secret
  1183  
  1184  	fakeName := url.PathEscape(archive.Name)
  1185  
  1186  	links := &jsonapi.LinksList{
  1187  		Related: "/files/archive/" + secret + "/" + fakeName + ".zip",
  1188  	}
  1189  
  1190  	return jsonapi.Data(c, http.StatusOK, &apiArchive{archive}, links)
  1191  }
  1192  
  1193  // FileDownloadCreateHandler stores the required path into a secret
  1194  // usable for download handler below.
  1195  func FileDownloadCreateHandler(c echo.Context) error {
  1196  	instance := middlewares.GetInstance(c)
  1197  	var doc *vfs.FileDoc
  1198  	var err error
  1199  	var path string
  1200  	var versionID string
  1201  
  1202  	if path = c.QueryParam("Path"); path != "" {
  1203  		if doc, err = instance.VFS().FileByPath(path); err != nil {
  1204  			return WrapVfsError(err)
  1205  		}
  1206  	} else if id := c.QueryParam("Id"); id != "" {
  1207  		if doc, err = instance.VFS().FileByID(id); err != nil {
  1208  			return WrapVfsError(err)
  1209  		}
  1210  		if path, err = doc.Path(instance.VFS()); err != nil {
  1211  			return WrapVfsError(err)
  1212  		}
  1213  	} else if versionID = c.QueryParam("VersionId"); versionID != "" {
  1214  		docID := strings.Split(versionID, "/")[0]
  1215  		if doc, err = instance.VFS().FileByID(docID); err != nil {
  1216  			return WrapVfsError(err)
  1217  		}
  1218  	}
  1219  
  1220  	err = checkPerm(c, "GET", nil, doc)
  1221  	if err != nil {
  1222  		return err
  1223  	}
  1224  
  1225  	var secret string
  1226  	if versionID == "" {
  1227  		secret, err = vfs.GetStore().AddFile(instance, path)
  1228  	} else {
  1229  		secret, err = vfs.GetStore().AddVersion(instance, versionID)
  1230  		secret = "v-" + secret
  1231  	}
  1232  	if err != nil {
  1233  		return WrapVfsError(err)
  1234  	}
  1235  
  1236  	filename := c.QueryParam("Filename")
  1237  	if filename == "" {
  1238  		filename = doc.DocName
  1239  	}
  1240  	links := &jsonapi.LinksList{
  1241  		Related: "/files/downloads/" + secret + "/" + filename,
  1242  	}
  1243  
  1244  	return FileData(c, http.StatusOK, doc, false, links)
  1245  }
  1246  
  1247  // ArchiveDownloadHandler handles requests to /files/archive/:secret/whatever.zip
  1248  // and creates on the fly zip archive from the parameters linked to secret.
  1249  func ArchiveDownloadHandler(c echo.Context) error {
  1250  	instance := middlewares.GetInstance(c)
  1251  	secret := c.Param("secret")
  1252  	archive, err := vfs.GetStore().GetArchive(instance, secret)
  1253  	if err != nil {
  1254  		return WrapVfsError(err)
  1255  	}
  1256  	if err := archive.Serve(instance.VFS(), c.Response()); err != nil {
  1257  		return WrapVfsError(err)
  1258  	}
  1259  	return nil
  1260  }
  1261  
  1262  // FileDownloadHandler send a file that have previously be defined
  1263  // through FileDownloadCreateHandler
  1264  func FileDownloadHandler(c echo.Context) error {
  1265  	secret := c.Param("secret")
  1266  	if strings.HasPrefix(secret, "v-") {
  1267  		return versionDownloadHandler(c, strings.TrimPrefix(secret, "v-"))
  1268  	}
  1269  	instance := middlewares.GetInstance(c)
  1270  	path, err := vfs.GetStore().GetFile(instance, secret)
  1271  	if err != nil {
  1272  		return WrapVfsError(err)
  1273  	}
  1274  	return sendFileFromPath(c, path, false)
  1275  }
  1276  
  1277  func versionDownloadHandler(c echo.Context, secret string) error {
  1278  	instance := middlewares.GetInstance(c)
  1279  	versionID, err := vfs.GetStore().GetVersion(instance, secret)
  1280  	if err != nil {
  1281  		return WrapVfsError(err)
  1282  	}
  1283  
  1284  	fileID := strings.Split(versionID, "/")[0]
  1285  	doc, err := instance.VFS().FileByID(fileID)
  1286  	if err != nil {
  1287  		return WrapVfsError(err)
  1288  	}
  1289  	version, err := vfs.FindVersion(instance, versionID)
  1290  	if err != nil {
  1291  		return WrapVfsError(err)
  1292  	}
  1293  
  1294  	disposition := "inline"
  1295  	if c.QueryParam("Dl") == "1" {
  1296  		disposition = "attachment"
  1297  	} else {
  1298  		addCSPRuleForDirectLink(c, doc.Class, doc.Mime)
  1299  	}
  1300  
  1301  	filename := c.Param("fake-name")
  1302  	err = vfs.ServeFileContent(instance.VFS(), doc, version, filename, disposition, c.Request(), c.Response())
  1303  	if err != nil {
  1304  		return WrapVfsError(err)
  1305  	}
  1306  	return nil
  1307  }
  1308  
  1309  // TrashHandler handles all DELETE requests on /files/:file-id and
  1310  // moves the file or directory with the specified file-id to the
  1311  // trash.
  1312  func TrashHandler(c echo.Context) error {
  1313  	instance := middlewares.GetInstance(c)
  1314  
  1315  	fileID := c.Param("file-id")
  1316  	dir, file, err := instance.VFS().DirOrFileByID(fileID)
  1317  	if err != nil {
  1318  		return WrapVfsError(err)
  1319  	}
  1320  
  1321  	err = checkPerm(c, permission.PATCH, dir, file)
  1322  	if err != nil {
  1323  		return err
  1324  	}
  1325  
  1326  	var rev string
  1327  	if dir != nil {
  1328  		rev = dir.Rev()
  1329  	} else {
  1330  		rev = file.Rev()
  1331  	}
  1332  
  1333  	if err := CheckIfMatch(c, rev); err != nil {
  1334  		return WrapVfsError(err)
  1335  	}
  1336  
  1337  	ensureCleanOldTrashedTrigger(instance)
  1338  
  1339  	if dir != nil {
  1340  		updateDirCozyMetadata(c, dir)
  1341  		doc, errt := vfs.TrashDir(instance.VFS(), dir)
  1342  		if errt != nil {
  1343  			return WrapVfsError(errt)
  1344  		}
  1345  		return dirData(c, http.StatusOK, doc)
  1346  	}
  1347  
  1348  	updateFileCozyMetadata(c, file, false)
  1349  	doc, errt := vfs.TrashFile(instance.VFS(), file)
  1350  	if errt != nil {
  1351  		return WrapVfsError(errt)
  1352  	}
  1353  	return FileData(c, http.StatusOK, doc, false, nil)
  1354  }
  1355  
  1356  // ReadTrashFilesHandler handle GET requests on /files/trash and return the
  1357  // list of trashed files and directories
  1358  func ReadTrashFilesHandler(c echo.Context) error {
  1359  	instance := middlewares.GetInstance(c)
  1360  
  1361  	trash, err := instance.VFS().DirByID(consts.TrashDirID)
  1362  	if err != nil {
  1363  		return WrapVfsError(err)
  1364  	}
  1365  
  1366  	err = checkPerm(c, permission.GET, trash, nil)
  1367  	if err != nil {
  1368  		return err
  1369  	}
  1370  
  1371  	return dirDataList(c, http.StatusOK, trash)
  1372  }
  1373  
  1374  // RestoreTrashFileHandler handle POST requests on /files/trash/file-id and
  1375  // can be used to restore a file or directory from the trash.
  1376  func RestoreTrashFileHandler(c echo.Context) error {
  1377  	instance := middlewares.GetInstance(c)
  1378  
  1379  	fileID := c.Param("file-id")
  1380  
  1381  	dir, file, err := instance.VFS().DirOrFileByID(fileID)
  1382  	if err != nil {
  1383  		return WrapVfsError(err)
  1384  	}
  1385  
  1386  	err = checkPerm(c, permission.PATCH, dir, file)
  1387  	if err != nil {
  1388  		return err
  1389  	}
  1390  
  1391  	if dir != nil {
  1392  		updateDirCozyMetadata(c, dir)
  1393  		doc, errt := vfs.RestoreDir(instance.VFS(), dir)
  1394  		if errt != nil {
  1395  			return WrapVfsError(errt)
  1396  		}
  1397  		return dirData(c, http.StatusOK, doc)
  1398  	}
  1399  
  1400  	updateFileCozyMetadata(c, file, false)
  1401  	doc, errt := vfs.RestoreFile(instance.VFS(), file)
  1402  	if errt != nil {
  1403  		return WrapVfsError(errt)
  1404  	}
  1405  	return FileData(c, http.StatusOK, doc, false, nil)
  1406  }
  1407  
  1408  // ClearTrashHandler handles DELETE request to clear the trash
  1409  func ClearTrashHandler(c echo.Context) error {
  1410  	inst := middlewares.GetInstance(c)
  1411  
  1412  	fs := inst.VFS()
  1413  	trash, err := fs.DirByID(consts.TrashDirID)
  1414  	if err != nil {
  1415  		return WrapVfsError(err)
  1416  	}
  1417  
  1418  	err = checkPerm(c, permission.DELETE, trash, nil)
  1419  	if err != nil {
  1420  		return err
  1421  	}
  1422  
  1423  	files, _ := fs.FilesUsage()
  1424  	versions, _ := fs.VersionsUsage()
  1425  	quota := fs.DiskQuota()
  1426  	freeSpace := quota - files - versions
  1427  	inTrash, _ := fs.TrashUsage()
  1428  
  1429  	err = fs.DestroyDirContent(trash, pushTrashJob(inst))
  1430  	if err != nil {
  1431  		return WrapVfsError(err)
  1432  	}
  1433  
  1434  	// As a rule of thumb if the freed space (= inTrash) was more than the free
  1435  	// space, we want to ping other instances with common sharing to tell them
  1436  	// to try reuploading files that have may have been blocked because of the
  1437  	// quota.
  1438  	if inTrash > freeSpace {
  1439  		go func() {
  1440  			i := inst.Clone().(*instance.Instance)
  1441  			if err := sharing.AskReupload(i); err != nil {
  1442  				i.Logger().WithNamespace("files").
  1443  					Warnf("sharing.AskReupload failed with %s", err)
  1444  			}
  1445  		}()
  1446  	}
  1447  
  1448  	return c.NoContent(204)
  1449  }
  1450  
  1451  // DestroyFileHandler handles DELETE request to clear one element from the trash
  1452  func DestroyFileHandler(c echo.Context) error {
  1453  	inst := middlewares.GetInstance(c)
  1454  
  1455  	fileID := c.Param("file-id")
  1456  
  1457  	dir, file, err := inst.VFS().DirOrFileByID(fileID)
  1458  	if err != nil {
  1459  		return WrapVfsError(err)
  1460  	}
  1461  
  1462  	err = checkPerm(c, permission.DELETE, dir, file)
  1463  	if err != nil {
  1464  		return err
  1465  	}
  1466  
  1467  	var rev string
  1468  	if dir != nil {
  1469  		rev = dir.Rev()
  1470  	} else {
  1471  		rev = file.Rev()
  1472  	}
  1473  
  1474  	if err = CheckIfMatch(c, rev); err != nil {
  1475  		return WrapVfsError(err)
  1476  	}
  1477  
  1478  	if dir != nil {
  1479  		err = inst.VFS().DestroyDirAndContent(dir, pushTrashJob(inst))
  1480  	} else {
  1481  		err = inst.VFS().DestroyFile(file)
  1482  	}
  1483  	if err != nil {
  1484  		return WrapVfsError(err)
  1485  	}
  1486  
  1487  	return c.NoContent(204)
  1488  }
  1489  
  1490  // FindFilesMango is the route POST /files/_find
  1491  // used to retrieve files and their metadata from a mango query.
  1492  func FindFilesMango(c echo.Context) error {
  1493  	instance := middlewares.GetInstance(c)
  1494  	var findRequest map[string]interface{}
  1495  
  1496  	if err := json.NewDecoder(c.Request().Body).Decode(&findRequest); err != nil {
  1497  		return jsonapi.Errorf(http.StatusBadRequest, "%s", err)
  1498  	}
  1499  
  1500  	if err := middlewares.AllowWholeType(c, permission.GET, consts.Files); err != nil {
  1501  		return err
  1502  	}
  1503  
  1504  	includePath := true
  1505  	if reqFields, ok := findRequest["fields"].([]interface{}); ok {
  1506  		includePath = false
  1507  		// Those fields are necessary for the JSON-API response
  1508  		fields := []string{"_id", "_rev", "type", "class", "size", "trashed"}
  1509  		for _, v := range reqFields {
  1510  			v := v.(string)
  1511  			if v == "path" {
  1512  				// path is not stored in database, but added by the stack, and
  1513  				// it requires the dir_id
  1514  				includePath = true
  1515  				fields = append(fields, "dir_id")
  1516  			}
  1517  			fields = append(fields, v)
  1518  		}
  1519  		findRequest["fields"] = fields
  1520  	}
  1521  
  1522  	limit, hasLimit := findRequest["limit"].(float64)
  1523  	if !hasLimit || limit > consts.MaxItemsPerPageForMango {
  1524  		limit = 100
  1525  	}
  1526  	if pageLimit := c.QueryParam("page[limit]"); pageLimit != "" {
  1527  		if limitInt, err := strconv.Atoi(pageLimit); err == nil {
  1528  			limit = float64(limitInt)
  1529  		}
  1530  	}
  1531  	findRequest["limit"] = limit
  1532  
  1533  	skip := 0
  1534  	if skipF64, ok := findRequest["skip"].(float64); ok {
  1535  		skip = int(skipF64)
  1536  	}
  1537  	if pageSkip := c.QueryParam("page[skip]"); pageSkip != "" {
  1538  		if skipInt, err := strconv.Atoi(pageSkip); err == nil {
  1539  			findRequest["skip"] = skipInt
  1540  			skip = skipInt
  1541  		}
  1542  	}
  1543  
  1544  	// XXX page[cursor] should be preferred to cursor, but we still accept
  1545  	// cursor to keep compatibility with the past
  1546  	if bookmark := c.QueryParam("cursor"); bookmark != "" {
  1547  		findRequest["bookmark"] = bookmark
  1548  	}
  1549  	if bookmark := c.QueryParam("page[cursor]"); bookmark != "" {
  1550  		findRequest["bookmark"] = bookmark
  1551  	}
  1552  
  1553  	var results []vfs.DirOrFileDoc
  1554  	resp, err := couchdb.FindDocsRaw(instance, consts.Files, &findRequest, &results)
  1555  	if err != nil {
  1556  		return err
  1557  	}
  1558  
  1559  	// XXX: in theory, we should avoid pagination link for POST requests, but
  1560  	// it is here and used, so let's keep it for compatibility.
  1561  	var links jsonapi.LinksList
  1562  	if resp.Bookmark != "" && len(results) >= int(limit) {
  1563  		links.Next = "/files/_find?page[cursor]=" + resp.Bookmark
  1564  	}
  1565  
  1566  	var total int
  1567  	if len(results) >= int(limit) {
  1568  		total = math.MaxInt32 - 1 // we dont know the actual number
  1569  	} else {
  1570  		total = skip + len(results) // let the client know its done.
  1571  	}
  1572  
  1573  	// Create secrets for thumbnail links in batch for performance reasons
  1574  	var thumbIDs []string
  1575  	for _, dof := range results {
  1576  		_, f := dof.Refine()
  1577  		if f != nil {
  1578  			if f.Class == "image" || f.Class == "pdf" {
  1579  				thumbIDs = append(thumbIDs, f.ID())
  1580  			}
  1581  		}
  1582  	}
  1583  	var secrets map[string]string
  1584  	if len(thumbIDs) > 0 {
  1585  		secrets, _ = vfs.GetStore().AddThumbs(instance, thumbIDs)
  1586  	}
  1587  	if secrets == nil {
  1588  		secrets = make(map[string]string)
  1589  	}
  1590  
  1591  	fp := vfs.NewFilePatherWithCache(instance.VFS())
  1592  	out := make([]jsonapi.Object, len(results))
  1593  	fields, ok := findRequest["fields"].([]string)
  1594  	for i, dof := range results {
  1595  		d, f := dof.Refine()
  1596  		if d != nil {
  1597  			if ok {
  1598  				out[i] = newFindDir(d, fields)
  1599  			} else {
  1600  				out[i] = newDir(d)
  1601  			}
  1602  		} else {
  1603  			if ok {
  1604  				file := newFindFile(f, fields, instance)
  1605  				if includePath {
  1606  					file.IncludePath(fp)
  1607  				}
  1608  				if secret, ok := secrets[f.ID()]; ok {
  1609  					file.SetThumbSecret(secret)
  1610  				}
  1611  				out[i] = file
  1612  			} else {
  1613  				file := NewFile(f, instance)
  1614  				if includePath {
  1615  					file.IncludePath(fp)
  1616  				}
  1617  				if secret, ok := secrets[f.ID()]; ok {
  1618  					file.SetThumbSecret(secret)
  1619  				}
  1620  				out[i] = file
  1621  			}
  1622  		}
  1623  	}
  1624  
  1625  	meta := jsonapi.Meta{
  1626  		Count:          &total,
  1627  		ExecutionStats: resp.ExecutionStats,
  1628  		Warning:        resp.Warning,
  1629  	}
  1630  	return jsonapi.DataListWithMeta(c, http.StatusOK, meta, out, &links)
  1631  }
  1632  
  1633  var allowedChangesParams = map[string]bool{
  1634  	// supported by CouchDB
  1635  	"since":        true,
  1636  	"limit":        true,
  1637  	"include_docs": true,
  1638  
  1639  	// custom
  1640  	"fields":            false,
  1641  	"include_file_path": false,
  1642  	"skip_deleted":      false,
  1643  	"skip_trashed":      false,
  1644  }
  1645  
  1646  // ChangesFeed is the handler for GET /files/_changes. It is similar to the
  1647  // changes feed of CouchDB with some additional options, like skip_trashed.
  1648  func ChangesFeed(c echo.Context) error {
  1649  	inst := middlewares.GetInstance(c)
  1650  	if err := middlewares.AllowWholeType(c, permission.GET, consts.Files); err != nil {
  1651  		return err
  1652  	}
  1653  
  1654  	// Drop a clear error for parameters not supported by stack
  1655  	filter := &changesFilter{}
  1656  	for key := range c.QueryParams() {
  1657  		if byCouch, ok := allowedChangesParams[key]; !ok {
  1658  			return jsonapi.Errorf(http.StatusBadRequest, "Unsupported query parameter '%s'", key)
  1659  		} else if !byCouch {
  1660  			filter.Add(key, c.QueryParam(key))
  1661  		}
  1662  	}
  1663  
  1664  	limitString := c.QueryParam("limit")
  1665  	limit := 0
  1666  	if limitString != "" {
  1667  		var err error
  1668  		if limit, err = strconv.Atoi(limitString); err != nil {
  1669  			return jsonapi.Errorf(http.StatusBadRequest, "Invalid limit value '%s': %s", limitString, err.Error())
  1670  		}
  1671  		if limit > 10000 {
  1672  			limit = 10000
  1673  		}
  1674  	}
  1675  
  1676  	includeDocs := c.QueryParam("include_docs") == "true"
  1677  	if !includeDocs && (filter.IncludePath || filter.SkipTrashed) {
  1678  		return jsonapi.Errorf(http.StatusBadRequest, "Invalid options: include_docs should be set to true")
  1679  	}
  1680  
  1681  	// Use the VFS lock for the files to avoid sending the changed feed while
  1682  	// the VFS is moving a directory.
  1683  	mu := config.Lock().ReadWrite(inst, "vfs")
  1684  	if err := mu.Lock(); err != nil {
  1685  		return err
  1686  	}
  1687  
  1688  	couchReq := &couchdb.ChangesRequest{
  1689  		DocType:     consts.Files,
  1690  		Since:       c.QueryParam("since"),
  1691  		Limit:       limit,
  1692  		IncludeDocs: includeDocs,
  1693  		Filter:      "_selector",
  1694  	}
  1695  	results, err := couchdb.PostChanges(inst, couchReq, filter)
  1696  	mu.Unlock()
  1697  	if err != nil {
  1698  		return err
  1699  	}
  1700  
  1701  	if client, ok := middlewares.GetOAuthClient(c); ok {
  1702  		err = vfs.FilterNotSynchronizedDocs(inst.VFS(), client.ID(), results)
  1703  		if err != nil {
  1704  			return err
  1705  		}
  1706  	}
  1707  
  1708  	filter.Reject(results)
  1709  	c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
  1710  	c.Response().WriteHeader(http.StatusOK)
  1711  	if err := filter.Stream(c.Response(), inst, results); err != nil {
  1712  		inst.Logger().WithNamespace("files").Warnf("error on _changes: %s", err)
  1713  		return err
  1714  	}
  1715  	return nil
  1716  }
  1717  
  1718  type changesFilter struct {
  1719  	Fields      []string
  1720  	IncludePath bool
  1721  	SkipDeleted bool
  1722  	SkipTrashed bool
  1723  	reader      io.Reader
  1724  }
  1725  
  1726  func (filter *changesFilter) Add(key, value string) {
  1727  	switch key {
  1728  	case "fields":
  1729  		filter.Fields = strings.Split(value, ",")
  1730  	case "include_file_path":
  1731  		filter.IncludePath = true
  1732  	case "skip_deleted":
  1733  		filter.SkipDeleted = true
  1734  	case "skip_trashed":
  1735  		filter.SkipTrashed = true
  1736  	}
  1737  }
  1738  
  1739  func (filter *changesFilter) Reject(results *couchdb.ChangesResponse) {
  1740  	if !filter.SkipDeleted && !filter.SkipTrashed {
  1741  		return
  1742  	}
  1743  
  1744  	changes := results.Results[:0]
  1745  	for _, change := range results.Results {
  1746  		if filter.SkipDeleted && change.Deleted {
  1747  			continue
  1748  		}
  1749  		if filter.SkipTrashed {
  1750  			if change.Doc.M["type"] == "file" && change.Doc.M["trashed"] == true {
  1751  				continue
  1752  			}
  1753  			if change.Doc.M["type"] == "directory" {
  1754  				path, _ := change.Doc.M["path"].(string)
  1755  				if path == vfs.TrashDirName {
  1756  					continue
  1757  				}
  1758  				if strings.HasPrefix(path, vfs.TrashDirName+"/") {
  1759  					continue
  1760  				}
  1761  			}
  1762  		}
  1763  		changes = append(changes, change)
  1764  	}
  1765  	results.Results = changes
  1766  }
  1767  
  1768  func (filter *changesFilter) Stream(
  1769  	w io.Writer,
  1770  	inst *instance.Instance,
  1771  	results *couchdb.ChangesResponse,
  1772  ) error {
  1773  	first := fmt.Sprintf(`{"last_seq": %q, "pending": %d, "results": [`, results.LastSeq, results.Pending)
  1774  	if _, err := w.Write([]byte(first)); err != nil {
  1775  		return err
  1776  	}
  1777  
  1778  	fp := vfs.NewFilePatherWithCache(inst.VFS())
  1779  	for i, result := range results.Results {
  1780  		if filter.IncludePath && result.Doc.M != nil && result.Doc.M["type"] == "file" {
  1781  			dirID, _ := result.Doc.M["dir_id"].(string)
  1782  			name, _ := result.Doc.M["name"].(string)
  1783  			doc := &vfs.FileDoc{DirID: dirID, DocName: name}
  1784  			if pth, err := fp.FilePath(doc); err == nil {
  1785  				result.Doc.M["path"] = pth
  1786  			}
  1787  		}
  1788  		buf, err := json.Marshal(&result)
  1789  		if err != nil {
  1790  			return err
  1791  		}
  1792  		if i != len(results.Results)-1 {
  1793  			buf = append(buf, ',')
  1794  		}
  1795  		if _, err := w.Write(buf); err != nil {
  1796  			return err
  1797  		}
  1798  	}
  1799  
  1800  	_, err := w.Write([]byte("]}"))
  1801  	return err
  1802  }
  1803  
  1804  func (filter *changesFilter) Body() []byte {
  1805  	selector := map[string]interface{}{
  1806  		"_id": map[string]interface{}{
  1807  			"$not": map[string]interface{}{
  1808  				"$regex": "^_design/",
  1809  			},
  1810  		},
  1811  	}
  1812  	payload := map[string]interface{}{
  1813  		"selector": selector,
  1814  	}
  1815  
  1816  	// Cf https://github.com/apache/couchdb/discussions/3774#discussioncomment-1416510
  1817  	if len(filter.Fields) > 0 {
  1818  		if filter.IncludePath || filter.SkipTrashed {
  1819  			for _, mandatory := range []string{"type", "name", "dir_id"} {
  1820  				found := false
  1821  				for _, f := range filter.Fields {
  1822  					if f == mandatory {
  1823  						found = true
  1824  					}
  1825  				}
  1826  				if !found {
  1827  					filter.Fields = append(filter.Fields, mandatory)
  1828  				}
  1829  			}
  1830  		}
  1831  		payload["fields"] = filter.Fields
  1832  	}
  1833  
  1834  	body, _ := json.Marshal(payload)
  1835  	return body
  1836  }
  1837  
  1838  func (filter *changesFilter) Read(p []byte) (int, error) {
  1839  	if filter.reader == nil {
  1840  		filter.reader = bytes.NewReader(filter.Body())
  1841  	}
  1842  	return filter.reader.Read(p)
  1843  }
  1844  
  1845  func (filter *changesFilter) Close() error {
  1846  	filter.reader = nil
  1847  	return nil
  1848  }
  1849  
  1850  func fsckHandler(c echo.Context) error {
  1851  	instance := middlewares.GetInstance(c)
  1852  	cacheStorage := config.GetConfig().CacheStorage
  1853  
  1854  	if err := middlewares.AllowWholeType(c, permission.GET, consts.Files); err != nil {
  1855  		return err
  1856  	}
  1857  
  1858  	noCache, _ := strconv.ParseBool(c.QueryParam("NoCache"))
  1859  	key := "fsck:" + instance.DBPrefix()
  1860  	if !noCache {
  1861  		if r, ok := cacheStorage.GetCompressed(key); ok {
  1862  			return c.Stream(http.StatusOK, echo.MIMEApplicationJSON, r)
  1863  		}
  1864  	}
  1865  
  1866  	logs := make([]*vfs.FsckLog, 0)
  1867  	err := instance.VFS().CheckFilesConsistency(func(log *vfs.FsckLog) {
  1868  		if log.Type == vfs.ContentMismatch {
  1869  			logs = append(logs, log)
  1870  		}
  1871  	}, false)
  1872  	if err != nil {
  1873  		return err
  1874  	}
  1875  
  1876  	logsData, err := json.Marshal(logs)
  1877  	if err != nil {
  1878  		return err
  1879  	}
  1880  
  1881  	if !noCache {
  1882  		expiration := utils.DurationFuzzing(3*30*24*time.Hour, 0.10)
  1883  		cacheStorage.SetCompressed(key, logsData, expiration)
  1884  	}
  1885  
  1886  	return c.JSONBlob(http.StatusOK, logsData)
  1887  }
  1888  
  1889  // Routes sets the routing for the files service
  1890  func Routes(router *echo.Group) {
  1891  	router.HEAD("/download", ReadFileContentFromPathHandler)
  1892  	router.GET("/download", ReadFileContentFromPathHandler)
  1893  	router.HEAD("/download/:file-id", ReadFileContentFromIDHandler)
  1894  	router.GET("/download/:file-id", ReadFileContentFromIDHandler)
  1895  
  1896  	router.HEAD("/download/:file-id/:version-id", ReadFileContentFromVersion)
  1897  	router.GET("/download/:file-id/:version-id", ReadFileContentFromVersion)
  1898  	router.POST("/revert/:file-id/:version-id", RevertFileVersion)
  1899  	router.PATCH("/:file-id/:version-id", ModifyFileVersionMetadata)
  1900  	router.DELETE("/:file-id/:version-id", DeleteFileVersionMetadata)
  1901  	router.POST("/:file-id/versions", CopyVersionHandler)
  1902  	router.DELETE("/versions", ClearOldVersions)
  1903  
  1904  	router.POST("/_find", FindFilesMango)
  1905  	router.GET("/_changes", ChangesFeed)
  1906  
  1907  	router.HEAD("/:file-id", HeadDirOrFile)
  1908  
  1909  	router.GET("/metadata", ReadMetadataFromPathHandler)
  1910  	router.GET("/:file-id", ReadMetadataFromIDHandler)
  1911  	router.GET("/:file-id/relationships/contents", GetChildrenHandler)
  1912  	router.GET("/:file-id/size", GetDirSize)
  1913  
  1914  	router.PATCH("/metadata", ModifyMetadataByPathHandler)
  1915  	router.PATCH("/:file-id", ModifyMetadataByIDHandler)
  1916  	router.PATCH("/", ModifyMetadataByIDInBatchHandler)
  1917  
  1918  	router.POST("/shared-drives", SharedDrivesCreationHandler)
  1919  	router.POST("/", CreationHandler)
  1920  	router.POST("/:file-id", CreationHandler)
  1921  	router.PUT("/:file-id", OverwriteFileContentHandler)
  1922  	router.POST("/upload/metadata", UploadMetadataHandler)
  1923  	router.POST("/:file-id/copy", FileCopyHandler)
  1924  
  1925  	router.GET("/:file-id/icon/:secret", IconHandler)
  1926  	router.GET("/:file-id/preview/:secret", PreviewHandler)
  1927  	router.GET("/:file-id/thumbnails/:secret/:format", ThumbnailHandler)
  1928  
  1929  	router.POST("/archive", ArchiveDownloadCreateHandler)
  1930  	router.GET("/archive/:secret/:fake-name", ArchiveDownloadHandler)
  1931  
  1932  	router.POST("/downloads", FileDownloadCreateHandler)
  1933  	router.GET("/downloads/:secret/:fake-name", FileDownloadHandler)
  1934  
  1935  	router.POST("/:file-id/relationships/referenced_by", AddReferencedHandler)
  1936  	router.DELETE("/:file-id/relationships/referenced_by", RemoveReferencedHandler)
  1937  
  1938  	router.POST("/:file-id/relationships/not_synchronized_on", AddNotSynchronizedOn)
  1939  	router.DELETE("/:file-id/relationships/not_synchronized_on", RemoveNotSynchronizedOn)
  1940  
  1941  	router.GET("/trash", ReadTrashFilesHandler)
  1942  	router.DELETE("/trash", ClearTrashHandler)
  1943  
  1944  	router.POST("/trash/:file-id", RestoreTrashFileHandler)
  1945  	router.DELETE("/trash/:file-id", DestroyFileHandler)
  1946  
  1947  	router.DELETE("/:file-id", TrashHandler)
  1948  	router.GET("/fsck", fsckHandler)
  1949  }
  1950  
  1951  // WrapVfsError returns a formatted error from a golang error emitted by the vfs
  1952  func WrapVfsError(err error) error {
  1953  	if errj := wrapVfsError(err); errj != nil {
  1954  		return errj
  1955  	}
  1956  	return err
  1957  }
  1958  
  1959  func wrapVfsErrorJSONAPI(err error) *jsonapi.Error {
  1960  	if errj := wrapVfsError(err); errj != nil {
  1961  		return errj
  1962  	}
  1963  	return jsonapi.InternalServerError(err)
  1964  }
  1965  
  1966  func wrapVfsError(err error) *jsonapi.Error {
  1967  	switch err {
  1968  	case ErrDocTypeInvalid:
  1969  		return jsonapi.InvalidAttribute("type", err)
  1970  	case os.ErrExist:
  1971  		return jsonapi.Conflict(err)
  1972  	case os.ErrNotExist, swift.ObjectNotFound:
  1973  		return jsonapi.NotFound(err)
  1974  	case vfs.ErrParentDoesNotExist:
  1975  		return jsonapi.NotFound(err)
  1976  	case vfs.ErrParentInTrash:
  1977  		return jsonapi.NotFound(err)
  1978  	case vfs.ErrForbiddenDocMove:
  1979  		return jsonapi.PreconditionFailed("dir-id", err)
  1980  	case vfs.ErrIllegalFilename:
  1981  		return jsonapi.InvalidParameter("name", err)
  1982  	case vfs.ErrIllegalPath:
  1983  		return jsonapi.InvalidParameter("path", err)
  1984  	case vfs.ErrIllegalMime:
  1985  		return jsonapi.InvalidParameter("mime", err)
  1986  	case vfs.ErrIllegalTime:
  1987  		return jsonapi.InvalidParameter("UpdatedAt", err)
  1988  	case vfs.ErrInvalidHash:
  1989  		return jsonapi.PreconditionFailed("Content-MD5", err)
  1990  	case vfs.ErrContentLengthMismatch:
  1991  		return jsonapi.PreconditionFailed("Content-Length", err)
  1992  	case vfs.ErrConflict:
  1993  		return jsonapi.Conflict(err)
  1994  	case vfs.ErrFileInTrash, vfs.ErrNonAbsolutePath,
  1995  		vfs.ErrDirNotEmpty:
  1996  		return jsonapi.BadRequest(err)
  1997  	case vfs.ErrFileTooBig, vfs.ErrMaxFileSize:
  1998  		return jsonapi.Errorf(http.StatusRequestEntityTooLarge, "%s", err)
  1999  	case vfs.ErrWrongToken:
  2000  		return jsonapi.BadRequest(err)
  2001  	case vfs.ErrInvalidMetadataID:
  2002  		return jsonapi.InvalidParameter("MetadataID", err)
  2003  	}
  2004  	if _, ok := err.(*jsonapi.Error); !ok {
  2005  		logger.WithNamespace("files").Warnf("Not wrapped error: %s", err)
  2006  	}
  2007  	return nil
  2008  }
  2009  
  2010  // FileDocFromReq creates a FileDoc from an incoming request.
  2011  func FileDocFromReq(c echo.Context, name, dirID string) (*vfs.FileDoc, error) {
  2012  	header := c.Request().Header
  2013  	size := c.Request().ContentLength
  2014  	if size == -1 {
  2015  		if param := c.QueryParam("Size"); param != "" {
  2016  			if s, err := strconv.ParseInt(param, 10, 64); err == nil {
  2017  				size = s
  2018  			}
  2019  		}
  2020  	}
  2021  
  2022  	var err error
  2023  	var md5Sum []byte
  2024  	if md5Str := header.Get("Content-MD5"); md5Str != "" {
  2025  		md5Sum, err = parseMD5Hash(md5Str)
  2026  	}
  2027  	if err != nil {
  2028  		err = jsonapi.InvalidParameter("Content-MD5", err)
  2029  		return nil, err
  2030  	}
  2031  
  2032  	cdate := time.Now()
  2033  	if date := header.Get("Date"); date != "" {
  2034  		if t, err := time.Parse(time.RFC1123, date); err == nil {
  2035  			cdate = t
  2036  		}
  2037  	}
  2038  
  2039  	var mime, class string
  2040  	contentType := header.Get(echo.HeaderContentType)
  2041  	if contentType == "" || contentType == echo.MIMEOctetStream {
  2042  		mime, class = vfs.ExtractMimeAndClassFromFilename(name)
  2043  	} else {
  2044  		ext := strings.ToLower(path.Ext(name))
  2045  		// Force the mime-type for .url files
  2046  		if ext == ".url" {
  2047  			contentType = consts.ShortcutMimeType
  2048  		}
  2049  		if contentType == "text/xml" && ext == "svg" {
  2050  			contentType = "image/svg+xml"
  2051  		}
  2052  		mime, class = vfs.ExtractMimeAndClass(contentType)
  2053  	}
  2054  
  2055  	tags := strings.Split(c.QueryParam("Tags"), TagSeparator)
  2056  	executable := c.QueryParam("Executable") == "true"
  2057  	encrypted := c.QueryParam("Encrypted") == "true"
  2058  	trashed := false
  2059  	doc, err := vfs.NewFileDoc(
  2060  		name,
  2061  		dirID,
  2062  		size,
  2063  		md5Sum,
  2064  		mime,
  2065  		class,
  2066  		cdate,
  2067  		executable,
  2068  		trashed,
  2069  		encrypted,
  2070  		tags,
  2071  	)
  2072  	if err != nil {
  2073  		return nil, err
  2074  	}
  2075  
  2076  	// This way to send metadata is deprecated, but is still here to ensure
  2077  	// compatibility with existing clients.
  2078  	if meta := c.QueryParam("Metadata"); meta != "" {
  2079  		if err := json.Unmarshal([]byte(meta), &doc.Metadata); err != nil {
  2080  			return nil, err
  2081  		}
  2082  	}
  2083  
  2084  	if secret := c.QueryParam("MetadataID"); secret != "" {
  2085  		instance := middlewares.GetInstance(c)
  2086  		meta, err := vfs.GetStore().GetMetadata(instance, secret)
  2087  		if err != nil {
  2088  			return nil, err
  2089  		}
  2090  		doc.Metadata = *meta
  2091  	}
  2092  
  2093  	if len(doc.Metadata) > 0 {
  2094  		if _, ok := doc.Metadata[consts.CarbonCopyKey]; ok {
  2095  			if err := middlewares.AllowWholeType(c, permission.POST, consts.CertifiedCarbonCopy); err != nil {
  2096  				delete(doc.Metadata, consts.CarbonCopyKey)
  2097  			}
  2098  		}
  2099  		if _, ok := doc.Metadata[consts.ElectronicSafeKey]; ok {
  2100  			if err := middlewares.AllowWholeType(c, permission.POST, consts.CertifiedElectronicSafe); err != nil {
  2101  				delete(doc.Metadata, consts.ElectronicSafeKey)
  2102  			}
  2103  		}
  2104  	}
  2105  
  2106  	return doc, nil
  2107  }
  2108  
  2109  // CheckIfMatch checks if the revision provided matches the revision number
  2110  // given in the request, in the header and/or the query.
  2111  func CheckIfMatch(c echo.Context, rev string) error {
  2112  	ifMatch := c.Request().Header.Get("If-Match")
  2113  	revQuery := c.QueryParam("rev")
  2114  	var wantedRev string
  2115  	if ifMatch != "" {
  2116  		wantedRev = ifMatch
  2117  	}
  2118  	if revQuery != "" && wantedRev == "" {
  2119  		wantedRev = revQuery
  2120  	}
  2121  	return checkIfMatch(rev, wantedRev)
  2122  }
  2123  
  2124  func checkIfMatch(rev, wantedRev string) error {
  2125  	if wantedRev != "" && rev != wantedRev {
  2126  		return jsonapi.PreconditionFailed("If-Match", fmt.Errorf("Revision does not match"))
  2127  	}
  2128  	return nil
  2129  }
  2130  
  2131  func checkPerm(c echo.Context, v permission.Verb, d *vfs.DirDoc, f *vfs.FileDoc) error {
  2132  	if d != nil {
  2133  		return middlewares.AllowVFS(c, v, d)
  2134  	}
  2135  	return middlewares.AllowVFS(c, v, f)
  2136  }
  2137  
  2138  func parseMD5Hash(md5B64 string) ([]byte, error) {
  2139  	// Encoded md5 hash in base64 should at least have 22 caracters in
  2140  	// base64: 16*3/4 = 21+1/3
  2141  	//
  2142  	// The padding may add up to 2 characters (non useful). If we are
  2143  	// out of these boundaries we know we don't have a good hash and we
  2144  	// can bail immediately.
  2145  	if len(md5B64) < 22 || len(md5B64) > 24 {
  2146  		return nil, fmt.Errorf("Given Content-MD5 is invalid")
  2147  	}
  2148  
  2149  	md5Sum, err := base64.StdEncoding.DecodeString(md5B64)
  2150  	if err != nil || len(md5Sum) != 16 {
  2151  		return nil, fmt.Errorf("Given Content-MD5 is invalid")
  2152  	}
  2153  
  2154  	return md5Sum, nil
  2155  }
  2156  
  2157  func pushTrashJob(inst *instance.Instance) func(vfs.TrashJournal) error {
  2158  	return func(journal vfs.TrashJournal) error {
  2159  		msg, err := job.NewMessage(journal)
  2160  		if err != nil {
  2161  			return err
  2162  		}
  2163  		_, err = job.System().PushJob(inst, &job.JobRequest{
  2164  			WorkerType: "trash-files",
  2165  			Message:    msg,
  2166  		})
  2167  		return err
  2168  	}
  2169  }
  2170  
  2171  func ensureCleanOldTrashedTrigger(inst *instance.Instance) {
  2172  	// 1. Check if we need a trigger for clean-old-trashed worker
  2173  	cfg := config.GetConfig().Fs.AutoCleanTrashedAfter
  2174  	after, ok := cfg[inst.ContextName]
  2175  	if !ok || after == "" {
  2176  		return
  2177  	}
  2178  
  2179  	// 2. Check if the trigger already exists
  2180  	sched := job.System()
  2181  	infos := job.TriggerInfos{
  2182  		Type:       "@cron",
  2183  		WorkerType: "clean-old-trashed",
  2184  	}
  2185  	if sched.HasTrigger(inst, infos) {
  2186  		return
  2187  	}
  2188  
  2189  	// 3. Create the trigger
  2190  	now := time.Now()
  2191  	hours := (now.Hour() + 12) % 24
  2192  	infos.Arguments = fmt.Sprintf("0 %d %d * * *", now.Minute(), hours)
  2193  	trigger, err := job.NewTrigger(inst, infos, nil)
  2194  	if err != nil {
  2195  		inst.Logger().Errorf("Cannot create clean-old-trashed trigger: %s", err)
  2196  		return
  2197  	}
  2198  	if err = sched.AddTrigger(trigger); err != nil {
  2199  		inst.Logger().Errorf("Cannot create clean-old-trashed trigger: %s", err)
  2200  	}
  2201  }
  2202  
  2203  func instanceURL(c echo.Context) string {
  2204  	return middlewares.GetInstance(c).PageURL("/", nil)
  2205  }
  2206  
  2207  func updateDirCozyMetadata(c echo.Context, dir *vfs.DirDoc) {
  2208  	fcm, _ := CozyMetadataFromClaims(c, false)
  2209  	if dir.CozyMetadata == nil {
  2210  		fcm.CreatedAt = dir.CreatedAt
  2211  		fcm.CreatedByApp = ""
  2212  		fcm.CreatedByAppVersion = ""
  2213  		dir.CozyMetadata = fcm
  2214  	} else {
  2215  		dir.CozyMetadata.UpdatedAt = fcm.UpdatedAt
  2216  		if len(fcm.UpdatedByApps) > 0 {
  2217  			dir.CozyMetadata.UpdatedByApp(fcm.UpdatedByApps[0])
  2218  		}
  2219  	}
  2220  }
  2221  
  2222  func updateFileCozyMetadata(c echo.Context, file *vfs.FileDoc, setUploadFields bool) {
  2223  	var oldSourceAccount, oldSourceIdentifier string
  2224  	fcm, slug := CozyMetadataFromClaims(c, setUploadFields)
  2225  	if file.CozyMetadata == nil {
  2226  		fcm.CreatedAt = file.CreatedAt
  2227  		fcm.CreatedByApp = ""
  2228  		fcm.CreatedByAppVersion = ""
  2229  		uploadedAt := file.CreatedAt
  2230  		fcm.UploadedAt = &uploadedAt
  2231  		file.CozyMetadata = fcm
  2232  	} else {
  2233  		oldSourceAccount = file.CozyMetadata.SourceAccount
  2234  		oldSourceIdentifier = file.CozyMetadata.SourceIdentifier
  2235  		file.CozyMetadata.UpdatedAt = fcm.UpdatedAt
  2236  		if len(fcm.UpdatedByApps) > 0 {
  2237  			file.CozyMetadata.UpdatedByApp(fcm.UpdatedByApps[0])
  2238  		}
  2239  		if setUploadFields {
  2240  			file.CozyMetadata.UploadedAt = fcm.UploadedAt
  2241  			file.CozyMetadata.UploadedBy = fcm.UploadedBy
  2242  			file.CozyMetadata.UploadedOn = fcm.UploadedOn
  2243  		}
  2244  	}
  2245  
  2246  	if setUploadFields {
  2247  		if oldSourceAccount == "" && fcm.SourceAccount != "" {
  2248  			file.CozyMetadata.SourceAccount = fcm.SourceAccount
  2249  			// To ease the transition to cozyMetadata for io.cozy.files, we fill
  2250  			// the CreatedByApp for konnectors that updates a file: the stack can
  2251  			// recognize that by the presence of the SourceAccount.
  2252  			if file.CozyMetadata.CreatedByApp == "" && slug != "" {
  2253  				file.CozyMetadata.CreatedByApp = slug
  2254  			}
  2255  		}
  2256  		if oldSourceIdentifier == "" && fcm.SourceIdentifier != "" {
  2257  			file.CozyMetadata.SourceIdentifier = fcm.SourceIdentifier
  2258  		}
  2259  	}
  2260  }
  2261  
  2262  // CozyMetadataFromClaims returns a FilesCozyMetadata struct, with the app
  2263  // fields filled with information from the permission claims.
  2264  func CozyMetadataFromClaims(c echo.Context, setUploadFields bool) (*vfs.FilesCozyMetadata, string) {
  2265  	fcm := vfs.NewCozyMetadata(instanceURL(c))
  2266  
  2267  	var slug, version string
  2268  	var client map[string]string
  2269  	if claims := c.Get("claims"); claims != nil {
  2270  		cl := claims.(permission.Claims)
  2271  		switch cl.AudienceString() {
  2272  		case consts.AppAudience, consts.KonnectorAudience:
  2273  			slug = cl.Subject
  2274  		case consts.AccessTokenAudience:
  2275  			if perms, err := middlewares.GetPermission(c); err == nil {
  2276  				if cli, ok := perms.Client.(*oauth.Client); ok {
  2277  					slug = oauth.GetLinkedAppSlug(cli.SoftwareID)
  2278  					// Special case for cozy-desktop: it is an OAuth app not linked
  2279  					// to a web app, so it has no slug, but we still want to keep
  2280  					// in cozyMetadata its changes, so we use a fake slug.
  2281  					if slug == "" && strings.Contains(cli.SoftwareID, "cozy-desktop") {
  2282  						slug = "cozy-desktop"
  2283  					}
  2284  					// Special case for the flagship app
  2285  					if slug == "" && cli.Flagship {
  2286  						slug = "cozy-flagship"
  2287  					}
  2288  					version = cli.SoftwareVersion
  2289  					client = map[string]string{
  2290  						"id":   cli.ID(),
  2291  						"kind": cli.ClientKind,
  2292  						"name": cli.ClientName,
  2293  					}
  2294  				}
  2295  			}
  2296  		}
  2297  	}
  2298  
  2299  	if slug != "" {
  2300  		fcm.CreatedByApp = slug
  2301  		fcm.CreatedByAppVersion = version
  2302  		fcm.UpdatedByApps = []*metadata.UpdatedByAppEntry{
  2303  			{
  2304  				Slug:     slug,
  2305  				Version:  version,
  2306  				Date:     fcm.UpdatedAt,
  2307  				Instance: fcm.CreatedOn,
  2308  			},
  2309  		}
  2310  	}
  2311  
  2312  	if setUploadFields {
  2313  		uploadedAt := fcm.CreatedAt
  2314  		fcm.UploadedAt = &uploadedAt
  2315  		fcm.UploadedOn = fcm.CreatedOn
  2316  		if slug != "" {
  2317  			fcm.UploadedBy = &vfs.UploadedByEntry{
  2318  				Slug:    slug,
  2319  				Version: version,
  2320  				Client:  client,
  2321  			}
  2322  		}
  2323  	}
  2324  
  2325  	if account := c.QueryParam("SourceAccount"); account != "" {
  2326  		fcm.SourceAccount = account
  2327  	}
  2328  	if id := c.QueryParam("SourceAccountIdentifier"); id != "" {
  2329  		fcm.SourceIdentifier = id
  2330  	}
  2331  
  2332  	return fcm, slug
  2333  }
  2334  
  2335  func fileCopyName(inst *instance.Instance, name string) string {
  2336  	base, ext := name, ""
  2337  	ext = filepath.Ext(name)
  2338  	base = strings.TrimSuffix(base, ext)
  2339  	suffix := inst.Translate("File copy Suffix")
  2340  
  2341  	return fmt.Sprintf("%s (%s)%s", base, suffix, ext)
  2342  }