super_settings 1.0.2 → 2.0.0

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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/README.md +110 -16
  4. data/VERSION +1 -1
  5. data/app/helpers/super_settings/settings_helper.rb +13 -3
  6. data/app/views/layouts/super_settings/settings.html.erb +1 -1
  7. data/config/routes.rb +1 -1
  8. data/db/migrate/20210414004553_create_super_settings.rb +1 -7
  9. data/lib/super_settings/application/api.js +4 -1
  10. data/lib/super_settings/application/helper.rb +56 -17
  11. data/lib/super_settings/application/images/arrow-down-short.svg +3 -0
  12. data/lib/super_settings/application/images/arrow-up-short.svg +3 -0
  13. data/lib/super_settings/application/images/info-circle.svg +4 -0
  14. data/lib/super_settings/application/images/pencil-square.svg +4 -0
  15. data/lib/super_settings/application/images/plus.svg +3 -1
  16. data/lib/super_settings/application/images/trash3.svg +3 -0
  17. data/lib/super_settings/application/images/x-circle.svg +4 -0
  18. data/lib/super_settings/application/index.html.erb +54 -37
  19. data/lib/super_settings/application/layout.html.erb +5 -2
  20. data/lib/super_settings/application/layout_styles.css +7 -151
  21. data/lib/super_settings/application/layout_vars.css.erb +21 -0
  22. data/lib/super_settings/application/scripts.js +100 -21
  23. data/lib/super_settings/application/style_vars.css.erb +62 -0
  24. data/lib/super_settings/application/styles.css +183 -14
  25. data/lib/super_settings/application.rb +18 -11
  26. data/lib/super_settings/attributes.rb +1 -8
  27. data/lib/super_settings/configuration.rb +9 -0
  28. data/lib/super_settings/controller_actions.rb +2 -2
  29. data/lib/super_settings/engine.rb +1 -1
  30. data/lib/super_settings/history_item.rb +1 -1
  31. data/lib/super_settings/http_client.rb +165 -0
  32. data/lib/super_settings/rack_application.rb +3 -3
  33. data/lib/super_settings/rest_api.rb +5 -4
  34. data/lib/super_settings/setting.rb +13 -2
  35. data/lib/super_settings/storage/active_record_storage.rb +7 -0
  36. data/lib/super_settings/storage/history_attributes.rb +31 -0
  37. data/lib/super_settings/storage/http_storage.rb +60 -184
  38. data/lib/super_settings/storage/json_storage.rb +201 -0
  39. data/lib/super_settings/storage/mongodb_storage.rb +238 -0
  40. data/lib/super_settings/storage/redis_storage.rb +49 -111
  41. data/lib/super_settings/storage/s3_storage.rb +165 -0
  42. data/lib/super_settings/storage/storage_attributes.rb +64 -0
  43. data/lib/super_settings/storage/test_storage.rb +3 -5
  44. data/lib/super_settings/storage/transaction.rb +67 -0
  45. data/lib/super_settings/storage.rb +13 -6
  46. data/lib/super_settings/time_precision.rb +36 -0
  47. data/lib/super_settings.rb +11 -0
  48. data/super_settings.gemspec +4 -2
  49. metadata +22 -9
  50. data/lib/super_settings/application/images/edit.svg +0 -1
  51. data/lib/super_settings/application/images/info.svg +0 -1
  52. data/lib/super_settings/application/images/slash.svg +0 -1
  53. data/lib/super_settings/application/images/trash.svg +0 -1
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mongo"
4
+
5
+ module SuperSettings
6
+ module Storage
7
+ # MongoDB implementation of the SuperSettings::Storage model.
8
+ #
9
+ # You must define the connection URL to use by setting the `url` or `mongodb` attribute on the class.
10
+ #
11
+ # @example
12
+ # SuperSettings::Storage::MongoDBStorage.url = "mongodb://user:password@localhost:27017/super_settings"
13
+ #
14
+ # @example
15
+ # SuperSettings::Storage::MongoDBStorage.mongodb = Mongo::Client.new("mongodb://user:password@localhost:27017/super_settings")
16
+ class MongoDBStorage < StorageAttributes
17
+ include Storage
18
+ include Transaction
19
+
20
+ DEFAULT_COLLECTION_NAME = "super_settings"
21
+
22
+ @mongodb = nil
23
+ @url = nil
24
+ @url_hash = @url.hash
25
+ @collection_name = DEFAULT_COLLECTION_NAME
26
+ @mutex = Mutex.new
27
+
28
+ class HistoryStorage < HistoryAttributes
29
+ def as_bson
30
+ attributes = {
31
+ value: value,
32
+ changed_by: changed_by,
33
+ created_at: created_at
34
+ }
35
+ attributes[:deleted] = true if deleted?
36
+ attributes
37
+ end
38
+ end
39
+
40
+ class << self
41
+ attr_writer :url, :mongodb
42
+ attr_accessor :collection_name
43
+
44
+ def mongodb
45
+ if @mongodb.nil? || @url_hash != @url.hash
46
+ @mutex.synchronize do
47
+ unless @url_hash == @url.hash
48
+ @url_hash = @url.hash
49
+ @mongodb = Mongo::Client.new(@url)
50
+ create_indexes!(@mongodb)
51
+ end
52
+ end
53
+ end
54
+ @mongodb
55
+ end
56
+
57
+ def settings_collection
58
+ mongodb[collection_name]
59
+ end
60
+
61
+ def updated_since(time)
62
+ time = TimePrecision.new(time, :millisecond).time
63
+ settings_collection.find(updated_at: {"$gt": time}).projection(history: 0).sort({updated_at: -1}).collect do |attributes|
64
+ record = new(attributes)
65
+ record.persisted = true
66
+ record
67
+ end
68
+ end
69
+
70
+ def all
71
+ settings_collection.find.projection(history: 0).collect do |attributes|
72
+ record = new(attributes)
73
+ record.persisted = true
74
+ record
75
+ end
76
+ end
77
+
78
+ def find_by_key(key)
79
+ query = {
80
+ key: key,
81
+ deleted: false
82
+ }
83
+ record = settings_collection.find(query).projection(history: 0).first
84
+ new(record) if record
85
+ end
86
+
87
+ def last_updated_at
88
+ last_updated_setting = settings_collection.find.projection(updated_at: 1).sort(updated_at: -1).limit(1).first
89
+ last_updated_setting["updated_at"] if last_updated_setting
90
+ end
91
+
92
+ def destroy_all
93
+ settings_collection.delete_many({})
94
+ end
95
+
96
+ def save_all(changes)
97
+ upserts = changes.collect { |setting| upsert(setting) }
98
+ changes.each { |setting| setting.new_history.clear }
99
+ settings_collection.bulk_write(upserts)
100
+ true
101
+ end
102
+
103
+ protected
104
+
105
+ def default_load_asynchronous?
106
+ true
107
+ end
108
+
109
+ private
110
+
111
+ def upsert(setting)
112
+ doc = setting.as_bson
113
+ history = setting.new_history.collect(&:as_bson)
114
+ {
115
+ update_one: {
116
+ filter: {key: setting.key},
117
+ update: {
118
+ "$set": doc.except(:key, :history),
119
+ "$setOnInsert": {key: setting.key},
120
+ "$push": {history: {"$each": history}}
121
+ },
122
+ upsert: true
123
+ }
124
+ }
125
+ end
126
+
127
+ def create_indexes!(client)
128
+ collection = client[collection_name]
129
+ collection_exists = client.database.collection_names.include?(collection.name)
130
+ existing_indexes = (collection_exists ? collection.indexes.to_a : [])
131
+
132
+ unique_key_index = {key: 1}
133
+ unless existing_indexes.any? { |index| index["key"] == unique_key_index }
134
+ collection.indexes.create_one(unique_key_index, unique: true)
135
+ end
136
+
137
+ updated_at_index = {updated_at: -1}
138
+ unless existing_indexes.any? { |index| index["key"] == updated_at_index }
139
+ collection.indexes.create_one(updated_at_index)
140
+ end
141
+
142
+ history_created_at_desc_index = {key: 1, "history.created_at": -1}
143
+ unless existing_indexes.any? { |index| index["key"] == history_created_at_desc_index }
144
+ collection.indexes.create_one(history_created_at_desc_index)
145
+ end
146
+ end
147
+ end
148
+
149
+ attr_reader :new_history
150
+
151
+ def initialize(*)
152
+ @new_history = []
153
+ super
154
+ end
155
+
156
+ def history(limit: nil, offset: 0)
157
+ pipeline = [
158
+ {
159
+ "$match": {key: key}
160
+ },
161
+ {
162
+ "$addFields": {
163
+ history: {
164
+ "$sortArray": {
165
+ input: "$history",
166
+ sortBy: {created_at: -1}
167
+ }
168
+ }
169
+ }
170
+ }
171
+ ]
172
+
173
+ if limit || offset > 0
174
+ pipeline << {
175
+ "$addFields": {
176
+ history: {
177
+ "$slice": ["$history", offset, (limit || {"$size": "$history"})]
178
+ }
179
+ }
180
+ }
181
+ end
182
+
183
+ pipeline << {
184
+ "$project": {
185
+ _id: 0,
186
+ history: 1
187
+ }
188
+ }
189
+
190
+ record = self.class.settings_collection.aggregate(pipeline).to_a.first
191
+ return [] unless record && record["history"].is_a?(Array)
192
+
193
+ record["history"].collect do |record|
194
+ HistoryItem.new(key: key, value: record["value"], changed_by: record["changed_by"], created_at: record["created_at"], deleted: record["deleted"])
195
+ end
196
+ end
197
+
198
+ def create_history(changed_by:, created_at:, value: nil, deleted: false)
199
+ created_at = TimePrecision.new(created_at, :millisecond).time
200
+ history = HistoryStorage.new(key: key, value: value, changed_by: changed_by, created_at: created_at, deleted: deleted)
201
+ @new_history.unshift(history)
202
+ history
203
+ end
204
+
205
+ def created_at=(val)
206
+ super(TimePrecision.new(val, :millisecond).time)
207
+ end
208
+
209
+ def updated_at=(val)
210
+ super(TimePrecision.new(val, :millisecond).time)
211
+ end
212
+
213
+ def destroy
214
+ settings_collection.delete_one(key: key)
215
+ end
216
+
217
+ def as_bson
218
+ {
219
+ key: key,
220
+ raw_value: raw_value,
221
+ value_type: value_type,
222
+ description: description,
223
+ created_at: created_at,
224
+ updated_at: updated_at,
225
+ deleted: deleted?
226
+ }
227
+ end
228
+ end
229
+
230
+ def created_at=(val)
231
+ super(TimePrecision.new(val, :millisecond).time)
232
+ end
233
+
234
+ def updated_at=(val)
235
+ super(TimePrecision.new(val, :millisecond).time)
236
+ end
237
+ end
238
+ end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
4
3
  require "redis"
