aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Duncan <pabs@pablotron.org>2016-07-19 19:47:15 -0400
committerPaul Duncan <pabs@pablotron.org>2016-07-19 19:47:15 -0400
commitcff0eb6ae72e231a55d18200bff9244b3d6a2659 (patch)
treebf183c98eb70d60d7580390dff91a2f50151a487
parent65ea7c412eea275d999f8005f6e8c58687ff5b14 (diff)
downloadguff-cff0eb6ae72e231a55d18200bff9244b3d6a2659.tar.bz2
guff-cff0eb6ae72e231a55d18200bff9244b3d6a2659.zip
add theme installer
-rw-r--r--data/init.yaml24
-rw-r--r--src/guff/cli.cr12
-rw-r--r--src/guff/config.cr9
-rw-r--r--src/guff/models/theme.cr140
-rw-r--r--src/guff/theme/installer.cr31
-rw-r--r--src/guff/theme/installer/file-info.cr2
-rw-r--r--src/guff/theme/installer/manifest.cr2
7 files changed, 201 insertions, 19 deletions
diff --git a/data/init.yaml b/data/init.yaml
index 54fbfc3..4f260b4 100644
--- a/data/init.yaml
+++ b/data/init.yaml
@@ -67,7 +67,8 @@ init_sql:
theme_date DATE NOT NULL,
-- sha1 digest of theme file
- theme_hash TEXT NOT NULL CHECK (
+ -- (allow NULL for system themes)
+ theme_hash TEXT UNIQUE CHECK (
LENGTH(theme_hash) > 0 AND
theme_hash NOT LIKE '% %' AND
theme_hash = LOWER(theme_hash)
@@ -88,8 +89,8 @@ init_sql:
theme_hash,
is_system
) VALUES
- (1, 'default', 'Default', '1.0', '2016-07-18', 'n/a', 1),
- (2, 'blank', 'Blank', '1.0', '2016-07-18', 'n/a', 1)
+ (1, 'default', 'Default', '1.0', '2016-07-18', NULL, 1),
+ (2, 'blank', 'Blank', '1.0', '2016-07-18', NULL, 1)
- |
CREATE TABLE theme_data_types (
@@ -138,9 +139,15 @@ init_sql:
theme_id INTEGER NOT NULL
REFERENCES themes(theme_id),
+ -- path of file
+ file_path TEXT NOT NULL CHECK (
+ LENGTH(file_path) > 1 AND
+ file_path NOT LIKE '/%'
+ ),
+
-- size of file, in bytes
file_size INTEGER NOT NULL
- CHECK (file_size > 0),
+ CHECK (file_size >= 0),
-- sha1 digest of file
file_hash TEXT NOT NULL CHECK (
@@ -148,12 +155,6 @@ init_sql:
file_hash NOT LIKE '% %'
),
- -- path of file
- file_path TEXT NOT NULL CHECK (
- LENGTH(file_path) > 1 AND
- file_path NOT LIKE '/%'
- ),
-
UNIQUE (theme_id, file_path)
)
@@ -182,8 +183,7 @@ init_sql:
-- load order
sort_order INTEGER NOT NULL,
- PRIMARY KEY (file_id, type_id),
- UNIQUE (file_id, type_id)
+ PRIMARY KEY (file_id, type_id)
)
- |
diff --git a/src/guff/cli.cr b/src/guff/cli.cr
index 3752621..680e418 100644
--- a/src/guff/cli.cr
+++ b/src/guff/cli.cr
@@ -129,6 +129,16 @@ module Guff::CLI
)
end
end
+
+ class ThemeInstallAction < Action
+ def run
+ # run theme installer
+ Theme::Installer.run(
+ Context.new(@config),
+ @config.theme_zip_path.not_nil!
+ )
+ end
+ end
end
def self.run(app : String, args : Array(String))
@@ -151,6 +161,8 @@ module Guff::CLI
Actions::ThemeListAction.run(config)
when "pack"
Actions::ThemePackAction.run(config)
+ when "install"
+ Actions::ThemeInstallAction.run(config)
else
# never reached
raise "unknown theme action: #{config.theme_action}"
diff --git a/src/guff/config.cr b/src/guff/config.cr
index 205c65d..94f47df 100644
--- a/src/guff/config.cr
+++ b/src/guff/config.cr
@@ -2,7 +2,8 @@ require "option_parser"
class Guff::Config
property :mode, :env, :host, :port, :data_dir, :system_dir,
- :theme_action, :theme_src_dir, :theme_dst_path
+ :theme_action, :theme_src_dir, :theme_dst_path,
+ :theme_zip_path
DEFAULTS = {
mode: "help",
@@ -17,6 +18,7 @@ class Guff::Config
@theme_action : String
@theme_src_dir : String?
@theme_dst_path : String?
+ @theme_zip_path : String?
def initialize
@mode = DEFAULTS[:mode] as String
@@ -30,7 +32,7 @@ class Guff::Config
VALID_MODES = %w{init run help theme}
- VALID_THEME_ACTIONS = %w{list pack}
+ VALID_THEME_ACTIONS = %w{list pack install}
def mode=(mode : String)
raise "unknown mode: \"#{mode}\"" unless VALID_MODES.includes?(mode)
@@ -168,6 +170,9 @@ Examples:
else
r.theme_dst_path = File.basename(r.theme_src_dir.not_nil!) + ".zip"
end
+ when "install"
+ raise "missing theme zip file" unless args.size > 0
+ r.theme_zip_path = args.shift
end
when "help"
# print help
diff --git a/src/guff/models/theme.cr b/src/guff/models/theme.cr
index 61debce..1e09727 100644
--- a/src/guff/models/theme.cr
+++ b/src/guff/models/theme.cr
@@ -49,6 +49,52 @@ class Guff::Models::ThemeModel < Guff::Models::Model
WHERE a.theme_id = ?
AND b.name = ?
",
+
+ add_theme: "
+ INSERT INTO themes (
+ theme_name,
+ theme_version,
+ theme_date,
+ theme_slug,
+ theme_hash,
+ is_system
+ ) VALUES (?, ?, ?, ?, ?, 0)
+ ",
+
+ add_theme_data: "
+ INSERT INTO theme_data (
+ theme_id,
+ type_id,
+ data_key,
+ data_val
+ ) VALUES (
+ ?,
+ (SELECT type_id FROM theme_data_types WHERE name = ?),
+ ?,
+ ?
+ )
+ ",
+
+ add_theme_file: "
+ INSERT INTO theme_files (
+ theme_id,
+ file_path,
+ file_size,
+ file_hash
+ ) VALUES (?, ?, ?, ?)
+ ",
+
+ add_theme_asset: "
+ INSERT INTO theme_assets (
+ file_id,
+ type_id,
+ sort_order
+ ) VALUES (
+ (SELECT file_id FROM theme_files WHERE theme_id = ? AND file_path = ?),
+ (SELECT type_id FROM theme_asset_types WHERE name = ?),
+ ?
+ )
+ ",
}
def initialize(context : Context)
@@ -75,14 +121,37 @@ class Guff::Models::ThemeModel < Guff::Models::Model
rows
end
- def get(theme_id : Int32)
- @context.dbs.ro.row(SQL[:get], [theme_id.to_s]).not_nil!
+ def get(
+ theme_id : Int32? = nil,
+ slug : String? = nil,
+ )
+ sql = [] of String
+ args = [] of String
+
+ if !theme_id && !slug
+ raise " ThemeModel#get(): missing theme_id and slug arguments"
+ end
+
+ if theme_id
+ # add theme_id filter
+ sql << "theme_id = ?"
+ args << theme_id.to_s
+ end
+
+ if slug
+ # add slug filter
+ sql << "theme_slug = ?"
+ args << slug
+ end
+
+ # exec query, return row
+ @context.dbs.ro.row(SQL[:get] % sql.join(" AND "), args).not_nil!
end
def templates(theme_id : Int32)
unless @cache[:templates].includes?(theme_id)
# get theme
- row = get(theme_id)
+ row = get(theme_id: theme_id)
# load templates
@cache[:templates][theme_id] = (if row["is_system"] == "1"
@@ -105,7 +174,7 @@ class Guff::Models::ThemeModel < Guff::Models::Model
def metadata(theme_id : Int32)
unless @cache[:metadata].includes?(theme_id)
# get theme
- row = get(theme_id)
+ row = get(theme_id: theme_id)
# load templates
@cache[:metadata][theme_id] = (if row["is_system"] == "1"
@@ -139,4 +208,67 @@ class Guff::Models::ThemeModel < Guff::Models::Model
# return results
r
end
+
+ ###############
+ # add methods #
+ ###############
+
+ REQUIRED_FIELDS = %w{name version date}
+
+ def add(
+ manifest : Guff::Theme::Installer::Manifest,
+ zip_hash : String,
+ &done_cb : Proc(Int64, Void) \
+ )
+ # get writable db
+ db = @context.dbs.rw
+
+ # check theme format
+ unless manifest.format == 1
+ raise "Unsupported theme format: #{manifest.format}"
+ end
+
+ db.transaction do
+ # generate slug
+ slug = SecureRandom.hex(24)
+
+ # add theme
+ db.query(SQL[:add_theme], REQUIRED_FIELDS.map { |key|
+ manifest.metadata[key]
+ }.concat([slug, zip_hash]))
+
+ # get theme_id (as string)
+ theme_id = db.last_insert_row_id.to_s
+
+ # add metadata
+ manifest.metadata.each do |key, val|
+ unless REQUIRED_FIELDS.includes?(key)
+ db.query(SQL[:add_theme_data], [theme_id, "metadata", key, val])
+ end
+ end
+
+ # add templates
+ manifest.templates.each do |key, val|
+ db.query(SQL[:add_theme_data], [theme_id, "template", key, val])
+ end
+
+ # add files
+ manifest.files.each do |f|
+ db.query(SQL[:add_theme_file], [theme_id, f.name, f.size.to_s, f.hash])
+ end
+
+ # add style assets
+ manifest.assets.styles.each_with_index do |path, i|
+ db.query(SQL[:add_theme_asset], [theme_id, path, "style", i.to_s])
+ end
+
+ # add script assets
+ manifest.assets.scripts.each_with_index do |path, i|
+ db.query(SQL[:add_theme_asset], [theme_id, path, "script", i.to_s])
+ end
+
+ # pass id to callback
+ done_cb.call(theme_id.to_i64)
+ end
+ end
end
diff --git a/src/guff/theme/installer.cr b/src/guff/theme/installer.cr
new file mode 100644
index 0000000..a546156
--- /dev/null
+++ b/src/guff/theme/installer.cr
@@ -0,0 +1,31 @@
+require "file_utils"
+
+class Guff::Theme::Installer
+ def self.run(context : Context, zip_path : String)
+ new(context).run(zip_path)
+ end
+
+ def initialize(@context : Context)
+ end
+
+ def run(zip_path : String)
+ # parse manifest
+ manifest = Manifest.load(zip_path)
+
+ # add theme to db
+ @context.models.theme.add(manifest, hash_file(zip_path)) do |theme_id|
+ # copy theme to themes dir
+ FileUtils.cp(zip_path, File.join(themes_dir, theme_id.to_s))
+ end
+ end
+
+ private def themes_dir
+ File.join(@context.config.data_dir, "themes")
+ end
+
+ private def hash_file(path : String)
+ d = OpenSSL::Digest.new("SHA1")
+ d.file(path)
+ d.hexdigest
+ end
+end
diff --git a/src/guff/theme/installer/file-info.cr b/src/guff/theme/installer/file-info.cr
index dfbd9d8..51bd383 100644
--- a/src/guff/theme/installer/file-info.cr
+++ b/src/guff/theme/installer/file-info.cr
@@ -1,4 +1,6 @@
class Guff::Theme::Installer::FileInfo
+ getter :name, :size, :hash
+
JSON.mapping(
name: String,
size: Int64,
diff --git a/src/guff/theme/installer/manifest.cr b/src/guff/theme/installer/manifest.cr
index 5e7e07f..364b422 100644
--- a/src/guff/theme/installer/manifest.cr
+++ b/src/guff/theme/installer/manifest.cr
@@ -10,7 +10,7 @@ class Guff::Theme::Installer::Manifest
def self.load(zip_path : String) : Manifest
json = ""
Zip::Archive.open(zip_path) do |zip|
- json = zip.read("guff-manifest.json")
+ json = String.new(zip.read("guff-manifest.json"))
end
# return result