Finish the API endpoint for cover upload

This commit is contained in:
Alex Ling 2020-04-08 07:01:06 +00:00
parent fcf9d39047
commit 8262a163db
13 changed files with 268 additions and 73 deletions

View File

@ -4,11 +4,14 @@ class Config
include YAML::Serializable include YAML::Serializable
property port : Int32 = 9000 property port : Int32 = 9000
property library_path : String = File.expand_path "~/mango/library", home: true property library_path : String = File.expand_path "~/mango/library",
home: true
property db_path : String = File.expand_path "~/mango/mango.db", home: true property db_path : String = File.expand_path "~/mango/mango.db", home: true
@[YAML::Field(key: "scan_interval_minutes")] @[YAML::Field(key: "scan_interval_minutes")]
property scan_interval : Int32 = 5 property scan_interval : Int32 = 5
property log_level : String = "info" property log_level : String = "info"
property upload_path : String = File.expand_path "~/mango/uploads",
home: true
property mangadex = Hash(String, String | Int32).new property mangadex = Hash(String, String | Int32).new
@[YAML::Field(ignore: true)] @[YAML::Field(ignore: true)]

View File

@ -1,6 +1,6 @@
require "kemal" require "kemal"
require "./storage" require "../storage"
require "./util" require "../util"
class AuthHandler < Kemal::Handler class AuthHandler < Kemal::Handler
def initialize(@storage : Storage) def initialize(@storage : Storage)

View File

@ -1,5 +1,5 @@
require "kemal" require "kemal"
require "./logger" require "../logger"
class LogHandler < Kemal::BaseLogHandler class LogHandler < Kemal::BaseLogHandler
def initialize(@logger : Logger) def initialize(@logger : Logger)

View File

@ -1,16 +1,16 @@
require "baked_file_system" require "baked_file_system"
require "kemal" require "kemal"
require "./util" require "../util"
class FS class FS
extend BakedFileSystem extend BakedFileSystem
{% if flag?(:release) %} {% if flag?(:release) %}
{% if read_file? "#{__DIR__}/../dist/favicon.ico" %} {% if read_file? "#{__DIR__}/../../dist/favicon.ico" %}
{% puts "baking ../dist" %} {% puts "baking ../../dist" %}
bake_folder "../dist" bake_folder "../../dist"
{% else %} {% else %}
{% puts "baking ../public" %} {% puts "baking ../../public" %}
bake_folder "../public" bake_folder "../../public"
{% end %} {% end %}
{% end %} {% end %}
end end

View File

@ -0,0 +1,28 @@
require "kemal"
require "../util"
class UploadHandler < Kemal::Handler
def initialize(@upload_dir : String)
end
def call(env)
unless request_path_startswith(env, [UPLOAD_URL_PREFIX]) &&
env.request.method == "GET"
return call_next env
end
pp env.request.path
ary = env.request.path.split(File::SEPARATOR).select { |part| !part.empty? }
ary[0] = @upload_dir
path = File.join ary
pp path
if File.exists? path
send_file env, path
else
env.response.status_code = 404
end
end
end

View File

