How to backup and restore Restyaboard boards

Restyaboard does not provide an easy way to backup and restore created boards at this moment, but this is easy to overcome by using extensive API provided by the application.

Prerequisites

Install curl utility to perform API calls.

$ sudo apt-get install curl

Install jq utility to parse JSON files.

$ sudo apt-get install jq

Download Restyaboard boards

This is the simplest usage scenario. It does use curl and jq to perform basic JSON API calls to get all or only selected user boards.

Shell script

#!/bin/bash
# Download Restyaboard boards
# https://blog.sleeplessbeastie.eu/2018/04/02/how-to-backup-and-restore-restyaboard-boards/

# current date and time
current_datetime="$(date +%Y%m%d_%H%M%S)"

# usage info
usage(){
  echo "Usage:"
  echo "  $0 -r restyaboard_url -u username -p passsword [-b board_id|-t]"
  echo ""
  echo "Parameters:"
  echo "  -r restyaboard_url : set Restyaboard URL (required)"
  echo "  -u username        : set username (required)"
  echo "  -p password        : set password (required)"
  echo "  -b board_id        : set board id (optional)"
  echo "  -t                 : add timestamp to output filename (optional)"
  echo ""
}

# parse parameters
while getopts "r:u:p:b:t" option; do
  case $option in
    "r")
      param_board_address="${OPTARG}"
      param_board_address_defined=true
      ;;
    "u")
      param_username="${OPTARG}"
      param_username_defined=true
      ;;
    "p")
      param_password="${OPTARG}"
      param_password_defined=true
      ;;
    "b")
      param_board_id="${OPTARG}"
      param_board_id_defined=true
      ;;
    "t")
      param_date_prefix=true
      ;;
    \?|:|*)
      usage
      exit
      ;;
  esac
done

if [ "${param_board_address_defined}" = true ] && \
   [ "${param_username_defined}"      = true ] && \
   [ "${param_password_defined}"      = true ]; then

  # get access token
  access_token=$(curl --silent -X GET --header "Accept: application/json" "${param_board_address}/api/v1/oauth.json" | jq -r .access_token)
  if [ "${access_token}" == "" ]; then echo "Error connectiong to ${board_address}"; exit 1; fi

  # log in and use assign new access token
  access_token=$(curl --silent -X POST --header "Content-Type: application/json" --header "Accept: application/json" \
                      -d "{ \"email\": \"${param_username}\", \"password\": \"${param_password}\" }" \
                      "${param_board_address}/api/v1/users/login.json?token=${access_token}" | \
                 jq -r .access_token)
  if [ "${access_token}" == "" ]; then echo "Incorrect credentials"; exit 2; fi

  # define board IDs to dowload
  if [ "${param_board_id_defined}" = true ]; then
    board_ids="${param_board_id}"
  else
    board_ids=$(curl --silent -X GET --header "Accept: application/json" "${param_board_address}/api/v1/users/2/boards.json?token=${access_token}" | jq -r .user_boards[].board_id)
  fi
  if [ "${board_ids}" == "" ]; then echo "Board list is empty"; exit 4; fi

  # download boards
  for board_id in ${board_ids}; do
    if [ "${param_date_prefix}" = true ]; then
      filename="${board_id}-${current_datetime}.json"
    else
      filename="${board_id}.json"
    fi
    result=$(curl --silent --output - -X GET --header "Accept: application/json" "${param_board_address}/api/v1/boards/${board_id}.json?token=${access_token}" | jq -r 'select(.error != null) | .error | ("type \"\(.type)\" message \"\(.message)\"")')
    if [ -n "${result}" ]; then
      echo "There was an error ${result} when downloading board ID ${board_id}. Skipping."
    else
      curl --silent --output ${filename} -X GET --header "Accept: application/json" "${param_board_address}/api/v1/boards/${board_id}.json?token=${access_token}"
      board_name=$(jq '.name' ${filename})
      echo "Downloaded board ID ${board_id} with name ${board_name} to file \"${filename}\""
    fi
  done
else
  usage
fi

Usage

Download single board.

$ restyaboard_get.sh -r https://board.example.org -u milosz -p password -b 3 
Downloaded board ID 3 with name "Ideas" to file "3.json"

Download single boards and append timestamp to filename.

$ restyaboard_get.sh -r https://board.example.org -u milosz -p password -b 3 -t
Downloaded board ID 3 with name "Ideas" to file "3-20180218_165158.json"

Download selected boards and append timestamp to filename.

$ restyaboard_get.sh -r https://board.example.org -u milosz -p password -b "3 40" -t
Downloaded board ID 3 with name "Ideas" to file "3-20180218_165224.json"
Downloaded board ID 40 with name "Work" to file "40-20180218_165224.json"

