github.com/cs3org/reva/v2@v2.27.7/pkg/publicshare/manager/cs3/cs3_test.go (about)

     1  // Copyright 2018-2022 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 cs3_test
    20  
    21  import (
    22  	"context"
    23  	"encoding/json"
    24  	"os"
    25  	"path"
    26  	"strings"
    27  	"sync"
    28  	"time"
    29  
    30  	. "github.com/onsi/ginkgo/v2"
    31  	. "github.com/onsi/gomega"
    32  	"github.com/stretchr/testify/mock"
    33  	"golang.org/x/crypto/bcrypt"
    34  	"google.golang.org/protobuf/proto"
    35  
    36  	userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
    37  	link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
    38  	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
    39  	typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
    40  	ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
    41  	"github.com/cs3org/reva/v2/pkg/errtypes"
    42  	"github.com/cs3org/reva/v2/pkg/publicshare"
    43  	"github.com/cs3org/reva/v2/pkg/publicshare/manager/cs3"
    44  	indexerpkg "github.com/cs3org/reva/v2/pkg/storage/utils/indexer"
    45  	indexermocks "github.com/cs3org/reva/v2/pkg/storage/utils/indexer/mocks"
    46  	"github.com/cs3org/reva/v2/pkg/storage/utils/metadata"
    47  	storagemocks "github.com/cs3org/reva/v2/pkg/storage/utils/metadata/mocks"
    48  	"github.com/cs3org/reva/v2/pkg/utils"
    49  )
    50  
    51  var _ = Describe("Cs3", func() {
    52  	var (
    53  		m    publicshare.Manager
    54  		user *userpb.User
    55  		ctx  context.Context
    56  
    57  		indexer indexerpkg.Indexer
    58  		storage *storagemocks.Storage
    59  
    60  		ri    *provider.ResourceInfo
    61  		grant *link.Grant
    62  		share *link.PublicShare
    63  
    64  		tmpdir string
    65  	)
    66  
    67  	BeforeEach(func() {
    68  		var err error
    69  		tmpdir, err = os.MkdirTemp("", "cs3-publicshare-test")
    70  		Expect(err).ToNot(HaveOccurred())
    71  
    72  		ds, err := metadata.NewDiskStorage(tmpdir)
    73  		Expect(err).ToNot(HaveOccurred())
    74  		indexer = indexerpkg.CreateIndexer(ds)
    75  
    76  		storage = &storagemocks.Storage{}
    77  		storage.On("Init", mock.Anything, mock.Anything).Return(nil)
    78  		storage.On("MakeDirIfNotExist", mock.Anything, mock.Anything).Return(nil)
    79  		storage.On("SimpleUpload", mock.Anything, mock.MatchedBy(func(in string) bool {
    80  			return strings.HasPrefix(in, "publicshares/")
    81  		}), mock.Anything).Return(nil)
    82  		user = &userpb.User{
    83  			Id: &userpb.UserId{
    84  				Idp:      "localhost:1111",
    85  				OpaqueId: "1",
    86  			},
    87  		}
    88  		ctx = ctxpkg.ContextSetUser(context.Background(), user)
    89  
    90  		share = &link.PublicShare{
    91  			Id:    &link.PublicShareId{OpaqueId: "1"},
    92  			Token: "abcd",
    93  		}
    94  
    95  		ri = &provider.ResourceInfo{
    96  			Type:  provider.ResourceType_RESOURCE_TYPE_CONTAINER,
    97  			Path:  "/share1",
    98  			Id:    &provider.ResourceId{OpaqueId: "sharedId"},
    99  			Owner: user.Id,
   100  			PermissionSet: &provider.ResourcePermissions{
   101  				Stat: true,
   102  			},
   103  			Size: 10,
   104  		}
   105  		grant = &link.Grant{
   106  			Permissions: &link.PublicSharePermissions{
   107  				Permissions: &provider.ResourcePermissions{AddGrant: true},
   108  			},
   109  		}
   110  	})
   111  
   112  	JustBeforeEach(func() {
   113  		var err error
   114  		m, err = cs3.New(nil, storage, indexer, bcrypt.DefaultCost)
   115  		Expect(err).ToNot(HaveOccurred())
   116  	})
   117  
   118  	AfterEach(func() {
   119  		if tmpdir != "" {
   120  			os.RemoveAll(tmpdir)
   121  		}
   122  	})
   123  
   124  	Describe("New", func() {
   125  		It("returns a new instance", func() {
   126  			m, err := cs3.New(nil, storage, indexer, bcrypt.DefaultCost)
   127  			Expect(err).ToNot(HaveOccurred())
   128  			Expect(m).ToNot(BeNil())
   129  		})
   130  	})
   131  
   132  	Describe("CreatePublicShare", func() {
   133  		It("creates a new share and adds it to the index", func() {
   134  			link, err := m.CreatePublicShare(ctx, user, ri, grant)
   135  			Expect(err).ToNot(HaveOccurred())
   136  			Expect(link).ToNot(BeNil())
   137  			Expect(link.Token).ToNot(Equal(""))
   138  			Expect(link.PasswordProtected).To(BeFalse())
   139  		})
   140  
   141  		It("sets 'PasswordProtected' and stores the password hash if a password is set", func() {
   142  			grant.Password = "secret123"
   143  
   144  			link, err := m.CreatePublicShare(ctx, user, ri, grant)
   145  			Expect(err).ToNot(HaveOccurred())
   146  			Expect(link).ToNot(BeNil())
   147  			Expect(link.Token).ToNot(Equal(""))
   148  			Expect(link.PasswordProtected).To(BeTrue())
   149  			storage.AssertCalled(GinkgoT(), "SimpleUpload", mock.Anything, mock.Anything, mock.MatchedBy(func(in []byte) bool {
   150  				ps := publicshare.WithPassword{}
   151  				err = json.Unmarshal(in, &ps)
   152  				Expect(err).ToNot(HaveOccurred())
   153  				return bcrypt.CompareHashAndPassword([]byte(ps.Password), []byte("secret123")) == nil
   154  			}))
   155  		})
   156  
   157  		It("picks up the displayname from the metadata", func() {
   158  			ri.ArbitraryMetadata = &provider.ArbitraryMetadata{
   159  				Metadata: map[string]string{
   160  					"name": "metadata name",
   161  				},
   162  			}
   163  
   164  			link, err := m.CreatePublicShare(ctx, user, ri, grant)
   165  			Expect(err).ToNot(HaveOccurred())
   166  			Expect(link).ToNot(BeNil())
   167  			Expect(link.DisplayName).To(Equal("metadata name"))
   168  		})
   169  	})
   170  
   171  	Context("with an existing share", func() {
   172  		var (
   173  			existingShare  *link.PublicShare
   174  			hashedPassword string
   175  		)
   176  
   177  		JustBeforeEach(func() {
   178  			grant.Password = "foo"
   179  			var err error
   180  			existingShare, err = m.CreatePublicShare(ctx, user, ri, grant)
   181  			Expect(err).ToNot(HaveOccurred())
   182  
   183  			h, err := bcrypt.GenerateFromPassword([]byte(grant.Password), bcrypt.DefaultCost)
   184  			Expect(err).ToNot(HaveOccurred())
   185  			hashedPassword = string(h)
   186  			tmpShare := &publicshare.WithPassword{
   187  				Password: hashedPassword,
   188  			}
   189  			proto.Merge(&tmpShare.PublicShare, existingShare)
   190  			shareJSON, err := json.Marshal(tmpShare)
   191  			Expect(err).ToNot(HaveOccurred())
   192  			storage.On("SimpleDownload", mock.Anything, mock.MatchedBy(func(in string) bool {
   193  				return strings.HasPrefix(in, "publicshares/")
   194  			})).Return(shareJSON, nil)
   195  		})
   196  
   197  		Describe("Load", func() {
   198  			It("loads shares including state and mountpoint information", func() {
   199  				m, err := cs3.New(nil, storage, indexer, bcrypt.DefaultCost)
   200  				Expect(err).ToNot(HaveOccurred())
   201  
   202  				sharesChan := make(chan *publicshare.WithPassword)
   203  
   204  				wg := sync.WaitGroup{}
   205  				wg.Add(2)
   206  				go func() {
   207  					err := m.Load(ctx, sharesChan)
   208  					Expect(err).ToNot(HaveOccurred())
   209  					wg.Done()
   210  				}()
   211  				go func() {
   212  					tmpShare := &publicshare.WithPassword{
   213  						Password: hashedPassword,
   214  					}
   215  					proto.Merge(&tmpShare.PublicShare, existingShare)
   216  					sharesChan <- tmpShare
   217  					close(sharesChan)
   218  					wg.Done()
   219  				}()
   220  				wg.Wait()
   221  				Eventually(sharesChan).Should(BeClosed())
   222  
   223  				expectedPath := path.Join("publicshares", existingShare.Token)
   224  				storage.AssertCalled(GinkgoT(), "SimpleUpload", mock.Anything, expectedPath, mock.Anything)
   225  			})
   226  		})
   227  
   228  		Describe("ListPublicShares", func() {
   229  			It("lists existing shares", func() {
   230  				shares, err := m.ListPublicShares(ctx, user, []*link.ListPublicSharesRequest_Filter{}, false)
   231  				Expect(err).ToNot(HaveOccurred())
   232  				Expect(len(shares)).To(Equal(1))
   233  				Expect(shares[0].Signature).To(BeNil())
   234  			})
   235  
   236  			It("adds a signature", func() {
   237  				shares, err := m.ListPublicShares(ctx, user, []*link.ListPublicSharesRequest_Filter{}, true)
   238  				Expect(err).ToNot(HaveOccurred())
   239  				Expect(len(shares)).To(Equal(1))
   240  				Expect(shares[0].Signature).ToNot(BeNil())
   241  			})
   242  
   243  			It("filters by id", func() {
   244  				shares, err := m.ListPublicShares(ctx, user, []*link.ListPublicSharesRequest_Filter{
   245  					publicshare.ResourceIDFilter(&provider.ResourceId{OpaqueId: "UnknownId"}),
   246  				}, false)
   247  				Expect(err).ToNot(HaveOccurred())
   248  				Expect(len(shares)).To(Equal(0))
   249  			})
   250  
   251  			It("filters by storage", func() {
   252  				shares, err := m.ListPublicShares(ctx, user, []*link.ListPublicSharesRequest_Filter{
   253  					publicshare.StorageIDFilter("unknownstorage"),
   254  				}, false)
   255  				Expect(err).ToNot(HaveOccurred())
   256  				Expect(len(shares)).To(Equal(0))
   257  			})
   258  
   259  			Context("when the share has expired", func() {
   260  				BeforeEach(func() {
   261  					t := time.Date(2022, time.January, 1, 12, 0, 0, 0, time.UTC)
   262  					grant.Expiration = &typespb.Timestamp{
   263  						Seconds: uint64(t.Unix()),
   264  					}
   265  					storage.On("Delete", mock.Anything, mock.Anything).Return(nil, nil)
   266  				})
   267  
   268  				It("does not consider the share", func() {
   269  					shares, err := m.ListPublicShares(ctx, user, []*link.ListPublicSharesRequest_Filter{}, false)
   270  					Expect(err).ToNot(HaveOccurred())
   271  					Expect(len(shares)).To(Equal(0))
   272  				})
   273  			})
   274  		})
   275  
   276  		Describe("GetPublicShare", func() {
   277  			It("gets the public share by token", func() {
   278  				returnedShare, err := m.GetPublicShare(ctx, user, &link.PublicShareReference{
   279  					Spec: &link.PublicShareReference_Token{
   280  						Token: share.Token,
   281  					},
   282  				}, false)
   283  				Expect(err).ToNot(HaveOccurred())
   284  				Expect(returnedShare).ToNot(BeNil())
   285  				Expect(returnedShare.Id.OpaqueId).To(Equal(existingShare.Id.OpaqueId))
   286  				Expect(returnedShare.Token).To(Equal(existingShare.Token))
   287  			})
   288  
   289  			It("gets the public share by id", func() {
   290  				returnedShare, err := m.GetPublicShare(ctx, user, &link.PublicShareReference{
   291  					Spec: &link.PublicShareReference_Id{
   292  						Id: &link.PublicShareId{
   293  							OpaqueId: existingShare.Id.OpaqueId,
   294  						},
   295  					},
   296  				}, false)
   297  				Expect(err).ToNot(HaveOccurred())
   298  				Expect(returnedShare).ToNot(BeNil())
   299  				Expect(returnedShare.Signature).To(BeNil())
   300  			})
   301  
   302  			It("adds a signature", func() {
   303  				returnedShare, err := m.GetPublicShare(ctx, user, &link.PublicShareReference{
   304  					Spec: &link.PublicShareReference_Id{
   305  						Id: &link.PublicShareId{
   306  							OpaqueId: existingShare.Id.OpaqueId,
   307  						},
   308  					},
   309  				}, true)
   310  				Expect(err).ToNot(HaveOccurred())
   311  				Expect(returnedShare).ToNot(BeNil())
   312  				Expect(returnedShare.Signature).ToNot(BeNil())
   313  			})
   314  		})
   315  
   316  		Describe("RevokePublicShare", func() {
   317  			var (
   318  				mockIndexer *indexermocks.Indexer
   319  			)
   320  			BeforeEach(func() {
   321  				mockIndexer = &indexermocks.Indexer{}
   322  				mockIndexer.On("AddIndex", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
   323  				mockIndexer.On("Add", mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
   324  				mockIndexer.On("Delete", mock.Anything, mock.Anything).Return(nil, nil)
   325  				mockIndexer.On("FindBy", mock.Anything, mock.Anything, mock.Anything).Return([]string{existingShare.Token}, nil)
   326  
   327  				indexer = mockIndexer
   328  			})
   329  
   330  			It("deletes the share by token", func() {
   331  				storage.On("Delete", mock.Anything, mock.Anything).Return(nil)
   332  				ref := &link.PublicShareReference{
   333  					Spec: &link.PublicShareReference_Token{
   334  						Token: existingShare.Token,
   335  					},
   336  				}
   337  				err := m.RevokePublicShare(ctx, user, ref)
   338  				Expect(err).ToNot(HaveOccurred())
   339  				storage.AssertCalled(GinkgoT(), "Delete", mock.Anything, path.Join("publicshares", existingShare.Token))
   340  			})
   341  
   342  			It("deletes the share by id", func() {
   343  				storage.On("Delete", mock.Anything, mock.Anything).Return(nil)
   344  				ref := &link.PublicShareReference{
   345  					Spec: &link.PublicShareReference_Id{
   346  						Id: existingShare.Id,
   347  					},
   348  				}
   349  				err := m.RevokePublicShare(ctx, user, ref)
   350  				Expect(err).ToNot(HaveOccurred())
   351  				storage.AssertCalled(GinkgoT(), "Delete", mock.Anything, path.Join("publicshares", existingShare.Token))
   352  			})
   353  
   354  			It("still removes the share from the index when the share itself couldn't be found", func() {
   355  				storage.On("Delete", mock.Anything, mock.Anything).Return(errtypes.NotFound(""))
   356  				ref := &link.PublicShareReference{
   357  					Spec: &link.PublicShareReference_Token{
   358  						Token: existingShare.Token,
   359  					},
   360  				}
   361  				err := m.RevokePublicShare(ctx, user, ref)
   362  				Expect(err).ToNot(HaveOccurred())
   363  
   364  				mockIndexer.AssertCalled(GinkgoT(), "Delete", mock.Anything, mock.Anything)
   365  			})
   366  
   367  			It("does not removes the share from the index when the share itself couldn't be found", func() {
   368  				storage.On("Delete", mock.Anything, mock.Anything).Return(errtypes.InternalError(""))
   369  				ref := &link.PublicShareReference{
   370  					Spec: &link.PublicShareReference_Token{
   371  						Token: existingShare.Token,
   372  					},
   373  				}
   374  				err := m.RevokePublicShare(ctx, user, ref)
   375  				Expect(err).To(HaveOccurred())
   376  
   377  				mockIndexer.AssertNotCalled(GinkgoT(), "Delete", mock.Anything, mock.Anything)
   378  			})
   379  		})
   380  
   381  		Describe("GetPublicShareByToken", func() {
   382  			It("doesn't get the share using a wrong password", func() {
   383  				auth := &link.PublicShareAuthentication{
   384  					Spec: &link.PublicShareAuthentication_Password{
   385  						Password: "wroooong",
   386  					},
   387  				}
   388  				ps, err := m.GetPublicShareByToken(ctx, existingShare.Token, auth, false)
   389  				Expect(err).To(HaveOccurred())
   390  				Expect(ps).To(BeNil())
   391  			})
   392  
   393  			It("gets the share using a password", func() {
   394  				auth := &link.PublicShareAuthentication{
   395  					Spec: &link.PublicShareAuthentication_Password{
   396  						Password: grant.Password,
   397  					},
   398  				}
   399  				ps, err := m.GetPublicShareByToken(ctx, existingShare.Token, auth, false)
   400  				Expect(err).ToNot(HaveOccurred())
   401  				Expect(ps).ToNot(BeNil())
   402  			})
   403  
   404  			It("gets the share using a signature", func() {
   405  				err := publicshare.AddSignature(existingShare, hashedPassword)
   406  				Expect(err).ToNot(HaveOccurred())
   407  				auth := &link.PublicShareAuthentication{
   408  					Spec: &link.PublicShareAuthentication_Signature{
   409  						Signature: existingShare.Signature,
   410  					},
   411  				}
   412  				ps, err := m.GetPublicShareByToken(ctx, existingShare.Token, auth, false)
   413  				Expect(err).ToNot(HaveOccurred())
   414  				Expect(ps).ToNot(BeNil())
   415  
   416  			})
   417  
   418  			Context("when the share has expired", func() {
   419  				BeforeEach(func() {
   420  					t := time.Date(2022, time.January, 1, 12, 0, 0, 0, time.UTC)
   421  					grant.Expiration = &typespb.Timestamp{
   422  						Seconds: uint64(t.Unix()),
   423  					}
   424  				})
   425  				It("it doesn't consider expired shares", func() {
   426  					auth := &link.PublicShareAuthentication{
   427  						Spec: &link.PublicShareAuthentication_Password{
   428  							Password: grant.Password,
   429  						},
   430  					}
   431  					ps, err := m.GetPublicShareByToken(ctx, existingShare.Token, auth, false)
   432  					Expect(err).To(HaveOccurred())
   433  					Expect(ps).To(BeNil())
   434  				})
   435  			})
   436  		})
   437  
   438  		Describe("UpdatePublicShare", func() {
   439  			var (
   440  				ref *link.PublicShareReference
   441  			)
   442  
   443  			JustBeforeEach(func() {
   444  				ref = &link.PublicShareReference{
   445  					Spec: &link.PublicShareReference_Token{
   446  						Token: existingShare.Token,
   447  					},
   448  				}
   449  			})
   450  
   451  			It("fails when an invalid reference is given", func() {
   452  				_, err := m.UpdatePublicShare(ctx, user, &link.UpdatePublicShareRequest{
   453  					Ref: &link.PublicShareReference{Spec: &link.PublicShareReference_Id{Id: &link.PublicShareId{OpaqueId: "doesnotexist"}}},
   454  				})
   455  				Expect(err).To(HaveOccurred())
   456  			})
   457  
   458  			It("fails when no valid update request is given", func() {
   459  				_, err := m.UpdatePublicShare(ctx, user, &link.UpdatePublicShareRequest{
   460  					Ref:    ref,
   461  					Update: &link.UpdatePublicShareRequest_Update{},
   462  				})
   463  				Expect(err).To(HaveOccurred())
   464  			})
   465  
   466  			It("updates the display name", func() {
   467  				ps, err := m.UpdatePublicShare(ctx, user, &link.UpdatePublicShareRequest{
   468  					Ref: ref,
   469  					Update: &link.UpdatePublicShareRequest_Update{
   470  						Type:        link.UpdatePublicShareRequest_Update_TYPE_DISPLAYNAME,
   471  						DisplayName: "new displayname",
   472  					},
   473  				})
   474  				Expect(err).ToNot(HaveOccurred())
   475  				Expect(ps).ToNot(BeNil())
   476  				Expect(ps.DisplayName).To(Equal("new displayname"))
   477  				storage.AssertCalled(GinkgoT(), "SimpleUpload", mock.Anything, path.Join("publicshares", ps.Token), mock.MatchedBy(func(data []byte) bool {
   478  					s := publicshare.WithPassword{}
   479  					err := json.Unmarshal(data, &s)
   480  					Expect(err).ToNot(HaveOccurred())
   481  					return s.PublicShare.DisplayName == "new displayname"
   482  				}))
   483  			})
   484  
   485  			It("updates the password", func() {
   486  				ps, err := m.UpdatePublicShare(ctx, user, &link.UpdatePublicShareRequest{
   487  					Ref: ref,
   488  					Update: &link.UpdatePublicShareRequest_Update{
   489  						Type:  link.UpdatePublicShareRequest_Update_TYPE_PASSWORD,
   490  						Grant: &link.Grant{Password: "NewPass"},
   491  					},
   492  				})
   493  				Expect(err).ToNot(HaveOccurred())
   494  				Expect(ps).ToNot(BeNil())
   495  				storage.AssertCalled(GinkgoT(), "SimpleUpload", mock.Anything, path.Join("publicshares", ps.Token), mock.MatchedBy(func(data []byte) bool {
   496  					s := publicshare.WithPassword{}
   497  					err := json.Unmarshal(data, &s)
   498  					Expect(err).ToNot(HaveOccurred())
   499  					return s.Password != ""
   500  				}))
   501  			})
   502  
   503  			It("updates the permissions", func() {
   504  				ps, err := m.UpdatePublicShare(ctx, user, &link.UpdatePublicShareRequest{
   505  					Ref: ref,
   506  					Update: &link.UpdatePublicShareRequest_Update{
   507  						Type:  link.UpdatePublicShareRequest_Update_TYPE_PERMISSIONS,
   508  						Grant: &link.Grant{Permissions: &link.PublicSharePermissions{Permissions: &provider.ResourcePermissions{Delete: true}}},
   509  					},
   510  				})
   511  				Expect(err).ToNot(HaveOccurred())
   512  				Expect(ps).ToNot(BeNil())
   513  				storage.AssertCalled(GinkgoT(), "SimpleUpload", mock.Anything, path.Join("publicshares", ps.Token), mock.MatchedBy(func(data []byte) bool {
   514  					s := publicshare.WithPassword{}
   515  					err := json.Unmarshal(data, &s)
   516  					Expect(err).ToNot(HaveOccurred())
   517  					return s.PublicShare.Permissions.Permissions.Delete
   518  				}))
   519  			})
   520  
   521  			It("updates the expiration", func() {
   522  				ts := utils.TSNow()
   523  				ps, err := m.UpdatePublicShare(ctx, user, &link.UpdatePublicShareRequest{
   524  					Ref: ref,
   525  					Update: &link.UpdatePublicShareRequest_Update{
   526  						Type:  link.UpdatePublicShareRequest_Update_TYPE_EXPIRATION,
   527  						Grant: &link.Grant{Expiration: utils.TSNow()},
   528  					},
   529  				})
   530  				Expect(err).ToNot(HaveOccurred())
   531  				Expect(ps).ToNot(BeNil())
   532  				storage.AssertCalled(GinkgoT(), "SimpleUpload", mock.Anything, path.Join("publicshares", ps.Token), mock.MatchedBy(func(data []byte) bool {
   533  					s := publicshare.WithPassword{}
   534  					err := json.Unmarshal(data, &s)
   535  					Expect(err).ToNot(HaveOccurred())
   536  					return s.PublicShare.Expiration != nil && s.PublicShare.Expiration.Seconds == ts.Seconds
   537  				}))
   538  			})
   539  		})
   540  	})
   541  })