Use singleton in various classes

This commit is contained in:
Alex Ling 2020-05-04 16:18:16 +00:00
parent 09b297cd8e
commit 1bec9f0108
16 changed files with 178 additions and 117 deletions

View File

@ -26,6 +26,16 @@ class Config
"manga_rename_rule" => "{title}", "manga_rename_rule" => "{title}",
} }
@@singlet : Config?
def self.current
@@singlet.not_nil!
end
def set_current
@@singlet = self
end
def self.load(path : String?) def self.load(path : String?)
path = "~/.config/mango/config.yml" if path.nil? path = "~/.config/mango/config.yml" if path.nil?
cfg_path = File.expand_path path, home: true cfg_path = File.expand_path path, home: true

View File

@ -1,21 +0,0 @@
require "./config"
require "./library"
require "./storage"
require "./logger"
class Context
property config : Config
property library : Library
property storage : Storage
property logger : Logger
property queue : MangaDex::Queue
def initialize(@config, @logger, @library, @storage, @queue)
end
{% for lvl in Logger::LEVELS %}
def {{lvl.id}}(msg)
@logger.{{lvl.id}} msg
end
{% end %}
end

View File

@ -2,20 +2,17 @@ require "kemal"
require "../logger" require "../logger"
class LogHandler < Kemal::BaseLogHandler class LogHandler < Kemal::BaseLogHandler
def initialize(@logger : Logger)
end
def call(env) def call(env)
elapsed_time = Time.measure { call_next env } elapsed_time = Time.measure { call_next env }
elapsed_text = elapsed_text elapsed_time elapsed_text = elapsed_text elapsed_time
msg = "#{env.response.status_code} #{env.request.method}" \ msg = "#{env.response.status_code} #{env.request.method}" \
" #{env.request.resource} #{elapsed_text}" " #{env.request.resource} #{elapsed_text}"
@logger.debug msg Logger.debug msg
env env
end end
def write(msg) def write(msg)
@logger.debug msg Logger.debug msg
end end
private def elapsed_text(elapsed) private def elapsed_text(elapsed)

View File

