bookbinder 0.2.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.
Files changed (41) hide show
  1. data/README.md +97 -0
  2. data/Rakefile +12 -0
  3. data/bin/bookbinder +17 -0
  4. data/lib/bookbinder/document_proxy.rb +171 -0
  5. data/lib/bookbinder/file.rb +149 -0
  6. data/lib/bookbinder/file_system/directory.rb +62 -0
  7. data/lib/bookbinder/file_system/memory.rb +57 -0
  8. data/lib/bookbinder/file_system/zip_file.rb +106 -0
  9. data/lib/bookbinder/file_system.rb +35 -0
  10. data/lib/bookbinder/media_type.rb +17 -0
  11. data/lib/bookbinder/operations.rb +59 -0
  12. data/lib/bookbinder/package/epub.rb +69 -0
  13. data/lib/bookbinder/package/openbook.rb +33 -0
  14. data/lib/bookbinder/package.rb +295 -0
  15. data/lib/bookbinder/transform/epub/audio_overlay.rb +227 -0
  16. data/lib/bookbinder/transform/epub/audio_soundtrack.rb +73 -0
  17. data/lib/bookbinder/transform/epub/contributor.rb +11 -0
  18. data/lib/bookbinder/transform/epub/cover_image.rb +80 -0
  19. data/lib/bookbinder/transform/epub/cover_page.rb +148 -0
  20. data/lib/bookbinder/transform/epub/creator.rb +67 -0
  21. data/lib/bookbinder/transform/epub/description.rb +43 -0
  22. data/lib/bookbinder/transform/epub/language.rb +29 -0
  23. data/lib/bookbinder/transform/epub/metadata.rb +140 -0
  24. data/lib/bookbinder/transform/epub/nav.rb +60 -0
  25. data/lib/bookbinder/transform/epub/nav_toc.rb +177 -0
  26. data/lib/bookbinder/transform/epub/ncx.rb +63 -0
  27. data/lib/bookbinder/transform/epub/ocf.rb +33 -0
  28. data/lib/bookbinder/transform/epub/opf.rb +22 -0
  29. data/lib/bookbinder/transform/epub/package_identifier.rb +87 -0
  30. data/lib/bookbinder/transform/epub/rendition.rb +265 -0
  31. data/lib/bookbinder/transform/epub/resources.rb +38 -0
  32. data/lib/bookbinder/transform/epub/spine.rb +79 -0
  33. data/lib/bookbinder/transform/epub/title.rb +92 -0
  34. data/lib/bookbinder/transform/epub/version.rb +39 -0
  35. data/lib/bookbinder/transform/generator.rb +8 -0
  36. data/lib/bookbinder/transform/openbook/json.rb +15 -0
  37. data/lib/bookbinder/transform/organizer.rb +41 -0
  38. data/lib/bookbinder/transform.rb +7 -0
  39. data/lib/bookbinder/version.rb +5 -0
  40. data/lib/bookbinder.rb +29 -0
  41. metadata +131 -0
