@@ -28,6 +28,8 @@ import (
2828 "github.com/labstack/echo/v4"
2929)
3030
31+ const InteractiveSharingQueryParam = "interactive"
32+
3133// CreateSharingRequest sends information about the sharing to the recipient's cozy
3234func (m * Member ) CreateSharingRequest (inst * instance.Instance , s * Sharing , c * Credentials , u * url.URL ) error {
3335 if len (c .XorKey ) == 0 {
@@ -232,6 +234,16 @@ func countFilesInDirectory(inst *instance.Instance, dir *vfs.DirDoc) (int, error
232234
233235// RegisterCozyURL saves a new Cozy URL for a member
234236func (s * Sharing ) RegisterCozyURL (inst * instance.Instance , m * Member , cozyURL string ) error {
237+ return s .registerCozyURL (inst , m , cozyURL , false )
238+ }
239+
240+ // RegisterCozyURLInteractive saves a Cozy URL discovered during an interactive
241+ // acceptance flow and marks the recipient request so auto-accept is skipped.
242+ func (s * Sharing ) RegisterCozyURLInteractive (inst * instance.Instance , m * Member , cozyURL string ) error {
243+ return s .registerCozyURL (inst , m , cozyURL , true )
244+ }
245+
246+ func (s * Sharing ) registerCozyURL (inst * instance.Instance , m * Member , cozyURL string , interactive bool ) error {
235247 if ! s .Owner {
236248 return ErrInvalidSharing
237249 }
@@ -255,6 +267,11 @@ func (s *Sharing) RegisterCozyURL(inst *instance.Instance, m *Member, cozyURL st
255267 u .RawQuery = ""
256268 u .Fragment = ""
257269 m .Instance = u .String ()
270+ if interactive {
271+ q := u .Query ()
272+ q .Set (InteractiveSharingQueryParam , "true" )
273+ u .RawQuery = q .Encode ()
274+ }
258275
259276 creds := s .FindCredentials (m )
260277 if creds == nil {
@@ -382,6 +399,44 @@ func CreateAccessToken(inst *instance.Instance, cli *oauth.Client, sharingID str
382399 }, nil
383400}
384401
402+ // updateSharingWithConflictRetry persists the sharing and resolves stale-revision
403+ // conflicts by reconciling the desired changes onto the latest revision.
404+ // reconcile can mutate latest for the next retry and should return true when the
405+ // latest revision already contains a usable final state and no write is needed.
406+ func updateSharingWithConflictRetry (
407+ inst * instance.Instance ,
408+ s * Sharing ,
409+ retries int ,
410+ reconcile func (latest * Sharing ) bool ,
411+ ) error {
412+ log := inst .Logger ().WithNamespace ("sharing" )
413+ current := s
414+ for attempt := 0 ; ; attempt ++ {
415+ err := couchdb .UpdateDoc (inst , current )
416+ if err == nil {
417+ * s = * current
418+ return nil
419+ }
420+ if ! couchdb .IsConflictError (err ) {
421+ return err
422+ }
423+
424+ latest , findErr := FindSharing (inst , s .SID )
425+ if findErr != nil {
426+ return err
427+ }
428+ if reconcile (latest ) {
429+ * s = * latest
430+ return nil
431+ }
432+ if attempt >= retries {
433+ return err
434+ }
435+ log .Debugf ("Conflict while updating sharing %s, retrying with latest revision (%d/%d)" , s .SID , attempt + 1 , retries )
436+ current = latest
437+ }
438+ }
439+
385440// SendAnswer says to the sharer's Cozy that the sharing has been accepted, and
386441// materialize that by an exchange of credentials.
387442func (s * Sharing ) SendAnswer (inst * instance.Instance , state string ) error {
@@ -509,7 +564,19 @@ func (s *Sharing) SendAnswer(inst *instance.Instance, state string) error {
509564 }
510565 }
511566
512- return couchdb .UpdateDoc (inst , s )
567+ return updateSharingWithConflictRetry (inst , s , 1 , func (latest * Sharing ) bool {
568+ if latest .Active {
569+ return true
570+ }
571+ latest .Members = s .Members
572+ latest .Credentials = s .Credentials
573+ latest .Active = s .Active
574+ latest .Initial = s .Initial
575+ if s .ShortcutID != "" {
576+ latest .ShortcutID = s .ShortcutID
577+ }
578+ return false
579+ })
513580}
514581
515582// ProcessAnswer takes somes credentials and update the sharing with those.
@@ -561,22 +628,20 @@ func (s *Sharing) ProcessAnswer(inst *instance.Instance, creds *APICredentials)
561628 }
562629
563630 s .Active = true
564- if err := couchdb .UpdateDoc (inst , s ); err != nil {
565- if ! couchdb .IsConflictError (err ) {
566- return nil , err
567- }
568- // A conflict can occur when several users accept a sharing at
569- // the same time, and we should just retry in that case
570- s2 , err2 := FindSharing (inst , s .SID )
571- if err2 != nil {
572- return nil , err
573- }
574- s2 .Members [i + 1 ] = s .Members [i + 1 ]
575- s2 .Credentials [i ] = s .Credentials [i ]
576- if err2 := couchdb .UpdateDoc (inst , s2 ); err2 != nil {
577- return nil , err
631+ if err := updateSharingWithConflictRetry (inst , s , 1 , func (latest * Sharing ) bool {
632+ if latest .Members [i + 1 ].Status == MemberStatusReady &&
633+ latest .Credentials [i ].Client != nil &&
634+ latest .Credentials [i ].AccessToken != nil {
635+ ac .Credentials .Client = latest .Credentials [i ].Client
636+ ac .Credentials .AccessToken = latest .Credentials [i ].AccessToken
637+ return true
578638 }
579- s = s2
639+ latest .Members [i + 1 ] = s .Members [i + 1 ]
640+ latest .Credentials [i ] = s .Credentials [i ]
641+ latest .Active = s .Active
642+ return false
643+ }); err != nil {
644+ return nil , err
580645 }
581646 if creds .Bitwarden != nil {
582647 if err := s .SaveBitwarden (inst , & s .Members [i + 1 ], creds .Bitwarden ); err != nil {
0 commit comments