386 lines
16 KiB
Crystal
386 lines
16 KiB
Crystal
require "../http-errors"
|
|
require "http/client"
|
|
require "benchmark"
|
|
|
|
module Handling
|
|
extend self
|
|
|
|
def upload(env)
|
|
env.response.content_type = "application/json"
|
|
ip_address = Utils.ip_address(env)
|
|
protocol = Utils.protocol(env)
|
|
host = Utils.host(env)
|
|
# You can modify this if you want to allow files smaller than 1MiB.
|
|
# This is generally a good way to check the filesize but there is a better way to do it
|
|
# which is inspecting the file directly (If I'm not wrong).
|
|
if CONFIG.size_limit > 0
|
|
if env.request.headers["Content-Length"].to_i > 1048576*CONFIG.size_limit
|
|
return error413("File is too big. The maximum size allowed is #{CONFIG.size_limit}MiB")
|
|
end
|
|
end
|
|
filename = ""
|
|
extension = ""
|
|
original_filename = ""
|
|
uploaded_at = ""
|
|
checksum = ""
|
|
if CONFIG.deleteKeyLength > 0
|
|
delete_key = Random.base58(CONFIG.deleteKeyLength)
|
|
end
|
|
# TODO: Return the file that matches a checksum inside the database
|
|
HTTP::FormData.parse(env.request) do |upload|
|
|
if upload.filename.nil? || upload.filename.to_s.empty?
|
|
LOGGER.debug "No file provided by the user"
|
|
return error403("No file provided")
|
|
end
|
|
# TODO: upload.body is emptied when is copied or read
|
|
# Utils.check_duplicate(upload.dup)
|
|
extension = File.extname("#{upload.filename}")
|
|
if CONFIG.blockedExtensions.includes?(extension.split(".")[1])
|
|
return error401("Extension '#{extension}' is not allowed")
|
|
end
|
|
filename = Utils.generate_filename
|
|
file_path = ::File.join ["#{CONFIG.files}", filename + extension]
|
|
File.open(file_path, "w") do |output|
|
|
IO.copy(upload.body, output)
|
|
end
|
|
original_filename = upload.filename
|
|
uploaded_at = Time.utc
|
|
checksum = Utils.hash_file(file_path)
|
|
end
|
|
# X-Forwarded-For if behind a reverse proxy and the header is set in the reverse
|
|
# proxy configuration.
|
|
begin
|
|
spawn { Utils.generate_thumbnail(filename, extension) }
|
|
rescue ex
|
|
LOGGER.error "An error ocurred when trying to generate a thumbnail: #{ex.message}"
|
|
end
|
|
begin
|
|
# Insert SQL data just before returning the upload information
|
|
SQL.exec "INSERT INTO #{CONFIG.dbTableName} VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
original_filename, filename, extension, uploaded_at, checksum, ip_address, delete_key, nil
|
|
SQL.exec "INSERT OR IGNORE INTO #{CONFIG.ipTableName} (ip, date) VALUES (?, ?)", ip_address, Time.utc.to_unix
|
|
# SQL.exec "INSERT OR IGNORE INTO #{CONFIG.ipTableName} (ip) VALUES ('#{ip_address}')"
|
|
SQL.exec "UPDATE #{CONFIG.ipTableName} SET count = count + 1 WHERE ip = ('#{ip_address}')"
|
|
rescue ex
|
|
LOGGER.error "An error ocurred when trying to insert the data into the DB: #{ex.message}"
|
|
return error500("An error ocurred when trying to insert the data into the DB")
|
|
end
|
|
json = JSON.build do |j|
|
|
j.object do
|
|
j.field "link", "#{protocol}://#{host}/#{filename}"
|
|
j.field "linkExt", "#{protocol}://#{host}/#{filename}#{extension}"
|
|
j.field "id", filename
|
|
j.field "ext", extension
|
|
j.field "name", original_filename
|
|
j.field "checksum", checksum
|
|
if CONFIG.deleteKeyLength > 0
|
|
j.field "deleteKey", delete_key
|
|
j.field "deleteLink", "#{protocol}://#{host}/delete?key=#{delete_key}"
|
|
end
|
|
end
|
|
end
|
|
json
|
|
end
|
|
|
|
# The most unoptimized and unstable feature lol
|
|
def upload_url_bulk(env)
|
|
env.response.content_type = "application/json"
|
|
ip_address = Utils.ip_address(env)
|
|
protocol = Utils.protocol(env)
|
|
host = Utils.host(env)
|
|
files = env.params.json["files"].as((Array(JSON::Any)))
|
|
successfull_files = [] of NamedTuple(filename: String, extension: String, original_filename: String, checksum: String, delete_key: String | Nil)
|
|
failed_files = [] of String
|
|
# X-Forwarded-For if behind a reverse proxy and the header is set in the reverse
|
|
# proxy configuration.
|
|
if files.empty?
|
|
end
|
|
files.each do |url|
|
|
url = url.to_s
|
|
filename = Utils.generate_filename
|
|
original_filename = ""
|
|
extension = ""
|
|
checksum = ""
|
|
uploaded_at = Time.utc
|
|
extension = File.extname(URI.parse(url).path)
|
|
delete_key = nil
|
|
file_path = ::File.join ["#{CONFIG.files}", filename + extension]
|
|
File.open(file_path, "w") do |output|
|
|
begin
|
|
HTTP::Client.get(url) do |res|
|
|
IO.copy(res.body_io, output)
|
|
end
|
|
rescue ex
|
|
LOGGER.debug "Failed to download file '#{url}': #{ex.message}"
|
|
return error403("Failed to download file '#{url}'")
|
|
failed_files << url
|
|
end
|
|
end
|
|
# successfull_files << url
|
|
# end
|
|
if extension.empty?
|
|
extension = Utils.detect_extension(file_path)
|
|
File.rename(file_path, file_path + extension)
|
|
file_path = ::File.join ["#{CONFIG.files}", filename + extension]
|
|
end
|
|
# The second one is faster and it uses less memory
|
|
# original_filename = URI.parse("https://ayaya.beauty/PqC").path.split("/").last
|
|
original_filename = url.split("/").last
|
|
checksum = Utils.hash_file(file_path)
|
|
begin
|
|
spawn { Utils.generate_thumbnail(filename, extension) }
|
|
rescue ex
|
|
LOGGER.error "An error ocurred when trying to generate a thumbnail: #{ex.message}"
|
|
end
|
|
begin
|
|
# Insert SQL data just before returning the upload information
|
|
SQL.exec("INSERT INTO #{CONFIG.dbTableName} VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
original_filename, filename, extension, uploaded_at, checksum, ip_address, delete_key, nil)
|
|
successfull_files << {filename: filename,
|
|
original_filename: original_filename,
|
|
extension: extension,
|
|
delete_key: delete_key,
|
|
checksum: checksum}
|
|
rescue ex
|
|
LOGGER.error "An error ocurred when trying to insert the data into the DB: #{ex.message}"
|
|
return error500("An error ocurred when trying to insert the data into the DB")
|
|
end
|
|
end
|
|
json = JSON.build do |j|
|
|
j.array do
|
|
successfull_files.each do |fileinfo|
|
|
j.object do
|
|
j.field "link", "#{protocol}://#{host}/#{fileinfo[:filename]}"
|
|
j.field "linkExt", "#{protocol}://#{host}/#{fileinfo[:filename]}#{fileinfo[:extension]}"
|
|
j.field "id", fileinfo[:filename]
|
|
j.field "ext", fileinfo[:extension]
|
|
j.field "name", fileinfo[:original_filename]
|
|
j.field "checksum", fileinfo[:checksum]
|
|
if CONFIG.deleteKeyLength > 0
|
|
delete_key = Random.base58(CONFIG.deleteKeyLength)
|
|
j.field "deleteKey", fileinfo[:delete_key]
|
|
j.field "deleteLink", "#{protocol}://#{host}/delete?key=#{fileinfo[:delete_key]}"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
json
|
|
end
|
|
|
|
# TODO: Add delete url, same for upload_url_bulk
|
|
def upload_url(env)
|
|
env.response.content_type = "application/json"
|
|
ip_address = Utils.ip_address(env)
|
|
protocol = Utils.protocol(env)
|
|
host = Utils.host(env)
|
|
url = env.params.query["url"]
|
|
successfull_files = [] of NamedTuple(filename: String, extension: String, original_filename: String, checksum: String, delete_key: String | Nil)
|
|
failed_files = [] of String
|
|
# X-Forwarded-For if behind a reverse proxy and the header is set in the reverse
|
|
# proxy configuration.
|
|
if url.empty?
|
|
end
|
|
# files.each do |url|
|
|
url = url.to_s
|
|
filename = Utils.generate_filename
|
|
original_filename = ""
|
|
extension = ""
|
|
checksum = ""
|
|
uploaded_at = Time.utc
|
|
extension = File.extname(URI.parse(url).path)
|
|
delete_key = nil
|
|
file_path = ::File.join ["#{CONFIG.files}", filename + extension]
|
|
File.open(file_path, "w") do |output|
|
|
begin
|
|
HTTP::Client.get(url) do |res|
|
|
IO.copy(res.body_io, output)
|
|
end
|
|
rescue ex
|
|
LOGGER.debug "Failed to download file '#{url}': #{ex.message}"
|
|
return error403("Failed to download file '#{url}'")
|
|
failed_files << url
|
|
end
|
|
end
|
|
# successfull_files << url
|
|
# end
|
|
if extension.empty?
|
|
extension = Utils.detect_extension(file_path)
|
|
File.rename(file_path, file_path + extension)
|
|
file_path = ::File.join ["#{CONFIG.files}", filename + extension]
|
|
end
|
|
# The second one is faster and it uses less memory
|
|
# original_filename = URI.parse("https://ayaya.beauty/PqC").path.split("/").last
|
|
original_filename = url.split("/").last
|
|
checksum = Utils.hash_file(file_path)
|
|
begin
|
|
spawn { Utils.generate_thumbnail(filename, extension) }
|
|
rescue ex
|
|
LOGGER.error "An error ocurred when trying to generate a thumbnail: #{ex.message}"
|
|
end
|
|
begin
|
|
# Insert SQL data just before returning the upload information
|
|
SQL.exec("INSERT INTO #{CONFIG.dbTableName} VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
original_filename, filename, extension, uploaded_at, checksum, ip_address, delete_key, nil)
|
|
successfull_files << {filename: filename,
|
|
original_filename: original_filename,
|
|
extension: extension,
|
|
delete_key: delete_key,
|
|
checksum: checksum}
|
|
rescue ex
|
|
LOGGER.error "An error ocurred when trying to insert the data into the DB: #{ex.message}"
|
|
return error500("An error ocurred when trying to insert the data into the DB")
|
|
end
|
|
# end
|
|
json = JSON.build do |j|
|
|
j.array do
|
|
successfull_files.each do |fileinfo|
|
|
j.object do
|
|
j.field "link", "#{protocol}://#{host}/#{fileinfo[:filename]}"
|
|
j.field "linkExt", "#{protocol}://#{host}/#{fileinfo[:filename]}#{fileinfo[:extension]}"
|
|
j.field "id", fileinfo[:filename]
|
|
j.field "ext", fileinfo[:extension]
|
|
j.field "name", fileinfo[:original_filename]
|
|
j.field "checksum", fileinfo[:checksum]
|
|
if CONFIG.deleteKeyLength > 0
|
|
delete_key = Random.base58(CONFIG.deleteKeyLength)
|
|
j.field "deleteKey", fileinfo[:delete_key]
|
|
j.field "deleteLink", "#{protocol}://#{host}/delete?key=#{fileinfo[:delete_key]}"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
json
|
|
end
|
|
|
|
def retrieve_file(env)
|
|
begin
|
|
protocol = Utils.protocol(env)
|
|
host = Utils.host(env)
|
|
fileinfo = SQL.query_all("SELECT filename, original_filename, uploaded_at, extension, checksum, thumbnail
|
|
FROM #{CONFIG.dbTableName}
|
|
WHERE filename = ?",
|
|
env.params.url["filename"].split(".").first,
|
|
as: {filename: String, ofilename: String, up_at: String, ext: String, checksum: String, thumbnail: String | Nil})[0]
|
|
# Benchmark.ips do |x|
|
|
# x.report("header multiple") { headers(env, {"Content-Disposition" => "inline; filename*=UTF-8''#{fileinfo[:ofilename]}",
|
|
# "Last-Modified" => "#{fileinfo[:up_at]}",
|
|
# "ETag" => "#{fileinfo[:checksum]}"}) }
|
|
# x.report("shorter sleep") do
|
|
# env.response.headers["Content-Disposition"] = "inline; filename*=UTF-8''#{fileinfo[:ofilename]}"
|
|
# env.response.headers["Last-Modified"] = "#{fileinfo[:up_at]}"
|
|
# env.response.headers["ETag"] = "#{fileinfo[:checksum]}"
|
|
# end
|
|
# end
|
|
# `env.response.headers` is faster than `headers(env, Hash(String, String))`
|
|
# https://github.com/kemalcr/kemal/blob/3243b8e0e03568ad3bd9f0ad6f445c871605b821/src/kemal/helpers/helpers.cr#L102C1-L104C4
|
|
env.response.headers["Content-Disposition"] = "inline; filename*=UTF-8''#{fileinfo[:ofilename]}"
|
|
# env.response.headers["Last-Modified"] = "#{fileinfo[:up_at]}"
|
|
env.response.headers["ETag"] = "#{fileinfo[:checksum]}"
|
|
|
|
CONFIG.opengraphUseragents.each do |useragent|
|
|
if env.request.headers.try &.["User-Agent"].includes?(useragent)
|
|
env.response.content_type = "text/html"
|
|
return %(
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta property="og:title" content="#{fileinfo[:ofilename]}">
|
|
<meta property="og:url" content="#{protocol}://#{host}/#{fileinfo[:filename]}">
|
|
#{if fileinfo[:thumbnail]
|
|
%(<meta property="og:image" content="#{protocol}://#{host}/thumbnail/#{fileinfo[:filename]}.jpg">)
|
|
end}
|
|
</head>
|
|
</html>
|
|
)
|
|
end
|
|
end
|
|
send_file env, "#{CONFIG.files}/#{fileinfo[:filename]}#{fileinfo[:ext]}"
|
|
rescue ex
|
|
LOGGER.debug "File '#{env.params.url["filename"]}' does not exist: #{ex.message}"
|
|
return error403("File '#{env.params.url["filename"]}' does not exist")
|
|
end
|
|
end
|
|
|
|
def retrieve_thumbnail(env)
|
|
begin
|
|
send_file env, "#{CONFIG.thumbnails}/#{env.params.url["thumbnail"]}"
|
|
rescue ex
|
|
LOGGER.debug "Thumbnail '#{env.params.url["thumbnail"]}' does not exist: #{ex.message}"
|
|
return error403("Thumbnail '#{env.params.url["thumbnail"]}' does not exist")
|
|
end
|
|
end
|
|
|
|
def stats(env)
|
|
env.response.content_type = "application/json"
|
|
begin
|
|
json_data = JSON.build do |json|
|
|
json.object do
|
|
json.field "stats" do
|
|
json.object do
|
|
json.field "filesHosted", SQL.query_one "SELECT COUNT (filename) FROM #{CONFIG.dbTableName}", as: Int32
|
|
json.field "maxUploadSize", CONFIG.size_limit
|
|
json.field "thumbnailGeneration", CONFIG.generateThumbnails
|
|
json.field "filenameLength", CONFIG.fileameLength
|
|
json.field "alternativeDomains", CONFIG.alternativeDomains
|
|
end
|
|
end
|
|
end
|
|
end
|
|
rescue ex
|
|
LOGGER.error "Unknown error: #{ex.message}"
|
|
return error500("Unknown error")
|
|
end
|
|
json_data
|
|
end
|
|
|
|
def delete_file(env)
|
|
if SQL.query_one "SELECT EXISTS(SELECT 1 FROM #{CONFIG.dbTableName} WHERE delete_key = ?)", env.params.query["key"], as: Bool
|
|
begin
|
|
fileinfo = SQL.query_all("SELECT filename, extension, thumbnail
|
|
FROM #{CONFIG.dbTableName}
|
|
WHERE delete_key = ?",
|
|
env.params.query["key"],
|
|
as: {filename: String, extension: String, thumbnail: String | Nil})[0]
|
|
|
|
# Delete file
|
|
File.delete("#{CONFIG.files}/#{fileinfo[:filename]}#{fileinfo[:extension]}")
|
|
if fileinfo[:thumbnail]
|
|
# Delete thumbnail
|
|
File.delete("#{CONFIG.thumbnails}/#{fileinfo[:thumbnail]}")
|
|
end
|
|
# Delete entry from db
|
|
SQL.exec "DELETE FROM #{CONFIG.dbTableName} WHERE delete_key = ?", env.params.query["key"]
|
|
LOGGER.debug "File '#{fileinfo[:filename]}' was deleted using key '#{env.params.query["key"]}'}"
|
|
return msg("File '#{fileinfo[:filename]}' deleted successfully")
|
|
rescue ex
|
|
LOGGER.error("Unknown error: #{ex.message}")
|
|
return error500("Unknown error")
|
|
end
|
|
else
|
|
LOGGER.debug "Key '#{env.params.query["key"]}' does not exist"
|
|
return error401("Delete key '#{env.params.query["key"]}' does not exist. No files were deleted")
|
|
end
|
|
end
|
|
|
|
def sharex_config(env)
|
|
host = Utils.host(env)
|
|
protocol = Utils.protocol(env)
|
|
env.response.content_type = "application/json"
|
|
# So it's able to download the file instead of displaying it
|
|
env.response.headers["Content-Disposition"] = "attachment; filename=\"#{host}.sxcu\""
|
|
return %({
|
|
"Version": "14.0.1",
|
|
"DestinationType": "ImageUploader, FileUploader",
|
|
"RequestMethod": "POST",
|
|
"RequestURL": "#{protocol}://#{host}/upload",
|
|
"Body": "MultipartFormData",
|
|
"FileFormName": "file",
|
|
"URL": "{json:link}",
|
|
"DeletionURL": "{json:deleteLink}",
|
|
"ErrorMessage": "{json:error}"
|
|
})
|
|
end
|
|
end
|