From: Chris Jaekl
Date: Sat, 22 Jun 2024 22:51:39 +0000 (-0400)
Subject: Address the low-hanging-fruit RuboCop advisories
X-Git-Url: https://jaekl.net/gitweb/?a=commitdiff_plain;h=5cdb0d025521ac43a5d090d60c044d386d53b753;p=quanlib.git
Address the low-hanging-fruit RuboCop advisories
This leaves 50 "offenses" still to be examined. Most of those are complaints about code complexity, and will require some thought before attempting a refactor.
---
diff --git a/.rubocop.yml b/.rubocop.yml
new file mode 100644
index 0000000..9bbd52c
--- /dev/null
+++ b/.rubocop.yml
@@ -0,0 +1,17 @@
+AllCops:
+ TargetRubyVersion: 3.0
+
+Metrics/MethodLength:
+ Max: 25
+
+Style/StringLiterals:
+ EnforcedStyle: double_quotes
+
+Style/TrailingCommaInArrayLiteral:
+ EnforcedStyleForMultiline: comma
+
+Style/TrailingCommaInHashLiteral:
+ EnforcedStyleForMultiline: comma
+
+Style/StringLiteralsInInterpolation:
+ EnforcedStyle: double_quotes
diff --git a/Gemfile b/Gemfile
index 0181479..4f41955 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,9 +1,11 @@
-source 'http://rubygems.org'
+# frozen_string_literal: true
-gem 'inifile'
-gem 'mocha'
-gem 'nokogiri'
-gem 'pg'
-gem 'rspec'
-gem 'rubocop'
-gem 'rubyzip'
+source "http://rubygems.org"
+
+gem "inifile"
+gem "mocha"
+gem "nokogiri"
+gem "pg"
+gem "rspec"
+gem "rubocop"
+gem "rubyzip"
diff --git a/app/author.rb b/app/author.rb
index 781969c..716f5aa 100644
--- a/app/author.rb
+++ b/app/author.rb
@@ -1,18 +1,15 @@
# frozen_string_literal: true
+# Encapsulates information about an author
class Author
- attr_accessor :grouping
- attr_accessor :reading_order
- attr_accessor :sort_order
+ attr_accessor :grouping, :reading_order, :sort_order
def initialize(grouping, reading_order, sort_order)
@grouping = grouping
@reading_order = reading_order
@sort_order = sort_order
- if (nil == sort_order) || ('Unknown' == sort_order)
- @sort_order = reading_to_sort_order(reading_order)
- end
+ @sort_order = reading_to_sort_order(reading_order) if sort_order.nil? || sort_order == "Unknown"
end
def inspect
@@ -34,12 +31,9 @@ class Author
def reading_to_sort_order(reading_order)
sort_order = reading_order
- parts = reading_order.split(' ')
- if parts.length > 1
- sort_order = parts[-1] + ', ' + parts[0..-2].join(' ')
- end
+ parts = reading_order.split(" ")
+ sort_order = "#{parts[-1]}, #{parts[0..-2].join(" ")}" if parts.length > 1
- return sort_order
+ sort_order
end
end
-
diff --git a/app/book.rb b/app/book.rb
index c4d7070..3449b6d 100644
--- a/app/book.rb
+++ b/app/book.rb
@@ -1,106 +1,87 @@
+# frozen_string_literal: true
-require 'nokogiri'
-require 'rubygems'
-require 'zip'
+require "nokogiri"
+require "rubygems"
+require "zip"
-require_relative 'author'
-require_relative 'classification'
-require_relative 'cover'
-require_relative 'store'
+require_relative "author"
+require_relative "classification"
+require_relative "cover"
+require_relative "store"
+# Encapsulates info about a book in the library
class Book
- @@DC_NS_URL = 'http://purl.org/dc/elements/1.1/'
- @@SERIES_AND_VOLUME_REGEX = /^([A-Z]+)([0-9]+(\.[0-9]+)?)$/
-
- attr_accessor :arrived
- attr_accessor :author
- attr_accessor :classification_id
- attr_accessor :cover
- attr_accessor :description
- attr_accessor :language
- attr_accessor :path
- attr_accessor :series_id
- attr_accessor :title
- attr_accessor :volume
+ @@dc_ns_url = "http://purl.org/dc/elements/1.1/"
+ @@series_and_volume_regex = /^([A-Z]+)([0-9]+(\.[0-9]+)?)$/
+
+ attr_accessor(
+ :arrived,
+ :author,
+ :classification_id,
+ :cover,
+ :description,
+ :language,
+ :path,
+ :series_id,
+ :title,
+ :volume
+ )
def initialize(store)
@store = store
end
- def load_from_file!(fileName)
- @path = fileName
- parse_file_name!(fileName)
+ def load_from_file!(file_name)
+ @path = file_name
+ parse_file_name!(file_name)
end
- def self.can_handle?(fileName)
- if nil == fileName
- return false
- end
+ def self.can_handle?(file_name)
+ return false if file_name.nil?
- #puts "Filename: " + fileName.to_s
- lowerName = fileName.downcase()
+ lower_name = file_name.downcase
- if lowerName.end_with?(".epub")
- return true
- end
+ return true if lower_name.end_with?(".epub")
- if lowerName.end_with?(".pdf")
- return true
- end
+ return true if lower_name.end_with?(".pdf")
- return false
+ false
end
def self.grouping_for_title(title)
result = title
- '\'",!#'.split('').each do |c|
- result = result.gsub(c, '-')
+ "'\",!#".split("").each do |c|
+ result = result.gsub(c, "-")
end
- result = result.gsub(/: */, '--')
- result = result.gsub(' ', '_')
- result
+ result.gsub(/: */, "--").gsub(" ", "_")
end
def heading
result = []
- if nil != @title
- result.push('' + @title + '')
+ if @title.nil?
+ result.push("(Unknown title)")
else
- result.push('(Unknown title)')
- end
- if nil != @author
- result.push('by ' + @author.reading_order + '')
+ result.push("#{@title}")
end
+ result.push("by #{@author.reading_order}") unless @author.nil?
- seriesInfo = []
+ series_info = []
series = @store.load_series(@series_id)
- if nil != series and nil != series.descr
- seriesInfo.push(series.descr.to_s)
- end
- if nil != @volume
- seriesInfo.push(@volume.to_s)
- end
- if seriesInfo.length > 0
- result.push(seriesInfo.join(' '))
- end
+ series_info.push(series.descr.to_s) if series&.descr
+ series_info.push(@volume.to_s) unless @volume.nil?
+ result.push(series_info.join(" ")) unless series_info.empty?
classification = nil
- if nil != @classification_id
- classification = @store.load_classification(@classification_id)
- end
- if nil != classification
- if nil != classification.ddc
- result.push('Dewey: ' + classification.ddc.to_s)
- end
- if nil != classification.lcc
- result.push('LCC: ' + classification.lcc.to_s)
- end
+ classification = @store.load_classification(@classification_id) unless @classification_id.nil?
+ unless classification.nil?
+ result.push("Dewey: #{classification.ddc}") unless classification.ddc.nil?
+ result.push("LCC: #{classification.lcc}") unless classification.lcc.nil?
end
- return result.join('
')
+ result.join("
")
end
def inspect
@@ -113,35 +94,31 @@ class Book
path.nil? ? nil : "path=\"#{path.inspect}",
].compact.join(" ")
- return "(Book: #{field_info})"
+ "(Book: #{field_info})"
end
def to_s
- return inspect()
+ inspect
end
def title_grouping
return if path.nil?
- File.basename(@path, '.*')
+ File.basename(@path, ".*")
end
private
- def isUpper?(c)
- /[[:upper:]]/.match(c)
+ def upper?(character)
+ /[[:upper:]]/.match(character)
end
def massage_author(input)
- if nil == input
- return nil
- end
+ return if input.nil?
reading_order = ""
input.each_char do |c|
- if isUpper?(c) and (reading_order.length > 0)
- reading_order += " "
- end
+ reading_order += " " if upper?(c) && !reading_order.empty?
reading_order += c
end
@@ -149,50 +126,48 @@ class Book
end
# Returns (series, volumeNo, titleText)
- def processTitle(input)
- if nil == input
- return nil
- end
+ def process_title(input)
+ return if input.nil?
- arr = input.split('_')
+ arr = input.split("_")
series = nil
vol = nil
first = arr[0]
- matchData = (first).match(@@SERIES_AND_VOLUME_REGEX)
- if nil != matchData
- capt = matchData.captures
+ match_data = first.match(@@series_and_volume_regex)
+ unless match_data.nil?
+ capt = match_data.captures
series = capt[0]
vol = capt[1]
arr.shift
end
- pos = arr[-1].rindex('.')
- if nil != pos
- arr[-1] = arr[-1].slice(0, pos)
- end
+ pos = arr[-1].rindex(".")
+ arr[-1] = arr[-1].slice(0, pos) unless pos.nil?
- title = arr.join(' ')
+ title = arr.join(" ")
- bare_title_grouping = title_grouping
- .split('_')
- .reject { |part| part.match(@@SERIES_AND_VOLUME_REGEX) }
- .join('_')
+ bare_title_grouping =
+ title_grouping
+ .split("_")
+ .reject { |part| part.match(@@series_and_volume_regex) }
+ .join("_")
unless bare_title_grouping == Book.grouping_for_title(title)
- puts "WARNING: title_grouping mismatch: #{bare_title_grouping.inspect} vs. #{Book.grouping_for_title(title).inspect}"
+ discrepancy = "#{bare_title_grouping.inspect} vs. #{Book.grouping_for_title(title).inspect}"
+ puts "WARNING: title_grouping mismatch: #{discrepancy}"
end
- return series, vol, title
+ [series, vol, title]
end
def parse_file_name!(file_name)
- category = nil # e.g., non-fiction, fan-fiction
- grouping = ''
+ category = nil # e.g., non-fiction, fan-fiction
+ grouping = ""
- parts = file_name.split('/')
- (series_code, @volume, @title) = processTitle(parts[-1])
+ parts = file_name.split("/")
+ (series_code, @volume, @title) = process_title(parts[-1])
if parts.length > 1
grouping = parts[-2]
reading_order = massage_author(grouping)
@@ -200,72 +175,68 @@ class Book
@author = Author.new(grouping, reading_order, sort_order)
@series_id = @store.get_series(grouping, series_code)
end
- if parts.length > 2
- category = parts[-3]
- end
+ category = parts[-3] if parts.length > 2
lc_file_name = file_name.downcase
if lc_file_name.end_with?(".epub")
- scanEpub!(file_name)
+ scan_epub!(file_name)
elsif lc_file_name.end_with?(".pdf")
scan_pdf!(file_name)
end
@arrived = File.ctime(file_name)
- @classification_id = @store.find_classification(@author.grouping, File.basename(file_name, '.*'))
+ @classification_id = @store.find_classification(@author.grouping, File.basename(file_name, ".*"))
# TODO: Fix horrible hard-coded strings and paths
- if ('01_nonfic' == category) && (nil == classification_id)
- open(Store.unclassified_csv, 'a') do |fd|
- fd.puts('"' + grouping.to_s + '","' + path + '"')
- end
+ return unless category == "00_nonFic" && classification_id.nil?
+
+ File.open(Store.unclassified_csv, "a") do |fd|
+ fd.puts "#{grouping.inspect},#{path.inspect}"
end
end
- def scanEpub!(fileName)
- #puts 'Scanning "' + fileName.to_s + '"...'
- begin
- Zip.warn_invalid_date = false
- Zip::File.open(fileName) do |zipfile|
- entry = zipfile.find_entry('META-INF/container.xml')
- if nil == entry
- puts 'No META-INF/container.xml, skipping book ' + fileName
- return
- end
- contXml = zipfile.read('META-INF/container.xml')
- contDoc = Nokogiri::XML(contXml)
- opfPath = contDoc.css("container rootfiles rootfile")[0]['full-path']
-
- scanOpf!(zipfile, opfPath)
+ def scan_epub!(file_name)
+ Zip.warn_invalid_date = false
+ Zip::File.open(file_name) do |zipfile|
+ entry = zipfile.find_entry("META-INF/container.xml")
+ if entry.nil?
+ puts "No META-INF/container.xml, skipping book #{file_name.inspect}"
+ return nil
end
- rescue Zip::Error => exc
- puts 'ERROR processing file "' + fileName + '":'
- puts exc.message
- puts exc.backtrace
+ cont_xml = zipfile.read("META-INF/container.xml")
+ cont_doc = Nokogiri::XML(cont_xml)
+ opf_path = cont_doc.css("container rootfiles rootfile")[0]["full-path"]
+
+ scan_opf!(zipfile, opf_path)
+ rescue Zip::Error => e
+ puts "ERROR processing file #{file_name.inspect}:"
+ puts e.message
+ puts e.backtrace
end
end
def scan_pdf!(file_name)
pdf_path = File.expand_path(file_name).to_s
- if ! pdf_path.end_with?('.pdf')
- puts 'Unexpected internal error: path "' + file_name.to_s + '" does not end with ".pdf".'
+ unless pdf_path.end_with?(".pdf")
+ puts "Unexpected internal error: path #{file_name.inspect} does not end with \".pdf\"."
return
end
- jpeg_path = pdf_path[0..-5] + '.jpeg'
- if File.file?(jpeg_path)
- File.open(jpeg_path, 'r') do |is|
- @cover = Cover.new(is, jpeg_path, 'image/jpeg')
- end
+ jpeg_path = "#{pdf_path[0..-5]}.jpeg"
+
+ return unless File.file?(jpeg_path)
+
+ File.open(jpeg_path, "r") do |is|
+ @cover = Cover.new(is, jpeg_path, "image/jpeg")
end
end
- def scanOpf!(zipfile, opfPath)
- coverId = nil
+ def scan_opf!(zipfile, opf_path)
+ cover_id = nil
- opfXml = zipfile.read(opfPath)
- opfDoc = Nokogiri::XML(opfXml)
+ opf_xml = zipfile.read(opf_path)
+ opf_doc = Nokogiri::XML(opf_xml)
#-------
# Author
@@ -274,122 +245,111 @@ class Book
reading_order = @author.reading_order
sort_order = @author.sort_order
- creators = opfDoc.css('dc|creator', 'dc' => @@DC_NS_URL)
- if (creators.length > 0)
+ creators = opf_doc.css("dc|creator", "dc" => @@dc_ns_url)
+ unless creators.empty?
creator = creators[0]
- if nil != creator
- role = creator['opf:role']
- if 'aut' == role
- reading_order = creator.content
-
- file_as = creator['opf:file-as']
- if nil != file_as
- sort_order = file_as
- end
- end
- @author = Author.new(grouping, reading_order, sort_order)
+ return if creator.nil?
+
+ role = creator["opf:role"]
+ if role == "aut"
+ reading_order = creator.content
+
+ file_as = creator["opf:file-as"]
+ sort_order = file_as unless file_as.nil?
end
+
+ @author = Author.new(grouping, reading_order, sort_order)
end
#---------------------------------------
# Title
- titles = opfDoc.css('dc|title', 'dc' => @@DC_NS_URL)
- if titles.length > 0
+ titles = opf_doc.css("dc|title", "dc" => @@dc_ns_url)
+ unless titles.empty?
title = titles[0]
- if nil != title
- @title = title.content
- end
+ @title = title.content unless title.nil?
end
#---------------------------------------
# Description
- descrNodes = opfDoc.css('dc|description', 'dc' => @@DC_NS_URL)
- if (descrNodes.length > 0)
- descrNode = descrNodes[0]
- if nil != descrNode
- @description = descrNode.content
- end
+ descr_nodes = opf_doc.css("dc|description", "dc" => @@dc_ns_url)
+ unless descr_nodes.empty?
+ descr_node = descr_nodes[0]
+ @description = descr_node.content unless descr_node.nil?
end
#---------------------------------------
# Language
- langNodes = opfDoc.css('dc|language', 'dc' => @@DC_NS_URL)
- if (langNodes.length > 0)
- langNode = langNodes[0]
- if langNode
- @language = langNode.content
- end
+ lang_nodes = opf_doc.css("dc|language", "dc" => @@dc_ns_url)
+ unless lang_nodes.empty?
+ lang_node = lang_nodes[0]
+ @language = lang_node.content if lang_node
end
#---------------------------------------
# Other metadata: series, volume, cover
- metas = opfDoc.css('package metadata meta')
- for m in metas
- name = m['name']
- content = m['content']
+ metas = opf_doc.css("package metadata meta")
+ metas.each do |m|
+ name = m["name"]
+ content = m["content"]
- if 'calibre:series' == name
+ case name
+ when "calibre:series"
# TODO: Dynamically create a new series?
# @series_id = content
- elsif 'calibre:series-index' == name
+ when "calibre:series-index"
@volume = content
- elsif 'cover' == name
- coverId = content
- #puts 'File ' + @path + ' coverId ' + coverId
+ when "cover"
+ cover_id = content
end
end
#---------------
# Load the cover
- @cover = load_cover(zipfile, opfPath, opfDoc, coverId)
+ @cover = load_cover(zipfile, opf_path, opf_doc, cover_id)
end
- def load_cover(zipfile, opfPath, opfDoc, coverId)
- if nil == coverId
- coverId = "cover-image"
- end
+ def load_cover(zipfile, opf_path, opf_doc, cover_id)
+ cover_id = "cover-image" if cover_id.nil?
- items = opfDoc.css('package manifest item')
- for i in items
- href = i['href']
- id = i['id']
- mimeType = i['media-type']
-
- if coverId == id
- entry = zipfile.find_entry(href)
-
- if nil == entry
- # Although the epub standard requires the path to be relative
- # to the base of the epub (zip), some books encountered in the
- # wild have been found to use a bath relative to the location
- # of the opf file.
- parts = opfPath.split('/')
- opfBasePath = parts[0..-2].join('/')
- coverPath = opfBasePath + '/' + href
- entry = zipfile.find_entry(coverPath)
- end
+ items = opf_doc.css("package manifest item")
+ items.each do |i|
+ href = i["href"]
+ id = i["id"]
+ mime_type = i["media-type"]
- unless entry
- # Another case found in the wild: cover image is at the root, but path is '../cover.jpeg'
- if href.start_with? '../'
- coverPath = href[3..-1]
- entry = zipfile.find_entry(coverPath)
- end
- end
+ next unless cover_id == id
- if nil == entry
- puts 'WARNING! Cover image "' + href + '" not found in file "' + @path + '".'
- return nil
- else
- entry.get_input_stream() do |is|
- return Cover.new(is, href, mimeType)
- end
+ entry = zipfile.find_entry(href)
+
+ if entry.nil?
+ # Although the epub standard requires the path to be relative
+ # to the base of the epub (zip), some books encountered in the
+ # wild have been found to use a bath relative to the location
+ # of the opf file.
+ parts = opf_path.split("/")
+ opf_base_path = parts[0..-2].join("/")
+ cover_path = "#{opf_base_path}/#{href}"
+ entry = zipfile.find_entry(cover_path)
+ end
+
+ if !entry && href.start_with?("../")
+ # Another case found in the wild: cover image is at the root, but path is '../cover.jpeg'
+ cover_path = href[3..]
+ entry = zipfile.find_entry(cover_path)
+ end
+
+ if entry.nil?
+ puts "WARNING! Cover image #{href.inspect} not found in file #{@path.inspect}."
+ return nil
+ else
+ entry.get_input_stream do |is|
+ return Cover.new(is, href, mime_type)
end
end
end
@@ -397,4 +357,3 @@ class Book
nil
end
end
-
diff --git a/app/book_loader.rb b/app/book_loader.rb
index 07eb853..ca963ff 100644
--- a/app/book_loader.rb
+++ b/app/book_loader.rb
@@ -1,10 +1,11 @@
# frozen_string_literal: true
-require_relative 'book'
-require_relative 'store'
+require_relative "book"
+require_relative "store"
-class BookLoader
- DONE_MARKER = ''
+# Worker thread that pulls filenames from a queue and loads them as new books
+class BookLoader
+ DONE_MARKER = ""
def initialize(config_file, queue)
@config_file = config_file
@@ -13,10 +14,11 @@ class BookLoader
def run
@store = Store.new(@config_file)
- @store.connect()
+ @store.connect
file = @queue.pop
- until file == DONE_MARKER do
+
+ until file == DONE_MARKER
book = Book.new(@store)
book.load_from_file!(file)
@store.store_book(book)
@@ -24,6 +26,6 @@ class BookLoader
file = @queue.pop
end
- @store.disconnect()
+ @store.disconnect
end
end
diff --git a/app/classification.rb b/app/classification.rb
index 5eb6f7f..c9055a1 100644
--- a/app/classification.rb
+++ b/app/classification.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+# Encapsulates information about a book's classification, under the
+# Dewey Decimal and/or Library of Congress systems.
class Classification
attr_accessor :id, :ddc, :lcc, :author_grouping, :author, :title_grouping, :title
@@ -23,7 +25,7 @@ class Classification
title.nil? ? nil : "title=#{title.inspect}",
].compact.join(", ")
- return "(Classification: #{field_info})"
+ "(Classification: #{field_info})"
end
def to_s
@@ -35,12 +37,10 @@ class Classification
def reading_to_sort_order(reading_order)
sort_order = reading_order
- parts = reading_order.split(' ')
- if parts.length > 1
- sort_order = parts[-1] + ', ' + parts[0..-2].join(' ')
- end
+ parts = reading_order.split(" ")
- return sort_order
+ sort_order = [parts[-1], parts[0..-2].join(" ")].join(", ") if parts.length > 1
+
+ sort_order
end
end
-
diff --git a/app/cover.rb b/app/cover.rb
index e3dbad4..ba3c570 100644
--- a/app/cover.rb
+++ b/app/cover.rb
@@ -1,55 +1,46 @@
# frozen_string_literal: true
+# Encapsulates information about a book cover
class Cover
attr_reader :path
- def initialize(inputStream, path, mimeType)
- if nil != inputStream
- @data = inputStream.read
- else
- @data = nil
- end
+ def initialize(input_stream, path, mime_type)
+ @data =
+ if input_stream.nil?
+ nil
+ else
+ input_stream.read
+ end
+
@path = path
- @mimeType = mimeType
+ @mime_type = mime_type
end
def inspect
field_info = [
- @data.nil? ? nil : "size=#{@data.length.to_s}",
+ @data.nil? ? nil : "size=#{@data.length}",
@path.nil? ? nil : "path=#{@path.inspect}",
- @mimeType.nil? ? nil : "mime_type=#{@mimeType.inspect}",
+ @mime_type.nil? ? nil : "mime_type=#{@mime_type.inspect}",
].compact.join(" ")
"(Cover: #{field_info})"
end
def read_image(filename)
- open(filename, 'rb') do |fd|
- @data = fd.read()
+ File.open(filename, "rb") do |fd|
+ @data = fd.read
end
end
def to_s
- return inspect
+ inspect
end
- def write_image(outputDir, filename)
- open(outputDir + '/' + filename, 'wb') do |fd|
+ def write_image(output_dir, filename)
+ File.open("#{output_dir}/#{filename}", "wb") do |fd|
fd.write(@data)
end
- return filename, @mimeType
- end
-
- private
-
- def getExt
- pos = @path.rindex('.')
- if nil == pos
- return '.img'
- end
-
- @path.slice(pos, @path.length)
+ [filename, @mime_type]
end
end
-
diff --git a/app/extract.rb b/app/extract.rb
index c695941..13854ea 100644
--- a/app/extract.rb
+++ b/app/extract.rb
@@ -1,50 +1,44 @@
-require 'find'
-require 'pathname'
+# frozen_string_literal: true
+
+require "find"
+require "pathname"
def exec(cmdline)
puts "$ #{cmdline}"
result = system(cmdline)
- unless result
- puts "FAILED: #{cmdline}"
- end
+ puts "FAILED: #{cmdline}" unless result
result
end
def extract_epub(source_file, source_path, dest_path)
- relative_path = source_file[source_path.length .. source_file.length]
+ relative_path = source_file[source_path.length..source_file.length]
dest_file = "#{dest_path}/#{relative_path}"
- dest_file = dest_file[0 .. (dest_file.length - 6)] + ".txt"
+ dest_file = "#{dest_file[0..(dest_file.length - 6)]}.txt"
required_path = Pathname(dest_file).dirname
- unless File.directory? required_path
- unless exec("mkdir -p #{required_path}")
- return false
- end
- end
+
+ return false unless File.directory?(required_path) || exec("mkdir -p #{required_path}")
if File.exist? dest_file
source_time = File.mtime source_file
dest_time = File.mtime dest_file
comp = dest_time <=> source_time
- if comp > 0
- return true # Nothing to do, extraction is already up-to-date
- end
+ return true if comp.positive? # Nothing to do, extraction is already up-to-date
end
-
+
exec("ebook-convert #{source_file} #{dest_file}")
end
-def scan_dir(source_path, dest_path)
+def scan_dir(source_path, dest_path)
Find.find(source_path) do |f|
- if f.match(/.epub\Z/)
- unless (f.match(/_bis.epub\Z/) || f.match(/_ter.epub\Z/) || f.match(/_quater.epub\Z/))
- extract_epub(f, source_path, dest_path)
- end
- end
+ next unless f.match(/.epub\Z/)
+ next if f.match(/_bis.epub\Z/) || f.match(/_ter.epub\Z/) || f.match(/_quater.epub\Z/)
+
+ extract_epub(f, source_path, dest_path)
end
end
dest_path = ARGV[0]
-for arg in ARGV[1 .. ARGV.length]
+ARGV[1..ARGV.length].each do |arg|
scan_dir(arg, dest_path)
end
diff --git a/app/main.rb b/app/main.rb
index 1eee8b4..abc4601 100644
--- a/app/main.rb
+++ b/app/main.rb
@@ -1,58 +1,55 @@
# frozen_string_literal: true
-require_relative 'navigator'
-require_relative 'page'
-require_relative 'store'
-require_relative 'walk_dir'
+require_relative "navigator"
+require_relative "page"
+require_relative "store"
+require_relative "walk_dir"
-@outputDir = 'output'
+@output_dir = "output"
-@config_file = 'quanlib.ini'
+@config_file = "quanlib.ini"
@skip_class = false
-def handleArg(arg)
+def handle_arg(arg)
if arg.start_with?("--config=")
- @config_file = arg[9..-1]
- puts 'Using config file "' + @config_file + '".'
- elsif "--purge" == arg
- puts 'Purging database...'
- @store.dropSchema()
- if File.exists?(@store.unclassified_csv)
- File.delete(@store.unclassified_csv)
- end
- elsif "--skip-class" == arg
- puts 'Skipping load of classification table.'
+ @config_file = arg[9..]
+ puts "Using config file #{@config_file.inspect}."
+ elsif arg == "--purge"
+ puts "Purging database..."
+ @store.drop_schema
+ File.delete(@store.unclassified_csv) if File.exist?(@store.unclassified_csv)
+ elsif arg == "--skip-class"
+ puts "Skipping load of classification table."
@skip_class = true
elsif arg.start_with?("--")
- abort('ERROR: Unrecognized option "' + arg + '".')
+ abort("ERROR: Unrecognized option #{arg.inspect}.")
end
end
@store = Store.new(@config_file)
-@store.connect()
+@store.connect
-for arg in ARGV
- handleArg(arg)
+ARGV.each do |arg|
+ handle_arg(arg)
end
@store.init_db(@skip_class)
-for arg in ARGV
- if ! arg.start_with?("--")
- puts 'Scanning directory "' + arg + '"...'
- w = WalkDir.new(@config_file, arg)
- w.books
- end
+ARGV.each do |arg|
+ next if arg.start_with?("--")
+
+ puts "Scanning directory #{arg.inspect}..."
+ w = WalkDir.new(@config_file, arg)
+ w.books
end
@store.cross_reference_lists
-puts 'Creating output...'
+puts "Creating output..."
navigator = Navigator.new(@store)
-navigator.write_atoz_pages()
-navigator.write_series_listing()
-navigator.write_dewey()
-
-@store.disconnect()
+navigator.write_atoz_pages
+navigator.write_series_listing
+navigator.write_dewey
+@store.disconnect
diff --git a/app/navigator.rb b/app/navigator.rb
index 881b1fa..df0f4e1 100644
--- a/app/navigator.rb
+++ b/app/navigator.rb
@@ -1,6 +1,9 @@
-require_relative 'page'
-require_relative 'store'
+# frozen_string_literal: true
+require_relative "page"
+require_relative "store"
+
+# Generates navigation pages for the static HTML output of the library index
class Navigator
def initialize(store)
@store = store
@@ -9,55 +12,52 @@ class Navigator
def write_atoz_pages
atoz_counts = {}
- ('A'..'Z').each do |letter|
+ ("A".."Z").each do |letter|
atoz_counts[letter] = write_authors_starting_with(letter)
end
- content = 'Author | Books |
'
- ('A'..'Z').each do |letter|
- content += ' Starting with ' + letter + ' | ' + atoz_counts[letter].to_s + ' |
'
+ content = "Author | Books |
"
+ ("A".."Z").each do |letter|
+ link = "Starting with #{letter}"
+ content += " #{link} | #{atoz_counts[letter]} |
"
end
- content += '
'
+ content += "
"
page = Page.new(@store)
- page.output_dir = 'atoz'
+ page.output_dir = "atoz"
page.special = content
- page.up = ['../output/index.html', 'Up']
+ page.up = ["../output/index.html", "Up"]
- page.write_html( [] )
+ page.write_html([])
end
def write_authors_starting_with(letter)
- book_ids = @store.query_books_by_author(letter + '%')
- puts 'Authors starting with "' + letter + '": ' + book_ids.length.to_s() + ' books.'
+ book_ids = @store.query_books_by_author("#{letter}%")
+ puts "Authors starting with #{letter.inspect}: #{book_ids.length} books."
page = Page.new(@store)
- if 'A' != letter
- page.back = ['../atoz/output_' + (letter.ord - 1).chr + '.html', 'Prev']
- end
- if 'Z' != letter
- page.forward = ['../atoz/output_' + (letter.ord + 1).chr + '.html', 'Next']
- end
- page.output_dir = 'atoz'
- page.index_file = 'output_' + letter + '.html'
- page.title = "Authors starting with '" + letter + "'"
- page.up = ['../atoz/index.html', 'Up']
+ page.back = ["../atoz/output_#{(letter.ord - 1).chr}.html", "Prev"] if letter != "A"
+ page.forward = ["../atoz/output_#{(letter.ord + 1).chr}.html", "Next"] if letter != "Z"
+ page.output_dir = "atoz"
+ page.index_file = "output_#{letter}.html"
+ page.title = "Authors starting with #{letter.inspect}"
+ page.up = ["../atoz/index.html", "Up"]
page.write_html(book_ids)
- return book_ids.length
+ book_ids.length
end
def write_dewey
- book_ids = @store.query_books_by_ddc()
- puts 'Non-fiction books arranged by Dewey Decimal Classification: ' + book_ids.length.to_s() + ' books.'
+ book_ids = @store.query_books_by_ddc
+ puts "Non-fiction books arranged by Dewey Decimal Classification: #{book_ids.length} books."
page = Page.new(@store)
- page.output_dir = 'ddc'
- page.index_file = 'index.html'
+ page.output_dir = "ddc"
+ page.index_file = "index.html"
page.title = "Non-fiction books arranged by Dewey Decimal call number"
- page.up = ['../output/index.html', 'Up']
-
+ page.up = ["../output/index.html", "Up"]
+
page.write_html(book_ids)
- return book_ids.length
+ book_ids.length
end
def write_series_for_age(age)
@@ -68,45 +68,33 @@ class Navigator
series_ids.each do |id|
series = @store.load_series(id)
book_ids = @store.query_books_by_series_id(id)
- if nil != book_ids and book_ids.length > 0
- series_infos.push( [series, book_ids] )
- end
+ series_infos.push([series, book_ids]) unless book_ids&.empty?
end
- for idx in 0 .. (series_infos.length - 1) do
- #puts series.descr + ': ' + book_ids.length.to_s + ' books.'
-
+ 0..(series_infos.length - 1).each do |idx|
back = nil
fwd = nil
- if idx > 0
- back = series_infos[idx-1]
- end
- if (idx + 1) < series_infos.length
- fwd = series_infos[idx+1]
- end
+ back = series_infos[idx - 1] if idx.positive?
+ fwd = series_infos[idx + 1] if (idx + 1) < series_infos.length
cur = series_infos[idx]
series = cur[0]
book_ids = cur[1]
page = Page.new(@store)
- if nil != back
- page.back = [back[0].key + '.html', 'Back']
- end
- if nil != fwd
- page.forward = [fwd[0].key + '.html', 'Forward']
- end
- page.output_dir = 'series/series_' + age
- page.index_file = series.key + '.html'
- page.title = 'Series “' + series.descr + '” (' + book_ids.length.to_s + ' books)'
- page.up = ['index.html', 'Up']
-
+ page.back = ["#{back[0].key}.html", "Back"] unless back.nil?
+ page.forward = ["#{fwd[0].key}.html", "Forward"] unless fwd.nil?
+ page.output_dir = "series/series_#{age}"
+ page.index_file = "#{series.key}.html"
+ page.title = "Series “#{series.descr}” (#{book_ids.length} books)"
+ page.up = ["index.html", "Up"]
+
page.write_html(book_ids)
end
- content = '“' + age + '” Series
'
- content += 'Author | Series | Genre | Books |
'
+ content = "“#{age}” Series
"
+ content += "Author | Series | Genre | Books |
"
series_infos.each do |cur|
series = cur[0]
book_ids = cur[1]
@@ -114,44 +102,44 @@ class Navigator
author = series.grouping
letter = author[0]
- content += ' '
- content += '' + author + ' | '
- content += '' + series.descr + ' | '
- content += '' + series.genre + ' | '
- content += '' + book_ids.length.to_s + ' | '
- content += '
'
+ content += " "
+ content += "#{author} | "
+ content += "#{series.descr} | "
+ content += "#{series.genre} | "
+ content += "#{book_ids.length} | "
+ content += "
"
end
- content += '
'
+ content += "
"
page = Page.new(@store)
- page.output_dir = 'series/series_' + age
+ page.output_dir = "series/series_#{age}"
page.special = content
- page.up = ['../index.html', 'Up']
- page.write_html( [] )
+ page.up = ["../index.html", "Up"]
+ page.write_html([])
- return series_infos.length
+ series_infos.length
end
def write_series_listing
- ages = ['beginner', 'junior', 'ya', 'adult']
+ ages = %w[beginner junior ya adult]
series_counts = {}
ages.each do |age|
- puts 'Series for "' + age + '" readers...'
+ puts "Series for #{age.inspect} readers..."
series_counts[age] = write_series_for_age(age)
end
- content = 'Browse Books By Series
'
- content += ''
- content += '
Age | Number of Series |
'
+ content = "Browse Books By Series
"
+ content += ""
+ content += "
Age | Number of Series |
"
ages.each do |age|
- content += '' + age + ' | ' + series_counts[age].to_s + ' |
'
+ content += "#{age} | #{series_counts[age]} |
"
end
- content += '
'
+ content += "
"
page = Page.new(@store)
- page.output_dir = 'series'
+ page.output_dir = "series"
page.special = content
- page.up = ['../output/index.html', 'Up']
- page.write_html( [] )
+ page.up = ["../output/index.html", "Up"]
+ page.write_html([])
end
end
diff --git a/app/page.rb b/app/page.rb
index 638f9ad..2d78d05 100644
--- a/app/page.rb
+++ b/app/page.rb
@@ -1,166 +1,138 @@
-require 'fileutils'
+# frozen_string_literal: true
-require_relative 'store'
+require "fileutils"
+require_relative "store"
+
+# Handles producing a page of output for the Navigator
class Page
+ attr_accessor :back, :forward, :index_file, :output_dir, :special, :title, :up
+
def initialize(store)
@back = nil
@forward = nil
- @index_file = 'index.html'
- @output_dir = 'output'
+ @index_file = "index.html"
+ @output_dir = "output"
@special = nil
@store = store
- @title = 'Books'
+ @title = "Books"
@up = nil
end
- def back=(value)
- @back = value
- end
-
- def forward=(value)
- @forward = value
- end
-
- def index_file=(value)
- @index_file = value
- end
+ def write_books(file_descriptor, book_ids)
+ book_ids.each do |id|
+ book = @store.load_book(id)
+ image =
+ if book.cover.nil?
+ "(No cover image)"
+ else
+ ""
+ end
+
+ file_descriptor.puts " "
+ file_descriptor.puts " #{image} | "
+
+ heading = book.heading
+ description = book.description
+ if description.nil?
+ file_descriptor.puts " #{heading} |
"
+ else
+ pop_inner = "#{heading}
#{description}
"
+ file_descriptor.puts " | "
+ end
- def navig_link(data)
- if (nil == data)
- return ''
+ file_descriptor.puts "
"
end
- return '' + data[1] + ''
- end
-
- def output_dir=(value)
- @output_dir = value
- end
-
- def special=(value)
- @special = value
end
- def title=(value)
- @title = value
- end
+ private
- def up=(value)
- @up = value
+ def navig_back_up_forward
+ [navig_link(@back), navig_link(@up), navig_link(@forward)].join(" ")
end
- def write_books(fd, book_ids)
- for id in book_ids
- book = @store.load_book(id)
- image = nil
- if nil != book.cover
- #@imageCount += 1
- #(path, mimeType) = book.cover.write_image(@output_dir, 'image' + @imageCount.to_s)
- #image = ''
- path = book.cover.path
- image = ''
- else
- image = '(No cover image)'
- end
-
- fd.puts ' '
- fd.puts ' ' + image + ' | '
+ def navig_link(data)
+ return "" if data.nil?
- heading = book.heading()
- description = book.description()
- if nil != description
- fd.puts ' |
'
- else
- fd.puts ' ' + heading + ' | '
- end
-
- fd.puts '
'
- end
+ "#{data[1]}"
end
- def write_footer(fd)
- fd.puts ' ' + navig_link(@back) + ' ' + navig_link(@up) + ' ' + navig_link(@forward) + '
'
+ def write_footer(file_descriptor)
+ file_descriptor.puts " #{navig_back_up_forward}
"
end
- def write_header(fd)
- fd.puts ' '
+ def write_header(file_descriptor)
+ file_descriptor.puts " "
- fd.puts ' ' + navig_link(@back) + ' ' + navig_link(@up) + ' ' + navig_link(@forward) + '
'
+ file_descriptor.puts " #{navig_back_up_forward}
"
end
def write_html(book_ids)
- @imageCount = 0
+ FileUtils.mkdir_p(@output_dir) unless Dir.exist?(@output_dir)
- if ! Dir.exist?(@output_dir)
- FileUtils.mkdir_p(@output_dir)
- end
+ File.open("#{@output_dir}/#{@index_file}", "w") do |file_descriptor|
+ file_descriptor.puts ""
+ file_descriptor.puts " "
+ file_descriptor.puts " "
+ file_descriptor.puts " #{@title}"
- open(@output_dir + '/' + @index_file, 'w') do |fd|
- fd.puts ''
- fd.puts ' '
- fd.puts ' '
- fd.puts ' ' + @title + ''
+ write_style_sheet(file_descriptor)
- write_style_sheet(fd)
+ file_descriptor.puts " "
+ file_descriptor.puts " "
- fd.puts ' '
- fd.puts ' '
-
- write_header(fd)
+ write_header(file_descriptor)
- write_special(fd)
- write_books(fd, book_ids)
-
- write_footer(fd)
+ write_special(file_descriptor)
+ write_books(file_descriptor, book_ids)
- fd.puts " "
- fd.puts ""
+ write_footer(file_descriptor)
+
+ file_descriptor.puts " "
+ file_descriptor.puts ""
end
end
- def write_special(fd)
- if (nil != @special)
- fd.puts(@special)
- end
+ def write_special(file_descriptor)
+ file_descriptor.puts(@special) unless @special.nil?
end
- def write_style_sheet(fd)
- style =
-<
- div {
- display: inline-block;
- width: 400px;
- margin: 10px;
- border 3px solid #73ad21;
- }
- h1.header {
- background: #4040a0;
- color: #ffffff;
- text-align: center;
- }
- img.cover-thumb { max-height: 200px; max-width: 200px; }
- p.navigator { }
- span.popup { }
- span.popup:hover { text-decoration: none; background: #cfffff; z-index: 6; }
- span.popup span.pop-inner {
- border-color:black;
- border-style:solid;
- border-width:1px;
- display: none;
- margin: 4px 0 0 0px;
- padding: 3px 3px 3px 3px;
- position: absolute;
- }
- span.popup:hover span.pop-inner {
- background: #ffffaf;
- display: block;
- margin: 20px 0 0 0px;
- z-index:6;
- }
-
-EOS
- fd.puts style
+ def write_style_sheet(file_descriptor)
+ style = <<~END_STYLE
+
+ END_STYLE
+
+ file_descriptor.puts style
end
end
-
diff --git a/app/series.rb b/app/series.rb
index 8661704..418c70e 100644
--- a/app/series.rb
+++ b/app/series.rb
@@ -1,17 +1,15 @@
+# frozen_string_literal: true
+# Encapsulates data about a series of books
class Series
attr_reader :id
- attr_accessor :age
- attr_accessor :code
- attr_accessor :descr
- attr_accessor :genre
- attr_accessor :grouping
+ attr_accessor :age, :code, :descr, :genre, :grouping
def initialize(id)
@age = nil
@genre = nil
- @grouping = nil
+ @grouping = nil
@code = nil
@descr = nil
@id = id
@@ -30,9 +28,7 @@ class Series
end
def key
- if nil != grouping and nil != code
- return grouping.to_s + '_' + code.to_s
- end
+ return "#{grouping}_#{code}" if !grouping.nil? && !code.nil?
id.to_s
end
@@ -41,4 +37,3 @@ class Series
inspect
end
end
-
diff --git a/app/store.rb b/app/store.rb
index 2ee597a..e87ca09 100644
--- a/app/store.rb
+++ b/app/store.rb
@@ -1,52 +1,59 @@
# frozen_string_literal: true
-require 'csv'
-require 'fileutils'
-require 'inifile'
-require 'pg'
+require "csv"
+require "English"
+require "fileutils"
+require "inifile"
+require "pg"
-require_relative 'series'
-require_relative 'tconn'
+require_relative "series"
+require_relative "tconn"
+# Handles reads/writes of data stored in the DB
class Store
def unclassified_csv
- @basePath + '/csv/unclassified.csv'
+ "#{@base_path}/csv/unclassified.csv"
end
def initialize(config_file)
@conn = nil
config = IniFile.load(config_file)
- if nil == config
- puts 'FATAL: Failed to load config file "' + config_file + '". Aborting initialization.'
+ if config.nil?
+ puts "FATAL: Failed to load config file #{config_file.inspect}. Aborting initialization."
return
end
- section = config['database']
- @dbhost = section['host']
+ section = config["database"]
+ @dbhost = section["host"]
@dbport = 5432
- @dbname = section['name']
- @dbuser = section['user']
- @dbpass = section['pass']
+ @dbname = section["name"]
+ @dbuser = section["user"]
+ @dbpass = section["pass"]
- section = config['filesystem']
- @basePath = section['basePath']
+ section = config["filesystem"]
+ @base_path = section["basePath"]
end
def connect
- @conn = TimedConn.new(PG.connect(@dbhost, @dbport, '', '', @dbname, @dbuser, @dbpass))
- return @conn
+ @conn = TimedConn.new(PG.connect(@dbhost, @dbport, "", "", @dbname, @dbuser, @dbpass))
+ @conn
end
def disconnect
- @conn.close()
+ @conn.close
end
def construct_efs_path(efs_id)
- id_str = sprintf('%010d', efs_id)
- path = sprintf('%s/%s/%s/%s', id_str[0,2], id_str[2,2], id_str[4,2], id_str[6,2])
- name = id_str + '.dat'
- return path, name
+ id_str = format("%010d", efs_id)
+
+ # rubocop:disable Style/FormatStringToken
+ path = format("%s/%s/%s/%s", id_str[0, 2], id_str[2, 2], id_str[4, 2], id_str[6, 2])
+ # rubocop:enable Style/FormatStringToken
+
+ name = "#{id_str}.dat"
+
+ [path, name]
end
def cross_reference_lists
@@ -56,18 +63,16 @@ class Store
end
def create_schema(skip_class)
- create_authors =
-< exc
- puts 'WARNING: "' + stmt + '" failed: ' + exc.to_s
- end
+ stmts.each do |stmt|
+ @conn.exec(stmt)
+ rescue PG::Error => e
+ puts "WARNING: #{stmt.inspect} failed: #{e}"
end
end
def find_all_authors(author_name)
result = []
- sqlSelect = "SELECT id FROM Authors WHERE grouping=$1;"
+ sql_select = "SELECT id FROM Authors WHERE grouping=$1;"
args = [author_name]
- @conn.exec_params(sqlSelect, args) do |rs|
+ @conn.exec_params(sql_select, args) do |rs|
rs.each do |row|
- result << row['id']
+ result << row["id"]
end
end
@@ -231,191 +227,194 @@ EOS
end
def find_author(author)
- sqlSelect = "SELECT id FROM Authors WHERE grouping=$1 AND reading=$2 AND sort=$3;"
+ sql_select = "SELECT id FROM Authors WHERE grouping=$1 AND reading=$2 AND sort=$3;"
args = [author.grouping, author.reading_order, author.sort_order]
- @conn.exec_params(sqlSelect, args) do |rs|
- if rs.ntuples > 0
- return rs[0]['id']
- end
+ @conn.exec_params(sql_select, args) do |rs|
+ return rs[0]["id"] if rs.ntuples.positive?
end
- return nil
+ nil
end
def init_db(skip_class)
sql = "SELECT 1 FROM pg_tables WHERE tableowner='quanlib' AND tablename='books'"
found = false
- @conn.exec(sql).each do |row|
+ @conn.exec(sql).each do |_row|
found = true
end
- if ! found
- create_schema(skip_class)
- end
+ create_schema(skip_class) unless found
end
def load_author(id)
- sqlSelect = "SELECT grouping, reading, sort FROM Authors WHERE id=$1"
+ sql_select = "SELECT grouping, reading, sort FROM Authors WHERE id=$1"
args = [id]
- @conn.exec_params(sqlSelect, args) do |rs|
- if rs.ntuples != 1
- raise "Expected 1 row for " + id + " but got " + rs.ntuples + ": " + sqlSelect
- end
+ @conn.exec_params(sql_select, args) do |rs|
+ raise "Expected 1 row for #{id} but got #{rs.ntuples}: #{sql_select}" if rs.ntuples != 1
+
row = rs[0]
- author = Author.new(row['grouping'], row['reading'], row['sort'])
+ author = Author.new(row["grouping"], row["reading"], row["sort"])
return author
end
- return nil
+ nil
end
def store_author(author)
id = find_author(author)
- if nil == id
- id = next_id('author_id')
- sqlInsert = "INSERT INTO Authors(id, grouping, reading, sort) VALUES ($1, $2, $3, $4);"
+ if id.nil?
+ id = next_id("author_id")
+ sql_insert = "INSERT INTO Authors(id, grouping, reading, sort) VALUES ($1, $2, $3, $4);"
args = [id, author.grouping, author.reading_order, author.sort_order]
begin
- rs = @conn.exec_params(sqlInsert, args)
- rescue Exception => e
- puts sqlInsert + ": " + args.inspect()
+ rs = @conn.exec_params(sql_insert, args)
+ rescue PG::Error => e
+ puts "#{sql_insert}: #{args.inspect}"
puts e.message
- puts $@
+ puts $ERROR_POSITION
ensure
- rs.clear if rs
+ rs&.clear
end
end
- return id
+ id
end
def load_book(id)
- sql = "SELECT author, classification, cover, description, language, path, series, title, volume FROM Books WHERE id=$1;"
+ sql = <<~SQL
+ SELECT author, classification, cover, description, language, path, series, title, volume
+ FROM Books WHERE id=$1;
+ SQL
book = nil
begin
@conn.exec_params(sql, [id]) do |rs|
- if 1 != rs.ntuples
- raise 'Expected one row in Books for id ' + id + ', but found ' + rs.length + '.'
- return nil
- end
+ raise "Expected one row in Books for id #{id}, but found #{rs.length}." if rs.ntuples != 1
+
row = rs[0]
book = Book.new(self)
- book.author = load_author(row['author'])
- book.classification_id = row['classification']
- book.cover = load_cover(row['cover'])
- book.description = row['description']
- book.language = row['language']
- book.path = row['path']
- book.series_id = row['series']
- book.title = row['title']
- book.volume = row['volume']
+ book.author = load_author(row["author"])
+ book.classification_id = row["classification"]
+ book.cover = load_cover(row["cover"])
+ book.description = row["description"]
+ book.language = row["language"]
+ book.path = row["path"]
+ book.series_id = row["series"]
+ book.title = row["title"]
+ book.volume = row["volume"]
end
- rescue Exception => e
- puts sql + ": " + id
+ rescue PG::Error => e
+ puts "#{sql}: #{id}"
puts e.message
- puts $@
+ puts $ERROR_POSITION
end
- return book
+ book
end
def store_book(book)
- sql = "INSERT INTO Books (id, arrived, author, classification, cover, description, language, path, series, title, volume) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11);"
+ sql = <<~SQL
+ INSERT INTO
+ Books (id, arrived, author, classification, cover, description, language, path, series, title, volume)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11);
+ SQL
- book_id = next_id('book_id')
+ book_id = next_id("book_id")
author_id = store_author(book.author)
efs_id = store_cover(book)&.first
- args = [book_id, book.arrived, author_id, book.classification_id, efs_id, book.description, book.language, book.path, book.series_id, book.title, book.volume]
+ args = [
+ book_id,
+ book.arrived,
+ author_id,
+ book.classification_id,
+ efs_id,
+ book.description,
+ book.language,
+ book.path,
+ book.series_id,
+ book.title,
+ book.volume,
+ ]
begin
rs = @conn.exec_params(sql, args)
- rescue Exception => e
- puts sql + ": " + args.inspect()
+ rescue PG::Error => e
+ puts "#{sql}: #{args.inspect}"
puts e.message
- puts $@
+ puts $ERROR_POSITION
ensure
- rs.clear if rs
+ rs&.clear
end
- return book_id
+ book_id
end
def find_classification(author_grouping, title_grouping)
sql = "SELECT id FROM Classifications WHERE author_grouping = $1 AND title_grouping = $2;"
@conn.exec_params(sql, [author_grouping, title_grouping]) do |rs|
- if rs.ntuples > 0
- return rs[0]['id']
- end
+ return rs[0]["id"] if rs.ntuples.positive?
end
- return nil
+ nil
end
def load_classification(id)
sql = "SELECT ddc, lcc, author_grouping, author_sort, title_grouping, title "
sql += " FROM Classifications WHERE id=$1"
@conn.exec_params(sql, [id]) do |rs|
- if rs.ntuples > 0
- row = rs[0]
- ddc = row['ddc']
- lcc = row['lcc']
- author_grouping = row['author_grouping']
- author = row['author_sort']
- title_grouping = row['title_grouping']
- title = row['title']
-
- result = Classification.new(ddc, lcc, author_grouping, author, title_grouping, title)
- result.id = id
- return result
- end
+ next unless rs.ntuples.positive?
+
+ row = rs[0]
+ ddc = row["ddc"]
+ lcc = row["lcc"]
+ author_grouping = row["author_grouping"]
+ author = row["author_sort"]
+ title_grouping = row["title_grouping"]
+ title = row["title"]
+
+ result = Classification.new(ddc, lcc, author_grouping, author, title_grouping, title)
+ result.id = id
+ return result
end
- return nil
+ nil
end
def load_cover(id)
- if nil == id
- return nil
- end
+ return if id.nil?
- mime_type = 'application/octet-stream'
+ mime_type = "application/octet-stream"
sql = "SELECT mimeType FROM Efs WHERE id=$1"
@conn.exec_params(sql, [id]) do |rs|
- if rs.ntuples != 1
- raise "Expected one row but got " + rs.ntuples + ": " + sql + ": " + id
- end
- mime_type = rs[0]['mimeType']
+ raise "Expected one row but got #{rs.ntuples}: #{sql}: #{id}" if rs.ntuples != 1
+
+ mime_type = rs[0]["mimeType"]
end
(efspath, efsname) = construct_efs_path(id)
- fullpath = @basePath + '/efs/' + efspath + '/' + efsname
+ fullpath = "#{@base_path}/efs/#{efspath}/#{efsname}"
- return Cover.new(nil, fullpath, mime_type)
+ Cover.new(nil, fullpath, mime_type)
end
def store_cover(book)
efs_id = nil
- cover = book.cover()
+ cover = book.cover
- if nil == cover
- return nil
- end
+ return if cover.nil?
@conn.exec("SELECT nextval('efs_id')") do |rs|
- efs_id = rs[0]['nextval']
+ efs_id = rs[0]["nextval"]
end
- if nil == efs_id
- return nil
- end
+ return if efs_id.nil?
(efspath, efsname) = construct_efs_path(efs_id)
- efspath = @basePath + '/efs/' + efspath
+ efspath = "#{@base_path}/efs/#{efspath}"
FileUtils.mkdir_p(efspath)
@@ -424,85 +423,80 @@ EOS
sql = "INSERT INTO efs VALUES ($1, $2)"
begin
rs = @conn.exec_params(sql, [efs_id, mimetype])
- rescue Exception => e
- puts sql + ": " + efs_id + ", " + mimetype
+ rescue PG::Error => e
+ puts "#{sql}: #{efs_id}, #{mimetype}"
puts e.message
- puts $@
+ puts $ERROR_POSITION
ensure
- rs.clear if rs
+ rs&.clear
end
- return efs_id, mimetype
+ [efs_id, mimetype]
end
def exec_id_query(sql, args)
ids = []
@conn.exec_params(sql, args) do |rs|
rs.each do |row|
- ids.push(row['id'])
+ ids.push(row["id"])
end
end
- return ids
+ ids
end
def exec_update(sql, args)
- begin
- rs = @conn.exec_params(sql, args)
- rescue Exception => e
- puts sql + ": " + args.inspect()
- puts e.message
- puts $@
- ensure
- rs.clear if rs
- end
+ rs = @conn.exec_params(sql, args)
+ rescue PG::Error => e
+ puts "#{sql}: #{args.inspect}"
+ puts e.message
+ puts $ERROR_POSITION
+ ensure
+ rs&.clear
end
def next_id(seq_name)
id = nil
- @conn.exec("SELECT nextval('" + seq_name + "');") do |rs|
- id = rs[0]['nextval']
+ @conn.exec("SELECT nextval('#{seq_name}');") do |rs|
+ id = rs[0]["nextval"]
end
- return id
+ id
end
def get_series(grouping, code)
- if nil == code
- return nil
- end
+ return if code.nil?
sql = "SELECT id FROM Series WHERE grouping=$1 AND code=$2;"
args = [grouping, code]
- @conn.exec_params(sql, args).each do |row|
- return row['id']
- end
+ result = @conn.exec_params(sql, args)
+ return result.first["id"] if result.first
# TODO: Create a new series object here?
- puts 'WARNING: series("' + grouping + '", "' + code + '") not found.'
- return nil
+ puts "WARNING: series(#{grouping.inspect}, #{code.inspect}) not found."
+ nil
end
def load_series(id)
sql = "SELECT age,genre,grouping,code,descr FROM Series WHERE id=$1;"
args = [id]
@conn.exec_params(sql, args) do |rs|
- if rs.ntuples > 0
- row = rs[0]
- series = Series.new(id)
- series.age = row['age']
- series.genre = row['genre']
- series.grouping = row['grouping']
- series.code = row['code']
- series.descr = row['descr']
- return series
- end
+ next unless rs.ntuples.positive?
+
+ row = rs[0]
+ series = Series.new(id)
+ series.age = row["age"]
+ series.genre = row["genre"]
+ series.grouping = row["grouping"]
+ series.code = row["code"]
+ series.descr = row["descr"]
+ return series
end
- return nil
+ nil
end
def populate_classifications_table
puts "Populating the Classifications table..."
first = true
- CSV.foreach(@basePath + '/csv/class.csv') do |row|
+ CSV.foreach("#{@base_path}/csv/class.csv") do |row|
if first
# skip the header row
first = false
@@ -510,7 +504,7 @@ EOS
# First, add a row to the Classifications table
- id = next_id('classification_id')
+ id = next_id("classification_id")
ddc = row[0]
lcc = row[1]
author_grouping = row[2]
@@ -518,22 +512,23 @@ EOS
title_grouping = row[4]
title = row[5]
- sqlInsert = "INSERT INTO Classifications (id, ddc, lcc, author_grouping, author_sort, title_grouping, title) VALUES ($1, $2, $3, $4, $5, $6, $7);"
+ sql_insert = <<~SQL
+ INSERT INTO Classifications (id, ddc, lcc, author_grouping, author_sort, title_grouping, title)
+ VALUES ($1, $2, $3, $4, $5, $6, $7);
+ SQL
args = [id, ddc, lcc, author_grouping, author_sort, title_grouping, title]
- exec_update(sqlInsert, args)
+ exec_update(sql_insert, args)
# Second, link up with the appropriate FAST table entries
fast = []
input = row[6]
- if input.length > 0
- fast = input.split(';')
- end
+ fast = input.split(";") unless input.empty?
fast.each do |fast_id|
- sqlInsert = "INSERT INTO FAST_Classifications (fast, classification) VALUES ($1, $2);"
+ sql_insert = "INSERT INTO FAST_Classifications (fast, classification) VALUES ($1, $2);"
args = [fast_id, id]
- exec_update(sqlInsert, args)
+ exec_update(sql_insert, args)
end
end
end
@@ -542,14 +537,14 @@ EOS
def populate_fast_table
puts "Populating the FAST table..."
first = true
- CSV.foreach(@basePath + '/csv/fast.csv') do |row|
+ CSV.foreach("#{@base_path}/csv/fast.csv") do |row|
if first
first = false # skip the header row
else
id = row[0]
descr = row[1]
- sqlInsert = "INSERT INTO FAST (id, descr) VALUES ($1, $2);"
- exec_update(sqlInsert, [id, descr])
+ sql_insert = "INSERT INTO FAST (id, descr) VALUES ($1, $2);"
+ exec_update(sql_insert, [id, descr])
end
end
end
@@ -557,98 +552,93 @@ EOS
def populate_lists_table
puts "Populating the Lists table..."
- CSV.foreach(@basePath + "/csv/lists.csv", headers: true) do |row|
- author_ids = find_all_authors(row['author'])
+ CSV.foreach("#{@base_path}/csv/lists.csv", headers: true) do |row|
+ author_ids = find_all_authors(row["author"])
if author_ids.empty?
- specification = [row['age'], row['category'], row['code'], row['year'], row['author'], row['title']]
- .map { |x| x.inspect }
- .join(', ')
+ specification =
+ [row["age"], row["category"], row["code"], row["year"], row["author"], row["title"]]
+ .map(&:inspect)
+ .join(", ")
puts "WARNING: For list entry (#{specification}), no such author was found."
next
end
- sqlInsert = %Q(
+ sql_insert = <<~SQL
INSERT INTO Lists (id, age, category, code, year, author, title)
VALUES ($1, $2, $3, $4, $5, $6, $7);
- )
+ SQL
author_ids.each do |author_id|
- list_id = next_id('list_id')
- args = [list_id, row['age'], row['category'], row['code'], row['year'], author_id, row['title']]
- exec_update(sqlInsert, args)
+ list_id = next_id("list_id")
+ args = [list_id, row["age"], row["category"], row["code"], row["year"], author_id, row["title"]]
+ exec_update(sql_insert, args)
- update_lists_books_table(list_id, author_id, row['title'])
+ update_lists_books_table(list_id, author_id, row["title"])
end
end
end
# Scan for books that match this Lists entry, and add any matches to the Lists_Books associative table
def update_lists_books_table(list_id, author_id, title)
- title_pattern = Book.grouping_for_title(title).gsub('_', '%')
- sqlSelect = "SELECT id FROM Books WHERE author = $1 AND title LIKE $2;"
+ title_pattern = Book.grouping_for_title(title).gsub("_", "%")
+ sql_select = "SELECT id FROM Books WHERE author = $1 AND title LIKE $2;"
args = [author_id, title_pattern]
- @conn.exec_params(sqlSelect, args) do |rs|
+ @conn.exec_params(sql_select, args) do |rs|
rs.each do |row|
- sqlInsert = "INSERT INTO Lists_Books (list, book) VALUES ($1, $2)"
- args = [list_id, row['id']]
- exec_update(sqlInsert, args)
+ sql_insert = "INSERT INTO Lists_Books (list, book) VALUES ($1, $2)"
+ args = [list_id, row["id"]]
+ exec_update(sql_insert, args)
end
end
end
def populate_series_table
puts "Populating the Series table..."
- CSV.foreach(@basePath + '/csv/series.csv') do |row|
- id = next_id('series_id')
- sqlInsert = "INSERT INTO Series (id, age, genre, grouping, code, descr) VALUES ($1, $2, $3, $4, $5, $6);"
+ CSV.foreach("#{@base_path}/csv/series.csv") do |row|
+ id = next_id("series_id")
+ sql_insert = "INSERT INTO Series (id, age, genre, grouping, code, descr) VALUES ($1, $2, $3, $4, $5, $6);"
args = [id] + row
- exec_update(sqlInsert, args)
+ exec_update(sql_insert, args)
end
end
def query_books_by_author(pattern)
- sql =
-< 0)
- result.concat(sub)
- end
- elsif (! File.directory?(fullName))
- result.push(fullName)
+ children.each do |child|
+ full_name = "#{path.chomp("/")}/#{child}"
+ if should_descend_into_directory?(full_name)
+ sub = walk(full_name)
+ result.concat(sub) if !sub.nil? && !sub.empty?
+ elsif !File.directory?(full_name)
+ result.push(full_name)
end
end
- return result
+ result
end
+ private
+
def num_threads
# TOOD: make this (auto?) configurable
12
end
+
+ def should_descend_into_directory?(path)
+ child = File.basename(path)
+ File.directory?(path) && child != "." && child != ".." && !File.symlink?(path)
+ end
end
diff --git a/test/book_test.rb b/test/book_test.rb
index bb476c7..034ae04 100644
--- a/test/book_test.rb
+++ b/test/book_test.rb
@@ -7,13 +7,13 @@ require "store_mock"
class BookTest < Minitest::Test
def test_that_it_can_handle_epub_and_pdf_files
- %w(epub pdf).each do |extension|
+ %w[epub pdf].each do |extension|
assert_equal true, Book.can_handle?("sample.#{extension}")
end
end
def test_that_it_cannot_handle_mobi_html_txt_doc_zip_rtf_nor_rar
- %w(doc html mobi rar rtf txt zip).each do |extension|
+ %w[doc html mobi rar rtf txt zip].each do |extension|
assert_equal false, Book.can_handle?("sample.#{extension}")
end
end
@@ -33,7 +33,7 @@ class BookTest < Minitest::Test
def test_load_from_file
store = StoreMock.new
- store.expects(:get_series).returns(mock_series_LW)
+ store.expects(:get_series).returns(mock_series_lw)
store.connect
book = Book.new(store)
@@ -45,16 +45,19 @@ class BookTest < Minitest::Test
assert_equal "Louisa May Alcott", author.reading_order
assert_equal "Alcott, Louisa May", author.sort_order
- assert_equal "This story follows the lives of the four March sisters&emdash;Meg, Jo, Beth, and Amy&emdash;and details their coming of age.", book.description
+ expected_descr = "This story follows the lives of the four March sisters&emdash;Meg, Jo, Beth, and Amy&emdash;" \
+ "and details their coming of age."
+
+ assert_equal expected_descr, book.description
assert_equal "en", book.language
assert_equal "Little Women: Or, Meg, Jo, Beth and Amy", book.title
- assert_equal mock_series_LW.to_s, book.series_id.to_s
+ assert_equal mock_series_lw.to_s, book.series_id.to_s
assert_equal 1, book.volume.to_i
end
def test_heading
store = StoreMock.new
- store.expects(:get_series).returns(mock_series_LW)
+ store.expects(:get_series).returns(mock_series_lw)
store.connect
book = Book.new(store)
@@ -68,7 +71,7 @@ class BookTest < Minitest::Test
private
- def mock_series_LW
+ def mock_series_lw
id = 1
series = Series.new(id)
series.age = "ya"
diff --git a/test/classification_test.rb b/test/classification_test.rb
index fd11643..570393c 100644
--- a/test/classification_test.rb
+++ b/test/classification_test.rb
@@ -11,10 +11,12 @@ class ClassificationTest < Minitest::Test
author_sort = "Franklin, Benjamin"
title_grouping = "Autobiography_of_Benjamin_Franklin"
title = "Autobiography of Benjamin Franklin"
-
+
classification = Classification.new(ddc, lcc, author_grouping, author_sort, title_grouping, title)
- expected = '(Classification: Dewey="973.3/092 B", LCC="E302.6.F7", author_grouping="BenjaminFranklin", author="Franklin, Benjamin", title_grouping="Autobiography_of_Benjamin_Franklin", title="Autobiography of Benjamin Franklin")'
+ expected = '(Classification: Dewey="973.3/092 B", LCC="E302.6.F7", author_grouping="BenjaminFranklin", ' \
+ 'author="Franklin, Benjamin", title_grouping="Autobiography_of_Benjamin_Franklin", title="Autobiography ' \
+ 'of Benjamin Franklin")'
actual = classification.inspect
assert_equal expected, actual
diff --git a/test/conn_mock.rb b/test/conn_mock.rb
index c090404..ff3b4b3 100644
--- a/test/conn_mock.rb
+++ b/test/conn_mock.rb
@@ -12,13 +12,13 @@ class ConnMock
@mock_connected = false
end
- def exec(*args, &block)
+ def exec(*args, &_block)
@stmts << args
- return nil
+ nil
end
- def exec_params(*args, &block)
+ def exec_params(*args, &_block)
@stmts << args
- return nil
+ nil
end
end
diff --git a/test/store_mock.rb b/test/store_mock.rb
index 897e79a..5d16cff 100644
--- a/test/store_mock.rb
+++ b/test/store_mock.rb
@@ -4,16 +4,18 @@ require "conn_mock"
require "store"
class StoreMock < Store
+ # rubocop:disable Lint/MissingSuper
def initialize
@dbhost = "host"
@dbport = 5432
@dbname = "dbname"
@dbuser = "dbuser"
@dbpass = "dbpass"
- @basePath = "base_path"
+ @base_path = "base_path"
@conn = nil
end
+ # rubocop:enable Lint/MissingSuper
def connect
raise "Mock error: .connect called when already connected" if @conn
diff --git a/test/store_test.rb b/test/store_test.rb
index 46676b8..9fd0494 100644
--- a/test/store_test.rb
+++ b/test/store_test.rb
@@ -6,16 +6,18 @@ require "store"
class StoreTest < Minitest::Test
def test_construct_efs_path
+ # rubocop:disable Layout/ExtraSpacing,Layout/SpaceInsideArrayLiteralBrackets
data = [
- [ 1234, '00/00/00/12', '0000001234.dat'],
- [ 1, '00/00/00/00', '0000000001.dat'],
- [1234567890, '12/34/56/78', '1234567890.dat'],
- [ 7778123, '00/07/77/81', '0007778123.dat'],
- [ 0x1b, '00/00/00/00', '0000000027.dat']
+ [ 1234, "00/00/00/12", "0000001234.dat"],
+ [ 1, "00/00/00/00", "0000000001.dat"],
+ [1_234_567_890, "12/34/56/78", "1234567890.dat"],
+ [ 7_778_123, "00/07/77/81", "0007778123.dat"],
+ [ 0x1b, "00/00/00/00", "0000000027.dat"],
]
+ # rubocop:enable Layout/ExtraSpacing,Layout/SpaceInsideArrayLiteralBrackets
- IniFile.stubs(:load).returns({"database" => {}, "filesystem" => {}})
- store = Store.new('quanlib.ini')
+ IniFile.stubs(:load).returns({ "database" => {}, "filesystem" => {} })
+ store = Store.new("quanlib.ini")
data.each do |input, expected_path, expected_name|
(actual_path, actual_name) = store.construct_efs_path(input)
diff --git a/test/walk_dir_test.rb b/test/walk_dir_test.rb
index e47ef47..fc068d1 100644
--- a/test/walk_dir_test.rb
+++ b/test/walk_dir_test.rb
@@ -5,34 +5,34 @@ require "test_helper"
require "walk_dir"
class WalkDirTest < Minitest::Test
- def test_is_duplicate
- yes = %w(
+ def test_duplicate
+ yes = %w[
Little_Women_bis.pdf
The_Autobiography_of_Benjamin_Franklin_ter.epub
Romeo_and_Juliet_quater.pdf
As_You_Like_It_quinquies.epub
- )
+ ]
- no = %w(
+ no = %w[
Little_Women.pdf
The_Autobiography_of_Benjamin_Franklin.epub
Romeo_and_Juliet.pdf
As_You_Like_It.epub
- )
+ ]
walk_dir = create_walk_dir
yes.each do |filename|
- assert_equal true, walk_dir.is_duplicate?(filename)
+ assert_equal true, walk_dir.duplicate?(filename)
end
no.each do |filename|
- assert_equal false, walk_dir.is_duplicate?(filename)
+ assert_equal false, walk_dir.duplicate?(filename)
end
end
def test_remove_duplicates
- input = %w(
+ input = %w[
00_nonFiction/BenjaminFranklin/The_Autobiography_of_Benjamin_Franklin.epub
00_nonFiction/BenjaminFranklin/The_Autobiography_of_Benjamin_Franklin.jpeg
00_nonFiction/BenjaminFranklin/The_Autobiography_of_Benjamin_Franklin.pdf
@@ -41,14 +41,14 @@ class WalkDirTest < Minitest::Test
FranklinDixon/HB001_The_Tower_Treasure.jpeg
FranklinDixon/HB001_The_Tower_Treasure.pdf
FranklinDixon/HB002_The_House_on_the_Cliff.epub
- )
+ ]
- expected = %w(
+ expected = %w[
00_nonFiction/BenjaminFranklin/The_Autobiography_of_Benjamin_Franklin.epub
LouisaAlcott/Little_Women.epub
FranklinDixon/HB001_The_Tower_Treasure.epub
FranklinDixon/HB002_The_House_on_the_Cliff.epub
- )
+ ]
walk_dir = create_walk_dir
actual = walk_dir.remove_duplicates(input)
@@ -57,60 +57,70 @@ class WalkDirTest < Minitest::Test
end
def test_walk
- root_entries = %w(
+ root_entries = set_sample_dir_expectations
+
+ walk_dir = create_walk_dir(".", root_entries)
+ files = walk_dir.instance_variable_get(:@files)
+
+ expected = %w[
+ ./00_nonFiction/BenjaminFranklin/The_Autobiography_of_Benjamin_Franklin.epub
+ ./LouisaAlcott/LW01_Little_Women.epub
+ ./FranklinDixon/HB001_The_Tower_Treasure.epub
+ ./FranklinDixon/HB002_The_House_on_the_Cliff.epub
+ ]
+
+ assert_equal expected, files
+ end
+
+ private
+
+ def create_walk_dir(root_path = "/does/not/exist", root_entries = [])
+ config_file = "quanlib.ini"
+ Dir.expects(:entries).with(root_path).returns(root_entries)
+
+ WalkDir.new(config_file, root_path)
+ end
+
+ def set_sample_dir_expectations
+ root_entries = %w[
00_nonFiction
LouisaAlcott
FranklinDixon
- )
+ ]
- Dir.expects(:entries).with("./00_nonFiction").returns(%w(BenjaminFranklin))
- Dir.expects(:entries).with("./00_nonFiction/BenjaminFranklin").returns(%w(
- The_Autobiography_of_Benjamin_Franklin.epub
- ))
+ Dir.expects(:entries).with("./00_nonFiction").returns(%w[BenjaminFranklin])
+ Dir.expects(:entries).with("./00_nonFiction/BenjaminFranklin").returns(
+ %w[
+ The_Autobiography_of_Benjamin_Franklin.epub
+ ]
+ )
Dir.expects(:entries).with("./LouisaAlcott").returns(["LW01_Little_Women.epub"])
- Dir.expects(:entries).with("./FranklinDixon").returns(%w(
- HB001_The_Tower_Treasure.epub
- HB002_The_House_on_the_Cliff.epub
- ))
+ Dir.expects(:entries).with("./FranklinDixon").returns(
+ %w[
+ HB001_The_Tower_Treasure.epub
+ HB002_The_House_on_the_Cliff.epub
+ ]
+ )
- %w(
+ %w[
./00_nonFiction
./00_nonFiction/BenjaminFranklin
./LouisaAlcott
./FranklinDixon
- ).each do |path|
+ ].each do |path|
File.expects(:directory?).with(path).returns(true).at_least_once
File.expects(:symlink?).with(path).returns(false).at_least_once
end
- %w(
+ %w[
./00_nonFiction/BenjaminFranklin/The_Autobiography_of_Benjamin_Franklin.epub
./LouisaAlcott/LW01_Little_Women.epub
./FranklinDixon/HB001_The_Tower_Treasure.epub
./FranklinDixon/HB002_The_House_on_the_Cliff.epub
- ).each do |path|
+ ].each do |path|
File.expects(:directory?).with(path).returns(false).at_least_once
end
- walk_dir = create_walk_dir(".", root_entries)
- files = walk_dir.instance_variable_get(:@files)
-
- expected = %w(
- ./00_nonFiction/BenjaminFranklin/The_Autobiography_of_Benjamin_Franklin.epub
- ./LouisaAlcott/LW01_Little_Women.epub
- ./FranklinDixon/HB001_The_Tower_Treasure.epub
- ./FranklinDixon/HB002_The_House_on_the_Cliff.epub
- )
-
- assert_equal expected, files
- end
-
- private
-
- def create_walk_dir(root_path = "/does/not/exist", root_entries = [])
- config_file = "quanlib.ini"
- Dir.expects(:entries).with(root_path).returns(root_entries)
-
- WalkDir.new(config_file, root_path)
+ root_entries
end
end