diff --git a/.gitignore b/.gitignore index b8047b7..7516173 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ mango public/css/uikit.css public/img/*.svg public/js/*.min.js +public/css/*.css diff --git a/README.md b/README.md index 42dd8b0..27bd585 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ The official docker images are available on [Dockerhub](https://hub.docker.com/r ### CLI ``` - Mango - Manga Server and Web Reader. Version 0.18.3 + Mango - Manga Server and Web Reader. Version 0.19.0 Usage: diff --git a/migration/foreign_keys.6.cr b/migration/foreign_keys.6.cr new file mode 100644 index 0000000..8cc00f2 --- /dev/null +++ b/migration/foreign_keys.6.cr @@ -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 diff --git a/migration/ids.2.cr b/migration/ids.2.cr new file mode 100644 index 0000000..631c50d --- /dev/null +++ b/migration/ids.2.cr @@ -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 diff --git a/migration/tags.4.cr b/migration/tags.4.cr new file mode 100644 index 0000000..0518ecd --- /dev/null +++ b/migration/tags.4.cr @@ -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 diff --git a/migration/thumbnails.3.cr b/migration/thumbnails.3.cr new file mode 100644 index 0000000..4e94069 --- /dev/null +++ b/migration/thumbnails.3.cr @@ -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 diff --git a/migration/titles.5.cr b/migration/titles.5.cr new file mode 100644 index 0000000..7f01d11 --- /dev/null +++ b/migration/titles.5.cr @@ -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 diff --git a/migration/users.1.cr b/migration/users.1.cr new file mode 100644 index 0000000..78b699a --- /dev/null +++ b/migration/users.1.cr @@ -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 diff --git a/public/css/mango.css b/public/css/mango.css deleted file mode 100644 index 2eae165..0000000 --- a/public/css/mango.css +++ /dev/null @@ -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); -} diff --git a/public/css/mango.less b/public/css/mango.less new file mode 100644 index 0000000..f6e8951 --- /dev/null +++ b/public/css/mango.less @@ -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%; +} diff --git a/public/css/tags.less b/public/css/tags.less new file mode 100644 index 0000000..94effe3 --- /dev/null +++ b/public/css/tags.less @@ -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; + } + } +} diff --git a/public/js/title.js b/public/js/title.js index 7ddd093..1aca6d6 100644 --- a/public/js/title.js +++ b/public/js/title.js @@ -255,48 +255,65 @@ const bulkProgress = (action, el) => { const tagsComponent = () => { return { - loading: true, isAdmin: false, tags: [], - newTag: '', - inputShown: false, tid: $('.upload-field').attr('data-title-id'), + loading: true, + load(admin) { this.isAdmin = admin; - const url = `${base_url}api/tags/${this.tid}`; - this.request(url, 'GET', (data) => { - this.tags = data.tags; - this.loading = false; + + $('.tag-select').select2({ + tags: true, + 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() { - const tag = this.newTag.trim(); - const url = `${base_url}api/admin/tags/${this.tid}/${encodeURIComponent(tag)}`; - this.request(url, 'PUT', () => { - this.tags.push(tag); - this.newTag = ''; - }); - }, - keydown(event) { - if (event.key === 'Enter') - this.add() - }, - rm(event) { - const tag = event.currentTarget.id.split('-')[0]; - const url = `${base_url}api/admin/tags/${this.tid}/${encodeURIComponent(tag)}`; - this.request(url, 'DELETE', () => { - const idx = this.tags.indexOf(tag); - if (idx < 0) return; - this.tags.splice(idx, 1); - }); - }, - toggleInput(nextTick) { - this.inputShown = !this.inputShown; - if (this.inputShown) { - nextTick(() => { - $('#tag-input').get(0).focus(); + + this.request(`${base_url}api/tags`, 'GET', (data) => { + const allTags = data.tags; + const url = `${base_url}api/tags/${this.tid}`; + this.request(url, 'GET', data => { + this.tags = data.tags; + allTags.forEach(t => { + const op = new Option(t, t, false, this.tags.indexOf(t) >= 0); + $('.tag-select').append(op); + }); + $('.tag-select').on('select2:select', e => { + this.onAdd(e); + }); + $('.tag-select').on('select2:unselect', e => { + this.onDelete(e); + }); + $('.tag-select').on('change', () => { + this.onChange(); + }); + $('.tag-select').trigger('change'); + this.loading = false; }); - } + }); + }, + 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) { $.ajax({ @@ -305,9 +322,9 @@ const tagsComponent = () => { dataType: 'json' }) .done(data => { - if (data.success) - cb(data); - else { + if (data.success) { + if (cb) cb(data); + } else { alert('danger', data.error); } }) diff --git a/shard.lock b/shard.lock index 61ded6a..99d3c5a 100644 --- a/shard.lock +++ b/shard.lock @@ -52,6 +52,10 @@ shards: git: https://github.com/hkalexling/koa.git version: 0.5.0 + mg: + git: https://github.com/hkalexling/mg.git + version: 0.2.0+git.commit.171c46489d991a8353818e00fc6a3c4e0809ded9 + myhtml: git: https://github.com/kostya/myhtml.git version: 1.5.1 diff --git a/shard.yml b/shard.yml index 3395471..70f3d46 100644 --- a/shard.yml +++ b/shard.yml @@ -1,5 +1,5 @@ name: mango -version: 0.18.3 +version: 0.19.0 authors: - Alex Ling @@ -41,3 +41,5 @@ dependencies: github: hkalexling/koa tallboy: github: epoch/tallboy + mg: + github: hkalexling/mg diff --git a/src/handlers/auth_handler.cr b/src/handlers/auth_handler.cr index 53af8e8..b6891d5 100644 --- a/src/handlers/auth_handler.cr +++ b/src/handlers/auth_handler.cr @@ -82,7 +82,12 @@ class AuthHandler < Kemal::Handler if env.session.string? "token" should_reject = !validate_token_admin(env) 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 call_next env diff --git a/src/logger.cr b/src/logger.cr index 92be20e..d434cfa 100644 --- a/src/logger.cr +++ b/src/logger.cr @@ -6,26 +6,14 @@ class Logger SEVERITY_IDS = [0, 4, 5, 2, 3] COLORS = [:light_cyan, :light_red, :red, :light_yellow, :light_magenta] + getter raw_log = Log.for "" + @@severity : Log::Severity = :info use_default def initialize - level = Config.current.log_level - {% 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("") + @@severity = Logger.get_severity @backend = Log::IOBackend.new format_proc = ->(entry : Log::Entry, io : IO) do @@ -49,6 +37,24 @@ class Logger Log.setup @@severity, @backend 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 def log(msg) @backend.write Log::Entry.new "", Log::Severity::None, msg, @@ -61,7 +67,7 @@ class Logger {% for lvl in LEVELS %} def {{lvl.id}}(msg) - @log.{{lvl.id}} { msg } + raw_log.{{lvl.id}} { msg } end def self.{{lvl.id}}(msg) default.not_nil!.{{lvl.id}} msg diff --git a/src/mango.cr b/src/mango.cr index 67d222f..82f6178 100644 --- a/src/mango.cr +++ b/src/mango.cr @@ -8,7 +8,7 @@ require "option_parser" require "clim" require "tallboy" -MANGO_VERSION = "0.18.3" +MANGO_VERSION = "0.19.0" # From http://www.network-science.de/ascii/ BANNER = %{ @@ -63,7 +63,12 @@ class CLI < Clim Plugin::Downloader.default spawn do - Server.new.start + begin + Server.new.start + rescue e + Logger.fatal e + Process.exit 1 + end end MainFiber.start_and_block diff --git a/src/routes/api.cr b/src/routes/api.cr index d4f1aa4..7d60710 100644 --- a/src/routes/api.cr +++ b/src/routes/api.cr @@ -713,6 +713,24 @@ struct APIRouter 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.path "tid", desc: "A title ID" Koa.response 200, ref: "$result" diff --git a/src/server.cr b/src/server.cr index 71973d4..9d9cefa 100644 --- a/src/server.cr +++ b/src/server.cr @@ -7,10 +7,6 @@ require "./routes/*" class Server 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| message = "HTTP 404: Mango cannot find the page #{env.request.path}" layout "message" diff --git a/src/storage.cr b/src/storage.cr index 4818ebd..dcc337c 100644 --- a/src/storage.cr +++ b/src/storage.cr @@ -3,6 +3,8 @@ require "crypto/bcrypt" require "uuid" require "base64" require "./util/*" +require "mg" +require "../migration/*" def hash_password(pw) Crypto::Bcrypt::Password.create(pw).to_s @@ -13,9 +15,10 @@ def verify_password(hash, pw) end class Storage + @@insert_ids = [] of IDTuple + @path : String @db : DB::Database? - @insert_ids = [] of IDTuple alias IDTuple = NamedTuple(path: String, id: String, @@ -35,44 +38,15 @@ class Storage MainFiber.run do DB.open "sqlite3://#{@path}" do |db| begin - # v0.18.0 - 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)" + MG::Migration.new(db, log: Logger.default.raw_log).migrate rescue e - unless e.message.not_nil!.ends_with? "already exists" - Logger.fatal "Error when checking tables in DB: #{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 + Logger.fatal "DB migration failed. #{e}" + raise e 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 if Config.current.disable_login username = Config.current.default_username @@ -99,9 +73,11 @@ class Storage private def get_db(&block : DB::Database ->) if @db.nil? DB.open "sqlite3://#{@path}" do |db| + db.exec "PRAGMA foreign_keys = 1" yield db end else + @db.not_nil!.exec "PRAGMA foreign_keys = 1" yield @db.not_nil! end end @@ -258,28 +234,38 @@ class Storage id = nil MainFiber.run do get_db do |db| - id = db.query_one? "select id from ids where path = (?)", path, - as: {String} + if is_title + 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 id end def insert_id(tp : IDTuple) - @insert_ids << tp + @@insert_ids << tp end def bulk_insert_ids MainFiber.run do get_db do |db| - db.transaction do |tx| - @insert_ids.each do |tp| - tx.connection.exec "insert into ids values (?, ?, ?)", tp[:path], - tp[:id], tp[:is_title] ? 1 : 0 + db.transaction do |tran| + conn = tran.connection + @@insert_ids.each do |tp| + 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 - @insert_ids.clear + @@insert_ids.clear end end @@ -372,6 +358,7 @@ class Storage MainFiber.run do Logger.info "Starting DB optimization" get_db do |db| + # Delete dangling entry IDs trash_ids = [] of String db.query "select path, id from ids" do |rs| rs.each do @@ -380,29 +367,24 @@ class Storage end end - # Delete dangling IDs db.exec "delete from ids where id in " \ "(#{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 - # Delete dangling thumbnails - trash_thumbnails_count = db.query_one "select count(*) from " \ - "thumbnails where id not in " \ - "(select id from ids)", as: Int32 - if trash_thumbnails_count > 0 - db.exec "delete from thumbnails where id not in (select id from ids)" - Logger.info "#{trash_thumbnails_count} dangling thumbnails deleted" + # Delete dangling title IDs + trash_titles = [] of String + db.query "select path, id from titles" do |rs| + rs.each do + path = rs.read String + trash_titles << rs.read String unless Dir.exists? path + end end - # Delete dangling tags - trash_tags_count = db.query_one "select count(*) from tags " \ - "where id not in " \ - "(select id from ids)", as: Int32 - 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 + db.exec "delete from titles where id in " \ + "(#{trash_titles.map { |i| "'#{i}'" }.join ","})" + Logger.debug "#{trash_titles.size} dangling title IDs deleted" \ + if trash_titles.size > 0 end Logger.info "DB optimization finished" end diff --git a/src/util/web.cr b/src/util/web.cr index ee4108e..03c114d 100644 --- a/src/util/web.cr +++ b/src/util/web.cr @@ -1,19 +1,24 @@ # 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) base_url = Config.current.base_url + check_admin_access 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}} render "src/views/#{{{name}}}.html.ecr", "src/views/layout.html.ecr" rescue e @@ -24,6 +29,15 @@ macro layout(name) 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) send_file {{env}}, {{img}}.data, {{img}}.mime end diff --git a/src/views/components/tags.html.ecr b/src/views/components/tags.html.ecr deleted file mode 100644 index 67588a5..0000000 --- a/src/views/components/tags.html.ecr +++ /dev/null @@ -1,12 +0,0 @@ -
-

- Tags: - - -

- -
diff --git a/src/views/download-manager.html.ecr b/src/views/download-manager.html.ecr index 2f25d27..920d4d1 100644 --- a/src/views/download-manager.html.ecr +++ b/src/views/download-manager.html.ecr @@ -43,7 +43,7 @@ diff --git a/src/views/layout.html.ecr b/src/views/layout.html.ecr index 2c240de..82fa636 100644 --- a/src/views/layout.html.ecr +++ b/src/views/layout.html.ecr @@ -37,7 +37,7 @@
- +
-
+
<%= content %> diff --git a/src/views/reader.html.ecr b/src/views/reader.html.ecr index ef84fe2..86f4f4b 100644 --- a/src/views/reader.html.ecr +++ b/src/views/reader.html.ecr @@ -1,5 +1,5 @@ - + <% page = "Reader" %> <%= render_component "head" %> diff --git a/src/views/title.html.ecr b/src/views/title.html.ecr index 5f5c232..30b240e 100644 --- a/src/views/title.html.ecr +++ b/src/views/title.html.ecr @@ -34,7 +34,10 @@

<%= title.content_label %> found

-<%= render_component "tags" %> +
+ +
@@ -121,6 +124,9 @@ <% content_for "script" do %> <%= render_component "dots-scripts" %> + + +