5
4
 
6
5
  module SuperSettings
@@ -23,20 +22,15 @@ module SuperSettings
23
22
  #
24
23
  # @example
25
24
  # SuperSettings::Storage::RedisStorage.redis = ConnectionPool.new(size: 5) { Redis.new(url: ENV["REDIS_URL"]) }
26
- class RedisStorage
27
- include Storage
25
+ class RedisStorage < StorageAttributes
26
+ include Transaction
28
27
 
29
28
  SETTINGS_KEY = "SuperSettings.settings"
30
29
  UPDATED_KEY = "SuperSettings.order_by_updated_at"
31
30
 
32
- class HistoryStorage
31
+ class HistoryStorage < HistoryAttributes
33
32
  HISTORY_KEY_PREFIX = "SuperSettings.history"
34
33
 
35
- include SuperSettings::Attributes
36
-
37
- attr_accessor :key, :value, :changed_by, :deleted
38
- attr_reader :created_at
39
-
40
34
  class << self
41
35
  def find_all_by_key(key:, offset: 0, limit: nil)
42
36
  end_index = (limit.nil? ? -1 : offset + limit - 1)
@@ -55,10 +49,8 @@ module SuperSettings
55
49
  record
56
50
  end
57
51
 
58
- def destroy_all_by_key(key)
59
- RedisStorage.transaction do |redis|
60
- redis.del("#{HISTORY_KEY_PREFIX}.#{key}")
61
- end
52
+ def destroy_all_by_key(key, redis)
53
+ redis.del("#{HISTORY_KEY_PREFIX}.#{key}")
62
54
  end
