Module: Timet::DatabaseSyncer

Included in:
DatabaseSyncHelper
Defined in:
lib/timet/database_syncer.rb

Overview

Module responsible for synchronizing local and remote databases

Constant Summary collapse

ITEM_FIELDS =
%w[start end tag notes pomodoro updated_at created_at deleted].freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.extract_timestamp(item) ⇒ Object



173
174
175
# File 'lib/timet/database_syncer.rb', line 173

def extract_timestamp(item)
  item['updated_at'].to_i
end

.format_status_message(id, item, source) ⇒ Object



186
187
188
189
# File 'lib/timet/database_syncer.rb', line 186

def format_status_message(id, item, source)
  deleted = item['deleted'].to_i == 1 ? ' and deleted' : ''
  "#{source} item #{id} is newer#{deleted} - #{source == 'Remote' ? 'updating local' : 'will be uploaded'}"
end

.log_local_only(id) ⇒ Object



119
120
121
# File 'lib/timet/database_syncer.rb', line 119

def log_local_only(id)
  puts "Local item #{id} will be uploaded"
end

.report_sync_error(error) ⇒ Object



30
31
32
33
# File 'lib/timet/database_syncer.rb', line 30

def report_sync_error(error)
  puts "Error opening remote database: #{error.message}"
  puts 'Uploading local database to replace corrupted remote database'
end

.upload_local_database(remote_storage, bucket, local_db_path) ⇒ Object



36
37
38
# File 'lib/timet/database_syncer.rb', line 36

def upload_local_database(remote_storage, bucket, local_db_path)
  remote_storage.upload_file(bucket, local_db_path, 'timet.db')
end

Instance Method Details

#add_remote_item(local_db, id, remote_item) ⇒ Object



124
125
126
127
# File 'lib/timet/database_syncer.rb', line 124

def add_remote_item(local_db, id, remote_item)
  puts "Adding remote item #{id} to local"
  insert_item_from_hash(local_db, remote_item)
end

#get_insert_values(item) ⇒ Object



192
193
194
195
196
# File 'lib/timet/database_syncer.rb', line 192

def get_insert_values(item)
  @database_fields ||= ITEM_FIELDS
  values = @database_fields.map { |field| item[field] }
  [item['id'], *values]
end

#get_item_values(item, include_id_at_start: false) ⇒ Object



203
204
205
# File 'lib/timet/database_syncer.rb', line 203

def get_item_values(item, include_id_at_start: false)
  include_id_at_start ? get_insert_values(item) : get_update_values(item)
end

#get_update_values(item) ⇒ Object



198
199
200
201
# File 'lib/timet/database_syncer.rb', line 198

def get_update_values(item)
  @database_fields ||= ITEM_FIELDS
  @database_fields.map { |field| item[field] }
end

#handle_database_differences(*args) ⇒ Object



8
9
10
11
12
13
14
15
16
# File 'lib/timet/database_syncer.rb', line 8

def handle_database_differences(*args)
  local_db, remote_storage, bucket, local_db_path, remote_path = args
  puts 'Differences detected between local and remote databases'
  begin
    sync_with_remote_database(local_db, remote_path, remote_storage, bucket, local_db_path)
  rescue SQLite3::Exception => e
    handle_sync_error(e, remote_storage: remote_storage, bucket: bucket, local_db_path: local_db_path)
  end
end

#handle_sync_error(error, *args) ⇒ Object



18
19
20
21
22
23
24
25
26
27
28
# File 'lib/timet/database_syncer.rb', line 18

def handle_sync_error(error, *args)
  first_arg = args.first
  if first_arg.is_a?(Hash)
    options = first_arg
    remote_storage, bucket, local_db_path = options.values_at(:remote_storage, :bucket, :local_db_path)
  else
    remote_storage, bucket, local_db_path = args
  end
  report_sync_error(error)
  upload_local_database(remote_storage, bucket, local_db_path)
end

#insert_item_from_hash(db, item) ⇒ Object



154
155
156
157
158
159
160
161
# File 'lib/timet/database_syncer.rb', line 154

def insert_item_from_hash(db, item)
  fields = ['id', *ITEM_FIELDS].join(', ')
  placeholders = Array.new(ITEM_FIELDS.length + 1, '?').join(', ')
  db.execute_sql(
    "INSERT INTO items (#{fields}) VALUES (#{placeholders})",
    get_insert_values(item)
  )
end

#items_to_hash(items) ⇒ Object



178
179
180
# File 'lib/timet/database_syncer.rb', line 178

def items_to_hash(items)
  items.to_h { |item| [item['id'], item] }
end

#log_local_wins(id, local_item) ⇒ Object



149
150
151
152
# File 'lib/timet/database_syncer.rb', line 149

def log_local_wins(id, local_item)
  puts format_status_message(id, local_item, 'Local')
  :remote_update
end

#merge_and_track_changes?(local_db, id, local_item, remote_item) ⇒ Boolean

Returns:

  • (Boolean)


106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/timet/database_syncer.rb', line 106

