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.
- 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
|