63
55
 
64
56
  def redis_key(key)
@@ -66,24 +58,20 @@ module SuperSettings
66
58
  end
67
59
  end
68
60
 
69
- def initialize(*)
70
- @deleted = false
71
- super
72
- end
73
-
74
- def created_at=(val)
75
- @created_at = SuperSettings::Coerce.time(val)
76
- end
77
-
78
61
  def save!
79
62
  raise ArgumentError.new("Missing key") if Coerce.blank?(key)
80
- RedisStorage.transaction do |redis|
81
- redis.lpush(self.class.redis_key(key), payload_json.to_json)
63
+
64
+ RedisStorage.transaction do |changes|
65
+ changes << self
82
66
  end
83
67
  end
84
68
 
85
- def deleted?
86
- !!@deleted
69
+ def save_to_redis(redis)
70
+ redis.lpush(self.class.redis_key(key), payload_json.to_json)
71
+ end
72
+
73
+ def created_at
74
+ SuperSettings::Storage::RedisStorage.time_at_microseconds(super)
87
75
  end
88
76
 
89
77
  private
@@ -92,16 +80,13 @@ module SuperSettings
92
80
  payload = {
93
81
  value: value,
94
82
  changed_by: changed_by,
95
- created_at: created_at.to_f
83
+ created_at: SuperSettings::Storage::RedisStorage.microseconds(created_at)
96
84
  }
