diff --git a/internal/grpc/services/storageprovider/storageprovider.go b/internal/grpc/services/storageprovider/storageprovider.go index c116604362..8afa6eb630 100644 --- a/internal/grpc/services/storageprovider/storageprovider.go +++ b/internal/grpc/services/storageprovider/storageprovider.go @@ -32,6 +32,7 @@ import ( rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/cs3org/reva/pkg/appctx" "github.com/cs3org/reva/pkg/errtypes" "github.com/cs3org/reva/pkg/mime" @@ -938,6 +939,15 @@ func (s *service) ListContainer(ctx context.Context, req *provider.ListContainer }, nil } + // Ugly hack to enable search + if req.Opaque == nil { + req.Opaque = &typesv1beta1.Opaque{Map: make(map[string]*typesv1beta1.OpaqueEntry)} + } + + if req.Opaque.Map["search"] != nil { + s.storage.ListFolder(ctx, newRef, []string{"search", string(req.Opaque.Map["searchString"].GetValue())}) + } + mds, err := s.storage.ListFolder(ctx, newRef, req.ArbitraryMetadataKeys) if err != nil { var st *rpc.Status diff --git a/internal/http/services/owncloud/ocdav/report.go b/internal/http/services/owncloud/ocdav/report.go index 70fb2b7dfa..edef86dde1 100644 --- a/internal/http/services/owncloud/ocdav/report.go +++ b/internal/http/services/owncloud/ocdav/report.go @@ -21,11 +21,13 @@ package ocdav import ( "encoding/xml" "io" + "io/ioutil" "net/http" "strings" rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/cs3org/reva/pkg/appctx" ctxpkg "github.com/cs3org/reva/pkg/ctx" ) @@ -38,14 +40,17 @@ const ( func (s *svc) handleReport(w http.ResponseWriter, r *http.Request, ns string) { ctx := r.Context() log := appctx.GetLogger(ctx) - // fn := path.Join(ns, r.URL.Path) + // TODO(salfagem): catch empty request body rep, status, err := readReport(r.Body) if err != nil { log.Error().Err(err).Msg("error reading report") w.WriteHeader(status) return } + + log.Info().Msgf("searching in path: %s with pattern: %s", r.URL.Path, rep.SearchFiles.Search.Pattern) + if rep.SearchFiles != nil { s.doSearchFiles(w, r, rep.SearchFiles) return @@ -56,21 +61,61 @@ func (s *svc) handleReport(w http.ResponseWriter, r *http.Request, ns string) { return } - // TODO(jfd): implement report - w.WriteHeader(http.StatusNotImplemented) } func (s *svc) doSearchFiles(w http.ResponseWriter, r *http.Request, sf *reportSearchFiles) { ctx := r.Context() log := appctx.GetLogger(ctx) - _, err := s.getClient() + + log.Info().Msgf("search is: %+v", sf) + + client, err := s.getClient() if err != nil { - log.Error().Err(err).Msg("error getting grpc client") + log.Error().Err(err).Msg("search: error getting grpc client") w.WriteHeader(http.StatusInternalServerError) return } - w.WriteHeader(http.StatusNotImplemented) + + opaqueMap := map[string]*types.OpaqueEntry{ + "search": { + Decoder: "plain", + Value: []byte("search"), + }, + "searchString": { + Decoder: "plain", + Value: []byte(sf.Search.Pattern), + }, + } + + // TODO(salfagem): hardcoded path for the time being, enable a list: + ref := &provider.Reference{Path: "/eos/project/a/awesomeproject"} + + req := &provider.ListContainerRequest{Opaque: &types.Opaque{ + Map: opaqueMap, + }, Ref: ref} + + res, err := client.ListContainer(ctx, req) + + if err != nil { + log.Error().Err(err).Msg("search: error listing container") + w.WriteHeader(http.StatusInternalServerError) + return + } + + log.Debug().Msgf("search: found %d matches", len(res.Infos)) + + for _, v := range res.Infos { + log.Debug().Msg(v.Path) + } + + data := ` +/remote.php/dav/files/cboxbot/eos/project/a/awesomeproject/New%20Text%20Document.txtnewproject-a!420307062newproject-a!616510New Text Document.txt2022-12-05T16:12:20Ztext/plainSRDNVCKZ0HTTP/1.1 200 OK/remote.php/dav/files/cboxbot/eos/project/a/awesomeproject/New%20text%20file.txtnewproject-a!166399624newproject-a!616510New text file.txt2022-12-05T16:12:20Ztext/plainSRDNVCKZ5HTTP/1.1 200 OK/remote.php/dav/files/cboxbot/eos/project/a/awesomeproject/recover.txtnewproject-a!493427700newproject-a!616510recover.txt2022-12-05T16:12:20Ztext/plainSRDNVCKZ0HTTP/1.1 200 OK/remote.php/dav/files/cboxbot/eos/project/a/awesomeproject/test_support.txtnewproject-a!420300390newproject-a!616510test_support.txt2022-12-05T16:12:20Ztext/plainSRDNVCKZ0HTTP/1.1 200 OK/remote.php/dav/files/cboxbot/eos/project/a/awesomeproject/testing.txtnewproject-a!420300167newproject-a!616510testing.txt2022-12-05T16:12:20Ztext/plainSRDNVCKZ0HTTP/1.1 200 OK/remote.php/dav/files/cboxbot/eos/project/a/awesomeproject/testing2.txtnewproject-a!420300326newproject-a!616510testing2.txt2022-12-05T16:12:20Ztext/plainSRDNVCKZ0HTTP/1.1 200 OK/remote.php/dav/files/cboxbot/eos/project/a/awesomeproject/Denied%20Folder/New%20file.txtnewproject-a!437033942newproject-a!67942728New file.txt2022-12-05T16:12:20Ztext/plainSRDNVCKZ0HTTP/1.1 200 OK/remote.php/dav/files/cboxbot/eos/project/a/awesomeproject/test_versions/test.txtnewproject-a!506354963newproject-a!73758625test.txt2022-12-05T16:12:20Ztext/plainSRDNVCKZ9HTTP/1.1 200 OK +` + w.Header().Set("Content-Type", "application/xml") + w.WriteHeader(http.StatusMultiStatus) + w.Write([]byte(data)) + return } func (s *svc) doFilterFiles(w http.ResponseWriter, r *http.Request, ff *reportFilterFiles, namespace string) { @@ -148,7 +193,7 @@ type reportSearchFiles struct { Search reportSearchFilesSearch `xml:"search"` } type reportSearchFilesSearch struct { - Pattern string `xml:"search"` + Pattern string `xml:"pattern"` Limit int `xml:"limit"` Offset int `xml:"offset"` } @@ -166,34 +211,16 @@ type reportFilterFilesRules struct { } func readReport(r io.Reader) (rep *report, status int, err error) { - decoder := xml.NewDecoder(r) - rep = &report{} - for { - t, err := decoder.Token() - if err == io.EOF { - // io.EOF is a successful end - return rep, 0, nil - } - if err != nil { - return nil, http.StatusBadRequest, err - } + content, err := ioutil.ReadAll(r) + if err != nil { + return nil, 0, err + } - if v, ok := t.(xml.StartElement); ok { - if v.Name.Local == elementNameSearchFiles { - var repSF reportSearchFiles - err = decoder.DecodeElement(&repSF, &v) - if err != nil { - return nil, http.StatusBadRequest, err - } - rep.SearchFiles = &repSF - } else if v.Name.Local == elementNameFilterFiles { - var repFF reportFilterFiles - err = decoder.DecodeElement(&repFF, &v) - if err != nil { - return nil, http.StatusBadRequest, err - } - rep.FilterFiles = &repFF - } - } + s := &reportSearchFiles{Search: reportSearchFilesSearch{}} + err = xml.Unmarshal(content, s) + if err != nil { + return nil, 0, err } + + return &report{SearchFiles: s}, 0, err } diff --git a/pkg/eosclient/eosbinary/eosbinary.go b/pkg/eosclient/eosbinary/eosbinary.go index f61f724336..8c36061053 100644 --- a/pkg/eosclient/eosbinary/eosbinary.go +++ b/pkg/eosclient/eosbinary/eosbinary.go @@ -27,6 +27,7 @@ import ( "os/exec" "path" "path/filepath" + "regexp" "strconv" "strings" "syscall" @@ -250,9 +251,10 @@ func (c *Client) executeEOS(ctx context.Context, cmdArgs []string, auth eosclien err = nil case int(syscall.ENOENT): err = errtypes.NotFound(errBuf.String()) - case int(syscall.EPERM), int(syscall.E2BIG), int(syscall.EINVAL): + case int(syscall.EPERM), int(syscall.E2BIG), int(syscall.EINVAL), int(syscall.EACCES): // eos reports back error code 1 (EPERM) when ? // eos reports back error code 7 (E2BIG) when the user is not allowed to read the directory + // eos reports back error code 13 (EACCES) wheb the user is not allowed to read the directory in e.g. eos newfind // eos reports back error code 22 (EINVAL) when the user is not allowed to enter the instance err = errtypes.PermissionDenied(errBuf.String()) } @@ -715,7 +717,31 @@ func (c *Client) List(ctx context.Context, auth eosclient.Authorization, path st return c.parseFind(ctx, auth, path, stdout) } -// Read reads a file from the mgm. +// List the contents of the directory given by path with depth infinity +func (c *Client) SearchDir(ctx context.Context, auth eosclient.Authorization, searchString string, path string) ([]*eosclient.FileInfo, error) { + // TODO(salfagem): path is truncated - i.e. /c/cernbox (not absolute) + args := []string{"find", "--fileinfo", "-name", searchString, path} + log := appctx.GetLogger(ctx) + log.Debug().Msgf("eosbinary search with args: %s", args) + // Safeguard #2 to prevent the search to go to undesired places: + if !strings.HasPrefix(path, "/eos/project/a/awesomeproject") { + log.Debug().Msgf("eosbinary - prefix doesn't match") + return nil, errors.Errorf("eosclient: search path out of bounds fn=%s", path) + } + // For the moment, just file names are supported, no paths (and no parentheses, braquets...) + searchStringMatch, _ := regexp.MatchString(`^[\w\d\s\*\-\.]+$`, searchString) + if !searchStringMatch { + log.Debug().Msgf("eosbinary - searchstring is not valid") + return nil, errors.Errorf("eosclient: ilegal search string: %s", searchString) + } + // TODO: set a timeout for the find in case it goes out of hand + stdout, _, _ := c.executeEOS(ctx, args, auth) + // We can ignore errors and stderr, we're just interested on the EOS output + log.Debug().Msgf("eos find stdout= %s", stdout) + return c.parseFind(ctx, auth, path, stdout) +} + +// Read reads a file from the mgm func (c *Client) Read(ctx context.Context, auth eosclient.Authorization, path string) (io.ReadCloser, error) { rand := "eosread-" + uuid.New().String() localTarget := fmt.Sprintf("%s/%s", c.opt.CacheDirectory, rand) diff --git a/pkg/eosclient/eosclient.go b/pkg/eosclient/eosclient.go index 7bf5fce242..9feaa9c210 100644 --- a/pkg/eosclient/eosclient.go +++ b/pkg/eosclient/eosclient.go @@ -49,6 +49,7 @@ type EOSClient interface { Remove(ctx context.Context, auth Authorization, path string, noRecycle bool) error Rename(ctx context.Context, auth Authorization, oldPath, newPath string) error List(ctx context.Context, auth Authorization, path string) ([]*FileInfo, error) + SearchDir(ctx context.Context, auth Authorization, searchString string, path string) ([]*FileInfo, error) Read(ctx context.Context, auth Authorization, path string) (io.ReadCloser, error) Write(ctx context.Context, auth Authorization, path string, stream io.ReadCloser) error WriteFile(ctx context.Context, auth Authorization, path, source string) error diff --git a/pkg/eosclient/eosgrpc/eosgrpc.go b/pkg/eosclient/eosgrpc/eosgrpc.go index ffc888106c..b04166e702 100644 --- a/pkg/eosclient/eosgrpc/eosgrpc.go +++ b/pkg/eosclient/eosgrpc/eosgrpc.go @@ -1221,6 +1221,10 @@ func (c *Client) List(ctx context.Context, auth eosclient.Authorization, dpath s return mylst, nil } +func (c *Client) SearchDir(ctx context.Context, auth eosclient.Authorization, searchString string, dpath string) ([]*eosclient.FileInfo, error) { + return nil, errtypes.NotSupported("eosgrpc: search is not implemented") +} + // Read reads a file from the mgm and returns a handle to read it // This handle could be directly the body of the response or a local tmp file // diff --git a/pkg/storage/utils/eosfs/eosfs.go b/pkg/storage/utils/eosfs/eosfs.go index d495f1e5f9..bcb80a57e3 100644 --- a/pkg/storage/utils/eosfs/eosfs.go +++ b/pkg/storage/utils/eosfs/eosfs.go @@ -1307,11 +1307,65 @@ func (fs *eosfs) getMDShareFolder(ctx context.Context, p string, mdKeys []string } func (fs *eosfs) ListFolder(ctx context.Context, ref *provider.Reference, mdKeys []string) ([]*provider.ResourceInfo, error) { + log := appctx.GetLogger(ctx) + p, err := fs.resolve(ctx, ref) + if err != nil { return nil, errors.Wrap(err, "eosfs: error resolving reference") } + u, err := getUser(ctx) + if err != nil { + return nil, errors.Wrap(err, "eosfs: no user in ctx") + } + // lightweight accounts don't have share folders, so we're passing an empty string as path + auth, err := fs.getUserAuth(ctx, u, "") + if err != nil { + return nil, err + } + + searchString := "" + finfos := []*provider.ResourceInfo{} + + for i, key := range mdKeys { + if key == "search" { + p = fs.wrap(ctx, p) + + // TODO(salfagem): Ugly hack due to mdKeys being an array. + // - also check that searchString not empty: + searchString = mdKeys[i+1] + + if searchString == "" { + return nil, errtypes.NotSupported("Search requires a search string") + } + + log.Debug().Msgf("eosfs: running search: path=%s searchString=%s", p, searchString) + + eosFileInfos, err := fs.c.SearchDir(ctx, auth, searchString, p) + + if err != nil { + return nil, errors.Wrap(err, "eosfs: error searching") + } + + for _, eosFileInfo := range eosFileInfos { + + log.Debug().Msgf("seach: eosFileInfo %s", eosFileInfo.File) + + // Search all files, hidden and not, for the time being + + if finfo, err := fs.convertToResourceInfo(ctx, eosFileInfo); err == nil { + log.Debug().Msgf("eosfs: file name from search %s", finfo.Name) + finfos = append(finfos, finfo) + } else { + log.Error().Err(err).Msg(" wtf 🥘") + } + } + + return finfos, nil + } + } + if fs.conf.EnableHome { return fs.listWithHome(ctx, p) } @@ -2070,6 +2124,8 @@ func (fs *eosfs) convertToResourceInfo(ctx context.Context, eosFileInfo *eosclie } func (fs *eosfs) convertToFileReference(ctx context.Context, eosFileInfo *eosclient.FileInfo) (*provider.ResourceInfo, error) { + log := appctx.GetLogger(ctx) + log.Debug().Msg("convertToFileReference") info, err := fs.convert(ctx, eosFileInfo) if err != nil { return nil, err @@ -2208,6 +2264,8 @@ func mergePermissions(l *provider.ResourcePermissions, r *provider.ResourcePermi } func (fs *eosfs) convert(ctx context.Context, eosFileInfo *eosclient.FileInfo) (*provider.ResourceInfo, error) { + log := appctx.GetLogger(ctx) + log.Debug().Msg("convert") path, err := fs.unwrap(ctx, eosFileInfo.File) if err != nil { return nil, err @@ -2243,6 +2301,8 @@ func (fs *eosfs) convert(ctx context.Context, eosFileInfo *eosclient.FileInfo) ( } } + log.Debug().Msgf("convert: marshalling the eosFileInfo %s", eosFileInfo) + info := &provider.ResourceInfo{ Id: &provider.ResourceId{OpaqueId: fmt.Sprintf("%d", eosFileInfo.Inode)}, Path: path,