@ -16,9 +16,8 @@ end
class Entry class Entry
property zip_path : String, book : Title, title : String, property zip_path : String, book : Title, title : String,
size : String, pages : Int32, cover_url : String, id : String, size : String, pages : Int32, id : String, title_id : String,
title_id : String, encoded_path : String, encoded_title : String, encoded_path : String, encoded_title : String, mtime : Time
mtime : Time
def initialize(path, @book, @title_id, storage) def initialize(path, @book, @title_id, storage)
@zip_path = path @zip_path = path
@ -33,17 +32,17 @@ class Entry
end end
file.close file.close
@id = storage.get_id @zip_path, false @id = storage.get_id @zip_path, false
@cover_url = "/api/page/#{@title_id}/#{@id}/1"
@mtime = File.info(@zip_path).modification_time @mtime = File.info(@zip_path).modification_time
end end
def to_json(json : JSON::Builder) def to_json(json : JSON::Builder)
json.object do json.object do
{% for str in ["zip_path", "title", "size", "cover_url", "id", {% for str in ["zip_path", "title", "size", "id", "title_id",
"title_id", "encoded_path", "encoded_title"] %} "encoded_path", "encoded_title"] %}
json.field {{str}}, @{{str.id}} json.field {{str}}, @{{str.id}}
{% end %} {% end %}
json.field "display_name", @book.display_name @title json.field "display_name", @book.display_name @title
json.field "cover_url", cover_url
json.field "pages" { json.number @pages } json.field "pages" { json.number @pages }
json.field "mtime" { json.number @mtime.to_unix } json.field "mtime" { json.number @mtime.to_unix }
end end
@ -57,6 +56,17 @@ class Entry
URI.encode display_name URI.encode display_name
end end
def cover_url
url = "/api/page/#{@title_id}/#{@id}/1"
TitleInfo.new @book.dir do |info|
info_url = info.entry_cover_url[@title]?
unless info_url.nil? || info_url.empty?
url = info_url
end
end
url
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|
page = file.entries page = file.entries
@ -132,6 +142,7 @@ class Title
json.field {{str}}, @{{str.id}} json.field {{str}}, @{{str.id}}
{% end %} {% end %}
json.field "display_name", display_name json.field "display_name", display_name
json.field "cover_url", cover_url
json.field "mtime" { json.number @mtime.to_unix } json.field "mtime" { json.number @mtime.to_unix }
json.field "titles" do json.field "titles" do
json.raw self.titles.to_json json.raw self.titles.to_json
@ -190,9 +201,12 @@ class Title
end end
def display_name def display_name
info = TitleInfo.new @dir dn = @title
dn = info.display_name TitleInfo.new @dir do |info|
dn.empty? ? @title : dn info_dn = info.display_name
dn = info_dn unless info_dn.empty?
end
dn
end end
def encoded_display_name def encoded_display_name
@ -200,48 +214,80 @@ class Title
end end
def display_name(entry_name) def display_name(entry_name)
info = TitleInfo.new @dir dn = entry_name
dn = info.entry_display_name[entry_name]? TitleInfo.new @dir do |info|
unless dn.nil? || dn.empty? info_dn = info.entry_display_name[entry_name]?
return dn unless info_dn.nil? || info_dn.empty?
dn = info_dn
end end
entry_name end
dn
end end
def set_display_name(dn) def set_display_name(dn)
info = TitleInfo.new @dir TitleInfo.new @dir do |info|
info.display_name = dn info.display_name = dn
info.save info.save
end end
end
def set_display_name(entry_name : String, dn) def set_display_name(entry_name : String, dn)
info = TitleInfo.new @dir TitleInfo.new @dir do |info|
info.entry_display_name[entry_name] = dn info.entry_display_name[entry_name] = dn
info.save info.save
end end
end
def cover_url
url = "img/icon.png"
if @entries.size > 0
url = @entries[0].cover_url
end
TitleInfo.new @dir do |info|
info_url = info.cover_url
unless info_url.nil? || info_url.empty?
url = info_url
end
end
url
end
def set_cover_url(url : String)
TitleInfo.new @dir do |info|
info.cover_url = url
info.save
end
end
def set_cover_url(entry_name : String, url : String)
TitleInfo.new @dir do |info|
info.entry_cover_url[entry_name] = url
info.save
end
end
# For backward backward compatibility with v0.1.0, we save entry titles # For backward backward compatibility with v0.1.0, we save entry titles
# instead of IDs in info.json # instead of IDs in info.json
def save_progress(username, entry, page) def save_progress(username, entry, page)
info = TitleInfo.new @dir TitleInfo.new @dir do |info|
if info.progress[username]?.nil? if info.progress[username]?.nil?
info.progress[username] = {entry => page} info.progress[username] = {entry => page}
info.save else
return
end
info.progress[username][entry] = page info.progress[username][entry] = page
end
info.save info.save
end end
end
def load_progress(username, entry) def load_progress(username, entry)
info = TitleInfo.new @dir progress = 0
if info.progress[username]?.nil? TitleInfo.new @dir do |info|
return 0 unless info.progress[username]?.nil? ||
info.progress[username][entry]?.nil?
progress = info.progress[username][entry]
end end
if info.progress[username][entry]?.nil?
return 0
end end
info.progress[username][entry] progress
end end
def load_percetage(username, entry) def load_percetage(username, entry)
@ -272,22 +318,32 @@ class TitleInfo
include JSON::Serializable include JSON::Serializable
property comment = "Generated by Mango. DO NOT EDIT!" property comment = "Generated by Mango. DO NOT EDIT!"
# { user1: { entry1: 10, entry2: 0 } }
property progress = {} of String => Hash(String, Int32) property progress = {} of String => Hash(String, Int32)
property display_name = "" property display_name = ""
# { entry1 : "display name" }
property entry_display_name = {} of String => String property entry_display_name = {} of String => String
property cover_url = ""
property entry_cover_url = {} of String => String
@[JSON::Field(ignore: true)] @[JSON::Field(ignore: true)]
property dir : String = "" property dir : String = ""
def initialize(@dir) @@mutex_hash = {} of String => Mutex
json_path = File.join @dir, "info.json"
def self.new(dir, &)
if @@mutex_hash[dir]?
mutex = @@mutex_hash[dir]
else
mutex = Mutex.new
@@mutex_hash[dir] = mutex
end
mutex.synchronize do
instance = TitleInfo.allocate
json_path = File.join dir, "info.json"
if File.exists? json_path if File.exists? json_path
info = TitleInfo.from_json File.read json_path instance = TitleInfo.from_json File.read json_path
@progress = info.progress.clone end
@display_name = info.display_name instance.dir = dir
@entry_display_name = info.entry_display_name.clone yield instance
end end
end end