97
85
  payload[:deleted] = true if deleted?
98
86
  payload
99
87
  end
100
88
  end
101
89
 
102
- attr_reader :key, :raw_value, :description, :value_type, :updated_at, :created_at
103
- attr_accessor :changed_by
104
-
105
90
  class << self
106
91
  def all
107
92
  with_redis do |redis|
@@ -110,15 +95,15 @@ module SuperSettings
110
95
  end
111
96
 
112
97
  def updated_since(time)
113
- time = SuperSettings::Coerce.time(time)
98
+ min_score = microseconds(time)
114
99
  with_redis do |redis|
115
- min_score = time.to_f
116
100
  keys = redis.zrangebyscore(UPDATED_KEY, min_score, "+inf")
117
101
  return [] if keys.empty?
118
102
 
119
103
  settings = []
120
104
  redis.hmget(SETTINGS_KEY, *keys).each do |json|
121
- settings << load_from_json(json) if json
105
+ setting = load_from_json(json) if json
106
+ settings << setting if setting && setting.updated_at > time
122
107
  end
123
108
  settings
124
109
  end
@@ -134,7 +119,7 @@ module SuperSettings
134
119
  def last_updated_at
135
120
  result = with_redis { |redis| redis.zrevrange(UPDATED_KEY, 0, 1, withscores: true).first }
136
121
  return nil unless result
137
- Time.at(result[1])
122
+ time_at_microseconds(result[1])
138
123
  end
139
124
 
140
125
  def destroy_all
@@ -152,26 +137,23 @@ module SuperSettings
152
137
  end
153
138
  end
154
139
 
