diff --git a/src/library/entry.cr b/src/library/entry.cr index dd50ed3..9fb1ef9 100644 --- a/src/library/entry.cr +++ b/src/library/entry.cr @@ -1,62 +1,27 @@ require "image_size" require "yaml" -class Entry - include YAML::Serializable +abstract class Entry + getter id : String, book : Title, title : String, + size : String, pages : Int32, mtime : Time, + encoded_path : String, encoded_title : String, err_msg : String? - getter zip_path : String, book : Title, title : String, - size : String, pages : Int32, id : String, encoded_path : String, - encoded_title : String, mtime : Time, err_msg : String? + def initialize( + @id, @title, @book, + @size, @pages, @mtime, + @encoded_path, @encoded_title, @err_msg) + end - @[YAML::Field(ignore: true)] - @sort_title : String? - - def initialize(@zip_path, @book) - storage = Storage.default - @encoded_path = URI.encode @zip_path - @title = File.basename @zip_path, File.extname @zip_path - @encoded_title = URI.encode @title - @size = (File.size @zip_path).humanize_bytes - id = storage.get_entry_id @zip_path, File.signature(@zip_path) - if id.nil? - id = random_str - storage.insert_entry_id({ - path: @zip_path, - id: id, - signature: File.signature(@zip_path).to_s, - }) - end - @id = id - @mtime = File.info(@zip_path).modification_time - - unless File.readable? @zip_path - @err_msg = "File #{@zip_path} is not readable." - Logger.warn "#{@err_msg} Please make sure the " \ - "file permission is configured correctly." - return - end - - archive_exception = validate_archive @zip_path - unless archive_exception.nil? - @err_msg = "Archive error: #{archive_exception}" - Logger.warn "Unable to extract archive #{@zip_path}. " \ - "Ignoring it. #{@err_msg}" - return - end - - file = ArchiveFile.new @zip_path - @pages = file.entries.count do |e| - SUPPORTED_IMG_TYPES.includes? \ - MIME.from_filename? e.filename - end - file.close + def self.new(ctx : YAML::ParseContext, node : YAML::Nodes::Node) + # TODO: check node? and select proper subclass + ZippedEntry.new ctx, node end def build_json(*, slim = false) JSON.build do |json| json.object do - {% for str in %w(zip_path title size id) %} - json.field {{str}}, @{{str.id}} + {% for str in %w(path title size id) %} + json.field {{str}}, {{str.id}} {% end %} if err_msg json.field "err_msg", err_msg @@ -74,6 +39,9 @@ class Entry end end + @[YAML::Field(ignore: true)] + @sort_title : String? + def sort_title sort_title_cached = @sort_title return sort_title_cached if sort_title_cached @@ -131,58 +99,6 @@ class Entry url end - private def sorted_archive_entries - ArchiveFile.open @zip_path do |file| - entries = file.entries - .select { |e| - SUPPORTED_IMG_TYPES.includes? \ - MIME.from_filename? e.filename - } - .sort! { |a, b| - compare_numerically a.filename, b.filename - } - yield file, entries - end - end - - def read_page(page_num) - raise "Unreadble archive. #{@err_msg}" if @err_msg - img = nil - begin - sorted_archive_entries do |file, entries| - page = entries[page_num - 1] - data = file.read_entry page - if data - img = Image.new data, MIME.from_filename(page.filename), - page.filename, data.size - end - end - rescue e - Logger.warn "Unable to read page #{page_num} of #{@zip_path}. Error: #{e}" - end - img - end - - def page_dimensions - sizes = [] of Hash(String, Int32) - sorted_archive_entries do |file, entries| - entries.each_with_index do |e, i| - begin - data = file.read_entry(e).not_nil! - size = ImageSize.get data - sizes << { - "width" => size.width, - "height" => size.height, - } - rescue e - Logger.warn "Failed to read page #{i} of entry #{zip_path}. #{e}" - sizes << {"width" => 1000_i32, "height" => 1000_i32} - end - end - end - sizes - end - def next_entry(username) entries = @book.sorted_entries username idx = entries.index self @@ -197,20 +113,6 @@ class Entry entries[idx - 1] end - def date_added - date_added = nil - TitleInfo.new @book.dir do |info| - info_da = info.date_added[@title]? - if info_da.nil? - date_added = info.date_added[@title] = ctime @zip_path - info.save - else - date_added = info_da - end - end - date_added.not_nil! # is it ok to set not_nil! here? - end - # For backward backward compatibility with v0.1.0, we save entry titles # instead of IDs in info.json def save_progress(username, page) @@ -290,7 +192,7 @@ class Entry end Storage.default.save_thumbnail @id, img rescue e - Logger.warn "Failed to generate thumbnail for file #{@zip_path}. #{e}" + Logger.warn "Failed to generate thumbnail for file #{path}. #{e}" end img @@ -299,4 +201,139 @@ class Entry def get_thumbnail : Image? Storage.default.get_thumbnail @id end + + def date_added : Time + date_added = nil + TitleInfo.new @book.dir do |info| + info_da = info.date_added[@title]? + if info_da.nil? + date_added = info.date_added[@title] = createtime + info.save + else + date_added = info_da + end + end + date_added.not_nil! # is it ok to set not_nil! here? + end + + abstract def path : String + + abstract def createtime : Time + + abstract def read_page(page_num) + + abstract def page_dimensions + + abstract def exists? : Bool? +end + +class ZippedEntry < Entry + include YAML::Serializable + + getter zip_path : String + + def initialize(@zip_path, @book) + storage = Storage.default + @encoded_path = URI.encode @zip_path + @title = File.basename @zip_path, File.extname @zip_path + @encoded_title = URI.encode @title + @size = (File.size @zip_path).humanize_bytes + id = storage.get_entry_id @zip_path, File.signature(@zip_path) + if id.nil? + id = random_str + storage.insert_entry_id({ + path: @zip_path, + id: id, + signature: File.signature(@zip_path).to_s, + }) + end + @id = id + @mtime = File.info(@zip_path).modification_time + + unless File.readable? @zip_path + @err_msg = "File #{@zip_path} is not readable." + Logger.warn "#{@err_msg} Please make sure the " \ + "file permission is configured correctly." + return + end + + archive_exception = validate_archive @zip_path + unless archive_exception.nil? + @err_msg = "Archive error: #{archive_exception}" + Logger.warn "Unable to extract archive #{@zip_path}. " \ + "Ignoring it. #{@err_msg}" + return + end + + file = ArchiveFile.new @zip_path + @pages = file.entries.count do |e| + SUPPORTED_IMG_TYPES.includes? \ + MIME.from_filename? e.filename + end + file.close + end + + def path : String + @zip_path + end + + def createtime : Time + ctime @zip_path + end + + private def sorted_archive_entries + ArchiveFile.open @zip_path do |file| + entries = file.entries + .select { |e| + SUPPORTED_IMG_TYPES.includes? \ + MIME.from_filename? e.filename + } + .sort! { |a, b| + compare_numerically a.filename, b.filename + } + yield file, entries + end + end + + def read_page(page_num) + raise "Unreadble archive. #{@err_msg}" if @err_msg + img = nil + begin + sorted_archive_entries do |file, entries| + page = entries[page_num - 1] + data = file.read_entry page + if data + img = Image.new data, MIME.from_filename(page.filename), + page.filename, data.size + end + end + rescue e + Logger.warn "Unable to read page #{page_num} of #{@zip_path}. Error: #{e}" + end + img + end + + def page_dimensions + sizes = [] of Hash(String, Int32) + sorted_archive_entries do |file, entries| + entries.each_with_index do |e, i| + begin + data = file.read_entry(e).not_nil! + size = ImageSize.get data + sizes << { + "width" => size.width, + "height" => size.height, + } + rescue e + Logger.warn "Failed to read page #{i} of entry #{zip_path}. #{e}" + sizes << {"width" => 1000_i32, "height" => 1000_i32} + end + end + end + sizes + end + + def exists? : Bool + File.exists? @zip_path + end end diff --git a/src/library/title.cr b/src/library/title.cr index e3d79d5..eeef8e7 100644 --- a/src/library/title.cr +++ b/src/library/title.cr @@ -55,7 +55,7 @@ class Title next end if is_supported_file path - entry = Entry.new path, self + entry = ZippedEntry.new path, self @entries << entry if entry.pages > 0 || entry.err_msg end end @@ -127,12 +127,12 @@ class Title previous_entries_size = @entries.size @entries.select! do |entry| - existence = File.exists? entry.zip_path + existence = entry.exists? Fiber.yield context["deleted_entry_ids"] << entry.id unless existence existence end - remained_entry_zip_paths = @entries.map &.zip_path + remained_entry_zip_paths = @entries.map &.path is_titles_added = false is_entries_added = false @@ -162,7 +162,7 @@ class Title end if is_supported_file path next if remained_entry_zip_paths.includes? path - entry = Entry.new path, self + entry = ZippedEntry.new path, self if entry.pages > 0 || entry.err_msg @entries << entry is_entries_added = true @@ -627,7 +627,7 @@ class Title @entries.each do |e| next if da.has_key? e.title - da[e.title] = ctime e.zip_path + da[e.title] = ctime e.path end TitleInfo.new @dir do |info| diff --git a/src/routes/api.cr b/src/routes/api.cr index e664b28..03d6e53 100644 --- a/src/routes/api.cr +++ b/src/routes/api.cr @@ -40,7 +40,7 @@ struct APIRouter Koa.schema "entry", { "pages" => Int32, "mtime" => Int64, - }.merge(s %w(zip_path title size id title_id display_name cover_url)), + }.merge(s %w(path title size id title_id display_name cover_url)), desc: "An entry in a book" Koa.schema "title", { @@ -1138,7 +1138,7 @@ struct APIRouter entry = title.get_entry eid raise "Entry ID `#{eid}` of `#{title.title}` not found" if entry.nil? - file_hash = Digest::SHA1.hexdigest (entry.zip_path + entry.mtime.to_s) + file_hash = Digest::SHA1.hexdigest (entry.path + entry.mtime.to_s) e_tag = "W/#{file_hash}" if e_tag == prev_e_tag env.response.status_code = 304 @@ -1172,7 +1172,7 @@ struct APIRouter title = (Library.default.get_title env.params.url["tid"]).not_nil! entry = (title.get_entry env.params.url["eid"]).not_nil! - send_attachment env, entry.zip_path + send_attachment env, entry.path rescue e Logger.error e env.response.status_code = 404 diff --git a/src/routes/reader.cr b/src/routes/reader.cr index 40b86aa..052e212 100644 --- a/src/routes/reader.cr +++ b/src/routes/reader.cr @@ -53,6 +53,7 @@ struct ReaderRouter render "src/views/reader.html.ecr" rescue e Logger.error e + puts e.backtrace? env.response.status_code = 404 end end diff --git a/src/views/opds/title.xml.ecr b/src/views/opds/title.xml.ecr index b159687..1d82490 100644 --- a/src/views/opds/title.xml.ecr +++ b/src/views/opds/title.xml.ecr @@ -29,7 +29,7 @@ - + diff --git a/src/views/reader-error.html.ecr b/src/views/reader-error.html.ecr index 62a80fc..ad3580f 100644 --- a/src/views/reader-error.html.ecr +++ b/src/views/reader-error.html.ecr @@ -5,7 +5,7 @@

Error

-

<%= entry.zip_path %>

+

<%= entry.path %>

<%= entry.err_msg %>

diff --git a/src/views/reader.html.ecr b/src/views/reader.html.ecr index feac115..19e2b19 100644 --- a/src/views/reader.html.ecr +++ b/src/views/reader.html.ecr @@ -67,7 +67,7 @@

<%= entry.display_name %>

-

<%= entry.zip_path %>

+

<%= entry.path %>