crackup 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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