View File

@ -1,5 +1,6 @@
require "./router" require "./router"
require "../mangadex/*" require "../mangadex/*"
require "../upload"
class APIRouter < Router class APIRouter < Router
def setup def setup
@ -197,5 +198,59 @@ class APIRouter < Router
}.to_json }.to_json
end end
end end
post "/api/admin/upload/:target" do |env|
begin
target = env.params.url["target"]
HTTP::FormData.parse env.request do |part|
next if part.name != "file"
filename = part.filename
if filename.nil?
raise "No file uploaded"
end
case target
when "cover"
title_id = env.params.query["title"]
entry_id = env.params.query["entry"]?
title = @context.library.get_title(title_id).not_nil!
unless ["image/jpeg", "image/png"].includes? \
MIME.from_filename? filename
raise "The uploaded image must be either JPEG or PNG"
end
ext = File.extname filename
upload = Upload.new @context.config.upload_path, @context.logger
url = upload.path_to_url upload.save "img", ext, part.body
if url.nil?
raise "Failed to generate a public URL for the uploaded file"
end
if entry_id.nil?
title.set_cover_url url
else
entry_name = title.get_entry(entry_id).not_nil!.title
title.set_cover_url entry_name, url
end
else
raise "Unkown upload target #{target}"
end
send_json env, {"success" => true}.to_json
env.response.close
end
raise "No part with name `file` found"
rescue e
send_json env, {
"success" => false,
"error" => e.message,
}.to_json
end
end
end end
end end

View File

@ -1,8 +1,6 @@
require "kemal" require "kemal"
require "./context" require "./context"
require "./auth_handler" require "./handlers/*"
require "./static_handler"
require "./log_handler"
require "./util" require "./util"
require "./routes/*" require "./routes/*"
@ -29,6 +27,7 @@ class Server
Kemal.config.logging = false Kemal.config.logging = false
add_handler LogHandler.new @context.logger add_handler LogHandler.new @context.logger
add_handler AuthHandler.new @context.storage add_handler AuthHandler.new @context.storage
add_handler UploadHandler.new @context.config.upload_path
{% if flag?(:release) %} {% if flag?(:release) %}
# when building for relase, embed the static files in binary # when building for relase, embed the static files in binary
@context.debug "We are in release mode. Using embedded static files." @context.debug "We are in release mode. Using embedded static files."

View File

@ -2,6 +2,7 @@ require "sqlite3"
require "crypto/bcrypt" require "crypto/bcrypt"
require "uuid" require "uuid"
require "base64" require "base64"
require "./util"
def hash_password(pw) def hash_password(pw)
Crypto::Bcrypt::Password.create(pw).to_s Crypto::Bcrypt::Password.create(pw).to_s
@ -11,10 +12,6 @@ def verify_password(hash, pw)
(Crypto::Bcrypt::Password.new hash).verify pw (Crypto::Bcrypt::Password.new hash).verify pw
end end
def random_str
UUID.random.to_s.gsub "-", ""
end
class Storage class Storage
def initialize(@path : String, @logger : Logger) def initialize(@path : String, @logger : Logger)
dir = File.dirname path dir = File.dirname path

