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

     1  package remote
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"net/http"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"github.com/cozy/cozy-stack/model/nextcloud"
    13  	"github.com/cozy/cozy-stack/model/permission"
    14  	"github.com/cozy/cozy-stack/model/vfs"
    15  	"github.com/cozy/cozy-stack/pkg/config/config"
    16  	"github.com/cozy/cozy-stack/pkg/consts"
    17  	"github.com/cozy/cozy-stack/pkg/jsonapi"
    18  	"github.com/cozy/cozy-stack/pkg/webdav"
    19  	"github.com/cozy/cozy-stack/web/files"
    20  	"github.com/cozy/cozy-stack/web/middlewares"
    21  	"github.com/labstack/echo/v4"
    22  	"github.com/ncw/swift/v2"
    23  )
    24  
    25  func nextcloudGet(c echo.Context) error {
    26  	inst := middlewares.GetInstance(c)
    27  	if err := middlewares.AllowWholeType(c, permission.PUT, consts.Files); err != nil {
    28  		return err
    29  	}
    30  
    31  	accountID := c.Param("account")
    32  	nc, err := nextcloud.New(inst, accountID)
    33  	if err != nil {
    34  		return wrapNextcloudErrors(err)
    35  	}
    36  
    37  	path := c.Param("*")
    38  	if c.QueryParam("Dl") == "1" {
    39  		return nextcloudDownload(c, nc, path)
    40  	}
    41  
    42  	files, err := nc.ListFiles(path)
    43  	if err != nil {
    44  		return wrapNextcloudErrors(err)
    45  	}
    46  	return jsonapi.DataList(c, http.StatusOK, files, nil)
    47  }
    48  
    49  func nextcloudDownload(c echo.Context, nc *nextcloud.NextCloud, path string) error {
    50  	f, err := nc.Download(path)
    51  	if err != nil {
    52  		return wrapNextcloudErrors(err)
    53  	}
    54  	defer f.Content.Close()
    55  
    56  	w := c.Response()
    57  	header := w.Header()
    58  	filename := filepath.Base(path)
    59  	disposition := vfs.ContentDisposition("attachment", filename)
    60  	header.Set(echo.HeaderContentDisposition, disposition)
    61  	header.Set(echo.HeaderContentType, f.Mime)
    62  	if f.Length != "" {
    63  		header.Set(echo.HeaderContentLength, f.Length)
    64  	}
    65  	if f.LastModified != "" {
    66  		header.Set(echo.HeaderLastModified, f.LastModified)
    67  	}
    68  	if f.ETag != "" {
    69  		header.Set("Etag", f.ETag)
    70  	}
    71  	if !config.GetConfig().CSPDisabled {
    72  		middlewares.AppendCSPRule(c, "form-action", "'none'")
    73  	}
    74  
    75  	w.WriteHeader(http.StatusOK)
    76  	_, err = io.Copy(w, f.Content)
    77  	return err
    78  }
    79  
    80  func nextcloudPut(c echo.Context) error {
    81  	inst := middlewares.GetInstance(c)
    82  	if err := middlewares.AllowWholeType(c, permission.PUT, consts.Files); err != nil {
    83  		return err
    84  	}
    85  
    86  	accountID := c.Param("account")
    87  	nc, err := nextcloud.New(inst, accountID)
    88  	if err != nil {
    89  		return wrapNextcloudErrors(err)
    90  	}
    91  
    92  	path := c.Param("*")
    93  	if c.QueryParam("Type") == "file" {
    94  		return nextcloudUpload(c, nc, path)
    95  	}
    96  
    97  	if err := nc.Mkdir(path); err != nil {
    98  		return wrapNextcloudErrors(err)
    99  	}
   100  	return c.JSON(http.StatusCreated, echo.Map{"ok": true})
   101  }
   102  
   103  func nextcloudUpload(c echo.Context, nc *nextcloud.NextCloud, path string) error {
   104  	req := c.Request()
   105  	mime := req.Header.Get(echo.HeaderContentType)
   106  	if err := nc.Upload(path, mime, req.Body); err != nil {
   107  		return wrapNextcloudErrors(err)
   108  	}
   109  	return c.JSON(http.StatusCreated, echo.Map{"ok": true})
   110  }
   111  
   112  func nextcloudDelete(c echo.Context) error {
   113  	inst := middlewares.GetInstance(c)
   114  	if err := middlewares.AllowWholeType(c, permission.DELETE, consts.Files); err != nil {
   115  		return err
   116  	}
   117  
   118  	accountID := c.Param("account")
   119  	nc, err := nextcloud.New(inst, accountID)
   120  	if err != nil {
   121  		return wrapNextcloudErrors(err)
   122  	}
   123  
   124  	path := c.Param("*")
   125  	if err := nc.Delete(path); err != nil {
   126  		return wrapNextcloudErrors(err)
   127  	}
   128  	return c.NoContent(http.StatusNoContent)
   129  }
   130  
   131  func nextcloudMove(c echo.Context) error {
   132  	inst := middlewares.GetInstance(c)
   133  	if err := middlewares.AllowWholeType(c, permission.POST, consts.Files); err != nil {
   134  		return err
   135  	}
   136  
   137  	accountID := c.Param("account")
   138  	nc, err := nextcloud.New(inst, accountID)
   139  	if err != nil {
   140  		return wrapNextcloudErrors(err)
   141  	}
   142  
   143  	oldPath := c.Param("*")
   144  	newPath := c.QueryParam("To")
   145  	if newPath == "" {
   146  		return jsonapi.BadRequest(errors.New("missing To parameter"))
   147  	}
   148  
   149  	if err := nc.Move(oldPath, newPath); err != nil {
   150  		return wrapNextcloudErrors(err)
   151  	}
   152  	return c.NoContent(http.StatusNoContent)
   153  }
   154  
   155  func nextcloudCopy(c echo.Context) error {
   156  	inst := middlewares.GetInstance(c)
   157  	if err := middlewares.AllowWholeType(c, permission.POST, consts.Files); err != nil {
   158  		return err
   159  	}
   160  
   161  	accountID := c.Param("account")
   162  	nc, err := nextcloud.New(inst, accountID)
   163  	if err != nil {
   164  		return wrapNextcloudErrors(err)
   165  	}
   166  
   167  	oldPath := c.Param("*")
   168  	newPath := oldPath
   169  	if newName := c.QueryParam("Name"); newName != "" {
   170  		newPath = filepath.Join(filepath.Dir(oldPath), newName)
   171  	} else {
   172  		ext := filepath.Ext(oldPath)
   173  		base := strings.TrimSuffix(oldPath, ext)
   174  		suffix := inst.Translate("File copy Suffix")
   175  		newPath = fmt.Sprintf("%s (%s)%s", base, suffix, ext)
   176  	}
   177  
   178  	if err := nc.Copy(oldPath, newPath); err != nil {
   179  		return wrapNextcloudErrors(err)
   180  	}
   181  	return c.JSON(http.StatusCreated, echo.Map{"ok": true})
   182  }
   183  
   184  func nextcloudDownstream(c echo.Context) error {
   185  	inst := middlewares.GetInstance(c)
   186  	if err := middlewares.AllowWholeType(c, permission.POST, consts.Files); err != nil {
   187  		return err
   188  	}
   189  
   190  	accountID := c.Param("account")
   191  	nc, err := nextcloud.New(inst, accountID)
   192  	if err != nil {
   193  		return wrapNextcloudErrors(err)
   194  	}
   195  
   196  	path := c.Param("*")
   197  	to := c.QueryParam("To")
   198  	if to == "" {
   199  		return jsonapi.BadRequest(errors.New("missing To parameter"))
   200  	}
   201  
   202  	cozyMetadata, _ := files.CozyMetadataFromClaims(c, true)
   203  	f, err := nc.Downstream(path, to, cozyMetadata)
   204  	if err != nil {
   205  		return wrapNextcloudErrors(err)
   206  	}
   207  	obj := files.NewFile(f, inst)
   208  	return jsonapi.Data(c, http.StatusCreated, obj, nil)
   209  }
   210  
   211  func nextcloudUpstream(c echo.Context) error {
   212  	inst := middlewares.GetInstance(c)
   213  	if err := middlewares.AllowWholeType(c, permission.POST, consts.Files); err != nil {
   214  		return err
   215  	}
   216  
   217  	accountID := c.Param("account")
   218  	nc, err := nextcloud.New(inst, accountID)
   219  	if err != nil {
   220  		return wrapNextcloudErrors(err)
   221  	}
   222  
   223  	path := c.Param("*")
   224  	from := c.QueryParam("From")
   225  	if from == "" {
   226  		return jsonapi.BadRequest(errors.New("missing From parameter"))
   227  	}
   228  
   229  	if err := nc.Upstream(path, from); err != nil {
   230  		return wrapNextcloudErrors(err)
   231  	}
   232  	return c.NoContent(http.StatusNoContent)
   233  }
   234  
   235  func nextcloudRoutes(router *echo.Group) {
   236  	group := router.Group("/nextcloud/:account")
   237  	group.GET("/*", nextcloudGet)
   238  	group.PUT("/*", nextcloudPut)
   239  	group.DELETE("/*", nextcloudDelete)
   240  	group.POST("/move/*", nextcloudMove)
   241  	group.POST("/copy/*", nextcloudCopy)
   242  	group.POST("/downstream/*", nextcloudDownstream)
   243  	group.POST("/upstream/*", nextcloudUpstream)
   244  }
   245  
   246  func wrapNextcloudErrors(err error) error {
   247  	switch err {
   248  	case nextcloud.ErrAccountNotFound:
   249  		return jsonapi.NotFound(err)
   250  	case nextcloud.ErrInvalidAccount:
   251  		return jsonapi.BadRequest(err)
   252  	case webdav.ErrInvalidAuth:
   253  		return jsonapi.Unauthorized(err)
   254  	case webdav.ErrAlreadyExist, vfs.ErrConflict:
   255  		return jsonapi.Conflict(err)
   256  	case webdav.ErrParentNotFound:
   257  		return jsonapi.NotFound(err)
   258  	case webdav.ErrNotFound, os.ErrNotExist, swift.ObjectNotFound:
   259  		return jsonapi.NotFound(err)
   260  	case webdav.ErrInternalServerError:
   261  		return jsonapi.InternalServerError(err)
   262  	}
   263  	return err
   264  }