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 +4 -0
- data/LICENSE +25 -0
- data/README +24 -0
- data/bin/crackup +191 -0
- data/bin/crackup-restore +163 -0
- data/lib/crackup/dirobject.rb +92 -0
- data/lib/crackup/driver.rb +80 -0
- data/lib/crackup/drivers/file.rb +73 -0
- data/lib/crackup/drivers/ftp.rb +71 -0
- data/lib/crackup/errors.rb +9 -0
- data/lib/crackup/fileobject.rb +86 -0
- data/lib/crackup/fsobject.rb +18 -0
- data/lib/crackup.rb +321 -0
- metadata +62 -0
data/HISTORY
ADDED
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
|
data/bin/crackup-restore
ADDED
@@ -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,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
|
+
|