Compare commits

...

11 Commits

Author SHA1 Message Date
Alex Ling bf0f5270f0 Merge branch 'v0.1.1' 2020-02-23 21:16:23 +00:00
Alex Ling ac620e1f2a Update docker-compose example 2020-02-23 21:09:50 +00:00
Alex Ling a7519a791e Fix the problem that minified assets in dist/ are not used. 2020-02-23 19:18:30 +00:00
Alex Ling 7a21f4dc9b Sort titles in library by name by default 2020-02-23 18:36:10 +00:00
Alex Ling 650ebc7f9d Fix #6 2020-02-23 18:35:27 +00:00
Alex Ling 5b34c05243 Use Babel, so I can write modern JS and save my sanity 2020-02-23 18:26:57 +00:00
Alex Ling 803fc8c44b Split routes in server.cr into small files 2020-02-23 18:24:32 +00:00
Alex Ling dd49f75079 Update README.md 2020-02-21 21:22:55 -05:00
Alex Ling 6be9c3eac6 Update README.md 2020-02-21 21:21:34 -05:00
Alex Ling 5a2f80b5e1 Add issue templates 2020-02-21 16:42:12 -05:00
Alex Ling 5b4d79220c Provide pre-built binary (amd64) in README 2020-02-21 14:08:55 -05:00
20 changed files with 465 additions and 327 deletions
+32
View File
@@ -0,0 +1,32 @@
---
name: Bug report
about: I found a bug in Mango!
title: "[Bug Report]"
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Environment (please complete the following information):**
- OS: [e.g. Ubuntu 18.04]
- Browser [e.g. chrome, safari, if applicable]
- Mango Version [e.g. v0.1.0]
**Docker (if you are running Mango in a Docker container)**
- The `docker-compose.yml` file you are using
**Additional context**
Add any other context about the problem here. Add screenshots if applicable.
+17
View File
@@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest a feature for Mango
title: "[Feature Request]"
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Additional context**
Add any other context or screenshots about the feature request here.
@@ -0,0 +1,10 @@
---
name: General Question
about: I have a question about Mango
title: "[Question]"
labels: general question
assignees: ''
---
+5 -2
View File
@@ -12,6 +12,10 @@ Mango is a self-hosted manga server and reader. Its features include
## Installation ## Installation
### Pre-built Binary
1. Simply download the pre-built binary file `mango` for the latest [release](https://github.com/hkalexling/Mango/releases). All the dependencies are statically linked, and it should work with most Linux systems on amd64.
### Docker ### Docker
1. Make sure you have docker installed and running. You will also need `docker-compose` 1. Make sure you have docker installed and running. You will also need `docker-compose`
@@ -21,10 +25,9 @@ Mango is a self-hosted manga server and reader. Its features include
5. Run `docker-compose up`. This should build the docker image and start the container with Mango running inside 5. Run `docker-compose up`. This should build the docker image and start the container with Mango running inside
6. Head over to `localhost:9000` to log in 6. Head over to `localhost:9000` to log in
### Build from source ### Build from source
1. Make sure you have Crystal, Node and Yarn installed 1. Make sure you have Crystal, Node and Yarn installed. You might also need to install the development headers for `libsqlite3` and `libyaml`.
2. Clone the repository 2. Clone the repository
3. `make && sudo make install` 3. `make && sudo make install`
4. Start Mango by running the command `mango` 4. Start Mango by running the command `mango`
+2 -2
View File
@@ -11,5 +11,5 @@ services:
ports: ports:
- 9000:9000 - 9000:9000
volumes: volumes:
- ./mango:/root/mango - ~/mango:/root/mango
- ./config:/root/.config/mango - ~/.config/mango:/root/.config/mango
+2 -2
View File
@@ -1,10 +1,10 @@
const gulp = require('gulp'); const gulp = require('gulp');
const uglify = require('gulp-uglify'); const minify = require("gulp-babel-minify");
const minifyCss = require('gulp-minify-css'); const minifyCss = require('gulp-minify-css');
gulp.task('minify-js', () => { gulp.task('minify-js', () => {
return gulp.src('public/js/*.js') return gulp.src('public/js/*.js')
.pipe(uglify()) .pipe(minify())
.pipe(gulp.dest('dist/js')); .pipe(gulp.dest('dist/js'));
}); });
+2 -2
View File
@@ -7,8 +7,8 @@
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"gulp": "^4.0.2", "gulp": "^4.0.2",
"gulp-minify-css": "^1.2.4", "gulp-babel-minify": "^0.5.1",
"gulp-uglify": "^3.0.2" "gulp-minify-css": "^1.2.4"
}, },
"scripts": { "scripts": {
"uglify": "gulp" "uglify": "gulp"
+10 -7
View File
@@ -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(){ $('#modal button, #modal a').each(function(){
$(this).removeAttr('hidden'); $(this).removeAttr('hidden');
}); });
@@ -16,20 +19,20 @@ function showModal(title, zipPath, pages, percentage, title, entry) {
$('#path-text').text(zipPath); $('#path-text').text(zipPath);
$('#pages-text').text(pages + ' pages'); $('#pages-text').text(pages + ' pages');
$('#beginning-btn').attr('href', '/reader/' + title + '/' + entry + '/1'); $('#beginning-btn').attr('href', '/reader/' + titleID + '/' + entryID + '/1');
$('#continue-btn').attr('href', '/reader/' + title + '/' + entry); $('#continue-btn').attr('href', '/reader/' + titleID + '/' + entryID);
$('#read-btn').click(function(){ $('#read-btn').click(function(){
updateProgress(title, entry, pages); updateProgress(titleID, entryID, pages);
}); });
$('#unread-btn').click(function(){ $('#unread-btn').click(function(){
updateProgress(title, entry, 0); updateProgress(titleID, entryID, 0);
}); });
UIkit.modal($('#modal')).show(); UIkit.modal($('#modal')).show();
} }
function updateProgress(title, entry, page) { function updateProgress(titleID, entryID, page) {
$.post('/api/progress/' + title + '/' + entry + '/' + page, function(data) { $.post('/api/progress/' + titleID + '/' + entryID + '/' + page, function(data) {
if (data.success) { if (data.success) {
location.reload(); location.reload();
} }
+30 -16
View File
@@ -1,6 +1,7 @@
require "zip" require "zip"
require "mime" require "mime"
require "json" require "json"
require "uri"
struct Image struct Image
property data : Bytes property data : Bytes
@@ -14,11 +15,14 @@ end
class Entry class Entry
JSON.mapping zip_path: String, book_title: String, title: String, \ 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 @zip_path = path
@encoded_path = URI.encode path
@title = File.basename path, File.extname path @title = File.basename path, File.extname path
@encoded_title = URI.encode @title
@size = (File.size path).humanize_bytes @size = (File.size path).humanize_bytes
@pages = Zip::File.new(path).entries @pages = Zip::File.new(path).entries
.select { |e| .select { |e|
@@ -26,7 +30,8 @@ class Entry
MIME.from_filename? e.filename MIME.from_filename? e.filename
} }
.size .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 end
def read_page(page_num) def read_page(page_num)
Zip::File.open @zip_path do |file| Zip::File.open @zip_path do |file|
@@ -51,20 +56,27 @@ class Entry
end end
class Title 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 @dir = dir
@id = storage.get_id @dir, true
@title = File.basename dir @title = File.basename dir
@encoded_title = URI.encode @title
@entries = (Dir.entries dir) @entries = (Dir.entries dir)
.select { |path| [".zip", ".cbz"].includes? File.extname path } .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 } .select { |e| e.pages > 0 }
.sort { |a, b| a.title <=> b.title } .sort { |a, b| a.title <=> b.title }
end end
def get_entry(name) def get_entry(eid)
@entries.find { |e| e.title == name } @entries.find { |e| e.id == eid }
end 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) def save_progress(username, entry, page)
info = TitleInfo.new @dir info = TitleInfo.new @dir
if info.progress[username]?.nil? if info.progress[username]?.nil?
@@ -75,7 +87,7 @@ class Title
info.progress[username][entry] = page info.progress[username][entry] = page
info.save @dir info.save @dir
end end
def load_progress(username, entry : String) def load_progress(username, entry)
info = TitleInfo.new @dir info = TitleInfo.new @dir
if info.progress[username]?.nil? if info.progress[username]?.nil?
return 0 return 0
@@ -85,10 +97,10 @@ class Title
end end
info.progress[username][entry] info.progress[username][entry]
end end
def load_percetage(username, entry : String) def load_percetage(username, entry)
info = TitleInfo.new @dir info = TitleInfo.new @dir
page = load_progress username, entry page = load_progress username, entry
entry_obj = get_entry entry entry_obj = @entries.find{|e| e.title == entry}
return 0 if entry_obj.nil? return 0 if entry_obj.nil?
page / entry_obj.pages page / entry_obj.pages
end end
@@ -136,9 +148,10 @@ class TitleInfo
end end
class Library 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 # 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
@titles = [] of Title @titles = [] of Title
@@ -154,8 +167,8 @@ class Library
end end
end end
end end
def get_title(name) def get_title(tid)
@titles.find { |t| t.title == name } @titles.find { |t| t.id == tid }
end end
def scan def scan
unless Dir.exists? @dir unless Dir.exists? @dir
@@ -165,8 +178,9 @@ class Library
end end
@titles = (Dir.entries @dir) @titles = (Dir.entries @dir)
.select { |path| File.directory? File.join @dir, path } .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? } .select { |title| !title.entries.empty? }
.sort { |a, b| a.title <=> b.title }
@logger.debug "Scan completed" @logger.debug "Scan completed"
@logger.debug "Scanned library: \n#{self.to_pretty_json}" @logger.debug "Scanned library: \n#{self.to_pretty_json}"
end end
+1 -1
View File
@@ -25,8 +25,8 @@ end
config = Config.load config_path config = Config.load config_path
logger = MLogger.new config logger = MLogger.new config
library = Library.new config.library_path, config.scan_interval, logger
storage = Storage.new config.db_path, 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 context = Context.new config, logger, library, storage
+103
View File
@@ -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
+92
View File
@@ -0,0 +1,92 @@
require "./router"
class APIRouter < Router
def setup
get "/api/page/:tid/:eid/:page" do |env|
begin
tid = env.params.url["tid"]
eid = env.params.url["eid"]
page = env.params.url["page"].to_i
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
@context.error e
env.response.status_code = 500
e.message
end
end
get "/api/book/:title" do |env|
begin
tid = env.params.url["tid"]
title = @context.library.get_title tid
raise "Title ID `#{tid}` not found" if title.nil?
send_json env, title.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
+56
View File
@@ -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
+58
View File
@@ -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.id}/#{entry.id}/#{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.id}/#{entry.id}/#{idx}" }
reader_urls = pages.map { |idx|
"/reader/#{title.id}/#{entry.id}/#{idx}" }
next_page = page + IMGS_PER_PAGE
next_url = next_page > entry.pages ? nil :
"/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.id}/#{next_entry.id}"
render "src/views/reader.ecr"
rescue e
@context.error e
env.response.status_code = 404
end
end
end
end
+6
View File
@@ -0,0 +1,6 @@
require "../context"
class Router
def initialize(@context : Context)
end
end
+5 -290
View File
@@ -4,6 +4,7 @@ require "./auth_handler"
require "./static_handler" require "./static_handler"
require "./log_handler" require "./log_handler"
require "./util" require "./util"
require "./routes/*"
class Server class Server
def initialize(@context : Context) def initialize(@context : Context)
@@ -13,296 +14,10 @@ class Server
layout "message" layout "message"
end end
get "/" do |env| MainRouter.new(@context).setup
titles = @context.library.titles AdminRouter.new(@context).setup
username = get_username env ReaderRouter.new(@context).setup
percentage = titles.map &.load_percetage username APIRouter.new(@context).setup
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
Kemal.config.logging = false Kemal.config.logging = false
add_handler LogHandler.new @context.logger add_handler LogHandler.new @context.logger
+3 -1
View File
@@ -5,9 +5,11 @@ require "./util"
class FS class FS
extend BakedFileSystem extend BakedFileSystem
{% if read_file? "./dist" %} {% if read_file? "#{__DIR__}/../dist/favicon.ico" %}
{% puts "baking ../dist" %}
bake_folder "../dist" bake_folder "../dist"
{% else %} {% else %}
{% puts "baking ../public" %}
bake_folder "../public" bake_folder "../public"
{% end %} {% end %}
end end
+29 -2
View File
@@ -12,7 +12,7 @@ def verify_password(hash, pw)
end end
def random_str def random_str
Base64.strict_encode UUID.random().to_s UUID.random.to_s.gsub "-", ""
end end
class Storage class Storage
@@ -25,10 +25,18 @@ class Storage
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
# 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" \ db.exec "create table users" \
"(username text, password text, token text, admin integer)" "(username text, password text, token text, admin integer)"
rescue e 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}" @logger.fatal "Error when checking tables in DB: #{e}"
raise e raise e
end end
@@ -147,4 +155,23 @@ class Storage
end end
end 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 end
+1 -1
View File
@@ -9,7 +9,7 @@
<div class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid> <div class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
<%- titles.each_with_index do |t, i| -%> <%- titles.each_with_index do |t, i| -%>
<div class="item"> <div class="item">
<a class="acard" href="/book/<%= t.title %>"> <a class="acard" href="/book/<%= t.id %>">
<div class="uk-card uk-card-default"> <div class="uk-card uk-card-default">
<div class="uk-card-media-top"> <div class="uk-card-media-top">
<img src="<%= t.entries[0].cover_url %>" alt=""> <img src="<%= t.entries[0].cover_url %>" alt="">
+1 -1
View File
@@ -11,7 +11,7 @@
<%- title.entries.each_with_index do |e, i| -%> <%- title.entries.each_with_index do |e, i| -%>
<div class="item"> <div class="item">
<a class="acard"> <a class="acard">
<div class="uk-card uk-card-default" onclick="showModal('<%= e.title %>', '<%= e.zip_path %>', '<%= e.pages %>', <%= (percentage[i] * 100).round(1) %>, '<%= title.title %>', '<%= e.title %>')"> <div class="uk-card uk-card-default" onclick="showModal(&quot;<%= e.encoded_path %>&quot;, '<%= e.pages %>', <%= (percentage[i] * 100).round(1) %>, &quot;<%= title.encoded_title %>&quot;, &quot;<%= e.encoded_title %>&quot;, '<%= e.title_id %>', '<%= e.id %>')">
<div class="uk-card-media-top"> <div class="uk-card-media-top">
<img src="<%= e.cover_url %>" alt=""> <img src="<%= e.cover_url %>" alt="">
</div> </div>