awshucks 0.0.1

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.
Files changed (44) hide show
  1. data/CHANGES +4 -0
  2. data/LICENSE +19 -0
  3. data/README +32 -0
  4. data/Rakefile +14 -0
  5. data/TODO +143 -0
  6. data/bin/awshucks +10 -0
  7. data/lib/awshucks.rb +29 -0
  8. data/lib/awshucks/cli.rb +76 -0
  9. data/lib/awshucks/command.rb +98 -0
  10. data/lib/awshucks/commands.rb +3 -0
  11. data/lib/awshucks/commands/backup.rb +59 -0
  12. data/lib/awshucks/commands/backups.rb +25 -0
  13. data/lib/awshucks/commands/help.rb +22 -0
  14. data/lib/awshucks/commands/list.rb +39 -0
  15. data/lib/awshucks/commands/new_config.rb +36 -0
  16. data/lib/awshucks/commands/reset_metadata_cache.rb +38 -0
  17. data/lib/awshucks/commands/restore.rb +54 -0
  18. data/lib/awshucks/config.rb +83 -0
  19. data/lib/awshucks/ext.rb +23 -0
  20. data/lib/awshucks/file_info.rb +23 -0
  21. data/lib/awshucks/file_store.rb +111 -0
  22. data/lib/awshucks/gemspec.rb +48 -0
  23. data/lib/awshucks/scanner.rb +58 -0
  24. data/lib/awshucks/specification.rb +128 -0
  25. data/lib/awshucks/version.rb +18 -0
  26. data/resources/awshucks.yml +21 -0
  27. data/spec/awshucks_spec.rb +7 -0
  28. data/spec/cli_spec.rb +130 -0
  29. data/spec/command_spec.rb +111 -0
  30. data/spec/commands/backup_spec.rb +164 -0
  31. data/spec/commands/backups_spec.rb +41 -0
  32. data/spec/commands/help_spec.rb +42 -0
  33. data/spec/commands/list_spec.rb +77 -0
  34. data/spec/commands/new_config_spec.rb +102 -0
  35. data/spec/commands/reset_metadata_cache_spec.rb +93 -0
  36. data/spec/commands/restore_spec.rb +219 -0
  37. data/spec/config_spec.rb +152 -0
  38. data/spec/ext_spec.rb +28 -0
  39. data/spec/file_info_spec.rb +45 -0
  40. data/spec/file_store_spec.rb +352 -0
  41. data/spec/scanner_spec.rb +106 -0
  42. data/spec/spec_helper.rb +36 -0
  43. data/spec/specification_spec.rb +41 -0
  44. metadata +121 -0
