standard-file 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/standard_file.rb +6 -2
- data/lib/standard_file/2016_12_15/sync_manager.rb +143 -0
- data/lib/standard_file/2016_12_15/user_manager.rb +7 -0
- data/lib/standard_file/2019_05_20/sync_manager.rb +161 -0
- data/lib/standard_file/2019_05_20/user_manager.rb +7 -0
- data/lib/standard_file/abstract/sync_manager.rb +61 -0
- data/lib/standard_file/{user_manager.rb → abstract/user_manager.rb} +1 -1
- data/lib/standard_file/version.rb +1 -1
- metadata +8 -4
- data/lib/standard_file/sync_manager.rb +0 -197
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4713fd9c76de2ac82f16a1fe84b27a53a25d4c3a
|
4
|
+
data.tar.gz: c85729b0055a6b4df9038f4804a4442905ed4dc5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6d42f737a016dba90c00cc661170b5b93f21a830664e65a3a2568f778bfd120e2ee344bbb8fa63ebbb9b7eb1a880a320f0685cf042090337c2c13f85535a7da1
|
7
|
+
data.tar.gz: 94fe38ec72e42df859cbae76cb50ac1affd49b131e8fa5e5dc366a4f5134c4003a2e3611663b235d1ae58553c40ee8357d4fb724c315a31863b5c424b806c927
|
data/lib/standard_file.rb
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
require "standard_file/engine"
|
2
|
-
require_relative 'standard_file/sync_manager'
|
3
|
-
require_relative 'standard_file/user_manager'
|
2
|
+
require_relative 'standard_file/abstract/sync_manager'
|
3
|
+
require_relative 'standard_file/abstract/user_manager'
|
4
|
+
require_relative 'standard_file/2016_12_15/sync_manager'
|
5
|
+
require_relative 'standard_file/2019_05_20/sync_manager'
|
6
|
+
require_relative 'standard_file/2016_12_15/user_manager'
|
7
|
+
require_relative 'standard_file/2019_05_20/user_manager'
|
4
8
|
require_relative 'standard_file/jwt_helper'
|
5
9
|
|
6
10
|
module StandardFile
|
@@ -0,0 +1,143 @@
|
|
1
|
+
module StandardFile
|
2
|
+
module V20161215
|
3
|
+
class SyncManager < StandardFile::AbstractSyncManager
|
4
|
+
|
5
|
+
def sync(item_hashes, options, request)
|
6
|
+
in_sync_token = options[:sync_token]
|
7
|
+
in_cursor_token = options[:cursor_token]
|
8
|
+
limit = options[:limit]
|
9
|
+
content_type = options[:content_type] # optional, only return items of these type if present
|
10
|
+
|
11
|
+
retrieved_items, cursor_token = _sync_get(in_sync_token, in_cursor_token, limit, content_type).to_a
|
12
|
+
last_updated = DateTime.now
|
13
|
+
saved_items, unsaved_items = _sync_save(item_hashes, request)
|
14
|
+
if saved_items.length > 0
|
15
|
+
last_updated = saved_items.sort_by{|m| m.updated_at}.last.updated_at
|
16
|
+
end
|
17
|
+
|
18
|
+
check_for_conflicts(saved_items, retrieved_items, unsaved_items)
|
19
|
+
|
20
|
+
# add 1 microsecond to avoid returning same object in subsequent sync
|
21
|
+
last_updated = (last_updated.to_time + 1/100000.0).to_datetime.utc
|
22
|
+
|
23
|
+
sync_token = sync_token_from_datetime(last_updated)
|
24
|
+
return {
|
25
|
+
:retrieved_items => retrieved_items,
|
26
|
+
:saved_items => saved_items,
|
27
|
+
:unsaved => unsaved_items,
|
28
|
+
:sync_token => sync_token,
|
29
|
+
:cursor_token => cursor_token
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def check_for_conflicts(saved_items, retrieved_items, unsaved_items)
|
34
|
+
# conflicts occur when you are trying to save an item for which there is a pending change already
|
35
|
+
min_conflict_interval = 20
|
36
|
+
|
37
|
+
if Rails.env.development?
|
38
|
+
min_conflict_interval = 1
|
39
|
+
end
|
40
|
+
|
41
|
+
saved_ids = saved_items.map{|x| x.uuid }
|
42
|
+
retrieved_ids = retrieved_items.map{|x| x.uuid }
|
43
|
+
conflicts = saved_ids & retrieved_ids # & is the intersection
|
44
|
+
# saved items take precedence, retrieved items are duplicated with a new uuid
|
45
|
+
conflicts.each do |conflicted_uuid|
|
46
|
+
# if changes are greater than min_conflict_interval seconds apart,
|
47
|
+
# push the retrieved item in the unsaved array so that the client can duplicate it
|
48
|
+
saved = saved_items.find{|i| i.uuid == conflicted_uuid}
|
49
|
+
conflicted = retrieved_items.find{|i| i.uuid == conflicted_uuid}
|
50
|
+
if (saved.updated_at - conflicted.updated_at).abs > min_conflict_interval
|
51
|
+
# puts "\n\n\n Creating conflicted copy of #{saved.uuid}\n\n\n"
|
52
|
+
|
53
|
+
unsaved_items.push({
|
54
|
+
:item => conflicted,
|
55
|
+
:error => {:tag => "sync_conflict"}
|
56
|
+
})
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
# We remove the item from retrieved items whether or not it satisfies the min_conflict_interval
|
61
|
+
# This is because the 'saved' value takes precedence, since that's the current value in the database.
|
62
|
+
# So by removing it from retrieved, we are forcing the client to ignore this change.
|
63
|
+
retrieved_items.delete(conflicted)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def _sync_save(item_hashes, request)
|
70
|
+
if !item_hashes
|
71
|
+
return [], []
|
72
|
+
end
|
73
|
+
saved_items = []
|
74
|
+
unsaved = []
|
75
|
+
|
76
|
+
item_hashes.each do |item_hash|
|
77
|
+
begin
|
78
|
+
item = @user.items.find_or_create_by(:uuid => item_hash[:uuid])
|
79
|
+
rescue => error
|
80
|
+
unsaved.push({
|
81
|
+
:item => item_hash,
|
82
|
+
:error => {:message => error.message, :tag => "uuid_conflict"}
|
83
|
+
})
|
84
|
+
next
|
85
|
+
end
|
86
|
+
|
87
|
+
item.last_user_agent = request.user_agent
|
88
|
+
item.update(item_hash.permit(*permitted_params))
|
89
|
+
# we want to force update the updated_at field, even if no changes were made
|
90
|
+
# item.touch
|
91
|
+
|
92
|
+
|
93
|
+
if item.deleted == true
|
94
|
+
set_deleted(item)
|
95
|
+
item.save
|
96
|
+
end
|
97
|
+
|
98
|
+
saved_items.push(item)
|
99
|
+
end
|
100
|
+
|
101
|
+
return saved_items, unsaved
|
102
|
+
end
|
103
|
+
|
104
|
+
def _sync_get(sync_token, input_cursor_token, limit, content_type)
|
105
|
+
cursor_token = nil
|
106
|
+
if limit == nil
|
107
|
+
limit = 100000
|
108
|
+
end
|
109
|
+
|
110
|
+
# if both are present, cursor_token takes precendence as that would eventually return all results
|
111
|
+
# the distinction between getting results for a cursor and a sync token is that cursor results use a
|
112
|
+
# >= comparison, while a sync token uses a > comparison. The reason for this is that cursor tokens are
|
113
|
+
# typically used for initial syncs or imports, where a bunch of notes could have the exact same updated_at
|
114
|
+
# by using >=, we don't miss those results on a subsequent call with a cursor token
|
115
|
+
if input_cursor_token
|
116
|
+
date = datetime_from_sync_token(input_cursor_token)
|
117
|
+
items = @user.items.order(:updated_at).where("updated_at >= ?", date)
|
118
|
+
elsif sync_token
|
119
|
+
date = datetime_from_sync_token(sync_token)
|
120
|
+
items = @user.items.order(:updated_at).where("updated_at > ?", date)
|
121
|
+
else
|
122
|
+
# if no cursor token and no sync token, this is an initial sync. No need to return deleted items.
|
123
|
+
items = @user.items.order(:updated_at).where(:deleted => false)
|
124
|
+
end
|
125
|
+
|
126
|
+
if content_type
|
127
|
+
items = items.where(:content_type => content_type)
|
128
|
+
end
|
129
|
+
|
130
|
+
items = items.sort_by{|m| m.updated_at}
|
131
|
+
|
132
|
+
if items.count > limit
|
133
|
+
items = items.slice(0, limit)
|
134
|
+
date = items.last.updated_at
|
135
|
+
cursor_token = sync_token_from_datetime(date)
|
136
|
+
end
|
137
|
+
|
138
|
+
return items, cursor_token
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
module StandardFile
|
2
|
+
module V20190520
|
3
|
+
class SyncManager < StandardFile::AbstractSyncManager
|
4
|
+
|
5
|
+
def sync(item_hashes, options, request)
|
6
|
+
in_sync_token = options[:sync_token]
|
7
|
+
in_cursor_token = options[:cursor_token]
|
8
|
+
limit = options[:limit]
|
9
|
+
content_type = options[:content_type] # optional, only return items of these type if present
|
10
|
+
|
11
|
+
retrieved_items, cursor_token = _sync_get(in_sync_token, in_cursor_token, limit, content_type).to_a
|
12
|
+
last_updated = DateTime.now
|
13
|
+
saved_items, conflicts = _sync_save(item_hashes, request, retrieved_items)
|
14
|
+
|
15
|
+
if saved_items.length > 0
|
16
|
+
last_updated = saved_items.sort_by{|m| m.updated_at}.last.updated_at
|
17
|
+
end
|
18
|
+
|
19
|
+
# add 1 microsecond to avoid returning same object in subsequent sync
|
20
|
+
last_updated = (last_updated.to_time + 1/100000.0).to_datetime.utc
|
21
|
+
sync_token = sync_token_from_datetime(last_updated)
|
22
|
+
|
23
|
+
return {
|
24
|
+
:retrieved_items => retrieved_items,
|
25
|
+
:saved_items => saved_items,
|
26
|
+
:conflicts => conflicts,
|
27
|
+
:sync_token => sync_token,
|
28
|
+
:cursor_token => cursor_token
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
# Ignore differences that are at most this many seconds apart
|
34
|
+
# Anything over this threshold will be conflicted.
|
35
|
+
MIN_CONFLICT_INTERVAL = 1.0
|
36
|
+
|
37
|
+
def _sync_save(item_hashes, request, retrieved_items)
|
38
|
+
if !item_hashes
|
39
|
+
return [], []
|
40
|
+
end
|
41
|
+
|
42
|
+
saved_items = []
|
43
|
+
conflicts = []
|
44
|
+
|
45
|
+
item_hashes.each do |item_hash|
|
46
|
+
is_new_record = false
|
47
|
+
begin
|
48
|
+
item = @user.items.find_or_create_by(:uuid => item_hash[:uuid]) do |created_item|
|
49
|
+
# this block is executed if this is a new record.
|
50
|
+
is_new_record = true
|
51
|
+
end
|
52
|
+
rescue => error
|
53
|
+
conflicts.push({
|
54
|
+
:unsaved_item => item_hash,
|
55
|
+
:type => "uuid_conflict"
|
56
|
+
})
|
57
|
+
next
|
58
|
+
end
|
59
|
+
|
60
|
+
# SFJS did not send updated_at prior to 0.3.59.
|
61
|
+
# updated_at value from client will not be saved, as it is not a permitted_param.
|
62
|
+
if item_hash['updated_at']
|
63
|
+
incoming_updated_at = DateTime.parse(item_hash['updated_at'])
|
64
|
+
else
|
65
|
+
# Default to epoch
|
66
|
+
incoming_updated_at = Time.at(0).to_datetime
|
67
|
+
end
|
68
|
+
|
69
|
+
if !is_new_record
|
70
|
+
# We want to check if this updated_at value is equal to the item's current updated_at value.
|
71
|
+
# If they differ, it means the client is attempting to save an item which hasn't been updated.
|
72
|
+
# In this case, if the incoming_item.updated_at < server_item.updated_at, always conflict.
|
73
|
+
# We don't want old items overriding newer ones.
|
74
|
+
# incoming_item.updated_at > server_item.updated_at would seem to be impossible, as only servers are responsible for setting updated_at.
|
75
|
+
# But assuming a rogue client has gotten away with it,
|
76
|
+
# we should also conflict in this case if the difference between the dates is greater than MIN_CONFLICT_INTERVAL seconds.
|
77
|
+
|
78
|
+
save_incoming = true
|
79
|
+
|
80
|
+
our_updated_at = item.updated_at
|
81
|
+
difference = incoming_updated_at.to_f - our_updated_at.to_f
|
82
|
+
|
83
|
+
if difference < 0
|
84
|
+
# incoming is less than ours. This implies stale data. Don't save if greater than interval
|
85
|
+
save_incoming = difference.abs < MIN_CONFLICT_INTERVAL
|
86
|
+
elsif difference > 0
|
87
|
+
# incoming is greater than ours. Should never be the case. If so though, don't save.
|
88
|
+
save_incoming = difference.abs < MIN_CONFLICT_INTERVAL
|
89
|
+
else
|
90
|
+
# incoming is equal to ours (which is desired, healthy behavior), continue with saving.
|
91
|
+
save_incoming = true
|
92
|
+
end
|
93
|
+
|
94
|
+
if !save_incoming
|
95
|
+
# Dont save incoming and send it back. At this point the server item is likely to be included
|
96
|
+
# in retrieved_items in a subsequent sync, so when that value comes into the client,
|
97
|
+
server_value = item.as_json({})
|
98
|
+
conflicts.push({
|
99
|
+
:server_item => server_value, # as_json to get values as-is, befor modifying below,
|
100
|
+
:type => "sync_conflict"
|
101
|
+
})
|
102
|
+
|
103
|
+
retrieved_items.delete(item)
|
104
|
+
next
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
item.last_user_agent = request.user_agent
|
109
|
+
item.update(item_hash.permit(*permitted_params))
|
110
|
+
|
111
|
+
if item.deleted == true
|
112
|
+
set_deleted(item)
|
113
|
+
item.save
|
114
|
+
end
|
115
|
+
|
116
|
+
saved_items.push(item)
|
117
|
+
end
|
118
|
+
|
119
|
+
return saved_items, conflicts
|
120
|
+
end
|
121
|
+
|
122
|
+
def _sync_get(sync_token, input_cursor_token, limit, content_type)
|
123
|
+
cursor_token = nil
|
124
|
+
if limit == nil
|
125
|
+
limit = 100000
|
126
|
+
end
|
127
|
+
|
128
|
+
# if both are present, cursor_token takes precendence as that would eventually return all results
|
129
|
+
# the distinction between getting results for a cursor and a sync token is that cursor results use a
|
130
|
+
# >= comparison, while a sync token uses a > comparison. The reason for this is that cursor tokens are
|
131
|
+
# typically used for initial syncs or imports, where a bunch of notes could have the exact same updated_at
|
132
|
+
# by using >=, we don't miss those results on a subsequent call with a cursor token
|
133
|
+
if input_cursor_token
|
134
|
+
date = datetime_from_sync_token(input_cursor_token)
|
135
|
+
items = @user.items.order(:updated_at).where("updated_at >= ?", date)
|
136
|
+
elsif sync_token
|
137
|
+
date = datetime_from_sync_token(sync_token)
|
138
|
+
items = @user.items.order(:updated_at).where("updated_at > ?", date)
|
139
|
+
else
|
140
|
+
# if no cursor token and no sync token, this is an initial sync. No need to return deleted items.
|
141
|
+
items = @user.items.order(:updated_at).where(:deleted => false)
|
142
|
+
end
|
143
|
+
|
144
|
+
if content_type
|
145
|
+
items = items.where(:content_type => content_type)
|
146
|
+
end
|
147
|
+
|
148
|
+
items = items.sort_by{|m| m.updated_at}
|
149
|
+
|
150
|
+
if items.count > limit
|
151
|
+
items = items.slice(0, limit)
|
152
|
+
date = items.last.updated_at
|
153
|
+
cursor_token = sync_token_from_datetime(date)
|
154
|
+
end
|
155
|
+
|
156
|
+
return items, cursor_token
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module StandardFile
|
2
|
+
class AbstractSyncManager
|
3
|
+
|
4
|
+
attr_accessor :sync_fields
|
5
|
+
|
6
|
+
def initialize(user)
|
7
|
+
@user = user
|
8
|
+
raise "User must be set" unless @user
|
9
|
+
end
|
10
|
+
|
11
|
+
def set_sync_fields(val)
|
12
|
+
@sync_fields = val
|
13
|
+
end
|
14
|
+
|
15
|
+
def sync_fields
|
16
|
+
return @sync_fields || [:content, :enc_item_key, :content_type, :auth_hash, :deleted, :created_at]
|
17
|
+
end
|
18
|
+
|
19
|
+
def destroy_items(uuids)
|
20
|
+
items = @user.items.where(uuid: uuids)
|
21
|
+
items.destroy_all
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def sync_token_from_datetime(datetime)
|
27
|
+
version = 2
|
28
|
+
Base64.encode64("#{version}:" + "#{datetime.to_f}")
|
29
|
+
end
|
30
|
+
|
31
|
+
def datetime_from_sync_token(sync_token)
|
32
|
+
decoded = Base64.decode64(sync_token)
|
33
|
+
parts = decoded.rpartition(":")
|
34
|
+
timestamp_string = parts.last
|
35
|
+
version = parts.first
|
36
|
+
if version == "1"
|
37
|
+
date = DateTime.strptime(timestamp_string,'%s')
|
38
|
+
elsif version == "2"
|
39
|
+
date = Time.at(timestamp_string.to_f).to_datetime.utc
|
40
|
+
end
|
41
|
+
|
42
|
+
return date
|
43
|
+
end
|
44
|
+
|
45
|
+
def set_deleted(item)
|
46
|
+
item.deleted = true
|
47
|
+
item.content = nil if item.has_attribute?(:content)
|
48
|
+
item.enc_item_key = nil if item.has_attribute?(:enc_item_key)
|
49
|
+
item.auth_hash = nil if item.has_attribute?(:auth_hash)
|
50
|
+
end
|
51
|
+
|
52
|
+
def item_params
|
53
|
+
params.permit(*permitted_params)
|
54
|
+
end
|
55
|
+
|
56
|
+
def permitted_params
|
57
|
+
sync_fields
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: standard-file
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Standard File
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-05-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -64,10 +64,14 @@ files:
|
|
64
64
|
- README.md
|
65
65
|
- Rakefile
|
66
66
|
- lib/standard_file.rb
|
67
|
+
- lib/standard_file/2016_12_15/sync_manager.rb
|
68
|
+
- lib/standard_file/2016_12_15/user_manager.rb
|
69
|
+
- lib/standard_file/2019_05_20/sync_manager.rb
|
70
|
+
- lib/standard_file/2019_05_20/user_manager.rb
|
71
|
+
- lib/standard_file/abstract/sync_manager.rb
|
72
|
+
- lib/standard_file/abstract/user_manager.rb
|
67
73
|
- lib/standard_file/engine.rb
|
68
74
|
- lib/standard_file/jwt_helper.rb
|
69
|
-
- lib/standard_file/sync_manager.rb
|
70
|
-
- lib/standard_file/user_manager.rb
|
71
75
|
- lib/standard_file/version.rb
|
72
76
|
- lib/tasks/standard_file_tasks.rake
|
73
77
|
homepage: https://standardnotes.org
|
@@ -1,197 +0,0 @@
|
|
1
|
-
module StandardFile
|
2
|
-
class SyncManager
|
3
|
-
|
4
|
-
attr_accessor :sync_fields
|
5
|
-
|
6
|
-
def initialize(user)
|
7
|
-
@user = user
|
8
|
-
raise "User must be set" unless @user
|
9
|
-
end
|
10
|
-
|
11
|
-
def set_sync_fields(val)
|
12
|
-
@sync_fields = val
|
13
|
-
end
|
14
|
-
|
15
|
-
def sync_fields
|
16
|
-
return @sync_fields || [:content, :enc_item_key, :content_type, :auth_hash, :deleted, :created_at]
|
17
|
-
end
|
18
|
-
|
19
|
-
def sync(item_hashes, options, request)
|
20
|
-
|
21
|
-
in_sync_token = options[:sync_token]
|
22
|
-
in_cursor_token = options[:cursor_token]
|
23
|
-
limit = options[:limit]
|
24
|
-
content_type = options[:content_type] # optional, only return items of these type if present
|
25
|
-
|
26
|
-
retrieved_items, cursor_token = _sync_get(in_sync_token, in_cursor_token, limit, content_type).to_a
|
27
|
-
last_updated = DateTime.now
|
28
|
-
saved_items, unsaved_items = _sync_save(item_hashes, request)
|
29
|
-
if saved_items.length > 0
|
30
|
-
last_updated = saved_items.sort_by{|m| m.updated_at}.last.updated_at
|
31
|
-
end
|
32
|
-
|
33
|
-
check_for_conflicts(saved_items, retrieved_items, unsaved_items)
|
34
|
-
|
35
|
-
# add 1 microsecond to avoid returning same object in subsequent sync
|
36
|
-
last_updated = (last_updated.to_time + 1/100000.0).to_datetime.utc
|
37
|
-
|
38
|
-
sync_token = sync_token_from_datetime(last_updated)
|
39
|
-
return {
|
40
|
-
:retrieved_items => retrieved_items,
|
41
|
-
:saved_items => saved_items,
|
42
|
-
:unsaved => unsaved_items,
|
43
|
-
:sync_token => sync_token,
|
44
|
-
:cursor_token => cursor_token
|
45
|
-
}
|
46
|
-
end
|
47
|
-
|
48
|
-
def check_for_conflicts(saved_items, retrieved_items, unsaved_items)
|
49
|
-
# conflicts occur when you are trying to save an item for which there is a pending change already
|
50
|
-
min_conflict_interval = 20
|
51
|
-
|
52
|
-
if Rails.env.development?
|
53
|
-
min_conflict_interval = 1
|
54
|
-
end
|
55
|
-
|
56
|
-
saved_ids = saved_items.map{|x| x.uuid }
|
57
|
-
retrieved_ids = retrieved_items.map{|x| x.uuid }
|
58
|
-
conflicts = saved_ids & retrieved_ids # & is the intersection
|
59
|
-
# saved items take precedence, retrieved items are duplicated with a new uuid
|
60
|
-
conflicts.each do |conflicted_uuid|
|
61
|
-
# if changes are greater than min_conflict_interval seconds apart,
|
62
|
-
# push the retrieved item in the unsaved array so that the client can duplicate it
|
63
|
-
saved = saved_items.find{|i| i.uuid == conflicted_uuid}
|
64
|
-
conflicted = retrieved_items.find{|i| i.uuid == conflicted_uuid}
|
65
|
-
if (saved.updated_at - conflicted.updated_at).abs > min_conflict_interval
|
66
|
-
# puts "\n\n\n Creating conflicted copy of #{saved.uuid}\n\n\n"
|
67
|
-
|
68
|
-
unsaved_items.push({
|
69
|
-
:item => conflicted,
|
70
|
-
:error => {:tag => "sync_conflict"}
|
71
|
-
})
|
72
|
-
|
73
|
-
end
|
74
|
-
|
75
|
-
# We remove the item from retrieved items whether or not it satisfies the min_conflict_interval
|
76
|
-
# This is because the 'saved' value takes precedence, since that's the current value in the database.
|
77
|
-
# So by removing it from retrieved, we are forcing the client to ignore this change.
|
78
|
-
retrieved_items.delete(conflicted)
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
def destroy_items(uuids)
|
83
|
-
items = @user.items.where(uuid: uuids)
|
84
|
-
items.destroy_all
|
85
|
-
end
|
86
|
-
|
87
|
-
|
88
|
-
private
|
89
|
-
|
90
|
-
def sync_token_from_datetime(datetime)
|
91
|
-
version = 2
|
92
|
-
Base64.encode64("#{version}:" + "#{datetime.to_f}")
|
93
|
-
end
|
94
|
-
|
95
|
-
def datetime_from_sync_token(sync_token)
|
96
|
-
decoded = Base64.decode64(sync_token)
|
97
|
-
parts = decoded.rpartition(":")
|
98
|
-
timestamp_string = parts.last
|
99
|
-
version = parts.first
|
100
|
-
if version == "1"
|
101
|
-
date = DateTime.strptime(timestamp_string,'%s')
|
102
|
-
elsif version == "2"
|
103
|
-
date = Time.at(timestamp_string.to_f).to_datetime.utc
|
104
|
-
end
|
105
|
-
|
106
|
-
return date
|
107
|
-
end
|
108
|
-
|
109
|
-
def _sync_save(item_hashes, request)
|
110
|
-
if !item_hashes
|
111
|
-
return [], []
|
112
|
-
end
|
113
|
-
saved_items = []
|
114
|
-
unsaved = []
|
115
|
-
|
116
|
-
item_hashes.each do |item_hash|
|
117
|
-
begin
|
118
|
-
item = @user.items.find_or_create_by(:uuid => item_hash[:uuid])
|
119
|
-
rescue => error
|
120
|
-
unsaved.push({
|
121
|
-
:item => item_hash,
|
122
|
-
:error => {:message => error.message, :tag => "uuid_conflict"}
|
123
|
-
})
|
124
|
-
next
|
125
|
-
end
|
126
|
-
|
127
|
-
item.last_user_agent = request.user_agent
|
128
|
-
item.update(item_hash.permit(*permitted_params))
|
129
|
-
# we want to force update the updated_at field, even if no changes were made
|
130
|
-
# item.touch
|
131
|
-
|
132
|
-
|
133
|
-
if item.deleted == true
|
134
|
-
set_deleted(item)
|
135
|
-
item.save
|
136
|
-
end
|
137
|
-
|
138
|
-
saved_items.push(item)
|
139
|
-
end
|
140
|
-
|
141
|
-
return saved_items, unsaved
|
142
|
-
end
|
143
|
-
|
144
|
-
def _sync_get(sync_token, input_cursor_token, limit, content_type)
|
145
|
-
cursor_token = nil
|
146
|
-
if limit == nil
|
147
|
-
limit = 100000
|
148
|
-
end
|
149
|
-
|
150
|
-
# if both are present, cursor_token takes precendence as that would eventually return all results
|
151
|
-
# the distinction between getting results for a cursor and a sync token is that cursor results use a
|
152
|
-
# >= comparison, while a sync token uses a > comparison. The reason for this is that cursor tokens are
|
153
|
-
# typically used for initial syncs or imports, where a bunch of notes could have the exact same updated_at
|
154
|
-
# by using >=, we don't miss those results on a subsequent call with a cursor token
|
155
|
-
if input_cursor_token
|
156
|
-
date = datetime_from_sync_token(input_cursor_token)
|
157
|
-
items = @user.items.order(:updated_at).where("updated_at >= ?", date)
|
158
|
-
elsif sync_token
|
159
|
-
date = datetime_from_sync_token(sync_token)
|
160
|
-
items = @user.items.order(:updated_at).where("updated_at > ?", date)
|
161
|
-
else
|
162
|
-
# if no cursor token and no sync token, this is an initial sync. No need to return deleted items.
|
163
|
-
items = @user.items.order(:updated_at).where(:deleted => false)
|
164
|
-
end
|
165
|
-
|
166
|
-
if content_type
|
167
|
-
items = items.where(:content_type => content_type)
|
168
|
-
end
|
169
|
-
|
170
|
-
items = items.sort_by{|m| m.updated_at}
|
171
|
-
|
172
|
-
if items.count > limit
|
173
|
-
items = items.slice(0, limit)
|
174
|
-
date = items.last.updated_at
|
175
|
-
cursor_token = sync_token_from_datetime(date)
|
176
|
-
end
|
177
|
-
|
178
|
-
return items, cursor_token
|
179
|
-
end
|
180
|
-
|
181
|
-
def set_deleted(item)
|
182
|
-
item.deleted = true
|
183
|
-
item.content = nil if item.has_attribute?(:content)
|
184
|
-
item.enc_item_key = nil if item.has_attribute?(:enc_item_key)
|
185
|
-
item.auth_hash = nil if item.has_attribute?(:auth_hash)
|
186
|
-
end
|
187
|
-
|
188
|
-
def item_params
|
189
|
-
params.permit(*permitted_params)
|
190
|
-
end
|
191
|
-
|
192
|
-
def permitted_params
|
193
|
-
sync_fields
|
194
|
-
end
|
195
|
-
|
196
|
-
end
|
197
|
-
end
|