#!/usr/bin/env ruby require 'siteleaf' require 'jekyll' require 'fileutils' require 'pathutil' require 'open-uri' require 'tempfile' require 'yaml' def help %Q( Usage: siteleaf [COMMAND] [OPTIONS] Commands: auth Login in with your credentials c, config DOMAIN Configure an existing directory n, new DOMAIN Creates new site on siteleaf.net clean Clean up unused uploads pull Pulls files for configured site from Siteleaf push Pushes all files in dir to configured site publish Publish website to hosting provider preview Generate cloud preview help Prints this help document version Prints the siteleaf gem version Options: -h, --help Prints this help document -v, --version Prints the siteleaf gem version -q, --quiet Suppress publish status See https://github.com/siteleaf/siteleaf-gem for additional documentation. ) end def auth(re_auth = false) Siteleaf.load_settings if !re_auth && !(Siteleaf.api_key && Siteleaf.api_secret) if re_auth or !(Siteleaf.api_key && Siteleaf.api_secret) print 'Enter your Siteleaf email: ' email = $stdin.gets.chomp print 'Enter your Siteleaf password: ' system 'stty -echo' password = $stdin.gets.chomp system 'stty echo' puts "\nAuthorizing..." auth = Siteleaf::Client.auth(email, password) if auth.is_a?(Hash) && auth.has_key?('api_key') Siteleaf.save_settings({api_key: auth['api_key'], api_secret: auth['api_secret']}) Siteleaf.load_settings puts "=> Account authorized." if re_auth return true else raise "Could not authorize, check your email or password." end end end def pull(site_id) print "Reading site...\n" # get all the things site = Siteleaf::Site.find(site_id) site_files = site.source_files('.', recursive: true) updated_count = 0 # download unmatched files site_files.each do |file| sha = ::File.exist?(file.name) && Siteleaf::GitHash.file(file.name) if file.sha != sha print "Downloading #{file.name}..." FileUtils.mkdir_p(::File.dirname(file.name)) ::File.open(file.name, 'w') { |f| f.write(file.to_file) } updated_count += 1 print "complete.\n" end end # check for old files local_files = read_dir missing_files = [] local_files.each do |path| missing_files << path if !site_files.find{|a| a.name.casecmp(path) == 0 } end if missing_files.empty? puts "=> #{updated_count} file(s) downloaded.\n" else print "=> #{updated_count} file(s) downloaded. Delete the following #{missing_files.size} unmatched local file(s)?\n" missing_files.each do |path| puts path end print '(y/n)? ' if $stdin.gets.chomp.downcase == 'y' missing_files.each do |path| print "Deleting #{path}..." ::File.delete(path) print "complete.\n" end puts "=> #{missing_files.size} file(s) deleted.\n" end end end def push(site_id) print "Reading site...\n" # get all the things site = Siteleaf::Site.find(site_id) site_files = site.source_files('.', recursive: true) local_files = read_dir updated_count = 0 # find changed files changed_files = local_files.reject do |path| file = site_files.find{|a| a.name.casecmp(path) == 0 } file && Siteleaf::GitHash.file(path) == file.sha end changed_files.unshift('_config.yml') if changed_files.delete('_config.yml') # upload changed files changed_files.each do |path| print "Uploading #{path}..." response = Siteleaf::SourceFile.create(site_id: site_id, name: path, file: ::File.new(path)) updated_count += 1 print "complete.\n" if response.name && response.name != path local_files.push(response.name) puts "Warning: Remote filename has been normalized to `#{response.name}`." end end # check for old files missing_assets = [] site_files.each do |asset| missing_assets << asset if !local_files.find{|p| p.casecmp(asset.name) == 0 } end if missing_assets.empty? puts "=> #{updated_count} file(s) uploaded.\n" else print "=> #{updated_count} file(s) uploaded. Delete the following #{missing_assets.size} unmatched remote file(s)?\n" missing_assets.each do |asset| puts asset.name end print '(y/n)? ' if $stdin.gets.chomp == 'y' missing_assets.each do |asset| print "Deleting #{asset.name}..." asset.delete print "complete.\n" end puts "=> #{missing_assets.size} file(s) deleted.\n" end end end def import(file, quiet = true) job = Siteleaf::Site.import(file: ::File.new(file)) if quiet puts "=> Import queued.\n" else last_msg = nil job.stream do |s| if (msg = s["message"]) && (msg != last_msg) puts msg last_msg = msg end end puts "=> Import completed.\n" end end def publish(site_id, quiet = true) site = Siteleaf::Site.new(id: site_id) job = site.publish if quiet puts "=> Publish queued.\n" else last_msg = nil job.stream do |s| if (msg = s["message"]) && (msg != last_msg) puts msg last_msg = msg end end puts "=> Publish completed.\n" end end def preview(site_id, quiet = true) site = Siteleaf::Site.new(id: site_id) job = site.preview if quiet puts "=> Preview queued.\n" else last_msg = nil job.stream do |s| if (msg = s["message"]) && (msg != last_msg) puts msg last_msg = msg end end puts "=> Preview completed: https://#{site_id}.cloud-preview.siteleaf.com\n" end end def clean(path = '_uploads') searcher = if system('which ag', out: File::NULL) ['ag', '-lQ'] else puts "\nWarning: Silver Searcher not found, install for faster results (https://geoff.greer.fm/ag/)\n" ['grep', '-lr'] end puts "\nBuilding Jekyll site..." system 'bundle exec jekyll build --future --unpublished --drafts' puts "\nFinding unused files..." unused = Dir.glob("#{path}/**/*").select do |file| next unless File.file? file escaped = Jekyll::URL.escape_path file found = system(*searcher, escaped.sub(/^_/, '/'), '_site', out: File::NULL) || system(*searcher, file.sub(/^_/, '/'), '_site', out: File::NULL) if !found puts file true end end if unused.length == 0 puts "\nNo unused files found." else puts "=> Delete above #{unused.length} file(s)? (y/n)" if $stdin.gets.chomp.downcase == 'y' FileUtils.rm unused puts "=> #{unused.size} file(s) deleted." end end end def config(site) Siteleaf.save_settings({site_id: site.id}, '.siteleaf.yml') puts "=> Site configured." end def get_site_id # check env vars [:api_key, :api_secret, :api_base, :api_version].each do |key| if value = ENV['SITELEAF_'+key.to_s.upcase] Siteleaf.send "#{key}=", value end end ENV['SITELEAF_SITE_ID'] || if settings = Siteleaf.load_settings('.siteleaf.yml') settings[:site_id] end end def read_dir jekyll_site = Jekyll::Site.new(Jekyll.configuration(quiet: true)) ignore_paths = ['config.ru', '.*', '_site/*', 'Gemfile', 'Gemfile.lock'] ignore_paths += ::File.read('.siteleafignore').split(/\r?\n/) if ::File.exists?('.siteleafignore') ignore_paths += jekyll_site.exclude entry_filter = Jekyll::EntryFilter.new(jekyll_site) Dir.glob("**/*").reject do |path| ::File.directory?(path) || entry_filter.glob_include?(ignore_paths, path) end end begin case ARGV[0] when '-v', '--version', 'version' puts Siteleaf::VERSION when '-h', '--help', 'help' puts help when 'auth' auth true when 'c', 'config', 'setup' if auth != false if site = Siteleaf::Site.find_by_domain(ARGV[1]) config site else raise "No site found for `#{ARGV[1]}`, run `siteleaf new #{ARGV[1]}` to create it.\n" end end when 'n', 'new' if auth != false site = Siteleaf::Site.create(:title => ARGV[1], :domain => ARGV[1]) dir = ARGV.size >= 3 ? ARGV[2] : ARGV[1] Dir.mkdir(dir) unless ::File.directory?(dir) Dir.chdir(dir) config site end when 'pull' if ARGV.size == 1 site_id = get_site_id if auth != false if site_id pull(site_id) else raise "Site not configured, run `siteleaf config yoursite.com`.\n" end end else raise "`#{ARGV.join(' ')}` command not found.\n" end when 'push' if ARGV.size == 1 site_id = get_site_id if auth != false if site_id push(site_id) else raise "Site not configured, run `siteleaf config yoursite.com`.\n" end end else raise "`#{ARGV.join(' ')}` command not found.\n" end when 'publish' site_id = get_site_id if auth != false quiet = %w[-q --quiet].include?(ARGV[1]) && ARGV[1] if site_id publish(site_id, quiet) else raise "Site not configured, run `siteleaf config yoursite.com`.\n" end end when 'preview' site_id = get_site_id if auth != false quiet = %w[-q --quiet].include?(ARGV[1]) && ARGV[1] if site_id preview(site_id, quiet) else raise "Site not configured, run `siteleaf config yoursite.com`.\n" end end when 'import' site_id = get_site_id if auth != false file = ARGV[1] if File.extname(file) != '.zip' raise "Import file must be ZIP format.\n" elsif !File.exist?(file) raise "Import file not found.\n" else quiet = %w[-q --quiet].include?(ARGV[2]) && ARGV[2] import(file, quiet) end end when 'clean' ARGV[1] ? clean(ARGV[1]) : clean else puts "Error: `#{ARGV[0]}` command not found.\n" puts help exit(1) end rescue Exception => e puts "Error: #{e.message}\n" exit(1) end