Download all user boards and append timestamp to filename.

$ restyaboard_get.sh -r https://board.example.org -u milosz -p pasword -t
Downloaded board ID 16 with name "Shell scripts" to file "16-20180218_165242.json"
Downloaded board ID 3 with name "Ideas" to file "3-20180218_165242.json"
Downloaded board ID 26 with name "Netflix" to file "26-20180218_165242.json"
Downloaded board ID 40 with name "Work" to file "22-20180218_165242.json"
Downloaded board ID 9 with name "Blog tasks" to file "9-20180218_165242.json"
[...]

Upload Restyaboard boards

This is more complicated as there are many objects like boards, lists, cards, labels, checklists, checklist_items and many more. The following shell script will restore these objects except comments, attachments and some other minor things that can be easily implemented later if necessary.

Shell scriupt

#!/bin/bash
# Upload and restore Restyaboard board
# https://blog.sleeplessbeastie.eu/2018/04/02/how-to-backup-and-restore-restyaboard-boards/

# usage info
usage(){
  echo "Usage:"
  echo "  $0 -r restyaboard_url -u username -p passsword -f json_board_file"
  echo ""
  echo "Parameters:"
  echo "  -r restyaboard_url : set Restyaboard URL (required)"
  echo "  -u username        : set username (required)"
  echo "  -p password        : set password (required)"
  echo "  -f JSON file       : exported JSON board (optional)"
  echo ""
}

# parse parameters
while getopts "r:u:p:f:" option; do
  case $option in
    "r")
      param_board_address="${OPTARG}"
      param_board_address_defined=true
      ;;
    "u")
      param_username="${OPTARG}"
      param_username_defined=true
      ;;
    "p")
      param_password="${OPTARG}"
      param_password_defined=true
      ;;
    "f")
      param_file="${OPTARG}"
      param_file_defined=true
      ;;
    \?|:|*)
      usage
      exit
      ;;
  esac
done

