Skip to content

Commit f9d2cd6

Browse files
authored
fix: stale sharing shortcut counter after recipient removal (#4780)
## Summary Fixes the Drive sharing counter after a recipient is removed from a pending shared drive. A recipient can have multiple shortcut files for the same sharing: for example, one pending `new` shortcut and another shortcut created when opening the sharing authorization flow. Previously, revocation only removed the shortcut pointed to by `ShortcutID`, leaving older referenced shortcuts counted by `/sharings/news`. ## Changes - Clean up the shortcut referenced by `ShortcutID`. - Also remove remaining shortcut files linked to the same sharing through `referenced_by`. - Keep referenced cleanup limited to shortcut files.
2 parents 20b68c3 + 600fcfa commit f9d2cd6

3 files changed

Lines changed: 198 additions & 40 deletions

File tree

model/sharing/invitation.go

Lines changed: 62 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -356,24 +356,7 @@ func (s *Sharing) CreateDriveShortcut(inst *instance.Instance, seen bool) error
356356
Type: consts.Sharings,
357357
})
358358

359-
file, err := inst.VFS().CreateFile(fileDoc, nil)
360-
if err != nil {
361-
basename := fileDoc.DocName
362-
for i := 2; i < 100; i++ {
363-
fileDoc.DocName = fmt.Sprintf("%s (%d)", basename, i)
364-
file, err = inst.VFS().CreateFile(fileDoc, nil)
365-
if err == nil {
366-
break
367-
}
368-
}
369-
if err != nil {
370-
return err
371-
}
372-
}
373-
_, err = file.Write(body)
374-
if cerr := file.Close(); cerr != nil && err == nil {
375-
err = cerr
376-
}
359+
fileDoc, err = s.createOrUpdateShortcut(inst, fileDoc, body)
377360
if err != nil {
378361
return err
379362
}
@@ -448,12 +431,59 @@ func (s *Sharing) CreateShortcut(inst *instance.Instance, previewURL string, see
448431
Type: consts.Sharings,
449432
})
450433

