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.



168
169
170
171
172
173
174
175
176
177
178
# File 'lib/sundae.rb', line 168

def self.all_mnts 
  @all_mnts ||= @paths.map do |path| 
    unless path.exist?
      ## TODO: This should probably only be displayed with some sort
      ## of verbose option.
      # warn "Path doesn't exist: #{path}"
      next
    end
    mnts_in_path(path)
  end.compact.flatten
end

.combine_directories(old1, old2, new) ⇒ Object

Create a directory and create links in it pointing to old1 and old2.



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

def self.combine_directories(old1, old2, new) 
  new = Pathname.new(new)
  old1 = Pathname.new(old1).expand_path
  old2 = Pathname.new(old2).expand_path

  raise "combine_directories in #{new}" unless new.symlink?
  return if old1 == old2
  
  new.delete
  new.mkpath
  minimally_create_links(old1, new)
  minimally_create_links(old2, new)
end

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

Raises:

  • (ArgumentError)


357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
# File 'lib/sundae.rb', line 357

def self.create_directory_link(old, new) 
  old = Pathname.new(old)
  new = Pathname.new(new)

  raise ArgumentError unless old.directory?
  if not new.exist? || new.symlink?
    new.make_symlink(old)
  else
    case new.ftype
    when 'file'
      raise "Could not link #{new} to #{old}: old exists."
    when 'directory'
      minimally_create_links(old, new)
    when 'link'
      case new.realpath.ftype
      when 'file'
        raise "Could not link #{new} to #{old}: another link exists there."
      when 'directory'
        combine_directories(old, new.readlink, new)   
      end
    end
  end
end

Create a symbolic link to old from new.

Raises:

  • (ArgumentError)


340
341
342
343
344
345
346
347
348
349
350
351
# File 'lib/sundae.rb', line 340

def self.create_file_link(old, new) 
  old = Pathname.new(old)
  new = Pathname.new(new)

  raise ArgumentError, "#{old} does not exist" unless old.file? || old.symlink?
  if new.symlink?
    raise ArgumentError, "#{new.to_s} cannot be overwritten for #{old}: points to #{new.readlink.to_s}" unless new.readlink.to_s == old.to_s
  else
    raise ArgumentError, "#{new} cannot be overwritten for #{old}." if new.exist?
    new.make_symlink(old)
  end
end

.create_filesystemObject

Call minimally_create_links for each mnt.



272
273
274
275
276
277
# File 'lib/sundae.rb', line 272

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.



323
324
325
326
327
328
329
330
331
332
333
334
335
336
# File 'lib/sundae.rb', line 323

def self.create_link(old, new) 
  old    = Pathname.new(old)
  new = Pathname.new(new)

  if old.directory?
    begin
      create_directory_link(old, new)
    rescue => message
      puts message
    end
  elsif old.file?
    create_file_link(old, new)
  end
end

.create_template_config_file(config_file) ⇒ Object

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



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
85
86
87
88
89
90
91
92
# File 'lib/sundae.rb', line 59

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.



410
411
412
413
414
415
416
417
418
419
420
# File 'lib/sundae.rb', line 410

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.



223
224
225
226
227
228
229
230
# File 'lib/sundae.rb', line 223

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.



200
201
202
# File 'lib/sundae.rb', line 200

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.



184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/sundae.rb', line 184

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)


97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/sundae.rb', line 97

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.



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/sundae.rb', line 115

def self.install_location(mnt) 
  mnt = Pathname.new(mnt).expand_path
  mnt_config = mnt + '.sundae_path'

  if mnt_config.exist?
    line = mnt_config.readlines[0]
    if line then
      return Pathname.new(line.strip).expand_path
    end
  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.



138
139
140
# File 'lib/sundae.rb', line 138

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.



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/sundae.rb', line 39

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 old, create a link at link_name. If there is currently no file at new, create a symbolic link there. If there is currently a symbolic link, combine the contents at the link location and old in a new directory and proceed recursively.



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/sundae.rb', line 285

def self.minimally_create_links(old, new) 
  old    = Pathname.new(old)
  new = Pathname.new(new)

  unless old.exist?
    raise "attempt to create links from missing directory: " + old
  end

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

    rel_path = path.relative_path_from(old.realpath)
    link_name = new + rel_path

#      puts "#{link_name} -> #{old + rel_path}"
    create_link(old + rel_path, link_name)

    Find.prune if path.directory?
  end
end

.mnts_in_path(path) ⇒ Object

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



145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/sundae.rb', line 145

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

  return collections.map do |c|
    unless c.exist?
      warn("Skipping mnt: #{c}")
      next
    end
    if c.children(false).include? Pathname.new('.sundae_path')
      c
    else
      collection_mnts = c.children.delete_if {|kid| kid.basename.to_s =~ /^\./}
      # collection_mnts.keep_if { |k| (path + c + k).directory? }
      collection_mnts.reject! { |k| ! (path + c + k).directory? }
      collection_mnts.map! { |mnt| (c + mnt) }
    end
  end.flatten.compact.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.



425
426
427
428
429
430
431
432
433
434
435
436
437
438
# File 'lib/sundae.rb', line 425

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)


442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
# File 'lib/sundae.rb', line 442

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.



207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/sundae.rb', line 207

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.exist?
      next unless root_path(file.readlink)
      file.delete 
    end
  end
end

.remove_filesystemObject



403
404
405
406
# File 'lib/sundae.rb', line 403

def self.remove_filesystem
  remove_dead_links
  remove_generated_files
end

.remove_generated_filesObject

# if sf = find_static_file(dir)

  #   puts "found static file: #{sf}"
  # else
  #   dir.rmtree
  # end
end

end



249
250
251
252
253
254
255
# File 'lib/sundae.rb', line 249

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



257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/sundae.rb', line 257

def self.remove_generated_stuff(fod)
  return unless fod.exist? || fod.symlink?
  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_.



310
311
312
313
314
315
316
317
318
319
# File 'lib/sundae.rb', line 310

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



398
399
400
401
# File 'lib/sundae.rb', line 398

def self.update_filesystem
  remove_filesystem
  create_filesystem
end