if [ "${param_board_address_defined}" = true ] && \
   [ "${param_username_defined}"      = true ] && \
   [ "${param_password_defined}"      = true ] && \
   [ "${param_file_defined}"          = true ] && \
   [ -f "${param_file}"                      ]; then

  # get access token
  access_token=$(curl --silent -X GET --header "Accept: application/json" "${param_board_address}/api/v1/oauth.json" | \
                 jq -r .access_token)
  if [ -z "${access_token}" ]; then echo "Error connectiong to ${board_address}"; exit 1; fi

  # log in and use assign new access token
  access_token=$(curl --silent -X POST --header "Content-Type: application/json" --header "Accept: application/json" \
                      -d "{ \"email\": \"${param_username}\", \"password\": \"${param_password}\" }" \
                      "${param_board_address}/api/v1/users/login.json?token=${access_token}" | \
                 jq -r .access_token)
  if [ -z "${access_token}" ]; then echo "Incorrect credentials"; exit 2; fi

  # get user ID
  user_id=$(curl --silent -X GET --header "Accept: application/json" "${param_board_address}/api/v1/users/me.json?token=${access_token}" | \
            jq -r .id)
  if [ -z "${user_id}" ]; then echo "Cannot get user id"; exit 3; fi

  # read basic board paramters: id, name and background color
  # board_visibility will be deliberately set to 0
  board_name=$(jq -r .name ${param_file})
  board_id=$(jq -r .id ${param_file})
  board_background_color=$(jq -r 'select(.background_color != null) | .background_color' ${param_file})
  if [ -z "${board_name}" ] || [ -z "${board_id}" ]; then echo "Board name or id is not defined"; exit 4; fi

  new_board_id=$(curl --silent \
                       -X POST \
                      --header 'Content-Type: application/json' \
                      --header 'Accept: application/json' \
                       -d '{
                             "board_visibility": 0,
                             "name": "'"${board_name}"'",
                             "user_id": '${user_id}',
                             "background_color": "'${board_background_color}'"
                           }' \
                      "${param_board_address}/api/v1/boards.json?token=${access_token}" | \
                 jq -r '.id')

  echo "Created board \"${board_name}\" - ${param_board_address}/#/board/${new_board_id}"

  # parse and create lists
  for list_id in $(jq -r .lists[].id ${param_file}); do
    # read basic list parameters: id, name, is_archived, position, color
    list_name=$(jq -r '.lists[] | select(.id == '${list_id}') | .name' ${param_file})
    list_is_archived=$(jq -r '.lists[] | select(.id == '${list_id}') | .is_archived' ${param_file})
    list_position=$(jq -r '.lists[] | select(.id == '${list_id}') | .position' ${param_file})
    list_color=$(jq -r '.lists[] | select(.id == '${list_id}') | select(.color != null) | .color' ${param_file})
    list_cards=$(jq -r '.lists[] | select(.id == '${list_id}') | select(.cards != null) | .cards[].id' ${param_file})

    new_list_id=$(curl --silent \
                        -X POST \
                       --header 'Content-Type: application/json' \
                       --header 'Accept: application/json' \
                        -d '{
                              "board_id": '${new_board_id}',
                              "name": "'"${list_name}"'",
                              "is_archived": '${list_is_archived}',
                              "position": '${list_position}',
                              "color": "'${list_color}'"
                            }' \
                       "${param_board_address}/api/v1/boards/${new_board_id}/lists.json?token=${access_token}" | \
                  jq -r '.id')

    echo " + Created list \"${list_name}\""

    # parse list cards
    if [ -n "${list_cards}" ]; then # 
      for card_id in ${list_cards}; do
        # read basic card parameters: id, name, description, position, color, is_archived, notification_due_date
        card_name=$(jq -r '.lists[] | select(.id == '${list_id}') | .cards[] | select(.id == '${card_id}') | .name' ${param_file})
        card_description=$(jq '.lists[] | select(.id == '${list_id}') | .cards[] | select(.id == '${card_id}') | select(.description != null) | .description' ${param_file})
        card_position=$(jq -r '.lists[] | select(.id == '${list_id}') | .cards[] | select(.id == '${card_id}') | .position ' ${param_file})
        card_color=$(jq -r '.lists[] | select(.id == '${list_id}') | .cards[] | select(.id == '${card_id}') | .color ' ${param_file})
        card_is_archived=$(jq -r '.lists[] | select(.id == '${list_id}') | .cards[] | select(.id == '${card_id}') | .is_archived ' ${param_file})
        card_notification_due_date=$(jq -r '.lists[] | select(.id == '${list_id}') | .cards[] | select(.id == '${card_id}') | select(.notification_due_date != null) |  .notification_due_date ' ${param_file})

        # fix empty description
        if [ -z "${card_description}" ]; then
          card_description='""'
        fi
      
        new_card_id=$(curl --silent -X POST \
                           --header 'Content-Type: application/json' \
                           --header 'Accept: application/json' \
                            -d '{
                                  "board_id": '${new_board_id}',
                                  "list_id": '${new_list_id}',
                                  "name": "'"${card_name}"'",
                                  "position": '${card_position}',
                                  "description": '"${card_description}"',
                                  "due_date": "'"${card_notification_due_date}"'",
                                  "color": "'"${card_color}"'",
                                  "is_archived": "'"${card_is_archived}"'"
                                }' \
                           "${param_board_address}/api/v1/boards/${new_board_id}/lists/${new_list_id}/cards.json?token=${access_token}" | \
                      jq -r '.id')

        echo "   - Added card \"${card_name}\"" 

        # parse labels
        # each label is a single word
        card_labels=$(jq -r '.lists[] | select(.id == '${list_id}') | .cards[] | select(.id == '${card_id}') | select(.cards_labels != null) | .cards_labels[].label_id' ${param_file})
        card_label_names="" # initialize empty variable as will send all labels in one api call
        if [ -n "${card_labels}" ]; then
          for card_label_id in $card_labels; do
            # read basic label parameters: id, name
            card_label_name=$(jq -r '.lists[] | select(.id == '${list_id}') | .cards[] | select(.id == '${card_id}') | .cards_labels[] | select(.label_id == '${card_label_id}') | .name' ${param_file})

            # join labels
            if [ -z "${card_label_names}" ]; then
              card_label_names=$(echo "${card_label_name}")
            else
              card_label_names=$(echo "${card_label_names},${card_label_name}")
            fi
          done

          new_card_label_id=$(curl --silent \
                                    -X POST \
                                   --header 'Content-Type: application/json' \
                                   --header 'Accept: application/json' \
                                    -d '{
                                          "board_id": '${new_board_id}',
                                          "list_id": '${new_list_id}',
                                          "card_id": '${new_card_id}',
                                          "name": "'"${card_label_names}"'"
                                        }' \
                                   "${param_board_address}/api/v1/boards/${new_board_id}/lists/${new_list_id}/cards/${new_card_id}/labels.json?token=${access_token}" | \
                              jq -r '.id')

          echo "     @${card_label_names}" | sed "s/,/, /g"
        fi

        # parse checklists
        card_checklists=$(jq -r '.lists[] | select(.id == '${list_id}') | .cards[] | select(.id == '${card_id}') | select(.cards_checklists != null) | .cards_checklists[].id' ${param_file})
        if [ -n "${card_checklists}" ]; then
          for card_checklist_id in $card_checklists; do
            # read basic checklist parameters: id, name, position
            card_checklist_name=$(jq -r '.lists[] | select(.id == '${list_id}') | .cards[] | select(.id == '${card_id}') | .cards_checklists[] | select(.id == '${card_checklist_id}') | .name' ${param_file})
            card_checklist_position=$(jq -r '.lists[] | select(.id == '${list_id}') | .cards[] | select(.id == '${card_id}') | .cards_checklists[] | select(.id == '${card_checklist_id}') | .position' ${param_file})

            new_card_checklist_id=$(curl --silent \
                                          -X POST \
                                         --header 'Content-Type: application/json' \
                                         --header 'Accept: application/json' \
                                          -d '{
                                                "board_id": '${new_board_id}',
                                                "list_id": '${new_list_id}',
                                                "card_id": '${new_card_id}',
                                                "position": '${card_checklist_position}',
                                                "name": "'"${card_checklist_name}"'"
                                              }' \
                                         "${param_board_address}/api/v1/boards/${new_board_id}/lists/${new_list_id}/cards/${new_card_id}/checklists.json?token=${access_token}"  | \
                                    jq -r '.id')

            echo "      + Checklist $card_checklist_name"

            # parse checklist item
            card_checklist_items=$(jq -r '.lists[] | select(.id == '${list_id}') | .cards[] | select(.id == '${card_id}') | .cards_checklists[] | select(.id == '${card_checklist_id}') | select(.checklists_items != null) | .checklists_items[].id' ${param_file})
            if [ -n "${card_checklist_items}" ]; then
              for card_checklist_item_id in ${card_checklist_items}; do
                # read basic checklist item parameters: id, name, is_completed, position
                card_checklist_item_name=$(jq -r '.lists[] | select(.id == '${list_id}') | .cards[] | select(.id == '${card_id}') | .cards_checklists[] | select(.id == '${card_checklist_id}') | .checklists_items[] | select(.id == '${card_checklist_item_id}') | .name' ${param_file})
                card_checklist_item_is_completed=$(jq -r '.lists[] | select(.id == '${list_id}') | .cards[] | select(.id == '${card_id}') | .cards_checklists[] | select(.id == '${card_checklist_id}') | .checklists_items[] | select(.id == '${card_checklist_item_id}') | .is_completed' ${param_file})
                card_checklist_item_position=$(jq -r '.lists[] | select(.id == '${list_id}') | .cards[] | select(.id == '${card_id}') | .cards_checklists[] | select(.id == '${card_checklist_id}') | .checklists_items[] | select(.id == '${card_checklist_item_id}') | .position' ${param_file})

                new_card_checklist_item_id=$(curl --silent \
                                                   -X POST \
                                                  --header 'Content-Type: application/json' \
                                                  --header 'Accept: application/json' \
                                                   -d '{
                                                         "board_id": '${new_board_id}',
                                                         "list_id": '${new_list_id}',
                                                         "card_id": '${new_card_id}',
                                                         "checklist_id": '${new_card_checklist_id}',
                                                         "is_completed": '${card_checklist_item_is_completed}',
                                                         "position": '${card_checklist_item_position}',
                                                         "name": "'"${card_checklist_item_name}"'"
                                                       }' \
                                                  "${param_board_address}/api/v1/boards/${new_board_id}/lists/${new_list_id}/cards/${new_card_id}/checklists/${new_card_checklist_id}/items.json?token=${access_token}"  | \
                                              jq -r '.id')

                echo "        - $card_checklist_item_name"
              done # checklist items
            fi # checklist items
          done # checklists
        fi # checklists
      done # cards
    fi # cards
  done # lists
else
  usage
fi

Usage

Restore sample board.

$ bash ./restyaboard_put.sh -r https://board.example.org -u milosz -p password -f 84-20180218_165600.json 
Created board "Restyaboard installation" - https://board.example.org/#/board/91
 + Created list "Required utilities"
   - Added card "unzip"
   - Added card "wget"
   - Added card "git"
 + Created list "Required daemons"
   - Added card "PostgreSQL"
   - Added card "Nginx"
 + Created list "Required PHP packages"
   - Added card "php-fpm "
   - Added card "php-pgsql"
   - Added card "php-curl "
   - Added card "php-imagick "
   - Added card "php-mbstring "
   - Added card "php-xml"
 + Created list "Restyaboard apps"
   - Added card "Hide Card ID"
   - Added card "Theming/CSSilize"

Additional notes

Remember about API Explorer!

About Milosz Galazka

Milosz is a Linux Foundation Certified Engineer working for a successful Polish company as a system administrator and a long time supporter of Free Software Foundation and Debian operating system. He is also open for new opportunities and challenges.