451-
file, err := inst.VFS().CreateFile(fileDoc, nil)
434+
fileDoc, err = s.createOrUpdateShortcut(inst, fileDoc, body)
435+
if err != nil {
436+
return err
437+
}
438+
439+
s.ShortcutID = fileDoc.DocID
440+
if err := couchdb.UpdateDoc(inst, s); err != nil {
441+
inst.Logger().Warnf("Cannot save shortcut id %s: %s", s.ShortcutID, err)
442+
}
443+
444+
if !seen {
445+
return s.SendShortcutNotification(inst, fileDoc, previewURL)
446+
}
447+
return nil
448+
}
449+
450+
func (s *Sharing) createOrUpdateShortcut(inst *instance.Instance, fileDoc *vfs.FileDoc, body []byte) (*vfs.FileDoc, error) {
451+
fs := inst.VFS()
452+
if s.ShortcutID != "" {
453+
if olddoc, err := fs.FileByID(s.ShortcutID); err == nil && isReusableShortcut(olddoc) {
454+
newdoc := olddoc.Clone().(*vfs.FileDoc)
455+
newdoc.ByteSize = int64(len(body))
456+
newdoc.MD5Sum = nil
457+
newdoc.Mime = fileDoc.Mime
458+
newdoc.Class = fileDoc.Class
459+
newdoc.Executable = fileDoc.Executable
460+
newdoc.Encrypted = fileDoc.Encrypted
461+
newdoc.UpdatedAt = fileDoc.UpdatedAt
462+
newdoc.Metadata = fileDoc.Metadata
463+
newdoc.ReferencedBy = fileDoc.ReferencedBy
464+
newdoc.CozyMetadata = fileDoc.CozyMetadata
465+
return newdoc, writeShortcutFile(fs, newdoc, olddoc, body)
466+
}
467+
}
468+
469+
if err := createShortcutFile(fs, fileDoc, body); err != nil {
470+
return nil, err
471+
}
472+
return fileDoc, nil
473+
}
474+
475+
func isReusableShortcut(file *vfs.FileDoc) bool {
476+
return !file.Trashed &&
477+
(file.Mime == consts.ShortcutMimeType || file.Class == "shortcut")
478+
}
479+
480+
func createShortcutFile(fs vfs.VFS, fileDoc *vfs.FileDoc, body []byte) error {
481+
file, err := fs.CreateFile(fileDoc, nil)
452482
if err != nil {
453483
basename := fileDoc.DocName
454484
for i := 2; i < 100; i++ {
455485
fileDoc.DocName = fmt.Sprintf("%s (%d)", basename, i)
456-
file, err = inst.VFS().CreateFile(fileDoc, nil)
486+
file, err = fs.CreateFile(fileDoc, nil)
457487
if err == nil {
458488
break
459489
}
@@ -462,23 +492,23 @@ func (s *Sharing) CreateShortcut(inst *instance.Instance, previewURL string, see
462492
return err
463493
}
464494
}
465-
_, err = file.Write(body)
466-
if cerr := file.Close(); cerr != nil && err == nil {
467-
err = cerr
468-
}
495+
return writeShortcutContent(file, body)
496+
}
497+
498+
func writeShortcutFile(fs vfs.VFS, newdoc, olddoc *vfs.FileDoc, body []byte) error {
499+
file, err := fs.CreateFile(newdoc, olddoc)
469500
if err != nil {
470501
return err
471502
}
503+
return writeShortcutContent(file, body)
504+
}
472505

473-
s.ShortcutID = fileDoc.DocID
474-
if err := couchdb.UpdateDoc(inst, s); err != nil {
475-
inst.Logger().Warnf("Cannot save shortcut id %s: %s", s.ShortcutID, err)
476-
}
477-
478-
if !seen {
479-
return s.SendShortcutNotification(inst, fileDoc, previewURL)
506+
func writeShortcutContent(file vfs.File, body []byte) error {
507+
_, err := file.Write(body)
508+
if cerr := file.Close(); cerr != nil && err == nil {
509+
err = cerr
480510
}
481-
return nil
511+
return err
482512
}
483513

484514
// SendShortcut sends the HTTP request to the cozy of the recipient for adding

model/sharing/sharing.go

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,21 +1188,76 @@ func (s *Sharing) GetSharecodeFromShortcut(inst *instance.Instance) (string, err
11881188
}
11891189

11901190
func (s *Sharing) cleanShortcutID(inst *instance.Instance) string {
1191-
if s.ShortcutID == "" {
1191+
fs := inst.VFS()
1192+
// remove a shortcut by id, as it before
1193+
parentID, deletedByID, err := destroyShortcutByID(fs, s.ShortcutID)
1194+
if err != nil {
11921195
return ""
11931196
}
1197+
shouldClearShortcutID := deletedByID || s.ShortcutID != ""
1198+
1199+
// remove shortcut reference from sharing
1200+
referencedParentID, deletedByRef, err := destroyReferencedShortcutFiles(inst, fs, s.SID)
1201+
if err == nil && parentID == "" {
1202+
parentID = referencedParentID
1203+
}
1204+
if err == nil && deletedByRef {
1205+
shouldClearShortcutID = true
1206+
}
1207+
1208+
if shouldClearShortcutID {
1209+
s.ShortcutID = ""
1210+
_ = couchdb.UpdateDoc(inst, s)
1211+
}
1212+
return parentID
1213+
}
1214+
1215+
func destroyShortcutByID(fs vfs.VFS, shortcutID string) (string, bool, error) {
1216+
if shortcutID == "" {
1217+
return "", false, nil
1218+
}
1219+
file, err := fs.FileByID(shortcutID)
1220+
if err != nil {
1221+
return "", false, nil
1222+
}
1223+
if err := fs.DestroyFile(file); err != nil {
1224+
return "", false, err
1225+
}
1226+
return file.DirID, true, nil
1227+
}
1228+
1229+
func destroyReferencedShortcutFiles(inst *instance.Instance, fs vfs.VFS, sharingID string) (string, bool, error) {
1230+
key := []string{consts.Sharings, sharingID}
1231+
req := &couchdb.ViewRequest{
1232+
StartKey: key,
1233+
EndKey: []string{key[0], key[1], couchdb.MaxString},
1234+
}
1235+
var res couchdb.ViewResponse
1236+
if err := couchdb.ExecView(inst, couchdb.FilesReferencedByView, req, &res); err != nil {
1237+
return "", false, err
1238+
}
11941239

11951240
var parentID string
1196-
fs := inst.VFS()
1197-
if file, err := fs.FileByID(s.ShortcutID); err == nil {
1198-
parentID = file.DirID
1241+
var deleted bool
1242+
for _, row := range res.Rows {
1243+
file, err := fs.FileByID(row.ID)
1244+
if err != nil || !isShortcutFile(file) {
1245+
continue
1246+
}
11991247
if err := fs.DestroyFile(file); err != nil {
1200-
return ""
1248+
return "", false, err
1249+
}
1250+
if parentID == "" {
1251+
parentID = file.DirID
12011252
}
1253+
deleted = true
12021254
}
1203-
s.ShortcutID = ""
1204-
_ = couchdb.UpdateDoc(inst, s)
1205-
return parentID
1255+
return parentID, deleted, nil
1256+
}
1257+
1258+
func isShortcutFile(file *vfs.FileDoc) bool {
1259+
return file.Type == consts.FileType &&
1260+
(file.Mime == consts.ShortcutMimeType || file.Class == "shortcut")
12061261
}
12071262

12081263
// GetPreviewURL asks the owner's Cozy the URL for previewing the sharing.

web/sharings/drives_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4141,6 +4141,79 @@ func TestSharedDriveRevocation(t *testing.T) {
41414141
})
41424142
}
41434143

4144+
func TestOpeningPendingSharedDriveShortcutClearsNewsCounter(t *testing.T) {
4145+
if testing.Short() {
4146+
t.Skip("an instance is required for this test: test skipped due to the use of --short flag")
4147+
}
4148+
4149+
env := setupSharedDrivesEnv(t)
4150+
eA, eB, _ := env.createClients(t)
4151+
sharingID := createDirectRecipientDriveSharing(
4152+
t,
4153+
env.acme,
4154+
eA,
4155+
env.acmeToken,
4156+
"Betty Pending",
4157+
"betty@example.net",
4158+
env.tsB.URL,
4159+
"Pending Revocation Drive",
4160+
"Pending drive for revocation counter",
4161+
)
4162+
4163+
recipientSharing := waitForSharingOnRecipient(t, env.betty, sharingID)
4164+
require.NotEmpty(t, recipientSharing.ShortcutID)
4165+
newShortcutID := recipientSharing.ShortcutID
4166+
4167+
eB.GET("/sharings/news").
4168+
WithHeader("Authorization", "Bearer "+env.bettyToken).
4169+
Expect().Status(http.StatusOK).
4170+
JSON().Object().
4171+
Path("$.meta.count").Number().IsEqual(1)
4172+
4173+
loginSharingRecipient(t, eB)
4174+
FakeOwnerInstanceForSharing(t, env.betty, env.tsA.URL, sharingID)
4175+
require.NotEmpty(t, recipientSharing.Credentials)
4176+
require.NotEmpty(t, recipientSharing.Credentials[0].State)
4177+
authorizeLink := fmt.Sprintf(
4178+
"%s/auth/authorize/sharing?sharing_id=%s&state=%s",
4179+
env.tsB.URL,
4180+
sharingID,
4181+
url.QueryEscape(recipientSharing.Credentials[0].State),
4182+
)
4183+
openSharingAuthorize(t, eB, authorizeLink, sharingID)
4184+
4185+
recipientSharing, err := sharing.FindSharing(env.betty, sharingID)
4186+
require.NoError(t, err)
4187+
require.NotEmpty(t, recipientSharing.ShortcutID)
4188+
seenShortcutID := recipientSharing.ShortcutID
4189+
require.Equal(t, newShortcutID, seenShortcutID)
4190+
4191+
eB.GET("/sharings/news").
4192+
WithHeader("Authorization", "Bearer "+env.bettyToken).
4193+
Expect().Status(http.StatusOK).
4194+
JSON().Object().
4195+
Path("$.meta.count").Number().IsEqual(0)
4196+
4197+
require.Eventually(t, func() bool {
4198+
count, err := sharing.CountNewShortcuts(env.betty)
4199+
return err == nil && count == 0
4200+
}, 5*time.Second, 100*time.Millisecond, "Betty's sharing counter should not include the opened pending drive")
4201+
4202+
require.Eventually(t, func() bool {
4203+
_, err := env.betty.VFS().FileByID(newShortcutID)
4204+
return err == nil
4205+
}, 5*time.Second, 100*time.Millisecond, "Betty's opened shared-drive shortcut should still exist before revocation")
4206+
4207+
eA.DELETE("/sharings/"+sharingID+"/recipients/1").
4208+
WithHeader("Authorization", "Bearer "+env.acmeToken).
4209+
Expect().Status(http.StatusNoContent)
4210+
4211+
require.Eventually(t, func() bool {
4212+
_, err := env.betty.VFS().FileByID(newShortcutID)
4213+
return os.IsNotExist(err)
4214+
}, 5*time.Second, 100*time.Millisecond, "Betty's opened shared-drive shortcut should be removed after revocation")
4215+
}
4216+
41444217
func TestDirectoryRootSharedDriveOwnerDeletionRevokesRecipient(t *testing.T) {
41454218
if testing.Short() {
41464219
t.Skip("an instance is required for this test: test skipped due to the use of --short flag")

0 commit comments

Comments
 (0)