155
- def transaction(&block)
156
- if Thread.current[:super_settings_transaction_redis]
157
- block.call(Thread.current[:super_settings_transaction_redis])
158
- else
159
- begin
160
- with_redis do |redis|
161
- redis.multi do |multi_redis|
162
- Thread.current[:super_settings_transaction_redis] = multi_redis
163
- Thread.current[:super_settings_transaction_after_commit] = []
164
- block.call(multi_redis)
165
- end
166
- after_commits = Thread.current[:super_settings_transaction_after_commit]
167
- Thread.current[:super_settings_transaction_after_commit] = nil
168
- after_commits.each(&:call)
140
+ def save_all(changes)
141
+ with_redis do |redis|
142
+ redis.multi do |multi_redis|
143
+ changes.each do |object|
144
+ object.save_to_redis(multi_redis)
169
145
  end
170
- ensure
171
- Thread.current[:super_settings_transaction_redis] = nil
172
- Thread.current[:super_settings_transaction_after_commit] = nil
173
146
  end
174
147
  end
148
+ true
149
+ end
150
+
151
+ def time_at_microseconds(time)
152
+ TimePrecision.new(time, :microsecond).time
153
+ end
154
+
155
+ def microseconds(time)
156
+ TimePrecision.new(time, :microsecond).to_f
175
157
  end
176
158
 
177
159
  protected
@@ -185,7 +167,7 @@ module SuperSettings
185
167
  def load_from_json(json)
186
168
  attributes = JSON.parse(json)
187
169
  setting = new(attributes)
188
- setting.send(:set_persisted!)
170
+ setting.persisted = true
189
171
  setting
190
172
  end
191
173
  end
@@ -206,83 +188,39 @@ module SuperSettings
206
188
  HistoryStorage.create!(key: key, value: value, deleted: deleted, changed_by: changed_by, created_at: created_at)
207
189
  end
208
190
 
209
- def save!
210
- self.updated_at ||= Time.now
211
- self.created_at ||= updated_at
212
- self.class.transaction do |redis|
213
- redis.hset(SETTINGS_KEY, key, payload_json)
214
- redis.zadd(UPDATED_KEY, updated_at.to_f, key)
215
- set_persisted!
216
- end
217
- true
191
+ def save_to_redis(redis)
192
+ redis.hset(SETTINGS_KEY, key, payload_json)
193
+ redis.zadd(UPDATED_KEY, self.class.microseconds(updated_at), key)
218
194
  end
219
195
 
220
196
  def destroy
221
- self.class.transaction do |redis|
222
- redis.hdel(SETTINGS_KEY, key)
223
- redis.zrem(UPDATED_KEY, key)
224
- HistoryStorage.destroy_all_by_key(key)
197
+ self.class.with_redis do |redis|
198
+ redis.multi do |multi_redis|
199
+ multi_redis.hdel(SETTINGS_KEY, key)
200
+ multi_redis.zrem(UPDATED_KEY, key)
201
+ HistoryStorage.destroy_all_by_key(key, multi_redis)
202
+ end
225
203
  end
226
204
  end
227
205
 
228
- def key=(value)
229
- @key = (Coerce.blank?(value) ? nil : value.to_s)
206
+ def created_at
207
+ self.class.time_at_microseconds(super)
230
208
  end
231
209
 
232
- def raw_value=(value)
233
- @raw_value = (Coerce.blank?(value) ? nil : value.to_s)
234
- end
235
-
236
- def value_type=(value)
237
- @value_type = (Coerce.blank?(value) ? nil : value.to_s)
238
- end
239
-
240
- def description=(value)
241
- @description = (Coerce.blank?(value) ? nil : value.to_s)
242
- end
243
-
244
- def deleted=(value)
245
- @deleted = Coerce.boolean(value)
246
- end
247
-
248
- def created_at=(value)
249
- @created_at = SuperSettings::Coerce.time(value)
250
- end
251
-
252
- def updated_at=(value)
253
- @updated_at = SuperSettings::Coerce.time(value)
254
- end
255
-
256
- def deleted?
257
- !!@deleted
258
- end
259
-
260
- def persisted?
261
- !!@persisted
210
+ def updated_at
211
+ self.class.time_at_microseconds(super)
262
212
  end
