bookbinder 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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