github.com/cs3org/reva/v2@v2.27.7/pkg/publicshare/manager/json/json.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 json 20 21 import ( 22 "context" 23 "encoding/json" 24 "fmt" 25 "os" 26 "os/signal" 27 "strconv" 28 "strings" 29 "sync" 30 "syscall" 31 "time" 32 33 "github.com/rs/zerolog/log" 34 "golang.org/x/crypto/bcrypt" 35 "google.golang.org/protobuf/proto" 36 37 user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" 38 rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" 39 link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" 40 provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" 41 typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" 42 "github.com/cs3org/reva/v2/pkg/appctx" 43 "github.com/cs3org/reva/v2/pkg/errtypes" 44 "github.com/cs3org/reva/v2/pkg/publicshare" 45 "github.com/cs3org/reva/v2/pkg/publicshare/manager/json/persistence" 46 "github.com/cs3org/reva/v2/pkg/publicshare/manager/json/persistence/cs3" 47 "github.com/cs3org/reva/v2/pkg/publicshare/manager/json/persistence/file" 48 "github.com/cs3org/reva/v2/pkg/publicshare/manager/json/persistence/memory" 49 "github.com/cs3org/reva/v2/pkg/publicshare/manager/registry" 50 "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" 51 "github.com/cs3org/reva/v2/pkg/storage/utils/metadata" 52 "github.com/cs3org/reva/v2/pkg/utils" 53 "github.com/mitchellh/mapstructure" 54 "github.com/pkg/errors" 55 ) 56 57 func init() { 58 registry.Register("json", NewFile) 59 registry.Register("jsoncs3", NewCS3) 60 registry.Register("jsonmemory", NewMemory) 61 } 62 63 // NewFile returns a new filesystem public shares manager. 64 func NewFile(c map[string]interface{}) (publicshare.Manager, error) { 65 conf := &fileConfig{} 66 if err := mapstructure.Decode(c, conf); err != nil { 67 return nil, err 68 } 69 70 conf.init() 71 if conf.File == "" { 72 conf.File = "/var/tmp/reva/publicshares" 73 } 74 75 p := file.New(conf.File) 76 return New(conf.GatewayAddr, conf.SharePasswordHashCost, conf.JanitorRunInterval, conf.EnableExpiredSharesCleanup, p) 77 } 78 79 // NewMemory returns a new in-memory public shares manager. 80 func NewMemory(c map[string]interface{}) (publicshare.Manager, error) { 81 conf := &commonConfig{} 82 if err := mapstructure.Decode(c, conf); err != nil { 83 return nil, err 84 } 85 86 conf.init() 87 p := memory.New() 88 89 return New(conf.GatewayAddr, conf.SharePasswordHashCost, conf.JanitorRunInterval, conf.EnableExpiredSharesCleanup, p) 90 } 91 92 // NewCS3 returns a new cs3 public shares manager. 93 func NewCS3(c map[string]interface{}) (publicshare.Manager, error) { 94 conf := &cs3Config{} 95 if err := mapstructure.Decode(c, conf); err != nil { 96 return nil, err 97 } 98 99 conf.init() 100 101 s, err := metadata.NewCS3Storage(conf.ProviderAddr, conf.ProviderAddr, conf.ServiceUserID, conf.ServiceUserIdp, conf.MachineAuthAPIKey) 102 if err != nil { 103 return nil, err 104 } 105 p := cs3.New(s) 106 107 return New(conf.GatewayAddr, conf.SharePasswordHashCost, conf.JanitorRunInterval, conf.EnableExpiredSharesCleanup, p) 108 } 109 110 // New returns a new public share manager instance 111 func New(gwAddr string, pwHashCost, janitorRunInterval int, enableCleanup bool, p persistence.Persistence) (publicshare.Manager, error) { 112 m := &manager{ 113 gatewayAddr: gwAddr, 114 mutex: &sync.Mutex{}, 115 passwordHashCost: pwHashCost, 116 janitorRunInterval: janitorRunInterval, 117 enableExpiredSharesCleanup: enableCleanup, 118 persistence: p, 119 } 120 121 go m.startJanitorRun() 122 return m, nil 123 } 124 125 type commonConfig struct { 126 GatewayAddr string `mapstructure:"gateway_addr"` 127 SharePasswordHashCost int `mapstructure:"password_hash_cost"` 128 JanitorRunInterval int `mapstructure:"janitor_run_interval"` 129 EnableExpiredSharesCleanup bool `mapstructure:"enable_expired_shares_cleanup"` 130 } 131 132 type fileConfig struct { 133 commonConfig `mapstructure:",squash"` 134 135 File string `mapstructure:"file"` 136 } 137 138 type cs3Config struct { 139 commonConfig `mapstructure:",squash"` 140 141 ProviderAddr string `mapstructure:"provider_addr"` 142 ServiceUserID string `mapstructure:"service_user_id"` 143 ServiceUserIdp string `mapstructure:"service_user_idp"` 144 MachineAuthAPIKey string `mapstructure:"machine_auth_apikey"` 145 } 146 147 func (c *commonConfig) init() { 148 if c.SharePasswordHashCost == 0 { 149 c.SharePasswordHashCost = 11 150 } 151 if c.JanitorRunInterval == 0 { 152 c.JanitorRunInterval = 60 153 } 154 } 155 156 type manager struct { 157 gatewayAddr string 158 mutex *sync.Mutex 159 persistence persistence.Persistence 160 161 passwordHashCost int 162 janitorRunInterval int 163 enableExpiredSharesCleanup bool 164 } 165 166 func (m *manager) init() error { 167 return m.persistence.Init(context.Background()) 168 } 169 170 func (m *manager) startJanitorRun() { 171 if !m.enableExpiredSharesCleanup { 172 return 173 } 174 175 ticker := time.NewTicker(time.Duration(m.janitorRunInterval) * time.Second) 176 work := make(chan os.Signal, 1) 177 signal.Notify(work, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT) 178 179 for { 180 select { 181 case <-work: 182 return 183 case <-ticker.C: 184 m.cleanupExpiredShares() 185 } 186 } 187 } 188 189 // Dump exports public shares to channels (e.g. during migration) 190 func (m *manager) Dump(ctx context.Context, shareChan chan<- *publicshare.WithPassword) error { 191 log := appctx.GetLogger(ctx) 192 193 m.mutex.Lock() 194 defer m.mutex.Unlock() 195 196 if err := m.init(); err != nil { 197 return err 198 } 199 200 db, err := m.persistence.Read(ctx) 201 if err != nil { 202 return err 203 } 204 205 for _, v := range db { 206 var local publicshare.WithPassword 207 if err := utils.UnmarshalJSONToProtoV1([]byte(v.(map[string]interface{})["share"].(string)), &local.PublicShare); err != nil { 208 log.Error().Err(err).Msg("error unmarshalling share") 209 } 210 local.Password = v.(map[string]interface{})["password"].(string) 211 shareChan <- &local 212 } 213 214 return nil 215 } 216 217 // Load imports public shares and received shares from channels (e.g. during migration) 218 func (m *manager) Load(ctx context.Context, shareChan <-chan *publicshare.WithPassword) error { 219 m.mutex.Lock() 220 defer m.mutex.Unlock() 221 222 if err := m.init(); err != nil { 223 return err 224 } 225 226 db, err := m.persistence.Read(ctx) 227 if err != nil { 228 return err 229 } 230 231 for ps := range shareChan { 232 encShare, err := utils.MarshalProtoV1ToJSON(&ps.PublicShare) 233 if err != nil { 234 return err 235 } 236 237 db[ps.PublicShare.Id.GetOpaqueId()] = map[string]interface{}{ 238 "share": string(encShare), 239 "password": ps.Password, 240 } 241 } 242 return m.persistence.Write(ctx, db) 243 } 244 245 // CreatePublicShare adds a new entry to manager.shares 246 func (m *manager) CreatePublicShare(ctx context.Context, u *user.User, rInfo *provider.ResourceInfo, g *link.Grant) (*link.PublicShare, error) { 247 id := &link.PublicShareId{ 248 OpaqueId: utils.RandString(15), 249 } 250 251 tkn := utils.RandString(15) 252 now := time.Now().UnixNano() 253 254 displayName, ok := rInfo.ArbitraryMetadata.Metadata["name"] 255 if !ok { 256 displayName = tkn 257 } 258 259 quicklink, _ := strconv.ParseBool(rInfo.ArbitraryMetadata.Metadata["quicklink"]) 260 261 var passwordProtected bool 262 password := g.Password 263 if len(password) > 0 { 264 h, err := bcrypt.GenerateFromPassword([]byte(password), m.passwordHashCost) 265 if err != nil { 266 return nil, errors.Wrap(err, "could not hash share password") 267 } 268 password = string(h) 269 passwordProtected = true 270 } 271 272 createdAt := &typespb.Timestamp{ 273 Seconds: uint64(now / int64(time.Second)), 274 Nanos: uint32(now % int64(time.Second)), 275 } 276 277 s := &link.PublicShare{ 278 Id: id, 279 Owner: rInfo.GetOwner(), 280 Creator: u.Id, 281 ResourceId: rInfo.Id, 282 Token: tkn, 283 Permissions: g.Permissions, 284 Ctime: createdAt, 285 Mtime: createdAt, 286 PasswordProtected: passwordProtected, 287 Expiration: g.Expiration, 288 DisplayName: displayName, 289 Quicklink: quicklink, 290 } 291 292 ps := &publicShare{ 293 Password: password, 294 } 295 proto.Merge(&ps.PublicShare, s) 296 297 m.mutex.Lock() 298 defer m.mutex.Unlock() 299 300 if err := m.init(); err != nil { 301 return nil, err 302 } 303 304 encShare, err := utils.MarshalProtoV1ToJSON(&ps.PublicShare) 305 if err != nil { 306 return nil, err 307 } 308 309 db, err := m.persistence.Read(ctx) 310 if err != nil { 311 return nil, err 312 } 313 314 if _, ok := db[s.Id.GetOpaqueId()]; !ok { 315 db[s.Id.GetOpaqueId()] = map[string]interface{}{ 316 "share": string(encShare), 317 "password": ps.Password, 318 } 319 } else { 320 return nil, errors.New("key already exists") 321 } 322 323 err = m.persistence.Write(ctx, db) 324 if err != nil { 325 return nil, err 326 } 327 328 return s, nil 329 } 330 331 // UpdatePublicShare updates the public share 332 func (m *manager) UpdatePublicShare(ctx context.Context, u *user.User, req *link.UpdatePublicShareRequest) (*link.PublicShare, error) { 333 log := appctx.GetLogger(ctx) 334 share, err := m.GetPublicShare(ctx, u, req.Ref, false) 335 if err != nil { 336 return nil, errors.New("ref does not exist") 337 } 338 339 now := time.Now().UnixNano() 340 var newPasswordEncoded string 341 passwordChanged := false 342 343 switch req.GetUpdate().GetType() { 344 case link.UpdatePublicShareRequest_Update_TYPE_DISPLAYNAME: 345 log.Debug().Str("json", "update display name").Msgf("from: `%v` to `%v`", share.DisplayName, req.Update.GetDisplayName()) 346 share.DisplayName = req.Update.GetDisplayName() 347 case link.UpdatePublicShareRequest_Update_TYPE_PERMISSIONS: 348 old, _ := json.Marshal(share.Permissions) 349 new, _ := json.Marshal(req.Update.GetGrant().Permissions) 350 351 if req.GetUpdate().GetGrant().GetPassword() != "" { 352 passwordChanged = true 353 h, err := bcrypt.GenerateFromPassword([]byte(req.Update.GetGrant().Password), m.passwordHashCost) 354 if err != nil { 355 return nil, errors.Wrap(err, "could not hash share password") 356 } 357 newPasswordEncoded = string(h) 358 share.PasswordProtected = true 359 } 360 361 log.Debug().Str("json", "update grants").Msgf("from: `%v`\nto\n`%v`", old, new) 362 share.Permissions = req.Update.GetGrant().GetPermissions() 363 case link.UpdatePublicShareRequest_Update_TYPE_EXPIRATION: 364 old, _ := json.Marshal(share.Expiration) 365 new, _ := json.Marshal(req.Update.GetGrant().Expiration) 366 log.Debug().Str("json", "update expiration").Msgf("from: `%v`\nto\n`%v`", old, new) 367 share.Expiration = req.Update.GetGrant().Expiration 368 case link.UpdatePublicShareRequest_Update_TYPE_PASSWORD: 369 passwordChanged = true 370 if req.Update.GetGrant().Password == "" { 371 share.PasswordProtected = false 372 newPasswordEncoded = "" 373 } else { 374 h, err := bcrypt.GenerateFromPassword([]byte(req.Update.GetGrant().Password), m.passwordHashCost) 375 if err != nil { 376 return nil, errors.Wrap(err, "could not hash share password") 377 } 378 newPasswordEncoded = string(h) 379 share.PasswordProtected = true 380 } 381 default: 382 return nil, fmt.Errorf("invalid update type: %v", req.GetUpdate().GetType()) 383 } 384 385 share.Mtime = &typespb.Timestamp{ 386 Seconds: uint64(now / int64(time.Second)), 387 Nanos: uint32(now % int64(time.Second)), 388 } 389 390 m.mutex.Lock() 391 defer m.mutex.Unlock() 392 393 if err := m.init(); err != nil { 394 return nil, err 395 } 396 397 db, err := m.persistence.Read(ctx) 398 if err != nil { 399 return nil, err 400 } 401 402 encShare, err := utils.MarshalProtoV1ToJSON(share) 403 if err != nil { 404 return nil, err 405 } 406 407 data, ok := db[share.Id.OpaqueId].(map[string]interface{}) 408 if !ok { 409 data = map[string]interface{}{} 410 } 411 412 if ok && passwordChanged { 413 data["password"] = newPasswordEncoded 414 } 415 data["share"] = string(encShare) 416 417 db[share.Id.OpaqueId] = data 418 419 err = m.persistence.Write(ctx, db) 420 if err != nil { 421 return nil, err 422 } 423 424 return share, nil 425 } 426 427 // GetPublicShare gets a public share either by ID or Token. 428 func (m *manager) GetPublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference, sign bool) (*link.PublicShare, error) { 429 m.mutex.Lock() 430 defer m.mutex.Unlock() 431 432 if err := m.init(); err != nil { 433 return nil, err 434 } 435 436 if ref.GetToken() != "" { 437 ps, pw, err := m.getByToken(ctx, ref.GetToken()) 438 if err != nil { 439 return nil, errtypes.NotFound("no shares found by token") 440 } 441 if ps.PasswordProtected && sign { 442 err := publicshare.AddSignature(ps, pw) 443 if err != nil { 444 return nil, err 445 } 446 } 447 return ps, nil 448 } 449 450 db, err := m.persistence.Read(ctx) 451 if err != nil { 452 return nil, err 453 } 454 455 for _, v := range db { 456 d := v.(map[string]interface{})["share"] 457 passDB := v.(map[string]interface{})["password"].(string) 458 459 var ps link.PublicShare 460 if err := utils.UnmarshalJSONToProtoV1([]byte(d.(string)), &ps); err != nil { 461 return nil, err 462 } 463 464 if ref.GetId().GetOpaqueId() == ps.Id.OpaqueId { 465 if publicshare.IsExpired(&ps) { 466 if err := m.revokeExpiredPublicShare(ctx, &ps); err != nil { 467 return nil, err 468 } 469 return nil, errtypes.NotFound("no shares found by id:" + ref.GetId().String()) 470 } 471 if ps.PasswordProtected && sign { 472 err := publicshare.AddSignature(&ps, passDB) 473 if err != nil { 474 return nil, err 475 } 476 } 477 return &ps, nil 478 } 479 480 } 481 return nil, errtypes.NotFound("no shares found by id:" + ref.GetId().String()) 482 } 483 484 // ListPublicShares retrieves all the shares on the manager that are valid. 485 func (m *manager) ListPublicShares(ctx context.Context, u *user.User, filters []*link.ListPublicSharesRequest_Filter, sign bool) ([]*link.PublicShare, error) { 486 m.mutex.Lock() 487 defer m.mutex.Unlock() 488 489 if err := m.init(); err != nil { 490 return nil, err 491 } 492 493 log := appctx.GetLogger(ctx) 494 495 db, err := m.persistence.Read(ctx) 496 if err != nil { 497 return nil, err 498 } 499 500 client, err := pool.GetGatewayServiceClient(m.gatewayAddr) 501 if err != nil { 502 return nil, errors.Wrap(err, "failed to list shares") 503 } 504 cache := make(map[string]struct{}) 505 506 shares := []*link.PublicShare{} 507 for _, v := range db { 508 var local publicShare 509 if err := utils.UnmarshalJSONToProtoV1([]byte(v.(map[string]interface{})["share"].(string)), &local.PublicShare); err != nil { 510 return nil, err 511 } 512 513 if publicshare.IsExpired(&local.PublicShare) { 514 if err := m.revokeExpiredPublicShare(ctx, &local.PublicShare); err != nil { 515 log.Error().Err(err). 516 Str("share_token", local.Token). 517 Msg("failed to revoke expired public share") 518 } 519 continue 520 } 521 522 if !publicshare.MatchesFilters(&local.PublicShare, filters) { 523 continue 524 } 525 526 key := strings.Join([]string{local.ResourceId.StorageId, local.ResourceId.OpaqueId}, "!") 527 if _, hit := cache[key]; !hit && !publicshare.IsCreatedByUser(&local.PublicShare, u) { 528 sRes, err := client.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ResourceId: local.ResourceId}}) 529 if err != nil { 530 log.Error(). 531 Err(err). 532 Interface("resource_id", local.ResourceId). 533 Msg("ListShares: an error occurred during stat on the resource") 534 continue 535 } 536 if sRes.Status.Code != rpc.Code_CODE_OK { 537 if sRes.Status.Code == rpc.Code_CODE_NOT_FOUND { 538 log.Debug(). 539 Str("message", sRes.Status.Message). 540 Interface("status", sRes.Status). 541 Interface("resource_id", local.ResourceId). 542 Msg("ListShares: Resource not found") 543 continue 544 } 545 log.Error(). 546 Str("message", sRes.Status.Message). 547 Interface("status", sRes.Status). 548 Interface("resource_id", local.ResourceId). 549 Msg("ListShares: could not stat resource") 550 continue 551 } 552 if !sRes.Info.PermissionSet.ListGrants { 553 // skip because the user doesn't have the permissions to list 554 // shares of this file. 555 continue 556 } 557 cache[key] = struct{}{} 558 } 559 560 if local.PublicShare.PasswordProtected && sign { 561 if err := publicshare.AddSignature(&local.PublicShare, local.Password); err != nil { 562 return nil, err 563 } 564 } 565 566 shares = append(shares, &local.PublicShare) 567 } 568 return shares, nil 569 } 570 571 func (m *manager) cleanupExpiredShares() { 572 m.mutex.Lock() 573 defer m.mutex.Unlock() 574 575 if err := m.init(); err != nil { 576 return 577 } 578 579 db, _ := m.persistence.Read(context.Background()) 580 581 for _, v := range db { 582 d := v.(map[string]interface{})["share"] 583 584 var ps link.PublicShare 585 _ = utils.UnmarshalJSONToProtoV1([]byte(d.(string)), &ps) 586 587 if publicshare.IsExpired(&ps) { 588 _ = m.revokeExpiredPublicShare(context.Background(), &ps) 589 } 590 } 591 } 592 593 // revokeExpiredPublicShare doesn't have a lock inside, ensure a lock before call 594 func (m *manager) revokeExpiredPublicShare(ctx context.Context, s *link.PublicShare) error { 595 if !m.enableExpiredSharesCleanup { 596 return nil 597 } 598 599 err := m.revokePublicShare(ctx, &link.PublicShareReference{ 600 Spec: &link.PublicShareReference_Id{ 601 Id: &link.PublicShareId{ 602 OpaqueId: s.Id.OpaqueId, 603 }, 604 }, 605 }) 606 if err != nil { 607 log.Err(err).Msg(fmt.Sprintf("publicShareJSONManager: error deleting public share with opaqueId: %s", s.Id.OpaqueId)) 608 return err 609 } 610 611 return nil 612 } 613 614 // RevokePublicShare undocumented. 615 func (m *manager) RevokePublicShare(ctx context.Context, _ *user.User, ref *link.PublicShareReference) error { 616 m.mutex.Lock() 617 defer m.mutex.Unlock() 618 619 if err := m.init(); err != nil { 620 return err 621 } 622 623 return m.revokePublicShare(ctx, ref) 624 } 625 626 // revokePublicShare doesn't have a lock inside, ensure a lock before call 627 func (m *manager) revokePublicShare(ctx context.Context, ref *link.PublicShareReference) error { 628 db, err := m.persistence.Read(ctx) 629 if err != nil { 630 return err 631 } 632 633 switch { 634 case ref.GetId() != nil && ref.GetId().OpaqueId != "": 635 if _, ok := db[ref.GetId().OpaqueId]; ok { 636 delete(db, ref.GetId().OpaqueId) 637 } else { 638 return errors.New("reference does not exist") 639 } 640 case ref.GetToken() != "": 641 share, _, err := m.getByToken(ctx, ref.GetToken()) 642 if err != nil { 643 return err 644 } 645 delete(db, share.Id.OpaqueId) 646 default: 647 return errors.New("reference does not exist") 648 } 649 650 return m.persistence.Write(ctx, db) 651 } 652 653 // getByToken doesn't have a lock inside, ensure a lock before call 654 func (m *manager) getByToken(ctx context.Context, token string) (*link.PublicShare, string, error) { 655 db, err := m.persistence.Read(ctx) 656 if err != nil { 657 return nil, "", err 658 } 659 660 for _, v := range db { 661 var local link.PublicShare 662 if err := utils.UnmarshalJSONToProtoV1([]byte(v.(map[string]interface{})["share"].(string)), &local); err != nil { 663 return nil, "", err 664 } 665 666 if local.Token == token { 667 passDB := v.(map[string]interface{})["password"].(string) 668 return &local, passDB, nil 669 } 670 } 671 672 return nil, "", fmt.Errorf("share with token: `%v` not found", token) 673 } 674 675 // GetPublicShareByToken gets a public share by its opaque token. 676 func (m *manager) GetPublicShareByToken(ctx context.Context, token string, auth *link.PublicShareAuthentication, sign bool) (*link.PublicShare, error) { 677 m.mutex.Lock() 678 defer m.mutex.Unlock() 679 680 if err := m.init(); err != nil { 681 return nil, err 682 } 683 684 db, err := m.persistence.Read(ctx) 685 if err != nil { 686 return nil, err 687 } 688 689 for _, v := range db { 690 passDB := v.(map[string]interface{})["password"].(string) 691 var local link.PublicShare 692 if err := utils.UnmarshalJSONToProtoV1([]byte(v.(map[string]interface{})["share"].(string)), &local); err != nil { 693 return nil, err 694 } 695 696 if local.Token == token { 697 if publicshare.IsExpired(&local) { 698 if err := m.revokeExpiredPublicShare(ctx, &local); err != nil { 699 return nil, err 700 } 701 break 702 } 703 704 if local.PasswordProtected { 705 if publicshare.Authenticate(&local, passDB, auth) { 706 if sign { 707 err := publicshare.AddSignature(&local, passDB) 708 if err != nil { 709 return nil, err 710 } 711 } 712 return &local, nil 713 } 714 715 return nil, errtypes.InvalidCredentials("json: invalid password") 716 } 717 return &local, nil 718 } 719 } 720 721 return nil, errtypes.NotFound(fmt.Sprintf("share with token: `%v` not found", token)) 722 } 723 724 type publicShare struct { 725 link.PublicShare 726 Password string `json:"password"` 727 }