activerecord_hoarder 0.0.1
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 +7 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +18 -0
- data/LICENSE.txt +21 -0
- data/README.md +62 -0
- data/Rakefile +6 -0
- data/activerecord_hoarder.gemspec +27 -0
- data/bin/console +29 -0
- data/bin/setup +8 -0
- data/config/activerecord_hoarder.yml.template +6 -0
- data/config/dbspec.yml.template +2 -0
- data/config/dbspec_rspec.yml.template +2 -0
- data/example/example.rb +3 -0
- data/example/fixture.rb +6 -0
- data/example/schema.rb +8 -0
- data/lib/activerecord_hoarder/aws_s3_storage.rb +64 -0
- data/lib/activerecord_hoarder/batch.rb +26 -0
- data/lib/activerecord_hoarder/batch_archiver.rb +16 -0
- data/lib/activerecord_hoarder/core.rb +13 -0
- data/lib/activerecord_hoarder/record_collector.rb +67 -0
- data/lib/activerecord_hoarder/record_query.rb +85 -0
- data/lib/activerecord_hoarder/restore.rb +16 -0
- data/lib/activerecord_hoarder/serializer.rb +9 -0
- data/lib/activerecord_hoarder/storage.rb +27 -0
- data/lib/activerecord_hoarder/storage_error.rb +4 -0
- data/lib/activerecord_hoarder/storage_key.rb +18 -0
- data/lib/activerecord_hoarder/storages.rb +20 -0
- data/lib/activerecord_hoarder/version.rb +3 -0
- data/lib/activerecord_hoarder.rb +21 -0
- data/spec/activerecord_hoarder/aws_s3_storage_spec.rb +26 -0
- data/spec/activerecord_hoarder/batch_archiver_spec.rb +10 -0
- data/spec/activerecord_hoarder/core_spec.rb +13 -0
- data/spec/activerecord_hoarder/serializer_spec.rb +7 -0
- data/spec/activerecord_hoarder/storage_spec.rb +29 -0
- data/spec/activerecord_hoarder_spec.rb +168 -0
- data/spec/factories/examples.rb +40 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/support/0_rspec_configuration.rb +14 -0
- data/spec/support/1_active_record_configuration.rb +1 -0
- data/spec/support/2_database_content.rb +14 -0
- data/spec/support/3_test_storage.rb +7 -0
- metadata +162 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 476f76a46fae44bba761080f33f0c5073733de0e
|
4
|
+
data.tar.gz: 5040b1ce780046408e8ab065d18452e9ff6803b1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 195f314b03bf2f5f0e2f82da4a9c409b03ca4896d39e1bb055a8d3090d956b5ad929a69e0c1ab9015f6c5726233318062f364e2c392ccc0b58fcf662f7740370
|
7
|
+
data.tar.gz: ebd4a813bb3de31d7e61f962b4ff1b5098f71857dda2d016782c91227d22725de4c31dc45c99579de60b5e2f7f374cbde7fb2c6556f6fd6e5adf4bdc7abca75d
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
source "https://rubygems.org"
|
2
|
+
|
3
|
+
repo_name = "gem_batch_archiving"
|
4
|
+
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
|
5
|
+
|
6
|
+
gem 'aws-sdk-s3', '~> 1'
|
7
|
+
|
8
|
+
group :test do
|
9
|
+
gem 'factory_girl_rails'
|
10
|
+
gem 'sqlite3'
|
11
|
+
gem 'timecop'
|
12
|
+
end
|
13
|
+
|
14
|
+
group :development do
|
15
|
+
gem 'sqlite3'
|
16
|
+
end
|
17
|
+
|
18
|
+
gemspec
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 ACCESS MARKETING & COMMUNICATION LLC
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
# Activerecord Hoarder
|
2
|
+
|
3
|
+
hoard records
|
4
|
+
|
5
|
+
## 1 Use
|
6
|
+
|
7
|
+
### 1.1 make model a hoarder
|
8
|
+
```
|
9
|
+
class ExampleModel < ActiveRecord::Base
|
10
|
+
acts_as_hoarder
|
11
|
+
end
|
12
|
+
```
|
13
|
+
|
14
|
+
### 1.2 hoarding records
|
15
|
+
from console:
|
16
|
+
```
|
17
|
+
ExampleModel.hoard
|
18
|
+
```
|
19
|
+
will create S3 entries with keys: `<bucket_sub_dir>/<table_name = example_models>/<year>/<month>/<year>-<month>-<day>.json` and json formatted content
|
20
|
+
|
21
|
+
### 1.3 restoring records
|
22
|
+
from console:
|
23
|
+
```
|
24
|
+
ExampleModel.restore_date(Date.new(<Y>,<m>,<d>))
|
25
|
+
```
|
26
|
+
|
27
|
+
## 2 Development
|
28
|
+
|
29
|
+
### 2.0 initial setup
|
30
|
+
|
31
|
+
Make a clone. Make a branch. Install dependencies.
|
32
|
+
|
33
|
+
### 2.1 playing around
|
34
|
+
|
35
|
+
#### Configure database
|
36
|
+
|
37
|
+
Create config file from template (`cp config/dbspec.yml.template config/dbspec.yml`). Change database from `postgresql` to `sqlite3` and database name from `activerecord_hoarder` to `<as_desired>.sqlite3`.
|
38
|
+
|
39
|
+
#### Configure archive
|
40
|
+
|
41
|
+
Create config file from template (`cp config/activerecord_hoarder.yml.template config/activerecord_hoarder.yml`). Add your S3 credentials `access_key_id` and `secret_access_key` for target bucket `bucket`. Change `region` if necessary. If you want, change `acl` and add `bucket_sub_dir`.
|
42
|
+
|
43
|
+
#### Hop into sandbox
|
44
|
+
```
|
45
|
+
bundler exec bin/console
|
46
|
+
```
|
47
|
+
|
48
|
+
#### bin/example
|
49
|
+
Convenience functionality
|
50
|
+
- `require_relative "bin/example/schema"` for creating an example table `examples`
|
51
|
+
- `require_relative "bin/example/example"` for an example archivable model `Example`
|
52
|
+
- `require_relative "bin/example/fixture"` for a factory method `create_examples(count, start: 0, deleted: true)` for creating examples
|
53
|
+
|
54
|
+
### 2.2 testing it
|
55
|
+
|
56
|
+
#### Configure test database
|
57
|
+
Create config file from template (`cp config/dbspec_rspec.yml.template config/dbspec_rspec.yml`). Modify settings if you want.
|
58
|
+
|
59
|
+
#### Run tests
|
60
|
+
```
|
61
|
+
bundler exec rspec spec
|
62
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "activerecord_hoarder/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "activerecord_hoarder"
|
8
|
+
spec.version = ActiverecordHoarder::VERSION
|
9
|
+
spec.authors = ["Matthias Engh"]
|
10
|
+
spec.email = ["matthias@wescrimmage.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{hoards records}
|
13
|
+
spec.description = %q{extend active records with simple archiving mechanics}
|
14
|
+
spec.homepage = "https://github.com/Scrimmage/gem_batch_archiving"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0")
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_dependency "activerecord", [">= 4.2", "< 6.0"]
|
23
|
+
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.15"
|
25
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
26
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
27
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
|
5
|
+
# require dependencies from gemspec
|
6
|
+
require "active_record"
|
7
|
+
require "aws-sdk-s3"
|
8
|
+
require "pp"
|
9
|
+
require "yaml"
|
10
|
+
|
11
|
+
dbspec_location = "config/dbspec.yml"
|
12
|
+
|
13
|
+
if File.exist?(dbspec_location)
|
14
|
+
dbspec = YAML.load_file(dbspec_location)
|
15
|
+
ActiveRecord::Base.establish_connection(dbspec)
|
16
|
+
ActiveRecord::Base.connection
|
17
|
+
else
|
18
|
+
raise "database configuration file #{dbspec_location} not found"
|
19
|
+
end
|
20
|
+
|
21
|
+
require "activerecord_hoarder"
|
22
|
+
|
23
|
+
storage_config_path = "config/activerecord_hoarder.yml"
|
24
|
+
raise "storage configuration file #{storage_config_path}` not found" if !File.exists?(storage_config_path)
|
25
|
+
storage_options = YAML.load_file(storage_config_path)
|
26
|
+
::ActiverecordHoarder::Storage.configure(storage: :aws_s3, storage_options: storage_options)
|
27
|
+
|
28
|
+
require "irb"
|
29
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/example/example.rb
ADDED
data/example/fixture.rb
ADDED
data/example/schema.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
module ActiverecordHoarder
|
2
|
+
class AwsS3
|
3
|
+
DEFAULT_ACL = "private"
|
4
|
+
OPTION_CONTENT_ACCESS = "acl"
|
5
|
+
OPTION_SUB_DIR = "bucket_sub_dir"
|
6
|
+
OPTION_BUCKET = "bucket"
|
7
|
+
OPTION_ACCESS_KEY_ID = "access_key_id"
|
8
|
+
OPTIONS_SECRET_ACCESS_KEY = "secret_access_key"
|
9
|
+
OPTION_REGION = "region"
|
10
|
+
|
11
|
+
attr_reader :storage_options
|
12
|
+
|
13
|
+
def initialize(table_name, storage_options)
|
14
|
+
@storage_options = storage_options
|
15
|
+
|
16
|
+
if storage_options[OPTION_SUB_DIR].blank?
|
17
|
+
@key_prefix = table_name
|
18
|
+
else
|
19
|
+
@key_prefix = File.join(storage_options[OPTION_SUB_DIR], table_name)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def fetch_data(key)
|
24
|
+
full_key = key_with_prefix(key)
|
25
|
+
begin
|
26
|
+
response = s3_client.get_object(bucket: s3_bucket, key: full_key)
|
27
|
+
rescue Aws::S3::Errors::NoSuchKey => e
|
28
|
+
raise ::ActiverecordHoarder::StorageError.new("fetch_data erred with '#{e.class}':'#{e.message}'' trying to access '#{full_key}'' in bucket: '#{s3_bucket}'")
|
29
|
+
end
|
30
|
+
response.body
|
31
|
+
end
|
32
|
+
|
33
|
+
def store_data(batch)
|
34
|
+
full_key = key_with_prefix(batch.key.to_s)
|
35
|
+
|
36
|
+
s3_client.put_object(bucket: s3_bucket, body: batch.content_string, key: full_key, acl: s3_acl)
|
37
|
+
true
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def key_with_prefix(key)
|
43
|
+
File.join(@key_prefix, key.to_s)
|
44
|
+
end
|
45
|
+
|
46
|
+
def s3_acl
|
47
|
+
storage_options[OPTION_CONTENT_ACCESS] || DEFAULT_ACL
|
48
|
+
end
|
49
|
+
|
50
|
+
def s3_bucket
|
51
|
+
storage_options[OPTION_BUCKET]
|
52
|
+
end
|
53
|
+
|
54
|
+
def s3_client
|
55
|
+
@s3_client ||= begin
|
56
|
+
access_key_id = storage_options[OPTION_ACCESS_KEY_ID] or raise StorageError.new("access_key_id missing")
|
57
|
+
secret_access_key = storage_options[OPTIONS_SECRET_ACCESS_KEY] or raise StorageError.new("secret_access_key missing")
|
58
|
+
credentials = Aws::Credentials.new(access_key_id, secret_access_key)
|
59
|
+
region = storage_options[OPTION_REGION] or raise StorageError.new("region missing")
|
60
|
+
Aws::S3::Client.new(credentials: credentials, region: region)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module ::ActiverecordHoarder
|
2
|
+
class Batch
|
3
|
+
RECORD_DATE_FIELD = "created_at"
|
4
|
+
|
5
|
+
def self.from_records(record_data)
|
6
|
+
record_data.present? ? new(record_data) : nil
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(record_data)
|
10
|
+
@record_data = record_data
|
11
|
+
@serializer = ::ActiverecordHoarder::Serializer
|
12
|
+
end
|
13
|
+
|
14
|
+
def date
|
15
|
+
@date ||= @record_data.first[RECORD_DATE_FIELD].to_date
|
16
|
+
end
|
17
|
+
|
18
|
+
def key
|
19
|
+
@key ||= ::ActiverecordHoarder::StorageKey.from_date(date, @serializer.extension)
|
20
|
+
end
|
21
|
+
|
22
|
+
def content_string
|
23
|
+
@serializer.create_archive(@record_data)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class ::ActiverecordHoarder::BatchArchiver
|
2
|
+
def initialize(model_class, storage = nil)
|
3
|
+
@record_collector = ::ActiverecordHoarder::RecordCollector.new(model_class)
|
4
|
+
@archive_storage = storage || default_storage_for_records(model_class.table_name)
|
5
|
+
end
|
6
|
+
|
7
|
+
def archive_batch
|
8
|
+
@record_collector.in_batches(delete_on_success: true) do |batch|
|
9
|
+
@archive_storage.store_data(batch)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def default_storage_for_records(table_name)
|
14
|
+
::ActiverecordHoarder::Storage.new(table_name)
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module ActiverecordHoarder::Core
|
2
|
+
def self.included(base)
|
3
|
+
raise 'created_at accessor needed' if !base.column_names.include?("created_at")
|
4
|
+
raise 'deleted_at accessor needed' if !base.column_names.include?("deleted_at")
|
5
|
+
base.extend ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def hoard
|
10
|
+
::ActiverecordHoarder::BatchArchiver.new(self).archive_batch
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
class ::ActiverecordHoarder::RecordCollector
|
2
|
+
attr_reader :relative_limit
|
3
|
+
|
4
|
+
def initialize(model_class)
|
5
|
+
@model_class = model_class
|
6
|
+
end
|
7
|
+
|
8
|
+
def in_batches(delete_on_success: false)
|
9
|
+
while collect_batch
|
10
|
+
success = yield @batch
|
11
|
+
return if !success
|
12
|
+
next if !delete_on_success
|
13
|
+
destroy_current_records!
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def collect_batch
|
20
|
+
activate_limit if batch_data_cached? && !limit_toggled?
|
21
|
+
if limit_toggled?
|
22
|
+
@batch = ensuring_new_records do
|
23
|
+
retrieve_next_batch
|
24
|
+
end
|
25
|
+
else
|
26
|
+
@batch = retrieve_first_batch
|
27
|
+
end
|
28
|
+
batch_data_cached?
|
29
|
+
end
|
30
|
+
|
31
|
+
def activate_limit
|
32
|
+
@relative_limit = [@batch.date.end_of_week + 1, archive_timeframe_upper_limit].min
|
33
|
+
end
|
34
|
+
|
35
|
+
def archive_timeframe_upper_limit
|
36
|
+
Time.now.getutc.beginning_of_day
|
37
|
+
end
|
38
|
+
|
39
|
+
def batch_data_cached?
|
40
|
+
@batch.present?
|
41
|
+
end
|
42
|
+
|
43
|
+
def destroy_current_records!
|
44
|
+
@model_class.connection.execute(@batch_query.delete(@batch.date))
|
45
|
+
end
|
46
|
+
|
47
|
+
def ensuring_new_records
|
48
|
+
record_batch = yield
|
49
|
+
@batch.date == record_batch.try(:date) ? nil : record_batch
|
50
|
+
end
|
51
|
+
|
52
|
+
def limit_toggled?
|
53
|
+
@relative_limit.present?
|
54
|
+
end
|
55
|
+
|
56
|
+
def retrieve_first_batch
|
57
|
+
@batch_query = ::ActiverecordHoarder::BatchQuery.new(archive_timeframe_upper_limit, @model_class)
|
58
|
+
batch_data = @model_class.connection.exec_query(@batch_query.fetch)
|
59
|
+
::ActiverecordHoarder::Batch.from_records(batch_data)
|
60
|
+
end
|
61
|
+
|
62
|
+
def retrieve_next_batch
|
63
|
+
@batch_query = ::ActiverecordHoarder::BatchQuery.new(relative_limit, @model_class)
|
64
|
+
batch_data = @model_class.connection.exec_query(@batch_query.fetch)
|
65
|
+
::ActiverecordHoarder::Batch.from_records(batch_data)
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module ActiverecordHoarder
|
2
|
+
class BatchQuery
|
3
|
+
SUBQUERY_DELETED_RECORDS = <<~SQL.strip_heredoc
|
4
|
+
SELECT
|
5
|
+
%{fields}
|
6
|
+
FROM
|
7
|
+
%{table_name}
|
8
|
+
WHERE
|
9
|
+
deleted_at IS NOT NULL
|
10
|
+
SQL
|
11
|
+
|
12
|
+
SUBQUERY_NON_DELETED_RECORDS = <<~SQL.strip_heredoc
|
13
|
+
SELECT
|
14
|
+
%{fields}
|
15
|
+
FROM
|
16
|
+
%{table_name}
|
17
|
+
WHERE
|
18
|
+
deleted_at IS NULL
|
19
|
+
SQL
|
20
|
+
|
21
|
+
QUERY_TEMPLATE_FOR_DATE_DELETION = <<~SQL.strip_heredoc
|
22
|
+
DELETE FROM %{table_name} WHERE date(created_at) = '%{date}';
|
23
|
+
SQL
|
24
|
+
|
25
|
+
QUERY_TEMPLATE_FOR_RECORD_WITH_LIMIT = <<~SQL.strip_heredoc
|
26
|
+
SELECT
|
27
|
+
%{fields}
|
28
|
+
FROM
|
29
|
+
%{table_name}
|
30
|
+
WHERE
|
31
|
+
date(created_at) = (
|
32
|
+
SELECT
|
33
|
+
min(dates_with_deleted.creation_date)
|
34
|
+
FROM
|
35
|
+
(
|
36
|
+
#{SUBQUERY_DELETED_RECORDS % {
|
37
|
+
fields: "date(created_at) as creation_date",
|
38
|
+
table_name: "%{table_name}"
|
39
|
+
}}
|
40
|
+
) as dates_with_deleted
|
41
|
+
LEFT OUTER JOIN
|
42
|
+
(
|
43
|
+
#{SUBQUERY_NON_DELETED_RECORDS % {
|
44
|
+
fields: "date(created_at) as creation_date",
|
45
|
+
table_name: "%{table_name}"
|
46
|
+
}}
|
47
|
+
) as dates_with_non_deleted
|
48
|
+
ON
|
49
|
+
dates_with_deleted.creation_date = dates_with_non_deleted.creation_date
|
50
|
+
WHERE
|
51
|
+
dates_with_non_deleted.creation_date IS NULL
|
52
|
+
AND
|
53
|
+
created_at < '%{limit}'
|
54
|
+
)
|
55
|
+
;
|
56
|
+
SQL
|
57
|
+
|
58
|
+
def initialize(limit, model_class)
|
59
|
+
@limit = limit
|
60
|
+
@model_class = model_class
|
61
|
+
end
|
62
|
+
|
63
|
+
def delete(date)
|
64
|
+
QUERY_TEMPLATE_FOR_DATE_DELETION % {
|
65
|
+
fields: @model_class.column_names.join(", "),
|
66
|
+
date: date,
|
67
|
+
table_name: table_name
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
def fetch
|
72
|
+
QUERY_TEMPLATE_FOR_RECORD_WITH_LIMIT % {
|
73
|
+
fields: @model_class.column_names.join(", "),
|
74
|
+
limit: @limit,
|
75
|
+
table_name: table_name
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def table_name
|
82
|
+
@model_class.table_name
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ActiverecordHoarder
|
2
|
+
module Restore
|
3
|
+
def self.included(base)
|
4
|
+
base.extend(ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def restore_archive_records(date)
|
9
|
+
storage = ::ActiverecordHoarder::Storage.new(self.table_name)
|
10
|
+
key = ::ActiverecordHoarder::StorageKey.from_date(date, :json)
|
11
|
+
dataIO = storage.fetch_data(key)
|
12
|
+
create(JSON.parse(dataIO.read))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class ::ActiverecordHoarder::Storage
|
2
|
+
class_attribute :storage, :storage_options
|
3
|
+
|
4
|
+
def self.configure(storage:, storage_options:)
|
5
|
+
::ActiverecordHoarder::Storages.is_valid_storage?(storage)
|
6
|
+
|
7
|
+
self.storage_options = storage_options
|
8
|
+
self.storage = storage
|
9
|
+
self
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def self.new(table_name, storage_override: nil, storage_options_override: {})
|
15
|
+
self.check_configured
|
16
|
+
storage_class = ::ActiverecordHoarder::Storages.retrieve(storage_override || storage)
|
17
|
+
storage_class.new(table_name, storage_options.merge(storage_options_override))
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.check_configured
|
21
|
+
raise ::ActiverecordHoarder::StorageError.new("storage needs to be configured") unless is_configured?
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.is_configured?
|
25
|
+
storage.present? && storage_options.is_a?(Hash)
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module ActiverecordHoarder
|
2
|
+
class StorageKey
|
3
|
+
def self.from_date(date, file_extension = nil)
|
4
|
+
key_parts = [date.year.to_s, date.month.to_s, date.iso8601]
|
5
|
+
new(key_parts, file_extension)
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(key_parts, file_extension)
|
9
|
+
@key_parts = key_parts
|
10
|
+
@file_extension = file_extension
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
key_without_extension = File.join(@key_parts)
|
15
|
+
@file_extension.present? ? key_without_extension + '.' + @file_extension.to_s : key_without_extension
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module ::ActiverecordHoarder
|
2
|
+
class Storages
|
3
|
+
STORAGE_DICT = {
|
4
|
+
aws_s3: ::ActiverecordHoarder::AwsS3
|
5
|
+
}
|
6
|
+
|
7
|
+
def self.check_storage(storage_key)
|
8
|
+
raise ::ActiverecordHoarder::StorageError.new("unknown storage (#{storage_key}), known keys are #{STORAGE_DICT.keys}") if !is_valid_storage?(storage_key)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.is_valid_storage?(storage_key)
|
12
|
+
STORAGE_DICT.keys.include?(storage_key)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.retrieve(storage_key)
|
16
|
+
check_storage(storage_key)
|
17
|
+
STORAGE_DICT[storage_key]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'activerecord_hoarder/aws_s3_storage'
|
2
|
+
require 'activerecord_hoarder/batch'
|
3
|
+
require 'activerecord_hoarder/batch_archiver'
|
4
|
+
require 'activerecord_hoarder/core'
|
5
|
+
require 'activerecord_hoarder/record_collector'
|
6
|
+
require 'activerecord_hoarder/record_query'
|
7
|
+
require 'activerecord_hoarder/restore'
|
8
|
+
require 'activerecord_hoarder/serializer'
|
9
|
+
require 'activerecord_hoarder/storage'
|
10
|
+
require 'activerecord_hoarder/storage_error'
|
11
|
+
require 'activerecord_hoarder/storage_key'
|
12
|
+
require 'activerecord_hoarder/storages'
|
13
|
+
|
14
|
+
module ActiverecordHoarder
|
15
|
+
def acts_as_hoarder
|
16
|
+
include ActiverecordHoarder::Core
|
17
|
+
include ActiverecordHoarder::Restore
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
::ActiveRecord::Base.send :extend, ActiverecordHoarder
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
DATA_TEMPLATE = "data for key %{key}"
|
4
|
+
|
5
|
+
RSpec.describe ::ActiverecordHoarder::AwsS3 do
|
6
|
+
describe ".fetch_data" do
|
7
|
+
let(:date_data) { DATA_TEMPLATE % { key: full_key } }
|
8
|
+
let(:full_key) { File.join(table_name, key_string) }
|
9
|
+
let(:key) { double(key_string, to_s: key_string) }
|
10
|
+
let(:key_string) { "key" }
|
11
|
+
let(:storage) { ::ActiverecordHoarder::AwsS3.new(table_name, storage_options) }
|
12
|
+
let(:storage_options) { {} }
|
13
|
+
let(:table_name) { "records" }
|
14
|
+
|
15
|
+
before do
|
16
|
+
allow(storage).to receive(:s3_client).and_return(Aws::S3::Client.new(stub_responses: true))
|
17
|
+
allow_any_instance_of(Aws::S3::Client).to receive(:get_object) do |object, args|
|
18
|
+
double(body: DATA_TEMPLATE % { key: args[:key] })
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
it "returns the archive entry for given date" do
|
23
|
+
expect(storage.fetch_data(key)).to eq(date_data)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe ::ActiverecordHoarder::BatchArchiver do
|
4
|
+
let(:model_class) { double("Record", table_name: "records") }
|
5
|
+
|
6
|
+
it "creates instance that can archive_batch" do
|
7
|
+
allow(::ActiverecordHoarder::Storage).to receive(:new).and_return(double)
|
8
|
+
expect(described_class.new(model_class)).to respond_to(:archive_batch)
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe 'ActiverecordHoarder::Core' do
|
4
|
+
context 'included and instantiated' do
|
5
|
+
describe 'hoard' do
|
6
|
+
it "it works, no questions asked" do
|
7
|
+
expect(::ActiverecordHoarder::Storage).to receive(:new).and_return(double)
|
8
|
+
expect_any_instance_of(::ActiverecordHoarder::BatchArchiver).to receive(:archive_batch)
|
9
|
+
expect{ ExampleHoarder.hoard }.not_to raise_error
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe ::ActiverecordHoarder::Storage do
|
4
|
+
let(:stub_model) { double }
|
5
|
+
|
6
|
+
before do
|
7
|
+
allow(stub_model).to receive(:table_name)
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "new" do
|
11
|
+
let(:storage) { ::ActiverecordHoarder::Storage.new(stub_model) }
|
12
|
+
|
13
|
+
context "not configured" do
|
14
|
+
it "fails and complains about configuration" do
|
15
|
+
expect{ ::ActiverecordHoarder::Storage.new(stub_model) }.to raise_error(::ActiverecordHoarder::StorageError)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
context "aws_s3" do
|
20
|
+
before do
|
21
|
+
::ActiverecordHoarder::Storage.configure(storage: :aws_s3, storage_options: {})
|
22
|
+
end
|
23
|
+
|
24
|
+
it "returns an aws storage" do
|
25
|
+
expect(storage.class).to be(::ActiverecordHoarder::AwsS3)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
RSpec.describe ActiverecordHoarder do
|
4
|
+
|
5
|
+
around(:all) do |example|
|
6
|
+
current_zone = Time.zone
|
7
|
+
Time.zone = "America/Chicago"
|
8
|
+
Timecop.freeze((Date.today.beginning_of_week - 3).to_time(:utc).end_of_day) do
|
9
|
+
example.run
|
10
|
+
end
|
11
|
+
Time.zone = current_zone
|
12
|
+
end
|
13
|
+
|
14
|
+
it "has a version number" do
|
15
|
+
expect(ActiverecordHoarder::VERSION).not_to be nil
|
16
|
+
end
|
17
|
+
|
18
|
+
it "extends ::ActiveRecord::Base with acts_as_hoarder" do
|
19
|
+
expect(::ActiveRecord::Base.methods).to include(:acts_as_hoarder)
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "acts_as_hoarder" do
|
23
|
+
context "successfully included in ActiveRecord model" do
|
24
|
+
it "extends with public class method .hoard" do
|
25
|
+
expect(ExampleHoarder.methods).to include(:hoard)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "record archiving" do
|
31
|
+
let(:archive_data) {
|
32
|
+
@archivable_records.group_by { |item|
|
33
|
+
item.created_at.getutc.to_date
|
34
|
+
} .collect { |date, group|
|
35
|
+
JSON.pretty_generate(group.collect(&:serializable_hash))
|
36
|
+
}
|
37
|
+
}
|
38
|
+
let(:storage) { double }
|
39
|
+
|
40
|
+
before :each do
|
41
|
+
allow(::ActiverecordHoarder::Storage).to receive(:new).and_return(storage)
|
42
|
+
allow(storage).to receive(:store_data).and_return(true)
|
43
|
+
end
|
44
|
+
|
45
|
+
context "with records only in current week" do
|
46
|
+
before do
|
47
|
+
@archivable_records = create_list(
|
48
|
+
:examples_in_range,
|
49
|
+
20,
|
50
|
+
deleted: true,
|
51
|
+
start_time: Time.now.getutc.beginning_of_week,
|
52
|
+
end_time: (Date.today - 1).to_time(:utc).end_of_day
|
53
|
+
)
|
54
|
+
@non_archivable_records = create_list(
|
55
|
+
:examples_on_date,
|
56
|
+
4,
|
57
|
+
deleted: true,
|
58
|
+
records_date: Date.today
|
59
|
+
)
|
60
|
+
ExampleHoarder.hoard
|
61
|
+
end
|
62
|
+
|
63
|
+
it "ignores current day" do
|
64
|
+
expect(ExampleHoarder.unscoped.to_a).to include(*@non_archivable_records)
|
65
|
+
end
|
66
|
+
|
67
|
+
it "archives previous days" do
|
68
|
+
expect(ExampleHoarder.unscoped.to_a).not_to include(*@archivable_records)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
context "with records in multiple weeks, non-deleted records mixed in and trailing" do
|
73
|
+
before do
|
74
|
+
@archivable_records = create_list(
|
75
|
+
:examples_in_range,
|
76
|
+
20,
|
77
|
+
deleted: true,
|
78
|
+
end_time: (1.week.ago.getutc.beginning_of_week + 2.days).end_of_day,
|
79
|
+
start_time: 1.week.ago.getutc.beginning_of_week
|
80
|
+
) + create_list(
|
81
|
+
:examples_in_range,
|
82
|
+
20,
|
83
|
+
deleted: true,
|
84
|
+
end_time: 1.week.ago.getutc.end_of_week,
|
85
|
+
start_time: 1.week.ago.getutc.beginning_of_week + 4.days
|
86
|
+
)
|
87
|
+
@non_archivable_records = create_list(
|
88
|
+
:examples_on_date,
|
89
|
+
2,
|
90
|
+
deleted: true,
|
91
|
+
records_date: (1.week.ago.getutc.beginning_of_week - 1.day).to_date
|
92
|
+
) + create_list(
|
93
|
+
:examples_on_date,
|
94
|
+
1,
|
95
|
+
deleted: false,
|
96
|
+
records_date: (1.week.ago.getutc.beginning_of_week - 1.day).to_date
|
97
|
+
) + create_list(
|
98
|
+
:examples_on_date,
|
99
|
+
4,
|
100
|
+
deleted: true,
|
101
|
+
records_date: (1.week.ago.getutc.beginning_of_week + 3.days).to_date,
|
102
|
+
) + create_list(
|
103
|
+
:examples_on_date,
|
104
|
+
2,
|
105
|
+
deleted: false,
|
106
|
+
records_date: (1.week.ago.getutc.beginning_of_week + 3.days).to_date
|
107
|
+
)
|
108
|
+
@out_of_range_records = create_list(
|
109
|
+
:examples_in_range,
|
110
|
+
2,
|
111
|
+
deleted: true,
|
112
|
+
end_time: Time.now,
|
113
|
+
start_time: Time.now.getutc.beginning_of_week
|
114
|
+
) + create_list(
|
115
|
+
:examples_in_range,
|
116
|
+
2,
|
117
|
+
deleted: false,
|
118
|
+
end_time: Time.now,
|
119
|
+
start_time: Time.now.getutc.beginning_of_week
|
120
|
+
)
|
121
|
+
ExampleHoarder.hoard
|
122
|
+
end
|
123
|
+
|
124
|
+
it "skips records from days with active records" do
|
125
|
+
expect(ExampleHoarder.unscoped.to_a).to include(*@non_archivable_records)
|
126
|
+
end
|
127
|
+
|
128
|
+
it "archives one week of fully deleted records" do
|
129
|
+
expect(ExampleHoarder.unscoped.to_a).not_to include(*@archivable_records)
|
130
|
+
end
|
131
|
+
|
132
|
+
it "stops after one week" do
|
133
|
+
expect(ExampleHoarder.unscoped.to_a).to include(*@out_of_range_records)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
describe "workflow" do
|
139
|
+
let(:batch1) { double }
|
140
|
+
let(:collector) { ::ActiverecordHoarder::RecordCollector.new(ExampleHoarder) }
|
141
|
+
let(:storage) { double }
|
142
|
+
|
143
|
+
before do
|
144
|
+
allow(::ActiverecordHoarder::Storage).to receive(:new).and_return(storage)
|
145
|
+
allow(::ActiverecordHoarder::RecordCollector).to receive(:new).and_return(collector)
|
146
|
+
allow(collector).to receive(:collect_batch).and_return(true, false)
|
147
|
+
allow(collector).to receive(:batch_data_cached?).and_return(true)
|
148
|
+
allow_any_instance_of(::ActiverecordHoarder::BatchArchiver).to receive(:compose_key).and_return("key")
|
149
|
+
allow(collector).to receive(:destroy_current_records!)
|
150
|
+
end
|
151
|
+
|
152
|
+
after do
|
153
|
+
ExampleHoarder.hoard
|
154
|
+
end
|
155
|
+
|
156
|
+
it "fully processes one record batch before moving on to the next" do
|
157
|
+
expect(collector).to receive(:collect_batch)
|
158
|
+
expect(storage).to receive(:store_data).and_return(true)
|
159
|
+
expect(collector).to receive(:destroy_current_records!)
|
160
|
+
expect(collector).to receive(:collect_batch)
|
161
|
+
end
|
162
|
+
|
163
|
+
it "does not delete a record that wasn't successfully archived" do
|
164
|
+
expect(storage).to receive(:store_data).and_return(false)
|
165
|
+
expect(collector).not_to receive(:destroy_current_records!)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
def datetimes_in_range(n, step: 5, start:, stop:)
|
2
|
+
seconds_in_range = stop - start
|
3
|
+
seconds_ago = Time.now.getutc - stop
|
4
|
+
((n*(step.hours+1.minute + 1.second)) % seconds_in_range + seconds_ago).seconds.ago
|
5
|
+
end
|
6
|
+
|
7
|
+
FactoryGirl.define do
|
8
|
+
factory :example_hoarder do
|
9
|
+
transient do
|
10
|
+
deleted false
|
11
|
+
end
|
12
|
+
|
13
|
+
after :build do |record, evaluator|
|
14
|
+
if evaluator.deleted
|
15
|
+
record.deleted_at = record.created_at
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
factory :examples_in_range, parent: :example_hoarder do
|
21
|
+
transient do
|
22
|
+
end_time Time.now
|
23
|
+
start_time 2.weeks.ago
|
24
|
+
step 5
|
25
|
+
end
|
26
|
+
|
27
|
+
sequence(:created_at, 0) { |n| datetimes_in_range(n, step: step, start: start_time, stop: end_time) }
|
28
|
+
end
|
29
|
+
|
30
|
+
factory :examples_on_date, parent: :example_hoarder do
|
31
|
+
transient do
|
32
|
+
records_date Date.today - 1
|
33
|
+
step 5
|
34
|
+
end
|
35
|
+
|
36
|
+
sequence(:created_at, 0) { |n|
|
37
|
+
created_at_value = datetimes_in_range(n, step: step, start: records_date.to_time(:utc), stop: records_date.to_time(:utc).end_of_day)
|
38
|
+
}
|
39
|
+
end
|
40
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require "bundler/setup"
|
2
|
+
require "active_record"
|
3
|
+
require "aws-sdk-s3"
|
4
|
+
|
5
|
+
require "activerecord_hoarder"
|
6
|
+
|
7
|
+
require "factory_girl_rails"
|
8
|
+
require "pp"
|
9
|
+
require "timecop"
|
10
|
+
|
11
|
+
Dir.glob("spec/support/*.rb").each do |file| require File.expand_path(file) end
|
12
|
+
|
13
|
+
class ExampleHoarder < ActiveRecord::Base
|
14
|
+
acts_as_hoarder
|
15
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
RSpec.configure do |config|
|
2
|
+
# Enable flags like --only-failures and --next-failure
|
3
|
+
config.example_status_persistence_file_path = ".rspec_status"
|
4
|
+
|
5
|
+
# Disable RSpec exposing methods globally on `Module` and `main`
|
6
|
+
config.disable_monkey_patching!
|
7
|
+
|
8
|
+
config.expect_with :rspec do |c|
|
9
|
+
c.syntax = :expect
|
10
|
+
end
|
11
|
+
|
12
|
+
config.include FactoryGirl::Syntax::Methods
|
13
|
+
FactoryGirl.find_definitions
|
14
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
ActiveRecord::Base.establish_connection(YAML.load_file("config/dbspec_rspec.yml"))
|
@@ -0,0 +1,14 @@
|
|
1
|
+
ActiveRecord::Schema.define do
|
2
|
+
create_table "example_hoarders", force: true do |t|
|
3
|
+
t.datetime "created_at"
|
4
|
+
t.datetime "deleted_at"
|
5
|
+
t.datetime "updated_at"
|
6
|
+
t.string "content"
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
RSpec.configure do |config|
|
11
|
+
config.after(:each) do
|
12
|
+
ActiveRecord::Base.connection.execute("DELETE FROM example_hoarders;")
|
13
|
+
end
|
14
|
+
end
|
metadata
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activerecord_hoarder
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Matthias Engh
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-11-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.2'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '6.0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '4.2'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '6.0'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: bundler
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '1.15'
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '1.15'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rake
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '10.0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '10.0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: rspec
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '3.0'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '3.0'
|
75
|
+
description: extend active records with simple archiving mechanics
|
76
|
+
email:
|
77
|
+
- matthias@wescrimmage.com
|
78
|
+
executables:
|
79
|
+
- console
|
80
|
+
- setup
|
81
|
+
extensions: []
|
82
|
+
extra_rdoc_files: []
|
83
|
+
files:
|
84
|
+
- ".gitignore"
|
85
|
+
- ".rspec"
|
86
|
+
- ".travis.yml"
|
87
|
+
- Gemfile
|
88
|
+
- LICENSE.txt
|
89
|
+
- README.md
|
90
|
+
- Rakefile
|
91
|
+
- activerecord_hoarder.gemspec
|
92
|
+
- bin/console
|
93
|
+
- bin/setup
|
94
|
+
- config/activerecord_hoarder.yml.template
|
95
|
+
- config/dbspec.yml.template
|
96
|
+
- config/dbspec_rspec.yml.template
|
97
|
+
- example/example.rb
|
98
|
+
- example/fixture.rb
|
99
|
+
- example/schema.rb
|
100
|
+
- lib/activerecord_hoarder.rb
|
101
|
+
- lib/activerecord_hoarder/aws_s3_storage.rb
|
102
|
+
- lib/activerecord_hoarder/batch.rb
|
103
|
+
- lib/activerecord_hoarder/batch_archiver.rb
|
104
|
+
- lib/activerecord_hoarder/core.rb
|
105
|
+
- lib/activerecord_hoarder/record_collector.rb
|
106
|
+
- lib/activerecord_hoarder/record_query.rb
|
107
|
+
- lib/activerecord_hoarder/restore.rb
|
108
|
+
- lib/activerecord_hoarder/serializer.rb
|
109
|
+
- lib/activerecord_hoarder/storage.rb
|
110
|
+
- lib/activerecord_hoarder/storage_error.rb
|
111
|
+
- lib/activerecord_hoarder/storage_key.rb
|
112
|
+
- lib/activerecord_hoarder/storages.rb
|
113
|
+
- lib/activerecord_hoarder/version.rb
|
114
|
+
- spec/activerecord_hoarder/aws_s3_storage_spec.rb
|
115
|
+
- spec/activerecord_hoarder/batch_archiver_spec.rb
|
116
|
+
- spec/activerecord_hoarder/core_spec.rb
|
117
|
+
- spec/activerecord_hoarder/serializer_spec.rb
|
118
|
+
- spec/activerecord_hoarder/storage_spec.rb
|
119
|
+
- spec/activerecord_hoarder_spec.rb
|
120
|
+
- spec/factories/examples.rb
|
121
|
+
- spec/spec_helper.rb
|
122
|
+
- spec/support/0_rspec_configuration.rb
|
123
|
+
- spec/support/1_active_record_configuration.rb
|
124
|
+
- spec/support/2_database_content.rb
|
125
|
+
- spec/support/3_test_storage.rb
|
126
|
+
homepage: https://github.com/Scrimmage/gem_batch_archiving
|
127
|
+
licenses:
|
128
|
+
- MIT
|
129
|
+
metadata: {}
|
130
|
+
post_install_message:
|
131
|
+
rdoc_options: []
|
132
|
+
require_paths:
|
133
|
+
- lib
|
134
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
140
|
+
requirements:
|
141
|
+
- - ">="
|
142
|
+
- !ruby/object:Gem::Version
|
143
|
+
version: '0'
|
144
|
+
requirements: []
|
145
|
+
rubyforge_project:
|
146
|
+
rubygems_version: 2.4.8
|
147
|
+
signing_key:
|
148
|
+
specification_version: 4
|
149
|
+
summary: hoards records
|
150
|
+
test_files:
|
151
|
+
- spec/activerecord_hoarder/aws_s3_storage_spec.rb
|
152
|
+
- spec/activerecord_hoarder/batch_archiver_spec.rb
|
153
|
+
- spec/activerecord_hoarder/core_spec.rb
|
154
|
+
- spec/activerecord_hoarder/serializer_spec.rb
|
155
|
+
- spec/activerecord_hoarder/storage_spec.rb
|
156
|
+
- spec/activerecord_hoarder_spec.rb
|
157
|
+
- spec/factories/examples.rb
|
158
|
+
- spec/spec_helper.rb
|
159
|
+
- spec/support/0_rspec_configuration.rb
|
160
|
+
- spec/support/1_active_record_configuration.rb
|
161
|
+
- spec/support/2_database_content.rb
|
162
|
+
- spec/support/3_test_storage.rb
|