diff --git a/internal/grpc/services/storageprovider/storageprovider.go b/internal/grpc/services/storageprovider/storageprovider.go index d4e6ccf211..2c6253198b 100644 --- a/internal/grpc/services/storageprovider/storageprovider.go +++ b/internal/grpc/services/storageprovider/storageprovider.go @@ -940,6 +940,11 @@ func (s *service) ListContainer(ctx context.Context, req *provider.ListContainer }, nil } + // Ugly hack to enable search + 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 da921905af..6adea632c0 100644 --- a/internal/http/services/owncloud/ocdav/report.go +++ b/internal/http/services/owncloud/ocdav/report.go @@ -20,13 +20,16 @@ package ocdav import ( "encoding/xml" + "fmt" "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" providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/cs3org/reva/pkg/appctx" ctxpkg "github.com/cs3org/reva/pkg/ctx" ) @@ -39,14 +42,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("hugo: 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 @@ -57,21 +63,98 @@ 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("hugo: 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]*typespb.OpaqueEntry{ + "search": { + Decoder: "plain", + Value: []byte("search"), + }, + "searchString": { + Decoder: "plain", + Value: []byte(sf.Search.Pattern), + }, + } + + // TODO(salfagem): hardcoded path for the time being: + ref := &provider.Reference{Path: "/eos/project/c/cernbox"} + + req := &provider.ListContainerRequest{Opaque: &typespb.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 + } + + for _, v := range res.Infos { + fmt.Println(v) + } + + data := ` + + + + /remote.php/dav/spaces/1284d238-aa92-42ce-bdc4-0b0000009157$4c510ada-c86b-4815-8820-42cdf82c3d51/asd.txt + + + 1284d238-aa92-42ce-bdc4-0b0000009157$4c510ada-c86b-4815-8820-42cdf82c3d51!d5613880-307c-4e3e-b56d-97839fcf6d03 + 1284d238-aa92-42ce-bdc4-0b0000009157$4c510ada-c86b-4815-8820-42cdf82c3d51!4c510ada-c86b-4815-8820-42cdf82c3d51 + asd.txt + 2022-11-08T10:45:16Z + text/plain + RDNVW + + + 0 + 0.4809828996658325 + + HTTP/1.1 200 OK + + + + /remote.php/dav/spaces/1284d238-aa92-42ce-bdc4-0b0000009157$4c510ada-c86b-4815-8820-42cdf82c3d51/asdddddd + + + 1284d238-aa92-42ce-bdc4-0b0000009157$4c510ada-c86b-4815-8820-42cdf82c3d51!fa3c7a85-5fc4-46d3-a0b1-293a282da1b7 + 1284d238-aa92-42ce-bdc4-0b0000009157$4c510ada-c86b-4815-8820-42cdf82c3d51!4c510ada-c86b-4815-8820-42cdf82c3d51 + asdddddd + 2022-11-08T10:45:32Z + httpd/unix-directory + RDNVCK + + + + + 0 + 0.4809828996658325 + + HTTP/1.1 200 OK + + + +` + w.Write([]byte(data)) + w.WriteHeader(207) + return } func (s *svc) doFilterFiles(w http.ResponseWriter, r *http.Request, ff *reportFilterFiles, namespace string) { @@ -149,7 +232,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"` } @@ -167,34 +250,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 0f80ec55c6..aacfcfcf52 100644 --- a/pkg/eosclient/eosbinary/eosbinary.go +++ b/pkg/eosclient/eosbinary/eosbinary.go @@ -28,6 +28,7 @@ import ( "os/exec" "path" "path/filepath" + "regexp" "strconv" "strings" "syscall" @@ -742,6 +743,31 @@ func (c *Client) List(ctx context.Context, auth eosclient.Authorization, path st return c.parseFind(ctx, auth, path, stdout) } +// 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/c/cernbox") { + 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, _, err := c.executeEOS(ctx, args, auth) + if err != nil { + return nil, errors.Wrapf(err, "eosclient: error listing fn=%s", path) + } + 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() diff --git a/pkg/eosclient/eosclient.go b/pkg/eosclient/eosclient.go index b62c413458..53dbb5acfe 100644 --- a/pkg/eosclient/eosclient.go +++ b/pkg/eosclient/eosclient.go @@ -48,6 +48,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 47bd3c4a7f..03eb7258aa 100644 --- a/pkg/eosclient/eosgrpc/eosgrpc.go +++ b/pkg/eosclient/eosgrpc/eosgrpc.go @@ -1236,10 +1236,16 @@ func (c *Client) List(ctx context.Context, auth eosclient.Authorization, dpath s } +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 -// returning a handle to the body is nice, yet it gives less control on the transaction -// itself, e.g. strange timeouts or TCP issues may be more difficult to trace +// +// returning a handle to the body is nice, yet it gives less control on the transaction +// itself, e.g. strange timeouts or TCP issues may be more difficult to trace +// // Let's consider this experimental for the moment, maybe I'll like to add a config // parameter to choose between the two behaviours func (c *Client) Read(ctx context.Context, auth eosclient.Authorization, path string) (io.ReadCloser, error) { diff --git a/pkg/storage/utils/eosfs/eosfs.go b/pkg/storage/utils/eosfs/eosfs.go index 5983d8ade3..d21f8dbd09 100644 --- a/pkg/storage/utils/eosfs/eosfs.go +++ b/pkg/storage/utils/eosfs/eosfs.go @@ -1274,11 +1274,60 @@ 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") } + p = fs.wrap(ctx, p) + + 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" { + // TODO(salfagem): Ugly hack due to mdKeys being an array. + // - also check that searchString not empty: + searchString = mdKeys[i+1] + + log.Info().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 { + // filter out sys files + if !fs.conf.ShowHiddenSysFiles { + base := path.Base(eosFileInfo.File) + if hiddenReg.MatchString(base) { + continue + } + } + + if finfo, err := fs.convertToFileReference(ctx, eosFileInfo); err == nil { + finfos = append(finfos, finfo) + } + } + + return finfos, nil + } + } + if fs.conf.EnableHome { return fs.listWithHome(ctx, p) }