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

     1  package files
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"net/http"
     8  	"net/url"
     9  	"strings"
    10  
    11  	"github.com/cozy/cozy-stack/model/instance"
    12  	"github.com/cozy/cozy-stack/model/permission"
    13  	"github.com/cozy/cozy-stack/model/vfs"
    14  	"github.com/cozy/cozy-stack/pkg/config/config"
    15  	"github.com/cozy/cozy-stack/pkg/consts"
    16  	"github.com/cozy/cozy-stack/pkg/couchdb"
    17  	"github.com/cozy/cozy-stack/pkg/jsonapi"
    18  	"github.com/cozy/cozy-stack/web/middlewares"
    19  	"github.com/labstack/echo/v4"
    20  )
    21  
    22  const (
    23  	defaultRefsPerPage = 100
    24  	maxRefsPerPage     = 1000
    25  )
    26  
    27  func rawMessageToObject(i *instance.Instance, bb json.RawMessage) (jsonapi.Object, error) {
    28  	var dof vfs.DirOrFileDoc
    29  	err := json.Unmarshal(bb, &dof)
    30  	if err != nil {
    31  		return nil, err
    32  	}
    33  	d, f := dof.Refine()
    34  	if d != nil {
    35  		return newDir(d), nil
    36  	}
    37  
    38  	return NewFile(f, i), nil
    39  }
    40  
    41  // ListReferencesHandler list all files referenced by a doc
    42  // GET /data/:type/:id/relationships/references
    43  // Beware, this is actually used in the web/data Routes
    44  func ListReferencesHandler(c echo.Context) error {
    45  	instance := middlewares.GetInstance(c)
    46  
    47  	doctype := c.Param("doctype")
    48  	id := getDocID(c)
    49  	include := c.QueryParam("include")
    50  	includeDocs := include == "files"
    51  
    52  	if err := middlewares.AllowTypeAndID(c, permission.GET, doctype, id); err != nil {
    53  		if middlewares.AllowWholeType(c, permission.GET, consts.Files) != nil {
    54  			return err
    55  		}
    56  	}
    57  
    58  	cursor, err := jsonapi.ExtractPaginationCursor(c, defaultRefsPerPage, maxRefsPerPage)
    59  	if err != nil {
    60  		return err
    61  	}
    62  
    63  	mu := config.Lock().ReadWrite(instance, "vfs")
    64  	_ = mu.RLock()
    65  	defer mu.RUnlock()
    66  
    67  	key := []string{doctype, id}
    68  	reqCount := &couchdb.ViewRequest{Key: key, Reduce: true}
    69  
    70  	var resCount couchdb.ViewResponse
    71  	err = couchdb.ExecView(instance, couchdb.FilesReferencedByView, reqCount, &resCount)
    72  	if err != nil {
    73  		return err
    74  	}
    75  
    76  	count := 0
    77  	if len(resCount.Rows) > 0 {
    78  		count = int(resCount.Rows[0].Value.(float64))
    79  	}
    80  
    81  	// XXX Some references can contain `%2f` instead of `/` in the id (legacy),
    82  	// and to preserve compatibility, we try to find those documents if no
    83  	// documents with the correct reference are found.
    84  	if count == 0 && strings.Contains(id, "/") {
    85  		key[1] = c.Param("docid")
    86  		err = couchdb.ExecView(instance, couchdb.FilesReferencedByView, reqCount, &resCount)
    87  		if err == nil && len(resCount.Rows) > 0 {
    88  			count = int(resCount.Rows[0].Value.(float64))
    89  		}
    90  	}
    91  	meta := &jsonapi.Meta{Count: &count}
    92  
    93  	sort := c.QueryParam("sort")
    94  	descending := strings.HasPrefix(sort, "-")
    95  	start := key
    96  	end := []string{key[0], key[1], couchdb.MaxString}
    97  	if descending {
    98  		start, end = end, start
    99  	}
   100  	var view *couchdb.View
   101  	switch sort {
   102  	case "", "id", "-id":
   103  		view = couchdb.FilesReferencedByView
   104  	case "datetime", "-datetime":
   105  		view = couchdb.ReferencedBySortedByDatetimeView
   106  	default:
   107  		return jsonapi.BadRequest(errors.New("Invalid sort parameter"))
   108  	}
   109  
   110  	req := &couchdb.ViewRequest{
   111  		StartKey:    start,
   112  		EndKey:      end,
   113  		IncludeDocs: includeDocs,
   114  		Reduce:      false,
   115  		Descending:  descending,
   116  	}
   117  	cursor.ApplyTo(req)
   118  
   119  	var res couchdb.ViewResponse
   120  	err = couchdb.ExecView(instance, view, req, &res)
   121  	if err != nil {
   122  		return err
   123  	}
   124  
   125  	cursor.UpdateFrom(&res)
   126  
   127  	links := &jsonapi.LinksList{}
   128  	if cursor.HasMore() {
   129  		params, err2 := jsonapi.PaginationCursorToParams(cursor)
   130  		if err2 != nil {
   131  			return err2
   132  		}
   133  		links.Next = fmt.Sprintf("%s?%s",
   134  			c.Request().URL.Path, params.Encode())
   135  	}
   136  
   137  	refs := make([]couchdb.DocReference, len(res.Rows))
   138  	var docs []jsonapi.Object
   139  	if includeDocs {
   140  		docs = make([]jsonapi.Object, len(res.Rows))
   141  	}
   142  
   143  	var thumbIDs []string
   144  	for i, row := range res.Rows {
   145  		refs[i] = couchdb.DocReference{
   146  			ID:   row.ID,
   147  			Type: consts.Files,
   148  		}
   149  
   150  		if includeDocs {
   151  			docs[i], err = rawMessageToObject(instance, row.Doc)
   152  			if err != nil {
   153  				return err
   154  			}
   155  			if f, ok := docs[i].(*file); ok {
   156  				if f.doc.Class == "image" || f.doc.Class == "pdf" {
   157  					thumbIDs = append(thumbIDs, f.ID())
   158  				}
   159  			}
   160  		}
   161  	}
   162  
   163  	// Create secrets for thumbnail links in batch for performance reasons
   164  	if len(thumbIDs) > 0 {
   165  		if secrets, err := vfs.GetStore().AddThumbs(instance, thumbIDs); err == nil {
   166  			for _, doc := range docs {
   167  				if f, ok := doc.(*file); ok {
   168  					if secret, ok := secrets[f.ID()]; ok {
   169  						f.SetThumbSecret(secret)
   170  					}
   171  				}
   172  			}
   173  		}
   174  	}
   175  
   176  	return jsonapi.DataRelations(c, http.StatusOK, refs, meta, links, docs)
   177  }
   178  
   179  // AddReferencesHandler add some files references to a doc
   180  // POST /data/:type/:id/relationships/references
   181  // Beware, this is actually used in the web/data Routes
   182  func AddReferencesHandler(c echo.Context) error {
   183  	instance := middlewares.GetInstance(c)
   184  
   185  	doctype := c.Param("doctype")
   186  	id := getDocID(c)
   187  
   188  	references, err := jsonapi.BindRelations(c.Request())
   189  	if err != nil {
   190  		return WrapVfsError(err)
   191  	}
   192  
   193  	docRef := couchdb.DocReference{
   194  		Type: doctype,
   195  		ID:   id,
   196  	}
   197  
   198  	if err = middlewares.AllowTypeAndID(c, permission.PUT, doctype, id); err != nil {
   199  		if middlewares.AllowWholeType(c, permission.PATCH, consts.Files) != nil {
   200  			return err
   201  		}
   202  	}
   203  	docs := make([]interface{}, len(references))
   204  	oldDocs := make([]interface{}, len(references))
   205  
   206  	fs := instance.VFS()
   207  	for i, fRef := range references {
   208  		dir, file, errd := fs.DirOrFileByID(fRef.ID)
   209  		if errd != nil {
   210  			return WrapVfsError(errd)
   211  		}
   212  		if dir != nil {
   213  			oldDir := dir.Clone()
   214  			dir.AddReferencedBy(docRef)
   215  			updateDirCozyMetadata(c, dir)
   216  			docs[i] = dir
   217  			oldDocs[i] = oldDir
   218  		} else {
   219  			oldFile := file.Clone().(*vfs.FileDoc)
   220  			file.AddReferencedBy(docRef)
   221  			updateFileCozyMetadata(c, file, false)
   222  			_, _ = file.Path(fs)    // Ensure the fullpath is filled to realtime
   223  			_, _ = oldFile.Path(fs) // Ensure the fullpath is filled to realtime
   224  			docs[i] = file
   225  			oldDocs[i] = oldFile
   226  		}
   227  	}
   228  	// Use bulk update for better performances
   229  	defer lockVFS(instance)()
   230  	err = couchdb.BulkUpdateDocs(instance, consts.Files, docs, oldDocs)
   231  	if err != nil {
   232  		return WrapVfsError(err)
   233  	}
   234  	return c.NoContent(204)
   235  }
   236  
   237  // RemoveReferencesHandler remove some files references from a doc
   238  // DELETE /data/:type/:id/relationships/references
   239  // Beware, this is actually used in the web/data Routes
   240  func RemoveReferencesHandler(c echo.Context) error {
   241  	instance := middlewares.GetInstance(c)
   242  
   243  	doctype := c.Param("doctype")
   244  	id := getDocID(c)
   245  
   246  	references, err := jsonapi.BindRelations(c.Request())
   247  	if err != nil {
   248  		return WrapVfsError(err)
   249  	}
   250  
   251  	docRef := couchdb.DocReference{
   252  		Type: doctype,
   253  		ID:   id,
   254  	}
   255  
   256  	// XXX References with an ID that contains a / could have it encoded as %2F
   257  	// (legacy). We delete the references for both versions to preserve
   258  	// compatibility.
   259  	var altRef *couchdb.DocReference
   260  	if strings.Contains(id, "/") {
   261  		altRef = &couchdb.DocReference{
   262  			Type: doctype,
   263  			ID:   c.Param("docid"),
   264  		}
   265  	}
   266  
   267  	if err := middlewares.AllowTypeAndID(c, permission.DELETE, doctype, id); err != nil {
   268  		if middlewares.AllowWholeType(c, permission.PATCH, consts.Files) != nil {
   269  			return err
   270  		}
   271  	}
   272  	docs := make([]interface{}, len(references))
   273  	oldDocs := make([]interface{}, len(references))
   274  
   275  	fs := instance.VFS()
   276  	for i, fRef := range references {
   277  		dir, file, err := fs.DirOrFileByID(fRef.ID)
   278  		if err != nil {
   279  			return WrapVfsError(err)
   280  		}
   281  		if dir != nil {
   282  			oldDir := dir.Clone()
   283  			dir.RemoveReferencedBy(docRef)
   284  			if altRef != nil {
   285  				dir.RemoveReferencedBy(*altRef)
   286  			}
   287  			updateDirCozyMetadata(c, dir)
   288  			docs[i] = dir
   289  			oldDocs[i] = oldDir
   290  		} else {
   291  			oldFile := file.Clone().(*vfs.FileDoc)
   292  			file.RemoveReferencedBy(docRef)
   293  			if altRef != nil {
   294  				file.RemoveReferencedBy(*altRef)
   295  			}
   296  			updateFileCozyMetadata(c, file, false)
   297  			_, _ = file.Path(fs)    // Ensure the fullpath is filled to realtime
   298  			_, _ = oldFile.Path(fs) // Ensure the fullpath is filled to realtime
   299  			docs[i] = file
   300  			oldDocs[i] = oldFile
   301  		}
   302  	}
   303  	// Use bulk update for better performances
   304  	defer lockVFS(instance)()
   305  	err = couchdb.BulkUpdateDocs(instance, consts.Files, docs, oldDocs)
   306  	if err != nil {
   307  		return WrapVfsError(err)
   308  	}
   309  	return c.NoContent(204)
   310  }
   311  
   312  func getDocID(c echo.Context) string {
   313  	id := c.Param("docid")
   314  	if docid, err := url.PathUnescape(id); err == nil {
   315  		return docid
   316  	}
   317  	return id
   318  }
   319  
   320  // ReferencesRoutes adds the /data/:doctype/:docid/relationships/references routes.
   321  func ReferencesRoutes(router *echo.Group) {
   322  	router.GET("/:docid/relationships/references", ListReferencesHandler)
   323  	router.POST("/:docid/relationships/references", AddReferencesHandler)
   324  	router.DELETE("/:docid/relationships/references", RemoveReferencesHandler)
   325  }