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.
@@ -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
+ &mdash; 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> &raquo;
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>
@@ -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
@@ -1,10 +1,14 @@
1
1
  # The config class is used for storage of user settings and preferences releated to
2
- # S3 storage
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
- # TODO: move this into a class method responsible for parsing the YAML file. We
23
- # shouldn't have to explicitly check for a valid object unless the user has provided
24
- # a settings file
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?
@@ -6,6 +6,7 @@ module Mir
6
6
  class RemoteFileNotFound < StandardError ; end
7
7
 
8
8
  # Returns a disk object from the settings specified
9
+ # @return [Mir::Disk]
9
10
  def self.fetch(settings = {})
10
11
  case settings[:type]
11
12
  when "s3"
@@ -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] an S3-safe key with leading slashes removed
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 without streaming
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
- # @yields [Tempfile]
198
+ # @yield [Tempfile]
199
+ # @return [void]
175
200
  def temp_file(name, &block)
176
201
  file = Tempfile.new(File.basename(name))
177
202
  begin