mir 0.1.4 → 0.1.5
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/.gitignore +1 -0
- data/doc/Mir.html +353 -0
- data/doc/Mir/Application.html +1067 -0
- data/doc/Mir/Config.html +660 -0
- data/doc/Mir/Disk.html +211 -0
- data/doc/Mir/Disk/Amazon.html +1563 -0
- data/doc/Mir/Disk/IncompleteTransmission.html +116 -0
- data/doc/Mir/Disk/MultiPartFile.html +586 -0
- data/doc/Mir/Disk/RemoteFileNotFound.html +116 -0
- data/doc/Mir/Index.html +785 -0
- data/doc/Mir/Models.html +108 -0
- data/doc/Mir/Models/AppSetting.html +370 -0
- data/doc/Mir/Models/Resource.html +1140 -0
- data/doc/Mir/Options.html +297 -0
- data/doc/Mir/UndefinedConfigValue.html +116 -0
- data/doc/Mir/Utils.html +582 -0
- data/doc/_index.html +263 -0
- data/doc/class_list.html +47 -0
- data/doc/css/common.css +1 -0
- data/doc/css/full_list.css +53 -0
- data/doc/css/style.css +320 -0
- data/doc/file.README.html +134 -0
- data/doc/file_list.html +49 -0
- data/doc/frames.html +13 -0
- data/doc/index.html +134 -0
- data/doc/js/app.js +205 -0
- data/doc/js/full_list.js +150 -0
- data/doc/js/jquery.js +16 -0
- data/doc/method_list.html +590 -0
- data/doc/top-level-namespace.html +103 -0
- data/lib/mir/application.rb +92 -82
- data/lib/mir/config.rb +8 -6
- data/lib/mir/disk.rb +1 -0
- data/lib/mir/disk/amazon.rb +33 -8
- data/lib/mir/index.rb +24 -7
- data/lib/mir/logger.rb +5 -0
- data/lib/mir/models/resource.rb +5 -3
- data/lib/mir/utils.rb +9 -3
- data/lib/mir/version.rb +1 -1
- metadata +30 -1
@@ -0,0 +1,103 @@
|
|
1
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
2
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
3
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
4
|
+
<head>
|
5
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
6
|
+
<title>
|
7
|
+
Top Level Namespace
|
8
|
+
|
9
|
+
— Documentation by YARD 0.7.2
|
10
|
+
|
11
|
+
</title>
|
12
|
+
|
13
|
+
<link rel="stylesheet" href="css/style.css" type="text/css" media="screen" charset="utf-8" />
|
14
|
+
|
15
|
+
<link rel="stylesheet" href="css/common.css" type="text/css" media="screen" charset="utf-8" />
|
16
|
+
|
17
|
+
<script type="text/javascript" charset="utf-8">
|
18
|
+
relpath = '';
|
19
|
+
if (relpath != '') relpath += '/';
|
20
|
+
</script>
|
21
|
+
|
22
|
+
<script type="text/javascript" charset="utf-8" src="js/jquery.js"></script>
|
23
|
+
|
24
|
+
<script type="text/javascript" charset="utf-8" src="js/app.js"></script>
|
25
|
+
|
26
|
+
|
27
|
+
</head>
|
28
|
+
<body>
|
29
|
+
<script type="text/javascript" charset="utf-8">
|
30
|
+
if (window.top.frames.main) document.body.className = 'frames';
|
31
|
+
</script>
|
32
|
+
|
33
|
+
<div id="header">
|
34
|
+
<div id="menu">
|
35
|
+
|
36
|
+
<a href="_index.html">Index</a> »
|
37
|
+
|
38
|
+
|
39
|
+
<span class="title">Top Level Namespace</span>
|
40
|
+
|
41
|
+
|
42
|
+
<div class="noframes"><span class="title">(</span><a href="." target="_top">no frames</a><span class="title">)</span></div>
|
43
|
+
</div>
|
44
|
+
|
45
|
+
<div id="search">
|
46
|
+
|
47
|
+
<a id="class_list_link" href="#">Class List</a>
|
48
|
+
|
49
|
+
<a id="method_list_link" href="#">Method List</a>
|
50
|
+
|
51
|
+
<a id="file_list_link" href="#">File List</a>
|
52
|
+
|
53
|
+
</div>
|
54
|
+
<div class="clear"></div>
|
55
|
+
</div>
|
56
|
+
|
57
|
+
<iframe id="search_frame"></iframe>
|
58
|
+
|
59
|
+
<div id="content"><h1>Top Level Namespace
|
60
|
+
|
61
|
+
|
62
|
+
|
63
|
+
</h1>
|
64
|
+
|
65
|
+
<dl class="box">
|
66
|
+
|
67
|
+
|
68
|
+
|
69
|
+
|
70
|
+
|
71
|
+
|
72
|
+
|
73
|
+
|
74
|
+
</dl>
|
75
|
+
<div class="clear"></div>
|
76
|
+
|
77
|
+
<h2>Defined Under Namespace</h2>
|
78
|
+
<p class="children">
|
79
|
+
|
80
|
+
|
81
|
+
<strong class="modules">Modules:</strong> <span class='object_link'><a href="Mir.html" title="Mir (module)">Mir</a></span>
|
82
|
+
|
83
|
+
|
84
|
+
|
85
|
+
|
86
|
+
</p>
|
87
|
+
|
88
|
+
|
89
|
+
|
90
|
+
|
91
|
+
|
92
|
+
|
93
|
+
|
94
|
+
</div>
|
95
|
+
|
96
|
+
<div id="footer">
|
97
|
+
Generated on Fri Sep 23 18:24:39 2011 by
|
98
|
+
<a href="http://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
|
99
|
+
0.7.2 (ruby-1.9.2).
|
100
|
+
</div>
|
101
|
+
|
102
|
+
</body>
|
103
|
+
</html>
|
data/lib/mir/application.rb
CHANGED
@@ -9,13 +9,16 @@ module Mir
|
|
9
9
|
DEFAULT_SETTINGS_FILE_NAME = "mir_settings.yml"
|
10
10
|
DEFAULT_BATCH_SIZE = 100
|
11
11
|
|
12
|
-
# Creates a new Mir instance
|
12
|
+
# Creates a new Mir instance and starts the main execution thread
|
13
|
+
# @return [void]
|
13
14
|
def self.start
|
14
15
|
new.start
|
15
16
|
end
|
16
17
|
|
17
18
|
attr_reader :options, :disk, :index
|
18
19
|
|
20
|
+
# Creates a new Mir instance. Parses any command line arguments provided to the application
|
21
|
+
# @return [Mir::Application]
|
19
22
|
def initialize
|
20
23
|
@options = Mir::Options.parse(ARGV)
|
21
24
|
Mir.logger = Logger.new(options.log_destination)
|
@@ -29,6 +32,7 @@ module Mir
|
|
29
32
|
##
|
30
33
|
# Begins the synchronization operation after initializing the file index and remote storage
|
31
34
|
# container
|
35
|
+
# @return [void]
|
32
36
|
def start
|
33
37
|
if options.copy && options.flush
|
34
38
|
Mir.logger.error "Conflicting options: Cannot copy from remote source with an empty file index"
|
@@ -64,6 +68,92 @@ module Mir
|
|
64
68
|
self.class.config
|
65
69
|
end
|
66
70
|
|
71
|
+
##
|
72
|
+
# Synchronize the target directory to the remote disk. Will create a file index if one does
|
73
|
+
# not already exist
|
74
|
+
# @param target [String] the absolute path of the folder that will be synchronized remotely
|
75
|
+
# @return [void]
|
76
|
+
def push(target)
|
77
|
+
Mir.logger.info "Starting push operation"
|
78
|
+
|
79
|
+
if Models::AppSetting.backup_path != target
|
80
|
+
Mir.logger.error "Target does not match directory stored in index"
|
81
|
+
exit
|
82
|
+
end
|
83
|
+
|
84
|
+
index.update
|
85
|
+
|
86
|
+
time = Benchmark.measure do
|
87
|
+
queue = WorkQueue.new(config.max_threads)
|
88
|
+
while Models::Resource.pending_jobs? do
|
89
|
+
Models::Resource.pending_sync_groups(DEFAULT_BATCH_SIZE) do |resources|
|
90
|
+
push_group(queue, resources)
|
91
|
+
handle_push_failures(resources)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# If any assets have been deleted locally, also remove them from remote disk
|
97
|
+
index.orphans.each { |orphan| disk.delete(orphan.abs_path) }
|
98
|
+
index.clean! # Remove orphans from index
|
99
|
+
puts "Completed push operation #{time}"
|
100
|
+
Mir.logger.info time
|
101
|
+
end
|
102
|
+
|
103
|
+
##
|
104
|
+
# Copy the remote disk contents into the specified directory
|
105
|
+
# @param target [String] the absolute path to the destination of the S3 disk contents
|
106
|
+
# @return [void]
|
107
|
+
def pull(target)
|
108
|
+
write_dir = Utils.try_create_dir(target)
|
109
|
+
Mir.logger.info "Copying remote disk to #{write_dir.path} using #{config.max_threads} threads"
|
110
|
+
failed_downloads = []
|
111
|
+
|
112
|
+
time = Benchmark.measure do
|
113
|
+
queue = WorkQueue.new(config.max_threads)
|
114
|
+
|
115
|
+
Models::Resource.ordered_groups(DEFAULT_BATCH_SIZE) do |resources|
|
116
|
+
# Track the number of download attempts made per resource
|
117
|
+
batch = resources.inject({}) { |set, resource| set[resource] = 0; set }
|
118
|
+
|
119
|
+
while !batch.empty? do
|
120
|
+
batch.each do |resource, attempts|
|
121
|
+
dest = File.join(write_dir.path, resource.filename)
|
122
|
+
if resource.is_directory?
|
123
|
+
Utils.try_create_dir(dest)
|
124
|
+
batch.delete(resource)
|
125
|
+
elsif attempts >= config.max_download_attempts
|
126
|
+
Mir.logger.info "Resource #{resource.abs_path} failed to download"
|
127
|
+
failed_downloads << resource
|
128
|
+
batch.delete(resource)
|
129
|
+
elsif resource.synchronized? dest
|
130
|
+
Mir.logger.debug "Skipping already downloaded file #{resource.abs_path}"
|
131
|
+
batch.delete(resource)
|
132
|
+
else
|
133
|
+
batch[resource] += 1
|
134
|
+
queue.enqueue_b do
|
135
|
+
Mir.logger.debug "Beginning download of #{resource.abs_path}"
|
136
|
+
disk.copy(resource.abs_path, dest)
|
137
|
+
if resource.synchronized? dest
|
138
|
+
FileUtils.chmod_R 0755, dest # allow binaries to execute
|
139
|
+
puts "Pulled #{dest}"
|
140
|
+
batch.delete(resource)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
queue.join
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
Mir.logger.info time
|
150
|
+
puts "Completed pull operation #{time}"
|
151
|
+
unless failed_downloads.empty?
|
152
|
+
puts "The following files failed to download after several attempts:\n}"
|
153
|
+
puts failed_downloads.join("\n")
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
67
157
|
private
|
68
158
|
# Returns a path to the settings file. If the file was provided as an option it will always
|
69
159
|
# be returned. When no option is passed, the file 'mir_settings.yml' will be searched for
|
@@ -81,36 +171,7 @@ module Mir
|
|
81
171
|
nil
|
82
172
|
end
|
83
173
|
|
84
|
-
|
85
|
-
# Synchronize the local files to the remote disk
|
86
|
-
#
|
87
|
-
# @param [String] the absolute path of the folder that will be synchronized remotely
|
88
|
-
def push(target)
|
89
|
-
Mir.logger.info "Starting push operation"
|
90
|
-
|
91
|
-
if Models::AppSetting.backup_path != target
|
92
|
-
Mir.logger.error "Target does not match directory stored in index"
|
93
|
-
exit
|
94
|
-
end
|
95
|
-
|
96
|
-
index.update
|
97
|
-
|
98
|
-
time = Benchmark.measure do
|
99
|
-
queue = WorkQueue.new(config.max_threads)
|
100
|
-
while Models::Resource.pending_jobs? do
|
101
|
-
Models::Resource.pending_sync_groups(DEFAULT_BATCH_SIZE) do |resources|
|
102
|
-
push_group(queue, resources)
|
103
|
-
handle_push_failures(resources)
|
104
|
-
end
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
# If any assets have been deleted locally, also remove them from remote disk
|
109
|
-
index.orphans.each { |orphan| disk.delete(orphan.abs_path) }
|
110
|
-
index.clean! # Remove orphans from index
|
111
|
-
puts "Completed push operation #{time}"
|
112
|
-
Mir.logger.info time
|
113
|
-
end
|
174
|
+
|
114
175
|
|
115
176
|
##
|
116
177
|
# Uploads a collection of resouces. Blocks until all items in queue have been processed
|
@@ -143,57 +204,6 @@ module Mir
|
|
143
204
|
end
|
144
205
|
end
|
145
206
|
end
|
146
|
-
|
147
|
-
# Copy the remote disk contents into the specified directory
|
148
|
-
def pull(target)
|
149
|
-
write_dir = Utils.try_create_dir(target)
|
150
|
-
Mir.logger.info "Copying remote disk to #{write_dir.path} using #{config.max_threads} threads"
|
151
|
-
failed_downloads = []
|
152
|
-
|
153
|
-
time = Benchmark.measure do
|
154
|
-
queue = WorkQueue.new(config.max_threads)
|
155
|
-
|
156
|
-
Models::Resource.ordered_groups(DEFAULT_BATCH_SIZE) do |resources|
|
157
|
-
# Track the number of download attempts made per resource
|
158
|
-
batch = resources.inject({}) { |set, resource| set[resource] = 0; set }
|
159
|
-
|
160
|
-
while !batch.empty? do
|
161
|
-
batch.each do |resource, attempts|
|
162
|
-
dest = File.join(write_dir.path, resource.filename)
|
163
|
-
if resource.is_directory?
|
164
|
-
Utils.try_create_dir(dest)
|
165
|
-
batch.delete(resource)
|
166
|
-
elsif attempts >= config.max_download_attempts
|
167
|
-
Mir.logger.info "Resource #{resource.abs_path} failed to download"
|
168
|
-
failed_downloads << resource
|
169
|
-
batch.delete(resource)
|
170
|
-
elsif resource.synchronized? dest
|
171
|
-
Mir.logger.debug "Skipping already downloaded file #{resource.abs_path}"
|
172
|
-
batch.delete(resource)
|
173
|
-
else
|
174
|
-
batch[resource] += 1
|
175
|
-
queue.enqueue_b do
|
176
|
-
Mir.logger.debug "Beginning download of #{resource.abs_path}"
|
177
|
-
disk.copy(resource.abs_path, dest)
|
178
|
-
if resource.synchronized? dest
|
179
|
-
FileUtils.chmod_R 0755, dest # allow binaries to execute
|
180
|
-
puts "Pulled #{dest}"
|
181
|
-
batch.delete(resource)
|
182
|
-
end
|
183
|
-
end
|
184
|
-
end
|
185
|
-
end
|
186
|
-
queue.join
|
187
|
-
end
|
188
|
-
end
|
189
|
-
end
|
190
|
-
Mir.logger.info time
|
191
|
-
puts "Completed pull operation #{time}"
|
192
|
-
unless failed_downloads.empty?
|
193
|
-
puts "The following files failed to download after several attempts:\n}"
|
194
|
-
puts failed_downloads.join("\n")
|
195
|
-
end
|
196
|
-
end
|
197
207
|
|
198
208
|
end
|
199
209
|
end
|
data/lib/mir/config.rb
CHANGED
@@ -1,10 +1,14 @@
|
|
1
1
|
# The config class is used for storage of user settings and preferences releated to
|
2
|
-
#
|
2
|
+
# S3 storage
|
3
3
|
module Mir
|
4
4
|
class UndefinedConfigValue < StandardError; end
|
5
5
|
|
6
6
|
class Config
|
7
7
|
|
8
|
+
# Creates a new config instance
|
9
|
+
# @param config_file [String] path to the configuration file
|
10
|
+
# @yield [Mir::Config]
|
11
|
+
# @return [Mir::Config]
|
8
12
|
def initialize(config_file = nil)
|
9
13
|
@config_file = config_file
|
10
14
|
@settings, @database = nil, nil
|
@@ -19,9 +23,9 @@ module Mir
|
|
19
23
|
attr_accessor :max_upload_attempts, :max_download_attempts, :max_threads
|
20
24
|
|
21
25
|
# Validates configuration settings
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
26
|
+
# @todo move this into a class method responsible for parsing the YAML file. We
|
27
|
+
# shouldn't have to explicitly check for a valid object unless the user has provided
|
28
|
+
# a settings file
|
25
29
|
def valid?
|
26
30
|
if @config_file.nil? or !File.exist?(@config_file)
|
27
31
|
Mir.logger.error("Configuration file not found")
|
@@ -47,8 +51,6 @@ module Mir
|
|
47
51
|
true
|
48
52
|
end
|
49
53
|
|
50
|
-
|
51
|
-
|
52
54
|
def method_missing(meth, *args, &block)
|
53
55
|
val = @settings.send(meth)
|
54
56
|
unless val.nil?
|
data/lib/mir/disk.rb
CHANGED
data/lib/mir/disk/amazon.rb
CHANGED
@@ -13,11 +13,10 @@ module Mir
|
|
13
13
|
|
14
14
|
attr_reader :bucket_name, :connection
|
15
15
|
|
16
|
-
#
|
17
16
|
# Converts a path name to a key that can be stored on s3
|
18
17
|
#
|
19
|
-
# @param [String] the path to the file
|
20
|
-
# @return [String]
|
18
|
+
# @param path [String] the path to the file
|
19
|
+
# @return [String]
|
21
20
|
def self.s3_key(path)
|
22
21
|
if path[0] == File::SEPARATOR
|
23
22
|
path[1..-1]
|
@@ -26,6 +25,13 @@ module Mir
|
|
26
25
|
end
|
27
26
|
end
|
28
27
|
|
28
|
+
##
|
29
|
+
# Attempts to create a new connection to Amazon S3
|
30
|
+
# @option settings [Symbol] :bucket_name The name of the remote bucket
|
31
|
+
# @option settings [Symbol] :access_key_id The access key id for the amazon account
|
32
|
+
# @option settings [Symbol] :secret_access_key The secret access key for the amazon account
|
33
|
+
# @option settings [Symbol] :chunk_size The maximum number of bytes to use for each chunk
|
34
|
+
# of a file sent to S3
|
29
35
|
def initialize(settings = {})
|
30
36
|
@bucket_name = settings[:bucket_name]
|
31
37
|
@access_key_id = settings[:access_key_id]
|
@@ -35,22 +41,28 @@ module Mir
|
|
35
41
|
end
|
36
42
|
|
37
43
|
# Returns the buckets available from S3
|
44
|
+
# @return [Hash]
|
38
45
|
def collections
|
39
46
|
@connection.list_bucket.select(:key)
|
40
47
|
end
|
41
48
|
|
49
|
+
# Sets the maximum number of bytes to be used per chunk sent to S3
|
50
|
+
# @param n [Integer] Number of bytes
|
51
|
+
# @return [void]
|
42
52
|
def chunk_size=(n)
|
43
53
|
raise ArgumentError unless n > 0
|
44
54
|
@chunk_size = n
|
45
55
|
end
|
46
56
|
|
57
|
+
# The size in bytes of each chunk
|
58
|
+
# @return [Integer]
|
47
59
|
def chunk_size
|
48
60
|
@chunk_size
|
49
61
|
end
|
50
62
|
|
51
63
|
# Whether the key exists in S3
|
52
64
|
#
|
53
|
-
# @param [String] the S3 key name
|
65
|
+
# @param key [String] the S3 key name
|
54
66
|
# @return [Boolean]
|
55
67
|
def key_exists?(key)
|
56
68
|
begin
|
@@ -63,8 +75,9 @@ module Mir
|
|
63
75
|
end
|
64
76
|
|
65
77
|
# Copies the remote resource to the local filesystem
|
66
|
-
# @param [String] the remote name of the resource to copy
|
67
|
-
# @param [String] the local name of the destination
|
78
|
+
# @param from [String] the remote name of the resource to copy
|
79
|
+
# @param dest [String] the local name of the destination
|
80
|
+
# @return [void]
|
68
81
|
def copy(from, dest)
|
69
82
|
open(dest, 'w') do |file|
|
70
83
|
key = self.class.s3_key(from)
|
@@ -73,15 +86,23 @@ module Mir
|
|
73
86
|
end
|
74
87
|
end
|
75
88
|
|
76
|
-
# Retrieves the complete object from S3
|
89
|
+
# Retrieves the complete object from S3. Note this method will not stream the object
|
90
|
+
# and will return the value of the object stored on S3
|
91
|
+
#
|
92
|
+
# @param key [String] the S3 key name of the object
|
93
|
+
# @return [String]
|
77
94
|
def read(key)
|
78
95
|
connection.get_object(bucket_name, key)
|
79
96
|
end
|
80
97
|
|
98
|
+
# Whether a connection to S3 has been established
|
99
|
+
# @return [Boolean]
|
81
100
|
def connected?
|
82
101
|
@connection_success
|
83
102
|
end
|
84
103
|
|
104
|
+
# Retrieves the bucket from S3
|
105
|
+
# @return [RightAws::S3::Bucket]
|
85
106
|
def volume
|
86
107
|
connection.bucket(bucket_name, true)
|
87
108
|
end
|
@@ -111,6 +132,7 @@ module Mir
|
|
111
132
|
#
|
112
133
|
# @param [String] the absolute path of the file to be written
|
113
134
|
# @raise [Disk::IncompleteTransmission] raised when remote resource is different from local file
|
135
|
+
# @return [void]
|
114
136
|
def write(file_path)
|
115
137
|
key = self.class.s3_key(file_path)
|
116
138
|
|
@@ -154,6 +176,8 @@ module Mir
|
|
154
176
|
Digest::MD5.file(filename).to_s == remote_md5
|
155
177
|
end
|
156
178
|
|
179
|
+
# Attempts to establish a connection with Amazon S3
|
180
|
+
# @return [RightAws::S3Interface|Boolean]
|
157
181
|
def try_connect
|
158
182
|
begin
|
159
183
|
conn = RightAws::S3Interface.new(@access_key_id, @secret_access_key, {
|
@@ -171,7 +195,8 @@ module Mir
|
|
171
195
|
# Yields a temp file object that is immediately discarded after use
|
172
196
|
#
|
173
197
|
# @param [String] the filename
|
174
|
-
# @
|
198
|
+
# @yield [Tempfile]
|
199
|
+
# @return [void]
|
175
200
|
def temp_file(name, &block)
|
176
201
|
file = Tempfile.new(File.basename(name))
|
177
202
|
begin
|