data/CHANGES ADDED
@@ -0,0 +1,4 @@
1
+ = awshucks Changelog
2
+ === Version 0.0.1
3
+
4
+ * Initial public release
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2007 Nathan Witmer
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7
+ of the Software, and to permit persons to whom the Software is furnished to do
8
+ so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
data/README ADDED
@@ -0,0 +1,32 @@
1
+ == awshucks
2
+
3
+ * Homepage[http://awshucks.rubyforge.org/rabal/]
4
+ * {Rubyforge Project}[http://rubyforge.org/projects/awshucks]
5
+ * email nwitmer-rubyforge at otherward dot net
6
+
7
+ == DESCRIPTION
8
+
9
+ awshucks is a utility for backing up files to Amazon's Simple Storage Service (S3)
10
+
11
+ == INSTALL
12
+
13
+ gem install awshucks
14
+
15
+ == USAGE
16
+
17
+ awshucks help
18
+
19
+ == FEATURES
20
+
21
+ * back up specific directories
22
+ * caches file metadata for better performance
23
+ * stores all configured backups in a single bucket
24
+ * only uploads when files are new or changed
25
+
26
+ == CREDITS
27
+
28
+ Thanks to Jeremy Hinegardner and Tim Pease for the help getting started with my first gem!
29
+
30
+ == LICENSE
31
+
32
+ Released under the MIT license. See LICENSE.
@@ -0,0 +1,14 @@
1
+ # make sure our project's ./lib directory is added to the ruby search path
2
+ $: << File.join(File.dirname(__FILE__),"lib")
3
+
4
+ require 'rubygems'
5
+ require 'rake/gempackagetask'
6
+ require 'rake/clean'
7
+ require 'rake/rdoctask'
8
+ require 'rake/contrib/sshpublisher'
9
+
10
+ require 'awshucks'
11
+
12
+ load 'tasks/setup.rb'
13
+
14
+
data/TODO ADDED
@@ -0,0 +1,143 @@
1
+ Next:
2
+
3
+ * website and documentation
4
+
5
+ * look into marcel's usage of changes/changelog, and other automation
6
+
7
+ Awshucks
8
+
9
+ Awshucks is a utility for backing up your files to Amazon's http://aws.amazon.com/s3 Simple Storage Service (S3).
10
+
11
+ Getting started:
12
+
13
+ * Install awshucks via rubygems:
14
+
15
+ sudo gem install awshucks
16
+
17
+ * Generate a new config file
18
+
19
+ awshucks generate
20
+
21
+ This will generate a default config file called awshucks.yml into the current working directory.
22
+
23
+ * Edit the config file, specifically:
24
+
25
+ access_key_id: put your access key here
26
+
27
+ secret_access_key: put your secret access key here
28
+
29
+ bucket: bucket name where awshucks will store your backed-up files
30
+
31
+ * Add the locations you want to back up.
32
+
33
+ photos:
34
+ location: "~/pictures"
35
+
36
+ email:
37
+ location: "~/mail"
38
+
39
+ * Run a backup
40
+
41
+ awshucks backup
42
+
43
+ This will back up all of the files from the locations you specified.
44
+
45
+ Limitations:
46
+
47
+ * Awshucks will not ignore dotfiles or other files. It will not, however, follow symlinks.
48
+ * If you remove a file on the local filesystem, it will currently not be deleted from your bucket on S3.
49
+ * If you move a file on the local filesystem, awshucks will treat it as a new file and re-upload it.
50
+
51
+
52
+
53
+
54
+ Links in the sidebar:
55
+
56
+ download
57
+ project page
58
+ amazon s3
59
+
60
+
61
+
62
+
63
+
64
+
65
+
66
+
67
+ Todo:
68
+
69
+ * critical features before release:
70
+ * svn repo
71
+
72
+ * nice to have
73
+ * deletion of locally-removed files -- will need to flag scanned files somehow
74
+ * ignores
75
+ * better output/logging (cattr_accessor logger)
76
+ * archival of removed files -- same deal, but with move&rename with additional metadata
77
+ * list of backed up files
78
+ * list of archived files
79
+ * --dry-run -n flag
80
+
81
+ * ignores
82
+ * list ignores with backups
83
+ * allow list of ignore patterns in backup config
84
+ * set defaults in generated config file for ignore option, and automatically merge them with backup configs
85
+
86
+ * project
87
+ * update README, gemspec with name, version, etc. etc. including docs about how metadata is stored, how files are stored, etc. etc. (after a basic section about how things work at a high level)
88
+ * update README to include rspec as a dependency for running the test suite
89
+ * document: does not follow symlinks!
90
+ * sync svn repo to rubyforge: http://plans.inplanb.com/articles/2007/07/01/mirror-projects-to-a-read-only-svn-archive-with-svk
91
+ * write spec helper for input/output capturing?
92
+ * clean up the specs -- DRY up the mocks with helpers, etc.
93
+
94
+ * better error handling for file not found errors for file scanner (move exceptions to another file?)
95
+ * allow rename instead for files with matching md5s? smart algorithm for fixing data store stuff?
96
+ * refactor the redefinition of execute to make sure everything gets the same arguments? pppossibly, also with Command 'help' do ... ?
97
+ * add integration tests for each of the commands to make sure they get executed with the correct arguments (arity)
98
+ * DRY up the commands that take a backup config as an option
99
+ * DRY up the specs re: output handling -- can rspec descriptions have multiple before(:each) blocks?
100
+ * idea: constructor(s) take a block, and instance_eval it
101
+
102
+ Someday:
103
+
104
+ * AES encryption -- see keybox code, using Schneier's algorithm as a reference
105
+ * pgpme plugin / extra for encrypted backups (golgo)
106
+
107
+ Done:
108
+
109
+ * create rubyforge project
110
+ * figure out where to include various gems... probably best in the main file (yep, for now)
111
+ * rescue file-not-found error on -c flag
112
+ * find out how mtime (in about) gets set when uploading large files --> last-modified != file's mtime (IO stream)
113
+ * filesystem crawling
114
+ * reorganize command and specs
115
+ * remove alib dependency
116
+ * add backups command to list available backups and their configuration
117
+ * switch up IO handling in test cases and see if it's possible to replace dependence on CLI instance
118
+ * no more passing cli around at all!
119
+ * remove cli from args passed in to a command (piggyback help message onto config...)
120
+ * compute/store MD5sum of file on upload
121
+ * compare MD5 before uploading
122
+ * find out if ensure blocks will still get executed on SystemExit (they will)
123
+ * store metadata automatically as a separate file for better performance
124
+ * write code to actually use metadata instead of the file objects (most of the time)
125
+ * metadata: include bits of logic for handling touched-but-not-actually-changed files (update metadata cache)
126
+ * metadata: write "reset_metadata_cache" command, which takes an optional backup name as well -- deletes the metadata cache file
127
+ * write restore command
128
+ * default target, or specify a path
129
+ * compute/compare MD5sum of file on restore
130
+ * write list command
131
+ * generator for default config
132
+
133
+ Not Gonna Do:
134
+
135
+ * write sync_metadata command to use stored files' metadata as authoritative -- never mind, this happens implicitly.
136
+
137
+ Questions:
138
+
139
+ * can bucket names have spaces in them? --> no.
140
+ * how to handle deleted files? set metadata status? rename them with special naming and store metadata? (archive?) --> archive FTW!
141
+ * store metadata in special file about last-backed-up date and top-level directories? --> perhaps! it'd save bandwidth!
142
+ * warn if the same path is specified in two configs? pppossibly! --> ehhh not now, maybe later
143
+ * complain if a backup has a space in it? --> nah, it's allowable, it's just a YAML key. specify a string if you want.
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'awshucks'
5
+ rescue LoadError => le
6
+ $: << File.expand_path(File.join(File.dirname(__FILE__),"..","lib"))
7
+ require 'awshucks'
8
+ end
9
+
10
+ Awshucks::CLI.execute
@@ -0,0 +1,29 @@
1
+ module Awshucks
2
+
3
+ ROOT_DIR = File.expand_path(File.join(File.dirname(__FILE__),".."))
4
+ LIB_DIR = File.join(ROOT_DIR,"lib").freeze
5
+ RESOURCE_DIR = File.join(ROOT_DIR,"resources").freeze
6
+
7
+ #
8
+ # Utility method to require all files ending in .rb in the directory
9
+ # with the same name as this file minus .rb
10
+ #
11
+ def require_all_libs_relative_to(fname)
12
+ prepend = File.basename(fname,".rb")
13
+ search_me = File.join(File.dirname(fname),prepend)
14
+
15
+ Dir.entries(search_me).each do |rb|
16
+ if File.extname(rb) == ".rb" then
17
+ require "#{prepend}/#{File.basename(rb,".rb")}"
18
+ end
19
+ end
20
+ end
21
+ module_function :require_all_libs_relative_to
22
+
23
+ end
24
+
25
+ require 'rubygems'
26
+ require 'optparse'
27
+ require 'aws/s3'
28
+
29
+ Awshucks.require_all_libs_relative_to(__FILE__)
@@ -0,0 +1,76 @@
1
+ module Awshucks
2
+
3
+ class CLI
4
+
5
+ def self.execute
6
+ new.execute(ARGV)
7
+ end
8
+
9
+ # parses the options and runs the command
10
+ def execute(args)
11
+
12
+ option_parser.parse!(args) # changes args!
13
+
14
+ if parsed_options.show_help || args.empty?
15
+ puts help_message
16
+ else
17
+ Command.parse_and_execute(args, config)
18
+ end
19
+
20
+ rescue CommandError => e
21
+ error(e)
22
+ end
23
+
24
+ def help_message
25
+ option_parser.to_s
26
+ end
27
+
28
+ private
29
+
30
+ def error(msg)
31
+ $stderr.puts(msg)
32
+ exit(-1)
33
+ end
34
+
35
+ def option_parser
36
+
37
+ OptionParser.new do |opts|
38
+ opts.banner = "Usage: awshucks [options] <command> [command options]"
39
+ opts.separator "awshucks version #{Awshucks::VERSION}"
40
+ opts.separator "type 'awshucks help <subcommand'> for help with a specific command"
41
+ opts.separator "The default configuration file used is awshucks.yml in the current working directory."
42
+ opts.separator ""
43
+ opts.separator "Options:"
44
+
45
+ opts.on('-h', '--help', "Displays this help message") do
46
+ parsed_options.show_help = true
47
+ end
48
+
49
+ opts.on('-c', '--config-file FILE', "Set the awshucks config file to FILE") do |config_file|
50
+ parsed_options.config_file = config_file
51
+ end
52
+
53
+ opts.separator ""
54
+ opts.separator "Available Commands:"
55
+
56
+ Command.commands.keys.sort.each do |cmd|
57
+ opts.separator " " << Command.commands[cmd].command
58
+ end
59
+
60
+ end
61
+ end
62
+
63
+ def parsed_options
64
+ @parsed_options ||= OpenStruct.new
65
+ end
66
+
67
+ def config
68
+ config_file = parsed_options.config_file || 'awshucks.yml'
69
+ @config ||= returning(Config.new(config_file)) do |config|
70
+ config.help_message = help_message
71
+ end
72
+ end
73
+
74
+ end
75
+
76
+ end
@@ -0,0 +1,98 @@
1
+ module Awshucks
2
+
3
+ class CommandError < Exception; end
4
+
5
+ class ExecutionError < CommandError; end
6
+
7
+ class UnknownCommandError < CommandError
8
+ def initialize(msg)
9
+ super "Could not find command '#{msg}'"
10
+ end
11
+ end
12
+
13
+ # wraps up command parsing functions. Command class has class-level accessor for registering commands.
14
+ class Command
15
+
16
+ ##### class methods #####
17
+
18
+ class << self
19
+
20
+ def inherited(klass)
21
+ # save it for later, klass doesn't get fully defined until after this callback
22
+ inherited_commands << klass
23
+ end
24
+
25
+ def parse_and_execute(args, config)
26
+ key = args.shift
27
+ if key && commands[key] # be explicit about nil, no invalid command inheritance hijinks allowed
28
+ commands[key].execute(args, config)
29
+ else
30
+ raise UnknownCommandError, key
31
+ end
32
+ end
33
+
34
+ def commands
35
+ @commands ||= returning({}) do |commands|
36
+ inherited_commands.each do |command|
37
+ commands[command.command] = command
38
+ end
39
+ end
40
+ end
41
+
42
+ def option_string
43
+ new.option_parser.to_s # ehhh not a huge fan of this...
44
+ end
45
+
46
+ def execute(args, config)
47
+ new.execute(args, config)
48
+ end
49
+
50
+ attr_accessor :command
51
+ attr_accessor :usage
52
+ attr_accessor :description
53
+
54
+ #######
55
+ private
56
+ #######
57
+
58
+ def inherited_commands
59
+ @inherited ||= []
60
+ end
61
+
62
+ end
63
+
64
+ ##### instance methods ####
65
+
66
+ def execute(args, config)
67
+ raise CommandError, "must implement the execute method!"
68
+ end
69
+
70
+ def option_parser
71
+ @option_parser ||= OptionParser.new do |opts|
72
+ opts.banner = self.class.usage
73
+ opts.separator self.class.description
74
+ opts.separator ""
75
+ if respond_to?(:custom_options)
76
+ opts.separator "Options:"
77
+ custom_options(opts)
78
+ else
79
+ opts.separator "No options."
80
+ end
81
+ end
82
+ end
83
+
84
+ # define the method custom_options(opts) if options are required for a command
85
+
86
+ #######
87
+ private
88
+ #######
89
+
90
+ # for use in the option definitions
91
+ def parsed_options
92
+ @parsed_options ||= OpenStruct.new
93
+ end
94
+
95
+ end
96
+
97
+
98
+ end
@@ -0,0 +1,3 @@
1
+ Dir.entries(File.join(File.dirname(__FILE__), 'commands')).each do |file|
2
+ require "awshucks/commands/#{File.basename(file,".rb")}" if File.extname(file) == '.rb'
3
+ end
@@ -0,0 +1,59 @@
1
+ module Awshucks
2
+
3
+ class BackupCommand < Command
4
+ self.command = 'backup'
5
+ self.usage = 'backup [config name]'
6
+ self.description = "Backs up all locations specified in the config file. " +
7
+ "If you specify a config name, awshucks will back up only the files specified in that config."
8
+
9
+ def execute(args, config)
10
+ @config = config
11
+ list = []
12
+ if args.empty?
13
+ list += config.backups
14
+ else
15
+ list << config.backup(args.first)
16
+ end
17
+
18
+ list.each {|b| backup(b) }
19
+
20
+ rescue UnknownBackupError
21
+ $stderr.puts "Unknown backup: #{args.first}"
22
+ end
23
+
24
+ #######
25
+ private
26
+ #######
27
+
28
+ attr_reader :config
29
+
30
+ def backup(backup_info)
31
+
32
+ puts "backing up #{backup_info.location}:"
33
+
34
+ file_store = FileStore.new(config.connection, config.bucket, backup_info.name)
35
+
36
+ base_path = File.expand_path(backup_info.location)
37
+
38
+ begin
39
+ Scanner.each_file(base_path) do |file_info|
40
+ print " #{file_info.filename}: "
41
+ full_path = File.join(base_path, file_info.filename)
42
+ if file_store.different_from?(file_info)
43
+ print "changed, uploading (#{file_info.size} bytes)..."
44
+ file_store.store(file_info)
45
+ puts "done."
46
+ else
47
+ puts "unchanged."
48
+ end
49
+ end
50
+ ensure
51
+ puts "saving metadata cache..."
52
+ file_store.save_cache
53
+ end
54
+
55
+ end # backup
56
+
57
+ end # class
58
+
59
+ end