diff --git a/spec/util_spec.cr b/spec/util_spec.cr index da2bb66..aa7ad07 100644 --- a/spec/util_spec.cr +++ b/spec/util_spec.cr @@ -1,10 +1,10 @@ require "./spec_helper" -describe "compare_alphanumerically" do +describe "compare_numerically" do it "sorts filenames with leading zeros correctly" do ary = ["010.jpg", "001.jpg", "002.png"] ary.sort! { |a, b| - compare_alphanumerically a, b + compare_numerically a, b } ary.should eq ["001.jpg", "002.png", "010.jpg"] end @@ -12,7 +12,7 @@ describe "compare_alphanumerically" do it "sorts filenames without leading zeros correctly" do ary = ["10.jpg", "1.jpg", "0.png", "0100.jpg"] ary.sort! { |a, b| - compare_alphanumerically a, b + compare_numerically a, b } ary.should eq ["0.png", "1.jpg", "10.jpg", "0100.jpg"] end @@ -22,7 +22,7 @@ describe "compare_alphanumerically" do ary = ["2", "12", "200000", "1000000", "a", "a12", "b2", "text2", "text2a", "text2a2", "text2a12", "text2ab", "text12", "text12a"] ary.reverse.sort { |a, b| - compare_alphanumerically a, b + compare_numerically a, b }.should eq ary end @@ -30,7 +30,7 @@ describe "compare_alphanumerically" do it "handles numbers larger than Int32" do ary = ["14410155591588.jpg", "21410155591588.png", "104410155591588.jpg"] ary.reverse.sort { |a, b| - compare_alphanumerically a, b + compare_numerically a, b }.should eq ary end end diff --git a/src/handlers/auth_handler.cr b/src/handlers/auth_handler.cr index f66c786..be4f6b1 100644 --- a/src/handlers/auth_handler.cr +++ b/src/handlers/auth_handler.cr @@ -1,6 +1,6 @@ require "kemal" require "../storage" -require "../util" +require "../util/*" class AuthHandler < Kemal::Handler # Some of the code is copied form kemalcr/kemal-basic-auth on GitHub diff --git a/src/handlers/static_handler.cr b/src/handlers/static_handler.cr index adaf3c1..b3a2d0e 100644 --- a/src/handlers/static_handler.cr +++ b/src/handlers/static_handler.cr @@ -1,6 +1,6 @@ require "baked_file_system" require "kemal" -require "../util" +require "../util/*" class FS extend BakedFileSystem diff --git a/src/handlers/upload_handler.cr b/src/handlers/upload_handler.cr index 0846814..d34b820 100644 --- a/src/handlers/upload_handler.cr +++ b/src/handlers/upload_handler.cr @@ -1,5 +1,5 @@ require "kemal" -require "../util" +require "../util/*" class UploadHandler < Kemal::Handler def initialize(@upload_dir : String) diff --git a/src/library.cr b/src/library.cr index 7bbfc5b..8c0d7fe 100644 --- a/src/library.cr +++ b/src/library.cr @@ -1,7 +1,7 @@ require "mime" require "json" require "uri" -require "./util" +require "./util/*" require "./archive" SUPPORTED_IMG_TYPES = ["image/jpeg", "image/png", "image/webp"] @@ -87,7 +87,7 @@ class Entry MIME.from_filename? e.filename } .sort { |a, b| - compare_alphanumerically a.filename, b.filename + compare_numerically a.filename, b.filename } .[page_num - 1] data = file.read_entry page @@ -236,11 +236,11 @@ class Title @mtime = mtimes.max @title_ids.sort! do |a, b| - compare_alphanumerically @library.title_hash[a].title, + compare_numerically @library.title_hash[a].title, @library.title_hash[b].title end @entries.sort! do |a, b| - compare_alphanumerically a.title, b.title + compare_numerically a.title, b.title end end diff --git a/src/server.cr b/src/server.cr index 68195f9..1879cfc 100644 --- a/src/server.cr +++ b/src/server.cr @@ -2,7 +2,7 @@ require "kemal" require "kemal-session" require "./library" require "./handlers/*" -require "./util" +require "./util/*" require "./routes/*" class Context diff --git a/src/storage.cr b/src/storage.cr index 0b0f2e0..a9fb144 100644 --- a/src/storage.cr +++ b/src/storage.cr @@ -2,7 +2,7 @@ require "sqlite3" require "crypto/bcrypt" require "uuid" require "base64" -require "./util" +require "./util/*" def hash_password(pw) Crypto::Bcrypt::Password.create(pw).to_s diff --git a/src/upload.cr b/src/upload.cr index 04fe51f..a4facca 100644 --- a/src/upload.cr +++ b/src/upload.cr @@ -1,4 +1,4 @@ -require "./util" +require "./util/*" class Upload def initialize(@dir : String) diff --git a/src/util.cr b/src/util.cr deleted file mode 100644 index 6d0d2cc..0000000 --- a/src/util.cr +++ /dev/null @@ -1,169 +0,0 @@ -require "big" - -IMGS_PER_PAGE = 5 -UPLOAD_URL_PREFIX = "/uploads" -STATIC_DIRS = ["/css", "/js", "/img", "/favicon.ico"] - -def requesting_static_file(env) - request_path_startswith env, STATIC_DIRS -end - -macro layout(name) - base_url = Config.current.base_url - begin - is_admin = false - if token = env.session.string? "token" - is_admin = @context.storage.verify_admin token - end - page = {{name}} - render "src/views/#{{{name}}}.html.ecr", "src/views/layout.html.ecr" - rescue e - message = e.to_s - @context.error message - render "src/views/message.html.ecr", "src/views/layout.html.ecr" - end -end - -macro send_img(env, img) - send_file {{env}}, {{img}}.data, {{img}}.mime -end - -macro get_username(env) - # if the request gets here, it has gone through the auth handler, and - # we can be sure that a valid token exists, so we can use not_nil! here - token = env.session.string "token" - (@context.storage.verify_token token).not_nil! -end - -def send_json(env, json) - env.response.content_type = "application/json" - env.response.print json -end - -def send_attachment(env, path) - send_file env, path, filename: File.basename(path), disposition: "attachment" -end - -def hash_to_query(hash) - hash.map { |k, v| "#{k}=#{v}" }.join("&") -end - -def request_path_startswith(env, ary) - ary.each do |prefix| - if env.request.path.starts_with? prefix - return true - end - end - false -end - -def is_numeric(str) - /^\d+/.match(str) != nil -end - -def split_by_alphanumeric(str) - arr = [] of String - str.scan(/([^\d\n\r]*)(\d*)([^\d\n\r]*)/) do |match| - arr += match.captures.select { |s| s != "" } - end - arr -end - -def compare_alphanumerically(c, d) - is_c_bigger = c.size <=> d.size - if c.size > d.size - d += [nil] * (c.size - d.size) - elsif c.size < d.size - c += [nil] * (d.size - c.size) - end - c.zip(d) do |a, b| - return -1 if a.nil? - return 1 if b.nil? - if is_numeric(a) && is_numeric(b) - compare = a.to_big_i <=> b.to_big_i - return compare if compare != 0 - else - compare = a <=> b - return compare if compare != 0 - end - end - is_c_bigger -end - -def compare_alphanumerically(a : String, b : String) - compare_alphanumerically split_by_alphanumeric(a), split_by_alphanumeric(b) -end - -def validate_archive(path : String) : Exception? - file = nil - begin - file = ArchiveFile.new path - file.check - file.close - return - rescue e - file.close unless file.nil? - e - end -end - -def random_str - UUID.random.to_s.gsub "-", "" -end - -def redirect(env, path) - base = Config.current.base_url - env.redirect File.join base, path -end - -def validate_username(username) - if username.size < 3 - raise "Username should contain at least 3 characters" - end - if (username =~ /^[A-Za-z0-9_]+$/).nil? - raise "Username should contain alphanumeric characters " \ - "and underscores only" - end -end - -def validate_password(password) - if password.size < 6 - raise "Password should contain at least 6 characters" - end - if (password =~ /^[[:ascii:]]+$/).nil? - raise "password should contain ASCII characters only" - end -end - -macro render_xml(path) - base_url = Config.current.base_url - send_file env, ECR.render({{path}}).to_slice, "application/xml" -end - -macro render_component(filename) - render "src/views/components/#{{{filename}}}.html.ecr" -end - -# Works in all Unix systems. Follows https://github.com/crystal-lang/crystal/ -# blob/master/src/crystal/system/unix/file_info.cr#L42-L48 -def ctime(file_path : String) : Time - res = LibC.stat(file_path, out stat) - raise "Unable to get ctime of file #{file_path}" if res != 0 - - {% if flag?(:darwin) %} - Time.new stat.st_ctimespec, Time::Location::UTC - {% else %} - Time.new stat.st_ctim, Time::Location::UTC - {% end %} -end - -def register_mime_types - { - ".zip" => "application/zip", - ".rar" => "application/x-rar-compressed", - ".cbz" => "application/vnd.comicbook+zip", - ".cbr" => "application/vnd.comicbook-rar", - }.each do |k, v| - MIME.register k, v - end -end diff --git a/src/util/numeric_sort.cr b/src/util/numeric_sort.cr new file mode 100644 index 0000000..81279a4 --- /dev/null +++ b/src/util/numeric_sort.cr @@ -0,0 +1,40 @@ +# Properly sort alphanumeric strings +# Used to sort the images files inside the archives +# https://github.com/hkalexling/Mango/issues/12 + +def is_numeric(str) + /^\d+/.match(str) != nil +end + +def split_by_alphanumeric(str) + arr = [] of String + str.scan(/([^\d\n\r]*)(\d*)([^\d\n\r]*)/) do |match| + arr += match.captures.select { |s| s != "" } + end + arr +end + +def compare_numerically(c, d) + is_c_bigger = c.size <=> d.size + if c.size > d.size + d += [nil] * (c.size - d.size) + elsif c.size < d.size + c += [nil] * (d.size - c.size) + end + c.zip(d) do |a, b| + return -1 if a.nil? + return 1 if b.nil? + if is_numeric(a) && is_numeric(b) + compare = a.to_big_i <=> b.to_big_i + return compare if compare != 0 + else + compare = a <=> b + return compare if compare != 0 + end + end + is_c_bigger +end + +def compare_numerically(a : String, b : String) + compare_numerically split_by_alphanumeric(a), split_by_alphanumeric(b) +end diff --git a/src/util/util.cr b/src/util/util.cr new file mode 100644 index 0000000..3064885 --- /dev/null +++ b/src/util/util.cr @@ -0,0 +1,33 @@ +require "big" + +IMGS_PER_PAGE = 5 +UPLOAD_URL_PREFIX = "/uploads" +STATIC_DIRS = ["/css", "/js", "/img", "/favicon.ico"] + +def random_str + UUID.random.to_s.gsub "-", "" +end + +# Works in all Unix systems. Follows https://github.com/crystal-lang/crystal/ +# blob/master/src/crystal/system/unix/file_info.cr#L42-L48 +def ctime(file_path : String) : Time + res = LibC.stat(file_path, out stat) + raise "Unable to get ctime of file #{file_path}" if res != 0 + + {% if flag?(:darwin) %} + Time.new stat.st_ctimespec, Time::Location::UTC + {% else %} + Time.new stat.st_ctim, Time::Location::UTC + {% end %} +end + +def register_mime_types + { + ".zip" => "application/zip", + ".rar" => "application/x-rar-compressed", + ".cbz" => "application/vnd.comicbook+zip", + ".cbr" => "application/vnd.comicbook-rar", + }.each do |k, v| + MIME.register k, v + end +end diff --git a/src/util/validation.cr b/src/util/validation.cr new file mode 100644 index 0000000..6141900 --- /dev/null +++ b/src/util/validation.cr @@ -0,0 +1,31 @@ +def validate_username(username) + if username.size < 3 + raise "Username should contain at least 3 characters" + end + if (username =~ /^[A-Za-z0-9_]+$/).nil? + raise "Username should contain alphanumeric characters " \ + "and underscores only" + end +end + +def validate_password(password) + if password.size < 6 + raise "Password should contain at least 6 characters" + end + if (password =~ /^[[:ascii:]]+$/).nil? + raise "password should contain ASCII characters only" + end +end + +def validate_archive(path : String) : Exception? + file = nil + begin + file = ArchiveFile.new path + file.check + file.close + return + rescue e + file.close unless file.nil? + e + end +end diff --git a/src/util/web.cr b/src/util/web.cr new file mode 100644 index 0000000..93efebd --- /dev/null +++ b/src/util/web.cr @@ -0,0 +1,68 @@ +# Web related helper functions/macros + +macro layout(name) + base_url = Config.current.base_url + begin + is_admin = false + if token = env.session.string? "token" + is_admin = @context.storage.verify_admin token + end + page = {{name}} + render "src/views/#{{{name}}}.html.ecr", "src/views/layout.html.ecr" + rescue e + message = e.to_s + @context.error message + render "src/views/message.html.ecr", "src/views/layout.html.ecr" + end +end + +macro send_img(env, img) + send_file {{env}}, {{img}}.data, {{img}}.mime +end + +macro get_username(env) + # if the request gets here, it has gone through the auth handler, and + # we can be sure that a valid token exists, so we can use not_nil! here + token = env.session.string "token" + (@context.storage.verify_token token).not_nil! +end + +def send_json(env, json) + env.response.content_type = "application/json" + env.response.print json +end + +def send_attachment(env, path) + send_file env, path, filename: File.basename(path), disposition: "attachment" +end + +def redirect(env, path) + base = Config.current.base_url + env.redirect File.join base, path +end + +def hash_to_query(hash) + hash.map { |k, v| "#{k}=#{v}" }.join("&") +end + +def request_path_startswith(env, ary) + ary.each do |prefix| + if env.request.path.starts_with? prefix + return true + end + end + false +end + +def requesting_static_file(env) + request_path_startswith env, STATIC_DIRS +end + +macro render_xml(path) + base_url = Config.current.base_url + send_file env, ECR.render({{path}}).to_slice, "application/xml" +end + +macro render_component(filename) + render "src/views/components/#{{{filename}}}.html.ecr" +end