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 +20 -0
- data/History.txt +6 -0
- data/Manifest.txt +9 -0
- data/README.txt +108 -0
- data/Rakefile +18 -0
- data/bin/sundae +93 -0
- data/lib/sundae.rb +326 -0
- data/setup.rb +1599 -0
- data/test/test_sundae.rb +229 -0
- metadata +84 -0
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
data/Manifest.txt
ADDED
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
|
+
|