mirror of
https://github.com/discourse/discourse.git
synced 2025-03-06 03:09:43 +00:00
FEATURE: prototype of local theme directory watcher
(note this will be documented a bit late)
This commit is contained in:
parent
74ce2220a7
commit
758b9a7dda
@ -82,6 +82,13 @@ class Admin::ThemesController < Admin::AdminController
|
|||||||
rescue RuntimeError
|
rescue RuntimeError
|
||||||
render_json_error I18n.t('themes.error_importing')
|
render_json_error I18n.t('themes.error_importing')
|
||||||
end
|
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
|
else
|
||||||
render json: @theme.errors, status: :unprocessable_entity
|
render json: @theme.errors, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
require_dependency 'git_importer'
|
require_dependency 'theme_store/git_importer'
|
||||||
|
require_dependency 'theme_store/tgz_importer'
|
||||||
require_dependency 'upload_creator'
|
require_dependency 'upload_creator'
|
||||||
|
|
||||||
class RemoteTheme < ActiveRecord::Base
|
class RemoteTheme < ActiveRecord::Base
|
||||||
@ -7,8 +8,32 @@ class RemoteTheme < ActiveRecord::Base
|
|||||||
|
|
||||||
has_one :theme
|
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)
|
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!
|
importer.import!
|
||||||
|
|
||||||
theme_info = JSON.parse(importer["about.json"])
|
theme_info = JSON.parse(importer["about.json"])
|
||||||
@ -32,19 +57,19 @@ class RemoteTheme < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update_remote_version
|
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!
|
importer.import!
|
||||||
self.updated_at = Time.zone.now
|
self.updated_at = Time.zone.now
|
||||||
self.remote_version, self.commits_behind = importer.commits_since(remote_version)
|
self.remote_version, self.commits_behind = importer.commits_since(remote_version)
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_from_remote(importer = nil)
|
def update_from_remote(importer = nil, skip_update: false)
|
||||||
return unless remote_url
|
return unless remote_url
|
||||||
cleanup = false
|
cleanup = false
|
||||||
|
|
||||||
unless importer
|
unless importer
|
||||||
cleanup = true
|
cleanup = true
|
||||||
importer = GitImporter.new(remote_url, private_key: private_key)
|
importer = ThemeStore::GitImporter.new(remote_url, private_key: private_key)
|
||||||
importer.import!
|
importer.import!
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -99,10 +124,13 @@ class RemoteTheme < ActiveRecord::Base
|
|||||||
|
|
||||||
self.license_url ||= theme_info["license_url"]
|
self.license_url ||= theme_info["license_url"]
|
||||||
self.about_url ||= theme_info["about_url"]
|
self.about_url ||= theme_info["about_url"]
|
||||||
self.remote_updated_at = Time.zone.now
|
|
||||||
self.remote_version = importer.version
|
if !skip_update
|
||||||
self.local_version = importer.version
|
self.remote_updated_at = Time.zone.now
|
||||||
self.commits_behind = 0
|
self.remote_version = importer.version
|
||||||
|
self.local_version = importer.version
|
||||||
|
self.commits_behind = 0
|
||||||
|
end
|
||||||
|
|
||||||
update_theme_color_schemes(theme, theme_info["color_schemes"])
|
update_theme_color_schemes(theme, theme_info["color_schemes"])
|
||||||
|
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
class GitImporter
|
module ThemeStore; end
|
||||||
|
|
||||||
|
class ThemeStore::GitImporter
|
||||||
|
|
||||||
attr_reader :url
|
attr_reader :url
|
||||||
|
|
47
lib/theme_store/tgz_importer.rb
Normal file
47
lib/theme_store/tgz_importer.rb
Normal 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
85
script/theme-watcher
Executable 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
|
37
spec/components/theme_store/tgz_importer_spec.rb
Normal file
37
spec/components/theme_store/tgz_importer_spec.rb
Normal 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
|
Loading…
x
Reference in New Issue
Block a user