#!/bin/bash COLOR_NC='\033[0m' # No Color COLOR_GRAY='\033[1;30m' COLOR_RED='\033[0;31m' COLOR_LCYAN='\033[1;36m' COLOR_YELLOW='\033[1;33m' COLOR_LGREEN='\033[1;32m' logv() { ([[ ! -z "$VERBOSE" ]] && >&2 echo -e "${COLOR_GRAY}$@${COLOR_NC}") || true } logerr() { >&2 echo -e "${COLOR_RED}$@${COLOR_NC}" } ## *** Argument Parsing & validation *** usage() { echo "$0 [opts] Commands: migrate [dir|file] runs pending migration files in the given directory * [dir|file] optional - Default: db/migrate setup [file] initializes a space with bare-minimum schema and seeds * [file] optional - default: db/contentful-schema.json backup [file] downloads a backup of the current space to the given file * [file] optional - default: timestamped file in current directory clean [no-init] Deletes all data in a given space and optionally sets it up again using 'bin/contentful setup'. * [no-init] optional - Skips the 'setup' step at the end. restore [file] restores a given backup file into the current space * [file] optional - default: the most recent backup file in the current directory restore_from -s [to space ID] restores all data from the given space into the current space * required - the ID of the space to receive data from * -s [to space ID] optional - the current working space. Default: \$CONTENTFUL_SPACE_ID generate [name] Creates a sample migration in the db/migrate directory * [name] optional - default: 'contentful_migration' Flags:" && \ grep " .)\ #" $0 echo " Examples:" && \ grep -i "#\ example:" $0 | awk '{$1=""; $2=""; print " "$0}' } parse_args() { OPTIND=1 local s=$(echo "$1" | tr '[:upper:]' '[:lower:]') case "$s" in migrate|setup|backup|export|restore|restore_from|import|generate|clean|help|h|\?) export subcommand=$s OPTIND=2 ;; esac # Parse flags while getopts ":hyvs:a:" arg; do case $arg in y) # Yes - skip prompts export YES="-y" ;; s) # Contentful Space ID - overrides env var CONTENTFUL_SPACE_ID export CONTENTFUL_SPACE_ID=$OPTARG ;; a) # Contentful Mgmt Token - overrides env var CONTENTFUL_MANAGEMENT_TOKEN export CONTENTFUL_MANAGEMENT_TOKEN=$OPTARG ;; v) # Verbose mode - extra output export VERBOSE=true ;; h) # Display help. usage exit 0 ;; *) logerr "Unknown option: '$OPTARG'" usage exit -1 ;; esac done export OPTIND } parse_args $@ && shift $(($OPTIND - 1)) # If they put args before the command like 'bin/contentful -s 1xab migrate -y', try parsing again [[ -z "$subcommand" ]] && parse_args $@ && shift $(($OPTIND - 1)) require_environment() { [[ -z "$CONTENTFUL_SPACE_ID" ]] && logerr "Please set CONTENTFUL_SPACE_ID environment variable or use '-s' flag." && exit -1; [[ -z "$CONTENTFUL_MANAGEMENT_TOKEN" ]] && logerr "Please set CONTENTFUL_MANAGEMENT_TOKEN environment variable or use '-a' flag." && exit -1; if [[ ! -f node_modules/.bin/contentful-migration ]]; then command -v npm >/dev/null 2>&1 || (logerr "I require 'npm' but it's not installed. Please install nodejs."; exit -1) logv "npm install" npm install [[ -f node_modules/.bin/contentful-migration ]] || (logerr "Failed installing node modules - please ensure contentful CLI is installed"; exit -1) fi } ## *** Utility functions *** confirm() { [[ -z "$2" ]] && [[ ! -z "$YES" ]] && logv "$1 (y/n): confirmed by -y flag" && return 0; while true; do if [[ -z "$2" ]]; then read -p $'\033[1;36m'"$1"' (y/n): '$'\033[0m' yn else # double confirm - extra dangerous. read -p $'\033[0;31m'"$1"' (y/n): '$'\033[0m' yn fi case $yn in [Yy]* ) return 0;; [Nn]* ) return 1;; * ) echo "Please answer yes or no.";; esac done } get_space_name() { logv "curl -s https://api.contentful.com/spaces/$1?access_token=****" curl -s https://api.contentful.com/spaces/$1?access_token=$CONTENTFUL_MANAGEMENT_TOKEN | jq -r .name | tr '[:upper:]' '[:lower:]' } set -e # *** Commands *** # Example: bin/contentful migrate -y -s 1xab -a $MY_TOKEN db/migrate/20180101120000_add_content_type_dog.ts # equivalent to: bin/rake db:migrate migrate() { ARG="$1" [[ -z "$ARG" ]] && ARG="db/migrate" [[ -d "$ARG" ]] && ARG="batch $ARG" require_environment logv "contentful-migration -s $CONTENTFUL_SPACE_ID -a ***** $YES -p $ARG" node_modules/.bin/ts-node node_modules/.bin/contentful-migration \ -s $CONTENTFUL_SPACE_ID -a $CONTENTFUL_MANAGEMENT_TOKEN \ $YES -p $ARG mkdir -p db logv "contentful-export -s --export-dir db --content-file contentful-schema.json --space-id $CONTENTFUL_SPACE_ID --management-token ***** --query-entries 'content_type=migrationHistory' --query-assets 'sys.id=false'" node_modules/.bin/contentful-export --export-dir db --content-file contentful-schema.json \ --space-id $CONTENTFUL_SPACE_ID --management-token $CONTENTFUL_MANAGEMENT_TOKEN \ --query-entries 'content_type=migrationHistory' \ --query-assets 'sys.id=false' if [[ $(git diff-index --name-only HEAD | grep 'db/contentful-schema.json') == "" ]]; then echo -e "${COLOR_LGREEN}✓ Schema in contentful space is equivalent to stored schema${COLOR_NC}" else echo -e "${COLOR_YELLOW}⚠️ Schema changed after running migrations${COLOR_NC}" fi } # Example: bin/contentful backup -s 1xab -a $MY_TOKEN 2018_01_01.1xab.dump.json # equivalent to: bin/rake db:dump[2018_01_01.dump] backup() { FILE="$1" [[ ! -z "$FILE" ]] && FILE="--content-file $FILE" && shift require_environment logv "contentful-export $FILE --space-id $CONTENTFUL_SPACE_ID --management-token ***** $@" node_modules/.bin/contentful-export $FILE \ --space-id $CONTENTFUL_SPACE_ID --management-token $CONTENTFUL_MANAGEMENT_TOKEN \ $@ } # Example: bin/contentful restore -y -s 1xab -a $MY_TOKEN 2018_01_01.1xab.dump.json # equivalent to: bin/rake db:restore[2018_01_01.dump] restore() { FILE="$1" if [[ -z "$FILE" ]]; then FILE=$(ls contentful-export-$CONTENTFUL_SPACE_ID-* | sort -r | head -n 1) [[ -z "$FILE" ]] && logerr "No file given on command line" && exit -1 fi confirm "Import $FILE into $CONTENTFUL_SPACE_ID?" || exit -1 require_environment logv "contentful-import --space-id $CONTENTFUL_SPACE_ID --management-token ***** --content-file $FILE" node_modules/.bin/contentful-import \ --space-id $CONTENTFUL_SPACE_ID --management-token $CONTENTFUL_MANAGEMENT_TOKEN \ --content-file $FILE } # Example: bin/contentful setup -y -s 1xab -a $MY_TOKEN db/my-schema.json # equivalent to: bin/rake db:setup setup() { FILE="$1" [[ -z "$FILE" ]] && FILE=db/contentful-schema.json confirm "Initialize space $CONTENTFUL_SPACE_ID from seed file $FILE?" || exit -1 require_environment logv "contentful-import --space-id $CONTENTFUL_SPACE_ID --management-token ***** --content-file $FILE" node_modules/.bin/contentful-import \ --space-id $CONTENTFUL_SPACE_ID --management-token $CONTENTFUL_MANAGEMENT_TOKEN \ --content-file $FILE migrate } # Example: bin/contentful clean -s 1xab -a $MY_TOKEN clean() { command -v jq >/dev/null 2>&1 || (logerr "I require 'jq' but it's not installed. Please run 'brew install jq'"; exit -1) require_environment name=$(get_space_name $CONTENTFUL_SPACE_ID) confirm "This will delete all data and content types from $name. Are you sure?" || exit -1 [[ "$name" != *staging ]] && [[ "$name" != *test ]] && confirm "$name is not a staging or test environment! Are you really sure?" dangerous! local bkup_file="contentful-export-$CONTENTFUL_SPACE_ID-`date +"%Y-%m-%dT%H-%M-%S"`.json" backup $bkup_file content_types=$(cat $bkup_file | jq -r '.contentTypes[].sys.id') entries=$(cat $bkup_file | jq -r '.entries[].sys.id') assets=$(cat $bkup_file | jq -r '.assets[].sys.id') delete() { logv "curl -XDELETE https://api.contentful.com/spaces/$CONTENTFUL_SPACE_ID/$1/$2/published\?access_token\=*****" curl -s --fail -XDELETE https://api.contentful.com/spaces/$CONTENTFUL_SPACE_ID/$1/$2/published\?access_token\=$CONTENTFUL_MANAGEMENT_TOKEN > /dev/null logv "curl -XDELETE https://api.contentful.com/spaces/$CONTENTFUL_SPACE_ID/$1/$2\?access_token\=*****" curl -s --fail -XDELETE https://api.contentful.com/spaces/$CONTENTFUL_SPACE_ID/$1/$2\?access_token\=$CONTENTFUL_MANAGEMENT_TOKEN > /dev/null } [[ ! -z "$assets" ]] && while read -r id; do delete 'assets' $id; done <<< "${assets}" [[ ! -z "$entries" ]] && while read -r id; do delete 'entries' $id; done <<< "${entries}" [[ ! -z "$content_types" ]] && while read -r id; do delete 'content_types' $id; done <<< "${content_types}" echo -e "${COLOR_LGREEN}All content deleted - prior content saved at${COLOR_NC} $bkup_file" [[ "$1" == "no-init" ]] || (setup || logerr "Error setting up space $name!") } # Example: bin/contentful restore_from -s $staging_space_id -a $MY_TOKEN $production_space_id restore_from() { command -v jq >/dev/null 2>&1 || (logerr "I require 'jq' but it's not installed. Please run 'brew install jq'"; exit -1) require_environment from_space_id=$1 [[ -z "$from_space_id" ]] && logerr "Please provide the space ID from which to restore data" && exit -1 to_space_id=$CONTENTFUL_SPACE_ID from_name=$(get_space_name $from_space_id) to_name=$(get_space_name $CONTENTFUL_SPACE_ID) echo -e "${COLOR_LCYAN}This will backup all data from $from_name, delete all data in $to_name, and then write the $from_name data over it.${COLOR_NC}" confirm "Continue?" || exit -1 export YES='-y' # don't keep bugging the user export CONTENTFUL_SPACE_ID=$from_space_id local bkup_file="contentful-export-$from_space_id-`date +"%Y-%m-%dT%H-%M-%S"`.json" logv "bin/contentful backup -s $CONTENTFUL_SPACE_ID $bkup_file" backup $bkup_file --skip-roles --skip-webhooks export CONTENTFUL_SPACE_ID=$to_space_id logv "bin/contentful clean -s $CONTENTFUL_SPACE_ID" clean no-init logv "bin/contentful restore -s $CONTENTFUL_SPACE_ID $bkup_file" restore $bkup_file } # Example: bin/contentful generate add content type dog # equivalent to: bin/rails generate migration add_content_type_dog generate() { migration=" import Migration from 'contentful-migration-cli' export = function (migration: Migration) { const dog = migration.createContentType('dog', { name: 'Dog' }) const name = dog.createField('name') name.name('Name') .type('Symbol') .required(true) } " timestamp=$(date +%Y%m%d%H%M%S) filename="$@" [[ -z "$filename" ]] && filename="contentful_migration" filename=${filename// /\_} filename="db/migrate/${timestamp}_${filename}.ts" echo "$migration" > $filename echo "generated file $filename" } case $subcommand in migrate) migrate $@ ;; backup|export) backup $@ ;; restore|import) restore $@ ;; setup) setup $@ ;; generate) generate $@ ;; clean) clean $@ ;; restore_from) restore_from $@ ;; help|h|\?) usage ;; *) logerr "Unknown command: '$1'" usage exit -1 ;; esac