diff --git a/src/auth_handler.cr b/src/auth_handler.cr index d6c8887..d6633d7 100644 --- a/src/auth_handler.cr +++ b/src/auth_handler.cr @@ -3,9 +3,7 @@ require "./storage" require "./util" class AuthHandler < Kemal::Handler - property storage : Storage - - def initialize(@storage) + def initialize(@storage : Storage) end def call(env) @@ -18,7 +16,7 @@ class AuthHandler < Kemal::Handler end if request_path_startswith env, ["/admin", "/api/admin"] - unless storage.verify_admin cookie.value + unless @storage.verify_admin cookie.value env.response.status_code = 403 end end diff --git a/src/config.cr b/src/config.cr index 44f7d6b..4f4bc41 100644 --- a/src/config.cr +++ b/src/config.cr @@ -4,17 +4,22 @@ class Config include YAML::Serializable @[YAML::Field(key: "port")] - property port = 9000 + property port : Int32 = 9000 @[YAML::Field(key: "library_path")] - property library_path = File.expand_path "~/mango/library", home: true + property library_path : String = \ + File.expand_path "~/mango/library", home: true @[YAML::Field(key: "db_path")] - property db_path = File.expand_path "~/mango/mango.db", home: true + property db_path : String = \ + File.expand_path "~/mango/mango.db", home: true @[YAML::Field(key: "scan_interval_minutes")] property scan_interval : Int32 = 5 + @[YAML::Field(key: "log_level")] + property log_level : String = "info" + def self.load cfg_path = File.expand_path "~/.config/mango/config.yml", home: true if File.exists? cfg_path diff --git a/src/context.cr b/src/context.cr index 578060d..e0f34ac 100644 --- a/src/context.cr +++ b/src/context.cr @@ -1,17 +1,20 @@ require "./config" require "./library" require "./storage" -require "logger" +require "./logger" class Context property config : Config property library : Library property storage : Storage + property logger : MLogger - def initialize - @config = Config.load - @library = Library.new @config.library_path, @config.scan_interval - @storage = Storage.new @config.db_path + def initialize(@config, @logger, @library, @storage) end + {% for lvl in LEVELS %} + def {{lvl.id}}(msg) + @logger.{{lvl.id}} msg + end + {% end %} end diff --git a/src/library.cr b/src/library.cr index 7654648..c527801 100644 --- a/src/library.cr +++ b/src/library.cr @@ -7,18 +7,12 @@ class Image property mime : String property filename : String property size : Int32 + def initialize(@data, @mime, @filename, @size) end end class Entry - property zip_path : String - property book_title : String - property title : String - property size : String - property pages : Int32 - property cover_url : String - JSON.mapping zip_path: String, book_title: String, title: String, \ size: String, pages: Int32, cover_url: String @@ -144,7 +138,7 @@ end class Library JSON.mapping dir: String, titles: Array(Title), scan_interval: Int32 - def initialize(@dir, @scan_interval) + def initialize(@dir, @scan_interval, logger) # explicitly initialize @titles to bypass the compiler check. it will # be filled with actual Titles in the `scan` call below @titles = [] of Title @@ -152,11 +146,11 @@ class Library return scan if @scan_interval < 1 spawn do loop do + logger.info "Starting periodic scan" start = Time.local - puts "#{start} Starting periodic scan" scan ms = (Time.local - start).total_milliseconds - puts "Scanned #{@titles.size} titles in #{ms}ms" + logger.info "Scanned #{@titles.size} titles in #{ms}ms" sleep @scan_interval * 60 end end diff --git a/src/log_handler.cr b/src/log_handler.cr new file mode 100644 index 0000000..4990c7f --- /dev/null +++ b/src/log_handler.cr @@ -0,0 +1,26 @@ +require "kemal" +require "./logger" + +class LogHandler < Kemal::BaseLogHandler + def initialize(@logger : MLogger) + end + + def call(env) + elapsed_time = Time.measure { call_next env } + elapsed_text = elapsed_text elapsed_time + msg = "#{env.response.status_code} #{env.request.method}" \ + " #{env.request.resource} #{elapsed_text}" + @logger.debug(msg) + env + end + + def write(msg) + @logger.debug(msg) + end + + private def elapsed_text(elapsed) + millis = elapsed.total_milliseconds + return "#{millis.round(2)}ms" if millis >= 1 + "#{(millis * 1000).round(2)}µs" + end +end diff --git a/src/logger.cr b/src/logger.cr new file mode 100644 index 0000000..53c4af8 --- /dev/null +++ b/src/logger.cr @@ -0,0 +1,55 @@ +require "./config" +require "logger" +require "colorize" + +LEVELS = ["debug", "error", "fatal", "info", "warn"] +COLORS = [:light_cyan, :light_red, :red, :light_yellow, :light_magenta] + +class MLogger + def initialize(config : Config) + @logger = Logger.new STDOUT + + @log_off = false + log_level = config.log_level + if log_level == "off" + @log_off = true + return + end + + {% begin %} + case log_level + {% for lvl in LEVELS %} + when {{lvl}} + @logger.level = Logger::{{lvl.upcase.id}} + {% end %} + else + raise "Unknown log level #{log_level}" + end + {% end %} + + @logger.formatter = Logger::Formatter.new do \ + |severity, datetime, progname, message, io| + + color = :default + {% begin %} + case severity.to_s().downcase + {% for lvl, i in LEVELS %} + when {{lvl}} + color = COLORS[{{i}}] + {% end %} + end + {% end %} + + io << "[#{severity}]".ljust(8).colorize(color) + io << datetime.to_s("%Y/%m/%d %H:%M:%S") << " | " + io << message + end + end + + {% for lvl in LEVELS %} + def {{lvl.id}}(msg) + return if @log_off + @logger.{{lvl.id}} msg + end + {% end %} +end diff --git a/src/mango.cr b/src/mango.cr index 6579e8a..66c082c 100644 --- a/src/mango.cr +++ b/src/mango.cr @@ -1,6 +1,16 @@ require "./server" require "./context" +require "./config" +require "./library" +require "./storage" +require "./logger" + +config = Config.load +logger = MLogger.new config +library = Library.new config.library_path, config.scan_interval, logger +storage = Storage.new config.db_path, logger + +context = Context.new config, logger, library, storage -context = Context.new server = Server.new context server.start diff --git a/src/server.cr b/src/server.cr index 39e3b5a..623b046 100644 --- a/src/server.cr +++ b/src/server.cr @@ -2,12 +2,11 @@ require "kemal" require "./context" require "./auth_handler" require "./static_handler" +require "./log_handler" require "./util" class Server - property context : Context - - def initialize(@context) + def initialize(@context : Context) error 403 do |env| message = "You are not authorized to visit #{env.request.path}" @@ -87,7 +86,7 @@ class Server env.redirect "/admin/user" rescue e - puts e.message + @context.error e.message redirect_url = URI.new \ path: "/admin/user/edit",\ query: hash_to_query({"error" => e.message}) @@ -127,7 +126,7 @@ class Server env.redirect "/admin/user" rescue e - puts e.message + @context.error e.message redirect_url = URI.new \ path: "/admin/user/edit",\ query: hash_to_query({"username" => original_username, \ @@ -234,7 +233,7 @@ class Server send_img env, img rescue e - STDERR.puts e + @context.error e.message env.response.status_code = 500 e.message end @@ -249,7 +248,7 @@ class Server send_json env, t.to_json rescue e - STDERR.puts e + @context.error e.message env.response.status_code = 500 e.message end @@ -303,6 +302,8 @@ class Server end end + Kemal.config.logging = false + add_handler LogHandler.new @context.logger add_handler AuthHandler.new @context.storage {% if flag?(:release) %} # when building for relase, embed the static files in binary diff --git a/src/static_handler.cr b/src/static_handler.cr index ae10c44..f35be8b 100644 --- a/src/static_handler.cr +++ b/src/static_handler.cr @@ -9,11 +9,7 @@ class FS end class StaticHandler < Kemal::Handler - property dirs : Array(String) - - def initialize - @dirs = ["/css", "/js"] - end + @dirs = ["/css", "/js"] def call(env) if request_path_startswith env, @dirs diff --git a/src/storage.cr b/src/storage.cr index 2b50c32..c42063d 100644 --- a/src/storage.cr +++ b/src/storage.cr @@ -16,10 +16,7 @@ def random_str end class Storage - property path : String - - def initialize(path) - @path = path + def initialize(@path : String, @logger : MLogger) dir = File.dirname path unless Dir.exists? dir Dir.mkdir_p dir @@ -39,7 +36,7 @@ class Storage hash = hash_password random_pw db.exec "insert into users values (?, ?, ?, ?)", "admin", hash, nil, 1 - puts "Initial user created. You can log in with " \ + @logger.info "Initial user created. You can log in with " \ "#{{"username" => "admin", "password" => random_pw}}" end end