aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Duncan <pabs@pablotron.org>2016-07-19 21:03:35 -0400
committerPaul Duncan <pabs@pablotron.org>2016-07-19 21:03:35 -0400
commitd339a8fe4768bf6b7df241af896f1113c3f3d89b (patch)
tree84e50e6ed3e30fb8cfe347aea56a3bb765d3724c
parentcff0eb6ae72e231a55d18200bff9244b3d6a2659 (diff)
downloadguff-d339a8fe4768bf6b7df241af896f1113c3f3d89b.tar.bz2
guff-d339a8fe4768bf6b7df241af896f1113c3f3d89b.zip
add /guff/themes/ handler
-rw-r--r--src/guff/cli.cr2
-rw-r--r--src/guff/handlers.cr72
-rw-r--r--src/guff/models/theme.cr69
3 files changed, 140 insertions, 3 deletions
diff --git a/src/guff/cli.cr b/src/guff/cli.cr
index 680e418..e5d8c6c 100644
--- a/src/guff/cli.cr
+++ b/src/guff/cli.cr
@@ -32,7 +32,7 @@ module Guff::CLI
end
# list of subdirectories to create in data directory
- DIRS = %w{files themes cache/themes}
+ DIRS = %w{files themes cache/theme-files}
def run
# create data dir
diff --git a/src/guff/handlers.cr b/src/guff/handlers.cr
index a0cb335..efdf6e0 100644
--- a/src/guff/handlers.cr
+++ b/src/guff/handlers.cr
@@ -600,6 +600,73 @@ module Guff::Handlers
end
end
+ # FIXME: this is very similar to AssetsHandler, so maybe combine them?
+ class ThemesHandler < Handler
+ def call(context : HTTP::Server::Context)
+ req_path = context.request.path.not_nil!
+
+ if matching_request?(context.request.method, req_path) &&
+ true # valid_origin_headers?(context.request.headers)
+ # get expanded path to file
+ if abs_path = expand_path(req_path)
+ # get file digest
+ etag = 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 = @context.magic.file(abs_path)
+ context.response.content_length = File.size(abs_path)
+ context.response.headers["etag"] = etag
+
+ 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/themes/[^/]+/[^/]+}
+
+ EXTRACT_RE = %r{^/guff/themes/(?<slug>[^/]+)/(?<path>.+)$}
+
+ 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')
+
+ # get theme slug and file path
+ md = EXTRACT_RE.match(path)
+ return nil unless md
+
+ # map slug and file path to absolute path
+ @context.models.theme.get_file_path(
+ slug: md["slug"],
+ path: md["path"],
+ )
+ end
+ end
+
# FIXME: this is very similar to FilesHandler and AssetsHandler, so
# maybe combine them?
class FileAPIHandler < AuthenticatedHandler
@@ -691,6 +758,9 @@ module Guff::Handlers
:id => :files,
}, {
:dev => false,
+ :id => :themes,
+ }, {
+ :dev => false,
:id => :page,
}, {
:dev => false,
@@ -768,6 +838,8 @@ module Guff::Handlers
ProjectHandler.new(context)
when :files
FilesHandler.new(context)
+ when :themes
+ ThemesHandler.new(context)
else
raise "unknown handler id: #{handler_id}"
end
diff --git a/src/guff/models/theme.cr b/src/guff/models/theme.cr
index 1e09727..e2eda39 100644
--- a/src/guff/models/theme.cr
+++ b/src/guff/models/theme.cr
@@ -3,6 +3,7 @@ class Guff::Models::ThemeModel < Guff::Models::Model
all: "
SELECT a.theme_id,
a.theme_name,
+ a.theme_slug,
a.theme_version,
a.theme_date,
a.is_system,
@@ -21,6 +22,7 @@ class Guff::Models::ThemeModel < Guff::Models::Model
get: "
SELECT a.theme_id,
+ a.theme_slug,
a.theme_name,
a.theme_version,
a.theme_date,
@@ -35,7 +37,7 @@ class Guff::Models::ThemeModel < Guff::Models::Model
FROM themes a
- WHERE theme_id = ?
+ WHERE %s
",
get_data: "
@@ -95,6 +97,17 @@ class Guff::Models::ThemeModel < Guff::Models::Model
?
)
",
+
+ get_file_path: "
+ SELECT b.file_id
+
+ FROM themes a
+ JOIN theme_files b
+ ON (b.theme_id = a.theme_id)
+
+ WHERE a.theme_slug = ?
+ AND b.file_path = ?
+ ",
}
def initialize(context : Context)
@@ -230,7 +243,7 @@ class Guff::Models::ThemeModel < Guff::Models::Model
db.transaction do
# generate slug
- slug = SecureRandom.hex(24)
+ slug = SecureRandom.hex(16)
# add theme
db.query(SQL[:add_theme], REQUIRED_FIELDS.map { |key|
@@ -271,4 +284,56 @@ class Guff::Models::ThemeModel < Guff::Models::Model
done_cb.call(theme_id.to_i64)
end
end
+
+ ################
+ # file methods #
+ ################
+
+ def get_file_path(slug : String, path : String) : String?
+ # get theme
+ theme = get(slug: slug)
+ return nil unless theme
+
+ if theme["is_system"].to_s == "1"
+ File.join(
+ @context.config.system_dir,
+ "themes",
+ theme["theme_slug"].to_s,
+ "files",
+ File.expand_path(path, "/")
+ )
+ else
+ # map slug and path to file_id
+ file_id = @context.dbs.ro.one(SQL[:get_file_path], [slug, path])
+ return nil unless file_id
+
+ # build path to cached file
+ dst_path = File.join(
+ @context.config.data_dir,
+ "cache",
+ "theme-files",
+ file_id.not_nil!.to_s
+ )
+
+ unless File.exists?(dst_path)
+ # cached theme file does not exist, so extract it from
+ # theme archive and save it in cache directory
+ Zip::Archive.open(File.join(
+ @context.config.data_dir,
+ "themes",
+ theme["theme_id"].to_s
+ )) do |zip|
+ # extract file to cache
+ File.open(dst_path, "wb") do |fh|
+ zip.read(File.join("files", path)) do |buf, len|
+ fh.write(buf[0, len])
+ end
+ end
+ end
+ end
+
+ # return path
+ File.exists?(dst_path) ? dst_path : nil
+ end
+ end
end