diff --git a/CHANGELOG.md b/CHANGELOG.md index 908bac2..5d5fd7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## 0.2.1 - 2019-08-14 + +- Adds a `push` command (See #2) + ## 0.2.0 - 2019-08-12 - Adds export command (See [#1013](https://github.com/outline/outline/pull/1013) for corresponding Outline PR) diff --git a/Dockerfile b/Dockerfile index 731648a..36d4483 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,9 @@ WORKDIR /outliner COPY . /outliner/ RUN gem install bundler && \ - bundle install + bundle install && \ + apk add --no-cache git openssh-client && \ + echo -e "StrictHostKeyChecking no" >> /etc/ssh/ssh_config && \ + mkdir /root/.ssh ENTRYPOINT ["/outliner/entrypoint.sh"] diff --git a/README.md b/README.md index fe117b6..f72a1bc 100644 --- a/README.md +++ b/README.md @@ -81,10 +81,27 @@ docker run --env OUTLINE_BASE_URI="https://kb.example.com" \ import "/data" "Archive" ``` +### Push + +Note: Push is currently only available as a Docker Command + +```bash +docker run --env OUTLINE_BASE_URI="https://kb.example.com" \ + --env OUTLINE_TOKEN="PUT YOUR TOKEN HERE" \ + --env OUTLINE_TOKEN="PUT YOUR TOKEN HERE" \ + --env GIT_BRANCH=outline \ + --env GIT_REMOTE_URL=git@example.com:org/outline.backup.git + --volume /etc/ssh/private.key:/root/.ssh/id_rsa + captn3m0/outliner \ + push +``` + #### Limitations - Images are currently not imported. Host them externally for this to work. - Only `.md` files are currently supported +- `push` doesn't sync file-history, but is meant to push a one-time backup to Git. +- `StrictHostKeyChecking` is currently disabled for `push`, please only run this in trusted networks. ## Development diff --git a/entrypoint.sh b/entrypoint.sh index 446edb2..4ac69cb 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,19 +1,63 @@ #!/bin/sh - +set -eu if [ $# -eq 0 ]; then - echo "Please run with outliner [export|import] arguments" + echo "Please run with outliner [export|import|sync] arguments" exit fi +setup_git() { + if [ -f "$HOME/.ssh/id_rsa" ]; then + # This is required because Kubernetes secret mounts can't + # have file permissions set + chmod 0400 "$HOME/.ssh/id_rsa" + + if [ ! -d "$HOME/.ssh/id_rsa.pub" ]; then + ssh-keygen -y -f "$HOME/.ssh/id_rsa" > "$HOME/.ssh/id_rsa.pub" + fi + echo "[+] Using SSH key for git pushes" + else + echo "[E] Git credentials not available, quitting" + exit 1 + fi + + eval $(ssh-agent) + ssh-add "$HOME/.ssh/id_rsa" +} + +update_git_config() { + EMAIL=${GIT_EMAIL:-outliner@example.invalid} + git config --global user.email "$EMAIL" + git config --global user.name "Outliner Backup" + git remote add origin "$GIT_REMOTE_URL" +} + case $1 in export) shift - bundle exec outliner-export $@ + bundle exec outliner-export "$@" ;; import) shift - bundle exec outliner-import $@ - break + bundle exec outliner-import "$@" + ;; + sync) + tmp_dir=$(mktemp -d) + if [ -z "$GIT_REMOTE_URL" ]; then + echo "[E] GIT_REMOTE_URL not set" + exit 1 + else + setup_git + bundle exec outliner-export "$tmp_dir" + cd "$tmp_dir" + git init + update_git_config + git add . + git commit --message "Backup: $(date)" > /dev/null + BRANCH=${GIT_BRANCH:-master} + git checkout -b "$BRANCH" + git status + git push origin --force "HEAD:$BRANCH" + fi ;; *) echo "Invalid command, please check README" diff --git a/exe/outliner-export b/exe/outliner-export index cdcac3a..f12f103 100755 --- a/exe/outliner-export +++ b/exe/outliner-export @@ -1,16 +1,15 @@ #!/usr/bin/env ruby +# frozen_string_literal: true -require "bundler/setup" -require "outliner" +require 'bundler/setup' +require 'outliner' require 'tempfile' def validate - unless (ARGV.size == 1) and Dir.exists?(ARGV[0]) and ENV.key?('OUTLINE_BASE_URI') and ENV.key?('OUTLINE_TOKEN') - puts "[E] Please call as `outliner-export directory`" - puts "[E] Please export OUTLINE_BASE_URI and OUTLINE_TOKEN environment variables" - puts "[E] OUTLINE_BASE_URI should not include /api" - exit 1 - end + raise 'Missing arguments' if ARGV.size != 1 + raise 'Invalid directory' unless Dir.exist?(ARGV[0]) + raise 'OUTLINE_BASE_URI not set' unless ENV.key?('OUTLINE_BASE_URI') + raise 'OUTLINE_TOKEN not set' unless ENV.key?('OUTLINE_TOKEN') end # Run validations @@ -25,7 +24,7 @@ response = CLIENT.collections_exportAll(download: true) # Extract it to a tempfle file = Tempfile.new('download.zip') -File.open(file.path, 'w') { |file| file.write(response.body) } +File.open(file.path, 'w') { |f| f.write(response.body) } `unzip -o "#{file.path}" -d "#{local_directory}"` diff --git a/exe/outliner-import b/exe/outliner-import index 6a1222f..b2941a7 100755 --- a/exe/outliner-import +++ b/exe/outliner-import @@ -1,25 +1,28 @@ #!/usr/bin/env ruby +# frozen_string_literal: true -require "bundler/setup" -require "outliner" +require 'bundler/setup' +require 'outliner' def validate - unless (ARGV.size == 2) and Dir.exists?(ARGV[0]) and ARGV[1].match(/\w+/) and ENV.key?('OUTLINE_BASE_URI') and ENV.key?('OUTLINE_TOKEN') - puts "[E] Please call as `outliner-import local_directory remote_collection_name`" - puts "[E] Please export OUTLINE_BASE_URI and OUTLINE_TOKEN environment variables" - puts "[E] OUTLINE_BASE_URI should not include /api" - exit 1 - end + raise 'Missing arguments' if ARGV.size != 2 + raise 'Invalid directory' unless Dir.exist?(ARGV[0]) + raise 'Invalid collection' unless ARGV[1].match(/\w+/) + raise 'OUTLINE_BASE_URI not set' unless ENV.key?('OUTLINE_BASE_URI') + raise 'OUTLINE_TOKEN not set' unless ENV.key?('OUTLINE_TOKEN') end -def create_documents_recursively(directory, collection_id, parent_document_id=nil) +def create_documents_recursively(directory, collection_id, parent_document_id = nil) cwd = Dir.pwd Dir.chdir directory # Create all documents for this directory - Dir["*.md"].each do |file| + Dir['*.md'].each do |file| params = { title: file[0...-3], - text: file[0...-3] + "\n" + File.read(file) + "\n\n---\nImported at #{Time.now}", + text: file[0...-3] + + "\n" + + File.read(file) + + "\n\n---\nImported at #{Time.now}", collectionId: collection_id, publish: true } @@ -30,11 +33,11 @@ def create_documents_recursively(directory, collection_id, parent_document_id=ni end # Create child documents for each sub-directory - Dir.glob('*').select {|f| File.directory? f}.each do |dir| + Dir.glob('*').select { |f| File.directory? f }.each do |dir| puts "[-] #{dir}" params = { title: dir, - text: dir +"\nImported at #{Time.now}", + text: dir + "\nImported at #{Time.now}", collectionId: collection_id, publish: true, parentDocumentId: parent_document_id @@ -58,12 +61,11 @@ root_collection_id = CLIENT.find_or_create_collection(remote_collection_name) begin create_documents_recursively(local_directory, root_collection_id) - puts "[S] Import successful" -rescue Exception => e + puts '[S] Import successful' +rescue StandardError? => e # If we fail, print an error, and delete the collection puts "[E] Import failed with error: #{e.message}" CLIENT.collections_delete(id: root_collection_id) - puts "[E] Deleted collection, please report the issue or retry" + puts '[E] Deleted collection, please report the issue or retry' exit 1 end -