@ -97,7 +97,7 @@ class Title
encoded_title : String, mtime : Time encoded_title : String, mtime : Time
def initialize(@dir : String, @parent_id, storage, def initialize(@dir : String, @parent_id, storage,
@logger : Logger, @library : Library) @library : Library)
@id = storage.get_id @dir, true @id = storage.get_id @dir, true
@title = File.basename dir @title = File.basename dir
@encoded_title = URI.encode @title @encoded_title = URI.encode @title
@ -109,7 +109,7 @@ class Title
next if fn.starts_with? "." next if fn.starts_with? "."
path = File.join dir, fn path = File.join dir, fn
if File.directory? path if File.directory? path
title = Title.new path, @id, storage, @logger, library title = Title.new path, @id, storage, library
next if title.entries.size == 0 && title.titles.size == 0 next if title.entries.size == 0 && title.titles.size == 0
@library.title_hash[title.id] = title @library.title_hash[title.id] = title
@title_ids << title.id @title_ids << title.id
@ -118,9 +118,9 @@ class Title
if [".zip", ".cbz"].includes? File.extname path if [".zip", ".cbz"].includes? File.extname path
zip_exception = validate_zip path zip_exception = validate_zip path
unless zip_exception.nil? unless zip_exception.nil?
@logger.warn "File #{path} is corrupted or is not a valid zip " \ Logger.warn "File #{path} is corrupted or is not a valid zip " \
"archive. Ignoring it." "archive. Ignoring it."
@logger.debug "Zip error: #{zip_exception}" Logger.debug "Zip error: #{zip_exception}"
next next
end end
entry = Entry.new path, self, @id, storage entry = Entry.new path, self, @id, storage
@ -367,9 +367,19 @@ end
class Library class Library
property dir : String, title_ids : Array(String), scan_interval : Int32, property dir : String, title_ids : Array(String), scan_interval : Int32,
logger : Logger, storage : Storage, title_hash : Hash(String, Title) storage : Storage, title_hash : Hash(String, Title)
def initialize(@dir, @scan_interval, @logger, @storage) def self.default
unless @@default
@@default = new
end
@@default.not_nil!
end
def initialize
@storage = Storage.default
@dir = Config.current.library_path
@scan_interval = Config.current.scan_interval
# explicitly initialize @titles to bypass the compiler check. it will # explicitly initialize @titles to bypass the compiler check. it will
# be filled with actual Titles in the `scan` call below # be filled with actual Titles in the `scan` call below
@title_ids = [] of String @title_ids = [] of String
@ -381,7 +391,7 @@ class Library
start = Time.local start = Time.local
scan scan
ms = (Time.local - start).total_milliseconds ms = (Time.local - start).total_milliseconds
@logger.info "Scanned #{@title_ids.size} titles in #{ms}ms" Logger.info "Scanned #{@title_ids.size} titles in #{ms}ms"
sleep @scan_interval * 60 sleep @scan_interval * 60
end end
end end
@ -410,7 +420,7 @@ class Library
def scan def scan
unless Dir.exists? @dir unless Dir.exists? @dir
@logger.info "The library directory #{@dir} does not exist. " \ Logger.info "The library directory #{@dir} does not exist. " \
"Attempting to create it" "Attempting to create it"
Dir.mkdir_p @dir Dir.mkdir_p @dir
end end
@ -419,13 +429,13 @@ class Library
.select { |fn| !fn.starts_with? "." } .select { |fn| !fn.starts_with? "." }
.map { |fn| File.join @dir, fn } .map { |fn| File.join @dir, fn }
.select { |path| File.directory? path } .select { |path| File.directory? path }
.map { |path| Title.new path, "", @storage, @logger, self } .map { |path| Title.new path, "", @storage, self }
.select { |title| !(title.entries.empty? && title.titles.empty?) } .select { |title| !(title.entries.empty? && title.titles.empty?) }
.sort { |a, b| a.title <=> b.title } .sort { |a, b| a.title <=> b.title }
.each do |title| .each do |title|
@title_hash[title.id] = title @title_hash[title.id] = title
@title_ids << title.id @title_ids << title.id
end end
@logger.debug "Scan completed" Logger.debug "Scan completed"
end end
end end

View File

@ -8,7 +8,15 @@ class Logger
@@severity : Log::Severity = :info @@severity : Log::Severity = :info
def initialize(level : String) def self.default
unless @@default
@@default = new
end
@@default.not_nil!
end
def initialize
level = Config.current.log_level
{% begin %} {% begin %}
case level.downcase case level.downcase
when "off" when "off"
@ -50,9 +58,16 @@ class Logger
@backend.write Log::Entry.new "", Log::Severity::None, msg, nil @backend.write Log::Entry.new "", Log::Severity::None, msg, nil
end end
def self.log(msg)
default.log msg
end
{% for lvl in LEVELS %} {% for lvl in LEVELS %}
def {{lvl.id}}(msg) def {{lvl.id}}(msg)
@log.{{lvl.id}} { msg } @log.{{lvl.id}} { msg }
end end
def self.{{lvl.id}}(msg)
default.not_nil!.{{lvl.id}} msg
end
{% end %} {% end %}
end end

View File

@ -133,7 +133,15 @@ module MangaDex
end end
class API class API
def initialize(@base_url = "https://mangadex.org/api/") def self.default
unless @@default
@@default = new
end
@@default.not_nil!
end
def initialize
@base_url = Config.current.mangadex["api_url"].to_s || "https://mangadex.org/api/"
@lang = {} of String => String @lang = {} of String => String
CSV.each_row {{read_file "src/assets/lang_codes.csv"}} do |row| CSV.each_row {{read_file "src/assets/lang_codes.csv"}} do |row|
@lang[row[1]] = row[0] @lang[row[1]] = row[0]

View File

