Module: Sundae

Defined in:
lib/sundae.rb

Overview

A collection of methods to mix the contents of several directories together using symbolic links.

Constant Summary collapse

LIBPATH =

:stopdoc:

::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
PATH =
::File.dirname(LIBPATH) + ::File::SEPARATOR
VERSION =

:startdoc:

::File.read(PATH + 'version.txt').strip
DEFAULT_CONFIG_FILE =
(Pathname.new(Dir.home) + '.sundae').expand_path

Class Method Summary collapse

Class Method Details

.all_mntsObject

Return all mnts for every path as an array.



150
151
152
153
154
155
156
157
158
# File 'lib/sundae.rb', line 150

def self.all_mnts 
  mnts = []

  @paths.each do |path| 
    next unless path.exist?
    mnts |= mnts_in_path(path).map { |mnt| path + mnt } # |= is the union operator
  end
  return mnts
end

.combine_directories(link_name, target_path1, target_path2) ⇒ Object

Create a directory and create links in it pointing to target_path1 and target_path2.



360
361
362
363
364
365
366
367
368
# File 'lib/sundae.rb', line 360

def self.combine_directories(link_name, target_path1, target_path2) 
  raise unless File.symlink?(link_name)
  return if target_path1 == target_path2
  
  FileUtils.rm(link_name)
  FileUtils.mkdir_p(link_name)
  minimally_create_links(target_path1, link_name)
  minimally_create_links(target_path2, link_name)
end

Create a symbolic link to the directory at target from link_name, unless link_name already exists. In that case, create a directory and recursively run minimally_create_links.

Raises:

  • (ArgumentError)


335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
# File 'lib/sundae.rb', line 335

def self.create_directory_link(target, link_name) 
  raise ArgumentError unless File.directory?(target)
  if (not File.exist?(link_name)) || 
      (File.symlink?(link_name) && (not File.exist?(File.readlink(link_name))))
    FileUtils.ln_sf(target, link_name)
  else
    case File.ftype(link_name)
    when 'file'
      raise "Could not link #{link_name} to #{target}: target exists."
    when 'directory'
      minimally_create_links(target, link_name)
    when 'link'
      case File.ftype(File.readlink(link_name))
      when 'file'
        raise "Could not link #{link_name} to #{target}: another link exists there."
      when 'directory'
        combine_directories(link_name, target, File.readlink(link_name))          
      end
    end
  end
end

Create a symbolic link to target from link_name.

Raises:

  • (ArgumentError)


315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/sundae.rb', line 315

def self.create_file_link(target, link_name) 
  raise ArgumentError, "#{target} does not exist" unless File.file?(target)
  if File.exist?(link_name)
    raise ArgumentError, "#{link_name} cannot be overwritten for #{target}." unless File.symlink?(link_name)
    if (not File.exist?(File.readlink(link_name)))
      FileUtils.ln_sf(target, link_name)
    else
      unless (File.expand_path(File.readlink(link_name)) == File.expand_path(target))
        raise ArgumentError, "#{link_name} points to #{File.readlink(link_name)}, not #{target}" unless File.symlink?(link_name)
      end
    end
  else
    FileUtils.ln_s(target, link_name)
  end
end

.create_filesystemObject

Call minimally_create_links for each mnt.



252
253
254
255
256
257
# File 'lib/sundae.rb', line 252

def self.create_filesystem
  all_mnts.each do |mnt|
    install_location(mnt).expand_path.mkpath
    minimally_create_links(mnt, install_location(mnt))
  end
end

Dispatch calls to create_directory_link and create_file_link.



301
302
303
304
305
306
307
308
309
310
311
# File 'lib/sundae.rb', line 301

def self.create_link(target, link_name) 
  if File.directory?(target) 
    begin
      create_directory_link(target, link_name)
    rescue => message
      puts message
    end
  elsif File.file?(target) 
    create_file_link(target, link_name)
  end
end

.create_template_config_file(config_file) ⇒ Object

Create a template configuration file at config_file after asking the user.



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/sundae.rb', line 51

def self.create_template_config_file(config_file)
  config_file = Pathname.new(config_file).expand_path
  loop do
    print "#{config_file} does not exist.  Create template there? (y/n): "
    ans = gets.downcase.strip
    if ans == "y" || ans == "yes"
      File.open(config_file, "w") do |f|
        f.puts <<-EOM.gsub(/^ {14}/, '')
            # -*-Ruby-*- 

            # An array which lists the directories where mnts are stored.
            configatron.paths = ["~/mnt"]

            # These are the rules that are checked to see if a file in a mnt
            # should be ignored.
            #
            # For `ignore_rules', use either strings (can be globs)
            # or Ruby regexps.  You can mix both in the same array.
            # Globs are matched using the method File.fnmatch.
            configatron.ignore_rules = %w(.git, 
                                          .bzr,
                                          .svn,
                                          .DS_Store)
            EOM
      end
      puts 
      puts "Okay then."
      puts "#{config_file} template created, but it needs to be customized."
      exit
    elsif ans == "n" || ans == "no"
      exit
    end
  end