263
213
 
264
214
  private
265
215
 
266
- def after_commit(&block)
267
- if Thread.current[:super_settings_transaction_after_commit]
268
- Thread.current[:super_settings_transaction_after_commit] << block
269
- else
270
- block.call
271
- end
272
- end
273
-
274
- def set_persisted!
275
- @persisted = true
276
- end
277
-
278
216
  def payload_json
279
217
  payload = {
280
218
  key: key,
281
219
  raw_value: raw_value,
282
220
  value_type: value_type,
283
221
  description: description,
284
- created_at: created_at.to_f,
285
- updated_at: updated_at.to_f
222
+ created_at: self.class.microseconds(created_at),
223
+ updated_at: self.class.microseconds(updated_at)
286
224
  }
287
225
  payload[:deleted] = true if deleted?
288
226
  payload.to_json
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "aws-sdk-s3"
4
+
5
+ module SuperSettings
6
+ module Storage
7
+ # Storage backend for storing the settings in an S3 object. This should work with any S3-compatible
8
+ # storage service.
9
+ class S3Storage < JSONStorage
10
+ SETTINGS_FILE = "settings.json"
11
+ HISTORY_FILE_SUFFIX = ".history.json"
12
+ DEFAULT_PATH = "super_settings"
13
+
14
+ # Configuration for the S3 storage backend.
15
+ #
16
+ # * access_key_id - The AWS access key ID. Defaults to the SUPER_SETTINGS_AWS_ACCESS_KEY_ID
17
+ # or AWS_ACCESS_KEY_ID environment variable or whatever is set in the `Aws.config` object.
18
+ # * secret_access_key - The AWS secret access key. Defaults to the SUPER_SETTINGS_AWS_SECRET_ACCESS_KEY
19
+ # or AWS_SECRET_ACCESS_KEY environment variable or whatever is set in the `Aws.config` object.
20
+ # * region - The AWS region. Defaults to the SUPER_SETTINGS_AWS_REGION or AWS_REGION environment variable.
21
+ # This is required for AWS S3 but may be optional for S3-compatible services.
22
+ # * endpoint - The S3 endpoint URL. This is optional and should only be used for S3-compatible services.
23
+ # Defaults to the SUPER_SETTINGS_AWS_ENDPOINT or AWS_ENDPOINT environment variable.
24
+ # * bucket - The S3 bucket name. Defaults to the SUPER_SETTINGS_S3_BUCKET or AWS_S3_BUCKET
25
+ # environment variable.
26
+ # * object - The S3 object key. Defaults to "super_settings.json.gz" or the value set in the
27
+ # SUPER_SETTINGS_S3_OBJECT environment variable.
28
+ #
29
+ # You can also specify the configuration using a URL in the format using the SUPER_SETTINGS_S3_URL
30
+ # environment variable. The URL should be in the format:
31
+ #
32
+ # ```
33
+ # s3://access_key_id:secret_access_key@region/bucket/object
34
+ # ```
35
+ class Configuration
36
+ attr_accessor :access_key_id, :secret_access_key, :region, :endpoint, :bucket
37
+ attr_reader :path
38
+
39
+ def initialize
40
+ @access_key_id ||= ENV.fetch("SUPER_SETTINGS_AWS_ACCESS_KEY_ID", ENV["AWS_ACCESS_KEY_ID"])
41
+ @secret_access_key ||= ENV.fetch("SUPER_SETTINGS_AWS_SECRET_ACCESS_KEY", ENV["AWS_SECRET_ACCESS_KEY"])
42
+ @region ||= ENV.fetch("SUPER_SETTINGS_AWS_REGION", ENV["AWS_REGION"])
43
+ @endpoint ||= ENV.fetch("SUPER_SETTINGS_AWS_ENDPOINT", ENV["AWS_ENDPOINT"])
44
+ @bucket ||= ENV.fetch("SUPER_SETTINGS_S3_BUCKET", ENV["AWS_S3_BUCKET"])
45
+ @path ||= ENV.fetch("SUPER_SETTINGS_S3_OBJECT", DEFAULT_PATH)
46
+ self.url = ENV["SUPER_SETTINGS_S3_URL"] unless ENV["SUPER_SETTINGS_S3_URL"].to_s.empty?
47
+ end
48
+
49
+ def url=(url)
50
+ return if url.to_s.empty?
51
+
52
+ uri = URI.parse(url)
53
+ raise ArgumentError, "Invalid S3 URL" unless uri.scheme == "s3"
54
+
55
+ self.access_key_id = uri.user if uri.user
56
+ self.secret_access_key = uri.password if uri.password
57
+ self.region = uri.host if uri.host
58
+ _, bucket, path = uri.path.split("/", 3) if uri.path
59
+ self.bucket = bucket if bucket
60
+ self.path = path if path
61
+ end
62
+
63
+ def path=(value)
64
+ @path = "#{value}.chomp('/')/"
65
+ end
66
+
67
+ def hash
68
+ [self.class, access_key_id, secret_access_key, region, endpoint, bucket, path].hash
69
+ end
70
+ end
71
+
72
+ @bucket = nil
73
+ @bucket_hash = nil
74
+
75
+ class << self
76
+ def last_updated_at
77
+ all.collect(&:updated_at).compact.max
78
+ end
79
+
80
+ def configuration
81
+ @config ||= Configuration.new
82
+ end
83
+
84
+ def destroy_all
85
+ s3_bucket.objects(prefix: configuration.path).each do |object|
86
+ if object.key == file_path(SETTINGS_FILE) || object.key.end_with?(HISTORY_FILE_SUFFIX)
87
+ object.delete
88
+ end
89
+ end
90
+ end
91
+
92
+ protected
93
+
94
+ def default_load_asynchronous?
95
+ true
96
+ end
97
+
98
+ def settings_json_payload
99
+ object = settings_object
100
+ return nil unless object.exists?
101
+
102
+ object.get.body.read
103
+ end
104
+
105
+ def save_settings_json(json)
106
+ object = settings_object
107
+ object.put(body: json)
108
+ end
109
+
110
+ def save_history_json(key, json)
111
+ object = history_object(key)
112
+ object.put(body: json)
113
+ end
114
+
115
+ private
116
+
117
+ def s3_bucket
118
+ if configuration.hash != @bucket_hash
119
+ @bucket_hash = configuration.hash
120
+ options = {
121
+ endpoint: configuration.endpoint,
122
+ access_key_id: configuration.access_key_id,
123
+ secret_access_key: configuration.secret_access_key,
124
+ region: configuration.region
125
+ }
126
+ options[:force_path_style] = true if configuration.endpoint
127
+ options.compact!
128
+
129
+ @bucket = Aws::S3::Resource.new(options).bucket(configuration.bucket)
130
+ end
131
+ @bucket
132
+ end
133
+
134
+ def s3_object(filename)
135
+ s3_bucket.object(file_path(filename))
136
+ end
137
+
138
+ def file_path(filename)
139
+ "#{configuration.path}#{filename}"
140
+ end
141
+
142
+ def settings_object
143
+ s3_object(SETTINGS_FILE)
144
+ end
145
+
146
+ def history_object(key)
147
+ s3_object("#{key}#{HISTORY_FILE_SUFFIX}")
148
+ end
149
+
150
+ def history_json(key)
151
+ object = history_object(key)
152
+ return nil unless object.exists?
153
+
154
+ object.get.body.read
155
+ end
156
+ end
157
+
158
+ protected
159
+
160
+ def fetch_history_json
161
+ self.class.send(:history_json, key)
162
+ end
163
+ end
164
+ end
165
+ end