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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -0
- data/README.md +110 -16
- data/VERSION +1 -1
- data/app/helpers/super_settings/settings_helper.rb +13 -3
- data/app/views/layouts/super_settings/settings.html.erb +1 -1
- data/config/routes.rb +1 -1
- data/db/migrate/20210414004553_create_super_settings.rb +1 -7
- data/lib/super_settings/application/api.js +4 -1
- data/lib/super_settings/application/helper.rb +56 -17
- data/lib/super_settings/application/images/arrow-down-short.svg +3 -0
- data/lib/super_settings/application/images/arrow-up-short.svg +3 -0
- data/lib/super_settings/application/images/info-circle.svg +4 -0
- data/lib/super_settings/application/images/pencil-square.svg +4 -0
- data/lib/super_settings/application/images/plus.svg +3 -1
- data/lib/super_settings/application/images/trash3.svg +3 -0
- data/lib/super_settings/application/images/x-circle.svg +4 -0
- data/lib/super_settings/application/index.html.erb +54 -37
- data/lib/super_settings/application/layout.html.erb +5 -2
- data/lib/super_settings/application/layout_styles.css +7 -151
- data/lib/super_settings/application/layout_vars.css.erb +21 -0
- data/lib/super_settings/application/scripts.js +100 -21
- data/lib/super_settings/application/style_vars.css.erb +62 -0
- data/lib/super_settings/application/styles.css +183 -14
- data/lib/super_settings/application.rb +18 -11
- data/lib/super_settings/attributes.rb +1 -8
- data/lib/super_settings/configuration.rb +9 -0
- data/lib/super_settings/controller_actions.rb +2 -2
- data/lib/super_settings/engine.rb +1 -1
- data/lib/super_settings/history_item.rb +1 -1
- data/lib/super_settings/http_client.rb +165 -0
- data/lib/super_settings/rack_application.rb +3 -3
- data/lib/super_settings/rest_api.rb +5 -4
- data/lib/super_settings/setting.rb +13 -2
- data/lib/super_settings/storage/active_record_storage.rb +7 -0
- data/lib/super_settings/storage/history_attributes.rb +31 -0
- data/lib/super_settings/storage/http_storage.rb +60 -184
- data/lib/super_settings/storage/json_storage.rb +201 -0
- data/lib/super_settings/storage/mongodb_storage.rb +238 -0
- data/lib/super_settings/storage/redis_storage.rb +49 -111
- data/lib/super_settings/storage/s3_storage.rb +165 -0
- data/lib/super_settings/storage/storage_attributes.rb +64 -0
- data/lib/super_settings/storage/test_storage.rb +3 -5
- data/lib/super_settings/storage/transaction.rb +67 -0
- data/lib/super_settings/storage.rb +13 -6
- data/lib/super_settings/time_precision.rb +36 -0
- data/lib/super_settings.rb +11 -0
- data/super_settings.gemspec +4 -2
- metadata +22 -9
- data/lib/super_settings/application/images/edit.svg +0 -1
- data/lib/super_settings/application/images/info.svg +0 -1
- data/lib/super_settings/application/images/slash.svg +0 -1
- 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
|
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
|
-
|
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
|
-
|
81
|
-
|
63
|
+
|
64
|
+
RedisStorage.transaction do |changes|
|
65
|
+
changes << self
|
82
66
|
end
|
83
67
|
end
|
84
68
|
|
85
|
-
def
|
86
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
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.
|
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
|
210
|
-
|
211
|
-
self.
|
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.
|
222
|
-
redis.
|
223
|
-
|
224
|
-
|
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
|
229
|
-
|
206
|
+
def created_at
|
207
|
+
self.class.time_at_microseconds(super)
|
230
208
|
end
|
231
209
|
|
232
|
-
def
|
233
|
-
|
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
|
285
|
-
updated_at: updated_at
|
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
|