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 }