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 }