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