Merge branch 'dev' into feature/signature

This commit is contained in:
Alex Ling 2021-01-18 06:54:38 +00:00
commit 7f76322377
26 changed files with 597 additions and 298 deletions

1
.gitignore vendored
View File

@ -12,3 +12,4 @@ mango
public/css/uikit.css public/css/uikit.css
public/img/*.svg public/img/*.svg
public/js/*.min.js public/js/*.min.js
public/css/*.css

View File

@ -52,7 +52,7 @@ The official docker images are available on [Dockerhub](https://hub.docker.com/r
### CLI ### CLI
``` ```
Mango - Manga Server and Web Reader. Version 0.18.3 Mango - Manga Server and Web Reader. Version 0.19.0
Usage: Usage:

View File

@ -0,0 +1,85 @@
class ForeignKeys < MG::Base
def up : String
<<-SQL
-- add foreign key to tags
ALTER TABLE tags RENAME TO tmp;
CREATE TABLE tags (
id TEXT NOT NULL,
tag TEXT NOT NULL,
UNIQUE (id, tag),
FOREIGN KEY (id) REFERENCES titles (id)
ON UPDATE CASCADE
ON DELETE CASCADE
);
INSERT INTO tags
SELECT * FROM tmp;
DROP TABLE tmp;
CREATE INDEX tags_id_idx ON tags (id);
CREATE INDEX tags_tag_idx ON tags (tag);
-- add foreign key to thumbnails
ALTER TABLE thumbnails RENAME TO tmp;
CREATE TABLE thumbnails (
id TEXT NOT NULL,
data BLOB NOT NULL,
filename TEXT NOT NULL,
mime TEXT NOT NULL,
size INTEGER NOT NULL,
FOREIGN KEY (id) REFERENCES ids (id)
ON UPDATE CASCADE
ON DELETE CASCADE
);
INSERT INTO thumbnails
SELECT * FROM tmp;
DROP TABLE tmp;
CREATE UNIQUE INDEX tn_index ON thumbnails (id);
SQL
end
def down : String
<<-SQL
-- remove foreign key from thumbnails
ALTER TABLE thumbnails RENAME TO tmp;
CREATE TABLE thumbnails (
id TEXT NOT NULL,
data BLOB NOT NULL,
filename TEXT NOT NULL,
mime TEXT NOT NULL,
size INTEGER NOT NULL
);
INSERT INTO thumbnails
SELECT * FROM tmp;
DROP TABLE tmp;
CREATE UNIQUE INDEX tn_index ON thumbnails (id);
-- remove foreign key from tags
ALTER TABLE tags RENAME TO tmp;
CREATE TABLE tags (
id TEXT NOT NULL,
tag TEXT NOT NULL,
UNIQUE (id, tag)
);
INSERT INTO tags
SELECT * FROM tmp;
DROP TABLE tmp;
CREATE INDEX tags_id_idx ON tags (id);
CREATE INDEX tags_tag_idx ON tags (tag);
SQL
end
end

19
migration/ids.2.cr Normal file
View File

@ -0,0 +1,19 @@
class CreateIds < MG::Base
def up : String
<<-SQL
CREATE TABLE IF NOT EXISTS ids (
path TEXT NOT NULL,
id TEXT NOT NULL,
is_title INTEGER NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS path_idx ON ids (path);
CREATE UNIQUE INDEX IF NOT EXISTS id_idx ON ids (id);
SQL
end
def down : String
<<-SQL
DROP TABLE ids;
SQL
end
end

19
migration/tags.4.cr Normal file
View File

@ -0,0 +1,19 @@
class CreateTags < MG::Base
def up : String
<<-SQL
CREATE TABLE IF NOT EXISTS tags (
id TEXT NOT NULL,
tag TEXT NOT NULL,
UNIQUE (id, tag)
);
CREATE INDEX IF NOT EXISTS tags_id_idx ON tags (id);
CREATE INDEX IF NOT EXISTS tags_tag_idx ON tags (tag);
SQL
end
def down : String
<<-SQL
DROP TABLE tags;
SQL
end
end

20
migration/thumbnails.3.cr Normal file
View File

@ -0,0 +1,20 @@
class CreateThumbnails < MG::Base
def up : String
<<-SQL
CREATE TABLE IF NOT EXISTS thumbnails (
id TEXT NOT NULL,
data BLOB NOT NULL,
filename TEXT NOT NULL,
mime TEXT NOT NULL,
size INTEGER NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS tn_index ON thumbnails (id);
SQL
end
def down : String
<<-SQL
DROP TABLE thumbnails;
SQL
end
end

56
migration/titles.5.cr Normal file
View File

@ -0,0 +1,56 @@
class CreateTitles < MG::Base
def up : String
<<-SQL
-- create titles
CREATE TABLE titles (
id TEXT NOT NULL,
path TEXT NOT NULL,
signature TEXT
);
CREATE UNIQUE INDEX titles_id_idx on titles (id);
CREATE UNIQUE INDEX titles_path_idx on titles (path);
-- migrate data from ids to titles
INSERT INTO titles
SELECT id, path, null
FROM ids
WHERE is_title = 1;
DELETE FROM ids
WHERE is_title = 1;
-- remove the is_title column from ids
ALTER TABLE ids RENAME TO tmp;
CREATE TABLE ids (
path TEXT NOT NULL,
id TEXT NOT NULL
);
INSERT INTO ids
SELECT path, id
FROM tmp;
DROP TABLE tmp;
-- recreate the indices
CREATE UNIQUE INDEX path_idx ON ids (path);
CREATE UNIQUE INDEX id_idx ON ids (id);
SQL
end
def down : String
<<-SQL
-- insert the is_title column
ALTER TABLE ids ADD COLUMN is_title INTEGER NOT NULL DEFAULT 0;
-- migrate data from titles to ids
INSERT INTO ids
SELECT path, id, 1
FROM titles;
-- remove titles
DROP TABLE titles;
SQL
end
end

20
migration/users.1.cr Normal file
View File

@ -0,0 +1,20 @@
class CreateUsers < MG::Base
def up : String
<<-SQL
CREATE TABLE IF NOT EXISTS users (
username TEXT NOT NULL,
password TEXT NOT NULL,
token TEXT,
admin INTEGER NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS username_idx ON users (username);
CREATE UNIQUE INDEX IF NOT EXISTS token_idx ON users (token);
SQL
end
def down : String
<<-SQL
DROP TABLE users;
SQL
end
end

View File

@ -1,146 +0,0 @@
.uk-alert-close {
color: black !important;
}
.uk-card-body {
padding: 20px;
}
.uk-card-media-top {
width: 100%;
height: 250px;
}
@media (min-width: 600px) {
.uk-card-media-top {
height: 300px;
}
}
.uk-card-media-top>img {
height: 100%;
width: 100%;
object-fit: cover;
}
.uk-card-title {
max-height: 3em;
}
.acard:hover {
cursor: pointer;
}
.reader-bg {
background-color: black;
}
.break-word {
word-wrap: break-word;
}
.uk-logo>img {
height: 90px;
width: 90px;
}
.uk-search {
width: 100%;
}
#selectable .ui-selecting {
background: #EEE6B9;
}
#selectable .ui-selected {
background: #F4E487;
}
.uk-light #selectable .ui-selecting {
background: #5E5731;
}
.uk-light #selectable .ui-selected {
background: #9D9252;
}
td>.uk-dropdown {
white-space: pre-line;
}
#edit-modal .uk-grid>div {
height: 300px;
}
#edit-modal #cover {
height: 100%;
width: 100%;
object-fit: cover;
}
#edit-modal #cover-upload {
height: 100%;
box-sizing: border-box;
}
#edit-modal .uk-modal-body .uk-inline {
width: 100%;
}
.item .uk-card-title {
font-size: 1rem;
}
.grayscale {
filter: grayscale(100%);
}
.uk-light .uk-navbar-dropdown,
.uk-light .uk-modal-header,
.uk-light .uk-modal-body,
.uk-light .uk-modal-footer {
background: #222;
}
.uk-light .uk-dropdown {
background: #333;
}
.uk-light .uk-navbar-dropdown,
.uk-light .uk-dropdown {
color: #ccc;
}
.uk-light .uk-nav-header,
.uk-light .uk-description-list>dt {
color: #555;
}
[x-cloak] {
display: none;
}
#select-bar-controls a {
transform: scale(1.5, 1.5);
}
#select-bar-controls a:hover {
color: orange;
}
#main-section {
position: relative;
}
#totop-wrapper {
position: absolute;
top: 100vh;
right: 2em;
bottom: 0;
}
#totop-wrapper a {
position: fixed;
position: sticky;
top: calc(100vh - 5em);
}

124
public/css/mango.less Normal file
View File

@ -0,0 +1,124 @@
// Item cards
.item .uk-card {
cursor: pointer;
.uk-card-media-top {
width: 100%;
height: 250px;
@media (min-width: 600px) {
height: 300px;
}
img {
height: 100%;
width: 100%;
object-fit: cover;
&.grayscale {
filter: grayscale(100%);
}
}
}
.uk-card-body {
padding: 20px;
.uk-card-title {
max-height: 3em;
font-size: 1rem;
}
}
}
// jQuery selectable
#selectable {
.ui-selecting {
background: #EEE6B9;
}
.ui-selected {
background: #F4E487;
}
.uk-light & {
.ui-selecting {
background: #5E5731;
}
.ui-selected {
background: #9D9252;
}
}
}
// Edit modal
#edit-modal {
.uk-grid > div {
height: 300px;
}
#cover {
height: 100%;
width: 100%;
object-fit: cover;
}
#cover-upload {
height: 100%;
box-sizing: border-box;
}
.uk-modal-body .uk-inline {
width: 100%;
}
}
// Dark theme
.uk-light {
.uk-navbar-dropdown,
.uk-modal-header,
.uk-modal-body,
.uk-modal-footer {
background: #222;
}
.uk-navbar-dropdown,
.uk-dropdown {
color: #ccc;
}
.uk-nav-header,
.uk-description-list > dt {
color: #555;
}
}
// Alpine magic
[x-cloak] {
display: none;
}
// Batch select bar on title page
#select-bar-controls {
a {
transform: scale(1.5, 1.5);
&:hover {
color: orange;
}
}
}
// Totop button
#totop-wrapper {
position: absolute;
top: 100vh;
right: 2em;
bottom: 0;
a {
position: fixed;
position: sticky;
top: calc(100vh - 5em);
}
}
// Misc
.uk-alert-close {
color: black !important;
}
.break-word {
word-wrap: break-word;
}
.uk-search {
width: 100%;
}

58
public/css/tags.less Normal file
View File

@ -0,0 +1,58 @@
@light-gray: #e5e5e5;
@gray: #666666;
@black: #141414;
@blue: rgb(30, 135, 240);
@white1: rgba(255, 255, 255, .1);
@white2: rgba(255, 255, 255, .2);
@white7: rgba(255, 255, 255, .7);
.select2-container--default {
.select2-selection--multiple {
border: 1px solid @light-gray;
.select2-selection__choice,
.select2-selection__choice__remove,
.select2-selection__choice__remove:hover
{
background-color: @blue;
color: white;
border: none;
border-radius: 2px;
}
}
.select2-dropdown {
.select2-results__option--highlighted.select2-results__option--selectable {
background-color: @blue;
}
.select2-results__option--selected:not(.select2-results__option--highlighted) {
background-color: @light-gray
}
}
}
.uk-light {
.select2-container--default {
.select2-selection {
background-color: @white1;
}
.select2-selection--multiple {
border: 1px solid @white2;
.select2-selection__choice,
.select2-selection__choice__remove,
.select2-selection__choice__remove:hover
{
background-color: white;
color: @gray;
border: none;
}
.select2-search__field {
color: @white7;
}
}
}
.select2-dropdown {
background-color: @black;
.select2-results__option--selected:not(.select2-results__option--highlighted) {
background-color: @white2;
}
}
}

View File

@ -255,48 +255,65 @@ const bulkProgress = (action, el) => {
const tagsComponent = () => { const tagsComponent = () => {
return { return {
loading: true,
isAdmin: false, isAdmin: false,
tags: [], tags: [],
newTag: '',
inputShown: false,
tid: $('.upload-field').attr('data-title-id'), tid: $('.upload-field').attr('data-title-id'),
loading: true,
load(admin) { load(admin) {
this.isAdmin = admin; this.isAdmin = admin;
const url = `${base_url}api/tags/${this.tid}`;
this.request(url, 'GET', (data) => { $('.tag-select').select2({
this.tags = data.tags; tags: true,
this.loading = false; placeholder: this.isAdmin ? 'Tag the title' : 'No tags found',
disabled: !this.isAdmin,
templateSelection(state) {
const a = document.createElement('a');
a.setAttribute('href', `${base_url}tags/${encodeURIComponent(state.text)}`);
a.setAttribute('class', 'uk-link-reset');
a.onclick = event => {
event.stopPropagation();
};
a.innerText = state.text;
return a;
}
}); });
},
add() { this.request(`${base_url}api/tags`, 'GET', (data) => {
const tag = this.newTag.trim(); const allTags = data.tags;
const url = `${base_url}api/admin/tags/${this.tid}/${encodeURIComponent(tag)}`; const url = `${base_url}api/tags/${this.tid}`;
this.request(url, 'PUT', () => { this.request(url, 'GET', data => {
this.tags.push(tag); this.tags = data.tags;
this.newTag = ''; allTags.forEach(t => {
}); const op = new Option(t, t, false, this.tags.indexOf(t) >= 0);
}, $('.tag-select').append(op);
keydown(event) { });
if (event.key === 'Enter') $('.tag-select').on('select2:select', e => {
this.add() this.onAdd(e);
}, });
rm(event) { $('.tag-select').on('select2:unselect', e => {
const tag = event.currentTarget.id.split('-')[0]; this.onDelete(e);
const url = `${base_url}api/admin/tags/${this.tid}/${encodeURIComponent(tag)}`; });
this.request(url, 'DELETE', () => { $('.tag-select').on('change', () => {
const idx = this.tags.indexOf(tag); this.onChange();
if (idx < 0) return; });
this.tags.splice(idx, 1); $('.tag-select').trigger('change');
}); this.loading = false;
},
toggleInput(nextTick) {
this.inputShown = !this.inputShown;
if (this.inputShown) {
nextTick(() => {
$('#tag-input').get(0).focus();
}); });
} });
},
onChange() {
this.tags = $('.tag-select').select2('data').map(o => o.text);
},
onAdd(event) {
const tag = event.params.data.text;
const url = `${base_url}api/admin/tags/${this.tid}/${encodeURIComponent(tag)}`;
this.request(url, 'PUT');
},
onDelete(event) {
const tag = event.params.data.text;
const url = `${base_url}api/admin/tags/${this.tid}/${encodeURIComponent(tag)}`;
this.request(url, 'DELETE');
}, },
request(url, method, cb) { request(url, method, cb) {
$.ajax({ $.ajax({
@ -305,9 +322,9 @@ const tagsComponent = () => {
dataType: 'json' dataType: 'json'
}) })
.done(data => { .done(data => {
if (data.success) if (data.success) {
cb(data); if (cb) cb(data);
else { } else {
alert('danger', data.error); alert('danger', data.error);
} }
}) })

View File

@ -52,6 +52,10 @@ shards:
git: https://github.com/hkalexling/koa.git git: https://github.com/hkalexling/koa.git
version: 0.5.0 version: 0.5.0
mg:
git: https://github.com/hkalexling/mg.git
version: 0.2.0+git.commit.171c46489d991a8353818e00fc6a3c4e0809ded9
myhtml: myhtml:
git: https://github.com/kostya/myhtml.git git: https://github.com/kostya/myhtml.git
version: 1.5.1 version: 1.5.1

View File

@ -1,5 +1,5 @@
name: mango name: mango
version: 0.18.3 version: 0.19.0
authors: authors:
- Alex Ling <hkalexling@gmail.com> - Alex Ling <hkalexling@gmail.com>
@ -41,3 +41,5 @@ dependencies:
github: hkalexling/koa github: hkalexling/koa
tallboy: tallboy:
github: epoch/tallboy github: epoch/tallboy
mg:
github: hkalexling/mg

View File

@ -82,7 +82,12 @@ class AuthHandler < Kemal::Handler
if env.session.string? "token" if env.session.string? "token"
should_reject = !validate_token_admin(env) should_reject = !validate_token_admin(env)
end end
env.response.status_code = 403 if should_reject if should_reject
env.response.status_code = 403
send_error_page "HTTP 403: You are not authorized to visit " \
"#{env.request.path}"
return
end
end end
call_next env call_next env

View File

@ -6,26 +6,14 @@ class Logger
SEVERITY_IDS = [0, 4, 5, 2, 3] SEVERITY_IDS = [0, 4, 5, 2, 3]
COLORS = [:light_cyan, :light_red, :red, :light_yellow, :light_magenta] COLORS = [:light_cyan, :light_red, :red, :light_yellow, :light_magenta]
getter raw_log = Log.for ""
@@severity : Log::Severity = :info @@severity : Log::Severity = :info
use_default use_default
def initialize def initialize
level = Config.current.log_level @@severity = Logger.get_severity
{% begin %}
case level.downcase
when "off"
@@severity = :none
{% for lvl, i in LEVELS %}
when {{lvl}}
@@severity = Log::Severity.new SEVERITY_IDS[{{i}}]
{% end %}
else
raise "Unknown log level #{level}"
end
{% end %}
@log = Log.for("")
@backend = Log::IOBackend.new @backend = Log::IOBackend.new
format_proc = ->(entry : Log::Entry, io : IO) do format_proc = ->(entry : Log::Entry, io : IO) do
@ -49,6 +37,24 @@ class Logger
Log.setup @@severity, @backend Log.setup @@severity, @backend
end end
def self.get_severity(level = "") : Log::Severity
if level.empty?
level = Config.current.log_level
end
{% begin %}
case level.downcase
when "off"
return Log::Severity::None
{% for lvl, i in LEVELS %}
when {{lvl}}
return Log::Severity.new SEVERITY_IDS[{{i}}]
{% end %}
else
raise "Unknown log level #{level}"
end
{% end %}
end
# Ignores @@severity and always log msg # Ignores @@severity and always log msg
def log(msg) def log(msg)
@backend.write Log::Entry.new "", Log::Severity::None, msg, @backend.write Log::Entry.new "", Log::Severity::None, msg,
@ -61,7 +67,7 @@ class Logger
{% for lvl in LEVELS %} {% for lvl in LEVELS %}
def {{lvl.id}}(msg) def {{lvl.id}}(msg)
@log.{{lvl.id}} { msg } raw_log.{{lvl.id}} { msg }
end end
def self.{{lvl.id}}(msg) def self.{{lvl.id}}(msg)
default.not_nil!.{{lvl.id}} msg default.not_nil!.{{lvl.id}} msg

View File

@ -8,7 +8,7 @@ require "option_parser"
require "clim" require "clim"
require "tallboy" require "tallboy"
MANGO_VERSION = "0.18.3" MANGO_VERSION = "0.19.0"
# From http://www.network-science.de/ascii/ # From http://www.network-science.de/ascii/
BANNER = %{ BANNER = %{
@ -63,7 +63,12 @@ class CLI < Clim
Plugin::Downloader.default Plugin::Downloader.default
spawn do spawn do
Server.new.start begin
Server.new.start
rescue e
Logger.fatal e
Process.exit 1
end
end end
MainFiber.start_and_block MainFiber.start_and_block

View File

@ -713,6 +713,24 @@ struct APIRouter
end end
end end
Koa.describe "Returns all tags"
Koa.response 200, ref: "$tagsResult"
get "/api/tags" do |env|
begin
tags = Storage.default.list_tags
send_json env, {
"success" => true,
"tags" => tags,
}.to_json
rescue e
Logger.error e
send_json env, {
"success" => false,
"error" => e.message,
}.to_json
end
end
Koa.describe "Adds a new tag to a title" Koa.describe "Adds a new tag to a title"
Koa.path "tid", desc: "A title ID" Koa.path "tid", desc: "A title ID"
Koa.response 200, ref: "$result" Koa.response 200, ref: "$result"

View File

@ -7,10 +7,6 @@ require "./routes/*"
class Server class Server
def initialize def initialize
error 403 do |env|
message = "HTTP 403: You are not authorized to visit #{env.request.path}"
layout "message"
end
error 404 do |env| error 404 do |env|
message = "HTTP 404: Mango cannot find the page #{env.request.path}" message = "HTTP 404: Mango cannot find the page #{env.request.path}"
layout "message" layout "message"

View File

@ -3,6 +3,8 @@ require "crypto/bcrypt"
require "uuid" require "uuid"
require "base64" require "base64"
require "./util/*" require "./util/*"
require "mg"
require "../migration/*"
def hash_password(pw) def hash_password(pw)
Crypto::Bcrypt::Password.create(pw).to_s Crypto::Bcrypt::Password.create(pw).to_s
@ -13,9 +15,10 @@ def verify_password(hash, pw)
end end
class Storage class Storage
@@insert_ids = [] of IDTuple
@path : String @path : String
@db : DB::Database? @db : DB::Database?
@insert_ids = [] of IDTuple
alias IDTuple = NamedTuple(path: String, alias IDTuple = NamedTuple(path: String,
id: String, id: String,
@ -35,44 +38,15 @@ class Storage
MainFiber.run do MainFiber.run do
DB.open "sqlite3://#{@path}" do |db| DB.open "sqlite3://#{@path}" do |db|
begin begin
# v0.18.0 MG::Migration.new(db, log: Logger.default.raw_log).migrate
db.exec "create table tags (id text, tag text, unique (id, tag))"
db.exec "create index tags_id_idx on tags (id)"
db.exec "create index tags_tag_idx on tags (tag)"
# v0.15.0
db.exec "create table thumbnails " \
"(id text, data blob, filename text, " \
"mime text, size integer)"
db.exec "create unique index tn_index on thumbnails (id)"
# v0.1.1
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)"
# v0.1.0
db.exec "create table users" \
"(username text, password text, token text, admin integer)"
rescue e rescue e
unless e.message.not_nil!.ends_with? "already exists" Logger.fatal "DB migration failed. #{e}"
Logger.fatal "Error when checking tables in DB: #{e}" raise e
raise e
end
# If the DB is initialized through CLI but no user is added, we need
# to create the admin user when first starting the app
user_count = db.query_one "select count(*) from users", as: Int32
init_admin if init_user && user_count == 0
else
Logger.debug "Creating DB file at #{@path}"
db.exec "create unique index username_idx on users (username)"
db.exec "create unique index token_idx on users (token)"
init_admin if init_user
end end
user_count = db.query_one "select count(*) from users", as: Int32
init_admin if init_user && user_count == 0
# Verifies that the default username in config is valid # Verifies that the default username in config is valid
if Config.current.disable_login if Config.current.disable_login
username = Config.current.default_username username = Config.current.default_username
@ -99,9 +73,11 @@ class Storage
private def get_db(&block : DB::Database ->) private def get_db(&block : DB::Database ->)
if @db.nil? if @db.nil?
DB.open "sqlite3://#{@path}" do |db| DB.open "sqlite3://#{@path}" do |db|
db.exec "PRAGMA foreign_keys = 1"
yield db yield db
end end
else else
@db.not_nil!.exec "PRAGMA foreign_keys = 1"
yield @db.not_nil! yield @db.not_nil!
end end
end end
@ -258,28 +234,38 @@ class Storage
id = nil id = nil
MainFiber.run do MainFiber.run do
get_db do |db| get_db do |db|
id = db.query_one? "select id from ids where path = (?)", path, if is_title
as: {String} id = db.query_one? "select id from titles where path = (?)", path,
as: String
else
id = db.query_one? "select id from ids where path = (?)", path,
as: String
end
end end
end end
id id
end end
def insert_id(tp : IDTuple) def insert_id(tp : IDTuple)
@insert_ids << tp @@insert_ids << tp
end end
def bulk_insert_ids def bulk_insert_ids
MainFiber.run do MainFiber.run do
get_db do |db| get_db do |db|
db.transaction do |tx| db.transaction do |tran|
@insert_ids.each do |tp| conn = tran.connection
tx.connection.exec "insert into ids values (?, ?, ?)", tp[:path], @@insert_ids.each do |tp|
tp[:id], tp[:is_title] ? 1 : 0 if tp[:is_title]
conn.exec "insert into titles values (?, ?, null)", tp[:id],
tp[:path]
else
conn.exec "insert into ids values (?, ?)", tp[:path], tp[:id]
end
end end
end end
end end
@insert_ids.clear @@insert_ids.clear
end end
end end
@ -372,6 +358,7 @@ class Storage
MainFiber.run do MainFiber.run do
Logger.info "Starting DB optimization" Logger.info "Starting DB optimization"
get_db do |db| get_db do |db|
# Delete dangling entry IDs
trash_ids = [] of String trash_ids = [] of String
db.query "select path, id from ids" do |rs| db.query "select path, id from ids" do |rs|
rs.each do rs.each do
@ -380,29 +367,24 @@ class Storage
end end
end end
# Delete dangling IDs
db.exec "delete from ids where id in " \ db.exec "delete from ids where id in " \
"(#{trash_ids.map { |i| "'#{i}'" }.join ","})" "(#{trash_ids.map { |i| "'#{i}'" }.join ","})"
Logger.debug "#{trash_ids.size} dangling IDs deleted" \ Logger.debug "#{trash_ids.size} dangling entry IDs deleted" \
if trash_ids.size > 0 if trash_ids.size > 0
# Delete dangling thumbnails # Delete dangling title IDs
trash_thumbnails_count = db.query_one "select count(*) from " \ trash_titles = [] of String
"thumbnails where id not in " \ db.query "select path, id from titles" do |rs|
"(select id from ids)", as: Int32 rs.each do
if trash_thumbnails_count > 0 path = rs.read String
db.exec "delete from thumbnails where id not in (select id from ids)" trash_titles << rs.read String unless Dir.exists? path
Logger.info "#{trash_thumbnails_count} dangling thumbnails deleted" end
end end
# Delete dangling tags db.exec "delete from titles where id in " \
trash_tags_count = db.query_one "select count(*) from tags " \ "(#{trash_titles.map { |i| "'#{i}'" }.join ","})"
"where id not in " \ Logger.debug "#{trash_titles.size} dangling title IDs deleted" \
"(select id from ids)", as: Int32 if trash_titles.size > 0
if trash_tags_count > 0
db.exec "delete from tags where id not in (select id from ids)"
Logger.info "#{trash_tags_count} dangling tags deleted"
end
end end
Logger.info "DB optimization finished" Logger.info "DB optimization finished"
end end

View File

@ -1,19 +1,24 @@
# Web related helper functions/macros # Web related helper functions/macros
# This macro defines `is_admin` when used
macro check_admin_access
is_admin = false
# The token (if exists) takes precedence over the default user option.
# this is why we check the default username first before checking the
# token.
if Config.current.disable_login
is_admin = Storage.default.
username_is_admin Config.current.default_username
end
if token = env.session.string? "token"
is_admin = Storage.default.verify_admin token
end
end
macro layout(name) macro layout(name)
base_url = Config.current.base_url base_url = Config.current.base_url
check_admin_access
begin begin
is_admin = false
# The token (if exists) takes precedence over the default user option.
# this is why we check the default username first before checking the
# token.
if Config.current.disable_login
is_admin = Storage.default.
username_is_admin Config.current.default_username
end
if token = env.session.string? "token"
is_admin = Storage.default.verify_admin token
end
page = {{name}} page = {{name}}
render "src/views/#{{{name}}}.html.ecr", "src/views/layout.html.ecr" render "src/views/#{{{name}}}.html.ecr", "src/views/layout.html.ecr"
rescue e rescue e
@ -24,6 +29,15 @@ macro layout(name)
end end
end end
macro send_error_page(msg)
message = {{msg}}
base_url = Config.current.base_url
check_admin_access
page = "Error"
html = render "src/views/message.html.ecr", "src/views/layout.html.ecr"
send_file env, html.to_slice, "text/html"
end
macro send_img(env, img) macro send_img(env, img)
send_file {{env}}, {{img}}.data, {{img}}.mime send_file {{env}}, {{img}}.data, {{img}}.mime
end end

View File

@ -1,12 +0,0 @@
<div class="uk-margin" x-data="tagsComponent()" x-cloak x-init="load(<%= is_admin %>)">
<p class="uk-text-meta" @selectstart.prevent>
<span style="position:relative; bottom:3px; margin-right:5px;">Tags: </span>
<template x-for="tag in tags" :key="tag">
<span class="uk-label uk-label-primary" style="padding:2px 5px; margin:0 5px 5px 5px; text-transform:none;">
<a class="uk-link-reset" x-show="isAdmin" @click="rm($event)" :id="`${tag}-rm`"><span uk-icon="close" style="margin-right: 5px; position: relative; bottom: 1.5px;"></span></a><a class="uk-link-reset" x-text="tag" :href="`<%= base_url %>tags/${encodeURIComponent(tag)}`"></a>
</span>
</template>
<a class="uk-link-reset" style="position:relative; bottom:3px;" :uk-icon="inputShown ? 'close' : 'plus'" @click="toggleInput($nextTick)" x-show="isAdmin"></a>
</p>
<input id="tag-input" class="uk-input" type="text" placeholder="Type in a new tag and hit enter" x-model="newTag" @keydown="keydown($event)" x-show="inputShown">
</div>

View File

@ -43,7 +43,7 @@
<template x-if="job.status_message.length > 0"> <template x-if="job.status_message.length > 0">
<div class="uk-inline"> <div class="uk-inline">
<span uk-icon="info"></span> <span uk-icon="info"></span>
<div uk-dropdown x-text="job.status_message"></div> <div uk-dropdown x-text="job.status_message" style="white-space: pre-line;"></div>
</div> </div>
</template> </template>
</td> </td>

View File

@ -37,7 +37,7 @@
<div class="uk-navbar-toggle" uk-navbar-toggle-icon="uk-navbar-toggle-icon" uk-toggle="target: #mobile-nav"></div> <div class="uk-navbar-toggle" uk-navbar-toggle-icon="uk-navbar-toggle-icon" uk-toggle="target: #mobile-nav"></div>
</div> </div>
<div class="uk-navbar-left uk-visible@s"> <div class="uk-navbar-left uk-visible@s">
<a class="uk-navbar-item uk-logo" href="<%= base_url %>"><img src="<%= base_url %>img/icon.png"></a> <a class="uk-navbar-item uk-logo" href="<%= base_url %>"><img src="<%= base_url %>img/icon.png" style="width:90px;height:90px;"></a>
<ul class="uk-navbar-nav"> <ul class="uk-navbar-nav">
<li><a href="<%= base_url %>">Home</a></li> <li><a href="<%= base_url %>">Home</a></li>
<li><a href="<%= base_url %>library">Library</a></li> <li><a href="<%= base_url %>library">Library</a></li>
@ -69,7 +69,7 @@
</div> </div>
<div class="uk-section uk-section-small"> <div class="uk-section uk-section-small">
</div> </div>
<div class="uk-section uk-section-small" id="main-section"> <div class="uk-section uk-section-small" style="position:relative;">
<div class="uk-container uk-container-small"> <div class="uk-container uk-container-small">
<div id="alert"></div> <div id="alert"></div>
<%= content %> <%= content %>

View File

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html class="reader-bg"> <html style="background-color: black;">
<% page = "Reader" %> <% page = "Reader" %>
<%= render_component "head" %> <%= render_component "head" %>

View File

@ -34,7 +34,10 @@
</ul> </ul>
<p class="uk-text-meta"><%= title.content_label %> found</p> <p class="uk-text-meta"><%= title.content_label %> found</p>
<%= render_component "tags" %> <div class="uk-margin" x-data="tagsComponent()" x-cloak x-init="load(<%= is_admin %>)" x-show="!loading">
<select class="tag-select" multiple="multiple" style="width:100%">
</select>
</div>
<div class="uk-grid-small" uk-grid> <div class="uk-grid-small" uk-grid>
<div class="uk-margin-bottom uk-width-3-4@s"> <div class="uk-margin-bottom uk-width-3-4@s">
@ -121,6 +124,9 @@
<% content_for "script" do %> <% content_for "script" do %>
<%= render_component "dots-scripts" %> <%= render_component "dots-scripts" %>
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-beta.1/dist/css/select2.min.css" rel="stylesheet" />
<link href="/css/tags.css" rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-beta.1/dist/js/select2.min.js"></script>
<script src="<%= base_url %>js/alert.js"></script> <script src="<%= base_url %>js/alert.js"></script>
<script src="<%= base_url %>js/title.js"></script> <script src="<%= base_url %>js/title.js"></script>
<script src="<%= base_url %>js/search.js"></script> <script src="<%= base_url %>js/search.js"></script>