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



141
142
143
# File 'lib/timet/database_syncer.rb', line 141

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

.format_status_message(id, item, source) ⇒ Object



155
156
157
158
# File 'lib/timet/database_syncer.rb', line 155

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



89
90
91
# File 'lib/timet/database_syncer.rb', line 89

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



94
95
96
97
# File 'lib/timet/database_syncer.rb', line 94

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



161
162
163
164
165
# File 'lib/timet/database_syncer.rb', line 161

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



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

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



167
168
169
170
# File 'lib/timet/database_syncer.rb', line 167

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



124
125
126
127
128
129
130
131
# File 'lib/timet/database_syncer.rb', line 124

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



146
147
148
# File 'lib/timet/database_syncer.rb', line 146

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

#log_local_wins(id, local_item) ⇒ Object



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

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

#merge_item(*args) ⇒ Object



103
104
105
106
107
108
109
110
111
# File 'lib/timet/database_syncer.rb', line 103

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_database_items(local_db, remote_db) ⇒ Object



63
64
65
66
67
68
# File 'lib/timet/database_syncer.rb', line 63

def process_database_items(local_db, remote_db)
  remote_items = remote_db.execute('SELECT * FROM items ORDER BY updated_at DESC')
  local_items = local_db.execute_sql('SELECT * FROM items ORDER BY updated_at DESC')

  sync_items_by_id(local_db, items_to_hash(local_items), items_to_hash(remote_items))
end

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



99
100
101
# File 'lib/timet/database_syncer.rb', line 99

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:



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

def remote_wins?(remote_item, remote_time, local_time)
  time_diff = remote_time > local_time
  time_diff && (remote_item['deleted'].to_i == 1 || time_diff)
end

#resolve_remote_wins(local_db, id, remote_item) ⇒ Object



113
114
115
116
117
# File 'lib/timet/database_syncer.rb', line 113

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
# File 'lib/timet/database_syncer.rb', line 56

def sync_databases(*args)
  local_db, remote_db, remote_storage, bucket, local_db_path = args
  process_database_items(local_db, remote_db)
  remote_storage.upload_file(bucket, local_db_path, 'timet.db')
  puts 'Database sync completed'
end

#sync_items_by_id(local_db, local_items_by_id, remote_items_by_id) ⇒ Object



70
71
72
73
# File 'lib/timet/database_syncer.rb', line 70

def sync_items_by_id(local_db, local_items_by_id, remote_items_by_id)
  all_item_ids = (remote_items_by_id.keys + local_items_by_id.keys).uniq
  all_item_ids.each { |id| sync_single_item(local_db, id, local_items_by_id, remote_items_by_id) }
end

#sync_single_item(*args) ⇒ Object



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

def sync_single_item(*args)
  local_db, id, local_items_by_id, remote_items_by_id = args
  remote_item = remote_items_by_id[id]
  local_item = local_items_by_id[id]

  if !remote_item
    log_local_only(id)
  elsif !local_item
    add_remote_item(local_db, id, remote_item)
  else
    merge_item(local_db, id, local_item, remote_item)
  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



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

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