@ -1,5 +1,6 @@
require "./api" require "./api"
require "sqlite3" require "sqlite3"
require "zip"
module MangaDex module MangaDex
class PageJob class PageJob
@ -79,11 +80,19 @@ module MangaDex
class Queue class Queue
property downloader : Downloader? property downloader : Downloader?
@path : String = Config.current.mangadex["download_queue_db_path"].to_s
def initialize(@path : String, @logger : Logger) def self.default
dir = File.dirname path unless @@default
@@default = new
end
@@default.not_nil!
end
def initialize
dir = File.dirname @path
unless Dir.exists? dir unless Dir.exists? dir
@logger.info "The queue DB directory #{dir} does not exist. " \ Logger.info "The queue DB directory #{dir} does not exist. " \
"Attepmting to create it" "Attepmting to create it"
Dir.mkdir_p dir Dir.mkdir_p dir
end end
@ -101,7 +110,7 @@ module MangaDex
db.exec "create index if not exists status_idx " \ db.exec "create index if not exists status_idx " \
"on queue (status)" "on queue (status)"
rescue e rescue e
@logger.error "Error when checking tables in DB: #{e}" Logger.error "Error when checking tables in DB: #{e}"
raise e raise e
end end
end end
@ -254,11 +263,22 @@ module MangaDex
class Downloader class Downloader
property stopped = false property stopped = false
@wait_seconds : Int32 = Config.current.mangadex["download_wait_seconds"]
.to_i32
@retries : Int32 = Config.current.mangadex["download_retries"].to_i32
@library_path : String = Config.current.library_path
@downloading = false @downloading = false
def initialize(@queue : Queue, @api : API, @library_path : String, def self.default
@wait_seconds : Int32, @retries : Int32, unless @@default
@logger : Logger) @@default = new
end
@@default.not_nil!
end
def initialize
@queue = Queue.default
@api = API.default
@queue.downloader = self @queue.downloader = self
spawn do spawn do
@ -270,7 +290,7 @@ module MangaDex
next if job.nil? next if job.nil?
download job download job
rescue e rescue e
@logger.error e Logger.error e
end end
end end
end end
@ -282,7 +302,7 @@ module MangaDex
begin begin
chapter = @api.get_chapter(job.id) chapter = @api.get_chapter(job.id)
rescue e rescue e
@logger.error e Logger.error e
@queue.set_status JobStatus::Error, job @queue.set_status JobStatus::Error, job
unless e.message.nil? unless e.message.nil?
@queue.add_message e.message.not_nil!, job @queue.add_message e.message.not_nil!, job
@ -310,14 +330,14 @@ module MangaDex
ext = File.extname fn ext = File.extname fn
fn = "#{i.to_s.rjust len, '0'}#{ext}" fn = "#{i.to_s.rjust len, '0'}#{ext}"
page_job = PageJob.new url, fn, writer, @retries page_job = PageJob.new url, fn, writer, @retries
@logger.debug "Downloading #{url}" Logger.debug "Downloading #{url}"
loop do loop do
sleep @wait_seconds.seconds sleep @wait_seconds.seconds
download_page page_job download_page page_job
break if page_job.success || break if page_job.success ||
page_job.tries_remaning <= 0 page_job.tries_remaning <= 0
page_job.tries_remaning -= 1 page_job.tries_remaning -= 1
@logger.warn "Failed to download page #{url}. " \ Logger.warn "Failed to download page #{url}. " \
"Retrying... Remaining retries: " \ "Retrying... Remaining retries: " \
"#{page_job.tries_remaning}" "#{page_job.tries_remaning}"
end end
@ -330,7 +350,7 @@ module MangaDex
page_jobs = [] of PageJob page_jobs = [] of PageJob
chapter.pages.size.times do chapter.pages.size.times do
page_job = channel.receive page_job = channel.receive
@logger.debug "[#{page_job.success ? "success" : "failed"}] " \ Logger.debug "[#{page_job.success ? "success" : "failed"}] " \
"#{page_job.url}" "#{page_job.url}"
page_jobs << page_job page_jobs << page_job
if page_job.success if page_job.success
@ -339,14 +359,14 @@ module MangaDex
@queue.add_fail job @queue.add_fail job
msg = "Failed to download page #{page_job.url}" msg = "Failed to download page #{page_job.url}"
@queue.add_message msg, job @queue.add_message msg, job
@logger.error msg Logger.error msg
end end
end end
fail_count = page_jobs.count { |j| !j.success } fail_count = page_jobs.count { |j| !j.success }
@logger.debug "Download completed. " \ Logger.debug "Download completed. " \
"#{fail_count}/#{page_jobs.size} failed" "#{fail_count}/#{page_jobs.size} failed"
writer.close writer.close
@logger.debug "cbz File created at #{zip_path}" Logger.debug "cbz File created at #{zip_path}"
zip_exception = validate_zip zip_path zip_exception = validate_zip zip_path
if !zip_exception.nil? if !zip_exception.nil?
@ -363,7 +383,7 @@ module MangaDex
end end
private def download_page(job : PageJob) private def download_page(job : PageJob)
@logger.debug "downloading #{job.url}" Logger.debug "downloading #{job.url}"
headers = HTTP::Headers{ headers = HTTP::Headers{
"User-agent" => "Mangadex.cr", "User-agent" => "Mangadex.cr",
} }
@ -377,7 +397,7 @@ module MangaDex
end end
job.success = true job.success = true
rescue e rescue e
@logger.error e Logger.error e
job.success = false job.success = false
end end
end end

