s3archive 1.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.
- data/bin/s3archive +4 -0
- data/lib/s3archive/compress_and_upload.rb +100 -0
- data/lib/s3archive/config.rb +26 -0
- data/lib/s3archive/logging.rb +9 -0
- data/lib/s3archive/s3_file_synchronizer.rb +113 -0
- data/lib/tasks/s3archive.thor +22 -0
- metadata +100 -0
data/bin/s3archive
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
# The main entry point db:backup_to_s3 is run by /etc/logrotate.d/rails3app
|
2
|
+
# daily. changed
|
3
|
+
|
4
|
+
require 'thor'
|
5
|
+
require 'socket' # For hostname
|
6
|
+
require 'tempfile'
|
7
|
+
require_relative 'logging'
|
8
|
+
require_relative 'config'
|
9
|
+
require_relative 's3_file_synchronizer'
|
10
|
+
|
11
|
+
module S3Archive
|
12
|
+
class CompressAndUpload
|
13
|
+
include Logging
|
14
|
+
|
15
|
+
def self.run(path)
|
16
|
+
new(path).run
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :path
|
20
|
+
def initialize(path)
|
21
|
+
@path = path
|
22
|
+
end
|
23
|
+
|
24
|
+
def run
|
25
|
+
unless File.exists?(path)
|
26
|
+
logger.error("COULD NOT FIND '#{path}'")
|
27
|
+
return
|
28
|
+
end
|
29
|
+
|
30
|
+
logger.info("* Processing #{path}")
|
31
|
+
|
32
|
+
compress! if compress?
|
33
|
+
upload!
|
34
|
+
delete_tempfile! if compress?
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
def compress?
|
39
|
+
@do_compress ||= begin
|
40
|
+
if path.end_with?('.gz')
|
41
|
+
logger.info("** #{path} already compressed, skipping compression")
|
42
|
+
false
|
43
|
+
else
|
44
|
+
true
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def compress!
|
50
|
+
logger.info("** Compressing #{path} to #{tempfile.path}")
|
51
|
+
system "gzip -n -c < #{path} > #{tempfile.path}"
|
52
|
+
end
|
53
|
+
|
54
|
+
def upload!
|
55
|
+
bucket = S3Archive.config.bucket
|
56
|
+
logger.info("** Uploading #{path_to_upload} to s3://#{bucket}/#{key}")
|
57
|
+
S3FileSynchronizer.run(path_to_upload, bucket, key)
|
58
|
+
end
|
59
|
+
|
60
|
+
def path_to_upload
|
61
|
+
compress? ? tempfile.path : path
|
62
|
+
end
|
63
|
+
|
64
|
+
def delete_tempfile!
|
65
|
+
logger.info("** Deleting #{tempfile.path}")
|
66
|
+
tempfile.unlink
|
67
|
+
end
|
68
|
+
|
69
|
+
def tempfile
|
70
|
+
@tempfile ||= begin
|
71
|
+
tempfile = Tempfile.new([filename, '.gz'])
|
72
|
+
tempfile.close # An external process will write to it
|
73
|
+
tempfile
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def key
|
78
|
+
year, month, day = Time.now.strftime("%Y-%m-%d").split('-')
|
79
|
+
[hostname, year, month, day, "#{filename}.gz"].join('/')
|
80
|
+
end
|
81
|
+
|
82
|
+
def filename
|
83
|
+
@filename ||= File.basename(path)
|
84
|
+
end
|
85
|
+
|
86
|
+
def hostname
|
87
|
+
Socket.gethostname
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
if $0 == __FILE__
|
94
|
+
path = File.join(File.dirname(__FILE__), 'config.rb')
|
95
|
+
S3Archive::CompressAndUpload.run(path)
|
96
|
+
|
97
|
+
# bucket = 'this.is.my.test.bucket'
|
98
|
+
# key = 'this is a folder/foobar'
|
99
|
+
# S3FileSynchronizer.run(in_path, bucket, key)
|
100
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module S3Archive
|
5
|
+
def self.config_path=(config_path)
|
6
|
+
@config_path = config_path
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.config_path
|
10
|
+
@config_path || "/etc/s3archive.yml"
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.config
|
14
|
+
@config ||= Config.new(YAML.load(File.read(config_path)))
|
15
|
+
end
|
16
|
+
|
17
|
+
class Config
|
18
|
+
attr_accessor :bucket, :access_key_id, :secret_access_key
|
19
|
+
|
20
|
+
def initialize(params = {})
|
21
|
+
params.each do |key, val|
|
22
|
+
send("#{key}=", val)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'right_aws'
|
2
|
+
require 'digest/md5'
|
3
|
+
require_relative 'logging'
|
4
|
+
|
5
|
+
module S3Archive
|
6
|
+
class S3FileSynchronizer
|
7
|
+
include Logging
|
8
|
+
|
9
|
+
attr_reader :local_file, :s3_file
|
10
|
+
def initialize(local_file, s3_file)
|
11
|
+
@local_file = local_file
|
12
|
+
@s3_file = s3_file
|
13
|
+
end
|
14
|
+
|
15
|
+
def run
|
16
|
+
if s3_file.exists?
|
17
|
+
if s3_file.md5_hex == local_file.md5_hex
|
18
|
+
logger.info("'#{s3_file}' already exists and has correct checksum")
|
19
|
+
nil
|
20
|
+
else
|
21
|
+
new_s3_file = S3File.new(s3_file.bucket, "#{s3_file.key}.#{local_file.md5_hex}")
|
22
|
+
logger.error("'#{s3_file}' already exists and has wrong checksum. Uploading to '#{new_s3_file}'.")
|
23
|
+
new_s3_file.put(local_file)
|
24
|
+
end
|
25
|
+
else
|
26
|
+
s3_file.put(local_file)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.run(local_path, bucket, key)
|
31
|
+
local_file = LocalFile.new(local_path)
|
32
|
+
s3_file = S3File.new(bucket, key)
|
33
|
+
|
34
|
+
new(local_file, s3_file).run
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Just a wrapper around a path with some md5 functions
|
39
|
+
LocalFile = Struct.new(:path) do
|
40
|
+
include Logging
|
41
|
+
|
42
|
+
def open(*args, &block)
|
43
|
+
File.open(path, *args, &block)
|
44
|
+
end
|
45
|
+
|
46
|
+
def md5_hex
|
47
|
+
md5.hexdigest
|
48
|
+
end
|
49
|
+
|
50
|
+
def md5_base64
|
51
|
+
md5.base64digest
|
52
|
+
end
|
53
|
+
|
54
|
+
def md5
|
55
|
+
@md5 ||= Digest::MD5.file(path)
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_s
|
59
|
+
path
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# A wrapper around a s3 path (bucket, key) with some md5 and a put function
|
64
|
+
S3File = Struct.new(:bucket, :key) do
|
65
|
+
include Logging
|
66
|
+
|
67
|
+
def md5_hex
|
68
|
+
exists? && headers.fetch("etag").tr('"', '')
|
69
|
+
end
|
70
|
+
|
71
|
+
def exists?
|
72
|
+
!headers.nil?
|
73
|
+
end
|
74
|
+
|
75
|
+
def put(local_file)
|
76
|
+
local_file.open do |file|
|
77
|
+
logger.info("Putting '#{local_file}' to '#{self}'")
|
78
|
+
s3interface.put(bucket, key, file, 'Content-MD5' => local_file.md5_base64)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def to_s
|
83
|
+
"s3://#{bucket}/#{key}"
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
def headers
|
88
|
+
@headers ||= without_close_on_error do
|
89
|
+
begin
|
90
|
+
s3interface.head(bucket, key)
|
91
|
+
rescue RightAws::AwsError => e
|
92
|
+
raise unless e.http_code.to_s == '404'
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def without_close_on_error(&block)
|
98
|
+
old_val = RightAws::AWSErrorHandler.close_on_error
|
99
|
+
RightAws::AWSErrorHandler.close_on_error = false
|
100
|
+
block.call
|
101
|
+
ensure
|
102
|
+
RightAws::AWSErrorHandler.close_on_error = old_val
|
103
|
+
end
|
104
|
+
|
105
|
+
def s3interface
|
106
|
+
@s3interface ||= RightAws::S3Interface.new(
|
107
|
+
S3Archive.config.access_key_id,
|
108
|
+
S3Archive.config.secret_access_key,
|
109
|
+
:logger => logger
|
110
|
+
)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'thor'
|
2
|
+
$:.unshift File.join(File.dirname(__FILE__), '..')
|
3
|
+
require 's3archive/compress_and_upload'
|
4
|
+
|
5
|
+
module S3Archive
|
6
|
+
class Cli < Thor
|
7
|
+
namespace :s3archive
|
8
|
+
|
9
|
+
class_option "-c",
|
10
|
+
:desc => "Path to config file",
|
11
|
+
:banner => "CONFIG_FILE",
|
12
|
+
:type => :string,
|
13
|
+
:aliases => "--config",
|
14
|
+
:default => "/etc/s3archive.yml"
|
15
|
+
|
16
|
+
desc "upload PATH", "Compresses PATH and uploads to s3://<bucket>/<year>/<month>/<day>/<filename>.gz"
|
17
|
+
def upload (orig_path, options = {})
|
18
|
+
S3Archive.config_path = self.options["c"] if self.options["c"]
|
19
|
+
CompressAndUpload.run(orig_path)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
metadata
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: s3archive
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '1.0'
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Petter Remen
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-05-30 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: thor
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.14.6
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 0.14.6
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: right_aws
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 3.0.0
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 3.0.0
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 2.9.0
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 2.9.0
|
62
|
+
description: ''
|
63
|
+
email:
|
64
|
+
- petter@spnab.com
|
65
|
+
executables:
|
66
|
+
- s3archive
|
67
|
+
extensions: []
|
68
|
+
extra_rdoc_files: []
|
69
|
+
files:
|
70
|
+
- bin/s3archive
|
71
|
+
- lib/s3archive/compress_and_upload.rb
|
72
|
+
- lib/s3archive/config.rb
|
73
|
+
- lib/s3archive/logging.rb
|
74
|
+
- lib/s3archive/s3_file_synchronizer.rb
|
75
|
+
- lib/tasks/s3archive.thor
|
76
|
+
homepage: http://github.com/spab/s3archive
|
77
|
+
licenses: []
|
78
|
+
post_install_message:
|
79
|
+
rdoc_options: []
|
80
|
+
require_paths:
|
81
|
+
- lib
|
82
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
requirements: []
|
95
|
+
rubyforge_project:
|
96
|
+
rubygems_version: 1.8.24
|
97
|
+
signing_key:
|
98
|
+
specification_version: 3
|
99
|
+
summary: Simple script to safely archive a file to S3
|
100
|
+
test_files: []
|