github.com/cs3org/reva/v2@v2.27.7/internal/http/services/owncloud/ocdav/versions.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  	"context"
    23  	"net/http"
    24  	"path"
    25  
    26  	"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/config"
    27  	"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/errors"
    28  	"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/net"
    29  	"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/propfind"
    30  	"github.com/cs3org/reva/v2/pkg/storagespace"
    31  	"github.com/cs3org/reva/v2/pkg/utils"
    32  
    33  	rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
    34  	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
    35  	types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
    36  	"github.com/cs3org/reva/v2/pkg/appctx"
    37  	"github.com/cs3org/reva/v2/pkg/rhttp/router"
    38  )
    39  
    40  // VersionsHandler handles version requests
    41  type VersionsHandler struct {
    42  }
    43  
    44  func (h *VersionsHandler) init(c *config.Config) error {
    45  	return nil
    46  }
    47  
    48  // Handler handles requests
    49  // versions can be listed with a PROPFIND to /remote.php/dav/meta/<fileid>/v
    50  // a version is identified by a timestamp, eg. /remote.php/dav/meta/<fileid>/v/1561410426
    51  func (h *VersionsHandler) Handler(s *svc, rid *provider.ResourceId) http.Handler {
    52  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    53  		ctx := r.Context()
    54  
    55  		if rid == nil {
    56  			http.Error(w, "404 Not Found", http.StatusNotFound)
    57  			return
    58  		}
    59  
    60  		// baseURI is encoded as part of the response payload in href field
    61  		baseURI := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), storagespace.FormatResourceID(rid))
    62  		ctx = context.WithValue(ctx, net.CtxKeyBaseURI, baseURI)
    63  		r = r.WithContext(ctx)
    64  
    65  		var key string
    66  		key, r.URL.Path = router.ShiftPath(r.URL.Path)
    67  		if r.Method == http.MethodOptions {
    68  			s.handleOptions(w, r)
    69  			return
    70  		}
    71  		if key == "" && r.Method == MethodPropfind {
    72  			h.doListVersions(w, r, s, rid)
    73  			return
    74  		}
    75  		if key != "" {
    76  			switch r.Method {
    77  			case MethodCopy:
    78  				// TODO(jfd) cs3api has no delete file version call
    79  				// TODO(jfd) restore version to given Destination, but cs3api has no destination
    80  				h.doRestore(w, r, s, rid, key)
    81  				return
    82  			case http.MethodHead:
    83  				log := appctx.GetLogger(ctx)
    84  				ref := &provider.Reference{
    85  					ResourceId: &provider.ResourceId{
    86  						StorageId: rid.StorageId,
    87  						SpaceId:   rid.SpaceId,
    88  						OpaqueId:  key,
    89  					},
    90  					Path: utils.MakeRelativePath(r.URL.Path),
    91  				}
    92  				s.handleHead(ctx, w, r, ref, *log)
    93  				return
    94  			case http.MethodGet:
    95  				log := appctx.GetLogger(ctx)
    96  				ref := &provider.Reference{
    97  					ResourceId: &provider.ResourceId{
    98  						StorageId: rid.StorageId,
    99  						SpaceId:   rid.SpaceId,
   100  						OpaqueId:  key,
   101  					},
   102  					Path: utils.MakeRelativePath(r.URL.Path),
   103  				}
   104  				s.handleGet(ctx, w, r, ref, "spaces", *log)
   105  				return
   106  			}
   107  		}
   108  
   109  		http.Error(w, "501 Forbidden", http.StatusNotImplemented)
   110  	})
   111  }
   112  
   113  func (h *VersionsHandler) doListVersions(w http.ResponseWriter, r *http.Request, s *svc, rid *provider.ResourceId) {
   114  	ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "listVersions")
   115  	defer span.End()
   116  
   117  	sublog := appctx.GetLogger(ctx).With().Interface("resourceid", rid).Logger()
   118  
   119  	pf, status, err := propfind.ReadPropfind(r.Body)
   120  	if err != nil {
   121  		sublog.Debug().Err(err).Msg("error reading propfind request")
   122  		w.WriteHeader(status)
   123  		return
   124  	}
   125  
   126  	client, err := s.gatewaySelector.Next()
   127  	if err != nil {
   128  		sublog.Error().Err(err).Msg("error selecting next gateway client")
   129  		w.WriteHeader(http.StatusInternalServerError)
   130  		return
   131  	}
   132  	ref := &provider.Reference{ResourceId: rid}
   133  	res, err := client.Stat(ctx, &provider.StatRequest{Ref: ref})
   134  	if err != nil {
   135  		sublog.Error().Err(err).Msg("error sending a grpc stat request")
   136  		w.WriteHeader(http.StatusInternalServerError)
   137  		return
   138  	}
   139  	if res.Status.Code != rpc.Code_CODE_OK {
   140  		if res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED || res.Status.Code == rpc.Code_CODE_NOT_FOUND {
   141  			w.WriteHeader(http.StatusNotFound)
   142  			b, err := errors.Marshal(http.StatusNotFound, "Resource not found", "", "")
   143  			errors.HandleWebdavError(&sublog, w, b, err)
   144  			return
   145  		}
   146  		errors.HandleErrorStatus(&sublog, w, res.Status)
   147  		return
   148  	}
   149  
   150  	info := res.Info
   151  
   152  	lvRes, err := client.ListFileVersions(ctx, &provider.ListFileVersionsRequest{Ref: ref})
   153  	if err != nil {
   154  		sublog.Error().Err(err).Msg("error sending list container grpc request")
   155  		w.WriteHeader(http.StatusInternalServerError)
   156  		return
   157  	}
   158  	if lvRes.Status.Code != rpc.Code_CODE_OK {
   159  		if lvRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED {
   160  			w.WriteHeader(http.StatusForbidden)
   161  			b, err := errors.Marshal(http.StatusForbidden, "You have no permission to list file versions on this resource", "", "")
   162  			errors.HandleWebdavError(&sublog, w, b, err)
   163  			return
   164  		}
   165  		errors.HandleErrorStatus(&sublog, w, lvRes.Status)
   166  		return
   167  	}
   168  
   169  	versions := lvRes.GetVersions()
   170  	infos := make([]*provider.ResourceInfo, 0, len(versions)+1)
   171  	// add version dir . entry, derived from file info
   172  	infos = append(infos, &provider.ResourceInfo{
   173  		Type: provider.ResourceType_RESOURCE_TYPE_CONTAINER,
   174  	})
   175  
   176  	for i := range versions {
   177  		vi := &provider.ResourceInfo{
   178  			// TODO(jfd) we cannot access version content, this will be a problem when trying to fetch version thumbnails
   179  			// Opaque
   180  			Type: provider.ResourceType_RESOURCE_TYPE_FILE,
   181  			Id: &provider.ResourceId{
   182  				StorageId: "versions",
   183  				OpaqueId:  info.Id.OpaqueId + "@" + versions[i].GetKey(),
   184  			},
   185  			// Checksum
   186  			Etag: versions[i].Etag,
   187  			// MimeType
   188  			Mtime: &types.Timestamp{
   189  				Seconds: versions[i].Mtime,
   190  				// TODO cs3apis FileVersion should use types.Timestamp instead of uint64
   191  			},
   192  			Path: path.Join("v", versions[i].Key),
   193  			// PermissionSet
   194  			Size:  versions[i].Size,
   195  			Owner: info.Owner,
   196  		}
   197  		infos = append(infos, vi)
   198  	}
   199  
   200  	prefer := net.ParsePrefer(r.Header.Get("prefer"))
   201  	returnMinimal := prefer[net.HeaderPreferReturn] == "minimal"
   202  
   203  	propRes, err := propfind.MultistatusResponse(ctx, &pf, infos, s.c.PublicURL, "", nil, returnMinimal)
   204  	if err != nil {
   205  		sublog.Error().Err(err).Msg("error formatting propfind")
   206  		w.WriteHeader(http.StatusInternalServerError)
   207  		return
   208  	}
   209  	w.Header().Set(net.HeaderDav, "1, 3, extended-mkcol")
   210  	w.Header().Set(net.HeaderContentType, "application/xml; charset=utf-8")
   211  	w.Header().Set(net.HeaderVary, net.HeaderPrefer)
   212  	if returnMinimal {
   213  		w.Header().Set(net.HeaderPreferenceApplied, "return=minimal")
   214  	}
   215  	w.WriteHeader(http.StatusMultiStatus)
   216  	_, err = w.Write(propRes)
   217  	if err != nil {
   218  		sublog.Error().Err(err).Msg("error writing body")
   219  		return
   220  	}
   221  
   222  }
   223  
   224  func (h *VersionsHandler) doRestore(w http.ResponseWriter, r *http.Request, s *svc, rid *provider.ResourceId, key string) {
   225  	ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "restore")
   226  	defer span.End()
   227  
   228  	sublog := appctx.GetLogger(ctx).With().Interface("resourceid", rid).Str("key", key).Logger()
   229  
   230  	req := &provider.RestoreFileVersionRequest{
   231  		Ref: &provider.Reference{ResourceId: rid},
   232  		Key: key,
   233  	}
   234  
   235  	client, err := s.gatewaySelector.Next()
   236  	if err != nil {
   237  		sublog.Error().Err(err).Msg("error selecting next gateway client")
   238  		w.WriteHeader(http.StatusInternalServerError)
   239  		return
   240  	}
   241  	res, err := client.RestoreFileVersion(ctx, req)
   242  	if err != nil {
   243  		sublog.Error().Err(err).Msg("error sending a grpc restore version request")
   244  		w.WriteHeader(http.StatusInternalServerError)
   245  		return
   246  	}
   247  	if res.Status.Code != rpc.Code_CODE_OK {
   248  		if res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED {
   249  			w.WriteHeader(http.StatusForbidden)
   250  			b, err := errors.Marshal(http.StatusForbidden, "You have no permission to restore versions on this resource", "", "")
   251  			errors.HandleWebdavError(&sublog, w, b, err)
   252  			return
   253  		}
   254  		errors.HandleErrorStatus(&sublog, w, res.Status)
   255  		return
   256  	}
   257  	w.WriteHeader(http.StatusNoContent)
   258  }