sundae 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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
+