sundae 0.9.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/Copying.txt ADDED
@@ -0,0 +1,20 @@
1
+ (The MIT License)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ 'Software'), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/History.txt ADDED
@@ -0,0 +1,6 @@
1
+ === 0.9.0 / 2008-10-18
2
+
3
+ * Initial release.
4
+
5
+
6
+
data/Manifest.txt ADDED
@@ -0,0 +1,9 @@
1
+ Copying.txt
2
+ History.txt
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ bin/sundae
7
+ setup.rb
8
+ lib/sundae.rb
9
+ test/test_sundae.rb
data/README.txt ADDED
@@ -0,0 +1,108 @@
1
+ = Sundae
2
+
3
+ == Synopsis
4
+
5
+ (Re)generates directories by mixing the file hierarchies contained
6
+ in various 'mounted' directories. The generated directories contain
7
+ symbolic links to the mounted files. Combined with other tools,
8
+ this scheme allows you to create separate collections of files
9
+ (work, personal, reference, linux, osx, etc.), choose which of these
10
+ you want to mount on each of your computers, and then build a
11
+ hierarchy that allows you to work on them side by side.
12
+
13
+ == Install
14
+
15
+ sudo gem install sundae
16
+
17
+ == Usage
18
+
19
+ The first time you run Sundae, it will create a template config file
20
+ in your home directory. This file, <tt>.sundae</tt>, needs to be
21
+ customized. It is in YAML format and defines the following:
22
+
23
+ [+paths+]
24
+ array; where the collections are stored
25
+ [+collection_links+]
26
+ +true+ or +false+; if true, links are created in generated
27
+ directories to the analogous location in mounted directories
28
+ [+collection_link_prefix+]
29
+ string; the prefix applied to collection links, if they are to be
30
+ created
31
+ [+ignore_rules+]
32
+ array; each line is a Regexp and becomes a rule that prevents
33
+ links to files or directories that match the Regexp
34
+
35
+ The hierarchy in <em>path</em> should look something like
36
+ this:
37
+
38
+ path/
39
+ |-- collection1/
40
+ | |-- mnt1/
41
+ | | |-- real_files_and_dirs
42
+ | | ` ...
43
+ | |-- mnt2/
44
+ `-- collection2/
45
+ ` ...
46
+
47
+ For example, the hierarchy in my <em>path</em> looks sort of like this:
48
+
49
+ ~/mnt/ <-- "path"
50
+ |-- osx/ <-- "collection"
51
+ | |-- home/ <-- "mnt"
52
+ | | |-- .emacs
53
+ | | |-- doc/
54
+ | | ` ...
55
+ | |-- home_library/
56
+ | | |-- .sundae_path
57
+ | | `-- Library-Keyboard_Layouts/
58
+ | | `-- Keyboard Layouts/
59
+ | | ` Colemak.keylayout
60
+ | ` ...
61
+ |-- personal
62
+ | `-- home/
63
+ | |-- doc/
64
+ | | ` ...
65
+ | ` ...
66
+ ` ...
67
+
68
+ Sundae will act on all of the <em>mnt</em>s--subdirectories of the
69
+ <em>collection</em>s, that is, the sub-subdirectories of the
70
+ <em>path</em>. The "collections" are only there to facilitate
71
+ grouping common files and syncronizing them between computers.
72
+
73
+ By default, all of the contents in each of the <em>mnt</em>s are
74
+ placed in the user's home directory. This can be altered by
75
+ creating a file called <tt>.sundae_path</tt> in the top of the
76
+ <em>mnt</em>; the file should contain one line, which is the
77
+ absolute path to where that directory should be "mounted."
78
+
79
+ And that's it. When called, Sundae creates links so that you can
80
+ work on your files from seperate parts of life as if they were side
81
+ by side.
82
+
83
+ == Author
84
+ <don@ohspite.net>
85
+
86
+ == Copyright
87
+ Copyright (c) 2008 <don@ohspite.net>.
88
+ Licensed under the MIT License.
89
+
90
+ Permission is hereby granted, free of charge, to any person obtaining
91
+ a copy of this software and associated documentation files (the
92
+ 'Software'), to deal in the Software without restriction, including
93
+ without limitation the rights to use, copy, modify, merge, publish,
94
+ distribute, sublicense, and/or sell copies of the Software, and to
95
+ permit persons to whom the Software is furnished to do so, subject to
96
+ the following conditions:
97
+
98
+ The above copyright notice and this permission notice shall be
99
+ included in all copies or substantial portions of the Software.
100
+
101
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
102
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
103
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
104
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
105
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
106
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
107
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
108
+
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require './lib/sundae.rb'
6
+
7
+ Hoe.new('sundae', Sundae::VERSION) do |p|
8
+ p.developer('Don', 'don@ohspite.net')
9
+ p.email = 'don@ohspite.net'
10
+ p.description = "Mix collections of files while maintaining complete separation. Synchronize any combination of your documents and configuration settings between all of your computers."
11
+ p.summary = "Mix collections of files while maintaining complete separation."
12
+ p.url = "http://rubyforge.org/projects.sundae"
13
+ # p.changes = p.paragraphs_of('CHANGELOG', 0..1).join("\n\n")
14
+ p.remote_rdoc_dir = ''
15
+ p.extra_deps = ['configatron']
16
+ end
17
+
18
+ # vim: syntax=Ruby
data/bin/sundae ADDED
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # == Synopsis
4
+ #
5
+ # (Re)generates directories by mixing the file hierarchies contained
6
+ # in various 'mounted' directories.
7
+ #
8
+ # == Usage
9
+ #
10
+ # sundae [--config-path PATH]
11
+ #
12
+ # For command line details see
13
+ # sundae --help
14
+ #
15
+ # == Author
16
+ # <don@ohspite.net>
17
+ #
18
+ # == Copyright
19
+ # Copyright (c) 2008 <don@ohspite.net>.
20
+ # Licensed under the MIT License.
21
+
22
+ require 'rdoc/usage'
23
+ require 'optparse'
24
+
25
+ $:.unshift File.join(File.dirname(__FILE__), "../lib")
26
+
27
+ require 'sundae'
28
+
29
+ class App # :nodoc:
30
+ def initialize
31
+ parse_commandline(ARGV)
32
+
33
+ Sundae.load_config_file(@options[:config_path])
34
+
35
+ Sundae.remove_dead_links
36
+ Sundae.remove_generated_directories
37
+ Sundae.create_filesystem
38
+ end
39
+
40
+ private
41
+
42
+ def parse_commandline(option_line)
43
+ options = {:verbose => false}
44
+ option_parser = OptionParser.new do |opts|
45
+ opts.banner = "Usage: #{File.basename(__FILE__)} [options] "
46
+ opts.separator ""
47
+ opts.separator "Specific options:"
48
+ opts.on('-c',
49
+ '--config-path PATH',
50
+ 'specify the path to the \'.sundae\' directory (default is \'~/.sundae\')') do |path|
51
+ options[:config_path] = File.expand_path(path)
52
+ end
53
+ # opts.on('-v',
54
+ # '--verbose',
55
+ # 'verbose output') do
56
+ # options[:verbose] = true
57
+ # end
58
+ opts.separator ""
59
+ opts.separator "Common options:"
60
+ opts.on('-h',
61
+ '--help',
62
+ 'show the help message') do
63
+ puts opts
64
+ exit
65
+ end
66
+ opts.on('-a',
67
+ '--about',
68
+ 'show the about message') do
69
+ RDoc::usage
70
+ end
71
+ end
72
+
73
+ argv = Array.new
74
+ begin
75
+ option_parser.order!(option_line) do |command|
76
+ case command
77
+ when "some_value"
78
+
79
+ else
80
+ argv << command
81
+ end
82
+ end
83
+ rescue
84
+ RDoc::usage('usage')
85
+ end
86
+ argv.each { |a| option_line << a }
87
+
88
+ @options = options
89
+ end
90
+ end
91
+
92
+
93
+ App.new
data/lib/sundae.rb ADDED
@@ -0,0 +1,326 @@
1
+ require 'rubygems'
2
+ require 'configatron'
3
+ require 'fileutils'
4
+ require 'find'
5
+
6
+ # A collection of methods to mix the contents of several directories
7
+ # together using symbolic links.
8
+ #
9
+ module Sundae
10
+ VERSION = "0.9.0"
11
+
12
+ DEFAULT_CONFIG_FILE = File.expand_path(File.join(ENV['HOME'], '.sundae'))
13
+
14
+ @config_file = DEFAULT_CONFIG_FILE
15
+
16
+ # Read configuration from <tt>.sundae</tt>.
17
+ #
18
+ def self.load_config_file(config_file = DEFAULT_CONFIG_FILE)
19
+ config_file ||= DEFAULT_CONFIG_FILE
20
+ config_file = File.join(config_file, '.sundae') unless File.basename(config_file) == '.sundae'
21
+
22
+ create_template_config_file(config_file) unless File.file?(config_file)
23
+
24
+ configatron.set_default(:collection_links, false)
25
+ configatron.set_default(:collection_link_prefix, '_')
26
+
27
+ configatron.configure_from_yaml(config_file)
28
+ configatron.paths.map! { |p| File.expand_path(p) }
29
+ configatron.ignore_rules.map! { |a| Regexp.new(a) }
30
+
31
+ # An array which lists the directories where mnts are stored.
32
+ @paths = configatron.paths
33
+ # These are the rules that are checked to see if a file in a mnt
34
+ # should be ignored.
35
+ @ignore_rules = configatron.ignore_rules
36
+
37
+ @collection_links = configatron.collection_links
38
+ @collection_link_prefix = configatron.collection_link_prefix
39
+ end
40
+
41
+ # Create a template configuration file at <em>config_file</em> after
42
+ # asking the user.
43
+ #
44
+ def self.create_template_config_file(config_file)
45
+ loop do
46
+ print "#{config_file} does not exist. Create template there? (y/n): "
47
+ ans = gets.downcase.strip
48
+ if ans == "y" || ans == "yes"
49
+ File.open(config_file, "w") do |f|
50
+ f.puts ":paths:"
51
+ f.puts "- ~/mnt"
52
+ f.puts ":collection_links:"
53
+ f.puts " false"
54
+ f.puts ":collection_link_prefix:"
55
+ f.puts " '_'"
56
+ f.puts ":ignore_rules: # Ruby Regexps"
57
+ f.puts "- \\.svn"
58
+ f.puts "- \\.bzr"
59
+ f.puts "- \\.DS_Store"
60
+ end
61
+ puts
62
+ puts "Okay then."
63
+ puts "#{config_file} template created, but it needs to be customized."
64
+ exit
65
+ elsif ans == "n" || ans == "no"
66
+ exit
67
+ end
68
+ end
69
+ end
70
+
71
+ # Use the array of Regexp to see if a certain file should be
72
+ # ignored (i.e., no link will be made pointing to it).
73
+ #
74
+ def self.ignore_file?(file) # :doc:
75
+ return true if File.basename(file) =~ /^\.\.?$/
76
+ return true if File.basename(file) == ".sundae_path"
77
+ @ignore_rules.each { |r| return true if File.basename(file) =~ r }
78
+ return false
79
+ end
80
+
81
+ # Read the <tt>.sundae_path</tt> file in the root of a mnt to see
82
+ # where in the file system links should be created for this mnt.
83
+ #
84
+ def self.install_location(mnt)
85
+ mnt_config = File.join(mnt, '.sundae_path')
86
+ if File.exist?(mnt_config)
87
+ location = File.readlines(mnt_config)[0].strip
88
+ end
89
+
90
+ location ||= ENV['HOME']
91
+ end
92
+
93
+ # Return an array of all paths in the file system where links will
94
+ # be created.
95
+ #
96
+ def self.install_locations
97
+ locations = []
98
+
99
+ all_mnts.each do |mnt|
100
+ locations << install_location(mnt)
101
+ end
102
+ return locations.sort.uniq
103
+ end
104
+
105
+ # Given _path_, return all mnts (i.e., directories two levels down)
106
+ # as an array.
107
+ #
108
+ def self.mnts_in_path(path)
109
+ mnts = []
110
+ collections = Dir.entries(path).delete_if {|a| a=~/^\./}
111
+ collections.each do |c|
112
+ collection_mnts = Dir.entries(File.join(path, c)).delete_if {|a| a=~/^\./}
113
+ collection_mnts.map! { |mnt| File.join(c, mnt) }
114
+ mnts |= collection_mnts
115
+ end
116
+
117
+ return mnts.sort.uniq
118
+ end
119
+
120
+ # Return all mnts for every path as an array.
121
+ #
122
+ def self.all_mnts
123
+ mnts = []
124
+
125
+ @paths.each do |path|
126
+ next unless File.exist?(path)
127
+ mnts |= mnts_in_path(path).map { |mnt| File.join(path, mnt) }
128
+ end
129
+
130
+ return mnts
131
+ end
132
+
133
+ # Return all subdirectories of the mnts returned by all_mnts. These
134
+ # are the 'mirror' directories that are generated by sundae.
135
+ #
136
+ def self.generated_directories
137
+ dirs = Array.new
138
+
139
+ all_mnts.each do |mnt|
140
+ mnt_dirs = Dir.entries(mnt).delete_if { |e| ignore_file?(e) }
141
+ mnt_dirs.each do |dir|
142
+ dirs << File.join(install_location(mnt), dir)
143
+ end
144
+ end
145
+
146
+ return dirs.sort.uniq
147
+ end
148
+
149
+ # Check for symlinks in the base directories that are missing their
150
+ # targets.
151
+ #
152
+ def self.remove_dead_links
153
+ removed_list = []
154
+ install_locations.each do |location|
155
+ next unless File.exist?(location)
156
+ files = Dir.entries(location).map {|f| File.join(location, f)}
157
+ files.each do |file|
158
+ next unless File.symlink?(file)
159
+ next if File.exist? File.readlink(file)
160
+ FileUtils.rm(file)
161
+ removed_list << file
162
+ end
163
+ end
164
+ return removed_list
165
+ end
166
+
167
+ # Delete each generated directory if there aren't any real files in
168
+ # them.
169
+ #
170
+ def self.remove_generated_directories
171
+ removed_list = []
172
+ generated_directories.each do |dir|
173
+ next if File.basename(dir) == ('.sundae')
174
+
175
+ # Do a quick search to make sure no non-symlink file is being
176
+ # deleted. That would suck.
177
+ if sf = find_static_file(dir)
178
+ puts "found static file: #{sf}"
179
+ else
180
+ FileUtils.rmtree(dir)
181
+ removed_list << dir
182
+ end
183
+ end
184
+ return removed_list
185
+ end
186
+
187
+ # Search through _directory_ and return the first static file found,
188
+ # nil otherwise.
189
+ #
190
+ def self.find_static_file(directory)
191
+ Find.find(directory) do |path|
192
+ return path if File.exist?(path) && File.ftype(path) == 'file'
193
+ end
194
+ return nil
195
+ end
196
+
197
+ # Call minimally_create_links for each mnt.
198
+ #
199
+ def self.create_filesystem
200
+ mnt_list = []
201
+ all_mnts.each do |mnt|
202
+ minimally_create_links(mnt, install_location(mnt))
203
+ mnt_list << mnt
204
+ end
205
+ return mnt_list
206
+ end
207
+
208
+ # For each directory and file in _target_, create a link at <em>link_name</em>. If
209
+ # there is currently no file at <em>link_path</em>, create a symbolic link there.
210
+ # If there is currently a symbolic link, combine the contents at the
211
+ # link location and _target_ in a new directory and proceed
212
+ # recursively.
213
+ #
214
+ def self.minimally_create_links(target, link_path)
215
+ target = File.expand_path(target)
216
+ link_path = File.expand_path(link_path)
217
+
218
+ unless File.exist?(target)
219
+ raise "attempt to create links from missing directory: " + target
220
+ end
221
+
222
+ Find.find(target) do |path|
223
+ next if path == target
224
+ Find.prune if ignore_file?(File.basename(path))
225
+
226
+ rel_path = path.gsub(target, '')
227
+ link_name = File.join(link_path, rel_path)
228
+ create_link(path, link_name)
229
+
230
+ Find.prune if File.directory?(path)
231
+ end
232
+ create_collection_links(target, link_path)
233
+ end
234
+
235
+ # Create links in a generated mirror directory to the analogous
236
+ # location in the mounted directories.
237
+ #
238
+ def self.create_collection_links(target, link_name)
239
+ return unless @collection_links
240
+
241
+ collection_name = File.basename(root_path(target))
242
+ collection_link = File.join(link_name, @collection_link_prefix + collection_name)
243
+ create_link(target, collection_link) unless File.exist? collection_link
244
+ end
245
+
246
+ # Starting at _dir_, walk up the directory hierarchy and return the
247
+ # directory that is contained in _@paths_.
248
+ #
249
+ def self.root_path(dir)
250
+ raise ArgumentError if dir == '/'
251
+
252
+ parent = File.expand_path(File.join(dir, '..'))
253
+ if @paths.include? parent
254
+ return dir
255
+ else
256
+ root_path parent
257
+ end
258
+ end
259
+
260
+ # Dispatch calls to create_directory_link and create_file_link.
261
+ #
262
+ def self.create_link(target, link_name)
263
+ if File.directory?(target)
264
+ begin
265
+ create_directory_link(target, link_name)
266
+ rescue => message
267
+ puts message
268
+ end
269
+ elsif File.file?(target)
270
+ create_file_link(target, link_name)
271
+ end
272
+ end
273
+
274
+ # Create a symbolic link to <em>target</em> from <em>link_name</em>.
275
+ #
276
+ def self.create_file_link(target, link_name)
277
+ raise ArgumentError unless File.file?(target)
278
+ unless File.exist?(link_name)
279
+ FileUtils.ln_s(target, link_name)
280
+ else
281
+ unless (File.symlink?(link_name) &&
282
+ (File.readlink(link_name) == target || (not File.exist?(File.readlink(link_name)))))
283
+ raise "Could not link #{target} to #{link_name}"
284
+ end
285
+ end
286
+ end
287
+
288
+ # Create a symbolic link to the directory at <em>target</em> from
289
+ # <em>link_name</em>, unless <em>link_name</em> already exists. In that case,
290
+ # create a directory and recursively run minimally_create_links.
291
+ #
292
+ def self.create_directory_link(target, link_name)
293
+ raise ArgumentError unless File.directory?(target)
294
+ unless File.exist?(link_name)
295
+ FileUtils.ln_s(target, link_name)
296
+ else
297
+ case File.ftype(link_name)
298
+ when 'file'
299
+ raise "Could not link #{target} to #{link_name}"
300
+ when 'directory'
301
+ minimally_create_links(target, link_name)
302
+ when 'link'
303
+ case File.ftype(File.readlink(link_name))
304
+ when 'file'
305
+ raise "Could not link #{target} to #{link_name}"
306
+ when 'directory'
307
+ combine_directories(link_name, target, File.readlink(link_name))
308
+ end
309
+ end
310
+ end
311
+ end
312
+
313
+ # Create a directory and create links in it pointing to
314
+ # <em>target_path1</em> and <em>target_path2</em>.
315
+ #
316
+ def self.combine_directories(link_name, target_path1, target_path2)
317
+ return if target_path1 == target_path2
318
+
319
+ FileUtils.rm(link_name)
320
+ FileUtils.mkdir_p(link_name)
321
+ minimally_create_links(target_path1, link_name)
322
+ minimally_create_links(target_path2, link_name)
323
+ end
324
+
325
+ end
326
+