mir 0.1.2

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 ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "http://rubygems.org"
2
+ gemspec
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+ RSpec::Core::RakeTask.new
data/bin/mir ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require 'mir'
3
+ Mir::Application.start
@@ -0,0 +1,13 @@
1
+ class CreateAppSettings < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :app_settings do |t|
4
+ t.string :name
5
+ t.string :value
6
+ end
7
+ add_index :app_settings, :name
8
+ end
9
+
10
+ def self.down
11
+ drop_table :app_settings
12
+ end
13
+ end
@@ -0,0 +1,24 @@
1
+ class CreateResources < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :resources do |t|
4
+ t.string :filename, :null => false
5
+ t.integer :size, :default => 0, :null => false
6
+ t.string :checksum, :limit => 32
7
+ t.datetime :last_modified
8
+ t.datetime :add_date, :default => DateTime.now
9
+ t.datetime :last_synchronized
10
+ t.boolean :is_directory, :default => false, :null => false
11
+ t.boolean :in_progress, :default => false
12
+ t.boolean :queued, :default => false
13
+ t.integer :times_failed, :default => 0, :null => false
14
+ end
15
+
16
+ add_index :resources, :filename, :unique => true
17
+ add_index :resources, :in_progress
18
+ add_index :resources, :queued
19
+ end
20
+
21
+ def self.down
22
+ drop_table :resources
23
+ end
24
+ end
@@ -0,0 +1,147 @@
1
+ require "benchmark"
2
+ require "fileutils"
3
+ require "work_queue"
4
+
5
+ module Mir
6
+
7
+ class Application
8
+
9
+ DEFAULT_SETTINGS_FILE_NAME = "mir_settings.yml"
10
+ DEFAULT_BATCH_SIZE = 20
11
+
12
+ # Creates a new Mir instance
13
+ def self.start
14
+ new.start
15
+ end
16
+
17
+ attr_reader :options, :disk, :index
18
+
19
+ def initialize
20
+ @options = Mir::Options.parse(ARGV)
21
+ Mir.logger = Logger.new(options.log_destination)
22
+ end
23
+
24
+ def start
25
+ if options.copy && options.flush
26
+ Mir.logger.error "Conflicting options: Cannot copy from remote source with an empty file index"
27
+ exit
28
+ end
29
+
30
+ @@config = Config.new find_settings_file
31
+ param_path = File.expand_path(ARGV[0])
32
+ exit unless config.valid?
33
+
34
+ # Initialize our remote disk
35
+ @disk = Disk.fetch(config.cloud_provider)
36
+ exit unless @disk.connected?
37
+
38
+ # Initialize our local index
39
+ @index = Index.new(param_path, config.database)
40
+ @index.setup(:verbose => options.verbose, :force_flush => options.flush)
41
+
42
+ options.copy ? pull(param_path) : push(param_path)
43
+ end
44
+
45
+ def self.config
46
+ @@config
47
+ end
48
+
49
+ def config
50
+ @@config
51
+ end
52
+
53
+ private
54
+ # Returns a path to the settings file. If the file was provided as an option it will always
55
+ # be returned. When no option is passed, the file 'mir_settings.yml' will be searched for
56
+ # in the following paths: $HOME, /etc/mir
57
+ # @return [String] path to the settings or nil if none is found
58
+ def find_settings_file
59
+ if !options.settings_path.nil?
60
+ return options.settings_path
61
+ else
62
+ ["~", "/etc/mir"].each do |dir|
63
+ path = File.expand_path(File.join(dir, DEFAULT_SETTINGS_FILE_NAME))
64
+ return path.to_s if File.exist?(path)
65
+ end
66
+ end
67
+ nil
68
+ end
69
+
70
+ # Synchronize the local files to the remote disk
71
+ def push(target)
72
+ Mir.logger.info "Starting push operation"
73
+
74
+ if Models::AppSetting.backup_path != target
75
+ Mir.logger.error "Target does not match directory stored in index"
76
+ exit
77
+ end
78
+
79
+ index.update
80
+
81
+ time = Benchmark.measure do
82
+ queue = WorkQueue.new(config.max_threads)
83
+ while Models::Resource.pending_jobs? do
84
+ Models::Resource.pending_sync_groups(DEFAULT_BATCH_SIZE) do |resources|
85
+ push_group(queue, resources)
86
+ handle_push_failures(resources)
87
+ end
88
+ end
89
+ end
90
+ Mir.logger.info time
91
+ end
92
+
93
+
94
+ # Uploads a collection of resouces. Blocks until all items in queue have been processed
95
+ # @param [WorkQueue] a submission queue to manage resource uploads
96
+ # @param [Array] an array of Models::Resource objects that need to be uploaded
97
+ def push_group(work_queue, resources)
98
+ resources.each do |resource|
99
+ work_queue.enqueue_b do
100
+ resource.start_progress
101
+ disk.write resource.abs_path
102
+ resource.update_success
103
+ end
104
+ end
105
+ work_queue.join
106
+ end
107
+
108
+ # Scans a collection of resources for jobs that did no complete successfully and flags them
109
+ # for resubmission
110
+ # @param [Array] an array of Models::Resources
111
+ def handle_push_failures(resources)
112
+ resources.each do |resource|
113
+ if resource.in_progress?
114
+ Mir.logger.info "Resource '#{resource.abs_path}' failed to upload"
115
+ resource.update_failure
116
+ end
117
+ end
118
+ end
119
+
120
+ # Copy the remote disk contents into the specified directory
121
+ def pull(target)
122
+ Utils.try_create_dir(target)
123
+ write_dir = Dir.new(target)
124
+ Mir.logger.info "Copying remote disk to #{write_dir.path} using #{config.max_threads} threads"
125
+
126
+ time = Benchmark.measure do
127
+ queue = WorkQueue.new(config.max_threads)
128
+
129
+ # loop through each resource. If the resource is a directory, create the path
130
+ # otherwise download the file
131
+ Models::Resource.ordered_groups(DEFAULT_BATCH_SIZE) do |resources|
132
+ resources.each do |resource|
133
+ dest = File.join(write_dir.path, resource.filename)
134
+ if resource.is_directory?
135
+ Utils.try_create_dir(dest)
136
+ elsif !resource.synchronized?(dest)
137
+ queue.enqueue_b { @disk.copy(resource.abs_path, dest) }
138
+ end
139
+ end
140
+ queue.join
141
+ end
142
+ end
143
+ Mir.logger.info time
144
+ end
145
+
146
+ end
147
+ end
data/lib/mir/config.rb ADDED
@@ -0,0 +1,62 @@
1
+
2
+
3
+ # The config class is used for storage of user settings and preferences releated to
4
+ # S3 storage
5
+ module Mir
6
+ class UndefinedConfigValue < StandardError; end
7
+
8
+ class Config
9
+
10
+ def initialize(config_file = nil)
11
+ @config_file = config_file
12
+ @settings, @database = nil, nil
13
+ end
14
+
15
+ # Validates configuration settings
16
+ def valid?
17
+ if @config_file.nil? or !File.exist?(@config_file)
18
+ Mir.logger.error("Configuration file not found")
19
+ return false
20
+ end
21
+
22
+ if File.directory?(@config_file)
23
+ Mir.logger.error("Received directory instead of settings file")
24
+ return false
25
+ end
26
+
27
+ File.open(@config_file) do |f|
28
+ yml = YAML::load(f)
29
+ if yml.key? "settings"
30
+ @settings = OpenStruct.new yml["settings"]
31
+ @settings.database = symbolize_keys(@settings.database)
32
+ @settings.cloud_provider = symbolize_keys(@settings.cloud_provider)
33
+ else
34
+ Mir.logger.error("Malformed config file")
35
+ return false
36
+ end
37
+ end
38
+ true
39
+ end
40
+
41
+
42
+
43
+ def method_missing(meth, *args, &block)
44
+ val = @settings.send(meth)
45
+ unless val.nil?
46
+ val
47
+ else
48
+ raise UndefinedConfigValue
49
+ end
50
+ end
51
+
52
+ private
53
+ def symbolize_keys(hsh)
54
+ symbolized = hsh.inject({}) do |opts, (k,v)|
55
+ opts[(k.to_sym rescue k) || k] = v
56
+ opts
57
+ end
58
+ symbolized
59
+ end
60
+
61
+ end
62
+ end
@@ -0,0 +1,69 @@
1
+ require "right_aws"
2
+
3
+ module Mir
4
+ module Disk
5
+ class Amazon
6
+
7
+ attr_reader :bucket_name
8
+
9
+ def self.key_name(path)
10
+ if path[0] == File::SEPARATOR
11
+ path[1..-1]
12
+ else
13
+ path
14
+ end
15
+ end
16
+
17
+ def initialize(settings = {})
18
+ @bucket_name = settings[:bucket_name]
19
+ @access_key_id = settings[:access_key_id]
20
+ @secret_access_key = settings[:secret_access_key]
21
+ @connection = try_connect
22
+ end
23
+
24
+ # Returns the buckets available from S3
25
+ def collections
26
+ @connection.list_bucket.select(:key)
27
+ end
28
+
29
+ # Copies the remote resource to the local filesystem
30
+ # @param [String] the remote name of the resource to copy
31
+ # @param [String] the local name of the destination
32
+ def copy(from, to)
33
+ open(to, 'w') do |file|
34
+ @connection.get(bucket_name, self.class.key_name(from)) { |chunk| file.write(chunk) }
35
+ end
36
+ Mir.logger.info "Completed download '#{to}'"
37
+ end
38
+
39
+ def connected?
40
+ @connection_success
41
+ end
42
+
43
+ def volume
44
+ @connection.bucket(bucket_name, true)
45
+ end
46
+
47
+ def write(file_path)
48
+ @connection.put(bucket_name, self.class.key_name(file_path), File.open(file_path))
49
+ Mir.logger.info "Completed upload #{file_path}"
50
+ end
51
+
52
+ private
53
+ def try_connect
54
+ begin
55
+ conn = RightAws::S3Interface.new(@access_key_id, @secret_access_key, {
56
+ :multi_thread => true,
57
+ :logger => Mir.logger
58
+ })
59
+ @connection_success = true
60
+ return conn
61
+ rescue Exception => e
62
+ @connection_success = false
63
+ Mir.logger.error "Could not establish connection with S3: '#{e.message}'"
64
+ end
65
+ end
66
+
67
+ end
68
+ end
69
+ end
data/lib/mir/disk.rb ADDED
@@ -0,0 +1,17 @@
1
+ # The remote storage container where objects are saved
2
+ module Mir
3
+ module Disk
4
+
5
+ # Returns a disk object from the settings specified
6
+ def self.fetch(settings = {})
7
+ case settings[:type]
8
+ when "s3"
9
+ Mir::Disk::Amazon.new(settings)
10
+ else
11
+ Mir.logger.error "Could not find specified cloud provider"
12
+ end
13
+ end
14
+
15
+
16
+ end
17
+ end
data/lib/mir/index.rb ADDED
@@ -0,0 +1,92 @@
1
+ require 'active_record'
2
+ require "active_support/inflector"
3
+
4
+ # Manages database operations for application
5
+ module Mir
6
+ class Index
7
+
8
+ MIGRATIONS_PATH = File.join(File.dirname(__FILE__), "..", "..", "db", "migrate")
9
+
10
+ # Returns a databse object used to connect to the indexing database
11
+ # @param [Hash] database configuration settings. See ActiveRecord#Base::establish_connection
12
+ def initialize(sync_path, connection_params)
13
+ @sync_path = sync_path
14
+ @connection_params = connection_params
15
+ end
16
+
17
+ attr_reader :sync_path
18
+
19
+ # Creates necessary database and tables if this is the first time connecting
20
+ # @option opts [Boolean] :verbose Enable on ActiveRecord reporting
21
+ # @option opts [Boolean] :force_flush Rebuild index no matter what
22
+ def setup(options = {})
23
+ options[:force_flush] ||= false
24
+ options[:verbose] ||= false
25
+ @connection = ActiveRecord::Base.establish_connection(@connection_params).connection
26
+ ActiveRecord::Base.timestamped_migrations = false
27
+
28
+ if options[:verbose]
29
+ ActiveRecord::Base.logger = Mir.logger
30
+ ActiveRecord::Migration.verbose = true
31
+ end
32
+
33
+ load_tables
34
+ rebuild if !tables_created? or options[:force_flush]
35
+ end
36
+
37
+ # Scans the filesystem and flags any resources which need updating
38
+ def update
39
+ Mir.logger.info "Updating backup index for '#{sync_path}'"
40
+ Dir.glob(File.join(sync_path, "**", "*")) do |f|
41
+ fname = relative_path(f)
42
+ file = File.new(f)
43
+ resource = Models::Resource.find_by_filename(fname)
44
+
45
+ if resource.nil?
46
+ Mir.logger.info "Adding file to index #{fname}"
47
+ resource = Models::Resource.create_from_file_and_name(file, fname)
48
+ else
49
+ resource.flag_for_update unless resource.synchronized?(file)
50
+ end
51
+ end
52
+ Mir.logger.info "Index updated"
53
+ end
54
+
55
+ private
56
+ # Returns the path of a file relative to the backup directory
57
+ def relative_path(file)
58
+ File.absolute_path(file).gsub(sync_path,'')
59
+ end
60
+
61
+ # Regenerates the file system index for the backup directory
62
+ def rebuild
63
+ tables.each { |t| ActiveRecord::Migration.drop_table(t.table_name) if t.table_exists? }
64
+ ActiveRecord::Migration.drop_table(:schema_migrations) if @connection.table_exists? :schema_migrations
65
+ ActiveRecord::Migrator.migrate MIGRATIONS_PATH
66
+ Models::AppSetting.create(:name => Models::AppSetting::SYNC_PATH, :value => sync_path)
67
+ Models::AppSetting.create(:name => Models::AppSetting::INSTALL_DATE, :value => DateTime.now.to_s)
68
+ end
69
+
70
+ def load_tables
71
+ @tables = []
72
+ models = File.join(File.dirname(__FILE__), "models", "*.rb")
73
+
74
+ # load the AR models for the application
75
+ Dir.glob(models) do |f|
76
+ require f
77
+ name = "Models::" + ActiveSupport::Inflector.camelize(File.basename(f, ".rb"))
78
+ @tables << eval(name)
79
+ end
80
+ end
81
+
82
+ # Returns the activerecord classes for each table used by the application
83
+ def tables
84
+ @tables
85
+ end
86
+
87
+ # Checks whether any of the tables required by the applicaiton exist
88
+ def tables_created?
89
+ tables.any? { |t| t.table_exists? }
90
+ end
91
+ end
92
+ end
data/lib/mir/logger.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'logger'
2
+
3
+ module Mir
4
+ def self.logger=(logger)
5
+ @@logger = logger
6
+ end
7
+ def self.logger
8
+ @@logger
9
+ end
10
+ end
@@ -0,0 +1,15 @@
1
+ module Mir
2
+ module Models
3
+ class AppSetting < ActiveRecord::Base
4
+
5
+ SYNC_PATH = "sync_path"
6
+ INSTALL_DATE = "installation_date"
7
+
8
+ def self.backup_path
9
+ record = self.where(:name => SYNC_PATH).first
10
+ record.value rescue nil
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,97 @@
1
+ require 'digest/md5'
2
+
3
+ # Represents a local file asset
4
+ module Mir
5
+ module Models
6
+ class Resource < ActiveRecord::Base
7
+
8
+ # Builds a resource for the backup index from a file
9
+ # @param [File] a file object
10
+ # @param [String] the name of the file on the remote disk
11
+ # @returns [Resource] a new Resource instance that with a queued status
12
+ def self.create_from_file_and_name(file, name)
13
+ is_dir = File.directory?(file)
14
+ create(:filename => name,
15
+ :size => file.size,
16
+ :last_modified => file.ctime,
17
+ :add_date => DateTime.now,
18
+ :queued => !is_dir,
19
+ :checksum => is_dir ? nil : Digest::MD5.file(file).to_s,
20
+ :is_directory => is_dir)
21
+ end
22
+
23
+ # Returns true when jobs are still queued
24
+ def self.pending_jobs?
25
+ self.where(:queued => true).size > 0
26
+ end
27
+
28
+ # Yields a Models::Resource object that needs to be synchronized
29
+ def self.pending_sync_groups(response_size, &block)
30
+ qry = lambda { Resource.where(:queued => true, :is_directory => false) }
31
+ chunked_groups(qry, response_size) { |chunk| yield chunk }
32
+ end
33
+
34
+ # Returns groups of file resources ordered by name
35
+ # @param [Integer] the number of records to return per chunk
36
+ # @yields [Array] instances of Models::Resource
37
+ def self.ordered_groups(group_size = 10)
38
+ qry = lambda { Resource.order(:filename) }
39
+ chunked_groups(qry, group_size) { |chunk| yield chunk }
40
+ end
41
+
42
+ def self.chunked_groups(qry_func, chunk_size)
43
+ num_results = Resource.count
44
+ offset = 0
45
+ pages = (num_results/chunk_size.to_f).ceil
46
+ pages.times do |i|
47
+ response = qry_func.call().limit(chunk_size).offset(i*chunk_size)
48
+ yield response
49
+ end
50
+ end
51
+
52
+ # Compares a file asset to the index to deterimine whether the file needs to be updated
53
+ # @param [String] a path to a file or directory
54
+ # @returns [Boolean] whether the file and index are in sync with each other
55
+ def synchronized?(file)
56
+ if File.exists? file
57
+ return true if File.directory? file
58
+ return Digest::MD5.file(file).to_s == self.checksum
59
+ end
60
+ false
61
+ end
62
+
63
+ # Whether the item can be synchronized to a remote disk
64
+ # @returns [Boolean] true when the resource is not a directory
65
+ def synchronizable?
66
+ !is_directory?
67
+ end
68
+
69
+ # Places the resource into a queueble state
70
+ def flag_for_update
71
+ update_attributes :queued => true, :checksum => Digest::MD5.file(abs_path).to_s
72
+ end
73
+
74
+ def start_progress
75
+ update_attribute :in_progress, true
76
+ end
77
+
78
+ def update_success
79
+ update_attributes :in_progress => false,
80
+ :last_synchronized => DateTime.now,
81
+ :queued => false,
82
+ :times_failed => 0
83
+ end
84
+
85
+ def update_failure
86
+ num_times_failed = times_failed + 1
87
+ will_requeue = (num_times_failed < Mir::Application.config.max_upload_retries)
88
+ update_attributes :times_failed => num_times_failed, :in_progress => false, :queued => will_requeue
89
+ end
90
+
91
+ def abs_path
92
+ File.join(Models::AppSetting.backup_path, filename)
93
+ end
94
+
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,60 @@
1
+ require 'optparse'
2
+ require 'yaml'
3
+ require 'ostruct'
4
+
5
+ module Mir
6
+ class Options
7
+
8
+ USAGE_BANNER = "Usage: mir [options] [settings_file_path] [target]"
9
+
10
+ def self.parse(args)
11
+ options = OpenStruct.new
12
+ options.debug = false
13
+ options.verbose = false
14
+ options.settings_file = nil
15
+ options.log_destination = STDOUT
16
+ options.flush = false
17
+ options.copy = false
18
+ options.settings_path = nil
19
+
20
+ opts_parser = OptionParser.new do |opts|
21
+ opts.banner = USAGE_BANNER
22
+ opts.separator ""
23
+ opts.separator "Specific options:"
24
+
25
+ opts.on("--flush", "Flush the file index") do
26
+ options.flush = true
27
+ end
28
+
29
+ opts.on("-c", "--copy", "Copy the remote files to target") do
30
+ options.copy = true
31
+ end
32
+
33
+ opts.on("--settings", String, "The YAML settings file") do |path|
34
+ options.settings_path = path
35
+ end
36
+
37
+ opts.on("-l", "--log-path LOG_FILE", String, "Location for storing execution logs") do |log_file|
38
+ options.log_destination = log_file
39
+ end
40
+
41
+ opts.on("-v", "--verbose", "Run verbosely") do |v|
42
+ options.verbose = true
43
+ end
44
+
45
+ opts.on_tail("-h", "--help", "Show this message") do
46
+ puts opts
47
+ exit
48
+ end
49
+
50
+ opts.on_tail("--version", "Show version") do
51
+ puts Mir.version
52
+ exit
53
+ end
54
+ end
55
+
56
+ opts_parser.parse!(args)
57
+ options
58
+ end
59
+ end
60
+ end
data/lib/mir/utils.rb ADDED
@@ -0,0 +1,7 @@
1
+ module Mir
2
+ module Utils
3
+ def self.try_create_dir(path)
4
+ Dir.mkdir(path) unless Dir.exist?(path)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Mir
2
+ VERSION = [0,1,2]
3
+
4
+ def self.version
5
+ VERSION.join('.')
6
+ end
7
+ end
data/lib/mir.rb ADDED
@@ -0,0 +1,12 @@
1
+ require "mir/application"
2
+ require "mir/config"
3
+ require "mir/disk"
4
+ require "mir/disk/amazon"
5
+ require "mir/index"
6
+ require "mir/logger"
7
+ require "mir/options"
8
+ require "mir/utils"
9
+ require "mir/version"
10
+
11
+ module Mir
12
+ end
data/mir.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "mir/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "mir"
7
+ s.version = Mir.version
8
+ s.authors = ["Nate Miller"]
9
+ s.email = ["nate@natemiller.org"]
10
+ s.homepage = ""
11
+ s.summary = %q{A utility for backing up resources}
12
+ s.description = %q{A commandline tool useful for automating the back up of files to Amazon S3}
13
+
14
+ s.rubyforge_project = "mir"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_development_dependency "rspec"
22
+
23
+ s.add_runtime_dependency "bundler"
24
+ s.add_runtime_dependency "right_aws"
25
+ s.add_runtime_dependency "activerecord"
26
+ s.add_runtime_dependency "activesupport"
27
+ s.add_runtime_dependency "work_queue"
28
+ end
@@ -0,0 +1,5 @@
1
+ require "mir"
2
+
3
+ RSpec.configure do |config|
4
+
5
+ end
metadata ADDED
@@ -0,0 +1,141 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mir
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.1.2
6
+ platform: ruby
7
+ authors:
8
+ - Nate Miller
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-09-05 00:00:00 -07:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: rspec
18
+ prerelease: false
19
+ requirement: &id001 !ruby/object:Gem::Requirement
20
+ none: false
21
+ requirements:
22
+ - - ">="
23
+ - !ruby/object:Gem::Version
24
+ version: "0"
25
+ type: :development
26
+ version_requirements: *id001
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ prerelease: false
30
+ requirement: &id002 !ruby/object:Gem::Requirement
31
+ none: false
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: "0"
36
+ type: :runtime
37
+ version_requirements: *id002
38
+ - !ruby/object:Gem::Dependency
39
+ name: right_aws
40
+ prerelease: false
41
+ requirement: &id003 !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: "0"
47
+ type: :runtime
48
+ version_requirements: *id003
49
+ - !ruby/object:Gem::Dependency
50
+ name: activerecord
51
+ prerelease: false
52
+ requirement: &id004 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: "0"
58
+ type: :runtime
59
+ version_requirements: *id004
60
+ - !ruby/object:Gem::Dependency
61
+ name: activesupport
62
+ prerelease: false
63
+ requirement: &id005 !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: "0"
69
+ type: :runtime
70
+ version_requirements: *id005
71
+ - !ruby/object:Gem::Dependency
72
+ name: work_queue
73
+ prerelease: false
74
+ requirement: &id006 !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: "0"
80
+ type: :runtime
81
+ version_requirements: *id006
82
+ description: A commandline tool useful for automating the back up of files to Amazon S3
83
+ email:
84
+ - nate@natemiller.org
85
+ executables:
86
+ - mir
87
+ extensions: []
88
+
89
+ extra_rdoc_files: []
90
+
91
+ files:
92
+ - .gitignore
93
+ - Gemfile
94
+ - Rakefile
95
+ - bin/mir
96
+ - db/migrate/001_create_app_settings.rb
97
+ - db/migrate/002_create_resources.rb
98
+ - lib/mir.rb
99
+ - lib/mir/application.rb
100
+ - lib/mir/config.rb
101
+ - lib/mir/disk.rb
102
+ - lib/mir/disk/amazon.rb
103
+ - lib/mir/index.rb
104
+ - lib/mir/logger.rb
105
+ - lib/mir/models/app_setting.rb
106
+ - lib/mir/models/resource.rb
107
+ - lib/mir/options.rb
108
+ - lib/mir/utils.rb
109
+ - lib/mir/version.rb
110
+ - mir.gemspec
111
+ - spec/spec_helper.rb
112
+ has_rdoc: true
113
+ homepage: ""
114
+ licenses: []
115
+
116
+ post_install_message:
117
+ rdoc_options: []
118
+
119
+ require_paths:
120
+ - lib
121
+ required_ruby_version: !ruby/object:Gem::Requirement
122
+ none: false
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: "0"
127
+ required_rubygems_version: !ruby/object:Gem::Requirement
128
+ none: false
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: "0"
133
+ requirements: []
134
+
135
+ rubyforge_project: mir
136
+ rubygems_version: 1.6.2
137
+ signing_key:
138
+ specification_version: 3
139
+ summary: A utility for backing up resources
140
+ test_files:
141
+ - spec/spec_helper.rb