github.com/cs3org/reva/v2@v2.27.7/internal/http/services/owncloud/ocdav/trashbin.go (about) 1 // Copyright 2018-2021 CERN 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 // 15 // In applying this license, CERN does not waive the privileges and immunities 16 // granted to it by virtue of its status as an Intergovernmental Organization 17 // or submit itself to any jurisdiction. 18 19 package ocdav 20 21 import ( 22 "bytes" 23 "context" 24 "encoding/xml" 25 "fmt" 26 "net/http" 27 "path" 28 "strconv" 29 "strings" 30 "time" 31 32 "go.opentelemetry.io/otel/codes" 33 34 "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/config" 35 "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/errors" 36 "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/net" 37 "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/prop" 38 "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/propfind" 39 "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/spacelookup" 40 "github.com/cs3org/reva/v2/pkg/storagespace" 41 42 rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" 43 provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" 44 semconv "go.opentelemetry.io/otel/semconv/v1.20.0" 45 46 "github.com/cs3org/reva/v2/pkg/appctx" 47 ctxpkg "github.com/cs3org/reva/v2/pkg/ctx" 48 rstatus "github.com/cs3org/reva/v2/pkg/rgrpc/status" 49 "github.com/cs3org/reva/v2/pkg/utils" 50 ) 51 52 // TrashbinHandler handles trashbin requests 53 type TrashbinHandler struct { 54 gatewaySvc string 55 namespace string 56 allowPropfindDepthInfinitiy bool 57 } 58 59 func (h *TrashbinHandler) init(c *config.Config) error { 60 h.gatewaySvc = c.GatewaySvc 61 h.namespace = path.Join("/", c.FilesNamespace) 62 h.allowPropfindDepthInfinitiy = c.AllowPropfindDepthInfinitiy 63 return nil 64 } 65 66 // Handler handles requests 67 func (h *TrashbinHandler) Handler(s *svc) http.Handler { 68 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 69 ctx := r.Context() 70 log := appctx.GetLogger(ctx) 71 72 if r.Method == http.MethodOptions { 73 s.handleOptions(w, r) 74 return 75 } 76 77 var username string 78 username, r.URL.Path = splitSpaceAndKey(r.URL.Path) 79 if username == "" { 80 // listing is disabled, no auth will change that 81 w.WriteHeader(http.StatusMethodNotAllowed) 82 return 83 } 84 85 user, ok := ctxpkg.ContextGetUser(ctx) 86 if !ok { 87 w.WriteHeader(http.StatusBadRequest) 88 return 89 } 90 if user.Username != username { 91 log.Debug().Str("username", username).Interface("user", user).Msg("trying to read another users trash") 92 // listing other users trash is forbidden, no auth will change that 93 // do not leak existence of space and return 404 94 w.WriteHeader(http.StatusNotFound) 95 b, err := errors.Marshal(http.StatusNotFound, "not found", "", "") 96 if err != nil { 97 log.Error().Msgf("error marshaling xml response: %s", b) 98 w.WriteHeader(http.StatusInternalServerError) 99 return 100 } 101 _, err = w.Write(b) 102 if err != nil { 103 log.Error().Msgf("error writing xml response: %s", b) 104 w.WriteHeader(http.StatusInternalServerError) 105 return 106 } 107 return 108 } 109 110 useLoggedInUser := true 111 ns, newPath, err := s.ApplyLayout(ctx, h.namespace, useLoggedInUser, r.URL.Path) 112 if err != nil { 113 w.WriteHeader(http.StatusNotFound) 114 b, err := errors.Marshal(http.StatusNotFound, fmt.Sprintf("could not get storage for %s", r.URL.Path), "", "") 115 errors.HandleWebdavError(appctx.GetLogger(r.Context()), w, b, err) 116 } 117 r.URL.Path = newPath 118 119 basePath := path.Join(ns, newPath) 120 space, rpcstatus, err := spacelookup.LookUpStorageSpaceForPath(ctx, s.gatewaySelector, basePath) 121 switch { 122 case err != nil: 123 log.Error().Err(err).Str("path", basePath).Msg("failed to look up storage space") 124 w.WriteHeader(http.StatusInternalServerError) 125 return 126 case rpcstatus.Code != rpc.Code_CODE_OK: 127 httpStatus := rstatus.HTTPStatusFromCode(rpcstatus.Code) 128 w.WriteHeader(httpStatus) 129 b, err := errors.Marshal(httpStatus, rpcstatus.Message, "", "") 130 errors.HandleWebdavError(log, w, b, err) 131 return 132 } 133 ref := spacelookup.MakeRelativeReference(space, ".", false) 134 135 // key will be a base64 encoded cs3 path, it uniquely identifies a trash item with an opaque id and an optional path 136 key := r.URL.Path 137 138 switch r.Method { 139 case MethodPropfind: 140 h.listTrashbin(w, r, s, ref, user.Username, key) 141 case MethodMove: 142 if key == "" { 143 http.Error(w, "501 Not implemented", http.StatusNotImplemented) 144 break 145 } 146 // find path in url relative to trash base 147 trashBase := ctx.Value(net.CtxKeyBaseURI).(string) 148 baseURI := path.Join(path.Dir(trashBase), "files", username) 149 150 dh := r.Header.Get(net.HeaderDestination) 151 dst, err := net.ParseDestination(baseURI, dh) 152 if err != nil { 153 w.WriteHeader(http.StatusBadRequest) 154 return 155 } 156 157 p := path.Join(ns, dst) 158 // The destination can be in another space. E.g. the 'Shares Jail'. 159 space, rpcstatus, err := spacelookup.LookUpStorageSpaceForPath(ctx, s.gatewaySelector, p) 160 if err != nil { 161 log.Error().Err(err).Str("path", p).Msg("failed to look up destination storage space") 162 w.WriteHeader(http.StatusInternalServerError) 163 return 164 } 165 if rpcstatus.Code != rpc.Code_CODE_OK { 166 httpStatus := rstatus.HTTPStatusFromCode(rpcstatus.Code) 167 w.WriteHeader(httpStatus) 168 b, err := errors.Marshal(httpStatus, rpcstatus.Message, "", "") 169 errors.HandleWebdavError(log, w, b, err) 170 return 171 } 172 dstRef := spacelookup.MakeRelativeReference(space, p, false) 173 174 log.Debug().Str("key", key).Str("dst", dst).Msg("restore") 175 h.restore(w, r, s, ref, dstRef, key) 176 case http.MethodDelete: 177 h.delete(w, r, s, ref, key) 178 default: 179 http.Error(w, "501 Not implemented", http.StatusNotImplemented) 180 } 181 }) 182 } 183 184 func (h *TrashbinHandler) getDepth(r *http.Request) (net.Depth, error) { 185 dh := r.Header.Get(net.HeaderDepth) 186 depth, err := net.ParseDepth(dh) 187 if err != nil || depth == net.DepthInfinity && !h.allowPropfindDepthInfinitiy { 188 return "", errors.ErrInvalidDepth 189 } 190 return depth, nil 191 } 192 193 func (h *TrashbinHandler) listTrashbin(w http.ResponseWriter, r *http.Request, s *svc, ref *provider.Reference, refBase, key string) { 194 ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "list_trashbin") 195 defer span.End() 196 197 sublog := appctx.GetLogger(ctx).With().Logger() 198 199 depth, err := h.getDepth(r) 200 if err != nil { 201 span.RecordError(err) 202 span.SetStatus(codes.Error, "Invalid Depth header value") 203 span.SetAttributes(semconv.HTTPStatusCodeKey.Int(http.StatusBadRequest)) 204 sublog.Debug().Str("depth", r.Header.Get(net.HeaderDepth)).Msg(err.Error()) 205 w.WriteHeader(http.StatusBadRequest) 206 m := fmt.Sprintf("Invalid Depth header value: %v", r.Header.Get(net.HeaderDepth)) 207 b, err := errors.Marshal(http.StatusBadRequest, m, "", "") 208 errors.HandleWebdavError(&sublog, w, b, err) 209 return 210 } 211 212 pf, status, err := propfind.ReadPropfind(r.Body) 213 if err != nil { 214 sublog.Debug().Err(err).Msg("error reading propfind request") 215 w.WriteHeader(status) 216 return 217 } 218 219 if key == "" && depth == net.DepthZero { 220 // we are listing the trash root, but without children 221 // so we just fake a root element without actually querying the gateway 222 rootHref := path.Join(refBase, key) 223 propRes, err := h.formatTrashPropfind(ctx, s, ref.ResourceId.SpaceId, refBase, rootHref, &pf, nil, true) 224 if err != nil { 225 sublog.Error().Err(err).Msg("error formatting propfind") 226 w.WriteHeader(http.StatusInternalServerError) 227 return 228 } 229 w.Header().Set(net.HeaderDav, "1, 3, extended-mkcol") 230 w.Header().Set(net.HeaderContentType, "application/xml; charset=utf-8") 231 w.WriteHeader(http.StatusMultiStatus) 232 _, err = w.Write(propRes) 233 if err != nil { 234 sublog.Error().Err(err).Msg("error writing body") 235 return 236 } 237 return 238 } 239 240 if depth == net.DepthOne && key != "" && !strings.HasSuffix(key, "/") { 241 // when a key is provided and the depth is 1 we need to append a / to the key to list the children 242 key += "/" 243 } 244 245 client, err := s.gatewaySelector.Next() 246 if err != nil { 247 sublog.Error().Err(err).Msg("error selecting next gateway client") 248 w.WriteHeader(http.StatusInternalServerError) 249 return 250 } 251 // ask gateway for recycle items 252 getRecycleRes, err := client.ListRecycle(ctx, &provider.ListRecycleRequest{Ref: ref, Key: key}) 253 if err != nil { 254 sublog.Error().Err(err).Msg("error calling ListRecycle") 255 w.WriteHeader(http.StatusInternalServerError) 256 return 257 } 258 259 if getRecycleRes.Status.Code != rpc.Code_CODE_OK { 260 httpStatus := rstatus.HTTPStatusFromCode(getRecycleRes.Status.Code) 261 w.WriteHeader(httpStatus) 262 b, err := errors.Marshal(httpStatus, getRecycleRes.Status.Message, "", "") 263 errors.HandleWebdavError(&sublog, w, b, err) 264 return 265 } 266 267 items := getRecycleRes.RecycleItems 268 269 if depth == net.DepthInfinity { 270 var stack []string 271 // check sub-containers in reverse order and add them to the stack 272 // the reversed order here will produce a more logical sorting of results 273 for i := len(items) - 1; i >= 0; i-- { 274 // for i := range res.Infos { 275 if items[i].Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { 276 stack = append(stack, items[i].Key+"/") // fetch children of the item 277 } 278 } 279 280 for len(stack) > 0 { 281 key := stack[len(stack)-1] 282 getRecycleRes, err := client.ListRecycle(ctx, &provider.ListRecycleRequest{Ref: ref, Key: key}) 283 if err != nil { 284 sublog.Error().Err(err).Msg("error calling ListRecycle") 285 w.WriteHeader(http.StatusInternalServerError) 286 return 287 } 288 289 if getRecycleRes.Status.Code != rpc.Code_CODE_OK { 290 httpStatus := rstatus.HTTPStatusFromCode(getRecycleRes.Status.Code) 291 w.WriteHeader(httpStatus) 292 b, err := errors.Marshal(httpStatus, getRecycleRes.Status.Message, "", "") 293 errors.HandleWebdavError(&sublog, w, b, err) 294 return 295 } 296 items = append(items, getRecycleRes.RecycleItems...) 297 298 stack = stack[:len(stack)-1] 299 // check sub-containers in reverse order and add them to the stack 300 // the reversed order here will produce a more logical sorting of results 301 for i := len(getRecycleRes.RecycleItems) - 1; i >= 0; i-- { 302 // for i := range res.Infos { 303 if getRecycleRes.RecycleItems[i].Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { 304 stack = append(stack, getRecycleRes.RecycleItems[i].Key) 305 } 306 } 307 } 308 } 309 310 rootHref := path.Join(refBase, key) 311 propRes, err := h.formatTrashPropfind(ctx, s, ref.ResourceId.SpaceId, refBase, rootHref, &pf, items, depth != net.DepthZero) 312 if err != nil { 313 sublog.Error().Err(err).Msg("error formatting propfind") 314 w.WriteHeader(http.StatusInternalServerError) 315 return 316 } 317 w.Header().Set(net.HeaderDav, "1, 3, extended-mkcol") 318 w.Header().Set(net.HeaderContentType, "application/xml; charset=utf-8") 319 w.WriteHeader(http.StatusMultiStatus) 320 _, err = w.Write(propRes) 321 if err != nil { 322 sublog.Error().Err(err).Msg("error writing body") 323 return 324 } 325 } 326 327 func (h *TrashbinHandler) formatTrashPropfind(ctx context.Context, s *svc, spaceID, refBase, rootHref string, pf *propfind.XML, items []*provider.RecycleItem, fakeRoot bool) ([]byte, error) { 328 responses := make([]*propfind.ResponseXML, 0, len(items)+1) 329 if fakeRoot { 330 responses = append(responses, &propfind.ResponseXML{ 331 Href: net.EncodePath(path.Join(ctx.Value(net.CtxKeyBaseURI).(string), rootHref) + "/"), // url encode response.Href TODO 332 Propstat: []propfind.PropstatXML{ 333 { 334 Status: "HTTP/1.1 200 OK", 335 Prop: []prop.PropertyXML{ 336 prop.Raw("d:resourcetype", "<d:collection/>"), 337 }, 338 }, 339 { 340 Status: "HTTP/1.1 404 Not Found", 341 Prop: []prop.PropertyXML{ 342 prop.NotFound("oc:trashbin-original-filename"), 343 prop.NotFound("oc:trashbin-original-location"), 344 prop.NotFound("oc:trashbin-delete-datetime"), 345 prop.NotFound("d:getcontentlength"), 346 }, 347 }, 348 }, 349 }) 350 } 351 352 for i := range items { 353 res, err := h.itemToPropResponse(ctx, s, spaceID, refBase, pf, items[i]) 354 if err != nil { 355 return nil, err 356 } 357 responses = append(responses, res) 358 } 359 responsesXML, err := xml.Marshal(&responses) 360 if err != nil { 361 return nil, err 362 } 363 364 var buf bytes.Buffer 365 buf.WriteString(`<?xml version="1.0" encoding="utf-8"?><d:multistatus xmlns:d="DAV:" `) 366 buf.WriteString(`xmlns:s="http://sabredav.org/ns" xmlns:oc="http://owncloud.org/ns">`) 367 buf.Write(responsesXML) 368 buf.WriteString(`</d:multistatus>`) 369 return buf.Bytes(), nil 370 } 371 372 // itemToPropResponse needs to create a listing that contains a key and destination 373 // the key is the name of an entry in the trash listing 374 // for now we need to limit trash to the users home, so we can expect all trash keys to have the home storage as the opaque id 375 func (h *TrashbinHandler) itemToPropResponse(ctx context.Context, s *svc, spaceID, refBase string, pf *propfind.XML, item *provider.RecycleItem) (*propfind.ResponseXML, error) { 376 377 baseURI := ctx.Value(net.CtxKeyBaseURI).(string) 378 ref := path.Join(baseURI, refBase, item.GetKey()) 379 if item.GetType() == provider.ResourceType_RESOURCE_TYPE_CONTAINER { 380 ref += "/" 381 } 382 383 response := propfind.ResponseXML{ 384 Href: net.EncodePath(ref), // url encode response.Href 385 Propstat: []propfind.PropstatXML{}, 386 } 387 388 // TODO(jfd): if the path we list here is taken from the ListRecycle request we rely on the gateway to prefix it with the mount point 389 390 t := utils.TSToTime(item.GetDeletionTime()).UTC() 391 dTime := t.Format(time.RFC1123Z) 392 size := strconv.FormatUint(item.GetSize(), 10) 393 394 // when allprops has been requested 395 if pf.Allprop != nil { 396 // return all known properties 397 propstatOK := propfind.PropstatXML{ 398 Status: "HTTP/1.1 200 OK", 399 Prop: []prop.PropertyXML{}, 400 } 401 // yes this is redundant, can be derived from oc:trashbin-original-location which contains the full path, clients should not fetch it 402 propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:trashbin-original-filename", path.Base(item.GetRef().GetPath()))) 403 propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:trashbin-original-location", strings.TrimPrefix(item.GetRef().GetPath(), "/"))) 404 propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:trashbin-delete-timestamp", strconv.FormatUint(item.GetDeletionTime().GetSeconds(), 10))) 405 propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:trashbin-delete-datetime", dTime)) 406 if item.GetType() == provider.ResourceType_RESOURCE_TYPE_CONTAINER { 407 propstatOK.Prop = append(propstatOK.Prop, prop.Raw("d:resourcetype", "<d:collection/>")) 408 propstatOK.Prop = append(propstatOK.Prop, prop.Raw("oc:size", size)) 409 } else { 410 propstatOK.Prop = append(propstatOK.Prop, 411 prop.Escaped("d:resourcetype", ""), 412 prop.Escaped("d:getcontentlength", size), 413 ) 414 } 415 propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:spaceid", spaceID)) 416 response.Propstat = append(response.Propstat, propstatOK) 417 } else { 418 // otherwise return only the requested properties 419 propstatOK := propfind.PropstatXML{ 420 Status: "HTTP/1.1 200 OK", 421 Prop: []prop.PropertyXML{}, 422 } 423 propstatNotFound := propfind.PropstatXML{ 424 Status: "HTTP/1.1 404 Not Found", 425 Prop: []prop.PropertyXML{}, 426 } 427 for i := range pf.Prop { 428 switch pf.Prop[i].Space { 429 case net.NsOwncloud: 430 switch pf.Prop[i].Local { 431 case "oc:size": 432 if item.GetType() == provider.ResourceType_RESOURCE_TYPE_CONTAINER { 433 propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:size", size)) 434 } else { 435 propstatNotFound.Prop = append(propstatNotFound.Prop, prop.NotFound("oc:size")) 436 } 437 case "trashbin-original-filename": 438 // yes this is redundant, can be derived from oc:trashbin-original-location which contains the full path, clients should not fetch it 439 propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:trashbin-original-filename", path.Base(item.GetRef().GetPath()))) 440 case "trashbin-original-location": 441 // TODO (jfd) double check and clarify the cs3 spec what the Key is about and if Path is only the folder that contains the file or if it includes the filename 442 propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:trashbin-original-location", strings.TrimPrefix(item.GetRef().GetPath(), "/"))) 443 case "trashbin-delete-datetime": 444 propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:trashbin-delete-datetime", dTime)) 445 case "trashbin-delete-timestamp": 446 propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:trashbin-delete-timestamp", strconv.FormatUint(item.GetDeletionTime().GetSeconds(), 10))) 447 case "spaceid": 448 propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:spaceid", spaceID)) 449 default: 450 propstatNotFound.Prop = append(propstatNotFound.Prop, prop.NotFound("oc:"+pf.Prop[i].Local)) 451 } 452 case net.NsDav: 453 switch pf.Prop[i].Local { 454 case "getcontentlength": 455 if item.GetType() == provider.ResourceType_RESOURCE_TYPE_CONTAINER { 456 propstatNotFound.Prop = append(propstatNotFound.Prop, prop.NotFound("d:getcontentlength")) 457 } else { 458 propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("d:getcontentlength", size)) 459 } 460 case "resourcetype": 461 if item.GetType() == provider.ResourceType_RESOURCE_TYPE_CONTAINER { 462 propstatOK.Prop = append(propstatOK.Prop, prop.Raw("d:resourcetype", "<d:collection/>")) 463 } else { 464 propstatOK.Prop = append(propstatOK.Prop, prop.Raw("d:resourcetype", "")) 465 // redirectref is another option 466 } 467 case "getcontenttype": 468 if item.GetType() == provider.ResourceType_RESOURCE_TYPE_CONTAINER { 469 propstatOK.Prop = append(propstatOK.Prop, prop.Raw("d:getcontenttype", "httpd/unix-directory")) 470 } else { 471 propstatNotFound.Prop = append(propstatNotFound.Prop, prop.NotFound("d:getcontenttype")) 472 } 473 default: 474 propstatNotFound.Prop = append(propstatNotFound.Prop, prop.NotFound("d:"+pf.Prop[i].Local)) 475 } 476 default: 477 // TODO (jfd) lookup shortname for unknown namespaces? 478 propstatNotFound.Prop = append(propstatNotFound.Prop, prop.NotFound(pf.Prop[i].Space+":"+pf.Prop[i].Local)) 479 } 480 } 481 response.Propstat = append(response.Propstat, propstatOK, propstatNotFound) 482 } 483 484 return &response, nil 485 } 486 487 func (h *TrashbinHandler) restore(w http.ResponseWriter, r *http.Request, s *svc, ref, dst *provider.Reference, key string) { 488 ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "restore") 489 defer span.End() 490 491 sublog := appctx.GetLogger(ctx).With().Logger() 492 493 oh := r.Header.Get(net.HeaderOverwrite) 494 495 overwrite, err := net.ParseOverwrite(oh) 496 if err != nil { 497 w.WriteHeader(http.StatusBadRequest) 498 return 499 } 500 501 client, err := s.gatewaySelector.Next() 502 if err != nil { 503 sublog.Error().Err(err).Msg("error selecting next gateway client") 504 w.WriteHeader(http.StatusInternalServerError) 505 return 506 } 507 dstStatReq := &provider.StatRequest{Ref: dst} 508 dstStatRes, err := client.Stat(ctx, dstStatReq) 509 if err != nil { 510 sublog.Error().Err(err).Msg("error sending grpc stat request") 511 w.WriteHeader(http.StatusInternalServerError) 512 return 513 } 514 515 if dstStatRes.Status.Code != rpc.Code_CODE_OK && dstStatRes.Status.Code != rpc.Code_CODE_NOT_FOUND { 516 errors.HandleErrorStatus(&sublog, w, dstStatRes.Status) 517 return 518 } 519 520 // Restoring to a non-existent location is not supported by the WebDAV spec. The following block ensures the target 521 // restore location exists, and if it doesn't returns a conflict error code. 522 if dstStatRes.Status.Code == rpc.Code_CODE_NOT_FOUND && isNested(dst.Path) { 523 parentRef := &provider.Reference{ResourceId: dst.ResourceId, Path: utils.MakeRelativePath(path.Dir(dst.Path))} 524 parentStatReq := &provider.StatRequest{Ref: parentRef} 525 526 parentStatResponse, err := client.Stat(ctx, parentStatReq) 527 if err != nil { 528 sublog.Error().Err(err).Msg("error sending grpc stat request") 529 w.WriteHeader(http.StatusInternalServerError) 530 return 531 } 532 533 if parentStatResponse.Status.Code == rpc.Code_CODE_NOT_FOUND { 534 // 409 if intermediate dir is missing, see https://tools.ietf.org/html/rfc4918#section-9.8.5 535 w.WriteHeader(http.StatusConflict) 536 return 537 } 538 } 539 540 successCode := http.StatusCreated // 201 if new resource was created, see https://tools.ietf.org/html/rfc4918#section-9.9.4 541 if dstStatRes.Status.Code == rpc.Code_CODE_OK { 542 successCode = http.StatusNoContent // 204 if target already existed, see https://tools.ietf.org/html/rfc4918#section-9.9.4 543 544 if !overwrite { 545 sublog.Warn().Bool("overwrite", overwrite).Msg("dst already exists") 546 w.WriteHeader(http.StatusPreconditionFailed) // 412, see https://tools.ietf.org/html/rfc4918#section-9.9.4 547 b, err := errors.Marshal( 548 http.StatusPreconditionFailed, 549 "The destination node already exists, and the overwrite header is set to false", 550 net.HeaderOverwrite, 551 "", 552 ) 553 errors.HandleWebdavError(&sublog, w, b, err) 554 return 555 } 556 // delete existing tree 557 delReq := &provider.DeleteRequest{Ref: dst} 558 delRes, err := client.Delete(ctx, delReq) 559 if err != nil { 560 sublog.Error().Err(err).Msg("error sending grpc delete request") 561 w.WriteHeader(http.StatusInternalServerError) 562 return 563 } 564 565 if delRes.Status.Code != rpc.Code_CODE_OK && delRes.Status.Code != rpc.Code_CODE_NOT_FOUND { 566 errors.HandleErrorStatus(&sublog, w, delRes.Status) 567 return 568 } 569 } 570 571 req := &provider.RestoreRecycleItemRequest{ 572 Ref: ref, 573 Key: key, 574 RestoreRef: dst, 575 } 576 577 res, err := client.RestoreRecycleItem(ctx, req) 578 if err != nil { 579 sublog.Error().Err(err).Msg("error sending a grpc restore recycle item request") 580 w.WriteHeader(http.StatusInternalServerError) 581 return 582 } 583 584 if res.Status.Code != rpc.Code_CODE_OK { 585 if res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED { 586 w.WriteHeader(http.StatusForbidden) 587 b, err := errors.Marshal(http.StatusForbidden, "Permission denied to restore", "", "") 588 errors.HandleWebdavError(&sublog, w, b, err) 589 } 590 errors.HandleErrorStatus(&sublog, w, res.Status) 591 return 592 } 593 594 dstStatRes, err = client.Stat(ctx, dstStatReq) 595 if err != nil { 596 sublog.Error().Err(err).Msg("error sending grpc stat request") 597 w.WriteHeader(http.StatusInternalServerError) 598 return 599 } 600 if dstStatRes.Status.Code != rpc.Code_CODE_OK { 601 errors.HandleErrorStatus(&sublog, w, dstStatRes.Status) 602 return 603 } 604 605 info := dstStatRes.Info 606 w.Header().Set(net.HeaderContentType, info.MimeType) 607 w.Header().Set(net.HeaderETag, info.Etag) 608 w.Header().Set(net.HeaderOCFileID, storagespace.FormatResourceID(info.Id)) 609 w.Header().Set(net.HeaderOCETag, info.Etag) 610 611 w.WriteHeader(successCode) 612 } 613 614 // delete has only a key 615 func (h *TrashbinHandler) delete(w http.ResponseWriter, r *http.Request, s *svc, ref *provider.Reference, key string) { 616 ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "erase") 617 defer span.End() 618 619 sublog := appctx.GetLogger(ctx).With().Interface("reference", ref).Str("key", key).Logger() 620 621 req := &provider.PurgeRecycleRequest{ 622 Ref: ref, 623 Key: key, 624 } 625 626 client, err := s.gatewaySelector.Next() 627 if err != nil { 628 sublog.Error().Err(err).Msg("error selecting next gateway client") 629 w.WriteHeader(http.StatusInternalServerError) 630 return 631 } 632 res, err := client.PurgeRecycle(ctx, req) 633 if err != nil { 634 sublog.Error().Err(err).Msg("error sending a grpc restore recycle item request") 635 w.WriteHeader(http.StatusInternalServerError) 636 return 637 } 638 switch res.Status.Code { 639 case rpc.Code_CODE_OK: 640 w.WriteHeader(http.StatusNoContent) 641 case rpc.Code_CODE_NOT_FOUND: 642 sublog.Debug().Interface("status", res.Status).Msg("resource not found") 643 w.WriteHeader(http.StatusConflict) 644 m := fmt.Sprintf("key %s not found", key) 645 b, err := errors.Marshal(http.StatusConflict, m, "", "") 646 errors.HandleWebdavError(&sublog, w, b, err) 647 case rpc.Code_CODE_PERMISSION_DENIED: 648 w.WriteHeader(http.StatusForbidden) 649 var m string 650 if key == "" { 651 m = "Permission denied to purge recycle" 652 } else { 653 m = "Permission denied to delete" 654 } 655 b, err := errors.Marshal(http.StatusForbidden, m, "", "") 656 errors.HandleWebdavError(&sublog, w, b, err) 657 default: 658 errors.HandleErrorStatus(&sublog, w, res.Status) 659 } 660 } 661 662 func isNested(p string) bool { 663 dir, _ := path.Split(p) 664 return dir != "/" && dir != "./" 665 }