View File

@ -1,5 +1,5 @@
require "./config"
require "./server" require "./server"
require "./context"
require "./mangadex/*" require "./mangadex/*"
require "option_parser" require "option_parser"
@ -24,18 +24,7 @@ OptionParser.parse do |parser|
end end
end end
config = Config.load config_path Config.load(config_path).set_current
logger = Logger.new config.log_level
storage = Storage.new config.db_path, logger
library = Library.new config.library_path, config.scan_interval, logger, storage
queue = MangaDex::Queue.new config.mangadex["download_queue_db_path"].to_s,
logger
api = MangaDex::API.new config.mangadex["api_url"].to_s
MangaDex::Downloader.new queue, api, config.library_path,
config.mangadex["download_wait_seconds"].to_i,
config.mangadex["download_retries"].to_i, logger
context = Context.new config, logger, library, storage, queue server = Server.new
server = Server.new context
server.start server.start

View File

@ -1,7 +1,7 @@
require "./router" require "./router"
class AdminRouter < Router class AdminRouter < Router
def setup def initialize
get "/admin" do |env| get "/admin" do |env|
layout "admin" layout "admin"
end end
@ -96,7 +96,7 @@ class AdminRouter < Router
end end
get "/admin/downloads" do |env| get "/admin/downloads" do |env|
base_url = @context.config.mangadex["base_url"] base_url = Config.current.mangadex["base_url"]
layout "download-manager" layout "download-manager"
end end
end end

View File

@ -3,7 +3,7 @@ require "../mangadex/*"
require "../upload" require "../upload"
class APIRouter < Router class APIRouter < Router
def setup def initialize
get "/api/page/:tid/:eid/:page" do |env| get "/api/page/:tid/:eid/:page" do |env|
begin begin
tid = env.params.url["tid"] tid = env.params.url["tid"]
@ -123,7 +123,7 @@ class APIRouter < Router
get "/api/admin/mangadex/manga/:id" do |env| get "/api/admin/mangadex/manga/:id" do |env|
begin begin
id = env.params.url["id"] id = env.params.url["id"]
api = MangaDex::API.new @context.config.mangadex["api_url"].to_s api = MangaDex::API.default
manga = api.get_manga id manga = api.get_manga id
send_json env, manga.to_info_json send_json env, manga.to_info_json
rescue e rescue e
@ -230,7 +230,7 @@ class APIRouter < Router
end end
ext = File.extname filename ext = File.extname filename
upload = Upload.new @context.config.upload_path, @context.logger upload = Upload.new Config.current.upload_path
url = upload.path_to_url upload.save "img", ext, part.body url = upload.path_to_url upload.save "img", ext, part.body
if url.nil? if url.nil?

