From d339a8fe4768bf6b7df241af896f1113c3f3d89b Mon Sep 17 00:00:00 2001 From: Paul Duncan Date: Tue, 19 Jul 2016 21:03:35 -0400 Subject: add /guff/themes/ handler --- src/guff/cli.cr | 2 +- src/guff/handlers.cr | 72 ++++++++++++++++++++++++++++++++++++++++++++++++ src/guff/models/theme.cr | 69 ++++++++++++++++++++++++++++++++++++++++++++-- 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/(?[^/]+)/(?.+)$} + + 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 @@ -689,6 +756,9 @@ module Guff::Handlers }, { :dev => false, :id => :files, + }, { + :dev => false, + :id => :themes, }, { :dev => false, :id => :page, @@ -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 -- cgit v1.2.3