github.com/cs3org/reva/v2@v2.27.7/pkg/eosclient/eosbinary/eosbinary.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 eosbinary
    20  
    21  import (
    22  	"bytes"
    23  	"context"
    24  	"fmt"
    25  	"io"
    26  	"os"
    27  	"os/exec"
    28  	"path"
    29  	"path/filepath"
    30  	"strconv"
    31  	"strings"
    32  	"syscall"
    33  	"time"
    34  
    35  	"github.com/cs3org/reva/v2/pkg/appctx"
    36  	ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
    37  	"github.com/cs3org/reva/v2/pkg/eosclient"
    38  	"github.com/cs3org/reva/v2/pkg/errtypes"
    39  	"github.com/cs3org/reva/v2/pkg/storage/utils/acl"
    40  	"github.com/google/uuid"
    41  	"github.com/pkg/errors"
    42  	"go.opentelemetry.io/otel/trace"
    43  )
    44  
    45  const (
    46  	versionPrefix  = ".sys.v#."
    47  	lwShareAttrKey = "reva.lwshare"
    48  	userACLEvalKey = "eval.useracl"
    49  	favoritesKey   = "http://owncloud.org/ns/favorite"
    50  )
    51  
    52  func serializeAttribute(a *eosclient.Attribute) string {
    53  	return fmt.Sprintf("%s.%s=%s", attrTypeToString(a.Type), a.Key, a.Val)
    54  }
    55  
    56  func attrTypeToString(at eosclient.AttrType) string {
    57  	switch at {
    58  	case eosclient.SystemAttr:
    59  		return "sys"
    60  	case eosclient.UserAttr:
    61  		return "user"
    62  	default:
    63  		return "invalid"
    64  	}
    65  }
    66  
    67  func isValidAttribute(a *eosclient.Attribute) bool {
    68  	// validate that an attribute is correct.
    69  	if (a.Type != eosclient.SystemAttr && a.Type != eosclient.UserAttr) || a.Key == "" {
    70  		return false
    71  	}
    72  	return true
    73  }
    74  
    75  // Options to configure the Client.
    76  type Options struct {
    77  
    78  	// ForceSingleUserMode forces all connections to use only one user.
    79  	// This is the case when access to EOS is done from FUSE under apache or www-data.
    80  	ForceSingleUserMode bool
    81  
    82  	// UseKeyTabAuth changes will authenticate requests by using an EOS keytab.
    83  	UseKeytab bool
    84  
    85  	// Whether to maintain the same inode across various versions of a file.
    86  	// Requires extra metadata operations if set to true
    87  	VersionInvariant bool
    88  
    89  	// SingleUsername is the username to use when connecting to EOS.
    90  	// Defaults to apache
    91  	SingleUsername string
    92  
    93  	// Location of the eos binary.
    94  	// Default is /usr/bin/eos.
    95  	EosBinary string
    96  
    97  	// Location of the xrdcopy binary.
    98  	// Default is /opt/eos/xrootd/bin/xrdcopy.
    99  	XrdcopyBinary string
   100  
   101  	// URL of the EOS MGM.
   102  	// Default is root://eos-example.org
   103  	URL string
   104  
   105  	// Location on the local fs where to store reads.
   106  	// Defaults to os.TempDir()
   107  	CacheDirectory string
   108  
   109  	// Keytab is the location of the EOS keytab file.
   110  	Keytab string
   111  
   112  	// SecProtocol is the comma separated list of security protocols used by xrootd.
   113  	// For example: "sss, unix"
   114  	SecProtocol string
   115  
   116  	// TokenExpiry stores in seconds the time after which generated tokens will expire
   117  	// Default is 3600
   118  	TokenExpiry int
   119  }
   120  
   121  func (opt *Options) init() {
   122  	if opt.ForceSingleUserMode && opt.SingleUsername != "" {
   123  		opt.SingleUsername = "apache"
   124  	}
   125  
   126  	if opt.EosBinary == "" {
   127  		opt.EosBinary = "/usr/bin/eos"
   128  	}
   129  
   130  	if opt.XrdcopyBinary == "" {
   131  		opt.XrdcopyBinary = "/opt/eos/xrootd/bin/xrdcopy"
   132  	}
   133  
   134  	if opt.URL == "" {
   135  		opt.URL = "root://eos-example.org"
   136  	}
   137  
   138  	if opt.CacheDirectory == "" {
   139  		opt.CacheDirectory = os.TempDir()
   140  	}
   141  }
   142  
   143  // Client performs actions against a EOS management node (MGM).
   144  // It requires the eos-client and xrootd-client packages installed to work.
   145  type Client struct {
   146  	opt *Options
   147  }
   148  
   149  // New creates a new client with the given options.
   150  func New(opt *Options) (*Client, error) {
   151  	opt.init()
   152  	c := new(Client)
   153  	c.opt = opt
   154  	return c, nil
   155  }
   156  
   157  // executeXRDCopy executes xrdcpy commands and returns the stdout, stderr and return code
   158  func (c *Client) executeXRDCopy(ctx context.Context, cmdArgs []string) (string, string, error) {
   159  	log := appctx.GetLogger(ctx)
   160  
   161  	outBuf := &bytes.Buffer{}
   162  	errBuf := &bytes.Buffer{}
   163  
   164  	cmd := exec.CommandContext(ctx, c.opt.XrdcopyBinary, cmdArgs...)
   165  	cmd.Stdout = outBuf
   166  	cmd.Stderr = errBuf
   167  	cmd.Env = []string{
   168  		"EOS_MGM_URL=" + c.opt.URL,
   169  	}
   170  
   171  	if c.opt.UseKeytab {
   172  		cmd.Env = append(cmd.Env, "XrdSecPROTOCOL="+c.opt.SecProtocol)
   173  		cmd.Env = append(cmd.Env, "XrdSecSSSKT="+c.opt.Keytab)
   174  	}
   175  
   176  	err := cmd.Run()
   177  
   178  	var exitStatus int
   179  	if exiterr, ok := err.(*exec.ExitError); ok {
   180  		// The program has exited with an exit code != 0
   181  		// This works on both Unix and Windows. Although package
   182  		// syscall is generally platform dependent, WaitStatus is
   183  		// defined for both Unix and Windows and in both cases has
   184  		// an ExitStatus() method with the same signature.
   185  		if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
   186  
   187  			exitStatus = status.ExitStatus()
   188  			switch exitStatus {
   189  			case 0:
   190  				err = nil
   191  			case int(syscall.ENOENT):
   192  				err = errtypes.NotFound(errBuf.String())
   193  			}
   194  		}
   195  	}
   196  
   197  	// check for operation not permitted error
   198  	if strings.Contains(errBuf.String(), "Operation not permitted") {
   199  		err = errtypes.InvalidCredentials("eosclient: no sufficient permissions for the operation")
   200  	}
   201  
   202  	args := fmt.Sprintf("%s", cmd.Args)
   203  	env := fmt.Sprintf("%s", cmd.Env)
   204  	log.Info().Str("args", args).Str("env", env).Int("exit", exitStatus).Msg("eos cmd")
   205  
   206  	return outBuf.String(), errBuf.String(), err
   207  }
   208  
   209  // exec executes only EOS commands the command and returns the stdout, stderr and return code.
   210  func (c *Client) executeEOS(ctx context.Context, cmdArgs []string, auth eosclient.Authorization) (string, string, error) {
   211  	log := appctx.GetLogger(ctx)
   212  
   213  	outBuf := &bytes.Buffer{}
   214  	errBuf := &bytes.Buffer{}
   215  
   216  	cmd := exec.CommandContext(ctx, c.opt.EosBinary)
   217  	cmd.Stdout = outBuf
   218  	cmd.Stderr = errBuf
   219  	cmd.Env = []string{
   220  		"EOS_MGM_URL=" + c.opt.URL,
   221  	}
   222  
   223  	if auth.Token != "" {
   224  		cmd.Env = append(cmd.Env, "EOSAUTHZ="+auth.Token)
   225  	} else if auth.Role.UID != "" && auth.Role.GID != "" {
   226  		cmd.Args = append(cmd.Args, []string{"-r", auth.Role.UID, auth.Role.GID}...)
   227  	}
   228  
   229  	if c.opt.UseKeytab {
   230  		cmd.Env = append(cmd.Env, "XrdSecPROTOCOL="+c.opt.SecProtocol)
   231  		cmd.Env = append(cmd.Env, "XrdSecSSSKT="+c.opt.Keytab)
   232  	}
   233  
   234  	cmd.Args = append(cmd.Args, cmdArgs...)
   235  
   236  	span := trace.SpanFromContext(ctx)
   237  	cmd.Args = append(cmd.Args, "--comment", span.SpanContext().TraceID().String())
   238  
   239  	err := cmd.Run()
   240  
   241  	var exitStatus int
   242  	if exiterr, ok := err.(*exec.ExitError); ok {
   243  		// The program has exited with an exit code != 0
   244  		// This works on both Unix and Windows. Although package
   245  		// syscall is generally platform dependent, WaitStatus is
   246  		// defined for both Unix and Windows and in both cases has
   247  		// an ExitStatus() method with the same signature.
   248  		if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
   249  			exitStatus = status.ExitStatus()
   250  			switch exitStatus {
   251  			case 0:
   252  				err = nil
   253  			case int(syscall.ENOENT):
   254  				err = errtypes.NotFound(errBuf.String())
   255  			case int(syscall.EPERM), int(syscall.E2BIG), int(syscall.EINVAL):
   256  				// eos reports back error code 1 (EPERM) when ?
   257  				// eos reports back error code 7 (E2BIG) when the user is not allowed to read the directory
   258  				// eos reports back error code 22 (EINVAL) when the user is not allowed to enter the instance
   259  				err = errtypes.PermissionDenied(errBuf.String())
   260  			}
   261  		}
   262  	}
   263  
   264  	args := fmt.Sprintf("%s", cmd.Args)
   265  	env := fmt.Sprintf("%s", cmd.Env)
   266  	log.Info().Str("args", args).Str("env", env).Int("exit", exitStatus).Str("err", errBuf.String()).Msg("eos cmd")
   267  
   268  	if err != nil && exitStatus != int(syscall.ENOENT) { // don't wrap the errtypes.NotFoundError
   269  		err = errors.Wrap(err, "eosclient: error while executing command")
   270  	}
   271  
   272  	return outBuf.String(), errBuf.String(), err
   273  }
   274  
   275  // AddACL adds an new acl to EOS with the given aclType.
   276  func (c *Client) AddACL(ctx context.Context, auth, rootAuth eosclient.Authorization, path string, pos uint, a *acl.Entry) error {
   277  	finfo, err := c.getRawFileInfoByPath(ctx, auth, path)
   278  	if err != nil {
   279  		return err
   280  	}
   281  
   282  	if a.Type == acl.TypeLightweight {
   283  		sysACL := ""
   284  		aclStr, ok := finfo.Attrs["sys."+lwShareAttrKey]
   285  		if ok {
   286  			acls, err := acl.Parse(aclStr, acl.ShortTextForm)
   287  			if err != nil {
   288  				return err
   289  			}
   290  			err = acls.SetEntry(a.Type, a.Qualifier, a.Permissions)
   291  			if err != nil {
   292  				return err
   293  			}
   294  			sysACL = acls.Serialize()
   295  		} else {
   296  			sysACL = a.CitrineSerialize()
   297  		}
   298  		sysACLAttr := &eosclient.Attribute{
   299  			Type: eosclient.SystemAttr,
   300  			Key:  lwShareAttrKey,
   301  			Val:  sysACL,
   302  		}
   303  		return c.SetAttr(ctx, auth, sysACLAttr, false, finfo.IsDir, path)
   304  	}
   305  
   306  	sysACL := a.CitrineSerialize()
   307  	args := []string{"acl", "--sys"}
   308  	if finfo.IsDir {
   309  		args = append(args, "--recursive")
   310  	}
   311  
   312  	// set position of ACLs to add. The default is to append to the end, so no arguments will be added in this case
   313  	// the first position starts at 1 = eosclient.StartPosition
   314  	if pos != eosclient.EndPosition {
   315  		args = append(args, "--position", fmt.Sprint(pos))
   316  	}
   317  
   318  	args = append(args, sysACL, path)
   319  
   320  	_, _, err = c.executeEOS(ctx, args, rootAuth)
   321  	return err
   322  
   323  }
   324  
   325  // RemoveACL removes the acl from EOS.
   326  func (c *Client) RemoveACL(ctx context.Context, auth, rootAuth eosclient.Authorization, path string, a *acl.Entry) error {
   327  	finfo, err := c.getRawFileInfoByPath(ctx, auth, path)
   328  	if err != nil {
   329  		return err
   330  	}
   331  
   332  	if a.Type == acl.TypeLightweight {
   333  		sysACL := ""
   334  		aclStr, ok := finfo.Attrs["sys."+lwShareAttrKey]
   335  		if ok {
   336  			acls, err := acl.Parse(aclStr, acl.ShortTextForm)
   337  			if err != nil {
   338  				return err
   339  			}
   340  			acls.DeleteEntry(a.Type, a.Qualifier)
   341  			if err != nil {
   342  				return err
   343  			}
   344  			sysACL = acls.Serialize()
   345  		} else {
   346  			sysACL = a.CitrineSerialize()
   347  		}
   348  		sysACLAttr := &eosclient.Attribute{
   349  			Type: eosclient.SystemAttr,
   350  			Key:  lwShareAttrKey,
   351  			Val:  sysACL,
   352  		}
   353  		return c.SetAttr(ctx, auth, sysACLAttr, false, finfo.IsDir, path)
   354  	}
   355  
   356  	sysACL := a.CitrineSerialize()
   357  	args := []string{"acl", "--sys"}
   358  	if finfo.IsDir {
   359  		args = append(args, "--recursive")
   360  	}
   361  	args = append(args, sysACL, path)
   362  
   363  	_, _, err = c.executeEOS(ctx, args, rootAuth)
   364  	return err
   365  }
   366  
   367  // UpdateACL updates the EOS acl.
   368  func (c *Client) UpdateACL(ctx context.Context, auth, rootAuth eosclient.Authorization, path string, position uint, a *acl.Entry) error {
   369  	return c.AddACL(ctx, auth, rootAuth, path, position, a)
   370  }
   371  
   372  // GetACL for a file
   373  func (c *Client) GetACL(ctx context.Context, auth eosclient.Authorization, path, aclType, target string) (*acl.Entry, error) {
   374  	acls, err := c.ListACLs(ctx, auth, path)
   375  	if err != nil {
   376  		return nil, err
   377  	}
   378  	for _, a := range acls {
   379  		if a.Type == aclType && a.Qualifier == target {
   380  			return a, nil
   381  		}
   382  	}
   383  	return nil, errtypes.NotFound(fmt.Sprintf("%s:%s", aclType, target))
   384  
   385  }
   386  
   387  // ListACLs returns the list of ACLs present under the given path.
   388  // EOS returns uids/gid for Citrine version and usernames for older versions.
   389  // For Citire we need to convert back the uid back to username.
   390  func (c *Client) ListACLs(ctx context.Context, auth eosclient.Authorization, path string) ([]*acl.Entry, error) {
   391  
   392  	parsedACLs, err := c.getACLForPath(ctx, auth, path)
   393  	if err != nil {
   394  		return nil, err
   395  	}
   396  
   397  	// EOS Citrine ACLs are stored with uid. The UID will be resolved to the
   398  	// user opaque ID at the eosfs level.
   399  	return parsedACLs.Entries, nil
   400  }
   401  
   402  func (c *Client) getACLForPath(ctx context.Context, auth eosclient.Authorization, path string) (*acl.ACLs, error) {
   403  	finfo, err := c.GetFileInfoByPath(ctx, auth, path)
   404  	if err != nil {
   405  		return nil, err
   406  	}
   407  
   408  	return finfo.SysACL, nil
   409  }
   410  
   411  // GetFileInfoByInode returns the FileInfo by the given inode
   412  func (c *Client) GetFileInfoByInode(ctx context.Context, auth eosclient.Authorization, inode uint64) (*eosclient.FileInfo, error) {
   413  	args := []string{"file", "info", fmt.Sprintf("inode:%d", inode), "-m"}
   414  	stdout, _, err := c.executeEOS(ctx, args, auth)
   415  	if err != nil {
   416  		return nil, err
   417  	}
   418  	info, err := c.parseFileInfo(ctx, stdout, true)
   419  	if err != nil {
   420  		return nil, err
   421  	}
   422  
   423  	if c.opt.VersionInvariant && isVersionFolder(info.File) {
   424  		info, err = c.getFileInfoFromVersion(ctx, auth, info.File)
   425  		if err != nil {
   426  			return nil, err
   427  		}
   428  		info.Inode = inode
   429  	}
   430  
   431  	return c.mergeACLsAndAttrsForFiles(ctx, auth, info), nil
   432  }
   433  
   434  // GetFileInfoByFXID returns the FileInfo by the given file id in hexadecimal
   435  func (c *Client) GetFileInfoByFXID(ctx context.Context, auth eosclient.Authorization, fxid string) (*eosclient.FileInfo, error) {
   436  	args := []string{"file", "info", fmt.Sprintf("fxid:%s", fxid), "-m"}
   437  	stdout, _, err := c.executeEOS(ctx, args, auth)
   438  	if err != nil {
   439  		return nil, err
   440  	}
   441  
   442  	info, err := c.parseFileInfo(ctx, stdout, true)
   443  	if err != nil {
   444  		return nil, err
   445  	}
   446  
   447  	return c.mergeACLsAndAttrsForFiles(ctx, auth, info), nil
   448  }
   449  
   450  // GetFileInfoByPath returns the FilInfo at the given path
   451  func (c *Client) GetFileInfoByPath(ctx context.Context, auth eosclient.Authorization, path string) (*eosclient.FileInfo, error) {
   452  	args := []string{"file", "info", path, "-m"}
   453  	stdout, _, err := c.executeEOS(ctx, args, auth)
   454  	if err != nil {
   455  		return nil, err
   456  	}
   457  	info, err := c.parseFileInfo(ctx, stdout, true)
   458  	if err != nil {
   459  		return nil, err
   460  	}
   461  
   462  	if c.opt.VersionInvariant && !isVersionFolder(path) && !info.IsDir {
   463  		if inode, err := c.getVersionFolderInode(ctx, auth, path); err == nil {
   464  			info.Inode = inode
   465  		}
   466  	}
   467  
   468  	return c.mergeACLsAndAttrsForFiles(ctx, auth, info), nil
   469  }
   470  
   471  func (c *Client) getRawFileInfoByPath(ctx context.Context, auth eosclient.Authorization, path string) (*eosclient.FileInfo, error) {
   472  	args := []string{"file", "info", path, "-m"}
   473  	stdout, _, err := c.executeEOS(ctx, args, auth)
   474  	if err != nil {
   475  		return nil, err
   476  	}
   477  	return c.parseFileInfo(ctx, stdout, false)
   478  }
   479  
   480  func (c *Client) mergeACLsAndAttrsForFiles(ctx context.Context, auth eosclient.Authorization, info *eosclient.FileInfo) *eosclient.FileInfo {
   481  	// We need to inherit the ACLs for the parent directory as these are not available for files
   482  	// And the attributes from the version folders
   483  	if !info.IsDir {
   484  		parentInfo, err := c.getRawFileInfoByPath(ctx, auth, path.Dir(info.File))
   485  		// Even if this call fails, at least return the current file object
   486  		if err == nil {
   487  			info.SysACL.Entries = append(info.SysACL.Entries, parentInfo.SysACL.Entries...)
   488  		}
   489  
   490  		// We need to merge attrs set for the version folders, so get those resolved for the current user
   491  		versionFolderInfo, err := c.GetFileInfoByPath(ctx, auth, getVersionFolder(info.File))
   492  		if err == nil {
   493  			info.SysACL.Entries = append(info.SysACL.Entries, versionFolderInfo.SysACL.Entries...)
   494  			for k, v := range versionFolderInfo.Attrs {
   495  				info.Attrs[k] = v
   496  			}
   497  		}
   498  	}
   499  
   500  	return info
   501  }
   502  
   503  // SetAttr sets an extended attributes on a path.
   504  func (c *Client) SetAttr(ctx context.Context, auth eosclient.Authorization, attr *eosclient.Attribute, errorIfExists, recursive bool, path string) error {
   505  	if !isValidAttribute(attr) {
   506  		return errors.New("eos: attr is invalid: " + serializeAttribute(attr))
   507  	}
   508  
   509  	var info *eosclient.FileInfo
   510  	var err error
   511  	// We need to set the attrs on the version folder as they are not persisted across writes
   512  	// Except for the sys.eval.useracl attr as EOS uses that to determine if it needs to obey
   513  	// the user ACLs set on the file
   514  	if !(attr.Type == eosclient.SystemAttr && attr.Key == userACLEvalKey) {
   515  		info, err = c.getRawFileInfoByPath(ctx, auth, path)
   516  		if err != nil {
   517  			return err
   518  		}
   519  		if !info.IsDir {
   520  			path = getVersionFolder(path)
   521  		}
   522  	}
   523  
   524  	// Favorites need to be stored per user so handle these separately
   525  	if attr.Type == eosclient.UserAttr && attr.Key == favoritesKey {
   526  		return c.handleFavAttr(ctx, auth, attr, recursive, path, info, true)
   527  	}
   528  	return c.setEOSAttr(ctx, auth, attr, errorIfExists, recursive, path)
   529  }
   530  
   531  func (c *Client) setEOSAttr(ctx context.Context, auth eosclient.Authorization, attr *eosclient.Attribute, errorIfExists, recursive bool, path string) error {
   532  	args := []string{"attr"}
   533  	if recursive {
   534  		args = append(args, "-r")
   535  	}
   536  	args = append(args, "set")
   537  	if errorIfExists {
   538  		args = append(args, "-c")
   539  	}
   540  	args = append(args, serializeAttribute(attr), path)
   541  
   542  	_, _, err := c.executeEOS(ctx, args, auth)
   543  	if err != nil {
   544  		var exErr *exec.ExitError
   545  		if errors.As(err, &exErr) && exErr.ExitCode() == 17 {
   546  			return eosclient.AttrAlreadyExistsError
   547  		}
   548  		return err
   549  	}
   550  	return nil
   551  }
   552  
   553  func (c *Client) handleFavAttr(ctx context.Context, auth eosclient.Authorization, attr *eosclient.Attribute, recursive bool, path string, info *eosclient.FileInfo, set bool) error {
   554  	var err error
   555  	u := ctxpkg.ContextMustGetUser(ctx)
   556  	if info == nil {
   557  		info, err = c.getRawFileInfoByPath(ctx, auth, path)
   558  		if err != nil {
   559  			return err
   560  		}
   561  	}
   562  	favStr := info.Attrs[favoritesKey]
   563  	favs, err := acl.Parse(favStr, acl.ShortTextForm)
   564  	if err != nil {
   565  		return err
   566  	}
   567  	if set {
   568  		err = favs.SetEntry(acl.TypeUser, u.Id.OpaqueId, "1")
   569  		if err != nil {
   570  			return err
   571  		}
   572  	} else {
   573  		favs.DeleteEntry(acl.TypeUser, u.Id.OpaqueId)
   574  	}
   575  	attr.Val = favs.Serialize()
   576  	return c.setEOSAttr(ctx, auth, attr, false, recursive, path)
   577  }
   578  
   579  // UnsetAttr unsets an extended attribute on a path.
   580  func (c *Client) UnsetAttr(ctx context.Context, auth eosclient.Authorization, attr *eosclient.Attribute, recursive bool, path string) error {
   581  	if !isValidAttribute(attr) {
   582  		return errors.New("eos: attr is invalid: " + serializeAttribute(attr))
   583  	}
   584  
   585  	var info *eosclient.FileInfo
   586  	var err error
   587  	// We need to set the attrs on the version folder as they are not persisted across writes
   588  	// Except for the sys.eval.useracl attr as EOS uses that to determine if it needs to obey
   589  	// the user ACLs set on the file
   590  	if !(attr.Type == eosclient.SystemAttr && attr.Key == userACLEvalKey) {
   591  		info, err = c.getRawFileInfoByPath(ctx, auth, path)
   592  		if err != nil {
   593  			return err
   594  		}
   595  		if !info.IsDir {
   596  			path = getVersionFolder(path)
   597  		}
   598  	}
   599  
   600  	// Favorites need to be stored per user so handle these separately
   601  	if attr.Type == eosclient.UserAttr && attr.Key == favoritesKey {
   602  		return c.handleFavAttr(ctx, auth, attr, recursive, path, info, false)
   603  	}
   604  
   605  	var args []string
   606  	if recursive {
   607  		args = []string{"attr", "-r", "rm", fmt.Sprintf("%s.%s", attrTypeToString(attr.Type), attr.Key), path}
   608  	} else {
   609  		args = []string{"attr", "rm", fmt.Sprintf("%s.%s", attrTypeToString(attr.Type), attr.Key), path}
   610  	}
   611  	_, _, err = c.executeEOS(ctx, args, auth)
   612  	if err != nil {
   613  		var exErr *exec.ExitError
   614  		if errors.As(err, &exErr) && exErr.ExitCode() == 61 {
   615  			return eosclient.AttrNotExistsError
   616  		}
   617  		return err
   618  	}
   619  	return nil
   620  }
   621  
   622  // GetAttr returns the attribute specified by key
   623  func (c *Client) GetAttr(ctx context.Context, auth eosclient.Authorization, key, path string) (*eosclient.Attribute, error) {
   624  
   625  	// As SetAttr set the attr on the version folder, we will read the attribute on it
   626  	// if the resource is not a folder
   627  	info, err := c.getRawFileInfoByPath(ctx, auth, path)
   628  	if err != nil {
   629  		return nil, err
   630  	}
   631  	if !info.IsDir {
   632  		path = getVersionFolder(path)
   633  	}
   634  
   635  	args := []string{"attr", "get", key, path}
   636  	attrOut, _, err := c.executeEOS(ctx, args, auth)
   637  	if err != nil {
   638  		return nil, err
   639  	}
   640  	attr, err := deserializeAttribute(attrOut)
   641  	if err != nil {
   642  		return nil, err
   643  	}
   644  	return attr, nil
   645  }
   646  
   647  func deserializeAttribute(attrStr string) (*eosclient.Attribute, error) {
   648  	// the string is in the form sys.forced.checksum="adler"
   649  	keyValue := strings.SplitN(strings.TrimSpace(attrStr), "=", 2) // keyValue = ["sys.forced.checksum", "\"adler\""]
   650  	if len(keyValue) != 2 {
   651  		return nil, errtypes.InternalError("wrong attr format to deserialize")
   652  	}
   653  	type2key := strings.SplitN(keyValue[0], ".", 2) // type2key = ["sys", "forced.checksum"]
   654  	if len(type2key) != 2 {
   655  		return nil, errtypes.InternalError("wrong attr format to deserialize")
   656  	}
   657  	t, err := eosclient.AttrStringToType(type2key[0])
   658  	if err != nil {
   659  		return nil, err
   660  	}
   661  	// trim \" from value
   662  	value := strings.Trim(keyValue[1], "\"")
   663  	return &eosclient.Attribute{Type: t, Key: type2key[1], Val: value}, nil
   664  }
   665  
   666  // GetQuota gets the quota of a user on the quota node defined by path
   667  func (c *Client) GetQuota(ctx context.Context, username string, rootAuth eosclient.Authorization, path string) (*eosclient.QuotaInfo, error) {
   668  	args := []string{"quota", "ls", "-u", username, "-m"}
   669  	stdout, _, err := c.executeEOS(ctx, args, rootAuth)
   670  	if err != nil {
   671  		return nil, err
   672  	}
   673  	return c.parseQuota(path, stdout)
   674  }
   675  
   676  // SetQuota sets the quota of a user on the quota node defined by path
   677  func (c *Client) SetQuota(ctx context.Context, rootAuth eosclient.Authorization, info *eosclient.SetQuotaInfo) error {
   678  	maxBytes := fmt.Sprintf("%d", info.MaxBytes)
   679  	maxFiles := fmt.Sprintf("%d", info.MaxFiles)
   680  	args := []string{"quota", "set", "-u", info.Username, "-p", info.QuotaNode, "-v", maxBytes, "-i", maxFiles}
   681  	_, _, err := c.executeEOS(ctx, args, rootAuth)
   682  	if err != nil {
   683  		return err
   684  	}
   685  	return nil
   686  }
   687  
   688  // Touch creates a 0-size,0-replica file in the EOS namespace.
   689  func (c *Client) Touch(ctx context.Context, auth eosclient.Authorization, path string) error {
   690  	args := []string{"file", "touch", path}
   691  	_, _, err := c.executeEOS(ctx, args, auth)
   692  	return err
   693  }
   694  
   695  // Chown given path
   696  func (c *Client) Chown(ctx context.Context, auth, chownauth eosclient.Authorization, path string) error {
   697  	args := []string{"chown", chownauth.Role.UID + ":" + chownauth.Role.GID, path}
   698  	_, _, err := c.executeEOS(ctx, args, auth)
   699  	return err
   700  }
   701  
   702  // Chmod given path
   703  func (c *Client) Chmod(ctx context.Context, auth eosclient.Authorization, mode, path string) error {
   704  	args := []string{"chmod", mode, path}
   705  	_, _, err := c.executeEOS(ctx, args, auth)
   706  	return err
   707  }
   708  
   709  // CreateDir creates a directory at the given path
   710  func (c *Client) CreateDir(ctx context.Context, auth eosclient.Authorization, path string) error {
   711  	args := []string{"mkdir", "-p", path}
   712  	_, _, err := c.executeEOS(ctx, args, auth)
   713  	return err
   714  }
   715  
   716  // Remove removes the resource at the given path
   717  func (c *Client) Remove(ctx context.Context, auth eosclient.Authorization, path string, noRecycle bool) error {
   718  	args := []string{"rm", "-r"}
   719  	if noRecycle {
   720  		args = append(args, "--no-recycle-bin") // do not put the file in the recycle bin
   721  	}
   722  	args = append(args, path)
   723  	_, _, err := c.executeEOS(ctx, args, auth)
   724  	return err
   725  }
   726  
   727  // Rename renames the resource referenced by oldPath to newPath
   728  func (c *Client) Rename(ctx context.Context, auth eosclient.Authorization, oldPath, newPath string) error {
   729  	args := []string{"file", "rename", oldPath, newPath}
   730  	_, _, err := c.executeEOS(ctx, args, auth)
   731  	return err
   732  }
   733  
   734  // List the contents of the directory given by path
   735  func (c *Client) List(ctx context.Context, auth eosclient.Authorization, path string) ([]*eosclient.FileInfo, error) {
   736  	args := []string{"find", "--fileinfo", "--maxdepth", "1", path}
   737  	stdout, _, err := c.executeEOS(ctx, args, auth)
   738  	if err != nil {
   739  		return nil, errors.Wrapf(err, "eosclient: error listing fn=%s", path)
   740  	}
   741  	return c.parseFind(ctx, auth, path, stdout)
   742  }
   743  
   744  // Read reads a file from the mgm
   745  func (c *Client) Read(ctx context.Context, auth eosclient.Authorization, path string) (io.ReadCloser, error) {
   746  	rand := "eosread-" + uuid.New().String()
   747  	localTarget := fmt.Sprintf("%s/%s", c.opt.CacheDirectory, rand)
   748  	defer os.RemoveAll(localTarget)
   749  
   750  	xrdPath := fmt.Sprintf("%s//%s", c.opt.URL, path)
   751  	args := []string{"--nopbar", "--silent", "-f", xrdPath, localTarget}
   752  
   753  	if auth.Token != "" {
   754  		args[3] += "?authz=" + auth.Token
   755  	} else if auth.Role.UID != "" && auth.Role.GID != "" {
   756  		args = append(args, fmt.Sprintf("-OSeos.ruid=%s&eos.rgid=%s", auth.Role.UID, auth.Role.GID))
   757  	}
   758  
   759  	_, _, err := c.executeXRDCopy(ctx, args)
   760  	if err != nil {
   761  		return nil, err
   762  	}
   763  	return os.Open(localTarget)
   764  }
   765  
   766  // Write writes a stream to the mgm
   767  func (c *Client) Write(ctx context.Context, auth eosclient.Authorization, path string, stream io.ReadCloser) error {
   768  	fd, err := os.CreateTemp(c.opt.CacheDirectory, "eoswrite-")
   769  	if err != nil {
   770  		return err
   771  	}
   772  	defer fd.Close()
   773  	defer os.RemoveAll(fd.Name())
   774  
   775  	// copy stream to local temp file
   776  	_, err = io.Copy(fd, stream)
   777  	if err != nil {
   778  		return err
   779  	}
   780  
   781  	return c.WriteFile(ctx, auth, path, fd.Name())
   782  }
   783  
   784  // WriteFile writes an existing file to the mgm
   785  func (c *Client) WriteFile(ctx context.Context, auth eosclient.Authorization, path, source string) error {
   786  	xrdPath := fmt.Sprintf("%s//%s", c.opt.URL, path)
   787  	args := []string{"--nopbar", "--silent", "-f", source, xrdPath}
   788  
   789  	if auth.Token != "" {
   790  		args[4] += "?authz=" + auth.Token
   791  	} else if auth.Role.UID != "" && auth.Role.GID != "" {
   792  		args = append(args, fmt.Sprintf("-ODeos.ruid=%s&eos.rgid=%s", auth.Role.UID, auth.Role.GID))
   793  	}
   794  
   795  	_, _, err := c.executeXRDCopy(ctx, args)
   796  	return err
   797  }
   798  
   799  // ListDeletedEntries returns a list of the deleted entries.
   800  func (c *Client) ListDeletedEntries(ctx context.Context, auth eosclient.Authorization) ([]*eosclient.DeletedEntry, error) {
   801  	// TODO(labkode): add protection if slave is configured and alive to count how many files are in the trashbin before
   802  	// triggering the recycle ls call that could break the instance because of unavailable memory.
   803  	args := []string{"recycle", "ls", "-m"}
   804  	stdout, _, err := c.executeEOS(ctx, args, auth)
   805  	if err != nil {
   806  		return nil, err
   807  	}
   808  	return parseRecycleList(stdout)
   809  }
   810  
   811  // RestoreDeletedEntry restores a deleted entry.
   812  func (c *Client) RestoreDeletedEntry(ctx context.Context, auth eosclient.Authorization, key string) error {
   813  	args := []string{"recycle", "restore", key}
   814  	_, _, err := c.executeEOS(ctx, args, auth)
   815  	return err
   816  }
   817  
   818  // PurgeDeletedEntries purges all entries from the recycle bin.
   819  func (c *Client) PurgeDeletedEntries(ctx context.Context, auth eosclient.Authorization) error {
   820  	args := []string{"recycle", "purge"}
   821  	_, _, err := c.executeEOS(ctx, args, auth)
   822  	return err
   823  }
   824  
   825  // ListVersions list all the versions for a given file.
   826  func (c *Client) ListVersions(ctx context.Context, auth eosclient.Authorization, p string) ([]*eosclient.FileInfo, error) {
   827  	versionFolder := getVersionFolder(p)
   828  	finfos, err := c.List(ctx, auth, versionFolder)
   829  	if err != nil {
   830  		// we send back an empty list
   831  		return []*eosclient.FileInfo{}, nil
   832  	}
   833  	return finfos, nil
   834  }
   835  
   836  // RollbackToVersion rollbacks a file to a previous version.
   837  func (c *Client) RollbackToVersion(ctx context.Context, auth eosclient.Authorization, path, version string) error {
   838  	args := []string{"file", "versions", path, version}
   839  	_, _, err := c.executeEOS(ctx, args, auth)
   840  	return err
   841  }
   842  
   843  // ReadVersion reads the version for the given file.
   844  func (c *Client) ReadVersion(ctx context.Context, auth eosclient.Authorization, p, version string) (io.ReadCloser, error) {
   845  	versionFile := path.Join(getVersionFolder(p), version)
   846  	return c.Read(ctx, auth, versionFile)
   847  }
   848  
   849  // GenerateToken returns a token on behalf of the resource owner to be used by lightweight accounts
   850  func (c *Client) GenerateToken(ctx context.Context, auth eosclient.Authorization, p string, a *acl.Entry) (string, error) {
   851  	expiration := strconv.FormatInt(time.Now().Add(time.Duration(c.opt.TokenExpiry)*time.Second).Unix(), 10)
   852  	args := []string{"token", "--permission", a.Permissions, "--tree", "--path", p, "--expires", expiration}
   853  	stdout, _, err := c.executeEOS(ctx, args, auth)
   854  	return stdout, err
   855  }
   856  
   857  func (c *Client) getVersionFolderInode(ctx context.Context, auth eosclient.Authorization, p string) (uint64, error) {
   858  	versionFolder := getVersionFolder(p)
   859  	md, err := c.getRawFileInfoByPath(ctx, auth, versionFolder)
   860  	if err != nil {
   861  		if err = c.CreateDir(ctx, auth, versionFolder); err != nil {
   862  			return 0, err
   863  		}
   864  		md, err = c.getRawFileInfoByPath(ctx, auth, versionFolder)
   865  		if err != nil {
   866  			return 0, err
   867  		}
   868  	}
   869  	return md.Inode, nil
   870  }
   871  
   872  func (c *Client) getFileInfoFromVersion(ctx context.Context, auth eosclient.Authorization, p string) (*eosclient.FileInfo, error) {
   873  	file := getFileFromVersionFolder(p)
   874  	md, err := c.GetFileInfoByPath(ctx, auth, file)
   875  	if err != nil {
   876  		return nil, err
   877  	}
   878  	return md, nil
   879  }
   880  
   881  func isVersionFolder(p string) bool {
   882  	return strings.HasPrefix(path.Base(p), versionPrefix)
   883  }
   884  
   885  func getVersionFolder(p string) string {
   886  	return path.Join(path.Dir(p), versionPrefix+path.Base(p))
   887  }
   888  
   889  func getFileFromVersionFolder(p string) string {
   890  	return path.Join(path.Dir(p), strings.TrimPrefix(path.Base(p), versionPrefix))
   891  }
   892  
   893  func parseRecycleList(raw string) ([]*eosclient.DeletedEntry, error) {
   894  	entries := []*eosclient.DeletedEntry{}
   895  	rawLines := strings.FieldsFunc(raw, func(c rune) bool {
   896  		return c == '\n'
   897  	})
   898  	for _, rl := range rawLines {
   899  		if rl == "" {
   900  			continue
   901  		}
   902  		entry, err := parseRecycleEntry(rl)
   903  		if err != nil {
   904  			return nil, err
   905  		}
   906  		entries = append(entries, entry)
   907  	}
   908  	return entries, nil
   909  }
   910  
   911  // parse entries like these:
   912  // recycle=ls recycle-bin=/eos/backup/proc/recycle/ uid=gonzalhu gid=it size=0 deletion-time=1510823151 type=recursive-dir keylength.restore-path=45 restore-path=/eos/scratch/user/g/gonzalhu/.sys.v#.app.ico/ restore-key=0000000000a35100
   913  // recycle=ls recycle-bin=/eos/backup/proc/recycle/ uid=gonzalhu gid=it size=381038 deletion-time=1510823151 type=file keylength.restore-path=36 restore-path=/eos/scratch/user/g/gonzalhu/app.ico restore-key=000000002544fdb3
   914  func parseRecycleEntry(raw string) (*eosclient.DeletedEntry, error) {
   915  	partsBySpace := strings.FieldsFunc(raw, func(c rune) bool {
   916  		return c == ' '
   917  	})
   918  	restoreKeyPair, partsBySpace := partsBySpace[len(partsBySpace)-1], partsBySpace[:len(partsBySpace)-1]
   919  	restorePathPair := strings.Join(partsBySpace[8:], " ")
   920  
   921  	partsBySpace = partsBySpace[:8]
   922  	partsBySpace = append(partsBySpace, restorePathPair)
   923  	partsBySpace = append(partsBySpace, restoreKeyPair)
   924  
   925  	kv := getMap(partsBySpace)
   926  	size, err := strconv.ParseUint(kv["size"], 10, 64)
   927  	if err != nil {
   928  		return nil, err
   929  	}
   930  	isDir := false
   931  	if kv["type"] == "recursive-dir" {
   932  		isDir = true
   933  	}
   934  	deletionMTime, err := strconv.ParseUint(strings.Split(kv["deletion-time"], ".")[0], 10, 64)
   935  	if err != nil {
   936  		return nil, err
   937  	}
   938  	entry := &eosclient.DeletedEntry{
   939  		RestorePath:   kv["restore-path"],
   940  		RestoreKey:    kv["restore-key"],
   941  		Size:          size,
   942  		DeletionMTime: deletionMTime,
   943  		IsDir:         isDir,
   944  	}
   945  	return entry, nil
   946  }
   947  
   948  func getMap(partsBySpace []string) map[string]string {
   949  	kv := map[string]string{}
   950  	for _, pair := range partsBySpace {
   951  		parts := strings.Split(pair, "=")
   952  		if len(parts) > 1 {
   953  			kv[parts[0]] = parts[1]
   954  		}
   955  
   956  	}
   957  	return kv
   958  }
   959  
   960  func (c *Client) parseFind(ctx context.Context, auth eosclient.Authorization, dirPath, raw string) ([]*eosclient.FileInfo, error) {
   961  	finfos := []*eosclient.FileInfo{}
   962  	versionFolders := map[string]*eosclient.FileInfo{}
   963  	rawLines := strings.FieldsFunc(raw, func(c rune) bool {
   964  		return c == '\n'
   965  	})
   966  
   967  	var parent *eosclient.FileInfo
   968  	for _, rl := range rawLines {
   969  		if rl == "" {
   970  			continue
   971  		}
   972  		fi, err := c.parseFileInfo(ctx, rl, true)
   973  		if err != nil {
   974  			return nil, err
   975  		}
   976  		// dirs in eos end with a slash, like /eos/user/g/gonzalhu/
   977  		// we skip the current directory as eos find will return the directory we
   978  		// ask to find
   979  		if fi.File == path.Clean(dirPath) {
   980  			parent = fi
   981  			continue
   982  		}
   983  
   984  		// If it's a version folder, store it in a map, so that for the corresponding file,
   985  		// we can return its inode instead
   986  		if isVersionFolder(fi.File) {
   987  			versionFolders[fi.File] = fi
   988  		}
   989  
   990  		finfos = append(finfos, fi)
   991  	}
   992  
   993  	for _, fi := range finfos {
   994  		// For files, inherit ACLs from the parent
   995  		// And set the inode to that of their version folder
   996  		if !fi.IsDir && !isVersionFolder(dirPath) {
   997  			if parent != nil {
   998  				fi.SysACL.Entries = append(fi.SysACL.Entries, parent.SysACL.Entries...)
   999  			}
  1000  			versionFolderPath := getVersionFolder(fi.File)
  1001  			if vf, ok := versionFolders[versionFolderPath]; ok {
  1002  				fi.Inode = vf.Inode
  1003  				fi.SysACL.Entries = append(fi.SysACL.Entries, vf.SysACL.Entries...)
  1004  				for k, v := range vf.Attrs {
  1005  					fi.Attrs[k] = v
  1006  				}
  1007  
  1008  			} else if err := c.CreateDir(ctx, auth, versionFolderPath); err == nil { // Create the version folder if it doesn't exist
  1009  				if md, err := c.getRawFileInfoByPath(ctx, auth, versionFolderPath); err == nil {
  1010  					fi.Inode = md.Inode
  1011  				}
  1012  			}
  1013  		}
  1014  	}
  1015  
  1016  	return finfos, nil
  1017  }
  1018  
  1019  func (c Client) parseQuotaLine(line string) map[string]string {
  1020  	partsBySpace := strings.FieldsFunc(line, func(c rune) bool {
  1021  		return c == ' '
  1022  	})
  1023  	m := getMap(partsBySpace)
  1024  	return m
  1025  }
  1026  func (c *Client) parseQuota(path, raw string) (*eosclient.QuotaInfo, error) {
  1027  	rawLines := strings.FieldsFunc(raw, func(c rune) bool {
  1028  		return c == '\n'
  1029  	})
  1030  	for _, rl := range rawLines {
  1031  		if rl == "" {
  1032  			continue
  1033  		}
  1034  
  1035  		m := c.parseQuotaLine(rl)
  1036  		// map[maxbytes:2000000000000 maxlogicalbytes:1000000000000 percentageusedbytes:0.49 quota:node uid:gonzalhu space:/eos/scratch/user/ usedbytes:9829986500 usedlogicalbytes:4914993250 statusfiles:ok usedfiles:334 maxfiles:1000000 statusbytes:ok]
  1037  
  1038  		space := m["space"]
  1039  		if strings.HasPrefix(path, filepath.Clean(space)) {
  1040  			maxBytesString := m["maxlogicalbytes"]
  1041  			usedBytesString := m["usedlogicalbytes"]
  1042  			maxBytes, _ := strconv.ParseUint(maxBytesString, 10, 64)
  1043  			usedBytes, _ := strconv.ParseUint(usedBytesString, 10, 64)
  1044  
  1045  			maxInodesString := m["maxfiles"]
  1046  			usedInodesString := m["usedfiles"]
  1047  			maxInodes, _ := strconv.ParseUint(maxInodesString, 10, 64)
  1048  			usedInodes, _ := strconv.ParseUint(usedInodesString, 10, 64)
  1049  
  1050  			qi := &eosclient.QuotaInfo{
  1051  				AvailableBytes:  maxBytes,
  1052  				UsedBytes:       usedBytes,
  1053  				AvailableInodes: maxInodes,
  1054  				UsedInodes:      usedInodes,
  1055  			}
  1056  			return qi, nil
  1057  		}
  1058  	}
  1059  	return &eosclient.QuotaInfo{}, nil
  1060  }
  1061  
  1062  // TODO(labkode): better API to access extended attributes.
  1063  func (c *Client) parseFileInfo(ctx context.Context, raw string, parseFavoriteKey bool) (*eosclient.FileInfo, error) {
  1064  
  1065  	line := raw[15:]
  1066  	index := strings.Index(line, " file=/")
  1067  	lengthString := line[0:index]
  1068  	length, err := strconv.ParseUint(lengthString, 10, 64)
  1069  	if err != nil {
  1070  		return nil, err
  1071  	}
  1072  
  1073  	line = line[index+6:] // skip ' file='
  1074  	name := line[0:length]
  1075  
  1076  	kv := make(map[string]string)
  1077  	attrs := make(map[string]string)
  1078  	// strip trailing slash
  1079  	kv["file"] = strings.TrimSuffix(name, "/")
  1080  
  1081  	line = line[length+1:]
  1082  	partsBySpace := strings.FieldsFunc(line, func(c rune) bool { // we have [size=45 container=3 ...}
  1083  		return c == ' '
  1084  	})
  1085  	var previousXAttr = ""
  1086  	for _, p := range partsBySpace {
  1087  		partsByEqual := strings.SplitN(p, "=", 2) // we have kv pairs like [size 14]
  1088  		if len(partsByEqual) == 2 {
  1089  			// handle xattrn and xattrv special cases
  1090  			switch {
  1091  			case partsByEqual[0] == "xattrn":
  1092  				previousXAttr = partsByEqual[1]
  1093  				if previousXAttr != "user.acl" {
  1094  					previousXAttr = strings.Replace(previousXAttr, "user.", "", 1)
  1095  				}
  1096  			case partsByEqual[0] == "xattrv":
  1097  				attrs[previousXAttr] = partsByEqual[1]
  1098  				previousXAttr = ""
  1099  			default:
  1100  				kv[partsByEqual[0]] = partsByEqual[1]
  1101  
  1102  			}
  1103  		}
  1104  	}
  1105  	fi, err := c.mapToFileInfo(ctx, kv, attrs, parseFavoriteKey)
  1106  	if err != nil {
  1107  		return nil, err
  1108  	}
  1109  	return fi, nil
  1110  }
  1111  
  1112  // mapToFileInfo converts the dictionary to an usable structure.
  1113  // The kv has format:
  1114  // map[sys.forced.space:default files:0 mode:42555 ino:5 sys.forced.blocksize:4k sys.forced.layout:replica uid:0 fid:5 sys.forced.blockchecksum:crc32c sys.recycle:/eos/backup/proc/recycle/ fxid:00000005 pid:1 etag:5:0.000 keylength.file:4 file:/eos treesize:1931593933849913 container:3 gid:0 mtime:1498571294.108614409 ctime:1460121992.294326762 pxid:00000001 sys.forced.checksum:adler sys.forced.nstripes:2]
  1115  func (c *Client) mapToFileInfo(ctx context.Context, kv, attrs map[string]string, parseFavoriteKey bool) (*eosclient.FileInfo, error) {
  1116  	inode, err := strconv.ParseUint(kv["ino"], 10, 64)
  1117  	if err != nil {
  1118  		return nil, err
  1119  	}
  1120  	fid, err := strconv.ParseUint(kv["fid"], 10, 64)
  1121  	if err != nil {
  1122  		return nil, err
  1123  	}
  1124  	uid, err := strconv.ParseUint(kv["uid"], 10, 64)
  1125  	if err != nil {
  1126  		return nil, err
  1127  	}
  1128  	gid, err := strconv.ParseUint(kv["gid"], 10, 64)
  1129  	if err != nil {
  1130  		return nil, err
  1131  	}
  1132  
  1133  	var treeSize uint64
  1134  	// treeSize is only for containers, so we check
  1135  	if val, ok := kv["treesize"]; ok {
  1136  		treeSize, err = strconv.ParseUint(val, 10, 64)
  1137  		if err != nil {
  1138  			return nil, err
  1139  		}
  1140  	}
  1141  	var fileCounter uint64
  1142  	// fileCounter is only for containers
  1143  	if val, ok := kv["files"]; ok {
  1144  		fileCounter, err = strconv.ParseUint(val, 10, 64)
  1145  		if err != nil {
  1146  			return nil, err
  1147  		}
  1148  	}
  1149  	var dirCounter uint64
  1150  	// dirCounter is only for containers
  1151  	if val, ok := kv["container"]; ok {
  1152  		dirCounter, err = strconv.ParseUint(val, 10, 64)
  1153  		if err != nil {
  1154  			return nil, err
  1155  		}
  1156  	}
  1157  
  1158  	// treeCount is the number of entries under the tree
  1159  	treeCount := fileCounter + dirCounter
  1160  
  1161  	var size uint64
  1162  	if val, ok := kv["size"]; ok {
  1163  		size, err = strconv.ParseUint(val, 10, 64)
  1164  		if err != nil {
  1165  			return nil, err
  1166  		}
  1167  	}
  1168  
  1169  	// look for the stime first as mtime is not updated for parent dirs; if that isn't set, we use mtime
  1170  	var mtimesec, mtimenanos uint64
  1171  	var mtimeSet bool
  1172  	if val, ok := kv["stime"]; ok && val != "" {
  1173  		stimeSplit := strings.Split(val, ".")
  1174  		if mtimesec, err = strconv.ParseUint(stimeSplit[0], 10, 64); err == nil {
  1175  			mtimeSet = true
  1176  		}
  1177  		if mtimenanos, err = strconv.ParseUint(stimeSplit[1], 10, 32); err != nil {
  1178  			mtimeSet = false
  1179  		}
  1180  	}
  1181  	if !mtimeSet {
  1182  		mtimeSplit := strings.Split(kv["mtime"], ".")
  1183  		if mtimesec, err = strconv.ParseUint(mtimeSplit[0], 10, 64); err != nil {
  1184  			return nil, err
  1185  		}
  1186  		if mtimenanos, err = strconv.ParseUint(mtimeSplit[1], 10, 32); err != nil {
  1187  			return nil, err
  1188  		}
  1189  	}
  1190  
  1191  	isDir := false
  1192  	var xs *eosclient.Checksum
  1193  	if _, ok := kv["files"]; ok {
  1194  		isDir = true
  1195  	} else {
  1196  		xs = &eosclient.Checksum{
  1197  			XSSum:  kv["xs"],
  1198  			XSType: kv["xstype"],
  1199  		}
  1200  	}
  1201  
  1202  	sysACL, err := acl.Parse(attrs["sys.acl"], acl.ShortTextForm)
  1203  	if err != nil {
  1204  		return nil, err
  1205  	}
  1206  
  1207  	// Read lightweight ACLs recognized by the sys.reva.lwshare attr
  1208  	if lwACLStr, ok := attrs["sys."+lwShareAttrKey]; ok {
  1209  		lwAcls, err := acl.Parse(lwACLStr, acl.ShortTextForm)
  1210  		if err != nil {
  1211  			return nil, err
  1212  		}
  1213  		for _, e := range lwAcls.Entries {
  1214  			err = sysACL.SetEntry(e.Type, e.Qualifier, e.Permissions)
  1215  			if err != nil {
  1216  				return nil, err
  1217  			}
  1218  		}
  1219  	}
  1220  
  1221  	// Read the favorite attr
  1222  	if parseFavoriteKey {
  1223  		parseAndSetFavoriteAttr(ctx, attrs)
  1224  	}
  1225  
  1226  	fi := &eosclient.FileInfo{
  1227  		File:       kv["file"],
  1228  		Inode:      inode,
  1229  		FID:        fid,
  1230  		UID:        uid,
  1231  		GID:        gid,
  1232  		ETag:       kv["etag"],
  1233  		Size:       size,
  1234  		TreeSize:   treeSize,
  1235  		MTimeSec:   mtimesec,
  1236  		MTimeNanos: uint32(mtimenanos),
  1237  		IsDir:      isDir,
  1238  		Instance:   c.opt.URL,
  1239  		SysACL:     sysACL,
  1240  		TreeCount:  treeCount,
  1241  		Attrs:      attrs,
  1242  		XS:         xs,
  1243  	}
  1244  
  1245  	return fi, nil
  1246  }
  1247  
  1248  func parseAndSetFavoriteAttr(ctx context.Context, attrs map[string]string) {
  1249  	// Read and correctly set the favorite attr
  1250  	if user, ok := ctxpkg.ContextGetUser(ctx); ok {
  1251  		if favAttrStr, ok := attrs[favoritesKey]; ok {
  1252  			favUsers, err := acl.Parse(favAttrStr, acl.ShortTextForm)
  1253  			if err != nil {
  1254  				return
  1255  			}
  1256  			for _, u := range favUsers.Entries {
  1257  				// Check if the current user has favorited this resource
  1258  				if u.Qualifier == user.Id.OpaqueId {
  1259  					// Set attr val to 1
  1260  					attrs[favoritesKey] = "1"
  1261  					return
  1262  				}
  1263  			}
  1264  		}
  1265  	}
  1266  
  1267  	// Delete the favorite attr from the response
  1268  	delete(attrs, favoritesKey)
  1269  }