View File

@ -1,7 +1,7 @@
require "./router" require "./router"
class MainRouter < Router class MainRouter < Router
def setup def initialize
get "/login" do |env| get "/login" do |env|
render "src/views/login.ecr" render "src/views/login.ecr"
end end
@ -59,7 +59,7 @@ class MainRouter < Router
end end
get "/download" do |env| get "/download" do |env|
base_url = @context.config.mangadex["base_url"] base_url = Config.current.mangadex["base_url"]
layout "download" layout "download"
end end
end end

View File

@ -1,7 +1,7 @@
require "./router" require "./router"
class ReaderRouter < Router class ReaderRouter < Router
def setup def initialize
get "/reader/:title/:entry" do |env| get "/reader/:title/:entry" do |env|
begin begin
title = (@context.library.get_title env.params.url["title"]).not_nil! title = (@context.library.get_title env.params.url["title"]).not_nil!

View File

@ -1,6 +1,3 @@
require "../context"
class Router class Router
def initialize(@context : Context) @context : Context = Context.default
end
end end

View File

@ -1,11 +1,38 @@
require "kemal" require "kemal"
require "./context" require "./library"
require "./handlers/*" require "./handlers/*"
require "./util" require "./util"
require "./routes/*" require "./routes/*"
class Context
property library : Library
property storage : Storage
property queue : MangaDex::Queue
def self.default
unless @@default
@@default = new
end
@@default.not_nil!
end
def initialize
@storage = Storage.default
@library = Library.default
@queue = MangaDex::Queue.default
end
{% for lvl in Logger::LEVELS %}
def {{lvl.id}}(msg)
Logger.{{lvl.id}} msg
end
{% end %}
end
class Server class Server
def initialize(@context : Context) @context : Context = Context.default
def initialize
error 403 do |env| error 403 do |env|
message = "HTTP 403: You are not authorized to visit #{env.request.path}" message = "HTTP 403: You are not authorized to visit #{env.request.path}"
layout "message" layout "message"
@ -19,15 +46,15 @@ class Server
layout "message" layout "message"
end end
MainRouter.new(@context).setup MainRouter.new
AdminRouter.new(@context).setup AdminRouter.new
ReaderRouter.new(@context).setup ReaderRouter.new
APIRouter.new(@context).setup APIRouter.new
Kemal.config.logging = false Kemal.config.logging = false
add_handler LogHandler.new @context.logger add_handler LogHandler.new
add_handler AuthHandler.new @context.storage add_handler AuthHandler.new @context.storage
add_handler UploadHandler.new @context.config.upload_path add_handler UploadHandler.new Config.current.upload_path
{% if flag?(:release) %} {% if flag?(:release) %}
# when building for relase, embed the static files in binary # when building for relase, embed the static files in binary
@context.debug "We are in release mode. Using embedded static files." @context.debug "We are in release mode. Using embedded static files."
@ -41,7 +68,7 @@ class Server
{% if flag?(:release) %} {% if flag?(:release) %}
Kemal.config.env = "production" Kemal.config.env = "production"
{% end %} {% end %}
Kemal.config.port = @context.config.port Kemal.config.port = Config.current.port
Kemal.run Kemal.run
end end
end end

View File

