From 803fc8c44b67113cfad3a84ec72957838f664c75 Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Thu, 20 Feb 2020 23:39:54 +0000 Subject: [PATCH 1/6] Split routes in `server.cr` into small files --- src/routes/admin.cr | 103 +++++++++++++++ src/routes/api.cr | 92 ++++++++++++++ src/routes/main.cr | 56 ++++++++ src/routes/reader.cr | 58 +++++++++ src/routes/router.cr | 6 + src/server.cr | 295 +------------------------------------------ 6 files changed, 320 insertions(+), 290 deletions(-) create mode 100644 src/routes/admin.cr create mode 100644 src/routes/api.cr create mode 100644 src/routes/main.cr create mode 100644 src/routes/reader.cr create mode 100644 src/routes/router.cr diff --git a/src/routes/admin.cr b/src/routes/admin.cr new file mode 100644 index 00000000..899ddb62 --- /dev/null +++ b/src/routes/admin.cr @@ -0,0 +1,103 @@ +require "./router" + +class AdminRouter < Router + def setup + get "/admin" do |env| + layout "admin" + end + + get "/admin/user" do |env| + users = @context.storage.list_users + username = get_username env + layout "user" + end + + get "/admin/user/edit" do |env| + username = env.params.query["username"]? + admin = env.params.query["admin"]? + if admin + admin = admin == "true" + end + error = env.params.query["error"]? + current_user = get_username env + new_user = username.nil? && admin.nil? + layout "user-edit" + end + + post "/admin/user/edit" do |env| + # creating new user + begin + username = env.params.body["username"] + password = env.params.body["password"] + # if `admin` is unchecked, the body hash + # would not contain `admin` + admin = !env.params.body["admin"]?.nil? + + 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 + 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 + + @context.storage.new_user username, password, admin + + env.redirect "/admin/user" + rescue e + @context.error e + redirect_url = URI.new \ + path: "/admin/user/edit",\ + query: hash_to_query({"error" => e.message}) + env.redirect redirect_url.to_s + end + end + + post "/admin/user/edit/:original_username" do |env| + # editing existing user + begin + username = env.params.body["username"] + password = env.params.body["password"] + # if `admin` is unchecked, the body + # hash would not contain `admin` + admin = !env.params.body["admin"]?.nil? + original_username = env.params.url["original_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 + + if password.size != 0 + 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 + + @context.storage.update_user \ + original_username, username, password, admin + + env.redirect "/admin/user" + rescue e + @context.error e + redirect_url = URI.new \ + path: "/admin/user/edit",\ + query: hash_to_query({"username" => original_username, \ + "admin" => admin, "error" => e.message}) + env.redirect redirect_url.to_s + end + end + end +end diff --git a/src/routes/api.cr b/src/routes/api.cr new file mode 100644 index 00000000..478a2ff4 --- /dev/null +++ b/src/routes/api.cr @@ -0,0 +1,92 @@ +require "./router" + +class APIRouter < Router + def setup + get "/api/page/:title/:entry/:page" do |env| + begin + title = env.params.url["title"] + entry = env.params.url["entry"] + page = env.params.url["page"].to_i + + t = @context.library.get_title title + raise "Title `#{title}` not found" if t.nil? + e = t.get_entry entry + raise "Entry `#{entry}` of `#{title}` not found" if e.nil? + img = e.read_page page + raise "Failed to load page #{page} of `#{title}/#{entry}`"\ + if img.nil? + + send_img env, img + rescue e + @context.error e + env.response.status_code = 500 + e.message + end + end + + get "/api/book/:title" do |env| + begin + title = env.params.url["title"] + + t = @context.library.get_title title + raise "Title `#{title}` not found" if t.nil? + + send_json env, t.to_json + rescue e + @context.error e + env.response.status_code = 500 + e.message + end + end + + get "/api/book" do |env| + send_json env, @context.library.to_json + end + + post "/api/admin/scan" do |env| + start = Time.utc + @context.library.scan + ms = (Time.utc - start).total_milliseconds + send_json env, { + "milliseconds" => ms, + "titles" => @context.library.titles.size + }.to_json + end + + post "/api/admin/user/delete/:username" do |env| + begin + username = env.params.url["username"] + @context.storage.delete_user username + rescue e + @context.error e + send_json env, { + "success" => false, + "error" => e.message + }.to_json + else + send_json env, {"success" => true}.to_json + end + end + + post "/api/progress/:title/:entry/:page" do |env| + begin + username = get_username env + title = (@context.library.get_title env.params.url["title"]) + .not_nil! + entry = (title.get_entry env.params.url["entry"]).not_nil! + page = env.params.url["page"].to_i + + raise "incorrect page value" if page < 0 || page > entry.pages + title.save_progress username, entry.title, page + rescue e + @context.error e + send_json env, { + "success" => false, + "error" => e.message + }.to_json + else + send_json env, {"success" => true}.to_json + end + end + end +end diff --git a/src/routes/main.cr b/src/routes/main.cr new file mode 100644 index 00000000..1327a36e --- /dev/null +++ b/src/routes/main.cr @@ -0,0 +1,56 @@ +require "./router" + +class MainRouter < Router + def setup + get "/login" do |env| + render "src/views/login.ecr" + end + + get "/logout" do |env| + begin + cookie = env.request.cookies + .find { |c| c.name == "token" }.not_nil! + @context.storage.logout cookie.value + rescue e + @context.error "Error when attempting to log out: #{e}" + ensure + env.redirect "/login" + end + end + + post "/login" do |env| + begin + username = env.params.body["username"] + password = env.params.body["password"] + token = @context.storage.verify_user(username, password) + .not_nil! + + cookie = HTTP::Cookie.new "token", token + env.response.cookies << cookie + env.redirect "/" + rescue + env.redirect "/login" + end + end + get "/" do |env| + titles = @context.library.titles + username = get_username env + percentage = titles.map &.load_percetage username + layout "index" + end + + get "/book/:title" do |env| + begin + title = (@context.library.get_title env.params.url["title"]) + .not_nil! + username = get_username env + percentage = title.entries.map { |e| + title.load_percetage username, e.title } + layout "title" + rescue e + @context.error e + env.response.status_code = 404 + end + end + end +end diff --git a/src/routes/reader.cr b/src/routes/reader.cr new file mode 100644 index 00000000..7c8984e4 --- /dev/null +++ b/src/routes/reader.cr @@ -0,0 +1,58 @@ +require "./router" + +class ReaderRouter < Router + def setup + get "/reader/:title/:entry" do |env| + begin + title = (@context.library.get_title env.params.url["title"]) + .not_nil! + entry = (title.get_entry env.params.url["entry"]).not_nil! + + # load progress + username = get_username env + page = title.load_progress username, entry.title + # we go back 2 * `IMGS_PER_PAGE` pages. the infinite scroll + # library perloads a few pages in advance, and the user + # might not have actually read them + page = [page - 2 * IMGS_PER_PAGE, 1].max + + env.redirect "/reader/#{title.title}/#{entry.title}/#{page}" + rescue e + @context.error e + env.response.status_code = 404 + end + end + + get "/reader/:title/:entry/:page" do |env| + begin + title = (@context.library.get_title env.params.url["title"]) + .not_nil! + entry = (title.get_entry env.params.url["entry"]).not_nil! + page = env.params.url["page"].to_i + raise "" if page > entry.pages || page <= 0 + + # save progress + username = get_username env + title.save_progress username, entry.title, page + + pages = (page...[entry.pages + 1, page + IMGS_PER_PAGE].min) + urls = pages.map { |idx| + "/api/page/#{title.title}/#{entry.title}/#{idx}" } + reader_urls = pages.map { |idx| + "/reader/#{title.title}/#{entry.title}/#{idx}" } + next_page = page + IMGS_PER_PAGE + next_url = next_page > entry.pages ? nil : + "/reader/#{title.title}/#{entry.title}/#{next_page}" + exit_url = "/book/#{title.title}" + next_entry = title.next_entry entry + next_entry_url = next_entry.nil? ? nil : \ + "/reader/#{title.title}/#{next_entry.title}" + + render "src/views/reader.ecr" + rescue e + @context.error e + env.response.status_code = 404 + end + end + end +end diff --git a/src/routes/router.cr b/src/routes/router.cr new file mode 100644 index 00000000..16389fb8 --- /dev/null +++ b/src/routes/router.cr @@ -0,0 +1,6 @@ +require "../context" + +class Router + def initialize(@context : Context) + end +end diff --git a/src/server.cr b/src/server.cr index e84292b2..786fd33f 100644 --- a/src/server.cr +++ b/src/server.cr @@ -4,6 +4,7 @@ require "./auth_handler" require "./static_handler" require "./log_handler" require "./util" +require "./routes/*" class Server def initialize(@context : Context) @@ -13,296 +14,10 @@ class Server layout "message" end - get "/" do |env| - titles = @context.library.titles - username = get_username env - percentage = titles.map &.load_percetage username - layout "index" - end - - get "/book/:title" do |env| - begin - title = (@context.library.get_title env.params.url["title"]) - .not_nil! - username = get_username env - percentage = title.entries.map { |e| - title.load_percetage username, e.title } - layout "title" - rescue e - @context.error e - env.response.status_code = 404 - end - end - - get "/admin" do |env| - layout "admin" - end - - get "/admin/user" do |env| - users = @context.storage.list_users - username = get_username env - layout "user" - end - - get "/admin/user/edit" do |env| - username = env.params.query["username"]? - admin = env.params.query["admin"]? - if admin - admin = admin == "true" - end - error = env.params.query["error"]? - current_user = get_username env - new_user = username.nil? && admin.nil? - layout "user-edit" - end - - post "/admin/user/edit" do |env| - # creating new user - begin - username = env.params.body["username"] - password = env.params.body["password"] - # if `admin` is unchecked, the body hash - # would not contain `admin` - admin = !env.params.body["admin"]?.nil? - - 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 - 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 - - @context.storage.new_user username, password, admin - - env.redirect "/admin/user" - rescue e - @context.error e - redirect_url = URI.new \ - path: "/admin/user/edit",\ - query: hash_to_query({"error" => e.message}) - env.redirect redirect_url.to_s - end - end - - post "/admin/user/edit/:original_username" do |env| - # editing existing user - begin - username = env.params.body["username"] - password = env.params.body["password"] - # if `admin` is unchecked, the body - # hash would not contain `admin` - admin = !env.params.body["admin"]?.nil? - original_username = env.params.url["original_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 - - if password.size != 0 - 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 - - @context.storage.update_user \ - original_username, username, password, admin - - env.redirect "/admin/user" - rescue e - @context.error e - redirect_url = URI.new \ - path: "/admin/user/edit",\ - query: hash_to_query({"username" => original_username, \ - "admin" => admin, "error" => e.message}) - env.redirect redirect_url.to_s - end - end - - - get "/reader/:title/:entry" do |env| - begin - title = (@context.library.get_title env.params.url["title"]) - .not_nil! - entry = (title.get_entry env.params.url["entry"]).not_nil! - - # load progress - username = get_username env - page = title.load_progress username, entry.title - # we go back 2 * `IMGS_PER_PAGE` pages. the infinite scroll - # library perloads a few pages in advance, and the user - # might not have actually read them - page = [page - 2 * IMGS_PER_PAGE, 1].max - - env.redirect "/reader/#{title.title}/#{entry.title}/#{page}" - rescue e - @context.error e - env.response.status_code = 404 - end - end - - get "/reader/:title/:entry/:page" do |env| - begin - title = (@context.library.get_title env.params.url["title"]) - .not_nil! - entry = (title.get_entry env.params.url["entry"]).not_nil! - page = env.params.url["page"].to_i - raise "" if page > entry.pages || page <= 0 - - # save progress - username = get_username env - title.save_progress username, entry.title, page - - pages = (page...[entry.pages + 1, page + IMGS_PER_PAGE].min) - urls = pages.map { |idx| - "/api/page/#{title.title}/#{entry.title}/#{idx}" } - reader_urls = pages.map { |idx| - "/reader/#{title.title}/#{entry.title}/#{idx}" } - next_page = page + IMGS_PER_PAGE - next_url = next_page > entry.pages ? nil : - "/reader/#{title.title}/#{entry.title}/#{next_page}" - exit_url = "/book/#{title.title}" - next_entry = title.next_entry entry - next_entry_url = next_entry.nil? ? nil : \ - "/reader/#{title.title}/#{next_entry.title}" - - render "src/views/reader.ecr" - rescue e - @context.error e - env.response.status_code = 404 - end - end - - get "/login" do |env| - render "src/views/login.ecr" - end - - get "/logout" do |env| - begin - cookie = env.request.cookies - .find { |c| c.name == "token" }.not_nil! - @context.storage.logout cookie.value - rescue e - @context.error "Error when attempting to log out: #{e}" - ensure - env.redirect "/login" - end - end - - post "/login" do |env| - begin - username = env.params.body["username"] - password = env.params.body["password"] - token = @context.storage.verify_user(username, password) - .not_nil! - - cookie = HTTP::Cookie.new "token", token - env.response.cookies << cookie - env.redirect "/" - rescue - env.redirect "/login" - end - end - - get "/api/page/:title/:entry/:page" do |env| - begin - title = env.params.url["title"] - entry = env.params.url["entry"] - page = env.params.url["page"].to_i - - t = @context.library.get_title title - raise "Title `#{title}` not found" if t.nil? - e = t.get_entry entry - raise "Entry `#{entry}` of `#{title}` not found" if e.nil? - img = e.read_page page - raise "Failed to load page #{page} of `#{title}/#{entry}`"\ - if img.nil? - - send_img env, img - rescue e - @context.error e - env.response.status_code = 500 - e.message - end - end - - get "/api/book/:title" do |env| - begin - title = env.params.url["title"] - - t = @context.library.get_title title - raise "Title `#{title}` not found" if t.nil? - - send_json env, t.to_json - rescue e - @context.error e - env.response.status_code = 500 - e.message - end - end - - get "/api/book" do |env| - send_json env, @context.library.to_json - end - - post "/api/admin/scan" do |env| - start = Time.utc - @context.library.scan - ms = (Time.utc - start).total_milliseconds - send_json env, { - "milliseconds" => ms, - "titles" => @context.library.titles.size - }.to_json - end - - post "/api/admin/user/delete/:username" do |env| - begin - username = env.params.url["username"] - @context.storage.delete_user username - rescue e - @context.error e - send_json env, { - "success" => false, - "error" => e.message - }.to_json - else - send_json env, {"success" => true}.to_json - end - end - - post "/api/progress/:title/:entry/:page" do |env| - begin - username = get_username env - title = (@context.library.get_title env.params.url["title"]) - .not_nil! - entry = (title.get_entry env.params.url["entry"]).not_nil! - page = env.params.url["page"].to_i - - raise "incorrect page value" if page < 0 || page > entry.pages - title.save_progress username, entry.title, page - rescue e - @context.error e - send_json env, { - "success" => false, - "error" => e.message - }.to_json - else - send_json env, {"success" => true}.to_json - end - end + MainRouter.new(@context).setup + AdminRouter.new(@context).setup + ReaderRouter.new(@context).setup + APIRouter.new(@context).setup Kemal.config.logging = false add_handler LogHandler.new @context.logger From 5b34c05243bd80d06eedc7fd9697f92b82d5b1f2 Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Sat, 22 Feb 2020 02:12:01 +0000 Subject: [PATCH 2/6] Use Babel, so I can write modern JS and save my sanity --- gulpfile.js | 4 ++-- package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 0f34fcd0..b088d3ae 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,10 +1,10 @@ const gulp = require('gulp'); -const uglify = require('gulp-uglify'); +const minify = require("gulp-babel-minify"); const minifyCss = require('gulp-minify-css'); gulp.task('minify-js', () => { return gulp.src('public/js/*.js') - .pipe(uglify()) + .pipe(minify()) .pipe(gulp.dest('dist/js')); }); diff --git a/package.json b/package.json index 4522d0e0..dd815a69 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "license": "MIT", "devDependencies": { "gulp": "^4.0.2", - "gulp-minify-css": "^1.2.4", - "gulp-uglify": "^3.0.2" + "gulp-babel-minify": "^0.5.1", + "gulp-minify-css": "^1.2.4" }, "scripts": { "uglify": "gulp" From 650ebc7f9d3387100df659ac4ad58316f1864848 Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Sun, 23 Feb 2020 18:35:27 +0000 Subject: [PATCH 3/6] Fix #6 --- public/js/title.js | 17 ++++++++++------- src/library.cr | 45 ++++++++++++++++++++++++++++---------------- src/mango.cr | 2 +- src/routes/api.cr | 30 ++++++++++++++--------------- src/routes/reader.cr | 12 ++++++------ src/storage.cr | 31 ++++++++++++++++++++++++++++-- src/views/index.ecr | 2 +- src/views/title.ecr | 2 +- 8 files changed, 92 insertions(+), 49 deletions(-) diff --git a/public/js/title.js b/public/js/title.js index 077ee13c..e2d236da 100644 --- a/public/js/title.js +++ b/public/js/title.js @@ -1,4 +1,7 @@ -function showModal(title, zipPath, pages, percentage, title, entry) { +function showModal(encodedPath, pages, percentage, encodedeTitle, encodedEntryTitle, titleID, entryID) { + const zipPath = decodeURIComponent(encodedPath); + const title = decodeURIComponent(encodedeTitle); + const entry = decodeURIComponent(encodedEntryTitle); $('#modal button, #modal a').each(function(){ $(this).removeAttr('hidden'); }); @@ -16,20 +19,20 @@ function showModal(title, zipPath, pages, percentage, title, entry) { $('#path-text').text(zipPath); $('#pages-text').text(pages + ' pages'); - $('#beginning-btn').attr('href', '/reader/' + title + '/' + entry + '/1'); - $('#continue-btn').attr('href', '/reader/' + title + '/' + entry); + $('#beginning-btn').attr('href', '/reader/' + titleID + '/' + entryID + '/1'); + $('#continue-btn').attr('href', '/reader/' + titleID + '/' + entryID); $('#read-btn').click(function(){ - updateProgress(title, entry, pages); + updateProgress(titleID, entryID, pages); }); $('#unread-btn').click(function(){ - updateProgress(title, entry, 0); + updateProgress(titleID, entryID, 0); }); UIkit.modal($('#modal')).show(); } -function updateProgress(title, entry, page) { - $.post('/api/progress/' + title + '/' + entry + '/' + page, function(data) { +function updateProgress(titleID, entryID, page) { + $.post('/api/progress/' + titleID + '/' + entryID + '/' + page, function(data) { if (data.success) { location.reload(); } diff --git a/src/library.cr b/src/library.cr index 176772a3..eda954a3 100644 --- a/src/library.cr +++ b/src/library.cr @@ -1,6 +1,7 @@ require "zip" require "mime" require "json" +require "uri" struct Image property data : Bytes @@ -14,11 +15,14 @@ end class Entry JSON.mapping zip_path: String, book_title: String, title: String, \ - size: String, pages: Int32, cover_url: String + size: String, pages: Int32, cover_url: String, id: String, \ + title_id: String, encoded_path: String, encoded_title: String - def initialize(path, @book_title) + def initialize(path, @book_title, @title_id, storage) @zip_path = path + @encoded_path = URI.encode path @title = File.basename path, File.extname path + @encoded_title = URI.encode @title @size = (File.size path).humanize_bytes @pages = Zip::File.new(path).entries .select { |e| @@ -26,7 +30,8 @@ class Entry MIME.from_filename? e.filename } .size - @cover_url = "/api/page/#{@book_title}/#{title}/1" + @id = storage.get_id @zip_path, false + @cover_url = "/api/page/#{@title_id}/#{@id}/1" end def read_page(page_num) Zip::File.open @zip_path do |file| @@ -51,20 +56,27 @@ class Entry end class Title - JSON.mapping dir: String, entries: Array(Entry), title: String + JSON.mapping dir: String, entries: Array(Entry), title: String, + id: String, encoded_title: String - def initialize(dir : String) + def initialize(dir : String, storage) @dir = dir + @id = storage.get_id @dir, true @title = File.basename dir + @encoded_title = URI.encode @title @entries = (Dir.entries dir) .select { |path| [".zip", ".cbz"].includes? File.extname path } - .map { |path| Entry.new File.join(dir, path), @title } + .map { |path| + Entry.new File.join(dir, path), @title, @id, storage + } .select { |e| e.pages > 0 } .sort { |a, b| a.title <=> b.title } end - def get_entry(name) - @entries.find { |e| e.title == name } + def get_entry(eid) + @entries.find { |e| e.id == eid } end + # For backward backward compatibility with v0.1.0, we save entry titles + # instead of IDs in info.json def save_progress(username, entry, page) info = TitleInfo.new @dir if info.progress[username]?.nil? @@ -75,7 +87,7 @@ class Title info.progress[username][entry] = page info.save @dir end - def load_progress(username, entry : String) + def load_progress(username, entry) info = TitleInfo.new @dir if info.progress[username]?.nil? return 0 @@ -85,10 +97,10 @@ class Title end info.progress[username][entry] end - def load_percetage(username, entry : String) + def load_percetage(username, entry) info = TitleInfo.new @dir page = load_progress username, entry - entry_obj = get_entry entry + entry_obj = @entries.find{|e| e.title == entry} return 0 if entry_obj.nil? page / entry_obj.pages end @@ -136,9 +148,10 @@ class TitleInfo end class Library - JSON.mapping dir: String, titles: Array(Title), scan_interval: Int32, logger: MLogger + JSON.mapping dir: String, titles: Array(Title), scan_interval: Int32, + logger: MLogger, storage: Storage - def initialize(@dir, @scan_interval, @logger) + def initialize(@dir, @scan_interval, @logger, @storage) # explicitly initialize @titles to bypass the compiler check. it will # be filled with actual Titles in the `scan` call below @titles = [] of Title @@ -154,8 +167,8 @@ class Library end end end - def get_title(name) - @titles.find { |t| t.title == name } + def get_title(tid) + @titles.find { |t| t.id == tid } end def scan unless Dir.exists? @dir @@ -165,7 +178,7 @@ class Library end @titles = (Dir.entries @dir) .select { |path| File.directory? File.join @dir, path } - .map { |path| Title.new File.join @dir, path } + .map { |path| Title.new File.join(@dir, path), @storage } .select { |title| !title.entries.empty? } @logger.debug "Scan completed" @logger.debug "Scanned library: \n#{self.to_pretty_json}" diff --git a/src/mango.cr b/src/mango.cr index f45c4960..38616ff4 100644 --- a/src/mango.cr +++ b/src/mango.cr @@ -25,8 +25,8 @@ end config = Config.load config_path logger = MLogger.new config -library = Library.new config.library_path, config.scan_interval, logger storage = Storage.new config.db_path, logger +library = Library.new config.library_path, config.scan_interval, logger, storage context = Context.new config, logger, library, storage diff --git a/src/routes/api.cr b/src/routes/api.cr index 478a2ff4..0a752bcd 100644 --- a/src/routes/api.cr +++ b/src/routes/api.cr @@ -2,19 +2,20 @@ require "./router" class APIRouter < Router def setup - get "/api/page/:title/:entry/:page" do |env| + get "/api/page/:tid/:eid/:page" do |env| begin - title = env.params.url["title"] - entry = env.params.url["entry"] + tid = env.params.url["tid"] + eid = env.params.url["eid"] page = env.params.url["page"].to_i - t = @context.library.get_title title - raise "Title `#{title}` not found" if t.nil? - e = t.get_entry entry - raise "Entry `#{entry}` of `#{title}` not found" if e.nil? - img = e.read_page page - raise "Failed to load page #{page} of `#{title}/#{entry}`"\ - if img.nil? + title = @context.library.get_title tid + raise "Title ID `#{tid}` not found" if title.nil? + entry = title.get_entry eid + raise "Entry ID `#{eid}` of `#{title.title}` not found" if \ + entry.nil? + img = entry.read_page page + raise "Failed to load page #{page} of " \ + "`#{title.title}/#{entry.title}`" if img.nil? send_img env, img rescue e @@ -26,12 +27,11 @@ class APIRouter < Router get "/api/book/:title" do |env| begin - title = env.params.url["title"] + tid = env.params.url["tid"] + title = @context.library.get_title tid + raise "Title ID `#{tid}` not found" if title.nil? - t = @context.library.get_title title - raise "Title `#{title}` not found" if t.nil? - - send_json env, t.to_json + send_json env, title.to_json rescue e @context.error e env.response.status_code = 500 diff --git a/src/routes/reader.cr b/src/routes/reader.cr index 7c8984e4..266396da 100644 --- a/src/routes/reader.cr +++ b/src/routes/reader.cr @@ -16,7 +16,7 @@ class ReaderRouter < Router # might not have actually read them page = [page - 2 * IMGS_PER_PAGE, 1].max - env.redirect "/reader/#{title.title}/#{entry.title}/#{page}" + env.redirect "/reader/#{title.id}/#{entry.id}/#{page}" rescue e @context.error e env.response.status_code = 404 @@ -37,16 +37,16 @@ class ReaderRouter < Router pages = (page...[entry.pages + 1, page + IMGS_PER_PAGE].min) urls = pages.map { |idx| - "/api/page/#{title.title}/#{entry.title}/#{idx}" } + "/api/page/#{title.id}/#{entry.id}/#{idx}" } reader_urls = pages.map { |idx| - "/reader/#{title.title}/#{entry.title}/#{idx}" } + "/reader/#{title.id}/#{entry.id}/#{idx}" } next_page = page + IMGS_PER_PAGE next_url = next_page > entry.pages ? nil : - "/reader/#{title.title}/#{entry.title}/#{next_page}" - exit_url = "/book/#{title.title}" + "/reader/#{title.id}/#{entry.id}/#{next_page}" + exit_url = "/book/#{title.id}" next_entry = title.next_entry entry next_entry_url = next_entry.nil? ? nil : \ - "/reader/#{title.title}/#{next_entry.title}" + "/reader/#{title.id}/#{next_entry.id}" render "src/views/reader.ecr" rescue e diff --git a/src/storage.cr b/src/storage.cr index 7e57199f..8d20e153 100644 --- a/src/storage.cr +++ b/src/storage.cr @@ -12,7 +12,7 @@ def verify_password(hash, pw) end def random_str - Base64.strict_encode UUID.random().to_s + UUID.random.to_s.gsub "-", "" end class Storage @@ -25,10 +25,18 @@ class Storage end DB.open "sqlite3://#{path}" do |db| begin + # We create the `ids` table first. even if the uses has an + # early version installed and has the `user` table only, + # we will still be able to create `ids` + db.exec "create table ids" \ + "(path text, id text, is_title integer)" + db.exec "create unique index path_idx on ids (path)" + db.exec "create unique index id_idx on ids (id)" + db.exec "create table users" \ "(username text, password text, token text, admin integer)" rescue e - unless e.message == "table users already exists" + unless e.message.not_nil!.ends_with? "already exists" @logger.fatal "Error when checking tables in DB: #{e}" raise e end @@ -147,4 +155,23 @@ class Storage end end end + + def get_id(path, is_title) + DB.open "sqlite3://#{@path}" do |db| + begin + id = db.query_one "select id from ids where path = (?)", + path, as: {String} + return id + rescue + id = random_str + db.exec "insert into ids values (?, ?, ?)", path, id, + is_title ? 1 : 0 + return id + end + end + end + + def to_json(json : JSON::Builder) + json.string self + end end diff --git a/src/views/index.ecr b/src/views/index.ecr index ae384b5f..05a7de70 100644 --- a/src/views/index.ecr +++ b/src/views/index.ecr @@ -9,7 +9,7 @@
<%- titles.each_with_index do |t, i| -%>
- +
diff --git a/src/views/title.ecr b/src/views/title.ecr index bec82c36..42546ac5 100644 --- a/src/views/title.ecr +++ b/src/views/title.ecr @@ -11,7 +11,7 @@ <%- title.entries.each_with_index do |e, i| -%>
-
+
From 7a21f4dc9b33200182268e075f094b68bbafdd82 Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Sat, 22 Feb 2020 18:15:40 +0000 Subject: [PATCH 4/6] Sort titles in library by name by default --- src/library.cr | 1 + 1 file changed, 1 insertion(+) diff --git a/src/library.cr b/src/library.cr index eda954a3..5b72dc78 100644 --- a/src/library.cr +++ b/src/library.cr @@ -180,6 +180,7 @@ class Library .select { |path| File.directory? File.join @dir, path } .map { |path| Title.new File.join(@dir, path), @storage } .select { |title| !title.entries.empty? } + .sort { |a, b| a.title <=> b.title } @logger.debug "Scan completed" @logger.debug "Scanned library: \n#{self.to_pretty_json}" end From a7519a791e00b77225e26e5dfa69f41eea3d7eaa Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Sun, 23 Feb 2020 19:18:30 +0000 Subject: [PATCH 5/6] Fix the problem that minified assets in dist/ are not used. --- src/static_handler.cr | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/static_handler.cr b/src/static_handler.cr index 9162b00e..c7f9caca 100644 --- a/src/static_handler.cr +++ b/src/static_handler.cr @@ -5,9 +5,11 @@ require "./util" class FS extend BakedFileSystem - {% if read_file? "./dist" %} + {% if read_file? "#{__DIR__}/../dist/favicon.ico" %} + {% puts "baking ../dist" %} bake_folder "../dist" {% else %} + {% puts "baking ../public" %} bake_folder "../public" {% end %} end From ac620e1f2a230e450f256a6cba881d9d1dd8355c Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Sun, 23 Feb 2020 21:09:50 +0000 Subject: [PATCH 6/6] Update docker-compose example --- docker-compose.example.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 9f2f7002..7ac0d6cf 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -11,5 +11,5 @@ services: ports: - 9000:9000 volumes: - - ./mango:/root/mango - - ./config:/root/.config/mango + - ~/mango:/root/mango + - ~/.config/mango:/root/.config/mango