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 }