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

     1  package files
     2  
     3  // Links is used to generate a JSON-API link for the directory (part of
     4  import (
     5  	"encoding/json"
     6  	"time"
     7  
     8  	"github.com/cozy/cozy-stack/model/instance"
     9  	"github.com/cozy/cozy-stack/model/note"
    10  	"github.com/cozy/cozy-stack/model/vfs"
    11  	"github.com/cozy/cozy-stack/pkg/consts"
    12  	"github.com/cozy/cozy-stack/pkg/couchdb"
    13  	"github.com/cozy/cozy-stack/pkg/jsonapi"
    14  	"github.com/cozy/cozy-stack/web/middlewares"
    15  	"github.com/labstack/echo/v4"
    16  )
    17  
    18  const (
    19  	defPerPage = 30
    20  )
    21  
    22  type apiArchive struct {
    23  	*vfs.Archive
    24  }
    25  
    26  type apiMetadata struct {
    27  	*vfs.Metadata
    28  	secret string
    29  }
    30  
    31  type dir struct {
    32  	doc      *vfs.DirDoc
    33  	rel      jsonapi.RelationshipMap
    34  	included []jsonapi.Object
    35  }
    36  
    37  type file struct {
    38  	doc        *vfs.FileDoc
    39  	instance   *instance.Instance
    40  	versions   []*vfs.Version
    41  	noteImages []jsonapi.Object
    42  	// fileJSON is used for marshaling to JSON and we keep a reference here to
    43  	// avoid many allocations.
    44  	jsonDoc     *fileJSON
    45  	thumbSecret string
    46  	includePath bool
    47  }
    48  
    49  type fileJSON struct {
    50  	*vfs.FileDoc
    51  	// XXX Hide the internal_vfs_id and referenced_by
    52  	InternalID   *interface{} `json:"internal_vfs_id,omitempty"`
    53  	ReferencedBy *interface{} `json:"referenced_by,omitempty"`
    54  	// Include the path if asked for
    55  	Fullpath string `json:"path,omitempty"`
    56  }
    57  
    58  func newDir(doc *vfs.DirDoc) *dir {
    59  	rel := jsonapi.RelationshipMap{
    60  		"referenced_by": jsonapi.Relationship{
    61  			Links: &jsonapi.LinksList{
    62  				Self: "/files/" + doc.ID() + "/relationships/references",
    63  			},
    64  			Data: doc.ReferencedBy,
    65  		},
    66  	}
    67  	return &dir{doc: doc, rel: rel}
    68  }
    69  
    70  func getDirData(c echo.Context, doc *vfs.DirDoc) (int, couchdb.Cursor, []vfs.DirOrFileDoc, error) {
    71  	instance := middlewares.GetInstance(c)
    72  	fs := instance.VFS()
    73  
    74  	cursor, err := jsonapi.ExtractPaginationCursor(c, defPerPage, 0)
    75  	if err != nil {
    76  		return 0, nil, nil, err
    77  	}
    78  
    79  	count, err := fs.DirLength(doc)
    80  	if err != nil {
    81  		return 0, nil, nil, err
    82  	}
    83  
    84  	// Hide the trash folder when listing the root directory.
    85  	var limit int
    86  	if doc.ID() == consts.RootDirID {
    87  		if count > 0 {
    88  			count--
    89  		}
    90  		switch c := cursor.(type) {
    91  		case *couchdb.StartKeyCursor:
    92  			limit = c.Limit
    93  			if c.NextKey == nil {
    94  				c.Limit++
    95  			}
    96  		case *couchdb.SkipCursor:
    97  			limit = c.Limit
    98  			if c.Skip == 0 {
    99  				c.Limit++
   100  			} else {
   101  				c.Skip++
   102  			}
   103  		}
   104  	}
   105  
   106  	children, err := fs.DirBatch(doc, cursor)
   107  	if err != nil {
   108  		return 0, nil, nil, err
   109  	}
   110  
   111  	if doc.ID() == consts.RootDirID {
   112  		switch c := cursor.(type) {
   113  		case *couchdb.StartKeyCursor:
   114  			c.Limit = limit
   115  		case *couchdb.SkipCursor:
   116  			c.Limit = limit
   117  			c.Skip--
   118  		}
   119  	}
   120  
   121  	return count, cursor, children, nil
   122  }
   123  
   124  func dirData(c echo.Context, statusCode int, doc *vfs.DirDoc) error {
   125  	instance := middlewares.GetInstance(c)
   126  	count, cursor, children, err := getDirData(c, doc)
   127  	if err != nil {
   128  		return err
   129  	}
   130  
   131  	// Create secrets for thumbnail links in batch for performance reasons
   132  	var thumbIDs []string
   133  	for _, child := range children {
   134  		_, f := child.Refine()
   135  		if f != nil {
   136  			if f.Class == "image" || f.Class == "pdf" {
   137  				thumbIDs = append(thumbIDs, f.ID())
   138  			}
   139  		}
   140  	}
   141  	var secrets map[string]string
   142  	if len(thumbIDs) > 0 {
   143  		secrets, _ = vfs.GetStore().AddThumbs(instance, thumbIDs)
   144  	}
   145  	if secrets == nil {
   146  		secrets = make(map[string]string)
   147  	}
   148  
   149  	relsData := make([]couchdb.DocReference, 0)
   150  	included := make([]jsonapi.Object, 0)
   151  	for _, child := range children {
   152  		if child.ID() == consts.TrashDirID {
   153  			continue
   154  		}
   155  		relsData = append(relsData, couchdb.DocReference{ID: child.ID(), Type: child.DocType()})
   156  		d, f := child.Refine()
   157  		if d != nil {
   158  			included = append(included, newDir(d))
   159  		} else {
   160  			file := NewFile(f, instance)
   161  			if secret, ok := secrets[f.ID()]; ok {
   162  				file.SetThumbSecret(secret)
   163  			}
   164  			included = append(included, file)
   165  		}
   166  	}
   167  
   168  	var parent jsonapi.Relationship
   169  	if doc.ID() != consts.RootDirID {
   170  		parent = jsonapi.Relationship{
   171  			Links: &jsonapi.LinksList{
   172  				Self: "/files/" + doc.DirID,
   173  			},
   174  			Data: couchdb.DocReference{
   175  				ID:   doc.DirID,
   176  				Type: consts.Files,
   177  			},
   178  		}
   179  	}
   180  	rel := jsonapi.RelationshipMap{
   181  		"parent": parent,
   182  		"contents": jsonapi.Relationship{
   183  			Meta: &jsonapi.Meta{Count: &count},
   184  			Links: &jsonapi.LinksList{
   185  				Self: "/files/" + doc.DocID + "/relationships/contents",
   186  			},
   187  			Data: relsData},
   188  		"not_synchronized_on": jsonapi.Relationship{
   189  			Links: &jsonapi.LinksList{
   190  				Self: "/files/" + doc.ID() + "/relationships/not_synchronized_on",
   191  			},
   192  			Data: doc.NotSynchronizedOn,
   193  		},
   194  		"referenced_by": jsonapi.Relationship{
   195  			Links: &jsonapi.LinksList{
   196  				Self: "/files/" + doc.ID() + "/relationships/references",
   197  			},
   198  			Data: doc.ReferencedBy,
   199  		},
   200  	}
   201  
   202  	var links jsonapi.LinksList
   203  	if cursor.HasMore() {
   204  		params, err := jsonapi.PaginationCursorToParams(cursor)
   205  		if err != nil {
   206  			return err
   207  		}
   208  		next := "/files/" + doc.DocID + "/relationships/contents?" + params.Encode()
   209  		rel["contents"].Links.Next = next
   210  		links.Next = "/files/" + doc.DocID + "?" + params.Encode()
   211  	}
   212  
   213  	d := &dir{
   214  		doc:      doc,
   215  		rel:      rel,
   216  		included: included,
   217  	}
   218  
   219  	return jsonapi.Data(c, statusCode, d, &links)
   220  }
   221  
   222  func dirDataList(c echo.Context, statusCode int, doc *vfs.DirDoc) error {
   223  	instance := middlewares.GetInstance(c)
   224  	count, cursor, children, err := getDirData(c, doc)
   225  	if err != nil {
   226  		return err
   227  	}
   228  
   229  	included := make([]jsonapi.Object, 0)
   230  	for _, child := range children {
   231  		if child.ID() == consts.TrashDirID {
   232  			continue
   233  		}
   234  		d, f := child.Refine()
   235  		if d != nil {
   236  			included = append(included, newDir(d))
   237  		} else {
   238  			included = append(included, NewFile(f, instance))
   239  		}
   240  	}
   241  
   242  	var links jsonapi.LinksList
   243  	if cursor.HasMore() {
   244  		params, err := jsonapi.PaginationCursorToParams(cursor)
   245  		if err != nil {
   246  			return err
   247  		}
   248  		next := c.Request().URL.Path + "?" + params.Encode()
   249  		links.Next = next
   250  	}
   251  
   252  	meta := jsonapi.Meta{Count: &count}
   253  	return jsonapi.DataListWithMeta(c, statusCode, meta, included, &links)
   254  }
   255  
   256  // NewFile creates an instance of file struct from a vfs.FileDoc document.
   257  func NewFile(doc *vfs.FileDoc, i *instance.Instance) *file {
   258  	return &file{doc, i, nil, nil, &fileJSON{}, "", false}
   259  }
   260  
   261  // FileData returns a jsonapi representation of the given file.
   262  func FileData(c echo.Context, statusCode int, doc *vfs.FileDoc, withVersions bool, links *jsonapi.LinksList) error {
   263  	instance := middlewares.GetInstance(c)
   264  	f := NewFile(doc, instance)
   265  	if withVersions {
   266  		if versions, err := vfs.VersionsFor(instance, doc.ID()); err == nil {
   267  			f.versions = versions
   268  		}
   269  	}
   270  	if doc.Mime == consts.NoteMimeType {
   271  		images, err := note.GetImages(instance, doc.ID())
   272  		if err == nil {
   273  			for _, image := range images {
   274  				noteImage := NewNoteImage(instance, image)
   275  				f.noteImages = append(f.noteImages, noteImage)
   276  			}
   277  		}
   278  	}
   279  	return jsonapi.Data(c, statusCode, f, links)
   280  }
   281  
   282  var (
   283  	_ jsonapi.Object = (*apiArchive)(nil)
   284  	_ jsonapi.Object = (*apiMetadata)(nil)
   285  	_ jsonapi.Object = (*dir)(nil)
   286  	_ jsonapi.Object = (*file)(nil)
   287  )
   288  
   289  func (a *apiArchive) Relationships() jsonapi.RelationshipMap { return nil }
   290  func (a *apiArchive) Included() []jsonapi.Object             { return nil }
   291  func (a *apiArchive) MarshalJSON() ([]byte, error)           { return json.Marshal(a.Archive) }
   292  func (a *apiArchive) Links() *jsonapi.LinksList {
   293  	return &jsonapi.LinksList{Self: "/files/archive/" + a.Secret}
   294  }
   295  
   296  func (m *apiMetadata) ID() string                             { return m.secret }
   297  func (m *apiMetadata) Rev() string                            { return "" }
   298  func (m *apiMetadata) SetID(id string)                        { m.secret = id }
   299  func (m *apiMetadata) SetRev(rev string)                      {}
   300  func (m *apiMetadata) DocType() string                        { return consts.FilesMetadata }
   301  func (m *apiMetadata) Clone() couchdb.Doc                     { cloned := *m; return &cloned }
   302  func (m *apiMetadata) Relationships() jsonapi.RelationshipMap { return nil }
   303  func (m *apiMetadata) Included() []jsonapi.Object             { return nil }
   304  func (m *apiMetadata) MarshalJSON() ([]byte, error)           { return json.Marshal(m.Metadata) }
   305  func (m *apiMetadata) Links() *jsonapi.LinksList              { return nil }
   306  
   307  func (d *dir) ID() string                             { return d.doc.ID() }
   308  func (d *dir) Rev() string                            { return d.doc.Rev() }
   309  func (d *dir) SetID(id string)                        { d.doc.SetID(id) }
   310  func (d *dir) SetRev(rev string)                      { d.doc.SetRev(rev) }
   311  func (d *dir) DocType() string                        { return d.doc.DocType() }
   312  func (d *dir) Clone() couchdb.Doc                     { cloned := *d; return &cloned }
   313  func (d *dir) Relationships() jsonapi.RelationshipMap { return d.rel }
   314  func (d *dir) Included() []jsonapi.Object             { return d.included }
   315  func (d *dir) MarshalJSON() ([]byte, error)           { return json.Marshal(d.doc) }
   316  func (d *dir) Links() *jsonapi.LinksList {
   317  	return &jsonapi.LinksList{Self: "/files/" + d.doc.DocID}
   318  }
   319  
   320  func (f *file) ID() string                   { return f.doc.ID() }
   321  func (f *file) Rev() string                  { return f.doc.Rev() }
   322  func (f *file) SetID(id string)              { f.doc.SetID(id) }
   323  func (f *file) SetRev(rev string)            { f.doc.SetRev(rev) }
   324  func (f *file) DocType() string              { return f.doc.DocType() }
   325  func (f *file) Clone() couchdb.Doc           { cloned := *f; return &cloned }
   326  func (f *file) SetThumbSecret(secret string) { f.thumbSecret = secret }
   327  
   328  func (f *file) Relationships() jsonapi.RelationshipMap {
   329  	rels := jsonapi.RelationshipMap{
   330  		"parent": jsonapi.Relationship{
   331  			Links: &jsonapi.LinksList{
   332  				Related: "/files/" + f.doc.DirID,
   333  			},
   334  			Data: couchdb.DocReference{
   335  				ID:   f.doc.DirID,
   336  				Type: consts.Files,
   337  			},
   338  		},
   339  		"referenced_by": jsonapi.Relationship{
   340  			Links: &jsonapi.LinksList{
   341  				Self: "/files/" + f.doc.ID() + "/relationships/references",
   342  			},
   343  			Data: f.doc.ReferencedBy,
   344  		},
   345  	}
   346  	if len(f.versions) > 0 {
   347  		data := make([]couchdb.DocReference, len(f.versions))
   348  		for i, version := range f.versions {
   349  			data[i] = couchdb.DocReference{
   350  				ID:   version.DocID,
   351  				Type: consts.FilesVersions,
   352  			}
   353  		}
   354  		rels["old_versions"] = jsonapi.Relationship{
   355  			Data: data,
   356  		}
   357  	}
   358  	return rels
   359  }
   360  
   361  func (f *file) Included() []jsonapi.Object {
   362  	var included []jsonapi.Object
   363  	for _, version := range f.versions {
   364  		included = append(included, version)
   365  	}
   366  	included = append(included, f.noteImages...)
   367  	return included
   368  }
   369  
   370  func (f *file) MarshalJSON() ([]byte, error) {
   371  	f.jsonDoc.FileDoc = f.doc
   372  	if f.includePath {
   373  		f.jsonDoc.Fullpath, _ = f.doc.Path(nil)
   374  	}
   375  	res, err := json.Marshal(f.jsonDoc)
   376  	return res, err
   377  }
   378  
   379  func (f *file) Links() *jsonapi.LinksList {
   380  	links := jsonapi.LinksList{Self: "/files/" + f.doc.DocID}
   381  	if f.doc.Class == "image" || f.doc.Class == "pdf" {
   382  		if f.thumbSecret == "" {
   383  			if secret, err := vfs.GetStore().AddThumb(f.instance, f.doc.DocID); err == nil {
   384  				f.thumbSecret = secret
   385  			}
   386  		}
   387  		if f.thumbSecret != "" {
   388  			links.Tiny = "/files/" + f.doc.DocID + "/thumbnails/" + f.thumbSecret + "/tiny"
   389  			links.Small = "/files/" + f.doc.DocID + "/thumbnails/" + f.thumbSecret + "/small"
   390  			links.Medium = "/files/" + f.doc.DocID + "/thumbnails/" + f.thumbSecret + "/medium"
   391  			links.Large = "/files/" + f.doc.DocID + "/thumbnails/" + f.thumbSecret + "/large"
   392  			if f.doc.Class == "pdf" {
   393  				links.Icon = "/files/" + f.doc.DocID + "/icon/" + f.thumbSecret
   394  				links.Preview = "/files/" + f.doc.DocID + "/preview/" + f.thumbSecret
   395  			}
   396  		}
   397  	}
   398  	return &links
   399  }
   400  
   401  func (f *file) IncludePath(fp vfs.FilePather) {
   402  	_, err := f.doc.Path(fp)
   403  	f.includePath = err == nil
   404  }
   405  
   406  // findDir is used for the result of mango requests, where only some fields can
   407  // have been requested
   408  type findDir struct {
   409  	*vfs.DirDoc
   410  	// We may want to hide some fields from the JSON response if the fields has
   411  	// not been requested to CouchDB, as they are blank
   412  	CreatedAt *time.Time `json:"created_at,omitempty"`
   413  	UpdatedAt *time.Time `json:"updated_at,omitempty"`
   414  }
   415  
   416  func (d *findDir) Relationships() jsonapi.RelationshipMap {
   417  	return jsonapi.RelationshipMap{
   418  		"referenced_by": jsonapi.Relationship{
   419  			Links: &jsonapi.LinksList{
   420  				Self: "/files/" + d.ID() + "/relationships/references",
   421  			},
   422  			Data: d.DirDoc.ReferencedBy,
   423  		},
   424  	}
   425  }
   426  func (d *findDir) Included() []jsonapi.Object { return nil }
   427  func (d *findDir) Links() *jsonapi.LinksList  { return nil }
   428  
   429  func newFindDir(doc *vfs.DirDoc, fields []string) *findDir {
   430  	dir := &findDir{doc, nil, nil}
   431  	if hasField(fields, "created_at") {
   432  		dir.CreatedAt = &doc.CreatedAt
   433  	}
   434  	if hasField(fields, "updated_at") {
   435  		dir.UpdatedAt = &doc.UpdatedAt
   436  	}
   437  	return dir
   438  }
   439  
   440  // findFile is used for the result of mango requests, where only some fields can
   441  // have been requested
   442  type findFile struct {
   443  	*vfs.FileDoc
   444  	file *file
   445  	// We may want to hide some fields from the JSON response if the fields has
   446  	// not been requested to CouchDB, as they are blank
   447  	Fullpath   string     `json:"path,omitempty"`
   448  	CreatedAt  *time.Time `json:"created_at,omitempty"`
   449  	UpdatedAt  *time.Time `json:"updated_at,omitempty"`
   450  	Executable *bool      `json:"executable,omitempty"`
   451  	Encrypted  *bool      `json:"encrypted,omitempty"`
   452  	// Hide the internal_vfs_id and referenced_by
   453  	InternalID   *interface{} `json:"internal_vfs_id,omitempty"`
   454  	ReferencedBy *interface{} `json:"referenced_by,omitempty"`
   455  }
   456  
   457  func (f *findFile) SetThumbSecret(secret string)           { f.file.SetThumbSecret(secret) }
   458  func (f *findFile) Relationships() jsonapi.RelationshipMap { return f.file.Relationships() }
   459  func (f *findFile) Included() []jsonapi.Object             { return f.file.Included() }
   460  func (f *findFile) Links() *jsonapi.LinksList              { return f.file.Links() }
   461  
   462  func newFindFile(doc *vfs.FileDoc, fields []string, i *instance.Instance) *findFile {
   463  	f := NewFile(doc, i)
   464  	ff := &findFile{doc, f, "", nil, nil, nil, nil, nil, nil}
   465  	if hasField(fields, "created_at") {
   466  		ff.CreatedAt = &doc.CreatedAt
   467  	}
   468  	if hasField(fields, "updated_at") {
   469  		ff.UpdatedAt = &doc.UpdatedAt
   470  	}
   471  	if hasField(fields, "executable") {
   472  		ff.Executable = &doc.Executable
   473  	}
   474  	if hasField(fields, "encrypted") {
   475  		ff.Encrypted = &doc.Encrypted
   476  	}
   477  	return ff
   478  }
   479  
   480  func hasField(fields []string, field string) bool {
   481  	for _, f := range fields {
   482  		if f == field {
   483  			return true
   484  		}
   485  	}
   486  	return false
   487  }
   488  
   489  func (f *findFile) IncludePath(fp vfs.FilePather) {
   490  	f.Fullpath, _ = f.Path(fp)
   491  }