60
src/upload.cr Normal file
View File

@ -0,0 +1,60 @@
require "./util"
class Upload
def initialize(@dir : String, @logger : Logger)
unless Dir.exists? @dir
@logger.info "The uploads directory #{@dir} does not exist. " \
"Attempting to create it"
Dir.mkdir_p @dir
end
end
# Writes IO to a file with random filename in the uploads directory and
# returns the full path of created file
# e.g., save("image", ".png", <io>)
# ==> "~/mango/uploads/image/<random string>.png"
def save(sub_dir : String, ext : String, io : IO)
full_dir = File.join @dir, sub_dir
filename = random_str + ext
file_path = File.join full_dir, filename
unless Dir.exists? full_dir
@logger.debug "creating directory #{full_dir}"
Dir.mkdir_p full_dir
end
File.open file_path, "w" do |f|
IO.copy io, f
end
file_path
end
# Converts path to a file in the uploads directory to the URL path for
# accessing the file.
def path_to_url(path : String)
dir_mathed = false
ary = [] of String
# We fill it with parts until it equals to @upload_dir
dir_ary = [] of String
Path.new(path).each_part do |part|
if dir_mathed
ary << part
else
dir_ary << part
if File.same? @dir, File.join dir_ary
dir_mathed = true
end
end
end
if ary.empty?
@logger.warn "File #{path} is not in the upload directory #{@dir}"
return
end
ary.unshift UPLOAD_URL_PREFIX
File.join(ary).to_s
end
end

View File

@ -1,6 +1,7 @@
require "big" require "big"
IMGS_PER_PAGE = 5 IMGS_PER_PAGE = 5
UPLOAD_URL_PREFIX = "/uploads"
macro layout(name) macro layout(name)
begin begin
@ -27,9 +28,9 @@ macro get_username(env)
(@context.storage.verify_token cookie.value).not_nil! (@context.storage.verify_token cookie.value).not_nil!
end end
macro send_json(env, json) def send_json(env, json)
{{env}}.response.content_type = "application/json" env.response.content_type = "application/json"
{{json}} env.response.print json
end end
def hash_to_query(hash) def hash_to_query(hash)
@ -81,3 +82,7 @@ end
def compare_alphanumerically(a : String, b : String) def compare_alphanumerically(a : String, b : String)
compare_alphanumerically split_by_alphanumeric(a), split_by_alphanumeric(b) compare_alphanumerically split_by_alphanumeric(a), split_by_alphanumeric(b)
end end
def random_str
UUID.random.to_s.gsub "-", ""
end

View File

@ -26,11 +26,7 @@
<a class="acard" href="/book/<%= t.id %>"> <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">
<%- if t.entries.size > 0 -%> <img data-src="<%= t.cover_url %>" data-width data-height alt="" uk-img>
<img data-src="<%= t.entries[0].cover_url %>" data-width data-height alt="" uk-img>
<%- else -%>
<img data-src="/img/icon.png" data-width data-height alt="" uk-img>
<%- end -%>
</div> </div>
<div class="uk-card-body"> <div class="uk-card-body">
<%- if t.entries.size > 0 -%> <%- if t.entries.size > 0 -%>

View File

@ -48,11 +48,7 @@
<a class="acard" href="/book/<%= t.id %>"> <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">
<%- if t.entries.size > 0 -%> <img data-src="<%= t.cover_url %>" data-width data-height alt="" uk-img>
<img data-src="<%= t.entries[0].cover_url %>" data-width data-height alt="" uk-img>
<%- else -%>
<img data-src="/img/icon.png" data-width data-height alt="" uk-img>
<%- end -%>
</div> </div>
<div class="uk-card-body"> <div class="uk-card-body">
<h3 class="uk-card-title break-word" data-title="<%= t.display_name.gsub("\"", "&quot;") %>"><%= t.display_name %></h3> <h3 class="uk-card-title break-word" data-title="<%= t.display_name.gsub("\"", "&quot;") %>"><%= t.display_name %></h3>