end

.find_source_directories(path) ⇒ Object

Return an array of mnts that are installing to path.



383
384
385
386
387
388
389
390
391
392
393
# File 'lib/sundae.rb', line 383

def self.find_source_directories(path)
  sources = Array.new
  all_mnts.each do |mnt|
    install_location = File.expand_path(install_location(mnt))
    if path.include?(install_location)
      relative_path =  path.sub(Regexp.new(install_location), "")
      sources << mnt if File.exist?(File.join(mnt, relative_path))
    end
  end
  return sources
end

.find_static_file(directory) ⇒ Object

Search through directory and return the first static file found, nil otherwise.



203
204
205
206
207
208
209
210
# File 'lib/sundae.rb', line 203

def self.find_static_file(directory)
  directory = Pathname.new(directory).expand_path

  directory.find do |path|
    return path if path.exist? && path.ftype == 'file' 
  end
  return nil
end

.generated_directoriesObject

Return all subdirectories of the mnts returned by all_mnts. These are the ‘mirror’ directories that are generated by sundae.



180
181
182
# File 'lib/sundae.rb', line 180

def self.generated_directories 
  generated_files.select {|f| f.directory?} 
end

.generated_filesObject

Return all subdirectories and files in the mnts returned by all_mnts. These are the ‘mirror’ files and directories that are generated by sundae.



164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/sundae.rb', line 164

def self.generated_files
  dirs = Array.new

  all_mnts.each do |mnt|
    mnt_dirs = mnt.children(false).delete_if { |e| ignore_file?(e) }
    mnt_dirs.each do |dir|
      dirs << (install_location(mnt) + dir)
    end
  end

  return dirs.sort.uniq#.select { |d| d.directory? }
end

.ignore_file?(file) ⇒ Boolean

Use the array of Regexp to see if a certain file should be ignored (i.e., no link will be made pointing to it).

Returns:

  • (Boolean)


89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/sundae.rb', line 89

def self.ignore_file?(file) # :doc:
  file = Pathname.new(file)
  basename = file.basename.to_s
  return true if basename =~ /^\.\.?$/
  return true if basename == ".sundae_path"
  @ignore_rules.each do |r| 
    if r.kind_of? Regexp
      return true if basename =~ r 
    else
      return true if file.fnmatch(r)
    end
  end
  return false
end

.install_location(mnt) ⇒ Object

Read the .sundae_path file in the root of a mnt to see where in the file system links should be created for this mnt.



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/sundae.rb', line 107

def self.install_location(mnt) 
  mnt = Pathname.new(mnt).expand_path
  mnt_config = mnt + '.sundae_path'
  if mnt_config.exist?
    return Pathname.new(mnt_config.readlines[0].strip).expand_path
  end

  base = mnt.basename.to_s
  match = (/dot[-_](.*)/).match(base)
  if match
    return Pathname.new(Dir.home) + ('.' + match[1])
  end

  return Pathname.new(Dir.home)
end

.install_locationsObject

Return an array of all paths in the file system where links will be created.



126
127
128
# File 'lib/sundae.rb', line 126

def self.install_locations 
  all_mnts.map { |m| install_location(m) }.sort.uniq
end

.load_config_file(config_file = DEFAULT_CONFIG_FILE) ⇒ Object

Read configuration from .sundae.



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/sundae.rb', line 31

def self.load_config_file(config_file = DEFAULT_CONFIG_FILE)
  config_file ||= DEFAULT_CONFIG_FILE # if nil is passed
  config_file = Pathname.new(config_file).expand_path
  config_file += '.sundae' if config_file.directory?

  create_template_config_file(config_file) unless config_file.file?

  load(config_file)
  configatron.paths.map! { |p| Pathname.new(p).expand_path }

  # An array which lists the directories where mnts are stored.
  @paths = configatron.paths
  # These are the rules that are checked to see if a file in a mnt
  # should be ignored.
  @ignore_rules = configatron.ignore_rules
end

For each directory and file in target, create a link at link_name. If there is currently no file at link_path, create a symbolic link there. If there is currently a symbolic link, combine the contents at the link location and target in a new directory and proceed recursively.



265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/sundae.rb', line 265

def self.minimally_create_links(target, link_path) 
  target = File.expand_path(target)
  link_path = File.expand_path(link_path)

  unless File.exist?(target)
    raise "attempt to create links from missing directory: " + target
  end

  Find.find(target) do |path|
    next if path == target
    Find.prune if ignore_file?(File.basename(path))

    rel_path = path.gsub(target, '')
    link_name = File.join(link_path, rel_path)
    create_link(path, link_name)

    Find.prune if File.directory?(path) 
  end
end

.mnts_in_path(path) ⇒ Object

Given path, return all mnts (i.e., directories two levels down) as an array.



133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/sundae.rb', line 133

