crackup 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/HISTORY ADDED
@@ -0,0 +1,4 @@
1
+ Crackup Release History
2
+
3
+ Version 1.0.0 (11/16/2006)
4
+ * First release.
data/LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ Copyright (c) 2006 Ryan Grove <ryan@wonko.com>
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice,
8
+ this list of conditions and the following disclaimer.
9
+ * Redistributions in binary form must reproduce the above copyright notice,
10
+ this list of conditions and the following disclaimer in the documentation
11
+ and/or other materials provided with the distribution.
12
+ * Neither the name of Crackup nor the names of its contributors may be used
13
+ to endorse or promote products derived from this software without
14
+ specific prior written permission.
15
+
16
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
20
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
23
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README ADDED
@@ -0,0 +1,24 @@
1
+ = Crackup (Crappy Remote Backup)
2
+
3
+ Crackup is a pretty simple, pretty secure remote backup solution for folks who
4
+ want to keep their data securely backed up but aren't particularly concerned
5
+ about bandwidth usage.
6
+
7
+ Crackup is ideal for backing up lots of small files, but somewhat less ideal
8
+ for backing up large files, since any change to a file means the entire file
9
+ must be transferred. If you need something bandwidth-efficient, try Duplicity.
10
+
11
+ Backups are compressed and (optionally) encrypted via GPG and can be
12
+ transferred to the remote location over a variety of protocols, including FTP.
13
+ Additional storage drivers can easily be written in Ruby.
14
+
15
+ Author:: Ryan Grove (mailto:ryan@wonko.com)
16
+ Version:: 1.0.0
17
+ Copyright:: Copyright (c) 2006 Ryan Grove. All rights reserved.
18
+ License:: New BSD License (http://opensource.org/licenses/bsd-license.php)
19
+ Website:: http://wonko.com/software/crackup
20
+
21
+ == Dependencies
22
+
23
+ - Ruby 1.8.5+
24
+ - GPG 1.4.2+ (if you want to encrypt your backups)
data/bin/crackup ADDED
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # crackup - command-line tool for performing Crackup backups. See
4
+ # <tt>crackup -h</tt> for usage information.
5
+ #
6
+ # Author:: Ryan Grove (mailto:ryan@wonko.com)
7
+ # Version:: 1.0.0
8
+ # Copyright:: Copyright (c) 2006 Ryan Grove. All rights reserved.
9
+ # License:: New BSD License (http://opensource.org/licenses/bsd-license.php)
10
+ #
11
+
12
+ require 'crackup'
13
+ require 'optparse'
14
+
15
+ APP_NAME = 'crackup'
16
+ APP_VERSION = '1.0.0'
17
+ APP_COPYRIGHT = 'Copyright (c) 2006 Ryan Grove (ryan@wonko.com). All rights reserved.'
18
+ APP_URL = 'http://wonko.com/software/crackup'
19
+
20
+ for sig in [:SIGINT, :SIGTERM]
21
+ trap(sig) { abort 'Interrupted' }
22
+ end
23
+
24
+ $stdout.sync = true
25
+ $stderr.sync = true
26
+
27
+ module Crackup
28
+ @options = {
29
+ :from => [Dir.pwd],
30
+ :exclude => [],
31
+ :passphrase => nil,
32
+ :to => nil,
33
+ :verbose => false
34
+ }
35
+
36
+ optparse = OptionParser.new do |optparse|
37
+ optparse.summary_width = 24
38
+ optparse.summary_indent = ' '
39
+
40
+ optparse.banner = "Usage: #{File.basename(__FILE__)} -t <url> [-p <pass>] [-x <file>] [-v] [<file|dir> ...]"
41
+ optparse.separator ''
42
+
43
+ optparse.on '-p', '--passphrase <pass>',
44
+ 'Encryption passphrase (if not specified, no',
45
+ 'encryption will be used)' do |passphrase|
46
+ @options[:passphrase] = passphrase
47
+ end
48
+
49
+ optparse.on '-t', '--to <url>',
50
+ 'Destination URL (e.g.,',
51
+ 'ftp://user:pass@server.com/path)' do |url|
52
+ @options[:to] = url.gsub("\\", '/').chomp('/')
53
+ end
54
+
55
+ optparse.on '-v', '--verbose',
56
+ 'Verbose output' do
57
+ @options[:verbose] = true
58
+ end
59
+
60
+ optparse.on '-x', '--exclude <file>',
61
+ 'Exclude files and directories whose names match the',
62
+ 'list in the specified file' do |filename|
63
+ unless File.file?(filename)
64
+ error "Exclusion list does not exist: #{filename}"
65
+ end
66
+
67
+ unless File.readable?(filename)
68
+ error "Exclusion list is not readable: #{filename}"
69
+ end
70
+
71
+ begin
72
+ @options[:exclude] = File.readlines(filename)
73
+ @options[:exclude].map! {|item| item.chomp }
74
+ rescue => e
75
+ error "Error reading exclusion file: #{e}"
76
+ end
77
+ end
78
+
79
+ optparse.on_tail '-h', '--help',
80
+ 'Display usage information (this message)' do
81
+ puts optparse
82
+ exit
83
+ end
84
+
85
+ optparse.on_tail '--version',
86
+ 'Display version information' do
87
+ puts "#{APP_NAME} v#{APP_VERSION} <#{APP_URL}>"
88
+ puts "#{APP_COPYRIGHT}"
89
+ puts
90
+ puts "#{APP_NAME} comes with ABSOLUTELY NO WARRANTY."
91
+ puts
92
+ puts "This program is open source software distributed under the terms of"
93
+ puts "the New BSD License. For details, see the LICENSE file contained in"
94
+ puts "the source distribution."
95
+ exit
96
+ end
97
+ end
98
+
99
+ # Parse command line options.
100
+ begin
101
+ optparse.parse!(ARGV)
102
+ rescue => e
103
+ puts optparse
104
+ puts
105
+ abort("Error: #{e}")
106
+ end
107
+
108
+ if @options[:to].nil?
109
+ puts optparse
110
+ puts
111
+ abort 'Error: No destination URL specified.'
112
+ end
113
+
114
+ # Add files to the "from" array.
115
+ if ARGV.length > 0
116
+ @options[:from] = []
117
+
118
+ while filename = ARGV.shift
119
+ @options[:from] << filename.chomp('/')
120
+ end
121
+ end
122
+
123
+ # Load driver.
124
+ begin
125
+ @driver = Crackup::Driver.get_driver(@options[:to])
126
+ rescue => e
127
+ error e
128
+ end
129
+
130
+ # Get the remote file index.
131
+ debug 'Retrieving remote file index...'
132
+
133
+ begin
134
+ @remote_files = get_remote_files(@options[:to])
135
+ rescue => e
136
+ error e
137
+ end
138
+
139
+ # Build a list of local files and directories.
140
+ debug 'Building local file list...'
141
+
142
+ begin
143
+ @local_files = get_local_files()
144
+ rescue => e
145
+ error e
146
+ end
147
+
148
+ # Determine differences.
149
+ debug 'Determining differences...'
150
+ begin
151
+ update = get_updated_files(@local_files, @remote_files)
152
+ remove = get_removed_files(@local_files, @remote_files)
153
+ rescue => e
154
+ error e
155
+ end
156
+
157
+ # Remove files from the remote location if necessary.
158
+ unless remove.empty?
159
+ debug 'Removing stale files from remote location...'
160
+
161
+ begin
162
+ remove_files(remove)
163
+ rescue => e
164
+ error e
165
+ end
166
+ end
167
+
168
+ # Update files at the remote location if necessary.
169
+ unless update.empty?
170
+ debug 'Updating remote location with new/changed files...'
171
+
172
+ begin
173
+ update_files(update)
174
+ rescue => e
175
+ error e
176
+ end
177
+ end
178
+
179
+ # Update the remote file index if necessary.
180
+ unless remove.empty? && update.empty?
181
+ debug 'Updating remote index...'
182
+
183
+ begin
184
+ update_remote_index
185
+ rescue => e
186
+ error e
187
+ end
188
+ end
189
+
190
+ debug 'Finished!'
191
+ end
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # crackup-restore - command-line tool for restoring files from Crackup backups.
4
+ # See <tt>crackup-restore -h</tt> for usage information.
5
+ #
6
+ # Author:: Ryan Grove (mailto:ryan@wonko.com)
7
+ # Version:: 1.0.0
8
+ # Copyright:: Copyright (c) 2006 Ryan Grove. All rights reserved.
9
+ # License:: New BSD License (http://opensource.org/licenses/bsd-license.php)
10
+ #
11
+
12
+ require 'crackup'
13
+ require 'optparse'
14
+
15
+ APP_NAME = 'crackup-restore'
16
+ APP_VERSION = '1.0.0'
17
+ APP_COPYRIGHT = 'Copyright (c) 2006 Ryan Grove (ryan@wonko.com). All rights reserved.'
18
+ APP_URL = 'http://wonko.com/software/crackup'
19
+
20
+ for sig in [:SIGINT, :SIGTERM]
21
+ trap(sig) { abort 'Interrupted' }
22
+ end
23
+
24
+ $stdout.sync = true
25
+ $stderr.sync = true
26
+
27
+ module Crackup
28
+ @options = {
29
+ :all => false,
30
+ :from => nil,
31
+ :list => false,
32
+ :only => [],
33
+ :passphrase => nil,
34
+ :to => Dir.pwd,
35
+ :verbose => false
36
+ }
37
+
38
+ optparse = OptionParser.new do |optparse|
39
+ optparse.summary_width = 24
40
+ optparse.summary_indent = ' '
41
+
42
+ optparse.banner = "Usage: #{File.basename(__FILE__)} -f <url> -t <path> [-p <pass>] [-v] [<file|dir> ...]\n" +
43
+ " #{File.basename(__FILE__)} -f <url> -l [-p <pass>] [-v]"
44
+ optparse.separator ''
45
+
46
+ optparse.on '-f', '--from <url>',
47
+ 'Remote URL to restore from (e.g.,',
48
+ 'ftp://user:pass@server.com/path)' do |url|
49
+ @options[:from] = url.gsub("\\", '/').chomp('/')
50
+ end
51
+
52
+ optparse.on '-l', '--list',
53
+ 'List all files at the remote location' do
54
+ @options[:list] = true
55
+ end
56
+
57
+ optparse.on '-p', '--passphrase <pass>',
58
+ 'Encryption passphrase (if not specified, no',
59
+ 'encryption will be used)' do |passphrase|
60
+ @options[:passphrase] = passphrase
61
+ end
62
+
63
+ optparse.on '-t', '--to <path>',
64
+ 'Destination root directory for the restored files' do |path|
65
+ @options[:to] = path.chomp('/')
66
+ end
67
+
68
+ optparse.on '-v', '--verbose',
69
+ 'Verbose output' do
70
+ @options[:verbose] = true
71
+ end
72
+
73
+ optparse.on_tail '-h', '--help',
74
+ 'Display usage information (this message)' do
75
+ puts optparse
76
+ exit
77
+ end
78
+
79
+ optparse.on_tail '--version',
80
+ 'Display version information' do
81
+ puts "#{APP_NAME} v#{APP_VERSION} <#{APP_URL}>"
82
+ puts "#{APP_COPYRIGHT}"
83
+ puts
84
+ puts "#{APP_NAME} comes with ABSOLUTELY NO WARRANTY."
85
+ puts
86
+ puts "This program is open source software distributed under the terms of"
87
+ puts "the New BSD License. For details, see the LICENSE file contained in"
88
+ puts "the source distribution."
89
+ exit
90
+ end
91
+ end
92
+
93
+ # Parse command line options.
94
+ begin
95
+ optparse.parse!(ARGV)
96
+ rescue => e
97
+ puts optparse
98
+ puts
99
+ error e
100
+ end
101
+
102
+ if @options[:from].nil?
103
+ puts optparse
104
+ puts
105
+ abort 'Error: No remote URL specified.'
106
+ end
107
+
108
+ # Add files to the "only" array.
109
+ if ARGV.length > 0
110
+ @options[:only] = []
111
+
112
+ while filename = ARGV.shift
113
+ @options[:only] << filename.chomp('/')
114
+ end
115
+ else
116
+ @options[:all] = true
117
+ end
118
+
119
+ # Load driver.
120
+ begin
121
+ @driver = Crackup::Driver.get_driver(@options[:from])
122
+ rescue => e
123
+ error e
124
+ end
125
+
126
+ # Get the list of remote files and directories.
127
+ debug 'Retrieving remote file list...'
128
+
129
+ begin
130
+ @remote_files = get_remote_files(@options[:from])
131
+ rescue => e
132
+ error e
133
+ end
134
+
135
+ # List remote files if the --list option was given.
136
+ if @options[:list]
137
+ puts get_list(@remote_files).sort
138
+ exit
139
+ end
140
+
141
+ # Restore files.
142
+ debug 'Restoring files...'
143
+
144
+ begin
145
+ if @options[:all]
146
+ @remote_files.each_value {|file| file.restore(@options[:to]) }
147
+ else
148
+ @options[:only].each do |pattern|
149
+ files = find_remote_files(pattern)
150
+
151
+ if files.empty?
152
+ error "Remote file not found: #{pattern}"
153
+ end
154
+
155
+ files.each {|file| file.restore(@options[:to]) }
156
+ end
157
+ end
158
+ rescue => e
159
+ error e
160
+ end
161
+
162
+ debug 'Finished!'
163
+ end
@@ -0,0 +1,92 @@
1
+ require 'crackup/fsobject'
2
+
3
+ module Crackup
4
+
5
+ # Represents a directory on the local filesystem. Can contain any number of
6
+ # Crackup::FileSystemObjects as children.
7
+ class DirectoryObject
8
+ include FileSystemObject
9
+
10
+ attr_reader :children
11
+
12
+ #--
13
+ # Public Class Methods
14
+ #++
15
+
16
+ def initialize(name)
17
+ unless File.directory?(name)
18
+ raise ArgumentError, "#{name} is not a directory"
19
+ end
20
+
21
+ super(name)
22
+
23
+ refresh_children
24
+ end
25
+
26
+ #--
27
+ # Public Instance Methods
28
+ #++
29
+
30
+ # Gets an array of files contained in this directory or its children whose
31
+ # local filenames match _pattern_.
32
+ def find(pattern)
33
+ files = []
34
+
35
+ @children.each do |name, child|
36
+ if File.fnmatch?(pattern, child.name)
37
+ files << child
38
+ next
39
+ end
40
+
41
+ next unless child.is_a?(Crackup::DirectoryObject)
42
+ files << result if result = child.find(pattern)
43
+ end
44
+
45
+ return files
46
+ end
47
+
48
+ # Builds a Hash of child objects by analyzing the local filesystem. A
49
+ # refresh is automatically performed when the object is instantiated.
50
+ def refresh_children
51
+ @children = {}
52
+
53
+ Dir.open(@name) do |dir|
54
+ dir.each do |filename|
55
+ next if filename == '.' || filename == '..'
56
+
57
+ filename = File.join(dir.path, filename).gsub("\\", "/")
58
+
59
+ # Skip this file if it's in the exclusion list.
60
+ unless Crackup::options[:exclude].nil?
61
+ next if Crackup::options[:exclude].any? do |pattern|
62
+ File.fnmatch?(pattern, filename)
63
+ end
64
+ end
65
+
66
+ if File.directory?(filename)
67
+ @children[filename.chomp('/')] = Crackup::DirectoryObject.new(filename)
68
+ elsif File.file?(filename)
69
+ @children[filename] = Crackup::FileObject.new(filename)
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ # Removes the remote copy of this directory and all its children.
76
+ def remove
77
+ @children.each_value {|child| child.remove }
78
+ end
79
+
80
+ # Restores the remote copy of this directory to the specified local
81
+ # <em>path</em>.
82
+ def restore(path)
83
+ @children.each_value {|child| child.restore(path) }
84
+ end
85
+
86
+ # Uploads this directory and all its children to the remote location.
87
+ def update
88
+ @children.each_value {|child| child.update }
89
+ end
90
+ end
91
+
92
+ end
@@ -0,0 +1,80 @@
1
+ require 'uri'
2
+
3
+ module Crackup
4
+
5
+ # Base storage driver module for Crackup.
6
+ #
7
+ # To write a Crackup storage driver:
8
+ #
9
+ # - Create a class in Crackup::Driver named "FooDriver", where "Foo" is the
10
+ # capitalized version of the URI scheme your driver will handle (e.g.,
11
+ # "Ftp", "Sftp", etc.).
12
+ # - Name your class file <tt>foo.rb</tt> ("foo" being the lowercase URI scheme
13
+ # this time) and place it in Crackup's <tt>lib/crackup/drivers</tt>
14
+ # directory.
15
+ # - In your class, mixin the Crackup::Driver module and override at least the
16
+ # delete, get, and put methods.
17
+ #
18
+ # That's all there is to it. See Crackup::Driver::FileDriver and
19
+ # Crackup::Driver::FtpDriver for examples.
20
+ module Driver
21
+ attr_reader :url
22
+
23
+ # Gets an instance of the appropriate storage driver to handle the specified
24
+ # _url_. If no suitable driver is found, raises a Crackup::StorageError.
25
+ def self.get_driver(url)
26
+ begin
27
+ uri = URI::parse(url)
28
+ rescue => e
29
+ raise Crackup::StorageError, "Invalid URL: #{url}: #{e}"
30
+ end
31
+
32
+ # Use the filesystem driver if no scheme is specified or if the scheme is
33
+ # a single letter (which indicates a Windows drive letter).
34
+ if uri.scheme.nil? || uri.scheme =~ /^[a-z]$/i
35
+ scheme = 'file'
36
+ else
37
+ scheme = uri.scheme.downcase
38
+ end
39
+
40
+ # Load the driver.
41
+ unless require(File.dirname(__FILE__) + "/drivers/#{scheme}")
42
+ raise Crackup::StorageError, "Driver not found for scheme '#{uri.scheme}'"
43
+ end
44
+
45
+ return const_get("#{scheme.capitalize}Driver").new(url)
46
+ end
47
+
48
+ def initialize(url)
49
+ @url = url
50
+ end
51
+
52
+ # Deletes the file at the specified _url_. This method does nothing and is
53
+ # intended to be overridden by a driver class.
54
+ def delete(url)
55
+ return false
56
+ end
57
+
58
+ # Downloads the file at _url_ to <em>local_filename</em>. This method does
59
+ # nothing and is intended to be overridden by a driver class.
60
+ def get(url, local_filename)
61
+ return false
62
+ end
63
+
64
+ # Gets the path portion of _url_.
65
+ def get_path(url)
66
+ uri = URI::parse(url)
67
+ return uri.path
68
+
69
+ rescue => e
70
+ raise Crackup::StorageError, "Invalid URL: #{url}: #{e}"
71
+ end
72
+
73
+ # Uploads the file at <em>local_filename</em> to _url_. This method does
74
+ # nothing and is intended to be overridden by a driver class.
75
+ def put(url, local_filename)
76
+ return false
77
+ end
78
+ end
79
+
80
+ end
@@ -0,0 +1,73 @@
1
+ require 'crackup/driver'
2
+ require 'fileutils'
3
+ require 'uri'
4
+
5
+ module Crackup; module Driver
6
+
7
+ # Filesystem storage driver for Crackup.
8
+ #
9
+ # Author:: Ryan Grove (mailto:ryan@wonko.com)
10
+ # Copyright:: Copyright (c) 2006 Ryan Grove. All rights reserved.
11
+ # License:: New BSD License (http://opensource.org/licenses/bsd-license.php)
12
+ #
13
+ class FileDriver
14
+ include Driver
15
+
16
+ # Deletes the file at the specified _url_.
17
+ def delete(url)
18
+ File.delete(get_path(url))
19
+ return true
20
+
21
+ rescue => e
22
+ raise Crackup::StorageError, "Unable to delete #{url}: #{e}"
23
+ end
24
+
25
+ # Downloads the file at _url_ to _local_filename_.
26
+ def get(url, local_filename)
27
+ FileUtils::copy(get_path(url), local_filename)
28
+ return true
29
+
30
+ rescue => e
31
+ raise Crackup::StorageError, "Unable to get #{url}: #{e}"
32
+ end
33
+
34
+ # Gets the filesystem path represented by _url_. This method is capable of
35
+ # parsing URLs in any of the following formats:
36
+ #
37
+ # - file:///foo/bar
38
+ # - file://c:/foo/bar
39
+ # - c:/foo/bar
40
+ # - /foo/bar
41
+ # - //smbhost/foo/bar
42
+ def get_path(url)
43
+ uri = URI::parse(url)
44
+ path = ''
45
+
46
+ if uri.scheme =~ /^[a-z]$/i
47
+ # Windows drive letter.
48
+ path = uri.scheme + ':'
49
+ elsif uri.host =~ /^[a-z]$/i
50
+ # Windows drive letter.
51
+ path = uri.host + ':'
52
+ elsif uri.scheme.nil? && !uri.host.nil?
53
+ # SMB share.
54
+ path = '//' + uri.host
55
+ end
56
+
57
+ return path += uri.path
58
+
59
+ rescue => e
60
+ raise Crackup::StorageError, "Invalid URL: #{url}"
61
+ end
62
+
63
+ # Uploads the file at _local_filename_ to _url_.
64
+ def put(url, local_filename)
65
+ FileUtils::copy(local_filename, get_path(url))
66
+ return true
67
+
68
+ rescue => e
69
+ raise Crackup::StorageError, "Unable to put #{url}: #{e}"
70
+ end
71
+ end
72
+
73
+ end; end
@@ -0,0 +1,71 @@
1
+ require 'crackup/driver'
2
+ require 'net/ftp'
3
+ require 'uri'
4
+
5
+ module Crackup; module Driver
6
+
7
+ # FTP storage driver for Crackup.
8
+ #
9
+ # Author:: Ryan Grove (mailto:ryan@wonko.com)
10
+ # Copyright:: Copyright (c) 2006 Ryan Grove. All rights reserved.
11
+ # License:: New BSD License (http://opensource.org/licenses/bsd-license.php)
12
+ #
13
+ class FtpDriver
14
+ include Driver
15
+
16
+ # Connects to the FTP server specified in _url_.
17
+ def initialize(url)
18
+ super(url)
19
+
20
+ # Parse URL.
21
+ begin
22
+ uri = URI::parse(url)
23
+ rescue => e
24
+ raise Crackup::StorageError, "Invalid URL: #{url}: #{e}"
25
+ end
26
+
27
+ @ftp = Net::FTP.new
28
+ @ftp.passive = true
29
+
30
+ begin
31
+ @ftp.connect(uri.host, uri.port.nil? ? 21 : uri.port)
32
+ rescue => e
33
+ raise Crackup::StorageError, "FTP connect failed: #{e}"
34
+ end
35
+
36
+ begin
37
+ @ftp.login(uri.user.nil? ? 'anonymous' : uri.user, uri.password)
38
+ rescue => e
39
+ raise Crackup::StorageError, "FTP login failed: #{e}"
40
+ end
41
+ end
42
+
43
+ # Deletes the file at the specified _url_.
44
+ def delete(url)
45
+ @ftp.delete(get_path(url))
46
+ return true
47
+
48
+ rescue => e
49
+ raise Crackup::StorageError, "Unable to delete #{url}: #{e}"
50
+ end
51
+
52
+ # Downloads the file at _url_ to _local_filename_.
53
+ def get(url, local_filename)
54
+ @ftp.getbinaryfile(get_path(url), local_filename)
55
+ return true
56
+
57
+ rescue => e
58
+ raise Crackup::StorageError, "Unable to download #{url}: #{e}"
59
+ end
60
+
61
+ # Uploads the file at _local_filename_ to _url_.
62
+ def put(url, local_filename)
63
+ @ftp.putbinaryfile(local_filename, get_path(url))
64
+ return true
65
+
66
+ rescue => e
67
+ raise Crackup::StorageError, "Unable to upload #{url}: #{e}"
68
+ end
69
+ end
70
+
71
+ end; end
@@ -0,0 +1,9 @@
1
+ module Crackup
2
+
3
+ class Error < StandardError; end
4
+ class CompressionError < Crackup::Error; end
5
+ class EncryptionError < Crackup::Error; end
6
+ class IndexError < Crackup::Error; end
7
+ class StorageError < Crackup::Error; end
8
+
9
+ end
@@ -0,0 +1,86 @@
1
+ require 'crackup/fsobject'
2
+ require 'digest/sha2'
3
+ require 'fileutils'
4
+
5
+ module Crackup
6
+
7
+ # Represents a file on the local filesystem.
8
+ class FileObject
9
+ include FileSystemObject
10
+
11
+ attr_reader :file_hash, :url
12
+
13
+ def initialize(filename)
14
+ unless File.file?(filename)
15
+ raise ArgumentError, "#{filename} is not a file"
16
+ end
17
+
18
+ super(filename)
19
+
20
+ # Get the file's SHA256 hash.
21
+ digest = Digest::SHA256.new()
22
+
23
+ File.open(filename, 'rb') do |file|
24
+ until file.eof? do
25
+ digest << file.read(1048576)
26
+ end
27
+ end
28
+
29
+ @file_hash = digest.hexdigest()
30
+ @url = "#{Crackup.driver.url}/crackup_#{@name_hash}"
31
+ end
32
+
33
+ # Removes this file from the remote location.
34
+ def remove
35
+ Crackup.debug "--> #{@name}"
36
+ Crackup.driver.delete(@url)
37
+ end
38
+
39
+ # Restores the remote copy of this file to the local path specified by
40
+ # _path_.
41
+ def restore(path)
42
+ path = path.chomp('/') + '/' + File.dirname(@name).delete(':')
43
+ filename = path + '/' + File.basename(@name)
44
+
45
+ Crackup.debug "--> #{filename}"
46
+
47
+ # Create the path if it doesn't exist.
48
+ unless File.directory?(path)
49
+ begin
50
+ FileUtils.mkdir_p(path)
51
+ rescue => e
52
+ raise Crackup::Error, "Unable to create local directory: #{path}"
53
+ end
54
+ end
55
+
56
+ # Download the remote file.
57
+ tempfile = Crackup.get_tempfile()
58
+ Crackup.driver.get(@url, tempfile)
59
+
60
+ # Decompress/decrypt the file.
61
+ if Crackup.options[:passphrase].nil?
62
+ Crackup.decompress_file(tempfile, filename)
63
+ else
64
+ Crackup.decrypt_file(tempfile, filename)
65
+ end
66
+ end
67
+
68
+ # Uploads this file to the remote location.
69
+ def update
70
+ Crackup.debug "--> #{@name}"
71
+
72
+ # Compress/encrypt the file.
73
+ tempfile = Crackup.get_tempfile()
74
+
75
+ if Crackup.options[:passphrase].nil?
76
+ Crackup.compress_file(@name, tempfile)
77
+ else
78
+ Crackup.encrypt_file(@name, tempfile)
79
+ end
80
+
81
+ # Upload the file.
82
+ Crackup.driver.put(@url, tempfile)
83
+ end
84
+ end
85
+
86
+ end
@@ -0,0 +1,18 @@
1
+ require 'digest/sha2'
2
+
3
+ module Crackup
4
+
5
+ # Represents a filesystem object on the local filesystem.
6
+ module FileSystemObject
7
+ attr_reader :name, :name_hash
8
+
9
+ def initialize(name)
10
+ @name = name.chomp('/')
11
+ @name_hash = Digest::SHA256.hexdigest(name)
12
+ end
13
+
14
+ def remove; end
15
+ def restore(local_path); end
16
+ end
17
+
18
+ end
data/lib/crackup.rb ADDED
@@ -0,0 +1,321 @@
1
+ ENV['PATH'] = "#{File.dirname(__FILE__)};#{ENV['PATH']}"
2
+
3
+ require 'crackup/errors'
4
+ require 'crackup/dirobject'
5
+ require 'crackup/driver'
6
+ require 'crackup/fileobject'
7
+ require 'tempfile'
8
+ require 'zlib'
9
+
10
+ module Crackup
11
+
12
+ GPG_DECRYPT = 'echo :passphrase | gpg --batch --quiet --no-tty --no-secmem-warning --cipher-algo aes256 --compress-algo bzip2 --passphrase-fd 0 --output :output_file :input_file'
13
+ GPG_ENCRYPT = 'echo :passphrase | gpg --batch --quiet --no-tty --no-secmem-warning --cipher-algo aes256 --compress-algo bzip2 --passphrase-fd 0 --output :output_file --symmetric :input_file'
14
+
15
+ attr_accessor :driver, :local_files, :options, :remote_files
16
+
17
+ # Reads _infile_ and compresses it to _outfile_ using zlib compression.
18
+ def self.compress_file(infile, outfile)
19
+ File.open(infile, 'rb') do |input|
20
+ Zlib::GzipWriter.open(outfile, 9) do |output|
21
+ while data = input.read(1048576) do
22
+ output.write(data)
23
+ end
24
+ end
25
+ end
26
+
27
+ rescue => e
28
+ raise Crackup::CompressionError, "Unable to compress #{infile}: #{e}"
29
+ end
30
+
31
+ # Prints _message_ to +stdout+ if verbose mode is enabled.
32
+ def self.debug(message)
33
+ puts message if @options[:verbose] || $VERBOSE
34
+ end
35
+
36
+ # Reads _infile_ and decompresses it to _outfile_ using zlib compression.
37
+ def self.decompress_file(infile, outfile)
38
+ Zlib::GzipReader.open(infile) do |input|
39
+ File.open(outfile, 'wb') do |output|
40
+ while data = input.read(1048576) do
41
+ output.write(data)
42
+ end
43
+ end
44
+ end
45
+
46
+ rescue => e
47
+ raise Crackup::CompressionError, "Unable to decompress #{infile}: #{e}"
48
+ end
49
+
50
+ # Calls GPG to decrypt _infile_ to _outfile_.
51
+ def self.decrypt_file(infile, outfile)
52
+ File.delete(outfile) if File.exist?(outfile)
53
+
54
+ gpg_command = String.new(GPG_DECRYPT)
55
+ gpg_command.gsub!(':input_file', escapeshellarg(infile))
56
+ gpg_command.gsub!(':output_file', escapeshellarg(outfile))
57
+ gpg_command.gsub!(':passphrase', escapeshellarg(@options[:passphrase]))
58
+
59
+ unless system(gpg_command)
60
+ raise Crackup::EncryptionError, "Unable to decrypt file: #{infile}"
61
+ end
62
+ end
63
+
64
+ def self.driver
65
+ return @driver
66
+ end
67
+
68
+ # Calls GPG to encrypt _infile_ to _outfile_.
69
+ def self.encrypt_file(infile, outfile)
70
+ File.delete(outfile) if File.exist?(outfile)
71
+
72
+ gpg_command = String.new(GPG_ENCRYPT)
73
+ gpg_command.gsub!(':input_file', escapeshellarg(infile))
74
+ gpg_command.gsub!(':output_file', escapeshellarg(outfile))
75
+ gpg_command.gsub!(':passphrase', escapeshellarg(@options[:passphrase]))
76
+
77
+ unless system(gpg_command)
78
+ raise Crackup::EncryptionError, "Unable to encrypt file: #{infile}"
79
+ end
80
+ end
81
+
82
+ # Prints the specified _message_ to +stderr+ and exits with an error
83
+ # code of 1.
84
+ def self.error(message)
85
+ abort "#{APP_NAME}: #{message}"
86
+ end
87
+
88
+ # Wraps _arg_ in single quotes, escaping any single quotes contained therein,
89
+ # thus making it safe for use as a shell argument.
90
+ def self.escapeshellarg(arg)
91
+ return "'#{arg.gsub("'", "\\'")}'"
92
+ end
93
+
94
+ # Gets an array of files in the remote file index whose local paths match
95
+ # _pattern_.
96
+ def self.find_remote_files(pattern)
97
+ files = []
98
+ pattern.chomp!('/')
99
+
100
+ @remote_files.each do |name, file|
101
+ if File.fnmatch?(pattern, file.name)
102
+ files << file
103
+ next
104
+ end
105
+
106
+ next unless file.is_a?(Crackup::DirectoryObject)
107
+ files += file.find(pattern)
108
+ end
109
+
110
+ return files
111
+ end
112
+
113
+ # Gets a flat array of filenames from _files_, which may be either a Hash
114
+ # or a Crackup::FileSystemObject.
115
+ def self.get_list(files)
116
+ list = []
117
+
118
+ if files.is_a?(Hash)
119
+ files.each_value {|value| list += get_list(value) }
120
+ elsif files.is_a?(Crackup::DirectoryObject)
121
+ list += get_list(files.children)
122
+ elsif files.is_a?(Crackup::FileObject)
123
+ list << files.name
124
+ end
125
+
126
+ return list
127
+ end
128
+
129
+ # Gets a Hash of Crackup::FileSystemObjects representing the files and
130
+ # directories on the local system in the locations specified by the array of
131
+ # filenames in <tt>options[:from]</tt>.
132
+ def self.get_local_files
133
+ local_files = {}
134
+
135
+ @options[:from].each do |filename|
136
+ next unless File.exist?(filename = filename.chomp('/'))
137
+ next if local_files.has_key?(filename)
138
+
139
+ # Skip this file if it's in the exclusion list.
140
+ unless @options[:exclude].nil?
141
+ next if @options[:exclude].any? do |pattern|
142
+ File.fnmatch?(pattern, filename)
143
+ end
144
+ end
145
+
146
+ if File.directory?(filename)
147
+ debug "--> #{filename}"
148
+ local_files[filename] = Crackup::DirectoryObject.new(filename)
149
+ elsif File.file?(filename)
150
+ debug "--> #{filename}"
151
+ local_files[filename] = Crackup::FileObject.new(filename)
152
+ end
153
+ end
154
+
155
+ return local_files
156
+ end
157
+
158
+ # Gets a Hash of Crackup::FileSystemObjects present at the remote location.
159
+ def self.get_remote_files(url)
160
+ tempfile = get_tempfile()
161
+
162
+ # Download the index file.
163
+ begin
164
+ @driver.get(url + '/.crackup_index', tempfile)
165
+ rescue => e
166
+ return {}
167
+ end
168
+
169
+ # Decompress/decrypt the index file.
170
+ oldfile = tempfile
171
+ tempfile = get_tempfile()
172
+
173
+ if @options[:passphrase].nil?
174
+ begin
175
+ decompress_file(oldfile, tempfile)
176
+ rescue => e
177
+ raise Crackup::IndexError, "Unable to decompress index file. Maybe " +
178
+ "it's encrypted?"
179
+ end
180
+ else
181
+ begin
182
+ decrypt_file(oldfile, tempfile)
183
+ rescue => e
184
+ raise Crackup::IndexError, "Unable to decrypt index file."
185
+ end
186
+ end
187
+
188
+ # Load the index file.
189
+ file_list = {}
190
+
191
+ begin
192
+ File.open(tempfile, 'rb') {|file| file_list = Marshal.load(file) }
193
+ rescue => e
194
+ raise Crackup::IndexError, "Remote index is invalid!"
195
+ end
196
+
197
+ unless file_list.is_a?(Hash)
198
+ raise Crackup::IndexError, "Remote index is invalid!"
199
+ end
200
+
201
+ return file_list
202
+ end
203
+
204
+ # Gets an Array of Crackup::FileSystemObjects representing files and
205
+ # directories that exist at the remote location but no longer exist at the
206
+ # local location.
207
+ def self.get_removed_files(local_files, remote_files)
208
+ removed = []
209
+
210
+ remote_files.each do |name, remotefile|
211
+ unless local_files.has_key?(name)
212
+ removed << remotefile
213
+ next
214
+ end
215
+
216
+ localfile = local_files[name]
217
+
218
+ if remotefile.is_a?(Crackup::DirectoryObject) &&
219
+ localfile.is_a?(Crackup::DirectoryObject)
220
+ removed += get_removed_files(localfile.children, remotefile.children)
221
+ end
222
+ end
223
+
224
+ return removed
225
+ end
226
+
227
+ # Creates a new temporary file in the system's temporary directory and returns
228
+ # its name. All temporary files will be deleted when the program exits.
229
+ def self.get_tempfile
230
+ tempfile = Tempfile.new('.crackup')
231
+ tempfile.close
232
+
233
+ return tempfile.path
234
+ end
235
+
236
+ # Gets an Array of Crackup::FileSystemObjects representing files and
237
+ # directories that are new or have been modified at the local location and
238
+ # need to be updated at the remote location.
239
+ def self.get_updated_files(local_files, remote_files)
240
+ updated = []
241
+
242
+ local_files.each do |name, localfile|
243
+ # Add the file to the list if it doesn't exist at the remote location.
244
+ unless remote_files.has_key?(name)
245
+ updated << localfile
246
+ next
247
+ end
248
+
249
+ remotefile = remote_files[name]
250
+
251
+ if localfile.is_a?(Crackup::DirectoryObject) &&
252
+ remotefile.is_a?(Crackup::DirectoryObject)
253
+ # Add to the list all updated files contained in the directory and its
254
+ # subdirectories.
255
+ updated += get_updated_files(localfile.children, remotefile.children)
256
+ elsif localfile.is_a?(Crackup::FileObject) &&
257
+ remotefile.is_a?(Crackup::FileObject)
258
+ # Add the file to the list if the local file has been modified.
259
+ unless localfile.file_hash == remotefile.file_hash
260
+ updated << localfile
261
+ end
262
+ end
263
+ end
264
+
265
+ return updated
266
+ end
267
+
268
+ def self.options
269
+ return @options
270
+ end
271
+
272
+ # Prints _message_ to +stdout+ and waits for user input, which is then
273
+ # returned.
274
+ def self.prompt(message)
275
+ puts message + ': '
276
+ return $stdin.gets
277
+ end
278
+
279
+ # Deletes each Crackup::FileSystemObject specified in the _files_ array from
280
+ # the remote location.
281
+ def self.remove_files(files)
282
+ files.each do |file|
283
+ file.remove
284
+ end
285
+ end
286
+
287
+ # Uploads each Crackup::FileSystemObject specified in the _files_ array to the
288
+ # remote location.
289
+ def self.update_files(files)
290
+ files.each do |file|
291
+ file.update
292
+ end
293
+ end
294
+
295
+ # Brings the remote file index up to date with the local one.
296
+ def self.update_remote_index
297
+ tempfile = get_tempfile()
298
+ remotefile = @options[:to] + '/.crackup_index'
299
+
300
+ File.open(tempfile, 'wb') {|file| Marshal.dump(@local_files, file) }
301
+
302
+ oldfile = tempfile
303
+ tempfile = get_tempfile()
304
+
305
+ if @options[:passphrase].nil?
306
+ compress_file(oldfile, tempfile)
307
+ else
308
+ encrypt_file(oldfile, tempfile)
309
+ end
310
+
311
+ begin
312
+ success = @driver.put(remotefile, tempfile)
313
+ rescue => e
314
+ tryagain = prompt('Unable to update remote index. Try again? (y/n)')
315
+
316
+ retry if tryagain.downcase == 'y'
317
+ raise Crackup::IndexError, "Unable to update remote index: #{e}"
318
+ end
319
+ end
320
+
321
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.0
3
+ specification_version: 1
4
+ name: crackup
5
+ version: !ruby/object:Gem::Version
6
+ version: 1.0.0
7
+ date: 2006-11-15 00:00:00 -08:00
8
+ summary: Crackup is a pretty simple, pretty secure remote backup solution for folks who want to keep their data securely backed up but aren't particularly concerned about bandwidth usage.
9
+ require_paths:
10
+ - lib
11
+ email: ryan@wonko.com
12
+ homepage: http://wonko.com/software/crackup
13
+ rubyforge_project:
14
+ description:
15
+ autorequire: crackup
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.8.4
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors:
30
+ - Ryan Grove
31
+ files:
32
+ - bin/crackup
33
+ - bin/crackup-restore
34
+ - lib/crackup
35
+ - lib/crackup.rb
36
+ - lib/crackup/dirobject.rb
37
+ - lib/crackup/driver.rb
38
+ - lib/crackup/drivers
39
+ - lib/crackup/errors.rb
40
+ - lib/crackup/fileobject.rb
41
+ - lib/crackup/fsobject.rb
42
+ - lib/crackup/drivers/file.rb
43
+ - lib/crackup/drivers/ftp.rb
44
+ - LICENSE
45
+ - HISTORY
46
+ - README
47
+ test_files: []
48
+
49
+ rdoc_options: []
50
+
51
+ extra_rdoc_files:
52
+ - README
53
+ - LICENSE
54
+ executables:
55
+ - crackup
56
+ - crackup-restore
57
+ extensions: []
58
+
59
+ requirements: []
60
+
61
+ dependencies: []
62
+