5 def initialize(manifest_text="")
6 @tree = CollectionStream.new(".")
8 import_manifest!(manifest_text)
12 @manifest_text ||= @tree.manifest_text
15 def import_manifest!(manifest_text)
16 manifest = Keep::Manifest.new(manifest_text)
17 manifest.each_line do |stream_root, locators, file_specs|
18 if stream_root.empty? or locators.empty? or file_specs.empty?
19 raise ArgumentError.new("manifest text includes malformed line")
21 file_specs.map { |s| manifest.split_file_token(s) }.
22 each do |file_start, file_len, file_path|
23 @tree.file_at(normalize_path(stream_root, file_path)).
24 add_range(locators, file_start, file_len)
27 if @manifest_text == ""
28 @manifest_text = manifest_text
36 # We generate normalized manifests, so all we have to do is force
41 def copy!(source, target, source_collection=nil)
42 copy(:merge, source, target, source_collection)
45 def rename!(source, target)
46 copy(:add_copy, source, target) { remove!(source, recursive: true) }
49 def remove!(path, opts={})
50 stream, name = find(path)
52 return self if @tree.leaf?
53 @tree = CollectionStream.new(".")
55 stream.delete(name, opts)
63 normpath = normalize_path(*parts)
73 def copy(copy_method, source, target, source_collection=nil)
74 # Find the item at path `source` in `source_collection`, find the
75 # destination stream at path `target`, and use `copy_method` to copy
76 # the found object there. If a block is passed in, it will be called
77 # right before we do the actual copy, after we confirm that everything
78 # is found and can be copied.
79 source_collection = self if source_collection.nil?
80 src_stream, src_tail = source_collection.find(source)
81 dst_stream, dst_tail = find(target)
82 if (source_collection.equal?(self) and
83 (src_stream.path == dst_stream.path) and (src_tail == dst_tail))
87 src_tail = src_stream.name
89 src_item = src_stream[src_tail]
92 check_method = "check_can_#{copy_method}".to_sym
94 # Find out if `target` refers to a stream we should copy into.
95 tail_stream = dst_stream[dst_tail]
96 tail_stream.send(check_method, src_item, src_tail)
97 rescue Errno::ENOENT, Errno::ENOTDIR
98 # It does not. Check that we can copy `source` to the full
99 # path specified by `target`.
100 dst_stream.send(check_method, src_item, dst_tail)
101 target_name = dst_tail
103 # Yes, `target` is a stream. Copy the item at `source` into it with
105 dst_stream = tail_stream
106 target_name = src_tail
108 # At this point, we know the operation will work. Call any block as
112 # Re-find the destination stream, in case the block removed
113 # the original (that's how rename is implemented).
114 dst_path = normalize_path(dst_stream.path)
118 dst_stream = @tree.stream_at(dst_path)
121 dst_stream.send(copy_method, src_item, target_name)
130 def normalize_path(*parts)
131 path = File.join(*parts)
132 raise ArgumentError.new("empty path") if path.empty?
133 path.sub(/^\.(\/|$)/, "")
137 attr_reader :path, :name
141 @name = File.basename(path)
145 LocatorRange = Struct.new(:locators, :start_pos, :length)
147 class CollectionFile < CollectionItem
161 def add_range(locators, start_pos, length)
162 # Given an array of locators, and this file's start position and
163 # length within them, store a LocatorRange with information about
164 # the locators actually used.
165 loc_sizes = locators.map { |s| Keep::Locator.parse(s).size.to_i }
166 start_index, start_pos = loc_size_index(loc_sizes, start_pos, 0, :>=)
167 end_index, _ = loc_size_index(loc_sizes, length, start_index, :>)
168 @ranges << LocatorRange.
169 new(locators[start_index..end_index], start_pos, length)
172 def each_range(&block)
176 def check_can_add_copy(src_item, name)
177 raise Errno::ENOTDIR.new(path)
180 alias_method :check_can_merge, :check_can_add_copy
182 def copy_named(copy_path)
183 copy = self.class.new(copy_path)
184 each_range { |range| copy.add_range(*range) }
190 def loc_size_index(loc_sizes, length, index, comp_op)
191 # Pass in an array of locator size hints (integers). Starting from
192 # `index`, step through the size array until they provide a number
193 # of bytes that is `comp_op` (:>= or :>) to `length`. Return the
194 # index of the end locator and the amount of data to read from it.
195 while length.send(comp_op, loc_sizes[index])
197 length -= loc_sizes[index]
203 class CollectionStream < CollectionItem
219 raise Errno::ENOENT.new("%p not found in %p" % [key, path])
222 def delete(name, opts={})
224 if item.leaf? or opts[:recursive]
227 raise Errno::ENOTEMPTY.new(path)
232 # Given a POSIX-style path, return the CollectionStream that
233 # contains the object at that path, and the name of the object
235 components = find_path.split("/")
236 tail = components.pop
237 [components.reduce(self, :[]), tail]
240 def stream_at(find_path)
241 key, rest = find_path.split("/", 2)
242 next_stream = get_or_new(key, CollectionStream)
246 next_stream.stream_at(rest)
250 def file_at(find_path)
251 stream_path, _, file_name = find_path.rpartition("/")
252 if stream_path.empty?
253 get_or_new(file_name, CollectionFile)
255 stream_at(stream_path).file_at(file_name)
260 # Return a string with the normalized manifest text for this stream,
261 # including all substreams.
262 file_keys, stream_keys = items.keys.sort.partition do |key|
263 items[key].is_a?(CollectionFile)
265 my_line = StreamManifest.new(path)
266 file_keys.each do |file_name|
267 my_line.add_file(items[file_name])
269 sub_lines = stream_keys.map do |sub_name|
270 items[sub_name].manifest_text
272 my_line.to_s + sub_lines.join("")
275 def check_can_add_copy(src_item, key)
276 if existing = check_can_merge(src_item, key) and not existing.leaf?
277 raise Errno::ENOTEMPTY.new(existing.path)
281 def check_can_merge(src_item, key)
282 if existing = items[key] and (existing.class != src_item.class)
283 raise Errno::ENOTDIR.new(existing.path)
288 def add_copy(src_item, key)
289 items[key] = src_item.copy_named("#{path}/#{key}")
292 def merge(src_item, key)
293 # Do a recursive copy of the collection item `src_item` to destination
294 # `key`. If a simple copy is safe, do that; otherwise, recursively
295 # merge the contents of the stream `src_item` into the stream at
298 check_can_add_copy(src_item, key)
299 add_copy(src_item, key)
300 rescue Errno::ENOTEMPTY
303 # Copy as much as possible, then raise any error encountered.
304 src_item.items.each_pair do |sub_key, sub_item|
306 dest.merge(sub_item, sub_key)
307 rescue Errno::ENOTDIR => error
310 raise error unless error.nil?
314 def copy_named(copy_path)
315 copy = self.class.new(copy_path)
316 items.each_pair do |key, item|
317 copy.add_copy(item, key)
328 def get_or_new(key, klass)
329 # Return the collection item at `key` and ensure that it's a `klass`.
330 # If `key` does not exist, create a new `klass` there.
331 # If the value for `key` is not a `klass`, raise an ArgumentError.
334 items[key] = klass.new("#{path}/#{key}")
335 elsif not item.is_a?(klass)
337 new("in stream %p, %p is a %s, not a %s" %
338 [path, key, items[key].class.human_name, klass.human_name])
346 # Build a manifest text for a single stream, without substreams.
355 def add_file(coll_file)
356 coll_file.each_range do |range|
357 add(coll_file.name, *range)
362 if @file_specs.empty?
365 "%s %s %s\n" % [escape_name(@name), @locators.join(" "),
366 @file_specs.join(" ")]
372 def add(file_name, loc_a, file_start, file_len)
373 # Ensure that the locators in loc_a appear in this locator in sequence,
374 # adding as few as possible. Save a new file spec based on those
375 # locators' position.
376 loc_size = @locators.size
377 add_size = loc_a.size
380 while (loc_ii < loc_size) and (add_ii < add_size)
381 if @locators[loc_ii] == loc_a[add_ii]
389 to_add = loc_a[add_ii, add_size] || []
391 @loc_sizes += to_add.map { |s| Keep::Locator.parse(s).size.to_i }
392 start = @loc_sizes[0, loc_ii].reduce(0, &:+) + file_start
393 @file_specs << "#{start}:#{file_len}:#{escape_name(file_name)}"
396 def escape_name(name)
397 name.gsub(/\\/, "\\\\\\\\").gsub(/\s/) do |s|
398 s.each_byte.map { |c| "\\%03o" % c }.join("")