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 }