crackup 1.0.0

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/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
+