def merge_and_track_changes?(local_db, id, local_item, remote_item)
  local_time = extract_timestamp(local_item)
  remote_time = extract_timestamp(remote_item)

  if remote_time > local_time
    puts "Remote item #{id} is newer - updating local"
    update_item_from_hash(local_db, remote_item)
  elsif local_time > remote_time
    puts "Local item #{id} is newer - will be uploaded"
  end
  true
end

#merge_item(*args) ⇒ Object



133
134
135
136
137
138
139
140
141
# File 'lib/timet/database_syncer.rb', line 133

def merge_item(*args)
  local_db, id, local_item, remote_item = args
  local_time = extract_timestamp(local_item)
  remote_time = extract_timestamp(remote_item)

  return resolve_remote_wins(local_db, id, remote_item) if remote_wins?(remote_item, remote_time, local_time)

  log_local_wins(id, local_item)
end

#open_remote_database(remote_path) ⇒ Object



49
50
51
52
53
54
# File 'lib/timet/database_syncer.rb', line 49

def open_remote_database(remote_path)
  db_remote = SQLite3::Database.new(remote_path)
  raise 'Failed to initialize remote database' unless db_remote

  db_remote
end

#process_bidirectional_sync(local_db, local_items_by_id, remote_items_by_id) ⇒ Object



76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/timet/database_syncer.rb', line 76

def process_bidirectional_sync(local_db, local_items_by_id, remote_items_by_id)
  all_ids = (remote_items_by_id.keys + local_items_by_id.keys).uniq
  local_has_changes = false

  all_ids.each do |id|
    remote_item = remote_items_by_id[id]
    local_item = local_items_by_id[id]

    changed = sync_single_item_and_flag(local_db, id, local_item, remote_item)
    local_has_changes = true if changed
  end

  local_has_changes
end

#process_existing_item(id, local_item, remote_item, local_db) ⇒ Object



129
130
131
# File 'lib/timet/database_syncer.rb', line 129

def process_existing_item(id, local_item, remote_item, local_db)
  merge_item(local_db, id, local_item, remote_item)
end

#remote_wins?(_remote_item, remote_time, local_time) ⇒ Boolean

Returns:

  • (Boolean)


182
183
184
# File 'lib/timet/database_syncer.rb', line 182

def remote_wins?(_remote_item, remote_time, local_time)
  remote_time > local_time
end

#resolve_remote_wins(local_db, id, remote_item) ⇒ Object



143
144
145
146
147
# File 'lib/timet/database_syncer.rb', line 143

def resolve_remote_wins(local_db, id, remote_item)
  puts format_status_message(id, remote_item, 'Remote')
  update_item_from_hash(local_db, remote_item)
  :local_update
end

#sync_databases(*args) ⇒ Object



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/timet/database_syncer.rb', line 56

def sync_databases(*args)
  local_db, remote_db, remote_storage, bucket, local_db_path = args
  local_items = local_db.execute_sql('SELECT * FROM items ORDER BY updated_at DESC')
  remote_items = remote_db.execute('SELECT * FROM items ORDER BY updated_at DESC')

  local_by_id = items_to_hash(local_items)
  remote_by_id = items_to_hash(remote_items)

  local_changes = process_bidirectional_sync(local_db, local_by_id, remote_by_id)

  if local_changes
    remote_storage.upload_file(bucket, local_db_path, 'timet.db')
    puts 'Changes uploaded to remote'
  else
    puts 'No local changes to upload'
  end

  puts 'Database sync completed'
end

#sync_single_item_and_flag(local_db, id, local_item, remote_item) ⇒ Object



91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/timet/database_syncer.rb', line 91

def sync_single_item_and_flag(local_db, id, local_item, remote_item)
  if !remote_item && local_item
    puts "Local item #{id} will be uploaded"
    true
  elsif !local_item && remote_item
    puts "Adding remote item #{id} to local"
    insert_item_from_hash(local_db, remote_item)
    false
  elsif local_item && remote_item
    merge_and_track_changes?(local_db, id, local_item, remote_item)
  else
    false
  end
end

#sync_with_remote_database(*args) ⇒ Object



41
42
43
44
45
46
47
# File 'lib/timet/database_syncer.rb', line 41

def sync_with_remote_database(*args)
  local_db, remote_path, remote_storage, bucket, local_db_path = args
  db_remote = open_remote_database(remote_path)
  db_remote.results_as_hash = true
  local_db.instance_variable_get(:@db).results_as_hash = true
  sync_databases(local_db, db_remote, remote_storage, bucket, local_db_path)
end

#update_item_from_hash(db, item) ⇒ Object



163
164
165
166
167
168
169
170
171
# File 'lib/timet/database_syncer.rb', line 163

def update_item_from_hash(db, item)
  fields = "#{ITEM_FIELDS.join(' = ?, ')} = ?"
  values = get_update_values(item)
  values << item['id']
  db.execute_sql(
    "UPDATE items SET #{fields} WHERE id = ?",
    values
  )
end