dle 0.1.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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +71 -0
- data/Rakefile +1 -0
- data/VERSION +1 -0
- data/bin/dle +3 -0
- data/bin/dle.sh +14 -0
- data/dle.gemspec +24 -0
- data/lib/active_support/core_ext/object/blank.rb +112 -0
- data/lib/active_support/core_ext/object/try.rb +53 -0
- data/lib/banana/logger.rb +258 -0
- data/lib/dle.rb +24 -0
- data/lib/dle/application.rb +96 -0
- data/lib/dle/application/dispatch.rb +151 -0
- data/lib/dle/dl_file.rb +111 -0
- data/lib/dle/filesystem.rb +109 -0
- data/lib/dle/filesystem/destructive.rb +115 -0
- data/lib/dle/filesystem/node.rb +44 -0
- data/lib/dle/filesystem/softnode.rb +6 -0
- data/lib/dle/helpers.rb +43 -0
- data/lib/dle/version.rb +4 -0
- metadata +112 -0
data/lib/dle.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require "pathname"
|
2
|
+
require "yaml"
|
3
|
+
require "find"
|
4
|
+
require "optparse"
|
5
|
+
require "securerandom"
|
6
|
+
|
7
|
+
require "pry"
|
8
|
+
require "active_support/core_ext/object/blank"
|
9
|
+
require "active_support/core_ext/object/try"
|
10
|
+
|
11
|
+
require "banana/logger"
|
12
|
+
require "dle/version"
|
13
|
+
require "dle/helpers"
|
14
|
+
require "dle/dl_file"
|
15
|
+
require "dle/filesystem/destructive"
|
16
|
+
require "dle/filesystem/softnode"
|
17
|
+
require "dle/filesystem/node"
|
18
|
+
require "dle/filesystem"
|
19
|
+
require "dle/application/dispatch"
|
20
|
+
require "dle/application"
|
21
|
+
|
22
|
+
module Dle
|
23
|
+
ROOT = Pathname.new(File.expand_path("../..", __FILE__))
|
24
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module Dle
|
2
|
+
# Logger Singleton
|
3
|
+
MAIN_THREAD = ::Thread.main
|
4
|
+
def MAIN_THREAD.app_logger
|
5
|
+
MAIN_THREAD[:app_logger] ||= Banana::Logger.new
|
6
|
+
end
|
7
|
+
|
8
|
+
class Application
|
9
|
+
attr_reader :opts
|
10
|
+
include Dispatch
|
11
|
+
|
12
|
+
# =========
|
13
|
+
# = Setup =
|
14
|
+
# =========
|
15
|
+
def self.dispatch *a
|
16
|
+
new(*a) do |app|
|
17
|
+
app.parse_params
|
18
|
+
app.log "core ready"
|
19
|
+
app.dispatch
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize env, argv
|
24
|
+
@env, @argv = env, argv
|
25
|
+
@editor = which_editor
|
26
|
+
@opts = {
|
27
|
+
dispatch: :index,
|
28
|
+
dotfiles: false,
|
29
|
+
check_for_updates: true,
|
30
|
+
review: true,
|
31
|
+
simulate: false,
|
32
|
+
}
|
33
|
+
yield(self)
|
34
|
+
end
|
35
|
+
|
36
|
+
def parse_params
|
37
|
+
@optparse = OptionParser.new do |opts|
|
38
|
+
opts.banner = "Usage: dle [options] base_directory"
|
39
|
+
|
40
|
+
opts.on("-d", "--dotfiles", "Include dotfiles (unix invisible)") { @opts[:dotfiles] = true }
|
41
|
+
opts.on("-r", "--skip-review", "Skip review changes before applying") { @opts[:review] = false }
|
42
|
+
opts.on("-s", "--simulate", "Don't apply changes, show commands instead") { @opts[:simulate] = true ; @opts[:review] = false }
|
43
|
+
opts.on("-f", "--file DLFILE", "Use input file (be careful)") {|f| @opts[:input_file] = f }
|
44
|
+
opts.on("-m", "--monochrome", "Don't colorize output") { logger.colorize = false }
|
45
|
+
opts.on("-h", "--help", "Shows this help") { @opts[:dispatch] = :help }
|
46
|
+
opts.on("-v", "--version", "Shows version and other info") { @opts[:dispatch] = :info }
|
47
|
+
opts.on("-z", "Do not check for updates on GitHub (with -v/--version)") { @opts[:check_for_updates] = false }
|
48
|
+
end
|
49
|
+
|
50
|
+
begin
|
51
|
+
@optparse.parse!(@argv)
|
52
|
+
rescue OptionParser::ParseError => e
|
53
|
+
abort(e.message)
|
54
|
+
dispatch(:help)
|
55
|
+
exit 1
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def which_editor
|
60
|
+
ENV["DLE_EDITOR"].presence ||
|
61
|
+
ENV["EDITOR"].presence ||
|
62
|
+
`which nano`.presence.try(:strip) ||
|
63
|
+
`which vim`.presence.try(:strip) ||
|
64
|
+
`which vi`.presence.try(:strip)
|
65
|
+
end
|
66
|
+
|
67
|
+
def open_editor file
|
68
|
+
system "#{@editor} #{file}"
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
# ==========
|
73
|
+
# = Logger =
|
74
|
+
# ==========
|
75
|
+
[:log, :warn, :abort, :debug].each do |meth|
|
76
|
+
define_method meth, ->(*a, &b) { Thread.main.app_logger.send(meth, *a, &b) }
|
77
|
+
end
|
78
|
+
|
79
|
+
def logger
|
80
|
+
Thread.main.app_logger
|
81
|
+
end
|
82
|
+
|
83
|
+
# Shortcut for logger.colorize
|
84
|
+
def c str, color = :yellow
|
85
|
+
logger.colorize? ? logger.colorize(str, color) : str
|
86
|
+
end
|
87
|
+
|
88
|
+
def ask question
|
89
|
+
logger.log_with_print(false) do
|
90
|
+
log c("#{question} ", :blue)
|
91
|
+
STDOUT.flush
|
92
|
+
STDIN.gets.chomp
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
module Dle
|
2
|
+
class Application
|
3
|
+
module Dispatch
|
4
|
+
def dispatch action = (@opts[:dispatch] || :help)
|
5
|
+
case action
|
6
|
+
when :version, :info then dispatch_info
|
7
|
+
else
|
8
|
+
if respond_to?("dispatch_#{action}")
|
9
|
+
send("dispatch_#{action}")
|
10
|
+
else
|
11
|
+
abort("unknown action #{action}", 1)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def dispatch_help
|
17
|
+
@optparse.to_s.split("\n").each(&method(:log))
|
18
|
+
log ""
|
19
|
+
log "To set your favorite editor set the env variable #{c "DLE_EDITOR", :magenta}"
|
20
|
+
log "Note that you need a blocking call (e.g. subl -w, mate -w)"
|
21
|
+
log "Your current editor is: #{c @editor || "none", :magenta}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def dispatch_info
|
25
|
+
logger.log_without_timestr do
|
26
|
+
log ""
|
27
|
+
log " Your version: #{your_version = Gem::Version.new(Dle::VERSION)}"
|
28
|
+
|
29
|
+
# get current version
|
30
|
+
logger.log_with_print do
|
31
|
+
log " Current version: "
|
32
|
+
if @opts[:check_for_updates]
|
33
|
+
require "net/http"
|
34
|
+
log c("checking...", :blue)
|
35
|
+
|
36
|
+
begin
|
37
|
+
current_version = Gem::Version.new Net::HTTP.get_response(URI.parse(Dle::UPDATE_URL)).body.strip
|
38
|
+
|
39
|
+
if current_version > your_version
|
40
|
+
status = c("#{current_version} (consider update)", :red)
|
41
|
+
elsif current_version < your_version
|
42
|
+
status = c("#{current_version} (ahead, beta)", :green)
|
43
|
+
else
|
44
|
+
status = c("#{current_version} (up2date)", :green)
|
45
|
+
end
|
46
|
+
rescue
|
47
|
+
status = c("failed (#{$!.message})", :red)
|
48
|
+
end
|
49
|
+
|
50
|
+
logger.raw "#{"\b" * 11}#{" " * 11}#{"\b" * 11}", :print # reset cursor
|
51
|
+
log status
|
52
|
+
else
|
53
|
+
log c("check disabled", :red)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
log " Selected editor: #{c @editor || "none", :magenta}"
|
57
|
+
|
58
|
+
# more info
|
59
|
+
log ""
|
60
|
+
log " DLE DirectoryListEdit is brought to you by #{c "bmonkeys.net", :green}"
|
61
|
+
log " Contribute @ #{c "github.com/2called-chaos/dle", :cyan}"
|
62
|
+
log " Eat bananas every day!"
|
63
|
+
log ""
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def dispatch_index
|
68
|
+
# require base directory
|
69
|
+
base_dir = ARGV[0].present? ? File.expand_path(ARGV[0].to_s) : ARGV[0].to_s
|
70
|
+
if !FileTest.directory?(base_dir)
|
71
|
+
if base_dir.present?
|
72
|
+
abort c(ARGV[0].to_s, :magenta) << c(" is not a valid directory!", :red)
|
73
|
+
else
|
74
|
+
dispatch(:help)
|
75
|
+
abort "Please provide a base directory.", 1
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# index filesystem
|
80
|
+
log("index #{c base_dir, :magenta}")
|
81
|
+
@fs = Filesystem.new(base_dir, dotfiles: @opts[:dotfiles])
|
82
|
+
abort("Base directory is empty or not readable", 1) if @fs.index.empty?
|
83
|
+
|
84
|
+
file = "#{Dir.tmpdir}/#{SecureRandom.urlsafe_base64}"
|
85
|
+
begin
|
86
|
+
# read input file or open editor
|
87
|
+
if @opts[:input_file]
|
88
|
+
ifile = File.expand_path(@opts[:input_file])
|
89
|
+
if FileTest.file?(ifile) && FileTest.readable?(ifile)
|
90
|
+
@dlfile = DlFile.parse(ifile)
|
91
|
+
else
|
92
|
+
abort "Input file not readable: " << c(ifile, :magenta)
|
93
|
+
end
|
94
|
+
else
|
95
|
+
FileUtils.mkdir_p(File.dirname(file)) if !FileTest.exist?(File.dirname(file))
|
96
|
+
if !FileTest.exist?(file) || File.read(file).strip.empty?
|
97
|
+
File.open(file, "w") {|f| f.write @fs.to_dlfile }
|
98
|
+
end
|
99
|
+
log "open list for editing..."
|
100
|
+
open_editor(file)
|
101
|
+
@dlfile = DlFile.parse(file)
|
102
|
+
end
|
103
|
+
|
104
|
+
# delta changes
|
105
|
+
@delta = @fs.delta(@dlfile)
|
106
|
+
|
107
|
+
# no changes
|
108
|
+
if @delta.all?{|_, v| v.empty? }
|
109
|
+
abort c("No changes, nothing to do..."), 0
|
110
|
+
end
|
111
|
+
|
112
|
+
# review
|
113
|
+
if @opts[:review]
|
114
|
+
@delta.each do |action, snodes|
|
115
|
+
logger.ensure_prefix c("[#{action}]\t", :magenta) do
|
116
|
+
snodes.each do |snode|
|
117
|
+
if [:chown, :chmod].include?(action)
|
118
|
+
log(c("#{snode.node.relative_path} ", :blue) << c(snode.is, :red) << c(" » ") << c(snode.should, :green))
|
119
|
+
elsif [:cp, :mv].include?(action)
|
120
|
+
log(c(snode.is, :red) << c(" » ") << c(snode.should, :green))
|
121
|
+
else
|
122
|
+
log(c(snode.is, :red) << " (#{snode.snode.mode})")
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
answer = ask("Do you want to apply these changes? [yes/no/edit]")
|
129
|
+
while !["y", "yes", "n", "no", "e", "edit"].include?(answer.downcase)
|
130
|
+
answer = ask("Please be explicit, yes/no/edit:")
|
131
|
+
end
|
132
|
+
raise "retry" if ["e", "edit"].include?(answer.downcase)
|
133
|
+
abort("Aborted, nothing changed", 0) if !["y", "yes"].include?(answer.downcase)
|
134
|
+
end
|
135
|
+
rescue
|
136
|
+
$!.message == "retry" ? retry : raise
|
137
|
+
end
|
138
|
+
|
139
|
+
# apply changes
|
140
|
+
log "#{@opts[:simulate] ? "Simulating" : "Applying"} changes..."
|
141
|
+
@delta.each do |action, snodes|
|
142
|
+
logger.ensure_prefix c("[apply-#{action}]\t", :magenta) do
|
143
|
+
snodes.each do |snode|
|
144
|
+
Filesystem::Destructive.new(self, action, @fs, snode).perform
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
data/lib/dle/dl_file.rb
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
module Dle
|
2
|
+
class DlFile
|
3
|
+
def self.generate fs
|
4
|
+
Generator.new(fs).render
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.parse file
|
8
|
+
Parser.new(file).parse
|
9
|
+
end
|
10
|
+
|
11
|
+
class Parser
|
12
|
+
def initialize file
|
13
|
+
@file = file
|
14
|
+
end
|
15
|
+
|
16
|
+
def parse
|
17
|
+
{}.tap do |fs|
|
18
|
+
return fs unless File.readable?(@file)
|
19
|
+
File.readlines(@file).each do |l|
|
20
|
+
next if l.start_with?("#")
|
21
|
+
if l.strip.start_with?("<HD-BASE>")
|
22
|
+
fs[:HD_BASE] = l.strip.gsub("<HD-BASE>", "").gsub("</HD-BASE>", "")
|
23
|
+
elsif l.strip.start_with?()
|
24
|
+
fs[:HD_DOTFILES] = l.strip.gsub("<HD-DOTFILES>", "").gsub("</HD-DOTFILES>", "") == "true"
|
25
|
+
elsif l.count("|") >= 4
|
26
|
+
# parse line
|
27
|
+
chunks = l.split("|")
|
28
|
+
data = {}.tap do |r|
|
29
|
+
r[:inode] = chunks.shift.strip
|
30
|
+
r[:mode] = chunks.shift.strip
|
31
|
+
r[:uid], r[:gid] = chunks.shift.split(":").map(&:strip)
|
32
|
+
chunks.shift # ignore size
|
33
|
+
r[:relative_path] = chunks.join("|").strip
|
34
|
+
r[:path] = [fs[:HD_BASE], r[:relative_path]].join("/")
|
35
|
+
end
|
36
|
+
|
37
|
+
# skip headers
|
38
|
+
next if data[:inode].downcase == "in"
|
39
|
+
next if data[:mode].downcase == "mode"
|
40
|
+
next if data[:uid].downcase == "owner"
|
41
|
+
next if data[:relative_path].downcase == "file"
|
42
|
+
|
43
|
+
# map node
|
44
|
+
if fs.key? data[:inode]
|
45
|
+
Thread.main.app_logger.warn "inode #{data[:inode]} already mapped, ignore..."
|
46
|
+
else
|
47
|
+
fs[data[:inode]] = Filesystem::Softnode.new(data)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
Thread.main.app_logger.warn("DLFILE has no HD-BASE, deltaFS will fail!") unless fs[:HD_BASE].present?
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class Generator
|
57
|
+
include Helpers
|
58
|
+
|
59
|
+
def initialize fs
|
60
|
+
@fs = fs
|
61
|
+
end
|
62
|
+
|
63
|
+
def render
|
64
|
+
# inode, mode, own/grp, size, file
|
65
|
+
table = [[], [], [], [], []]
|
66
|
+
@fs.index.each do |rpath, node|
|
67
|
+
table[0] << node.inode
|
68
|
+
table[1] << node.mode
|
69
|
+
table[2] << node.owngrp
|
70
|
+
table[3] << human_filesize(node.size)
|
71
|
+
table[4] << node.relative_path
|
72
|
+
end
|
73
|
+
|
74
|
+
([
|
75
|
+
%{#},
|
76
|
+
%{# - If you remove a line we just don't care!},
|
77
|
+
%{# - If you add a line we just don't care!},
|
78
|
+
%{# - If you change a path we will "mkdir -p" the destination and move the file/dir},
|
79
|
+
%{# - If you change the owner we will "chown" the file/dir},
|
80
|
+
%{# - If you change the mode we will "chmod" the file/dir},
|
81
|
+
%{# - If you change the mode to "cp" and modify the path we will copy instead of moving/renaming},
|
82
|
+
%{# - If you change the mode to "del" we will "rm" the file},
|
83
|
+
%{# - If you change the mode to "delr" we will "rm" the file or directory},
|
84
|
+
%{# - If you change the mode to "delf" or "delrf" we will "rm -f" the file or directory},
|
85
|
+
%{# - We will apply changes in this order (inside-out):},
|
86
|
+
%{# - Ownership},
|
87
|
+
%{# - Permissions},
|
88
|
+
%{# - Rename/Move},
|
89
|
+
%{# - Copy},
|
90
|
+
%{# - Delete},
|
91
|
+
%{#},
|
92
|
+
%{# Gotchas:},
|
93
|
+
%{# - If you have "folder/subfolder/file" and want to rename "subfolder" to "dubfolder"},
|
94
|
+
%{# do it only in the specific node, don't change the path of "file"!},
|
95
|
+
%{# - If you want to copy a directory, only copy the directory node, not a file inside it.},
|
96
|
+
%{# Folders will be copied recursively.},
|
97
|
+
%{# - The script works with file IDs (IN column). That allows you to remove files in renamed},
|
98
|
+
%{# folders without adjusting paths. This is not a gotcha, it's a hint :)},
|
99
|
+
%{# - Note that indexing is quite fast but applying changes on a base directory with a lot},
|
100
|
+
%{# of files and directories may be slow since we fully reindex after each operation.},
|
101
|
+
%{# Maybe the mapping will keep track of changes in later updates so that this isn't necessary},
|
102
|
+
%{# --------------------------------------------------},
|
103
|
+
%{},
|
104
|
+
%{<HD-BASE>#{@fs.base_dir}</HD-BASE>},
|
105
|
+
%{<HD-DOTFILES>#{@fs.opts[:dotfiles]}</HD-DOTFILES>},
|
106
|
+
%{},
|
107
|
+
] + render_table(table, ["IN", "Mode", "Owner", "Size", "File"])).map(&:strip).join("\n")
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module Dle
|
2
|
+
class Filesystem
|
3
|
+
attr_reader :base_dir, :index, :opts
|
4
|
+
|
5
|
+
# ==========
|
6
|
+
# = Logger =
|
7
|
+
# ==========
|
8
|
+
[:log, :warn, :abort, :debug].each do |meth|
|
9
|
+
define_method meth, ->(*a, &b) { Thread.main.app_logger.send(meth, *a, &b) }
|
10
|
+
end
|
11
|
+
|
12
|
+
def logger
|
13
|
+
Thread.main.app_logger
|
14
|
+
end
|
15
|
+
|
16
|
+
# Shortcut for logger.colorize
|
17
|
+
def c str, color = :yellow
|
18
|
+
logger.colorize? ? logger.colorize(str, color) : str
|
19
|
+
end
|
20
|
+
|
21
|
+
# ---------
|
22
|
+
|
23
|
+
def initialize base_dir, opts = {}
|
24
|
+
raise ArgumentError, "#{base_dir} is not a directory" unless FileTest.directory?(base_dir)
|
25
|
+
@base_dir = File.expand_path(base_dir).freeze
|
26
|
+
@opts = { dotfiles: true }.merge(opts)
|
27
|
+
reindex!
|
28
|
+
end
|
29
|
+
|
30
|
+
def reindex!
|
31
|
+
@index = {}
|
32
|
+
index!
|
33
|
+
end
|
34
|
+
|
35
|
+
def index!
|
36
|
+
Find.find(@base_dir) do |path|
|
37
|
+
if File.basename(path)[0] == ?. && !@opts[:dotfiles]
|
38
|
+
Find.prune
|
39
|
+
else
|
40
|
+
index_node(path)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def relative_path path
|
46
|
+
if path.start_with?(@base_dir)
|
47
|
+
p = path[(@base_dir.length+1)..-1]
|
48
|
+
p.presence || "."
|
49
|
+
else
|
50
|
+
path
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_dlfile
|
55
|
+
DlFile.generate(self)
|
56
|
+
end
|
57
|
+
|
58
|
+
def delta dlfile
|
59
|
+
abort "cannot delta DLFILE without HD_BASE", 1 unless dlfile[:HD_BASE].present?
|
60
|
+
|
61
|
+
# WARNING: The order of this result hash is important as it defines the order we process things!
|
62
|
+
{chown: [], chmod: [], mv: [], cp: [], rm: []}.tap do |r|
|
63
|
+
logger.ensure_prefix c("[dFS]\t", :magenta) do
|
64
|
+
log "HD-BASE is " << c(dlfile[:HD_BASE], :magenta)
|
65
|
+
dlfile.each do |ino, snode|
|
66
|
+
next if ino == :HD_BASE || ino == :HD_DOTFILES
|
67
|
+
node = @index[ino]
|
68
|
+
unless node
|
69
|
+
warn("INODE " << c(ino, :magenta) << c(" not found, ignore...", :red))
|
70
|
+
next
|
71
|
+
end
|
72
|
+
|
73
|
+
# flagged for removal
|
74
|
+
if %w[del delr delf delrf].include?(snode.mode)
|
75
|
+
r[:rm] << Softnode.new(node: node, snode: snode, is: node.relative_path)
|
76
|
+
next
|
77
|
+
end
|
78
|
+
|
79
|
+
# mode changed
|
80
|
+
if "#{snode.mode}".present? && "#{snode.mode}" != "cp" && "#{node.mode}" != "#{snode.mode}"
|
81
|
+
r[:chmod] << Softnode.new(node: node, snode: snode, is: node.mode, should: snode.mode)
|
82
|
+
end
|
83
|
+
|
84
|
+
# uid/gid changed
|
85
|
+
if "#{node.owngrp}" != "#{snode.uid}:#{snode.gid}"
|
86
|
+
r[:chown] << Softnode.new(node: node, snode: snode, is: node.owngrp, should: "#{snode.uid}:#{snode.gid}")
|
87
|
+
end
|
88
|
+
|
89
|
+
# path changed
|
90
|
+
if "#{node.relative_path}" != "#{snode.relative_path}"
|
91
|
+
r[snode.mode == "cp" ? :cp : :mv] << Softnode.new(node: node, snode: snode, is: node.relative_path, should: snode.relative_path)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# sort results to perform actions inside-out
|
97
|
+
r.each do |k, v|
|
98
|
+
r[k] = v.sort_by{|snode| snode.node.relative_path.length }.reverse
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
protected
|
104
|
+
|
105
|
+
def index_node path
|
106
|
+
Node.new(self, path).tap{|node| @index[node.inode] = node }
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|