@ -13,14 +13,23 @@ def verify_password(hash, pw)
end end
class Storage class Storage
def initialize(@path : String, @logger : Logger) @path : String = Config.current.db_path
dir = File.dirname path
def self.default
unless @@default
@@default = new
end
@@default.not_nil!
end
def initialize
dir = File.dirname @path
unless Dir.exists? dir unless Dir.exists? dir
@logger.info "The DB directory #{dir} does not exist. " \ Logger.info "The DB directory #{dir} does not exist. " \
"Attepmting to create it" "Attepmting to create it"
Dir.mkdir_p dir Dir.mkdir_p dir
end end
DB.open "sqlite3://#{path}" do |db| DB.open "sqlite3://#{@path}" do |db|
begin begin
# We create the `ids` table first. even if the uses has an # We create the `ids` table first. even if the uses has an
# early version installed and has the `user` table only, # early version installed and has the `user` table only,
@ -34,18 +43,18 @@ class Storage
"(username text, password text, token text, admin integer)" "(username text, password text, token text, admin integer)"
rescue e rescue e
unless e.message.not_nil!.ends_with? "already exists" unless e.message.not_nil!.ends_with? "already exists"
@logger.fatal "Error when checking tables in DB: #{e}" Logger.fatal "Error when checking tables in DB: #{e}"
raise e raise e
end end
else else
@logger.debug "Creating DB file at #{@path}" Logger.debug "Creating DB file at #{@path}"
db.exec "create unique index username_idx on users (username)" db.exec "create unique index username_idx on users (username)"
db.exec "create unique index token_idx on users (token)" db.exec "create unique index token_idx on users (token)"
random_pw = random_str random_pw = random_str
hash = hash_password random_pw hash = hash_password random_pw
db.exec "insert into users values (?, ?, ?, ?)", db.exec "insert into users values (?, ?, ?, ?)",
"admin", hash, nil, 1 "admin", hash, nil, 1
@logger.log "Initial user created. You can log in with " \ Logger.log "Initial user created. You can log in with " \
"#{{"username" => "admin", "password" => random_pw}}" "#{{"username" => "admin", "password" => random_pw}}"
end end
end end
@ -58,18 +67,18 @@ class Storage
"users where username = (?)", "users where username = (?)",
username, as: {String, String?} username, as: {String, String?}
unless verify_password hash, password unless verify_password hash, password
@logger.debug "Password does not match the hash" Logger.debug "Password does not match the hash"
return nil return nil
end end
@logger.debug "User #{username} verified" Logger.debug "User #{username} verified"
return token if token return token if token
token = random_str token = random_str
@logger.debug "Updating token for #{username}" Logger.debug "Updating token for #{username}"
db.exec "update users set token = (?) where username = (?)", db.exec "update users set token = (?) where username = (?)",
token, username token, username
return token return token
rescue e rescue e
@logger.error "Error when verifying user #{username}: #{e}" Logger.error "Error when verifying user #{username}: #{e}"
return nil return nil
end end
end end
@ -82,7 +91,7 @@ class Storage
username = db.query_one "select username from users where " \ username = db.query_one "select username from users where " \
"token = (?)", token, as: String "token = (?)", token, as: String
rescue e rescue e
@logger.debug "Unable to verify token" Logger.debug "Unable to verify token"
end end
end end
username username
@ -95,7 +104,7 @@ class Storage
is_admin = db.query_one "select admin from users where " \ is_admin = db.query_one "select admin from users where " \
"token = (?)", token, as: Bool "token = (?)", token, as: Bool
rescue e rescue e
@logger.debug "Unable to verify user as admin" Logger.debug "Unable to verify user as admin"
end end
end end
is_admin is_admin

View File

@ -1,9 +1,9 @@
require "./util" require "./util"
class Upload class Upload
def initialize(@dir : String, @logger : Logger) def initialize(@dir : String)
unless Dir.exists? @dir unless Dir.exists? @dir
@logger.info "The uploads directory #{@dir} does not exist. " \ Logger.info "The uploads directory #{@dir} does not exist. " \
"Attempting to create it" "Attempting to create it"
Dir.mkdir_p @dir Dir.mkdir_p @dir
end end
@ -19,7 +19,7 @@ class Upload
file_path = File.join full_dir, filename file_path = File.join full_dir, filename
unless Dir.exists? full_dir unless Dir.exists? full_dir
@logger.debug "creating directory #{full_dir}" Logger.debug "creating directory #{full_dir}"
Dir.mkdir_p full_dir Dir.mkdir_p full_dir
end end
@ -50,7 +50,7 @@ class Upload
end end
if ary.empty? if ary.empty?
@logger.warn "File #{path} is not in the upload directory #{@dir}" Logger.warn "File #{path} is not in the upload directory #{@dir}"
return return
end end