def self.mnts_in_path(path) 
  Pathname.new(path).expand_path
  mnts = []
  collections = path.children(false).delete_if {|c| c.to_s =~ /^\./}

  collections.each do |c|
    collection_mnts = (path + c).children(false).delete_if {|kid| kid.to_s =~ /^\./}
    collection_mnts.map! { |mnt| (c + mnt) }

    mnts |= collection_mnts # |= is the union
  end

  return mnts.sort.uniq
end

.move_to_mnt(path, mnt) ⇒ Object

Move the file at path (or its target in the case of a link) to mnt preserving relative path.



398
399
400
401
402
403
404
405
406
407
408
409
410
411
# File 'lib/sundae.rb', line 398

def self.move_to_mnt(path, mnt)
  if File.symlink?(path)
    to_move = File.readlink(path)
    current = Sundae.find_source_directories(path)[0]
    relative_path = to_move.sub(Regexp.new(current), "")
    FileUtils.mv(to_move, mnt + relative_path) unless current == mnt
    FileUtils.ln_sf(mnt + relative_path, path)
  else
    location = Sundae.install_location(mnt)
    relative_path = path.sub(Regexp.new(location), "")
    FileUtils.mv(path, mnt + relative_path) unless path == mnt + relative_path
    FileUtils.ln_s(mnt + relative_path, path)
  end
end

.move_to_relative_path(link, relative_path) ⇒ Object

Move the target at link according to relative_path.

Raises:

  • (ArgumentError)


415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
# File 'lib/sundae.rb', line 415

def self.move_to_relative_path(link, relative_path)
  raise ArgumentError, "#{link} is not a link." unless File.symlink?(link)

  target = File.readlink(link)

  pwd = FileUtils.pwd
  mnt = Sundae.find_source_directories(link)[0]
  mnt_pwd = File.join(mnt, pwd.sub(Regexp.new(install_location(mnt)), ""))

  if File.directory?(relative_path)
    new_target_path = File.join(mnt_pwd, relative_path, File.basename(link))
    new_link_path   = File.join(pwd,     relative_path, File.basename(link))
  else
    new_target_path = File.join(mnt_pwd, relative_path)
    new_link_path   = File.join(pwd,     relative_path)   
  end

  target          = File.expand_path(target)
  new_target_path = File.expand_path(new_target_path)
  new_link_path   = File.expand_path(new_link_path)

  raise ArgumentError, "#{link} and #{new_target_path} are the same file" if target == new_target_path
  FileUtils.mkdir_p(File.dirname(new_target_path))
  FileUtils.mv(target, new_target_path)
  FileUtils.rm(link)
  FileUtils.ln_s(new_target_path, new_link_path)
end

Check for symlinks in the base directories that are missing their targets.



187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/sundae.rb', line 187

def self.remove_dead_links
  install_locations.each do |location|
    next unless location.exist?
    files = location.entries.map { |f| location + f }
    files.each do |file|
      next unless file.symlink?
      next if file.readlink.exist?
      next unless root_path(file.readlink)
      file.delete 
    end
  end
end

.remove_filesystemObject



376
377
378
379
# File 'lib/sundae.rb', line 376

def self.remove_filesystem
  remove_dead_links
  remove_generated_files
end

.remove_generated_directoriesObject

Delete each generated directory if there aren’t any real files in them.



215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/sundae.rb', line 215

def self.remove_generated_directories
  generated_directories.each do |dir| 
    # don't get rid of the linked config file
    next if dir.basename.to_s == '.sundae' 
    remove_generated_stuff dir

    # if sf = find_static_file(dir)
    #   puts "found static file: #{sf}"
    # else
    #   dir.rmtree
    # end
  end
end

.remove_generated_filesObject



229
230
231
232
233
234
235
# File 'lib/sundae.rb', line 229

def self.remove_generated_files
  generated_files.each do |fod| 
    # don't get rid of the linked config file
    next if fod.basename.to_s == '.sundae' 
    remove_generated_stuff fod
  end
end

.remove_generated_stuff(fod) ⇒ Object



237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/sundae.rb', line 237

def self.remove_generated_stuff(fod)
  return unless fod.exist?
  if fod.ftype == 'directory'
    fod.each_child do |c|
      remove_generated_stuff c
    end
    fod.rmdir if fod.children.empty?
  else
    return unless fod.symlink?
    fod.delete if root_path(fod.readlink) # try to only delete sundae links
  end
end

.root_path(path) ⇒ Object

Starting at dir, walk up the directory hierarchy and return the directory that is contained in _@paths_.



288
289
290
291
292
293
294
295
296
297
# File 'lib/sundae.rb', line 288

def self.root_path(path)
  path = Pathname.new(path).expand_path
  last = path
  path.ascend do |v|
    return last if @paths.include? v
    last = v
  end

  return nil
end

.update_filesystemObject



370
371
372
373
374
# File 'lib/sundae.rb', line 370

def self.update_filesystem
  remove_dead_links
  remove_generated_files
  create_filesystem
end