Skip to content

Commit aa503c8

Browse files
authored
Merge pull request #1480 from nono/hint-for-initial-files-sync
Add informations about the number of files for initial sync
2 parents 370944c + 12f4b8c commit aa503c8

File tree

12 files changed

+251
-10
lines changed

12 files changed

+251
-10
lines changed

docs/realtime.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,11 +156,11 @@ Then messages are sent using json
156156

157157
```
158158
client > {"method": "AUTH",
159-
"payload": "xxAppOrAuthTokenxx="
159+
"payload": "xxAppOrAuthTokenxx="}
160160
client > {"method": "SUBSCRIBE",
161-
"payload": {"type": "io.cozy.files"}
161+
"payload": {"type": "io.cozy.files"}}
162162
client > {"method": "SUBSCRIBE",
163-
"payload": {"type": "io.cozy.contacts"}
163+
"payload": {"type": "io.cozy.contacts"}}
164164
server > {"event": "UPDATED",
165165
"payload": {"id": "idA", "rev": "2-705...", "type": "io.cozy.contacts", "doc": {embeded doc ...}}}
166166
server > {"event": "DELETED",

docs/sharing-design.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,9 @@ revision (4-4aa). The resolution takes 4 steps:
307307
* `false` if only the owner can add a new recipient
308308
* Some technical data (`created_at`, `updated_at`, `app_slug`, `preview_path`,
309309
`triggers`, `credentials`)
310+
* a number of files to synchronize for the initial sync,
311+
`initial_number_of_files_to_sync` (if there are no files to sync or the
312+
initial replication has finished, the field won't be here)
310313
* A list of sharing `rules`, each rule being composed of:
311314
* a `title`, that will be displayed to the recipients before they accept the
312315
sharing

docs/sharing.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,7 @@ Content-Type: application/vnd.api+json
296296
"owner": true,
297297
"created_at": "2018-01-04T12:35:08Z",
298298
"updated_at": "2018-01-04T13:45:43Z",
299+
"initial_number_of_files_to_sync": 42,
299300
"members": [
300301
{
301302
"status": "owner",
@@ -1267,3 +1268,43 @@ Authorization: Bearer ...
12671268
```http
12681269
HTTP/1.1 204 No Content
12691270
```
1271+
1272+
### DELETE /sharings/:sharing-id/initial
1273+
1274+
This internal route is used by the sharer to inform a recipient's cozy that
1275+
the initial sync is finished.
1276+
1277+
```http
1278+
DELETE /sharings/ce8835a061d0ef68947afe69a0046722/initial HTTP/1.1
1279+
Host: bob.example.net
1280+
Authorization: Bearer ...
1281+
```
1282+
1283+
#### Response
1284+
1285+
```http
1286+
HTTP/1.1 204 No Content
1287+
```
1288+
1289+
## Real-time via websockets
1290+
1291+
You can subscribe to the [realtime](realtime.md) API for the normal doctypes,
1292+
but also for a special `io.cozy.sharings.initial-sync` doctype. For this
1293+
doctype, you can give the id of a sharing and you will be notified when a file
1294+
will be received during the initial synchronisation (`UPDATED`), and when the
1295+
sync will be done (`DELETED`).
1296+
1297+
### Example
1298+
1299+
```
1300+
client > {"method": "AUTH",
1301+
"payload": "xxAppOrAuthTokenxx="}
1302+
client > {"method": "SUBSCRIBE",
1303+
"payload": {"type": "io.cozy.sharings.initial-sync", "id": "ce8835a061d0ef68947afe69a0046722"}
1304+
server > {"event": "UPDATED",
1305+
"payload": {"id": "ce8835a061d0ef68947afe69a0046722", "type": "io.cozy.sharings.initial-sync", "doc": {"count": 12}}}
1306+
server > {"event": "UPDATED",
1307+
"payload": {"id": "ce8835a061d0ef68947afe69a0046722", "type": "io.cozy.sharings.initial-sync", "doc": {"count": 13}}}
1308+
server > {"event": "DELETED",
1309+
"payload": {"id": "ce8835a061d0ef68947afe69a0046722", "type": "io.cozy.sharings.initial-sync"}}
1310+
```

pkg/consts/consts.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ const (
5454
Sharings = "io.cozy.sharings"
5555
// SharingsAnswer doc type for credentials exchange for sharings
5656
SharingsAnswer = "io.cozy.sharings.answer"
57+
// SharingsInitialSync doc type for real-time events for initial sync of a
58+
// sharing
59+
SharingsInitialSync = "io.cozy.sharings.initial-sync"
5760
// Triggers doc type for triggers, jobs launchers
5861
Triggers = "io.cozy.triggers"
5962
// TriggersState doc type for triggers current state, jobs launchers

pkg/sharing/oauth.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/cozy/cozy-stack/pkg/instance"
1616
"github.com/cozy/cozy-stack/pkg/oauth"
1717
"github.com/cozy/cozy-stack/pkg/permissions"
18+
"github.com/cozy/cozy-stack/pkg/vfs"
1819
"github.com/cozy/cozy-stack/web/jsonapi"
1920
)
2021

@@ -61,6 +62,7 @@ func (m *Member) CreateSharingRequest(inst *instance.Instance, s *Sharing, c *Cr
6162
UpdatedAt: s.UpdatedAt,
6263
Rules: rules,
6364
Members: members,
65+
NbFiles: s.countFiles(inst),
6466
},
6567
nil,
6668
nil,
@@ -112,6 +114,40 @@ func clearAppInHost(host string) string {
112114
return parts[0] + "." + domain
113115
}
114116

117+
// countFiles returns the number of files that should be uploaded on the
118+
// initial synchronisation.
119+
func (s *Sharing) countFiles(inst *instance.Instance) int {
120+
count := 0
121+
for _, rule := range s.Rules {
122+
if rule.DocType != consts.Files || rule.Local || len(rule.Values) == 0 {
123+
continue
124+
}
125+
if rule.Selector == "" || rule.Selector == "id" {
126+
for _, fileID := range rule.Values {
127+
vfs.WalkByID(inst.VFS(), fileID, func(name string, dir *vfs.DirDoc, file *vfs.FileDoc, err error) error {
128+
if err != nil {
129+
return err
130+
}
131+
if file != nil {
132+
count++
133+
}
134+
return nil
135+
})
136+
}
137+
} else {
138+
var resCount couchdb.ViewResponse
139+
for _, val := range rule.Values {
140+
reqCount := &couchdb.ViewRequest{Key: val, Reduce: true}
141+
err := couchdb.ExecView(inst, consts.FilesReferencedByView, reqCount, &resCount)
142+
if err == nil && len(resCount.Rows) > 0 {
143+
count += int(resCount.Rows[0].Value.(float64))
144+
}
145+
}
146+
}
147+
}
148+
return count
149+
}
150+
115151
// RegisterCozyURL saves a new Cozy URL for a member
116152
func (s *Sharing) RegisterCozyURL(inst *instance.Instance, m *Member, cozyURL string) error {
117153
if !s.Owner {

pkg/sharing/sharing.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/cozy/cozy-stack/pkg/jobs"
1515
"github.com/cozy/cozy-stack/pkg/permissions"
1616
"github.com/cozy/cozy-stack/pkg/prefixer"
17+
"github.com/cozy/cozy-stack/pkg/realtime"
1718
multierror "github.com/hashicorp/go-multierror"
1819
)
1920

@@ -43,6 +44,7 @@ type Sharing struct {
4344
PreviewPath string `json:"preview_path,omitempty"`
4445
CreatedAt time.Time `json:"created_at"`
4546
UpdatedAt time.Time `json:"updated_at"`
47+
NbFiles int `json:"initial_number_of_files_to_sync,omitempty"`
4648

4749
Rules []Rule `json:"rules"`
4850

@@ -514,4 +516,21 @@ func (s *Sharing) RedirectAfterAuthorizeURL(inst *instance.Instance) *url.URL {
514516
return inst.SubDomain(app.Slug())
515517
}
516518

519+
// EndInitial is used to finish the initial sync phase of a sharing
520+
func (s *Sharing) EndInitial(inst *instance.Instance) error {
521+
if s.NbFiles == 0 {
522+
return nil
523+
}
524+
s.NbFiles = 0
525+
if err := couchdb.UpdateDoc(inst, s); err != nil {
526+
return err
527+
}
528+
doc := couchdb.JSONDoc{
529+
Type: consts.SharingsInitialSync,
530+
M: map[string]interface{}{"_id": s.SID},
531+
}
532+
realtime.GetHub().Publish(inst, realtime.EventDelete, doc, nil)
533+
return nil
534+
}
535+
517536
var _ couchdb.Doc = &Sharing{}

pkg/sharing/upload.go

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package sharing
33
import (
44
"bytes"
55
"encoding/json"
6+
"fmt"
67
"io"
78
"net/http"
89
"net/url"
@@ -14,6 +15,7 @@ import (
1415
"github.com/cozy/cozy-stack/pkg/couchdb"
1516
"github.com/cozy/cozy-stack/pkg/instance"
1617
"github.com/cozy/cozy-stack/pkg/lock"
18+
"github.com/cozy/cozy-stack/pkg/realtime"
1719
"github.com/cozy/cozy-stack/pkg/vfs"
1820
multierror "github.com/hashicorp/go-multierror"
1921
)
@@ -81,14 +83,45 @@ func (s *Sharing) InitialUpload(inst *instance.Instance, m *Member) error {
8183
return err
8284
}
8385
if !more {
84-
return nil
86+
return s.sendInitialEndNotif(inst, m)
8587
}
8688
}
8789

8890
s.pushJob(inst, "share-upload")
8991
return nil
9092
}
9193

94+
// sendInitialEndNotif sends a notification to the recipient that the initial
95+
// sync is finished
96+
func (s *Sharing) sendInitialEndNotif(inst *instance.Instance, m *Member) error {
97+
u, err := url.Parse(m.Instance)
98+
if err != nil {
99+
return err
100+
}
101+
c := s.FindCredentials(m)
102+
if c == nil {
103+
return ErrInvalidSharing
104+
}
105+
opts := &request.Options{
106+
Method: http.MethodDelete,
107+
Scheme: u.Scheme,
108+
Domain: u.Host,
109+
Path: fmt.Sprintf("/sharings/%s/initial", s.SID),
110+
Headers: request.Headers{
111+
"Authorization": "Bearer " + c.AccessToken.AccessToken,
112+
},
113+
}
114+
res, err := request.Req(opts)
115+
if err != nil {
116+
return err
117+
}
118+
res.Body.Close()
119+
if res.StatusCode/100 != 2 {
120+
return ErrInternalServerError
121+
}
122+
return nil
123+
}
124+
92125
// UploadTo uploads one file to the given member. It returns false if there
93126
// are no more files to upload to this member currently.
94127
func (s *Sharing) UploadTo(inst *instance.Instance, m *Member) (bool, error) {
@@ -517,9 +550,52 @@ func (s *Sharing) UploadNewFile(inst *instance.Instance, target *FileDocWithRevi
517550
Debugf("Cannot create file: %s", err)
518551
return err
519552
}
553+
if s.NbFiles > 0 {
554+
defer s.countReceivedFiles(inst)
555+
}
520556
return copyFileContent(inst, file, body)
521557
}
522558

559+
// countReceivedFiles counts the number of files received during the initial
560+
// sync, and pushs an event to the real-time system with this count
561+
func (s *Sharing) countReceivedFiles(inst *instance.Instance) {
562+
count := 0
563+
var req = &couchdb.ViewRequest{
564+
Key: s.SID,
565+
IncludeDocs: true,
566+
}
567+
var res couchdb.ViewResponse
568+
err := couchdb.ExecView(inst, consts.SharedDocsBySharingID, req, &res)
569+
if err == nil {
570+
for _, row := range res.Rows {
571+
var doc SharedRef
572+
if err = json.Unmarshal(row.Doc, &doc); err != nil {
573+
continue
574+
}
575+
if doc.Infos[s.SID].Binary {
576+
count++
577+
}
578+
}
579+
}
580+
581+
if count >= s.NbFiles {
582+
if err = s.EndInitial(inst); err != nil {
583+
inst.Logger().WithField("nspace", "sharing").
584+
Errorf("Can't save sharing %v: %s", s, err)
585+
}
586+
return
587+
}
588+
589+
doc := couchdb.JSONDoc{
590+
Type: consts.SharingsInitialSync,
591+
M: map[string]interface{}{
592+
"_id": s.SID,
593+
"count": count,
594+
},
595+
}
596+
realtime.GetHub().Publish(inst, realtime.EventUpdate, doc, nil)
597+
}
598+
523599
// UploadExistingFile is used to receive new content for an existing file.
524600
//
525601
// Note: if file was renamed + its content has changed, we modify the content

tests/sharing/Gemfile

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@ source 'https://rubygems.org'
22

33
gem "awesome_print"
44
gem "faker"
5+
gem "faye-websocket"
56
gem "mimemagic"
67
gem "minitest"
78
gem "pry"
89
gem "pry-rescue"
10+
gem "pry-stack_explorer"
911
gem "rest-client"
10-
11-
# Added at 2018-05-21 09:30:04 +0200 by nono:
12-
gem "pry-stack_explorer", "~> 0.4.9"

tests/sharing/Gemfile.lock

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@ GEM
99
debug_inspector (0.0.3)
1010
domain_name (0.5.20170404)
1111
unf (>= 0.0.5, < 1.0.0)
12+
eventmachine (1.2.7)
1213
faker (1.8.7)
1314
i18n (>= 0.7)
15+
faye-websocket (0.10.7)
16+
eventmachine (>= 0.12.0)
17+
websocket-driver (>= 0.5.1)
1418
http-cookie (1.0.3)
1519
domain_name (~> 0.5)
1620
i18n (1.0.0)
@@ -39,18 +43,22 @@ GEM
3943
unf (0.1.4)
4044
unf_ext
4145
unf_ext (0.0.7.5)
46+
websocket-driver (0.7.0)
47+
websocket-extensions (>= 0.1.0)
48+
websocket-extensions (0.1.3)
4249

4350
PLATFORMS
4451
ruby
4552

4653
DEPENDENCIES
4754
awesome_print
4855
faker
56+
faye-websocket
4957
mimemagic
5058
minitest
5159
pry
5260
pry-rescue
53-
pry-stack_explorer (~> 0.4.9)
61+
pry-stack_explorer
5462
rest-client
5563

5664
BUNDLED WITH

0 commit comments

Comments
 (0)