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  }