FEATURE: prototype of local theme directory watcher

(note this will be documented a bit late)
This commit is contained in:
Sam 2018-03-12 18:36:06 +11:00
parent 74ce2220a7
commit 758b9a7dda
6 changed files with 216 additions and 10 deletions

View File

@ -82,6 +82,13 @@ class Admin::ThemesController < Admin::AdminController
rescue RuntimeError
render_json_error I18n.t('themes.error_importing')
end
elsif params[:bundle]
begin
@theme = RemoteTheme.update_tgz_theme(params[:bundle].path, user: current_user)
render json: @theme, status: :created
rescue RuntimeError
render_json_error I18n.t('themes.error_importing')
end
else
render json: @theme.errors, status: :unprocessable_entity
end

View File

@ -1,4 +1,5 @@
require_dependency 'git_importer'
require_dependency 'theme_store/git_importer'
require_dependency 'theme_store/tgz_importer'
require_dependency 'upload_creator'
class RemoteTheme < ActiveRecord::Base
@ -7,8 +8,32 @@ class RemoteTheme < ActiveRecord::Base
has_one :theme
def self.update_tgz_theme(filename, user: Discourse.system_user)
importer = ThemeStore::TgzImporter.new(filename)
importer.import!
theme_info = JSON.parse(importer["about.json"])
theme = Theme.find_by(name: theme_info["name"])
theme ||= Theme.new(user_id: user&.id || -1, name: theme_info["name"])
remote_theme = new
remote_theme.theme = theme
remote_theme.remote_url = ""
remote_theme.update_from_remote(importer, skip_update: true)
theme.save!
theme
ensure
begin
importer.cleanup!
rescue => e
Rails.logger.warn("Failed cleanup remote path #{e}")
end
end
def self.import_theme(url, user = Discourse.system_user, private_key: nil)
importer = GitImporter.new(url, private_key: private_key)
importer = ThemeStore::GitImporter.new(url, private_key: private_key)
importer.import!
theme_info = JSON.parse(importer["about.json"])
@ -32,19 +57,19 @@ class RemoteTheme < ActiveRecord::Base
end
def update_remote_version
importer = GitImporter.new(remote_url, private_key: private_key)
importer = ThemeStore::GitImporter.new(remote_url, private_key: private_key)
importer.import!
self.updated_at = Time.zone.now
self.remote_version, self.commits_behind = importer.commits_since(remote_version)
end
def update_from_remote(importer = nil)
def update_from_remote(importer = nil, skip_update: false)
return unless remote_url
cleanup = false
unless importer
cleanup = true
importer = GitImporter.new(remote_url, private_key: private_key)
importer = ThemeStore::GitImporter.new(remote_url, private_key: private_key)
importer.import!
end
@ -99,10 +124,13 @@ class RemoteTheme < ActiveRecord::Base
self.license_url ||= theme_info["license_url"]
self.about_url ||= theme_info["about_url"]
if !skip_update
self.remote_updated_at = Time.zone.now
self.remote_version = importer.version
self.local_version = importer.version
self.commits_behind = 0
end
update_theme_color_schemes(theme, theme_info["color_schemes"])

View File

@ -1,4 +1,6 @@
class GitImporter
module ThemeStore; end
class ThemeStore::GitImporter
attr_reader :url

View File

@ -0,0 +1,47 @@
module ThemeStore; end
class ThemeStore::TgzImporter
attr_reader :url
def initialize(filename)
@temp_folder = "#{Pathname.new(Dir.tmpdir).realpath}/discourse_theme_#{SecureRandom.hex}"
@filename = filename
end
def import!
FileUtils.mkdir(@temp_folder)
Dir.chdir(@temp_folder) do
Discourse::Utils.execute_command("tar", "-xzvf", @filename, "--strip", "1")
end
end
def cleanup!
FileUtils.rm_rf(@temp_folder)
end
def version
""
end
def real_path(relative)
fullpath = "#{@temp_folder}/#{relative}"
return nil unless File.exist?(fullpath)
# careful to handle symlinks here, don't want to expose random data
fullpath = Pathname.new(fullpath).realpath.to_s
if fullpath && fullpath.start_with?(@temp_folder)
fullpath
else
nil
end
end
def [](value)
fullpath = real_path(value)
return nil unless fullpath
File.read(fullpath)
end
end

85
script/theme-watcher Executable file
View File

@ -0,0 +1,85 @@
#!/usr/bin/env ruby
require 'fileutils'
require 'pathname'
require 'tempfile'
require 'securerandom'
require 'minitar'
require 'zlib'
require 'find'
require 'net/http'
require 'net/http/post/multipart'
require 'uri'
require 'listen'
# Work in progress theme watcher for Discourse
#
# Monitor a theme directory locally and automatically keep it in sync with Discourse
def usage
puts "Usage: theme-watcher DIR SITE"
exit 1
end
$api_key = ENV['DISCOURSE_API_KEY']
$dir = ARGV[1]
$site = ARGV[2]
if !$api_key
puts "No API key found in DISCOURSE_API_KEY env var enter your API key: "
$api_key = gets
end
if !File.exist?("#{$dir}/about.json")
puts "No about.json file found in #{dir}!"
puts
usage
end
def compress_dir(gzip, dir)
sgz = Zlib::GzipWriter.new(File.open(gzip, 'wb'))
tar = Archive::Tar::Minitar::Output.new(sgz)
Dir.chdir(dir + "/../") do
Find.find(File.basename(dir)) do |x|
Find.prune if File.basename(x)[0] == ?.
next if File.directory?(x)
Minitar.pack_file(x, tar)
end
end
ensure
tar.close
sgz.close
end
def upload_full_theme(dir, site)
filename = "#{Pathname.new(Dir.tmpdir).realpath}/bundle_#{SecureRandom.hex}.tar.gz"
compress_dir(filename, dir)
# new full upload endpoint
uri = URI.parse(site + "/admin/themes/import.json?api_key=#{$api_key}")
http = Net::HTTP.new(uri.host, uri.port)
File.open(filename) do |tgz|
request = Net::HTTP::Post::Multipart.new(
uri.request_uri,
"bundle" => UploadIO.new(tgz, "application/tar+gzip", "bundle.tar.gz"),
)
response = http.request(request)
p response.code
end
ensure
FileUtils.rm_f filename
end
upload_full_theme($dir, $site)
listener = Listen.to($dir) do |modified, added, removed|
puts "Change detected"
upload_full_theme($dir, $site)
end
listener.start
sleep

View File

@ -0,0 +1,37 @@
# encoding: utf-8
require 'rails_helper'
require 'theme_store/tgz_importer'
describe ThemeStore::TgzImporter do
before do
@temp_folder = "#{Pathname.new(Dir.tmpdir).realpath}/discourse_theme_#{SecureRandom.hex}"
end
after do
FileUtils.rm_rf @temp_folder
end
it "can import a simple theme" do
FileUtils.mkdir(@temp_folder)
Dir.chdir(@temp_folder) do
FileUtils.mkdir('test/')
File.write("test/hello.txt", "hello world")
FileUtils.mkdir('test/a')
File.write("test/a/inner", "hello world inner")
`tar -cvzf test.tar.gz test/*`
end
importer = ThemeStore::TgzImporter.new("#{@temp_folder}/test.tar.gz")
importer.import!
expect(importer["hello.txt"]).to eq("hello world")
expect(importer["a/inner"]).to eq("hello world inner")
importer.cleanup!
end
end