bookbinder 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +97 -0
- data/Rakefile +12 -0
- data/bin/bookbinder +17 -0
- data/lib/bookbinder/document_proxy.rb +171 -0
- data/lib/bookbinder/file.rb +149 -0
- data/lib/bookbinder/file_system/directory.rb +62 -0
- data/lib/bookbinder/file_system/memory.rb +57 -0
- data/lib/bookbinder/file_system/zip_file.rb +106 -0
- data/lib/bookbinder/file_system.rb +35 -0
- data/lib/bookbinder/media_type.rb +17 -0
- data/lib/bookbinder/operations.rb +59 -0
- data/lib/bookbinder/package/epub.rb +69 -0
- data/lib/bookbinder/package/openbook.rb +33 -0
- data/lib/bookbinder/package.rb +295 -0
- data/lib/bookbinder/transform/epub/audio_overlay.rb +227 -0
- data/lib/bookbinder/transform/epub/audio_soundtrack.rb +73 -0
- data/lib/bookbinder/transform/epub/contributor.rb +11 -0
- data/lib/bookbinder/transform/epub/cover_image.rb +80 -0
- data/lib/bookbinder/transform/epub/cover_page.rb +148 -0
- data/lib/bookbinder/transform/epub/creator.rb +67 -0
- data/lib/bookbinder/transform/epub/description.rb +43 -0
- data/lib/bookbinder/transform/epub/language.rb +29 -0
- data/lib/bookbinder/transform/epub/metadata.rb +140 -0
- data/lib/bookbinder/transform/epub/nav.rb +60 -0
- data/lib/bookbinder/transform/epub/nav_toc.rb +177 -0
- data/lib/bookbinder/transform/epub/ncx.rb +63 -0
- data/lib/bookbinder/transform/epub/ocf.rb +33 -0
- data/lib/bookbinder/transform/epub/opf.rb +22 -0
- data/lib/bookbinder/transform/epub/package_identifier.rb +87 -0
- data/lib/bookbinder/transform/epub/rendition.rb +265 -0
- data/lib/bookbinder/transform/epub/resources.rb +38 -0
- data/lib/bookbinder/transform/epub/spine.rb +79 -0
- data/lib/bookbinder/transform/epub/title.rb +92 -0
- data/lib/bookbinder/transform/epub/version.rb +39 -0
- data/lib/bookbinder/transform/generator.rb +8 -0
- data/lib/bookbinder/transform/openbook/json.rb +15 -0
- data/lib/bookbinder/transform/organizer.rb +41 -0
- data/lib/bookbinder/transform.rb +7 -0
- data/lib/bookbinder/version.rb +5 -0
- data/lib/bookbinder.rb +29 -0
- 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
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
|