s3_meta_sync 0.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.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +2 -0
- data/bin/s3-meta-sync +5 -0
- data/lib/s3_meta_sync/version.rb +3 -0
- data/lib/s3_meta_sync.rb +162 -0
- data.tar.gz.sig +0 -0
- metadata +82 -0
- metadata.gz.sig +1 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: dc3bcfa388ab131fa0a8cd4117dac7c2e21460e7
|
4
|
+
data.tar.gz: 311333996d21e3ee022dd1beb8727b5298b2b5d6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1d6d33a5fbfac5e447a0aa76470d35ab31e691bca5a5464af9d767d2a2aac569d61669a75d14e32d177f18cb2bf8ec68dabd1b0adae3c755e910c1f5d02fd5f1
|
7
|
+
data.tar.gz: 3b2adea548763266b7c439148d629c6ac521567aea458158c15be01bf7c81cf481de867790998dd2c7695b4a603d98e9344e4e79a3a6cd09c3e603c0ba2ca65b
|
checksums.yaml.gz.sig
ADDED
data/bin/s3-meta-sync
ADDED
data/lib/s3_meta_sync.rb
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
require "s3_meta_sync/version"
|
2
|
+
require "open-uri"
|
3
|
+
require "yaml"
|
4
|
+
require "digest/md5"
|
5
|
+
require "aws/s3"
|
6
|
+
require "optparse"
|
7
|
+
|
8
|
+
module S3MetaSync
|
9
|
+
RemoteWithoutMeta = Class.new(Exception)
|
10
|
+
META_FILE = ".s3-meta-sync"
|
11
|
+
|
12
|
+
class Syncer
|
13
|
+
def initialize(config)
|
14
|
+
@config = config
|
15
|
+
end
|
16
|
+
|
17
|
+
def sync(source, destination)
|
18
|
+
raise if source.end_with?("/") or destination.end_with?("/")
|
19
|
+
|
20
|
+
if destination.include?(":")
|
21
|
+
@bucket, destination = destination.split(":")
|
22
|
+
upload(source, destination)
|
23
|
+
else
|
24
|
+
@bucket, source = source.split(":")
|
25
|
+
download(source, destination)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def upload(source, destination)
|
32
|
+
remote_info = begin
|
33
|
+
download_meta(destination)
|
34
|
+
rescue RemoteWithoutMeta
|
35
|
+
{}
|
36
|
+
end
|
37
|
+
generate_meta(source)
|
38
|
+
local_info = read_meta(source)
|
39
|
+
|
40
|
+
local_info.each do |path, md5|
|
41
|
+
next if remote_info[path] == md5
|
42
|
+
upload_file(source, path, destination)
|
43
|
+
end
|
44
|
+
|
45
|
+
(remote_info.keys - local_info.keys).each do |path|
|
46
|
+
delete_remote_file(destination, path)
|
47
|
+
end
|
48
|
+
|
49
|
+
upload_file(source, META_FILE, destination)
|
50
|
+
end
|
51
|
+
|
52
|
+
def upload_file(source, path, destination)
|
53
|
+
s3.objects["#{destination}/#{path}"].write File.read("#{source}/#{path}"), :acl => :public_read
|
54
|
+
end
|
55
|
+
|
56
|
+
def delete_remote_file(remote, path)
|
57
|
+
s3.objects["#{remote}/#{path}"].delete
|
58
|
+
end
|
59
|
+
|
60
|
+
def delete_local_file(local, path)
|
61
|
+
File.delete("#{local}/#{path}")
|
62
|
+
end
|
63
|
+
|
64
|
+
def s3
|
65
|
+
@s3 ||= ::AWS::S3.new(:access_key_id => @config[:key], :secret_access_key => @config[:secret]).buckets[@bucket]
|
66
|
+
end
|
67
|
+
|
68
|
+
def generate_meta(source)
|
69
|
+
meta = Hash[Dir["#{source}/**/*"].select { |f| File.file?(f) }.map do |file|
|
70
|
+
[file.sub("#{source}/", ""), Digest::MD5.file(file).to_s]
|
71
|
+
end]
|
72
|
+
file = "#{source}/#{META_FILE}"
|
73
|
+
FileUtils.mkdir_p(File.dirname(file))
|
74
|
+
File.write(file, meta.to_yaml)
|
75
|
+
end
|
76
|
+
|
77
|
+
def read_meta(source)
|
78
|
+
file = "#{source}/#{META_FILE}"
|
79
|
+
File.exist?(file) ? YAML.load(File.read(file)) : {}
|
80
|
+
end
|
81
|
+
|
82
|
+
def download_meta(destination)
|
83
|
+
content = download_content("#{destination}/#{META_FILE}")
|
84
|
+
YAML.load(content)
|
85
|
+
rescue
|
86
|
+
raise RemoteWithoutMeta
|
87
|
+
end
|
88
|
+
|
89
|
+
def download_file(source, path, destination)
|
90
|
+
content = download_content("#{source}/#{path}")
|
91
|
+
file = "#{destination}/#{path}"
|
92
|
+
FileUtils.mkdir_p(File.dirname(file))
|
93
|
+
File.write(file, content)
|
94
|
+
end
|
95
|
+
|
96
|
+
def download_content(path)
|
97
|
+
url = "https://s3#{"-#{region}" if region}.amazonaws.com/#{@bucket}/#{path}"
|
98
|
+
open(url).read
|
99
|
+
rescue OpenURI::HTTPError
|
100
|
+
raise "Unable to download #{url} -- #{$!}"
|
101
|
+
end
|
102
|
+
|
103
|
+
def download(source, destination)
|
104
|
+
remote_info = download_meta(source)
|
105
|
+
generate_meta(destination)
|
106
|
+
local_info = read_meta(destination) # TODO maybe generate !?
|
107
|
+
|
108
|
+
remote_info.each do |path, md5|
|
109
|
+
next if local_info[path] == md5
|
110
|
+
download_file(source, path, destination)
|
111
|
+
end
|
112
|
+
|
113
|
+
(local_info.keys - remote_info.keys).each do |path|
|
114
|
+
delete_local_file(destination, path)
|
115
|
+
end
|
116
|
+
|
117
|
+
download_file(source, META_FILE, destination)
|
118
|
+
|
119
|
+
`find #{destination} -depth -empty -delete`
|
120
|
+
end
|
121
|
+
|
122
|
+
def region
|
123
|
+
@config[:region] unless @config[:region].to_s.empty?
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
class << self
|
128
|
+
def run(argv)
|
129
|
+
source, dest, options = parse_options(argv)
|
130
|
+
Syncer.new(options).sync(source, dest)
|
131
|
+
0
|
132
|
+
end
|
133
|
+
|
134
|
+
def parse_options(argv)
|
135
|
+
options = {}
|
136
|
+
OptionParser.new do |opts|
|
137
|
+
opts.banner = <<-BANNER.gsub(/^ {10}/, "")
|
138
|
+
Sync folders with s3 using a metadata file with md5 sums.
|
139
|
+
|
140
|
+
# upload local files and remove everything that is not local
|
141
|
+
s3-meta-sync <local> <bucket:folder> --key <aws-access-key> --secret <aws-secret-key>
|
142
|
+
|
143
|
+
# download files and remove everything that is not remote
|
144
|
+
s3-meta-sync <bucket:folder> <local> # no credentials required
|
145
|
+
|
146
|
+
|
147
|
+
Options:
|
148
|
+
BANNER
|
149
|
+
opts.on("-k", "--key KEY", "AWS access key") { |c| options[:key] = c }
|
150
|
+
opts.on("-s", "--secret SECRET", "AWS secret key") { |c| options[:secret] = c }
|
151
|
+
opts.on("-r", "--region REGION", "AWS region if not us-standard") { |c| options[:region] = c }
|
152
|
+
opts.on("-h", "--help", "Show this.") { puts opts; exit }
|
153
|
+
opts.on("-v", "--version", "Show Version"){ puts VERSION; exit}
|
154
|
+
end.parse!(argv)
|
155
|
+
|
156
|
+
raise "need source and destination" unless argv.size == 2
|
157
|
+
raise "need credentials --key + --secret" if argv.last.include?(":") and not options[:key] or not options[:secret]
|
158
|
+
|
159
|
+
[*argv, options]
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
data.tar.gz.sig
ADDED
Binary file
|
metadata
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: s3_meta_sync
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Michael Grosser
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain:
|
11
|
+
- |
|
12
|
+
-----BEGIN CERTIFICATE-----
|
13
|
+
MIIDMjCCAhqgAwIBAgIBADANBgkqhkiG9w0BAQUFADA/MRAwDgYDVQQDDAdtaWNo
|
14
|
+
YWVsMRcwFQYKCZImiZPyLGQBGRYHZ3Jvc3NlcjESMBAGCgmSJomT8ixkARkWAml0
|
15
|
+
MB4XDTEzMDIwMzE4MTMxMVoXDTE0MDIwMzE4MTMxMVowPzEQMA4GA1UEAwwHbWlj
|
16
|
+
aGFlbDEXMBUGCgmSJomT8ixkARkWB2dyb3NzZXIxEjAQBgoJkiaJk/IsZAEZFgJp
|
17
|
+
dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMorXo/hgbUq97+kII9H
|
18
|
+
MsQcLdC/7wQ1ZP2OshVHPkeP0qH8MBHGg6eYisOX2ubNagF9YTCZWnhrdKrwpLOO
|
19
|
+
cPLaZbjUjljJ3cQR3B8Yn1veV5IhG86QseTBjymzJWsLpqJ1UZGpfB9tXcsFtuxO
|
20
|
+
6vHvcIHdzvc/OUkICttLbH+1qb6rsHUceqh+JrH4GrsJ5H4hAfIdyS2XMK7YRKbh
|
21
|
+
h+IBu6dFWJJByzFsYmV1PDXln3UBmgAt65cmCu4qPfThioCGDzbSJrGDGLmw/pFX
|
22
|
+
FPpVCm1zgYSb1v6Qnf3cgXa2f2wYGm17+zAVyIDpwryFru9yF/jJxE38z/DRsd9R
|
23
|
+
/88CAwEAAaM5MDcwCQYDVR0TBAIwADAdBgNVHQ4EFgQUsiNnXHtKeMYYcr4yJVmQ
|
24
|
+
WONL+IwwCwYDVR0PBAQDAgSwMA0GCSqGSIb3DQEBBQUAA4IBAQAlyN7kKo/NQCQ0
|
25
|
+
AOzZLZ3WAePvStkCFIJ53tsv5Kyo4pMAllv+BgPzzBt7qi605mFSL6zBd9uLou+W
|
26
|
+
Co3s48p1dy7CjjAfVQdmVNHF3MwXtfC2OEyvSQPi4xKR8iba8wa3xp9LVo1PuLpw
|
27
|
+
/6DsrChWw74HfsJN6qJOK684hJeT8lBYAUfiC3wD0owoPSg+XtyAAddisR+KV5Y1
|
28
|
+
NmVHuLtQcNTZy+gRht3ahJRMuC6QyLmkTsf+6MaenwAMkAgHdswGsJztOnNnBa3F
|
29
|
+
y0kCSWmK6D+x/SbfS6r7Ke07MRqziJdB9GuE1+0cIRuFh8EQ+LN6HXCKM5pon/GU
|
30
|
+
ycwMXfl0
|
31
|
+
-----END CERTIFICATE-----
|
32
|
+
date: 2013-09-25 00:00:00.000000000 Z
|
33
|
+
dependencies:
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: aws-sdk
|
36
|
+
requirement: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
type: :runtime
|
42
|
+
prerelease: false
|
43
|
+
version_requirements: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
description:
|
49
|
+
email: michael@grosser.it
|
50
|
+
executables:
|
51
|
+
- s3-meta-sync
|
52
|
+
extensions: []
|
53
|
+
extra_rdoc_files: []
|
54
|
+
files:
|
55
|
+
- bin/s3-meta-sync
|
56
|
+
- lib/s3_meta_sync.rb
|
57
|
+
- lib/s3_meta_sync/version.rb
|
58
|
+
homepage: http://github.com/grosser/s3_meta_sync
|
59
|
+
licenses:
|
60
|
+
- MIT
|
61
|
+
metadata: {}
|
62
|
+
post_install_message:
|
63
|
+
rdoc_options: []
|
64
|
+
require_paths:
|
65
|
+
- lib
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - '>='
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
requirements: []
|
77
|
+
rubyforge_project:
|
78
|
+
rubygems_version: 2.0.6
|
79
|
+
signing_key:
|
80
|
+
specification_version: 4
|
81
|
+
summary: Sync folders with s3 using a metadata file and md5 diffs
|
82
|
+
test_files: []
|
metadata.gz.sig
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
<]k1��le-*�Cs;�?㣻K�c\
|