data/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # Bookbinder
2
+
3
+ Ebook format conversion.
4
+
5
+
6
+ ## Basic use
7
+
8
+ Display the contents of an EPUB as a JSON "map":
9
+
10
+ $ bookbinder map path/to/file.epub
11
+
12
+
13
+ Convert an EPUB to an Openbook directory (the directory need not exist yet):
14
+
15
+ $ bookbinder convert path/to/file.epub path/to/dir
16
+
17
+ Convert an EPUB to an Openbook archive:
18
+
19
+ $ bookbinder convert path/to/file.epub path/to/file.openbook
20
+
21
+ Convert an Openbook to an EPUB (...is not yet fully implemented!)
22
+
23
+
24
+ ## Use as a Ruby library
25
+
26
+ EPUB to Openbook:
27
+
28
+ require 'bookbinder'
29
+ epub = Bookbinder::Package::EPUB.read('book.epub')
30
+ openbook = epub.export(Bookbinder::Package::Openbook)
31
+ openbook.write('book.openbook')
32
+
33
+ For other basic actions, take a look at `lib/bookbinder/operations.rb`. This
34
+ class provides a basic layer of convenience, such as reducing the above to:
35
+
36
+ require 'bookbinder'
37
+ Bookbinder::Operations.convert('book.epub', 'book.openbook')
38
+
39
+
40
+ ## Improving Bookbinder
41
+
42
+ Inside Bookbinder, a "book" is simply a nested hash of properties and values.
43
+ This hash is called "the map". Properties of the map that are transferrable
44
+ between ebook package formats should follow the Openbook convention, which is
45
+ currently maintained here:
46
+
47
+ > https://gist.github.com/joseph/7303930
48
+
49
+ The key to Bookbinder is this: for every feature of an ebook format, we create
50
+ a "transform" class that does two things:
51
+ - parses the raw config from the package into standard Openbook properties
52
+ on the map; and
53
+ - generates raw config into the package from those same standard Openbook
54
+ properties on the map
55
+
56
+ Basically, for every feature, the transform class describes how to read it,
57
+ and how to write it.
58
+
59
+ The nice thing about this set-up is that if multiple package formats
60
+ support the same feature, their transform classes work on the same map. Say
61
+ you are converting from EPUB3 to Openbook - the book's title is parsed out of
62
+ the EPUB file into the map using the transform at
63
+ `lib/bookbinder/transform/epub/title.rb`. Then the map is handed over to
64
+ the Openbook package, and the transform at
65
+ `lib/bookbinder/transform/openbook/title.rb` would write it out to the
66
+ new package file.
67
+
68
+ You can of course convert a package to its own format: in this case the same
69
+ transform class does both the reading and the writing out -- the effect of
70
+ this is to "tidy" the package.
71
+
72
+ To add a package format Bookbinder, you should create the package class in
73
+ `lib/bookbinder/package`, then create directory of transforms in
74
+ `lib/bookbinder/transform`. You can borrow transforms from other packages. For
75
+ instance, it might make sense for a DAISY package to share some of the
76
+ transforms in EPUB, or for a hPub package to borrow some transforms
77
+ from Openbook.
78
+
79
+ If you are adding a new feature to Bookbinder, you create the
80
+ appropriate transform class for each package that supports the feature, and
81
+ then add the equivalent tests.
82
+
83
+
84
+ ## Planned format support
85
+
86
+ * Openbook
87
+ * EPUB3
88
+ * EPUB2
89
+ * hPub
90
+ * PDF
91
+ * ...
92
+
93
+
94
+ ## Attribution and licensing
95
+
96
+ Bookbinder was originally developed at OverDrive, Inc. Released under the
97
+ MIT License. See `MIT-LICENSE` in this directory.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env rake
2
+ require 'bundler/gem_tasks'
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new { |t|
6
+ t.libs << 'test'
7
+ t.test_files = FileList['test/unit/**/test*.rb']
8
+ t.verbose = true
9
+ }
10
+
11
+ desc('Run tests')
12
+ task(:default => :test)
data/bin/bookbinder ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bookbinder'
4
+
5
+ if ARGV[0] == 'map'
6
+ puts Bookbinder::Operations.map(ARGV[1])
7
+ elsif ARGV[0] == 'validate'
8
+ puts('Validate: not yet implemented.')
9
+ elsif ARGV[0] == 'normalize'
10
+ puts('Normalize: still too dangerous.')
11
+ elsif ARGV[0] == 'convert'
12
+ src_path = ARGV[1]
13
+ dest_path = ARGV[2]
14
+ src_pkg, dest_pkg = Bookbinder::Operations.convert(src_path, dest_path)
15
+ puts("Converted #{src_pkg.class} at #{src_path}")
16
+ puts("------ to #{dest_pkg.class} at #{dest_path}")
17
+ end
@@ -0,0 +1,171 @@
1
+ class Bookbinder::DocumentProxy
2
+
3
+ XMLNS = {
4
+ 'xhtml' => 'http://www.w3.org/1999/xhtml',
5
+ 'dc' => 'http://purl.org/dc/elements/1.1/',
6
+ 'dcterms' => 'http://purl.org/dc/terms/',
7
+ 'mathml' => 'http://www.w3.org/1998/Math/MathML',
8
+ 'svg' => 'http://www.w3.org/2000/svg',
9
+ 'ocf' => 'urn:oasis:names:tc:opendocument:xmlns:container',
10
+ 'opf' => 'http://www.idpf.org/2007/opf',
11
+ 'ncx' => 'http://www.daisy.org/z3986/2005/ncx/',
12
+ 'epub' => 'http://www.idpf.org/2007/ops'
13
+ }
14
+
15
+ XML_PREFIX = {
16
+ 'ibooks' => 'http://vocabulary.itunes.apple.com/rdf/ibooks/vocabulary-extensions-1.0/',
17
+ 'rendition' => 'http://www.idpf.org/vocab/rendition/#'
18
+ }
19
+
20
+
21
+ attr_reader(:doc)
22
+
23
+
24
+ def load(string)
25
+ begin
26
+ @doc = Nokogiri::XML(string)
27
+ rescue
28
+ @doc = Nokogiri::HTML(string)
29
+ end
30
+ self
31
+ end
32
+
33
+
34
+ def build(&blk)
35
+ builder = Nokogiri::XML::Builder.new { |x|
36
+ @doc = x.doc
37
+ yield(self, x)
38
+ }
39
+ self
40
+ end
41
+
42
+
43
+ def new_node(tag, options = {})
44
+ Nokogiri::XML::Node.new(tag, doc).tap { |node|
45
+ yield(node) if block_given?
46
+ if parent = options[:append]
47
+ parent = find(parent) if parent.kind_of?(String)
48
+ parent.add_child(node)
49
+ end
50
+ }
51
+ end
52
+
53
+
54
+ # Given a CSS query, returns the first result, or nil.
55
+ #
56
+ def find(query)
57
+ @doc.at_css(query, node_namespaces(@doc.root))
58
+ end
59
+
60
+
61
+ # Given a CSS query, returns all results, or an empty array.
62
+ #
63
+ def search(query)
64
+ @doc.css(query, node_namespaces(@doc.root))
65
+ end
66
+
67
+
68
+ # Iterates over the results of the search for the given CSS query.
69
+ #
70
+ def each(query, &blk)
71
+ search(query).each(&blk)
72
+ end
73
+
74
+
75
+ def find_within(node, query)
76
+ node.at_css(query, node_namespaces(node))
77
+ end
78
+
79
+
80
+ def search_within(node, query)
81
+ node.css(query, node_namespaces(node))
82
+ end
83
+
84
+
85
+ def each_within(node, query, &blk)
86
+ search_within(node, query).each(&blk)
87
+ end
88
+
89
+
90
+ def add_namespace(namespace_label, default = false)
91
+ add_node_namespace(@doc.root, namespace_label, default)
92
+ end
93
+
94
+
95
+ def add_prefix(prefix_label, prefix_attribute = 'prefix')
96
+ add_node_prefix(@doc.root, prefix_label, prefix_attribute)
97
+ end
98
+
99
+
100
+ def add_node_namespace(node, namespace_label, default = false)
101
+ xmlns = default ? nil : namespace_label
102
+ node.add_namespace_definition(xmlns, XMLNS[namespace_label])
103
+ end
104
+
105
+
106
+ def add_node_prefix(node, prefix_label, prefix_attribute = 'prefix')
107
+ prefix = "#{prefix_label}: #{XML_PREFIX[prefix_label]}"
108
+ prefixes = [node[prefix_attribute], prefix].compact
109
+ node[prefix_attribute] = prefixes.join("\n")
110
+ end
111
+
112
+
113
+ def to_str
114
+ if @doc.kind_of?(Nokogiri::HTML::Document)
115
+ # Remove the old-style charset meta tag that Nokogiri auto-inserts.
116
+ # This is nasty business, but apparently once again Nokogiri is
117
+ # wrong and Markus Gylling knows best:
118
+ # http://code.google.com/p/epubcheck/issues/detail?id=135#c3
119
+ html = @doc.to_xhtml
120
+ html.sub!(
121
+ '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />',
122
+ ''
123
+ )
124
+ html
125
+ elsif @doc.kind_of?(Nokogiri::XML::Document)
126
+ @doc.to_xml
127
+ end
128
+ end
129
+
130
+
131
+ protected
132
+
133
+ # When performing a css() or at_css() query that has a namespaced
134
+ # element (epub|switch) or a namespaced attribute (*[epub|type]),
135
+ # you need to list the namespaces you want to use.
136
+ #
137
+ # You could just use the namespaces in the document, but it's possible
138
+ # that the document author has bound the namespace to something
139
+ # unexpected. For eg, they might have <dublincore:title> elements,
140
+ # rather than <dc:title> elements. They probably deserve everything
141
+ # that's coming to them, but it's valid XML, so we should support it.
142
+ #
143
+ # Therefore we should supply the namespace labels that WE expect,
144
+ # rather than what the document author supplied, and let LibXML
145
+ # do the translation automatically.
146
+ #
147
+ # This method constructs a hash of namespaces we can give to the
148
+ # Nokogiri query, using our XMLNS constant. It also includes any
149
+ # namespaces defined on the node or its ancestors -- typically
150
+ # so that we can get the 'default' namespace for the document.
151
+ #
152
+
153
+ def node_namespaces(node)
154
+ @node_namespaces ||= {}
155
+ @node_namespaces[node] ||= node.namespaces.merge(default_namespaces)
156
+ end
157
+
158
+
159
+ def default_namespaces
160
+ @default_namespaces ||= XMLNS.inject({}) { |acc, arr|
161
+ key, val = arr
162
+ acc.update("xmlns#{key ? ":#{key}" : ''}" => val)
163
+ }
164
+ end
165
+
166
+
167
+ def method_missing(mthd, *args, &blk)
168
+ @doc.send(mthd, *args, &blk)
169
+ end
170
+
171
+ end
@@ -0,0 +1,149 @@
1
+ class Bookbinder::File
2
+
3
+ attr_accessor(:path, :file_type)
4
+
5
+
6
+ def initialize(path, file_system)
7
+ @path = path
8
+ @file_system = file_system
9
+ @file_type = analyze_file_type(@path)
10
+ end
11
+
12
+
13
+ # Gets a representation of the contents -- if json, as a hash, if xml,
14
+ # as a Nokogiri document, etc. If mode includes 'w', then the document
15
+ # will be saved eventually.
16
+ #
17
+ def document(mode = 'rw')
18
+ dirty! if mode.match(/w/)
19
+ @document ||= string_to_document
20
+ end
21
+
22
+
23
+ def new_xml_document(&blk)
24
+ @file_system.write(@path, '') unless dirty?
25
+ dirty!
26
+ @document = Bookbinder::DocumentProxy.new.build(&blk)
27
+ end
28
+
29
+
30
+ # Indicates that we should write out the document on save.
31
+ #
32
+ def dirty!
33
+ @dirty = true
34
+ end
35
+
36
+
37
+ # Has the document changed, therefore we should write it out on save?
38
+ #
39
+ def dirty?
40
+ @dirty ? true : false
41
+ end
42
+
43
+
44
+ # Modifies the file in this file_system.
45
+ #
46
+ def save
47
+ @file_system.write(@path, document_to_string) if dirty?
48
+ reset
49
+ end
50
+
51
+
52
+ # Resets state so that the next call to `document` will load it
53
+ # fresh from the string. Returns self for easy chaining.
54
+ #
55
+ def reset
56
+ @document = nil
57
+ @dirty = false
58
+ self
59
+ end
60
+
61
+
62
+ # Writes the file to another file system.
63
+ #
64
+ def copy_to(dest_file_system, dest_path)
65
+ @file_system.get_file(@path) { |file|
66
+ dest_file_system.set_file(dest_path, file)
67
+ }
68
+ self
69
+ end
70
+
71
+
72
+ # Guesses the mime-type (aka content-type, aka media-type)
73
+ # of this file based on its extension.
74
+ #
75
+ def media_type
76
+ @media_type ||= Bookbinder::MediaType.of(@path)
77
+ end
78
+
79
+
80
+ # Proxy through to FileSystem#exists?.
81
+ #
82
+ def exists?
83
+ @file_system.exists?(path)
84
+ end
85
+
86
+
87
+ # Proxy through to FileSystem#read.
88
+ #
89
+ def read
90
+ @file_system.read(path)
91
+ end
92
+
93
+
94
+ # Proxy through to FileSystem#write.
95
+ #
96
+ def write(data)
97
+ @file_system.write(path, data)
98
+ end
99
+
100
+
101
+ # Proxy through to FileSystem#get_file.
102
+ #
103
+ def get_file(mode = 'r', &blk)
104
+ @file_system.get_file(path, mode, &blk)
105
+ end
106
+
107
+
108
+ # Proxy through to FileSystem#set_file.
109
+ #
110
+ def set_file(file_io)
111
+ @file_system.set_file(path, io)
112
+ end
113
+
114
+
115
+ protected
116
+
117
+ def analyze_file_type(path)
118
+ if media_type.match(/json/)
119
+ :json
120
+ elsif media_type.match(/xml$/) || media_type.match(/html$/)
121
+ :xml
122
+ end
123
+ end
124
+
125
+
126
+ def string_to_document(ftype = @file_type)
127
+ if ftype == :json
128
+ JSON.load(@file_system.read(@path))
129
+ elsif ftype == :xml
130
+ Bookbinder::DocumentProxy.new.load(@file_system.read(@path))
131
+ elsif ftype == :text
132
+ @file_system.read(@path)
133
+ end
134
+ end
135
+
136
+
137
+ def document_to_string
138
+ raise 'Document not loaded' unless @document
139
+ if @document.kind_of?(Bookbinder::DocumentProxy)
140
+ @document.to_str
141
+ elsif @document.kind_of?(Hash)
142
+ JSON.dump(@document)
143
+ else
144
+ @document.to_s
145
+ end
146
+ end
147
+
148
+
149
+ end
@@ -0,0 +1,62 @@
1
+ class Bookbinder::FileSystem::Directory
2
+
3
+ def initialize(path)
4
+ @dir = path
5
+ end
6
+
7
+
8
+ def exists?(path)
9
+ File.exists?(full_path(path))
10
+ end
11
+
12
+
13
+ def read(path)
14
+ fpath = full_path(path)
15
+ must_exist(fpath)
16
+ File.read(fpath)
17
+ end
18
+
19
+
20
+ def write(path, data)
21
+ fpath = full_path(path)
22
+ FileUtils.mkdir_p(File.dirname(fpath))
23
+ File.open(fpath, 'w') { |f| f.write(data) }
24
+ nil
25
+ end
26
+
27
+
28
+ def get_file(path, mode = 'r', &blk)
29
+ fpath = full_path(path)
30
+ must_exist(fpath) if mode[0] != 'w'
31
+ File.open(fpath, mode, &blk)
32
+ end
33
+
34
+
35
+ def set_file(path, file)
36
+ fpath = full_path(path)
37
+ FileUtils.mkdir_p(File.dirname(fpath))
38
+ FileUtils.cp(file.path, fpath)
39
+ end
40
+
41
+
42
+ def each
43
+ Dir.glob(File.join(@dir, '**', '*')) { |path|
44
+ next if File.directory?(path)
45
+ yield(path.sub(/#{@dir}\//, ''))
46
+ }
47
+ end
48
+
49
+
50
+ protected
51
+
52
+ def must_exist(fpath)
53
+ return if File.exists?(fpath)
54
+ raise(Bookbinder::FileSystem::UnknownPath, fpath)
55
+ end
56
+
57
+
58
+ def full_path(path)
59
+ File.join(@dir, path)
60
+ end
61
+
62
+ end
@@ -0,0 +1,57 @@
1
+ class Bookbinder::FileSystem::Memory
2
+
3
+ def initialize
4
+ @index = {}
5
+ end
6
+
7
+
8
+ def exists?(path)
9
+ @index.key?(path)
10
+ end
11
+
12
+
13
+ def read(path)
14
+ raise(Bookbinder::FileSystem::UnknownPath, path) unless exists?(path)
15
+ @index[path]
16
+ end
17
+
18
+
19
+ def write(path, data)
20
+ @index[path] = data
21
+ nil
22
+ end
23
+
24
+
25
+ # Creates a tempfile so you can do memory-efficient stuff.
26
+ #
27
+ def get_file(path, mode = 'r', &blk)
28
+ read_before = mode[0] != 'w'
29
+ write_after = mode[0] != 'r'
30
+ begin
31
+ tmpfile = Tempfile.new(File.basename(path))
32
+ if read_before
33
+ tmpfile.write(read(path))
34
+ tmpfile.rewind
35
+ end
36
+ blk.call(tmpfile)
37
+ if write_after
38
+ tmpfile.rewind
39
+ write(path, tmpfile.read)
40
+ end
41
+ ensure
42
+ tmpfile.close
43
+ tmpfile.unlink
44
+ end
45
+ end
46
+
47
+
48
+ def set_file(path, file)
49
+ write(path, file.read)
50
+ end
51
+
52
+
53
+ def each(&blk)
54
+ @index.each_key(&blk)
55
+ end
56
+
57
+ end
@@ -0,0 +1,106 @@
1
+ class Bookbinder::FileSystem::ZipFile < Bookbinder::FileSystem
2
+
3
+ def initialize(path)
4
+ @zipfile_path = path
5
+ @zipfile = nil
6
+ instantiate_zipfile if File.exists?(@zipfile_path)
7
+ end
8
+
9
+
10
+ def exists?(path)
11
+ @zipfile && @zipfile.find_entry(path) ? true : false
12
+ end
13
+
14
+
15
+ def read(path)
16
+ must_exist(path)
17
+ @zipfile.read(path)
18
+ end
19
+
20
+
21
+ def write(path, data)
22
+ return set_mimetype(data) if path == 'mimetype'
23
+ instantiate_zipfile
24
+ @zipfile.get_output_stream(path) { |f| f.write(data) }
25
+ nil
26
+ end
27
+
28
+
29
+ def get_file(path, mode = 'r', &blk)
30
+ read_before = mode[0] != 'w'
31
+ write_after = mode[0] != 'r'
32
+ if read_before
33
+ must_exist(path)
34
+ tmp_path = Dir::Tmpname.create(File.basename(path)) { |tmp_path|
35
+ raise Errno::EEXIST if File.exists?(tmp_path)
36
+ }
37
+ @zipfile.commit
38
+ @zipfile.extract(path, tmp_path)
39
+ File.open(tmp_path, mode) { |tmp_file|
40
+ blk.call(tmp_file)
41
+ if write_after
42
+ set_file(path, tmp_file)
43
+ @zipfile.commit
44
+ end
45
+ }
46
+ File.unlink(tmp_path)
47
+ else
48
+ tmp_file = Tempfile.new(File.basename(path))
49
+ blk.call(tmp_file)
50
+ tmp_file.close
51
+ set_file(path, tmp_file) if write_after
52
+ tmp_file.unlink
53
+ end
54
+ nil
55
+ end
56
+
57
+
58
+ def set_file(path, file)
59
+ instantiate_zipfile
60
+ @zipfile.add(path, file.path) { true }
61
+ @zipfile.commit
62
+ nil
63
+ end
64
+
65
+
66
+ def each(&blk)
67
+ @zipfile.each(&blk)
68
+ end
69
+
70
+
71
+ def close
72
+ @zipfile.close if @zipfile
73
+ end
74
+
75
+
76
+ protected
77
+
78
+ def must_exist(path)
79
+ raise(Bookbinder::FileSystem::UnknownPath, path) unless exists?(path)
80
+ end
81
+
82
+
83
+ def instantiate_zipfile(options = {})
84
+ @zipfile ||= Zip::File.open(@zipfile_path, Zip::File::CREATE)
85
+ end
86
+
87
+
88
+ # This is all EPUB's fault.
89
+ #
90
+ def set_mimetype(mimetype)
91
+ if @zipfile || File.exists?(@zipfile_path)
92
+ raise("Cannot set mimetype for existing archive: #{@zipfile_path}")
93
+ end
94
+ Zip::OutputStream.open(@zipfile_path) { |stream|
95
+ stream.put_next_entry(
96
+ 'mimetype',
97
+ nil,
98
+ nil,
99
+ Zip::Entry::STORED,
100
+ Zlib::NO_COMPRESSION
101
+ )
102
+ stream.write(mimetype)
103
+ }
104
+ end
105
+
106
+ end
@@ -0,0 +1,35 @@
1
+ class Bookbinder::FileSystem
2
+
3
+ def exists?(path)
4
+ # IMPLEMENT IN SUBCLASS
5
+ end
6
+
7
+
8
+ def read(path)
9
+ # IMPLEMENT IN SUBCLASS
10
+ end
11
+
12
+
13
+ def write(path, data)
14
+ # IMPLEMENT IN SUBCLASS
15
+ end
16
+
17
+
18
+ def get_file(path, mode = 'r', &blk)
19
+ # IMPLEMENT IN SUBCLASS
20
+ end
21
+
22
+
23
+ def set_file(path, file)
24
+ # IMPLEMENT IN SUBCLASS
25
+ end
26
+
27
+
28
+ def each(&blk)
29
+ # IMPLEMENT IN SUBCLASS
30
+ end
31
+
32
+
33
+ class UnknownPath < RuntimeError; end
34
+
35
+ end