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  }