aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/assets/js/admin/tabs/files.js8
-rw-r--r--src/guff/handlers.cr98
-rw-r--r--src/guff/models/file.cr2
3 files changed, 103 insertions, 5 deletions
diff --git a/data/assets/js/admin/tabs/files.js b/data/assets/js/admin/tabs/files.js
index c3737d0..82af4b4 100644
--- a/data/assets/js/admin/tabs/files.js
+++ b/data/assets/js/admin/tabs/files.js
@@ -10,6 +10,7 @@ jQuery(function($) {
"data-name='%{name|h}' ",
"data-size='%{size|h}' ",
"data-path='%{path|h}' ",
+ "data-url='%{url|h}' ",
">",
"<i class='fa fa-file-o'></i>",
" ",
@@ -130,7 +131,7 @@ jQuery(function($) {
} else {
// select and download file
$(this).click();
- $('#files-download').click();
+ $('#file-actions a[data-id="download"]').click();
}
// stop event
@@ -164,7 +165,10 @@ jQuery(function($) {
$('body').trigger('click');
if (data.id == 'download') {
- alert('TODO: download file');
+ // alert('TODO: download file');
+ var url = $('#files .active').data('url');
+ if (url)
+ location.href = url;
} else if (data.id == 'move') {
alert('TODO: move file');
} else if (data.id == 'delete') {
diff --git a/src/guff/handlers.cr b/src/guff/handlers.cr
index 3e51169..3ff58d0 100644
--- a/src/guff/handlers.cr
+++ b/src/guff/handlers.cr
@@ -576,9 +576,7 @@ module Guff::Handlers
if context.request.method == "GET"
# send body for GET requests
File.open(abs_path) do |fh|
- STDERR.puts "sending #{abs_path}"
IO.copy(fh, context.response)
- STDERR.puts "done sending #{abs_path}"
end
end
end
@@ -634,6 +632,95 @@ module Guff::Handlers
end
end
+ # FIXME: this is very similar to FilesHandler and AssetsHandler, so
+ # maybe combine them?
+ class FileAPIHandler < AuthenticatedHandler
+ def initialize(context : Context)
+ super(context, %w{admin editor})
+ @magic = Magic.new
+ end
+
+ def authenticated_call(context : HTTP::Server::Context)
+ req_path = context.request.path.not_nil!
+
+ if matching_request?(context.request.method, req_path) &&
+ valid_origin_headers?(context.request.headers)
+ # get expanded path to file
+ if abs_path = expand_path(req_path)
+ # get file digest
+ etag = get_file_etag(abs_path)
+
+ # check for cache header
+ if context.request.headers["if-none-match"]? == etag
+ # cached, send 304 not modified
+ context.response.status_code = 304
+ else
+ # not cached, set code and send headers
+ context.response.headers["x-frame-options"] = "SAMEORIGIN"
+ context.response.status_code = 200
+ context.response.content_type = get_mime_type(abs_path)
+ context.response.content_length = File.size(abs_path)
+ context.response.headers["etag"] = etag
+ context.response.headers["content-disposition"] = "attachment"
+
+ if context.request.method == "GET"
+ # send body for GET requests
+ File.open(abs_path) do |fh|
+ IO.copy(fh, context.response)
+ end
+ end
+ end
+ else
+ # expanded path does not exist
+ call_next(context)
+ end
+ else
+ # not a matching request
+ call_next(context)
+ end
+ end
+
+ VALID_METHODS = %w{GET HEAD}
+ PATH_RE = %r{^/guff/api/file/download/}
+
+ private def matching_request?(method, path)
+ VALID_METHODS.includes?(method) && PATH_RE.match(path)
+ end
+
+ private def expand_path(req_path : String) : String?
+ # unescape path, check for nil byte
+ path = URI.unescape(req_path)
+ return nil if path.includes?('\0')
+
+ # build absolute path
+ r = File.join(
+ @context.config.data_dir,
+ "files",
+ File.expand_path(path.gsub(PATH_RE, ""), "/")
+ )
+
+ # return path if file exists, or nil otherwise
+ File.file?(r) ? r : nil
+ end
+
+ private def get_file_etag(path : String) : String
+ st = File.stat(path)
+
+ # FIXME: rather than a hash this should be an HMAC
+ d = OpenSSL::Digest.new("SHA1")
+
+ # FIXME: should this be inode rather than path?
+ d << "%s-%d-%d" % [path, st.size, st.mtime.epoch_ms]
+
+ # return digest
+ d.hexdigest
+ end
+
+ private def get_mime_type(path : String) : String
+ @magic.file(path)
+ end
+ end
+
HANDLERS = [{
:dev => true,
@@ -672,6 +759,11 @@ module Guff::Handlers
:dev => false,
:id => :session,
}, {
+ # NOTE: special handler for api/file, should load
+ # before api handler
+ :dev => false,
+ :id => :file_api,
+ }, {
:dev => false,
:id => :api,
}, {
@@ -705,6 +797,8 @@ module Guff::Handlers
HTTP::DeflateHandler.new
when :session
SessionHandler.new(context)
+ when :file_api
+ FileAPIHandler.new(context)
when :api
APIHandler.new(context)
when :assets
diff --git a/src/guff/models/file.cr b/src/guff/models/file.cr
index bcd6db2..b001c2f 100644
--- a/src/guff/models/file.cr
+++ b/src/guff/models/file.cr
@@ -12,7 +12,7 @@ class Guff::Models::FileModel < Guff::Models::Model
# build base file path
base_path = File.expand_path(path)
- base_url = File.join("/files", base_path)
+ base_url = File.join("/guff/api/file/download", base_path)
Dir.entries(abs_path).select { |file|
# exclude hidden files