From f442f7a57b3b6b988f1d914b4ef5c53993b1fa90 Mon Sep 17 00:00:00 2001 From: Paul Duncan Date: Mon, 18 Jul 2016 21:16:59 -0400 Subject: add theme packer --- src/guff.cr | 1 + src/guff/cli.cr | 25 +++++++ src/guff/config.cr | 71 ++++++++++++++++---- src/guff/theme/assets.cr | 15 +++++ src/guff/theme/file-info.cr | 18 +++++ src/guff/theme/source-manifest.cr | 135 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 251 insertions(+), 14 deletions(-) create mode 100644 src/guff/theme/assets.cr create mode 100644 src/guff/theme/file-info.cr create mode 100644 src/guff/theme/source-manifest.cr (limited to 'src') diff --git a/src/guff.cr b/src/guff.cr index cfd020c..f0573a3 100644 --- a/src/guff.cr +++ b/src/guff.cr @@ -4,6 +4,7 @@ require "json" require "yaml" require "secure_random" require "sqlite3" +require "libzip-crystal/zip" module Guff # TODO: think about versioning a bit (semantic, datestamp, etc) diff --git a/src/guff/cli.cr b/src/guff/cli.cr index c3fb4d6..15ff5a4 100644 --- a/src/guff/cli.cr +++ b/src/guff/cli.cr @@ -103,6 +103,21 @@ module Guff::CLI end end end + + class ThemeListAction < Action + def run + puts "TODO: not implemented" + end + end + + class ThemePackAction < Action + def run + Theme::SourceManifest.save( + src_dir: @config.theme_src_dir.not_nil!, + dst_path: @config.theme_dst_path.not_nil!, + ) + end + end end def self.run(app : String, args : Array(String)) @@ -119,6 +134,16 @@ module Guff::CLI Actions::InitAction.run(config) when "run" Actions::RunAction.run(config) + when "theme" + case config.theme_action + when "list" + Actions::ThemeListAction.run(config) + when "pack" + Actions::ThemePackAction.run(config) + else + # never reached + raise "unknown theme action: #{config.theme_action}" + end when "help" # do nothing else diff --git a/src/guff/config.cr b/src/guff/config.cr index 534e796..205c65d 100644 --- a/src/guff/config.cr +++ b/src/guff/config.cr @@ -1,27 +1,36 @@ require "option_parser" class Guff::Config - property :mode, :env, :host, :port, :data_dir, :system_dir + property :mode, :env, :host, :port, :data_dir, :system_dir, + :theme_action, :theme_src_dir, :theme_dst_path DEFAULTS = { - mode: "help", - env: "production", - host: "127.0.0.1", - port: "8989", - data_dir: "./data", - system_dir: "/usr/local/share/guff", + mode: "help", + env: "production", + host: "127.0.0.1", + port: "8989", + data_dir: "./data", + system_dir: "/usr/local/share/guff", + theme_action: "list" } + @theme_action : String + @theme_src_dir : String? + @theme_dst_path : String? + def initialize - @mode = DEFAULTS[:mode] as String - @env = (ENV["GUFF_ENVIRONMENT"]? || DEFAULTS[:env]) as String - @host = (ENV["GUFF_HOST"]? || DEFAULTS[:host]) as String - @port = (ENV["GUFF_PORT"]? || DEFAULTS[:port]) as String - @data_dir = (ENV["GUFF_DATA_DIR"]? || DEFAULTS[:data_dir]) as String - @system_dir = (ENV["GUFF_SYSTEM_DIR"]? || DEFAULTS[:system_dir]) as String + @mode = DEFAULTS[:mode] as String + @env = (ENV["GUFF_ENVIRONMENT"]? || DEFAULTS[:env]) as String + @host = (ENV["GUFF_HOST"]? || DEFAULTS[:host]) as String + @port = (ENV["GUFF_PORT"]? || DEFAULTS[:port]) as String + @data_dir = (ENV["GUFF_DATA_DIR"]? || DEFAULTS[:data_dir]) as String + @system_dir = (ENV["GUFF_SYSTEM_DIR"]? || DEFAULTS[:system_dir]) as String + @theme_action = DEFAULTS[:theme_action] end - VALID_MODES = %w{init run help} + VALID_MODES = %w{init run help theme} + + VALID_THEME_ACTIONS = %w{list pack} def mode=(mode : String) raise "unknown mode: \"#{mode}\"" unless VALID_MODES.includes?(mode) @@ -120,12 +129,46 @@ class Guff::Config p.on("-h", "--help", "Print usage.") do r.mode = "help" end + + p.separator + p.separator(" +Examples: + # init new data directory + guff init some-app + + # run using directory some-app + guff run -D some-app + + # pack theme in directory /path/to/some-theme as ./some-theme.zip + guff theme pack /path/to/some-theme + + # pack theme in directory /path/to/some-theme as /other/path/output.zip + guff theme pack /path/to/some-theme /other/path/output.zip + ") end case r.mode when "init" # shortcut for -D parameter r.data_dir = args.shift if args.size > 0 + when "theme" + # get theme action + r.theme_action = args.shift if args.size > 0 + unless VALID_THEME_ACTIONS.includes?(r.theme_action) + raise "unknown theme action: \"#{r.theme_action}\"" + end + + case r.theme_action + when "pack" + raise "missing theme directory" unless args.size > 0 + r.theme_src_dir = args.shift + + if args.size > 0 + r.theme_dst_path = args.shift + else + r.theme_dst_path = File.basename(r.theme_src_dir.not_nil!) + ".zip" + end + end when "help" # print help puts p diff --git a/src/guff/theme/assets.cr b/src/guff/theme/assets.cr new file mode 100644 index 0000000..5f15106 --- /dev/null +++ b/src/guff/theme/assets.cr @@ -0,0 +1,15 @@ +class Guff::Theme::Assets + getter :scripts, :styles + + JSON.mapping( + scripts: Array(String), + styles: Array(String), + ) + + def to_json(io) + { + scripts: @scripts, + styles: @styles, + }.to_json(io) + end +end diff --git a/src/guff/theme/file-info.cr b/src/guff/theme/file-info.cr new file mode 100644 index 0000000..914be75 --- /dev/null +++ b/src/guff/theme/file-info.cr @@ -0,0 +1,18 @@ +class Guff::Theme::FileInfo + getter :size, :hash + + def initialize(abs_path : String, @path : String) + @size = File.size(abs_path) + + d = OpenSSL::Digest.new("SHA1") + @hash = d.file(abs_path).hexdigest as String + end + + def to_json(io) + { + name: @path, + size: @size, + hash: @hash, + }.to_json(io) + end +end diff --git a/src/guff/theme/source-manifest.cr b/src/guff/theme/source-manifest.cr new file mode 100644 index 0000000..5dc1065 --- /dev/null +++ b/src/guff/theme/source-manifest.cr @@ -0,0 +1,135 @@ +class Guff::Theme::SourceManifest + getter :format, :metadata, :assets, :files + + JSON.mapping( + format: Int32, + metadata: Hash(String, String), + assets: Assets, + files: Array(String), + ) + + REQUIRED_METADATA_FIELDS = { + "name" => /\S+/, + "version" => /\S+/, + "date" => /^\d{4}-\d{2}-\d{2}$/, + } + + def self.save(src_dir : String, dst_path : String) + load(src_dir).save(dst_path, src_dir) + end + + def self.load(src_dir : String) : SourceManifest + r = from_json(File.read(File.join(src_dir, "guff-manifest.json"))) + + # check format version + unless r.format == 1 + raise "unknown theme format: #{r.format}" + end + + # check for required manifest files + REQUIRED_METADATA_FIELDS.each do |key, re| + unless r.metadata[key]? + # missing field + raise "missing required metadata field: #{key}" + end + + unless r.metadata[key].match(re) + # invalid field format + raise "metadata field format mismatch: #{key}" + end + end + + # build path to files dir + base_dir = File.join(src_dir, "files") + + # check manifest for missing files + missing_files = r.files.select do |path| + !File.exists?(File.join(base_dir, path)) + end + + # raise error if there are missing files + if missing_files.size > 0 + raise "Missing files: #{missing_files.join(", ")}" + end + + # build path lut + lut = r.files.reduce({} of String => Bool) do |rl, path| + rl[path] = true + rl + end + + # check assets + { + scripts: r.assets.scripts, + styles: r.assets.styles, + }.each do |type, paths| + # find assets specified in manifest but not in + missing = paths.select { |path| !lut[path]? } + + if missing.size > 0 + raise "Missing #{type} in manifest: #{missing.join(", ")}" + end + end + + # check files directory for entries missing from manifest + Dir.glob(File.join(base_dir, "**")).each do |path| + unless File.directory?(path) + # build relative path + rel_path = path.gsub(base_dir, "") + raise "File missing from manifest: #{path}" unless lut[rel_path]? + end + end + + # return result + r + end + + def to_json(io : IO, src_dir : String) + # build directory paths + files_dir = File.join(src_dir, "files") + templates_dir = File.join(src_dir, "templates") + + # return output json + { + # format version + format: 1, + + # theme metadata + metadata: @metadata, + + # theme files + files: @files.map { |path| + FileInfo.new(File.join(files_dir, path), path) + }, + + # theme templates + templates: Dir.glob(File.join(templates_dir, "**")).select { |path| + !File.directory?(path) + }.reduce({} of String => String) do |r, path| + r[path.gsub(templates_dir + "/", "")] = File.read(path) + r + end, + + # theme assets (css and js files) + assets: @assets, + }.to_json(io) + end + + def save(dst_path : String, src_dir : String) + Zip::Archive.create(dst_path) do |zip| + # add manifest + zip.add("guff-manifest.json", String.build do |io| + to_json(io, src_dir) + end) + + # add files + @files.each do |path| + # build path + dst_path = File.join("files", path) + + # add file to archive + zip.add_file(dst_path, File.join(src_dir, dst_path)) + end + end + end +end -- cgit v1.2.3