github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/shortcuts/shortcuts.go (about) 1 // Package shortcuts is about the .url shortcuts. They are files and, as such, 2 // can be manipulated via the /files API. But the stack also offer some routes 3 // to make it easier to create and open them. 4 package shortcuts 5 6 import ( 7 "errors" 8 "net/http" 9 "os" 10 "strings" 11 12 "github.com/cozy/cozy-stack/model/permission" 13 "github.com/cozy/cozy-stack/model/vfs" 14 "github.com/cozy/cozy-stack/pkg/consts" 15 "github.com/cozy/cozy-stack/pkg/couchdb" 16 "github.com/cozy/cozy-stack/pkg/jsonapi" 17 "github.com/cozy/cozy-stack/pkg/shortcut" 18 "github.com/cozy/cozy-stack/web/files" 19 "github.com/cozy/cozy-stack/web/middlewares" 20 "github.com/labstack/echo/v4" 21 ) 22 23 // Shortcut is a struct with the high-level information about a .url file. 24 type Shortcut struct { 25 DocID string `json:"_id"` 26 DocRev string `json:"_rev,omitempty"` 27 Name string `json:"name"` 28 DirID string `json:"dir_id"` 29 URL string `json:"url"` 30 Metadata vfs.Metadata `json:"metadata"` 31 } 32 33 // ID returns the shortcut qualified identifier 34 func (s *Shortcut) ID() string { return s.DocID } 35 36 // Rev returns the shortcut revision 37 func (s *Shortcut) Rev() string { return s.DocRev } 38 39 // DocType returns the shortcut type 40 func (s *Shortcut) DocType() string { return consts.FilesShortcuts } 41 42 // Clone implements couchdb.Doc 43 func (s *Shortcut) Clone() couchdb.Doc { 44 cloned := *s 45 cloned.Metadata = make(vfs.Metadata, len(s.Metadata)) 46 for k, v := range s.Metadata { 47 cloned.Metadata[k] = v 48 } 49 return &cloned 50 } 51 52 // SetID changes the shortcut qualified identifier 53 func (s *Shortcut) SetID(id string) { s.DocID = id } 54 55 // SetRev changes the shortcut revision 56 func (s *Shortcut) SetRev(rev string) { s.DocRev = rev } 57 58 // Relationships is a method of the jsonapi.Document interface 59 func (s *Shortcut) Relationships() jsonapi.RelationshipMap { return nil } 60 61 // Included is a method of the jsonapi.Document interface 62 func (s *Shortcut) Included() []jsonapi.Object { return nil } 63 64 // Links is a method of the jsonapi.Document interface 65 func (s *Shortcut) Links() *jsonapi.LinksList { return nil } 66 67 // FromJSONAPI parses a JSON-API payload for a shortcut and returns a FileDoc 68 // and body for it. 69 func FromJSONAPI(c echo.Context) (*vfs.FileDoc, []byte, error) { 70 doc := &Shortcut{} 71 if _, err := jsonapi.Bind(c.Request().Body, doc); err != nil { 72 return nil, nil, err 73 } 74 if doc.URL == "" { 75 return nil, nil, jsonapi.InvalidAttribute("url", errors.New("No URL")) 76 } 77 if doc.Name == "" { 78 return nil, nil, jsonapi.InvalidAttribute("name", errors.New("No name")) 79 } 80 if !strings.HasSuffix(doc.Name, ".url") { 81 doc.Name += ".url" 82 } 83 if doc.DirID == "" { 84 doc.DirID = consts.RootDirID 85 } 86 87 body := shortcut.Generate(doc.URL) 88 cm, _ := files.CozyMetadataFromClaims(c, true) 89 fileDoc, err := vfs.NewFileDoc( 90 doc.Name, 91 doc.DirID, 92 int64(len(body)), 93 nil, // Let the VFS compute the md5sum 94 consts.ShortcutMimeType, 95 "shortcut", 96 cm.UpdatedAt, 97 false, // Not executable 98 false, // Not trashed, 99 false, // Not encrypted 100 nil, // No tags 101 ) 102 if err != nil { 103 return nil, nil, wrapError(err) 104 } 105 fileDoc.Metadata = doc.Metadata 106 fileDoc.CozyMetadata = cm 107 return fileDoc, body, nil 108 } 109 110 // Create is the API handler for POST /shortcuts. It can be used to create a 111 // shortcut from a JSON description. 112 func Create(c echo.Context) error { 113 fileDoc, body, err := FromJSONAPI(c) 114 if err != nil { 115 return err 116 } 117 118 if err := middlewares.AllowVFS(c, permission.POST, fileDoc); err != nil { 119 return err 120 } 121 122 inst := middlewares.GetInstance(c) 123 file, err := inst.VFS().CreateFile(fileDoc, nil) 124 if err != nil { 125 return wrapError(err) 126 } 127 _, err = file.Write(body) 128 if cerr := file.Close(); cerr != nil && err == nil { 129 err = cerr 130 } 131 if err != nil { 132 return wrapError(err) 133 } 134 135 return files.FileData(c, http.StatusCreated, fileDoc, false, nil) 136 } 137 138 // Get is the API handler for GET /shortcuts/:id. It follows the link or send a 139 // JSON-API response with information about the shortcut, depending on the 140 // Accept header. 141 func Get(c echo.Context) error { 142 inst := middlewares.GetInstance(c) 143 fs := inst.VFS() 144 fileID := c.Param("id") 145 file, err := fs.FileByID(fileID) 146 if err != nil { 147 return wrapError(err) 148 } 149 150 if err := middlewares.AllowVFS(c, permission.GET, file); err != nil { 151 return err 152 } 153 154 f, err := fs.OpenFile(file) 155 if err != nil { 156 return wrapError(err) 157 } 158 defer f.Close() 159 link, err := shortcut.Parse(f) 160 if err != nil { 161 return wrapError(err) 162 } 163 if link.URL == "" { 164 return jsonapi.BadRequest(errors.New("No URL found")) 165 } 166 167 if meta, ok := file.Metadata["sharing"].(map[string]interface{}); ok && meta["status"] != "seen" { 168 old := file.Clone().(*vfs.FileDoc) 169 meta["status"] = "seen" 170 _ = fs.UpdateFileDoc(old, file) 171 } 172 173 accept := c.Request().Header.Get(echo.HeaderAccept) 174 if strings.Contains(accept, echo.MIMEApplicationJSON) || 175 strings.Contains(accept, jsonapi.ContentType) { 176 doc := &Shortcut{ 177 DocID: file.DocID, 178 DocRev: file.DocRev, 179 Name: file.DocName, 180 DirID: file.DirID, 181 URL: link.URL, 182 Metadata: file.Metadata, 183 } 184 return jsonapi.Data(c, http.StatusOK, doc, nil) 185 } 186 187 return c.Redirect(http.StatusSeeOther, link.URL) 188 } 189 190 // Routes set the routing for the shortcuts. 191 func Routes(router *echo.Group) { 192 router.POST("", Create) 193 router.GET("/:id", Get) 194 } 195 196 func wrapError(err error) *jsonapi.Error { 197 switch err { 198 case os.ErrNotExist, vfs.ErrParentDoesNotExist, vfs.ErrParentInTrash: 199 return jsonapi.NotFound(err) 200 case vfs.ErrFileTooBig, vfs.ErrMaxFileSize: 201 return jsonapi.Errorf(http.StatusRequestEntityTooLarge, "%s", err) 202 case shortcut.ErrInvalidShortcut: 203 return jsonapi.BadRequest(err) 204 } 205 return jsonapi.InternalServerError(err) 206 }