crackup 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/HISTORY CHANGED
@@ -1,4 +1,11 @@
1
1
  Crackup Release History
2
2
 
3
+ Version 1.0.1 (11/21/2006)
4
+ * Fix broken handling of symbolic links.
5
+ * Expand relative paths before backing up.
6
+ * Win32: If gpg.exe isn't in the system path, look for it at the path
7
+ specified in the HKCU\Software\GNU\GnuPG\gpgProgram registry key (if the key
8
+ exists) before failing.
9
+
3
10
  Version 1.0.0 (11/16/2006)
4
11
  * First release.
data/README CHANGED
@@ -13,12 +13,12 @@ transferred to the remote location over a variety of protocols, including FTP.
13
13
  Additional storage drivers can easily be written in Ruby.
14
14
 
15
15
  Author:: Ryan Grove (mailto:ryan@wonko.com)
16
- Version:: 1.0.0
16
+ Version:: 1.0.1
17
17
  Copyright:: Copyright (c) 2006 Ryan Grove. All rights reserved.
18
18
  License:: New BSD License (http://opensource.org/licenses/bsd-license.php)
19
19
  Website:: http://wonko.com/software/crackup
20
20
 
21
21
  == Dependencies
22
22
 
23
- - Ruby 1.8.5+
23
+ - Ruby 1.8.4+
24
24
  - GPG 1.4.2+ (if you want to encrypt your backups)
@@ -4,7 +4,7 @@
4
4
  # <tt>crackup -h</tt> for usage information.
5
5
  #
6
6
  # Author:: Ryan Grove (mailto:ryan@wonko.com)
7
- # Version:: 1.0.0
7
+ # Version:: 1.0.1
8
8
  # Copyright:: Copyright (c) 2006 Ryan Grove. All rights reserved.
9
9
  # License:: New BSD License (http://opensource.org/licenses/bsd-license.php)
10
10
  #
@@ -13,7 +13,7 @@ require 'crackup'
13
13
  require 'optparse'
14
14
 
15
15
  APP_NAME = 'crackup'
16
- APP_VERSION = '1.0.0'
16
+ APP_VERSION = '1.0.1'
17
17
  APP_COPYRIGHT = 'Copyright (c) 2006 Ryan Grove (ryan@wonko.com). All rights reserved.'
18
18
  APP_URL = 'http://wonko.com/software/crackup'
19
19
 
@@ -116,7 +116,7 @@ module Crackup
116
116
  @options[:from] = []
117
117
 
118
118
  while filename = ARGV.shift
119
- @options[:from] << filename.chomp('/')
119
+ @options[:from] << File.expand_path(filename)
120
120
  end
121
121
  end
122
122
 
@@ -4,7 +4,7 @@
4
4
  # See <tt>crackup-restore -h</tt> for usage information.
5
5
  #
6
6
  # Author:: Ryan Grove (mailto:ryan@wonko.com)
7
- # Version:: 1.0.0
7
+ # Version:: 1.0.1
8
8
  # Copyright:: Copyright (c) 2006 Ryan Grove. All rights reserved.
9
9
  # License:: New BSD License (http://opensource.org/licenses/bsd-license.php)
10
10
  #
@@ -13,7 +13,7 @@ require 'crackup'
13
13
  require 'optparse'
14
14
 
15
15
  APP_NAME = 'crackup-restore'
16
- APP_VERSION = '1.0.0'
16
+ APP_VERSION = '1.0.1'
17
17
  APP_COPYRIGHT = 'Copyright (c) 2006 Ryan Grove (ryan@wonko.com). All rights reserved.'
18
18
  APP_URL = 'http://wonko.com/software/crackup'
19
19
 
@@ -134,7 +134,7 @@ module Crackup
134
134
 
135
135
  # List remote files if the --list option was given.
136
136
  if @options[:list]
137
- puts get_list(@remote_files).sort
137
+ puts get_list(@remote_files)
138
138
  exit
139
139
  end
140
140
 
@@ -1,19 +1,21 @@
1
- ENV['PATH'] = "#{File.dirname(__FILE__)};#{ENV['PATH']}"
2
-
3
1
  require 'crackup/errors'
4
- require 'crackup/dirobject'
2
+ require 'crackup/directory_object'
5
3
  require 'crackup/driver'
6
- require 'crackup/fileobject'
7
- require 'tempfile'
4
+ require 'crackup/file_object'
5
+ require 'crackup/symlink_object'
6
+ require 'find'
7
+ require 'tmpdir'
8
8
  require 'zlib'
9
9
 
10
10
  module Crackup
11
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'
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
14
 
15
15
  attr_accessor :driver, :local_files, :options, :remote_files
16
16
 
17
+ @gpg_path = nil
18
+
17
19
  # Reads _infile_ and compresses it to _outfile_ using zlib compression.
18
20
  def self.compress_file(infile, outfile)
19
21
  File.open(infile, 'rb') do |input|
@@ -52,10 +54,11 @@ module Crackup
52
54
  File.delete(outfile) if File.exist?(outfile)
53
55
 
54
56
  gpg_command = String.new(GPG_DECRYPT)
55
- gpg_command.gsub!(':input_file', escapeshellarg(infile))
57
+ gpg_command.gsub!(':gpg', find_gpg())
58
+ gpg_command.gsub!(':input_file', escapeshellarg(infile))
56
59
  gpg_command.gsub!(':output_file', escapeshellarg(outfile))
57
- gpg_command.gsub!(':passphrase', escapeshellarg(@options[:passphrase]))
58
-
60
+ gpg_command.gsub!(':passphrase', escapeshellarg(@options[:passphrase]))
61
+
59
62
  unless system(gpg_command)
60
63
  raise Crackup::EncryptionError, "Unable to decrypt file: #{infile}"
61
64
  end
@@ -70,9 +73,10 @@ module Crackup
70
73
  File.delete(outfile) if File.exist?(outfile)
71
74
 
72
75
  gpg_command = String.new(GPG_ENCRYPT)
73
- gpg_command.gsub!(':input_file', escapeshellarg(infile))
76
+ gpg_command.gsub!(':gpg', find_gpg())
77
+ gpg_command.gsub!(':input_file', escapeshellarg(infile))
74
78
  gpg_command.gsub!(':output_file', escapeshellarg(outfile))
75
- gpg_command.gsub!(':passphrase', escapeshellarg(@options[:passphrase]))
79
+ gpg_command.gsub!(':passphrase', escapeshellarg(@options[:passphrase]))
76
80
 
77
81
  unless system(gpg_command)
78
82
  raise Crackup::EncryptionError, "Unable to encrypt file: #{infile}"
@@ -85,12 +89,64 @@ module Crackup
85
89
  abort "#{APP_NAME}: #{message}"
86
90
  end
87
91
 
88
- # Wraps _arg_ in single quotes, escaping any single quotes contained therein,
89
- # thus making it safe for use as a shell argument.
92
+ # Wraps _arg_ in single quotes (double quotes in Windows), escaping any quotes
93
+ # contained therein, thus making it safe for use as a shell argument.
90
94
  def self.escapeshellarg(arg)
91
- return "'#{arg.gsub("'", "\\'")}'"
95
+ if RUBY_PLATFORM =~ /mswin32/
96
+ return "\"#{arg.gsub('"', '\\"')}\""
97
+ else
98
+ return "'#{arg.gsub("'", "\\'")}'"
99
+ end
92
100
  end
93
101
 
102
+ # Returns the name of the GnuPG executable to use. First we search for +gpg+
103
+ # or <tt>gpg.exe</tt> in the path. On Windows, if it isn't in the system path,
104
+ # we try to find a pointer to it in the registry. If everything fails, a
105
+ # Crackup::Error is raised.
106
+ def self.find_gpg
107
+ # Don't bother finding gpg again if we've already found it.
108
+ return @gpg_path unless @gpg_path.nil?
109
+
110
+ # First, check to see if gpg is in the path.
111
+ if RUBY_PLATFORM =~ /mswin32/
112
+ path_dirs = ENV['PATH'].split(';')
113
+ filename = 'gpg.exe'
114
+ else
115
+ path_dirs = ENV['PATH'].split(':')
116
+ filename = 'gpg'
117
+ end
118
+
119
+ Find.find(*path_dirs) do |path|
120
+ return @gpg_path = filename if File.executable?(File.join(path, filename))
121
+ end
122
+
123
+ # Okay, it's not in the path. Unix users are screwed, but if we're on
124
+ # Windows, we'll make a last-ditch attempt to find it by checking for its
125
+ # registry key.
126
+ if RUBY_PLATFORM =~ /mswin32/
127
+ # Bail out if we can't load the Win32::Registry library.
128
+ unless require('win32/registry')
129
+ raise Crackup::Error, 'GnuPG not found.'
130
+ end
131
+
132
+ # Try to read the GnuPG registry key.
133
+ begin
134
+ gpg_path = nil
135
+ Win32::Registry.open(Win32::Registry::HKEY_CURRENT_USER,
136
+ 'Software\GNU\GnuPG') {|reg| gpg_path = reg.read_s('gpgProgram') }
137
+ rescue => e
138
+ raise Crackup::Error, 'GnuPG not found.'
139
+ end
140
+
141
+ if File.executable?(gpg_path)
142
+ return @gpg_path = "\"#{gpg_path}\""
143
+ end
144
+ end
145
+
146
+ # No luck. Bail out.
147
+ raise Crackup::Error, 'GnuPG not found.'
148
+ end
149
+
94
150
  # Gets an array of files in the remote file index whose local paths match
95
151
  # _pattern_.
96
152
  def self.find_remote_files(pattern)
@@ -103,8 +159,7 @@ module Crackup
103
159
  next
104
160
  end
105
161
 
106
- next unless file.is_a?(Crackup::DirectoryObject)
107
- files += file.find(pattern)
162
+ files += file.find(pattern) if file.is_a?(Crackup::DirectoryObject)
108
163
  end
109
164
 
110
165
  return files
@@ -117,16 +172,14 @@ module Crackup
117
172
 
118
173
  if files.is_a?(Hash)
119
174
  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
175
+ elsif files.is_a?(Crackup::FileSystemObject)
176
+ list += files.to_s.split("\n")
124
177
  end
125
178
 
126
- return list
179
+ return list.sort
127
180
  end
128
181
 
129
- # Gets a Hash of Crackup::FileSystemObjects representing the files and
182
+ # Gets a Hash of {Crackup::FileSystemObject}s representing the files and
130
183
  # directories on the local system in the locations specified by the array of
131
184
  # filenames in <tt>options[:from]</tt>.
132
185
  def self.get_local_files
@@ -143,13 +196,8 @@ module Crackup
143
196
  end
144
197
  end
145
198
 
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
199
+ debug "--> #{filename}"
200
+ local_files[filename] = Crackup::FileSystemObject.from(filename)
153
201
  end
154
202
 
155
203
  return local_files
@@ -227,10 +275,24 @@ module Crackup
227
275
  # Creates a new temporary file in the system's temporary directory and returns
228
276
  # its name. All temporary files will be deleted when the program exits.
229
277
  def self.get_tempfile
230
- tempfile = Tempfile.new('.crackup')
231
- tempfile.close
278
+ # We would use Ruby's tempfile library here, but for some reason it
279
+ # sometimes deletes temp files before the program exits, which can cause all
280
+ # kinds of problems.
281
+ i = -1
282
+
283
+ while tempfile = File.join(Dir.tmpdir(),
284
+ ".crackup.#{Process.pid}.#{i += 1}") do
285
+ break unless File.exist?(tempfile)
286
+ end
232
287
 
233
- return tempfile.path
288
+ at_exit do
289
+ begin
290
+ File.delete(tempfile)
291
+ rescue => e
292
+ end
293
+ end
294
+
295
+ return tempfile
234
296
  end
235
297
 
236
298
  # Gets an Array of Crackup::FileSystemObjects representing files and
@@ -253,12 +315,8 @@ module Crackup
253
315
  # Add to the list all updated files contained in the directory and its
254
316
  # subdirectories.
255
317
  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
318
+ elsif localfile != remotefile
319
+ updated << localfile
262
320
  end
263
321
  end
264
322
 
@@ -279,17 +337,13 @@ module Crackup
279
337
  # Deletes each Crackup::FileSystemObject specified in the _files_ array from
280
338
  # the remote location.
281
339
  def self.remove_files(files)
282
- files.each do |file|
283
- file.remove
284
- end
340
+ files.each {|file| file.remove }
285
341
  end
286
342
 
287
343
  # Uploads each Crackup::FileSystemObject specified in the _files_ array to the
288
344
  # remote location.
289
345
  def self.update_files(files)
290
- files.each do |file|
291
- file.update
292
- end
346
+ files.each {|file| file.update }
293
347
  end
294
348
 
295
349
  # Brings the remote file index up to date with the local one.
@@ -1,16 +1,17 @@
1
- require 'crackup/fsobject'
1
+ require 'crackup/fs_object'
2
2
 
3
3
  module Crackup
4
4
 
5
5
  # Represents a directory on the local filesystem. Can contain any number of
6
6
  # Crackup::FileSystemObjects as children.
7
7
  class DirectoryObject
8
+ include Enumerable
8
9
  include FileSystemObject
9
10
 
10
11
  attr_reader :children
11
12
 
12
13
  #--
13
- # Public Class Methods
14
+ # Public Instance Methods
14
15
  #++
15
16
 
16
17
  def initialize(name)
@@ -20,12 +21,24 @@ module Crackup
20
21
 
21
22
  super(name)
22
23
 
23
- refresh_children
24
+ refresh_children()
24
25
  end
25
26
 
26
- #--
27
- # Public Instance Methods
28
- #++
27
+ # Compares the specified Crackup::DirectoryObject to this one. Returns
28
+ # +true+ if the directories and all their children are the same, +false+
29
+ # otherwise.
30
+ def ==(directory)
31
+ return false unless directory.name == @name
32
+ return directory.all?{|child| child == @children[child.name] }
33
+ end
34
+
35
+ def [](key)
36
+ return @children[key]
37
+ end
38
+
39
+ def each
40
+ @children.each {|child| yield child }
41
+ end
29
42
 
30
43
  # Gets an array of files contained in this directory or its children whose
31
44
  # local filenames match _pattern_.
@@ -38,8 +51,9 @@ module Crackup
38
51
  next
39
52
  end
40
53
 
41
- next unless child.is_a?(Crackup::DirectoryObject)
42
- files << result if result = child.find(pattern)
54
+ if child.is_a?(Crackup::DirectoryObject)
55
+ files << result if result = child.find(pattern)
56
+ end
43
57
  end
44
58
 
45
59
  return files
@@ -54,22 +68,20 @@ module Crackup
54
68
  dir.each do |filename|
55
69
  next if filename == '.' || filename == '..'
56
70
 
57
- filename = File.join(dir.path, filename).gsub("\\", "/")
71
+ path = File.join(dir.path, filename).gsub("\\", "/")
58
72
 
59
73
  # Skip this file if it's in the exclusion list.
60
74
  unless Crackup::options[:exclude].nil?
61
75
  next if Crackup::options[:exclude].any? do |pattern|
62
- File.fnmatch?(pattern, filename)
76
+ File.fnmatch?(pattern, path)
63
77
  end
64
78
  end
65
79
 
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
80
+ @children[path] = Crackup::FileSystemObject.from(path)
71
81
  end
72
82
  end
83
+
84
+ return @children
73
85
  end
74
86
 
75
87
  # Removes the remote copy of this directory and all its children.
@@ -77,12 +89,17 @@ module Crackup
77
89
  @children.each_value {|child| child.remove }
78
90
  end
79
91
 
80
- # Restores the remote copy of this directory to the specified local
81
- # <em>path</em>.
92
+ # Restores the remote copy of this directory to the specified local _path_.
82
93
  def restore(path)
83
94
  @children.each_value {|child| child.restore(path) }
84
95
  end
85
96
 
97
+ def to_s
98
+ childnames = []
99
+ @children.each_value {|child| childnames << child.to_s }
100
+ return childnames.join("\n")
101
+ end
102
+
86
103
  # Uploads this directory and all its children to the remote location.
87
104
  def update
88
105
  @children.each_value {|child| child.update }
@@ -1,4 +1,4 @@
1
- require 'crackup/fsobject'
1
+ require 'crackup/fs_object'
2
2
  require 'digest/sha2'
3
3
  require 'fileutils'
4
4
 
@@ -18,18 +18,25 @@ module Crackup
18
18
  super(filename)
19
19
 
20
20
  # Get the file's SHA256 hash.
21
- digest = Digest::SHA256.new()
21
+ digest = Digest::SHA256.new
22
22
 
23
23
  File.open(filename, 'rb') do |file|
24
- until file.eof? do
25
- digest << file.read(1048576)
24
+ while buffer = file.read(1048576) do
25
+ digest << buffer
26
26
  end
27
27
  end
28
28
 
29
29
  @file_hash = digest.hexdigest()
30
30
  @url = "#{Crackup.driver.url}/crackup_#{@name_hash}"
31
31
  end
32
-
32
+
33
+ # Compares the specified Crackup::FileObject to this one. Returns +false+ if
34
+ # _file_ is different, +true+ if _file_ is the same. The comparison is
35
+ # performed using an SHA256 hash of the file contents.
36
+ def ==(file)
37
+ return file.name == @name && file.file_hash == @file_hash
38
+ end
39
+
33
40
  # Removes this file from the remote location.
34
41
  def remove
35
42
  Crackup.debug "--> #{@name}"
@@ -0,0 +1,43 @@
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
+ #--
10
+ # Public Class Methods
11
+ #++
12
+
13
+ # Returns an instance of the appropriate FileSystemObject subclass to
14
+ # represent _path_.
15
+ def self.from(path)
16
+ return Crackup::SymlinkObject.new(path) if File.symlink?(path)
17
+ return Crackup::DirectoryObject.new(path) if File.directory?(path)
18
+ return Crackup::FileObject.new(path) if File.file?(path)
19
+
20
+ raise Crackup::Error, "Unsupported filesystem object: #{path}"
21
+ end
22
+
23
+ #--
24
+ # Public Instance Methods
25
+ #++
26
+
27
+ def initialize(name)
28
+ @name = name.chomp('/')
29
+ @name_hash = Digest::SHA256.hexdigest(name)
30
+ end
31
+
32
+ def ==(fs_object); end
33
+ def remove; end
34
+ def restore(path); end
35
+
36
+ def to_s
37
+ return @name
38
+ end
39
+
40
+ def update; end
41
+ end
42
+
43
+ end
@@ -0,0 +1,66 @@
1
+ require 'crackup/fs_object'
2
+ require 'fileutils'
3
+
4
+ module Crackup
5
+
6
+ # Represents a symbolic link on the local filesystem.
7
+ class SymlinkObject
8
+ include FileSystemObject
9
+
10
+ attr_reader :file_hash, :target, :url
11
+
12
+ #--
13
+ # Public Instance Methods
14
+ #++
15
+
16
+ def initialize(linkname)
17
+ unless File.symlink?(linkname)
18
+ raise ArgumentError, "#{linkname} is not a symbolic link"
19
+ end
20
+
21
+ super(linkname)
22
+
23
+ @target = File.readlink(linkname)
24
+ end
25
+
26
+ # Compares the specified Crackup::SymlinkObject to this one. Returns +true+
27
+ # if they're the same, +false+ if _symlink_ is different.
28
+ def ==(symlink)
29
+ return symlink.name == @name && symlink.target == @target
30
+ end
31
+
32
+ # Removes this link from the remote location. This is actually a noop, since
33
+ # link data is just stored in the index.
34
+ def remove
35
+ Crackup.debug "--> #{@name}"
36
+ end
37
+
38
+ # Restores the remote copy of this link to the local path specified by
39
+ # _path_.
40
+ def restore(path)
41
+ path = path.chomp('/') + '/' + File.dirname(@name).delete(':')
42
+ linkname = path + '/' + File.basename(@name)
43
+
44
+ Crackup.debug "--> #{linkname}"
45
+
46
+ # Create the path if it doesn't exist.
47
+ unless File.directory?(path)
48
+ begin
49
+ FileUtils.mkdir_p(path)
50
+ rescue => e
51
+ raise Crackup::Error, "Unable to create local directory: #{path}"
52
+ end
53
+ end
54
+
55
+ # Create the link.
56
+ File.symlink(@target, linkname)
57
+ end
58
+
59
+ # Uploads this link to the remote location. This is actually a noop, since
60
+ # link data is just stored in the index.
61
+ def update
62
+ Crackup.debug "--> #{@name}"
63
+ end
64
+ end
65
+
66
+ end
metadata CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.9.0
3
3
  specification_version: 1
4
4
  name: crackup
5
5
  version: !ruby/object:Gem::Version
6
- version: 1.0.0
7
- date: 2006-11-15 00:00:00 -08:00
6
+ version: 1.0.1
7
+ date: 2006-11-20 00:00:00 -08:00
8
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
9
  require_paths:
10
10
  - lib
@@ -33,12 +33,13 @@ files:
33
33
  - bin/crackup-restore
34
34
  - lib/crackup
35
35
  - lib/crackup.rb
36
- - lib/crackup/dirobject.rb
36
+ - lib/crackup/directory_object.rb
37
37
  - lib/crackup/driver.rb
38
38
  - lib/crackup/drivers
39
39
  - lib/crackup/errors.rb
40
- - lib/crackup/fileobject.rb
41
- - lib/crackup/fsobject.rb
40
+ - lib/crackup/file_object.rb
41
+ - lib/crackup/fs_object.rb
42
+ - lib/crackup/symlink_object.rb
42
43
  - lib/crackup/drivers/file.rb
43
44
  - lib/crackup/drivers/ftp.rb
44
45
  - LICENSE
@@ -1,18 +0,0 @@
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