From 040981c18998e52d3e26b5902710d1e856943358 Mon Sep 17 00:00:00 2001 From: Akianonymus Date: Fri, 7 May 2021 18:50:29 +0530 Subject: [PATCH] Add support for multiple accounts in config | Fix #98 Add four new flags * -a | --account - Use a different account than default * -la | --list accounts - List all the accounts configured with CLIENT_ID, CLIENT_SECRET and REFRESH_TOKEN * -ca | --create-account - Create a new account * -da | --delete-account - Delete an existing account --list-accounts, --create-accounts and --delete-account flags can be used without any usual input required The config format is changed, ACCOUNT_${account}_ prefix is added to every config var, where ${account} is the account name The old configs will be automatically migrated with account name default( if default taken then a number suffix will be added like default1, default2, etc ) move authentication/config related functions to auth-utils.[bash|sh] create more authentication functions to reduce some clutter add a lot of error checks, minute situations like chmod failing, failing to write to config file bash script changes: export all sub functions in their sub scripts export variables when it is created that are needed for parallel uploads --- README.md | 60 ++- bash/auth-utils.bash | 503 +++++++++++++++++++++++ bash/common-utils.bash | 54 ++- bash/drive-utils.bash | 37 +- bash/release/gsync | 54 ++- bash/release/gupload | 897 ++++++++++++++++++++++++++++++----------- bash/upload-utils.bash | 12 +- bash/upload.bash | 292 +++++--------- merge.sh | 5 +- sh/auth-utils.sh | 487 ++++++++++++++++++++++ sh/common-utils.sh | 34 +- sh/drive-utils.sh | 22 - sh/release/gsync | 34 +- sh/release/gupload | 844 +++++++++++++++++++++++++++----------- sh/upload-utils.sh | 4 +- sh/upload.sh | 298 +++++--------- 16 files changed, 2704 insertions(+), 933 deletions(-) create mode 100755 bash/auth-utils.bash create mode 100755 sh/auth-utils.sh diff --git a/README.md b/README.md index 2351b09..2518ca6 100755 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ - To anyone or a specific email. - Config file support - Easy to use on multiple machines. + - Support for multiple accounts in a single config. - Latest gdrive api used i.e v3 - Pretty logging - Easy to install and update @@ -359,16 +360,18 @@ To change the default config file or use a different one temporarily, see `-z / This is the format of a config file: ```shell -CLIENT_ID="client id" -CLIENT_SECRET="client secret" -REFRESH_TOKEN="refresh token" +ACCOUNT_default_CLIENT_ID="client id" +ACCOUNT_default_CLIENT_SECRET="client secret" +ACCOUNT_default_REFRESH_TOKEN="refresh token" SYNC_DEFAULT_ARGS="default args of gupload command for gsync" -ROOT_FOLDER_NAME="root folder name" -ROOT_FOLDER="root folder id" -ACCESS_TOKEN="access token" -ACCESS_TOKEN_EXPIRY="access token expiry" +ACCOUNT_default_ROOT_FOLDER_NAME="root folder name" +ACCOUNT_default_ROOT_FOLDER="root folder id" +ACCOUNT_default_ACCESS_TOKEN="access token" +ACCOUNT_default_ACCESS_TOKEN_EXPIRY="access token expiry" ``` +where `default` is the name of the account. + You can use a config file in multiple machines, the values that are explicitly required are `CLIENT_ID`, `CLIENT_SECRET` and `REFRESH_TOKEN`. If `ROOT_FOLDER` is not set, then it is asked if running in an interactive terminal, otherwise `root` is used. @@ -382,13 +385,14 @@ A pre-generated config file can be also used where interactive terminal access i Just have to print values to `"${HOME}/.googledrive.conf"`, e.g: ```shell -printf "%s\n" "CLIENT_ID=\"client id\" -CLIENT_SECRET=\"client secret\" -REFRESH_TOKEN=\"refresh token\" -" >| "${HOME}/.googledrive.conf" +printf "%s\n" ' +ACCOUNT_default_CLIENT_ID="client id" +ACCOUNT_default_CLIENT_SECRET="client secret" +ACCOUNT_default_REFRESH_TOKEN="refresh token" +' >| "${HOME}/.googledrive.conf" ``` -Note: Don't skip those backslashes before the double qoutes, it's necessary to handle spacing. +Note: If you have an old config, then nothing extra is needed, just need to run the script once and the default config will be automatically converted to the new format. ### Upload @@ -422,6 +426,38 @@ These are the custom flags that are currently implemented: --- +- -a | --account 'account name' + + Use different account than the default one. + + To change the default account name, do + + `gupload -a/--account default=account_name` + + --- + +- -la | --list-accounts + + Print all configured accounts in the config files. + + --- + +- -ca | --create-account 'account name' + + To create a new account with the given name if does not already exists. If the given account exists then script will ask for a new name. + + Note 1: Only for interactive terminal usage. + + Note 2: This flag is preferred over `--account`. + + --- + +- -da | --delete-account 'account name' + + To delete an account information from config file. + + --- + - -c | -C | --create-dir Option to create directory. Will provide folder id. Can be used to specify workspace folder for uploading files/folders. diff --git a/bash/auth-utils.bash b/bash/auth-utils.bash new file mode 100755 index 0000000..3cd7878 --- /dev/null +++ b/bash/auth-utils.bash @@ -0,0 +1,503 @@ +#!/usr/bin/env bash +# auth utils for Google Drive +# shellcheck source=/dev/null + +################################################### +# Check if account name is valid by a regex expression +# Globals: None +# Arguments: 1 +# ${1} = Account name +# Result: read description and return 1 or 0 +################################################### +_account_name_valid() { + declare name="${1:-}" account_name_regex='^([A-Za-z0-9_])+$' + [[ ${name} =~ ${account_name_regex} ]] || return 1 + return 0 +} + +################################################### +# Check if account exists +# First check if the given account is in correct format +# then check if client [id|token] and refresh token is present +# Globals: 2 functions +# _set_value, _account_name_valid +# Arguments: 1 +# ${1} = Account name +# Result: read description and return 1 or 0 +################################################### +_account_exists() { + declare name="${1:-}" client_id client_secret refresh_token + _account_name_valid "${name}" || return 1 + _set_value indirect client_id "ACCOUNT_${name}_CLIENT_ID" + _set_value indirect client_secret "ACCOUNT_${name}_CLIENT_SECRET" + _set_value indirect refresh_token "ACCOUNT_${name}_REFRESH_TOKEN" + [[ -z ${client_id:+${client_secret:+${refresh_token}}} ]] && return 1 + return 0 +} + +################################################### +# Show all accounts configured in config file +# Globals: 2 variables, 4 functions +# Variable - CONFIG, QUIET +# Functions - _account_exists, _set_value, _print_center, _reload_config +# Arguments: None +# Result: SHOW all accounts, export COUNT and ACC_${count}_ACC dynamic variables +# or print "No accounts configured yet." +################################################### +_all_accounts() { + { _reload_config && _handle_old_config; } || return 1 + declare all_accounts && COUNT=0 + mapfile -t all_accounts <<< "$(grep -oE '^ACCOUNT_.*_CLIENT_ID' "${CONFIG}" | sed -e "s/ACCOUNT_//g" -e "s/_CLIENT_ID//g")" + for account in "${all_accounts[@]}"; do + [[ -n ${account} ]] && _account_exists "${account}" && + { [[ ${COUNT} = 0 ]] && "${QUIET:-_print_center}" "normal" " All available accounts. " "=" || :; } && + printf "%b" "$((COUNT += 1)). ${account} \n" && _set_value direct "ACC_${COUNT}_ACC" "${account}" + done + { [[ ${COUNT} -le 0 ]] && "${QUIET:-_print_center}" "normal" " No accounts configured yet. " "=" 1>&2; } || printf '\n' + return 0 +} + +################################################### +# Setup a new account name +# If given account name is configured already, then ask for name +# after name has been properly setup, export ACCOUNT_NAME var +# Globals: 1 variable, 5 functions +# Variable - QUIET +# Functions - _print_center, _account_exists, _clear_line, _account_name_valid, _reload_config +# Arguments: 1 +# ${1} = Account name ( optional ) +# Result: read description and export ACCOUNT_NAME NEW_ACCOUNT_NAME +################################################### +_set_new_account_name() { + _reload_config || return 1 + declare new_account_name="${1:-}" name_valid + [[ -z ${new_account_name} ]] && { + _all_accounts 2>| /dev/null + "${QUIET:-_print_center}" "normal" " New account name: " "=" + "${QUIET:-_print_center}" "normal" "Info: Account names can only contain alphabets / numbers / dashes." " " && printf '\n' + } + until [[ -n ${name_valid} ]]; do + if [[ -n ${new_account_name} ]]; then + if _account_name_valid "${new_account_name}"; then + if _account_exists "${new_account_name}"; then + "${QUIET:-_print_center}" "normal" " Warning: Given account ( ${new_account_name} ) already exists, input different name. " "-" 1>&2 + unset new_account_name && continue + else + export NEW_ACCOUNT_NAME="${new_account_name}" ACCOUNT_NAME="${new_account_name}" && name_valid="true" && continue + fi + else + "${QUIET:-_print_center}" "normal" " Warning: Given account name ( ${new_account_name} ) invalid, input different name. " "-" 1>&2 + unset new_account_name && continue + fi + else + [[ -t 1 ]] || { "${QUIET:-_print_center}" "normal" " Error: Not running in an interactive terminal, cannot ask for new account name. " 1>&2 && return 1; } + printf -- "-> \e[?7l" + read -r new_account_name + printf '\e[?7h' + fi + _clear_line 1 + done + "${QUIET:-_print_center}" "normal" " Given account name: ${NEW_ACCOUNT_NAME} " "=" + export ACCOUNT_NAME="${NEW_ACCOUNT_NAME}" + return 0 +} + +################################################### +# Delete a account from config file +# Globals: 2 variables, 3 functions +# Variables - CONFIG, QUIET +# Functions - _account_exists, _print_center, _reload_config +# Arguments: None +# Result: check if account exists and delete from config, else print error message +################################################### +_delete_account() { + { _reload_config && _handle_old_config; } || return 1 + declare account="${1:?Error: give account name}" regex config_without_values + if _account_exists "${account}"; then + regex="^ACCOUNT_${account}_(CLIENT_ID=|CLIENT_SECRET=|REFRESH_TOKEN=|ROOT_FOLDER=|ROOT_FOLDER_NAME=|ACCESS_TOKEN=|ACCESS_TOKEN_EXPIRY=)|DEFAULT_ACCOUNT=\"${account}\"" + config_without_values="$(grep -vE "${regex}" "${CONFIG}")" + chmod u+w "${CONFIG}" || return 1 # change perms to edit + printf "%s\n" "${config_without_values}" >| "${CONFIG}" || return 1 + chmod "a-w-r-x,u+r" "${CONFIG}" || return 1 # restore perms + "${QUIET:-_print_center}" "normal" " Successfully deleted account ( ${account} ) from config. " "-" + _reload_config # reload config if successfully deleted + else + "${QUIET:-_print_center}" "normal" " Error: Cannot delete account ( ${account} ) from config. No such account exists. " "-" 1>&2 + fi + return 0 +} + +################################################### +# handle legacy config +# this will be triggered only if old config values are present, convert to new format +# new account will be created with "default" name, if default already taken, then add a number as suffix +# Globals: 7 variables, 2 functions +# Variables - CLIENT_ID CLIENT_SECRET, REFRESH_TOKEN, ROOT_FOLDER, ROOT_FOLDER_NAME CONFIG, ACCOUNT_NAME +# Functions - _account_exists, _reload_config +# Arguments: None +################################################### +_handle_old_config() { + export CLIENT_ID CLIENT_SECRET REFRESH_TOKEN # to handle a shellcheck warning + # only try to convert the if all three values are present + [[ -n ${CLIENT_ID:+${CLIENT_SECRET:+${REFRESH_TOKEN}}} ]] && { + declare account_name="default" regex config_without_values count=0 + # first try to name the new account as default, otherwise try to add numbers as suffix + until ! _account_exists "${account_name}"; do + account_name="${account_name}$((count += 1))" + done + # form a regex expression to remove values from config, _update_config isn't used here to prevent a loop and multiple grep calls + regex="^(CLIENT_ID=|CLIENT_SECRET=|REFRESH_TOKEN=|ROOT_FOLDER=|ROOT_FOLDER_NAME=|ACCESS_TOKEN=|ACCESS_TOKEN_EXPIRY=)" + config_without_values="$(grep -vE "${regex}" "${CONFIG}")" + chmod u+w "${CONFIG}" || return 1 # change perms to edit + printf "%s\n%s\n%s\n%s\n%s\n%s\n" \ + "ACCOUNT_${account_name}_CLIENT_ID=\"${CLIENT_ID}\"" \ + "ACCOUNT_${account_name}_CLIENT_SECRET=\"${CLIENT_SECRET}\"" \ + "ACCOUNT_${account_name}_REFRESH_TOKEN=\"${REFRESH_TOKEN}\"" \ + "ACCOUNT_${account_name}_ROOT_FOLDER=\"${ROOT_FOLDER}\"" \ + "ACCOUNT_${account_name}_ROOT_FOLDER_NAME=\"${ROOT_FOLDER_NAME}\"" \ + "${config_without_values}" >| "${CONFIG}" || return 1 + + chmod "a-w-r-x,u+r" "${CONFIG}" || return 1 # restore perms + + _reload_config || return 1 # reload config file + } + return 0 +} + +################################################### +# handle old config values, new account creation, custom account name, updating default config and account +# start token service if applicable +# Globals: 12 variables, 7 functions +# Variables - DEFAULT_CONFIG, NEW_ACCOUNT_NAME, CUSTOM_ACCOUNT_NAME, DELETE_ACCOUNT_NAME, LIST_ACCOUNTS, QUIET +# UPDATE_DEFAULT_ACCOUNT, UPDATE_DEFAULT_CONFIG, CONFIG_INFO, CONTINUE_WITH_NO_INPUT +# Functions - _reload_config, _handle_old_config, _set_new_account_name, _account_exists, _all_accounts +# _check_account_credentials, _token_bg_service, _print_center, _update_config, _set_value +# Arguments: None +# Result: read description and start access token check in bg if required +################################################### +_check_credentials() { + { _reload_config && _handle_old_config; } || return 1 + # set account name to default account name + ACCOUNT_NAME="${DEFAULT_ACCOUNT}" + + if [[ -n ${NEW_ACCOUNT_NAME} ]]; then + # create new account, --create-account flag + _set_new_account_name "${NEW_ACCOUNT_NAME}" || return 1 + _check_account_credentials "${ACCOUNT_NAME}" || return 1 + else + # use custom account, --account flag + if [[ -n ${CUSTOM_ACCOUNT_NAME} ]]; then + if _account_exists "${CUSTOM_ACCOUNT_NAME}"; then + ACCOUNT_NAME="${CUSTOM_ACCOUNT_NAME}" + else + # error out in case CUSTOM_ACCOUNT_NAME is invalid + "${QUIET:-_print_center}" "normal" " Error: No such account ( ${CUSTOM_ACCOUNT_NAME} ) exists. " "-" && return 1 + fi + elif [[ -n ${DEFAULT_ACCOUNT} ]]; then + # check if default account if valid or not, else set account name to nothing and remove default account in config + _account_exists "${DEFAULT_ACCOUNT}" || { + _update_config DEFAULT_ACCOUNT "" "${CONFIG}" && unset DEFAULT_ACCOUNT ACCOUNT_NAME && UPDATE_DEFAULT_ACCOUNT="_update_config" + } + # UPDATE_DEFAULT_ACCOUNT to true so that default config is updated later + else + UPDATE_DEFAULT_ACCOUNT="_update_config" # as default account doesn't exist + fi + + # in case no account name is set at this point of script + if [[ -z ${ACCOUNT_NAME} ]]; then + # if accounts are configured but default account is not set + # COUNT comes from _all_accounts function + if _all_accounts 2>| /dev/null && [[ ${COUNT} -gt 0 ]]; then + # set ACCOUNT_NAME without asking if only one account available + if [[ ${COUNT} -eq 1 ]]; then + _set_value indirect ACCOUNT_NAME "ACC_1_ACC" # ACC_1_ACC comes from _all_accounts function + else + "${QUIET:-_print_center}" "normal" " Above accounts are configured, but default one not set. " "=" + if [[ -t 1 ]]; then + \ "${QUIET:-_print_center}" "normal" " Choose default account: " "-" + until [[ -n ${ACCOUNT_NAME} ]]; do + printf -- "-> \e[?7l" + read -r account_name + printf '\e[?7h' + if [[ ${account_name} -gt 0 && ${account_name} -le ${COUNT} ]]; then + _set_value indirect ACCOUNT_NAME "ACC_${COUNT}_ACC" + else + _clear_line 1 + fi + done + else + # if not running in a terminal then choose 1st one as default + printf "%s\n" "Warning: Script is not running in a terminal, choosing first account as default." + _set_value indirect ACCOUNT_NAME "ACC_1_ACC" # ACC_1_ACC comes from _all_accounts function + fi + fi + else + _set_new_account_name "" || return 1 + _check_account_credentials "${ACCOUNT_NAME}" || return 1 + fi + fi + _check_account_credentials "${ACCOUNT_NAME}" || return 1 + fi + + "${UPDATE_DEFAULT_ACCOUNT:-:}" DEFAULT_ACCOUNT "${ACCOUNT_NAME}" "${CONFIG}" # update default account if required + "${UPDATE_DEFAULT_CONFIG:-:}" CONFIG "${CONFIG}" "${CONFIG_INFO}" # update default config if required + + # only launch the token service if there was some input + [[ -n ${CONTINUE_WITH_NO_INPUT} ]] || _token_bg_service # launch token bg service + return 0 +} + +################################################### +# check credentials for a given account name +# Globals: 3 functions +# Functions - _check_client, _check_refresh_token, _check_access_token +# Arguments: 2 +# ${1} = Account name ( optional ) +# Result: read description, return 1 or 0 +################################################### +_check_account_credentials() { + declare account_name="${1:-}" + { + _check_client ID "${account_name}" && + _check_client SECRET "${account_name}" && + _check_refresh_token "${account_name}" && + _check_access_token "${account_name}" check + } || return 1 + return 0 +} + +################################################### +# Check client id or secret and ask if required +# Globals: 4 variables, 3 functions +# Variables - CONFIG, QUIET, CLIENT_ID_${ACCOUNT_NAME}, CLIENT_SECRET_${ACCOUNT_NAME} +# Functions - _print_center, _update_config, _set_value +# Arguments: 2 +# ${1} = ID or SECRET +# ${2} = Account name ( optional - if not given, then just CLIENT_[ID|SECRET] var is used ) +# Result: read description and export ACCOUNT_name_CLIENT_[ID|SECRET] CLIENT_[ID|SECRET] +################################################### +_check_client() { + declare type="CLIENT_${1:?Error: ID or SECRET}" account_name="${2:-}" \ + type_name type_value type_regex valid client message + export client_id_regex='[0-9]+-[0-9A-Za-z_]{32}\.apps\.googleusercontent\.com' client_secret_regex='[0-9A-Za-z_-]+' + type_name="${account_name:+ACCOUNT_${account_name}_}${type}" + + # set the type_value to the actual value of ACCOUNT_${account_name}_[ID|SECRET] + _set_value indirect type_value "${type_name}" + # set the type_regex to the actual value of client_id_regex or client_secret_regex + _set_value indirect type_regex "${type}_regex" + + until [[ -n ${type_value} && -n ${valid} ]]; do + [[ -n ${type_value} ]] && { + if [[ ${type_value} =~ ${type_regex} ]]; then + [[ -n ${client} ]] && { _update_config "${type_name}" "${type_value}" "${CONFIG}" || return 1; } + valid="true" && continue + else + { [[ -n ${client} ]] && message="- Try again"; } || message="in config ( ${CONFIG} )" + "${QUIET:-_print_center}" "normal" " Invalid Client ${1} ${message} " "-" && unset "${type_name}" client + fi + } + [[ -z ${client} ]] && printf "\n" && "${QUIET:-_print_center}" "normal" " Enter Client ${1} " "-" + [[ -n ${client} ]] && _clear_line 1 + printf -- "-> " + read -r "${type_name?}" && client=1 + _set_value indirect type_value "${type_name}" + done + + # export ACCOUNT_name_CLIENT_[ID|SECRET] + _set_value direct "${type_name}" "${type_value}" + # export CLIENT_[ID|SECRET] + _set_value direct "${type}" "${type_value}" + + return 0 +} + +################################################### +# Check refresh token and ask if required +# Globals: 8 variables, 4 functions +# Variables - CLIENT_ID, CLIENT_SECRET, REDIRECT_URI, TOKEN_URL, CONFIG, QUIET +# Functions - _set_value, _print_center, _update_config, _check_access_token +# Arguments: 1 +# ${1} = Account name ( optional - if not given, then just REFRESH_TOKEN var is used ) +# Result: read description & export REFRESH_TOKEN ACCOUNT_${account_name}_REFRESH_TOKEN +################################################### +_check_refresh_token() { + # bail out before doing anything if client id and secret is not present, unlikely to happen but just in case + [[ -z ${CLIENT_ID:+${CLIENT_SECRET}} ]] && return 1 + declare account_name="${1:-}" \ + refresh_token_regex='[0-9]//[0-9A-Za-z_-]+' authorization_code_regex='[0-9]/[0-9A-Za-z_-]+' + declare refresh_token_name="${account_name:+ACCOUNT_${account_name}_}REFRESH_TOKEN" check_error + + _set_value indirect refresh_token_value "${refresh_token_name}" + + [[ -n ${refresh_token_value} ]] && { + ! [[ ${refresh_token_value} =~ ${refresh_token_regex} ]] && + "${QUIET:-_print_center}" "normal" " Error: Invalid Refresh token in config file, follow below steps.. " "-" && unset refresh_token_value + } + + [[ -z ${refresh_token_value} ]] && { + printf "\n" && "${QUIET:-_print_center}" "normal" "If you have a refresh token generated, then type the token, else leave blank and press return key.." " " + printf "\n" && "${QUIET:-_print_center}" "normal" " Refresh Token " "-" && printf -- "-> " + read -r refresh_token_value + if [[ -n ${refresh_token_value} ]]; then + "${QUIET:-_print_center}" "normal" " Checking refresh token.. " "-" + if [[ ${refresh_token_value} =~ ${refresh_token_regex} ]]; then + _set_value direct REFRESH_TOKEN "${refresh_token_value}" + { _check_access_token "${account_name}" skip_check && + _update_config "${refresh_token_name}" "${refresh_token_value}" "${CONFIG}" && + _clear_line 1; } || check_error=true + else + check_error=true + fi + [[ -n ${check_error} ]] && "${QUIET:-_print_center}" "normal" " Error: Invalid Refresh token given, follow below steps to generate.. " "-" && unset refresh_token_value + else + "${QUIET:-_print_center}" "normal" " No Refresh token given, follow below steps to generate.. " "-" && unset refresh_token_value + fi + + [[ -z ${refresh_token_value} ]] && { + printf "\n" && "${QUIET:-_print_center}" "normal" "Visit the below URL, tap on allow and then enter the code obtained" " " + URL="https://accounts.google.com/o/oauth2/auth?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&scope=${SCOPE}&response_type=code&prompt=consent" + printf "\n%s\n" "${URL}" + declare AUTHORIZATION_CODE authorization_code AUTHORIZATION_CODE_VALID response + until [[ -n ${AUTHORIZATION_CODE} && -n ${AUTHORIZATION_CODE_VALID} ]]; do + [[ -n ${AUTHORIZATION_CODE} ]] && { + if [[ ${AUTHORIZATION_CODE} =~ ${authorization_code_regex} ]]; then + AUTHORIZATION_CODE_VALID="true" && continue + else + "${QUIET:-_print_center}" "normal" " Invalid CODE given, try again.. " "-" && unset AUTHORIZATION_CODE authorization_code + fi + } + { [[ -z ${authorization_code} ]] && printf "\n" && "${QUIET:-_print_center}" "normal" " Enter the authorization code " "-"; } || _clear_line 1 + printf -- "-> \e[?7l" + read -r AUTHORIZATION_CODE && authorization_code=1 + printf '\e[?7h' + done + response="$(curl --compressed "${CURL_PROGRESS}" -X POST \ + --data "code=${AUTHORIZATION_CODE}&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&redirect_uri=${REDIRECT_URI}&grant_type=authorization_code" "${TOKEN_URL}")" || : + _clear_line 1 1>&2 + + refresh_token_value="$(_json_value refresh_token 1 1 <<< "${response}")" || + { printf "%s\n" "Error: Cannot fetch refresh token, make sure the authorization code was correct." && return 1; } + + _set_value direct REFRESH_TOKEN "${refresh_token_value}" + { _check_access_token "${account_name}" skip_check "${response}" && + _update_config "${refresh_token_name}" "${refresh_token_value}" "${CONFIG}"; } || return 1 + } + printf "\n" + } + + # export ACCOUNT_name_REFRESH_TOKEN + _set_value direct "${refresh_token_name}" "${refresh_token_value}" + # export REFRESH_TOKEN + _set_value direct REFRESH_TOKEN "${refresh_token_value}" + + return 0 +} + +################################################### +# Check access token and create/update if required +# Also update in config +# Globals: 9 variables, 3 functions +# Variables - CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN, TOKEN_URL, CONFIG, API_URL, API_VERSION, QUIET +# Functions - _print_center, _update_config, _set_value +# Arguments: 2 +# ${1} = Account name ( optional - if not given, then just ACCESS_TOKEN var is used ) +# ${2} = if skip_check, then force create access token, else check with regex and expiry +# ${3} = json response ( optional ) +# Result: read description & export ACCESS_TOKEN ACCESS_TOKEN_EXPIRY +################################################### +_check_access_token() { + # bail out before doing anything if client id|secret or refresh token is not present, unlikely to happen but just in case + [[ -z ${CLIENT_ID:+${CLIENT_SECRET:+${REFRESH_TOKEN}}} ]] && return 1 + + declare account_name="${1:-}" no_check="${2:-false}" response_json="${3:-}" \ + token_name token_expiry_name token_value token_expiry_value response \ + access_token_regex='ya29\.[0-9A-Za-z_-]+' + declare token_name="${account_name:+ACCOUNT_${account_name}_}ACCESS_TOKEN" + declare token_expiry_name="${token_name}_EXPIRY" + + _set_value indirect token_value "${token_name}" + _set_value indirect token_expiry_value "${token_expiry_name}" + + [[ ${no_check} = skip_check || -z ${token_value} || ${token_expiry_value:-0} -lt "$(printf "%(%s)T\\n" "-1")" || ! ${token_value} =~ ${access_token_regex} ]] && { + response="${response_json:-$(curl --compressed -s -X POST --data \ + "client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&refresh_token=${REFRESH_TOKEN}&grant_type=refresh_token" "${TOKEN_URL}")}" || : + + if token_value="$(_json_value access_token 1 1 <<< "${response}")"; then + token_expiry_value="$(($(printf "%(%s)T\\n" "-1") + $(_json_value expires_in 1 1 <<< "${response}") - 1))" + _update_config "${token_name}" "${token_value}" "${CONFIG}" || return 1 + _update_config "${token_expiry_name}" "${token_expiry_value}" "${CONFIG}" || return 1 + else + "${QUIET:-_print_center}" "justify" "Error: Something went wrong" ", printing error." "=" 1>&2 + printf "%s\n" "${response}" 1>&2 + return 1 + fi + } + + # export ACCESS_TOKEN and ACCESS_TOKEN_EXPIRY + _set_value direct ACCESS_TOKEN "${token_value}" + _set_value direct ACCESS_TOKEN_EXPIRY "${token_expiry_value}" + + # export INITIAL_ACCESS_TOKEN which is used on script cleanup + _set_value direct INITIAL_ACCESS_TOKEN "${ACCESS_TOKEN}" + return 0 +} + +################################################### +# load config file if available, else create a empty file +# uses global variable CONFIG +################################################### +_reload_config() { + { [[ -r ${CONFIG} ]] && . "${CONFIG}"; } || { printf "" >> "${CONFIG}" || return 1; } + return 0 +} + +################################################### +# launch a background service to check access token and update it +# checks ACCESS_TOKEN_EXPIRY, try to update before 5 mins of expiry, a fresh token gets 60 mins +# process will be killed when script exits or "${MAIN_PID}" is killed +# Globals: 4 variables, 1 function +# Variables - ACCESS_TOKEN, ACCESS_TOKEN_EXPIRY, MAIN_PID, TMPFILE +# Functions - _check_access_token +# Arguments: None +# Result: read description & export ACCESS_TOKEN_SERVICE_PID +################################################### +_token_bg_service() { + [[ -z ${MAIN_PID} ]] && return 0 # don't start if MAIN_PID is empty + printf "%b\n" "ACCESS_TOKEN=\"${ACCESS_TOKEN}\"\nACCESS_TOKEN_EXPIRY=\"${ACCESS_TOKEN_EXPIRY}\"" >| "${TMPFILE}_ACCESS_TOKEN" + { + until ! kill -0 "${MAIN_PID}" 2>| /dev/null 1>&2; do + . "${TMPFILE}_ACCESS_TOKEN" + CURRENT_TIME="$(printf "%(%s)T\\n" "-1")" + REMAINING_TOKEN_TIME="$((ACCESS_TOKEN_EXPIRY - CURRENT_TIME))" + if [[ ${REMAINING_TOKEN_TIME} -le 300 ]]; then + # timeout after 30 seconds, it shouldn't take too long anyway, and update tmp config + CONFIG="${TMPFILE}_ACCESS_TOKEN" _timeout 30 _check_access_token "" skip_check || : + else + TOKEN_PROCESS_TIME_TO_SLEEP="$(if [[ ${REMAINING_TOKEN_TIME} -le 301 ]]; then + printf "0\n" + else + printf "%s\n" "$((REMAINING_TOKEN_TIME - 300))" + fi)" + sleep "${TOKEN_PROCESS_TIME_TO_SLEEP}" + fi + sleep 1 + done + } & + export ACCESS_TOKEN_SERVICE_PID="${!}" + return 0 +} + +ALL_FUNCTIONS=(_account_name_valid + _account_exists + _all_accounts + _set_new_account_name + _delete_account + _handle_old_config + _check_credentials + _check_account_credentials + _check_client + _check_refresh_token + _check_access_token + _reload_config) +export -f "${ALL_FUNCTIONS[@]}" diff --git a/bash/common-utils.bash b/bash/common-utils.bash index 42142cf..13eb3a9 100755 --- a/bash/common-utils.bash +++ b/bash/common-utils.bash @@ -44,7 +44,6 @@ _check_bash_version() { # Check QUIET, then check terminal size and enable print functions accordingly. ################################################### _check_debug() { - _print_center_quiet() { { [[ $# = 3 ]] && printf "%s\n" "${2}"; } || { printf "%s%s\n" "${2}" "${3}"; }; } if [[ -n ${DEBUG} ]]; then set -x && PS4='-> ' _print_center() { { [[ $# = 3 ]] && printf "%s\n" "${2}"; } || { printf "%s%s\n" "${2}" "${3}"; }; } @@ -70,6 +69,7 @@ _check_debug() { fi set +x fi + export -f _print_center _clear_line _newline } ################################################### @@ -87,7 +87,7 @@ _check_internet() { if ! _timeout 10 curl -Is google.com; then _clear_line 1 "${QUIET:-_print_center}" "justify" "Error: Internet connection" " not available." "=" - exit 1 + return 1 fi _clear_line 1 } @@ -272,6 +272,30 @@ _print_center() { return 0 } +################################################### +# Quiet version of _print_center +################################################### +_print_center_quiet() { + { [[ $# = 3 ]] && printf "%s\n" "${2}"; } || printf "%s%s\n" "${2}" "${3}" +} + +################################################### +# Evaluates value1=value2 +# Globals: None +# Arguments: 3 +# ${1} = direct ( d ) or indirect ( i ) - ( evaluation mode ) +# ${2} = var name +# ${3} = var value +# Result: export value1=value2 +################################################### +_set_value() { + declare mode="${1:?}" var="${2:?}" value="${3:?}" + case "${mode}" in + d | direct) export "${var}=${value}" ;; + i | indirect) export "${var}=${!value}" ;; + esac +} + ################################################### # Check if script terminal supports ansi escapes # Globals: 1 variable @@ -321,10 +345,11 @@ _update_config() { [[ $# -lt 3 ]] && printf "Missing arguments\n" && return 1 declare value_name="${1}" value="${2}" config_path="${3}" ! [ -f "${config_path}" ] && : >| "${config_path}" # If config file doesn't exist. - chmod u+w "${config_path}" + chmod u+w "${config_path}" || return 1 printf "%s\n%s\n" "$(grep -v -e "^$" -e "^${value_name}=" "${config_path}" || :)" \ - "${value_name}=\"${value}\"" >| "${config_path}" - chmod a-w-r-x,u+r "${config_path}" + "${value_name}=\"${value}\"" >| "${config_path}" || return 1 + chmod a-w-r-x,u+r "${config_path}" || return 1 + return 0 } ################################################### @@ -351,3 +376,22 @@ _url_encode() { done 2>| /dev/null printf '\n' } + +ALL_FUNCTIONS=(_bytes_to_human + _check_bash_version + _check_debug + _check_internet + _clear_line + _count + _dirname + _display_time + _get_latest_sha + _json_value + _print_center + _print_center_quiet + _set_value + _support_ansi_escapes + _timeout + _update_config + _url_encode) +export -f "${ALL_FUNCTIONS[@]}" diff --git a/bash/drive-utils.bash b/bash/drive-utils.bash index 957d90f..86f8549 100755 --- a/bash/drive-utils.bash +++ b/bash/drive-utils.bash @@ -177,28 +177,6 @@ _extract_id() { printf "%b" "${ID:+${ID}\n}" } -################################################### -# Method to regenerate access_token ( also updates in config ). -# Make a request on https://www.googleapis.com/oauth2/""${API_VERSION}""/tokeninfo?access_token=${ACCESS_TOKEN} url and check if the given token is valid, if not generate one. -# Globals: 9 variables, 2 functions -# Variables - CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN, TOKEN_URL, CONFIG, API_URL, API_VERSION, QUIET, NO_UPDATE_TOKEN -# Functions - _update_config and _print_center -# Result: Update access_token and expiry else print error -################################################### -_get_access_token_and_update() { - RESPONSE="${1:-$(curl --compressed -s -X POST --data "client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&refresh_token=${REFRESH_TOKEN}&grant_type=refresh_token" "${TOKEN_URL}")}" || : - if ACCESS_TOKEN="$(_json_value access_token 1 1 <<< "${RESPONSE}")"; then - ACCESS_TOKEN_EXPIRY="$(($(printf "%(%s)T\\n" "-1") + $(_json_value expires_in 1 1 <<< "${RESPONSE}") - 1))" - _update_config ACCESS_TOKEN "${ACCESS_TOKEN}" "${CONFIG}" - _update_config ACCESS_TOKEN_EXPIRY "${ACCESS_TOKEN_EXPIRY}" "${CONFIG}" - else - "${QUIET:-_print_center}" "justify" "Error: Something went wrong" ", printing error." "=" 1>&2 - printf "%s\n" "${RESPONSE}" 1>&2 - return 1 - fi - return 0 -} - ################################################### # Upload ( Create/Update ) files on gdrive. # Interrupted uploads can be resumed. @@ -418,3 +396,18 @@ _share_id() { { _json_value id 1 1 <<< "${share_response}" 2>| /dev/null 1>&2 && return 0; } || { printf "%s\n" "Error: Cannot Share." 1>&2 && printf "%s\n" "${share_response}" 1>&2 && return 1; } } + +ALL_FUNCTIONS=(_check_existing_file + _clone_file + _create_directory + _drive_info + _extract_id + _upload_file + _generate_upload_link + _upload_file_from_uri + _normal_logging_upload + _log_upload_session + _remove_upload_session + _full_upload + _share_id) +export -f "${ALL_FUNCTIONS[@]}" diff --git a/bash/release/gsync b/bash/release/gsync index 0631a66..a8dded1 100755 --- a/bash/release/gsync +++ b/bash/release/gsync @@ -45,7 +45,6 @@ _check_bash_version() { # Check QUIET, then check terminal size and enable print functions accordingly. ################################################### _check_debug() { - _print_center_quiet() { { [[ $# = 3 ]] && printf "%s\n" "${2}"; } || { printf "%s%s\n" "${2}" "${3}"; }; } if [[ -n ${DEBUG} ]]; then set -x && PS4='-> ' _print_center() { { [[ $# = 3 ]] && printf "%s\n" "${2}"; } || { printf "%s%s\n" "${2}" "${3}"; }; } @@ -71,6 +70,7 @@ _check_debug() { fi set +x fi + export -f _print_center _clear_line _newline } ################################################### @@ -88,7 +88,7 @@ _check_internet() { if ! _timeout 10 curl -Is google.com; then _clear_line 1 "${QUIET:-_print_center}" "justify" "Error: Internet connection" " not available." "=" - exit 1 + return 1 fi _clear_line 1 } @@ -273,6 +273,30 @@ _print_center() { return 0 } +################################################### +# Quiet version of _print_center +################################################### +_print_center_quiet() { + { [[ $# = 3 ]] && printf "%s\n" "${2}"; } || printf "%s%s\n" "${2}" "${3}" +} + +################################################### +# Evaluates value1=value2 +# Globals: None +# Arguments: 3 +# ${1} = direct ( d ) or indirect ( i ) - ( evaluation mode ) +# ${2} = var name +# ${3} = var value +# Result: export value1=value2 +################################################### +_set_value() { + declare mode="${1:?}" var="${2:?}" value="${3:?}" + case "${mode}" in + d | direct) export "${var}=${value}" ;; + i | indirect) export "${var}=${!value}" ;; + esac +} + ################################################### # Check if script terminal supports ansi escapes # Globals: 1 variable @@ -322,10 +346,11 @@ _update_config() { [[ $# -lt 3 ]] && printf "Missing arguments\n" && return 1 declare value_name="${1}" value="${2}" config_path="${3}" ! [ -f "${config_path}" ] && : >| "${config_path}" # If config file doesn't exist. - chmod u+w "${config_path}" + chmod u+w "${config_path}" || return 1 printf "%s\n%s\n" "$(grep -v -e "^$" -e "^${value_name}=" "${config_path}" || :)" \ - "${value_name}=\"${value}\"" >| "${config_path}" - chmod a-w-r-x,u+r "${config_path}" + "${value_name}=\"${value}\"" >| "${config_path}" || return 1 + chmod a-w-r-x,u+r "${config_path}" || return 1 + return 0 } ################################################### @@ -352,6 +377,25 @@ _url_encode() { done 2>| /dev/null printf '\n' } + +ALL_FUNCTIONS=(_bytes_to_human + _check_bash_version + _check_debug + _check_internet + _clear_line + _count + _dirname + _display_time + _get_latest_sha + _json_value + _print_center + _print_center_quiet + _set_value + _support_ansi_escapes + _timeout + _update_config + _url_encode) +export -f "${ALL_FUNCTIONS[@]}" # Sync a FOLDER to google drive forever using labbots/google-drive-upload # shellcheck source=/dev/null diff --git a/bash/release/gupload b/bash/release/gupload index 0a930e7..a92855c 100755 --- a/bash/release/gupload +++ b/bash/release/gupload @@ -45,7 +45,6 @@ _check_bash_version() { # Check QUIET, then check terminal size and enable print functions accordingly. ################################################### _check_debug() { - _print_center_quiet() { { [[ $# = 3 ]] && printf "%s\n" "${2}"; } || { printf "%s%s\n" "${2}" "${3}"; }; } if [[ -n ${DEBUG} ]]; then set -x && PS4='-> ' _print_center() { { [[ $# = 3 ]] && printf "%s\n" "${2}"; } || { printf "%s%s\n" "${2}" "${3}"; }; } @@ -71,6 +70,7 @@ _check_debug() { fi set +x fi + export -f _print_center _clear_line _newline } ################################################### @@ -88,7 +88,7 @@ _check_internet() { if ! _timeout 10 curl -Is google.com; then _clear_line 1 "${QUIET:-_print_center}" "justify" "Error: Internet connection" " not available." "=" - exit 1 + return 1 fi _clear_line 1 } @@ -273,6 +273,30 @@ _print_center() { return 0 } +################################################### +# Quiet version of _print_center +################################################### +_print_center_quiet() { + { [[ $# = 3 ]] && printf "%s\n" "${2}"; } || printf "%s%s\n" "${2}" "${3}" +} + +################################################### +# Evaluates value1=value2 +# Globals: None +# Arguments: 3 +# ${1} = direct ( d ) or indirect ( i ) - ( evaluation mode ) +# ${2} = var name +# ${3} = var value +# Result: export value1=value2 +################################################### +_set_value() { + declare mode="${1:?}" var="${2:?}" value="${3:?}" + case "${mode}" in + d | direct) export "${var}=${value}" ;; + i | indirect) export "${var}=${!value}" ;; + esac +} + ################################################### # Check if script terminal supports ansi escapes # Globals: 1 variable @@ -322,10 +346,11 @@ _update_config() { [[ $# -lt 3 ]] && printf "Missing arguments\n" && return 1 declare value_name="${1}" value="${2}" config_path="${3}" ! [ -f "${config_path}" ] && : >| "${config_path}" # If config file doesn't exist. - chmod u+w "${config_path}" + chmod u+w "${config_path}" || return 1 printf "%s\n%s\n" "$(grep -v -e "^$" -e "^${value_name}=" "${config_path}" || :)" \ - "${value_name}=\"${value}\"" >| "${config_path}" - chmod a-w-r-x,u+r "${config_path}" + "${value_name}=\"${value}\"" >| "${config_path}" || return 1 + chmod a-w-r-x,u+r "${config_path}" || return 1 + return 0 } ################################################### @@ -353,6 +378,527 @@ _url_encode() { printf '\n' } +ALL_FUNCTIONS=(_bytes_to_human + _check_bash_version + _check_debug + _check_internet + _clear_line + _count + _dirname + _display_time + _get_latest_sha + _json_value + _print_center + _print_center_quiet + _set_value + _support_ansi_escapes + _timeout + _update_config + _url_encode) +export -f "${ALL_FUNCTIONS[@]}" +# auth utils for Google Drive +# shellcheck source=/dev/null + +################################################### +# Check if account name is valid by a regex expression +# Globals: None +# Arguments: 1 +# ${1} = Account name +# Result: read description and return 1 or 0 +################################################### +_account_name_valid() { + declare name="${1:-}" account_name_regex='^([A-Za-z0-9_])+$' + [[ ${name} =~ ${account_name_regex} ]] || return 1 + return 0 +} + +################################################### +# Check if account exists +# First check if the given account is in correct format +# then check if client [id|token] and refresh token is present +# Globals: 2 functions +# _set_value, _account_name_valid +# Arguments: 1 +# ${1} = Account name +# Result: read description and return 1 or 0 +################################################### +_account_exists() { + declare name="${1:-}" client_id client_secret refresh_token + _account_name_valid "${name}" || return 1 + _set_value indirect client_id "ACCOUNT_${name}_CLIENT_ID" + _set_value indirect client_secret "ACCOUNT_${name}_CLIENT_SECRET" + _set_value indirect refresh_token "ACCOUNT_${name}_REFRESH_TOKEN" + [[ -z ${client_id:+${client_secret:+${refresh_token}}} ]] && return 1 + return 0 +} + +################################################### +# Show all accounts configured in config file +# Globals: 2 variables, 4 functions +# Variable - CONFIG, QUIET +# Functions - _account_exists, _set_value, _print_center, _reload_config +# Arguments: None +# Result: SHOW all accounts, export COUNT and ACC_${count}_ACC dynamic variables +# or print "No accounts configured yet." +################################################### +_all_accounts() { + { _reload_config && _handle_old_config; } || return 1 + declare all_accounts && COUNT=0 + mapfile -t all_accounts <<< "$(grep -oE '^ACCOUNT_.*_CLIENT_ID' "${CONFIG}" | sed -e "s/ACCOUNT_//g" -e "s/_CLIENT_ID//g")" + for account in "${all_accounts[@]}"; do + [[ -n ${account} ]] && _account_exists "${account}" && + { [[ ${COUNT} = 0 ]] && "${QUIET:-_print_center}" "normal" " All available accounts. " "=" || :; } && + printf "%b" "$((COUNT += 1)). ${account} \n" && _set_value direct "ACC_${COUNT}_ACC" "${account}" + done + { [[ ${COUNT} -le 0 ]] && "${QUIET:-_print_center}" "normal" " No accounts configured yet. " "=" 1>&2; } || printf '\n' + return 0 +} + +################################################### +# Setup a new account name +# If given account name is configured already, then ask for name +# after name has been properly setup, export ACCOUNT_NAME var +# Globals: 1 variable, 5 functions +# Variable - QUIET +# Functions - _print_center, _account_exists, _clear_line, _account_name_valid, _reload_config +# Arguments: 1 +# ${1} = Account name ( optional ) +# Result: read description and export ACCOUNT_NAME NEW_ACCOUNT_NAME +################################################### +_set_new_account_name() { + _reload_config || return 1 + declare new_account_name="${1:-}" name_valid + [[ -z ${new_account_name} ]] && { + _all_accounts 2>| /dev/null + "${QUIET:-_print_center}" "normal" " New account name: " "=" + "${QUIET:-_print_center}" "normal" "Info: Account names can only contain alphabets / numbers / dashes." " " && printf '\n' + } + until [[ -n ${name_valid} ]]; do + if [[ -n ${new_account_name} ]]; then + if _account_name_valid "${new_account_name}"; then + if _account_exists "${new_account_name}"; then + "${QUIET:-_print_center}" "normal" " Warning: Given account ( ${new_account_name} ) already exists, input different name. " "-" 1>&2 + unset new_account_name && continue + else + export NEW_ACCOUNT_NAME="${new_account_name}" ACCOUNT_NAME="${new_account_name}" && name_valid="true" && continue + fi + else + "${QUIET:-_print_center}" "normal" " Warning: Given account name ( ${new_account_name} ) invalid, input different name. " "-" 1>&2 + unset new_account_name && continue + fi + else + [[ -t 1 ]] || { "${QUIET:-_print_center}" "normal" " Error: Not running in an interactive terminal, cannot ask for new account name. " 1>&2 && return 1; } + printf -- "-> \e[?7l" + read -r new_account_name + printf '\e[?7h' + fi + _clear_line 1 + done + "${QUIET:-_print_center}" "normal" " Given account name: ${NEW_ACCOUNT_NAME} " "=" + export ACCOUNT_NAME="${NEW_ACCOUNT_NAME}" + return 0 +} + +################################################### +# Delete a account from config file +# Globals: 2 variables, 3 functions +# Variables - CONFIG, QUIET +# Functions - _account_exists, _print_center, _reload_config +# Arguments: None +# Result: check if account exists and delete from config, else print error message +################################################### +_delete_account() { + { _reload_config && _handle_old_config; } || return 1 + declare account="${1:?Error: give account name}" regex config_without_values + if _account_exists "${account}"; then + regex="^ACCOUNT_${account}_(CLIENT_ID=|CLIENT_SECRET=|REFRESH_TOKEN=|ROOT_FOLDER=|ROOT_FOLDER_NAME=|ACCESS_TOKEN=|ACCESS_TOKEN_EXPIRY=)|DEFAULT_ACCOUNT=\"${account}\"" + config_without_values="$(grep -vE "${regex}" "${CONFIG}")" + chmod u+w "${CONFIG}" || return 1 # change perms to edit + printf "%s\n" "${config_without_values}" >| "${CONFIG}" || return 1 + chmod "a-w-r-x,u+r" "${CONFIG}" || return 1 # restore perms + "${QUIET:-_print_center}" "normal" " Successfully deleted account ( ${account} ) from config. " "-" + _reload_config # reload config if successfully deleted + else + "${QUIET:-_print_center}" "normal" " Error: Cannot delete account ( ${account} ) from config. No such account exists. " "-" 1>&2 + fi + return 0 +} + +################################################### +# handle legacy config +# this will be triggered only if old config values are present, convert to new format +# new account will be created with "default" name, if default already taken, then add a number as suffix +# Globals: 7 variables, 2 functions +# Variables - CLIENT_ID CLIENT_SECRET, REFRESH_TOKEN, ROOT_FOLDER, ROOT_FOLDER_NAME CONFIG, ACCOUNT_NAME +# Functions - _account_exists, _reload_config +# Arguments: None +################################################### +_handle_old_config() { + export CLIENT_ID CLIENT_SECRET REFRESH_TOKEN # to handle a shellcheck warning + # only try to convert the if all three values are present + [[ -n ${CLIENT_ID:+${CLIENT_SECRET:+${REFRESH_TOKEN}}} ]] && { + declare account_name="default" regex config_without_values count=0 + # first try to name the new account as default, otherwise try to add numbers as suffix + until ! _account_exists "${account_name}"; do + account_name="${account_name}$((count += 1))" + done + # form a regex expression to remove values from config, _update_config isn't used here to prevent a loop and multiple grep calls + regex="^(CLIENT_ID=|CLIENT_SECRET=|REFRESH_TOKEN=|ROOT_FOLDER=|ROOT_FOLDER_NAME=|ACCESS_TOKEN=|ACCESS_TOKEN_EXPIRY=)" + config_without_values="$(grep -vE "${regex}" "${CONFIG}")" + chmod u+w "${CONFIG}" || return 1 # change perms to edit + printf "%s\n%s\n%s\n%s\n%s\n%s\n" \ + "ACCOUNT_${account_name}_CLIENT_ID=\"${CLIENT_ID}\"" \ + "ACCOUNT_${account_name}_CLIENT_SECRET=\"${CLIENT_SECRET}\"" \ + "ACCOUNT_${account_name}_REFRESH_TOKEN=\"${REFRESH_TOKEN}\"" \ + "ACCOUNT_${account_name}_ROOT_FOLDER=\"${ROOT_FOLDER}\"" \ + "ACCOUNT_${account_name}_ROOT_FOLDER_NAME=\"${ROOT_FOLDER_NAME}\"" \ + "${config_without_values}" >| "${CONFIG}" || return 1 + + chmod "a-w-r-x,u+r" "${CONFIG}" || return 1 # restore perms + + _reload_config || return 1 # reload config file + } + return 0 +} + +################################################### +# handle old config values, new account creation, custom account name, updating default config and account +# start token service if applicable +# Globals: 12 variables, 7 functions +# Variables - DEFAULT_CONFIG, NEW_ACCOUNT_NAME, CUSTOM_ACCOUNT_NAME, DELETE_ACCOUNT_NAME, LIST_ACCOUNTS, QUIET +# UPDATE_DEFAULT_ACCOUNT, UPDATE_DEFAULT_CONFIG, CONFIG_INFO, CONTINUE_WITH_NO_INPUT +# Functions - _reload_config, _handle_old_config, _set_new_account_name, _account_exists, _all_accounts +# _check_account_credentials, _token_bg_service, _print_center, _update_config, _set_value +# Arguments: None +# Result: read description and start access token check in bg if required +################################################### +_check_credentials() { + { _reload_config && _handle_old_config; } || return 1 + # set account name to default account name + ACCOUNT_NAME="${DEFAULT_ACCOUNT}" + + if [[ -n ${NEW_ACCOUNT_NAME} ]]; then + # create new account, --create-account flag + _set_new_account_name "${NEW_ACCOUNT_NAME}" || return 1 + _check_account_credentials "${ACCOUNT_NAME}" || return 1 + else + # use custom account, --account flag + if [[ -n ${CUSTOM_ACCOUNT_NAME} ]]; then + if _account_exists "${CUSTOM_ACCOUNT_NAME}"; then + ACCOUNT_NAME="${CUSTOM_ACCOUNT_NAME}" + else + # error out in case CUSTOM_ACCOUNT_NAME is invalid + "${QUIET:-_print_center}" "normal" " Error: No such account ( ${CUSTOM_ACCOUNT_NAME} ) exists. " "-" && return 1 + fi + elif [[ -n ${DEFAULT_ACCOUNT} ]]; then + # check if default account if valid or not, else set account name to nothing and remove default account in config + _account_exists "${DEFAULT_ACCOUNT}" || { + _update_config DEFAULT_ACCOUNT "" "${CONFIG}" && unset DEFAULT_ACCOUNT ACCOUNT_NAME && UPDATE_DEFAULT_ACCOUNT="_update_config" + } + # UPDATE_DEFAULT_ACCOUNT to true so that default config is updated later + else + UPDATE_DEFAULT_ACCOUNT="_update_config" # as default account doesn't exist + fi + + # in case no account name is set at this point of script + if [[ -z ${ACCOUNT_NAME} ]]; then + # if accounts are configured but default account is not set + # COUNT comes from _all_accounts function + if _all_accounts 2>| /dev/null && [[ ${COUNT} -gt 0 ]]; then + # set ACCOUNT_NAME without asking if only one account available + if [[ ${COUNT} -eq 1 ]]; then + _set_value indirect ACCOUNT_NAME "ACC_1_ACC" # ACC_1_ACC comes from _all_accounts function + else + "${QUIET:-_print_center}" "normal" " Above accounts are configured, but default one not set. " "=" + if [[ -t 1 ]]; then + \ "${QUIET:-_print_center}" "normal" " Choose default account: " "-" + until [[ -n ${ACCOUNT_NAME} ]]; do + printf -- "-> \e[?7l" + read -r account_name + printf '\e[?7h' + if [[ ${account_name} -gt 0 && ${account_name} -le ${COUNT} ]]; then + _set_value indirect ACCOUNT_NAME "ACC_${COUNT}_ACC" + else + _clear_line 1 + fi + done + else + # if not running in a terminal then choose 1st one as default + printf "%s\n" "Warning: Script is not running in a terminal, choosing first account as default." + _set_value indirect ACCOUNT_NAME "ACC_1_ACC" # ACC_1_ACC comes from _all_accounts function + fi + fi + else + _set_new_account_name "" || return 1 + _check_account_credentials "${ACCOUNT_NAME}" || return 1 + fi + fi + _check_account_credentials "${ACCOUNT_NAME}" || return 1 + fi + + "${UPDATE_DEFAULT_ACCOUNT:-:}" DEFAULT_ACCOUNT "${ACCOUNT_NAME}" "${CONFIG}" # update default account if required + "${UPDATE_DEFAULT_CONFIG:-:}" CONFIG "${CONFIG}" "${CONFIG_INFO}" # update default config if required + + # only launch the token service if there was some input + [[ -n ${CONTINUE_WITH_NO_INPUT} ]] || _token_bg_service # launch token bg service + return 0 +} + +################################################### +# check credentials for a given account name +# Globals: 3 functions +# Functions - _check_client, _check_refresh_token, _check_access_token +# Arguments: 2 +# ${1} = Account name ( optional ) +# Result: read description, return 1 or 0 +################################################### +_check_account_credentials() { + declare account_name="${1:-}" + { + _check_client ID "${account_name}" && + _check_client SECRET "${account_name}" && + _check_refresh_token "${account_name}" && + _check_access_token "${account_name}" check + } || return 1 + return 0 +} + +################################################### +# Check client id or secret and ask if required +# Globals: 4 variables, 3 functions +# Variables - CONFIG, QUIET, CLIENT_ID_${ACCOUNT_NAME}, CLIENT_SECRET_${ACCOUNT_NAME} +# Functions - _print_center, _update_config, _set_value +# Arguments: 2 +# ${1} = ID or SECRET +# ${2} = Account name ( optional - if not given, then just CLIENT_[ID|SECRET] var is used ) +# Result: read description and export ACCOUNT_name_CLIENT_[ID|SECRET] CLIENT_[ID|SECRET] +################################################### +_check_client() { + declare type="CLIENT_${1:?Error: ID or SECRET}" account_name="${2:-}" \ + type_name type_value type_regex valid client message + export client_id_regex='[0-9]+-[0-9A-Za-z_]{32}\.apps\.googleusercontent\.com' client_secret_regex='[0-9A-Za-z_-]+' + type_name="${account_name:+ACCOUNT_${account_name}_}${type}" + + # set the type_value to the actual value of ACCOUNT_${account_name}_[ID|SECRET] + _set_value indirect type_value "${type_name}" + # set the type_regex to the actual value of client_id_regex or client_secret_regex + _set_value indirect type_regex "${type}_regex" + + until [[ -n ${type_value} && -n ${valid} ]]; do + [[ -n ${type_value} ]] && { + if [[ ${type_value} =~ ${type_regex} ]]; then + [[ -n ${client} ]] && { _update_config "${type_name}" "${type_value}" "${CONFIG}" || return 1; } + valid="true" && continue + else + { [[ -n ${client} ]] && message="- Try again"; } || message="in config ( ${CONFIG} )" + "${QUIET:-_print_center}" "normal" " Invalid Client ${1} ${message} " "-" && unset "${type_name}" client + fi + } + [[ -z ${client} ]] && printf "\n" && "${QUIET:-_print_center}" "normal" " Enter Client ${1} " "-" + [[ -n ${client} ]] && _clear_line 1 + printf -- "-> " + read -r "${type_name?}" && client=1 + _set_value indirect type_value "${type_name}" + done + + # export ACCOUNT_name_CLIENT_[ID|SECRET] + _set_value direct "${type_name}" "${type_value}" + # export CLIENT_[ID|SECRET] + _set_value direct "${type}" "${type_value}" + + return 0 +} + +################################################### +# Check refresh token and ask if required +# Globals: 8 variables, 4 functions +# Variables - CLIENT_ID, CLIENT_SECRET, REDIRECT_URI, TOKEN_URL, CONFIG, QUIET +# Functions - _set_value, _print_center, _update_config, _check_access_token +# Arguments: 1 +# ${1} = Account name ( optional - if not given, then just REFRESH_TOKEN var is used ) +# Result: read description & export REFRESH_TOKEN ACCOUNT_${account_name}_REFRESH_TOKEN +################################################### +_check_refresh_token() { + # bail out before doing anything if client id and secret is not present, unlikely to happen but just in case + [[ -z ${CLIENT_ID:+${CLIENT_SECRET}} ]] && return 1 + declare account_name="${1:-}" \ + refresh_token_regex='[0-9]//[0-9A-Za-z_-]+' authorization_code_regex='[0-9]/[0-9A-Za-z_-]+' + declare refresh_token_name="${account_name:+ACCOUNT_${account_name}_}REFRESH_TOKEN" check_error + + _set_value indirect refresh_token_value "${refresh_token_name}" + + [[ -n ${refresh_token_value} ]] && { + ! [[ ${refresh_token_value} =~ ${refresh_token_regex} ]] && + "${QUIET:-_print_center}" "normal" " Error: Invalid Refresh token in config file, follow below steps.. " "-" && unset refresh_token_value + } + + [[ -z ${refresh_token_value} ]] && { + printf "\n" && "${QUIET:-_print_center}" "normal" "If you have a refresh token generated, then type the token, else leave blank and press return key.." " " + printf "\n" && "${QUIET:-_print_center}" "normal" " Refresh Token " "-" && printf -- "-> " + read -r refresh_token_value + if [[ -n ${refresh_token_value} ]]; then + "${QUIET:-_print_center}" "normal" " Checking refresh token.. " "-" + if [[ ${refresh_token_value} =~ ${refresh_token_regex} ]]; then + _set_value direct REFRESH_TOKEN "${refresh_token_value}" + { _check_access_token "${account_name}" skip_check && + _update_config "${refresh_token_name}" "${refresh_token_value}" "${CONFIG}" && + _clear_line 1; } || check_error=true + else + check_error=true + fi + [[ -n ${check_error} ]] && "${QUIET:-_print_center}" "normal" " Error: Invalid Refresh token given, follow below steps to generate.. " "-" && unset refresh_token_value + else + "${QUIET:-_print_center}" "normal" " No Refresh token given, follow below steps to generate.. " "-" && unset refresh_token_value + fi + + [[ -z ${refresh_token_value} ]] && { + printf "\n" && "${QUIET:-_print_center}" "normal" "Visit the below URL, tap on allow and then enter the code obtained" " " + URL="https://accounts.google.com/o/oauth2/auth?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&scope=${SCOPE}&response_type=code&prompt=consent" + printf "\n%s\n" "${URL}" + declare AUTHORIZATION_CODE authorization_code AUTHORIZATION_CODE_VALID response + until [[ -n ${AUTHORIZATION_CODE} && -n ${AUTHORIZATION_CODE_VALID} ]]; do + [[ -n ${AUTHORIZATION_CODE} ]] && { + if [[ ${AUTHORIZATION_CODE} =~ ${authorization_code_regex} ]]; then + AUTHORIZATION_CODE_VALID="true" && continue + else + "${QUIET:-_print_center}" "normal" " Invalid CODE given, try again.. " "-" && unset AUTHORIZATION_CODE authorization_code + fi + } + { [[ -z ${authorization_code} ]] && printf "\n" && "${QUIET:-_print_center}" "normal" " Enter the authorization code " "-"; } || _clear_line 1 + printf -- "-> \e[?7l" + read -r AUTHORIZATION_CODE && authorization_code=1 + printf '\e[?7h' + done + response="$(curl --compressed "${CURL_PROGRESS}" -X POST \ + --data "code=${AUTHORIZATION_CODE}&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&redirect_uri=${REDIRECT_URI}&grant_type=authorization_code" "${TOKEN_URL}")" || : + _clear_line 1 1>&2 + + refresh_token_value="$(_json_value refresh_token 1 1 <<< "${response}")" || + { printf "%s\n" "Error: Cannot fetch refresh token, make sure the authorization code was correct." && return 1; } + + _set_value direct REFRESH_TOKEN "${refresh_token_value}" + { _check_access_token "${account_name}" skip_check "${response}" && + _update_config "${refresh_token_name}" "${refresh_token_value}" "${CONFIG}"; } || return 1 + } + printf "\n" + } + + # export ACCOUNT_name_REFRESH_TOKEN + _set_value direct "${refresh_token_name}" "${refresh_token_value}" + # export REFRESH_TOKEN + _set_value direct REFRESH_TOKEN "${refresh_token_value}" + + return 0 +} + +################################################### +# Check access token and create/update if required +# Also update in config +# Globals: 9 variables, 3 functions +# Variables - CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN, TOKEN_URL, CONFIG, API_URL, API_VERSION, QUIET +# Functions - _print_center, _update_config, _set_value +# Arguments: 2 +# ${1} = Account name ( optional - if not given, then just ACCESS_TOKEN var is used ) +# ${2} = if skip_check, then force create access token, else check with regex and expiry +# ${3} = json response ( optional ) +# Result: read description & export ACCESS_TOKEN ACCESS_TOKEN_EXPIRY +################################################### +_check_access_token() { + # bail out before doing anything if client id|secret or refresh token is not present, unlikely to happen but just in case + [[ -z ${CLIENT_ID:+${CLIENT_SECRET:+${REFRESH_TOKEN}}} ]] && return 1 + + declare account_name="${1:-}" no_check="${2:-false}" response_json="${3:-}" \ + token_name token_expiry_name token_value token_expiry_value response \ + access_token_regex='ya29\.[0-9A-Za-z_-]+' + declare token_name="${account_name:+ACCOUNT_${account_name}_}ACCESS_TOKEN" + declare token_expiry_name="${token_name}_EXPIRY" + + _set_value indirect token_value "${token_name}" + _set_value indirect token_expiry_value "${token_expiry_name}" + + [[ ${no_check} = skip_check || -z ${token_value} || ${token_expiry_value:-0} -lt "$(printf "%(%s)T\\n" "-1")" || ! ${token_value} =~ ${access_token_regex} ]] && { + response="${response_json:-$(curl --compressed -s -X POST --data \ + "client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&refresh_token=${REFRESH_TOKEN}&grant_type=refresh_token" "${TOKEN_URL}")}" || : + + if token_value="$(_json_value access_token 1 1 <<< "${response}")"; then + token_expiry_value="$(($(printf "%(%s)T\\n" "-1") + $(_json_value expires_in 1 1 <<< "${response}") - 1))" + _update_config "${token_name}" "${token_value}" "${CONFIG}" || return 1 + _update_config "${token_expiry_name}" "${token_expiry_value}" "${CONFIG}" || return 1 + else + "${QUIET:-_print_center}" "justify" "Error: Something went wrong" ", printing error." "=" 1>&2 + printf "%s\n" "${response}" 1>&2 + return 1 + fi + } + + # export ACCESS_TOKEN and ACCESS_TOKEN_EXPIRY + _set_value direct ACCESS_TOKEN "${token_value}" + _set_value direct ACCESS_TOKEN_EXPIRY "${token_expiry_value}" + + # export INITIAL_ACCESS_TOKEN which is used on script cleanup + _set_value direct INITIAL_ACCESS_TOKEN "${ACCESS_TOKEN}" + return 0 +} + +################################################### +# load config file if available, else create a empty file +# uses global variable CONFIG +################################################### +_reload_config() { + { [[ -r ${CONFIG} ]] && . "${CONFIG}"; } || { printf "" >> "${CONFIG}" || return 1; } + return 0 +} + +################################################### +# launch a background service to check access token and update it +# checks ACCESS_TOKEN_EXPIRY, try to update before 5 mins of expiry, a fresh token gets 60 mins +# process will be killed when script exits or "${MAIN_PID}" is killed +# Globals: 4 variables, 1 function +# Variables - ACCESS_TOKEN, ACCESS_TOKEN_EXPIRY, MAIN_PID, TMPFILE +# Functions - _check_access_token +# Arguments: None +# Result: read description & export ACCESS_TOKEN_SERVICE_PID +################################################### +_token_bg_service() { + [[ -z ${MAIN_PID} ]] && return 0 # don't start if MAIN_PID is empty + printf "%b\n" "ACCESS_TOKEN=\"${ACCESS_TOKEN}\"\nACCESS_TOKEN_EXPIRY=\"${ACCESS_TOKEN_EXPIRY}\"" >| "${TMPFILE}_ACCESS_TOKEN" + { + until ! kill -0 "${MAIN_PID}" 2>| /dev/null 1>&2; do + . "${TMPFILE}_ACCESS_TOKEN" + CURRENT_TIME="$(printf "%(%s)T\\n" "-1")" + REMAINING_TOKEN_TIME="$((ACCESS_TOKEN_EXPIRY - CURRENT_TIME))" + if [[ ${REMAINING_TOKEN_TIME} -le 300 ]]; then + # timeout after 30 seconds, it shouldn't take too long anyway, and update tmp config + CONFIG="${TMPFILE}_ACCESS_TOKEN" _timeout 30 _check_access_token "" skip_check || : + else + TOKEN_PROCESS_TIME_TO_SLEEP="$(if [[ ${REMAINING_TOKEN_TIME} -le 301 ]]; then + printf "0\n" + else + printf "%s\n" "$((REMAINING_TOKEN_TIME - 300))" + fi)" + sleep "${TOKEN_PROCESS_TIME_TO_SLEEP}" + fi + sleep 1 + done + } & + export ACCESS_TOKEN_SERVICE_PID="${!}" + return 0 +} + +ALL_FUNCTIONS=(_account_name_valid + _account_exists + _all_accounts + _set_new_account_name + _delete_account + _handle_old_config + _check_credentials + _check_account_credentials + _check_client + _check_refresh_token + _check_access_token + _reload_config) +export -f "${ALL_FUNCTIONS[@]}" + ################################################### # Search for an existing file on gdrive with write permission. # Globals: 3 variables, 2 functions @@ -530,28 +1076,6 @@ _extract_id() { printf "%b" "${ID:+${ID}\n}" } -################################################### -# Method to regenerate access_token ( also updates in config ). -# Make a request on https://www.googleapis.com/oauth2/""${API_VERSION}""/tokeninfo?access_token=${ACCESS_TOKEN} url and check if the given token is valid, if not generate one. -# Globals: 9 variables, 2 functions -# Variables - CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN, TOKEN_URL, CONFIG, API_URL, API_VERSION, QUIET, NO_UPDATE_TOKEN -# Functions - _update_config and _print_center -# Result: Update access_token and expiry else print error -################################################### -_get_access_token_and_update() { - RESPONSE="${1:-$(curl --compressed -s -X POST --data "client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&refresh_token=${REFRESH_TOKEN}&grant_type=refresh_token" "${TOKEN_URL}")}" || : - if ACCESS_TOKEN="$(_json_value access_token 1 1 <<< "${RESPONSE}")"; then - ACCESS_TOKEN_EXPIRY="$(($(printf "%(%s)T\\n" "-1") + $(_json_value expires_in 1 1 <<< "${RESPONSE}") - 1))" - _update_config ACCESS_TOKEN "${ACCESS_TOKEN}" "${CONFIG}" - _update_config ACCESS_TOKEN_EXPIRY "${ACCESS_TOKEN_EXPIRY}" "${CONFIG}" - else - "${QUIET:-_print_center}" "justify" "Error: Something went wrong" ", printing error." "=" 1>&2 - printf "%s\n" "${RESPONSE}" 1>&2 - return 1 - fi - return 0 -} - ################################################### # Upload ( Create/Update ) files on gdrive. # Interrupted uploads can be resumed. @@ -771,6 +1295,21 @@ _share_id() { { _json_value id 1 1 <<< "${share_response}" 2>| /dev/null 1>&2 && return 0; } || { printf "%s\n" "Error: Cannot Share." 1>&2 && printf "%s\n" "${share_response}" 1>&2 && return 1; } } + +ALL_FUNCTIONS=(_check_existing_file + _clone_file + _create_directory + _drive_info + _extract_id + _upload_file + _generate_upload_link + _upload_file_from_uri + _normal_logging_upload + _log_upload_session + _remove_upload_session + _full_upload + _share_id) +export -f "${ALL_FUNCTIONS[@]}" # shellcheck source=/dev/null ################################################### @@ -893,7 +1432,7 @@ _upload_file_main() { _upload_folder() { [[ $# -lt 3 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 declare mode="${1}" files="${3}" && PARSE_MODE="${2}" ID="${4:-}" && export PARSE_MODE ID - unset SUCCESS_STATUS SUCCESS_FILES ERROR_STATUS ERROR_FILES + SUCCESS_STATUS=0 SUCCESS_FILES="" ERROR_STATUS=0 ERROR_FILES="" case "${mode}" in normal) [[ ${PARSE_MODE} = parse ]] && _clear_line 1 && _newline "\n" @@ -916,7 +1455,7 @@ _upload_folder() { [[ -f "${TMPFILE}"ERROR ]] && rm "${TMPFILE}"ERROR # shellcheck disable=SC2016 - printf "%s\n" "${files}" | xargs -n1 -P"${NO_OF_PARALLEL_JOBS_FINAL}" -I {} bash -c ' + printf "%s\n" "${files}" | xargs -P"${NO_OF_PARALLEL_JOBS_FINAL}" -I "{}" -n 1 bash -c ' _upload_file_main "${PARSE_MODE}" "{}" "${ID}" true ' 1>| "${TMPFILE}"SUCCESS 2>| "${TMPFILE}"ERROR & pid="${!}" @@ -939,6 +1478,14 @@ _upload_folder() { esac return 0 } + +ALL_FUNCTIONS=(_api_request + _collect_file_info + _error_logging_upload + _get_rootdir_id + _upload_file_main + _upload_folder) +export -f "${ALL_FUNCTIONS[@]}" # Upload a file to Google Drive # shellcheck source=/dev/null @@ -949,6 +1496,11 @@ Usage:\n ${0##*/} [options.. ] \n Foldername argument is optional. If not provided, the file will be uploaded to preconfigured google drive.\n File name argument is optional if create directory option is used.\n Options:\n + -a | --account 'account name' - Use different account than the default one.\n + To change the default account name, use this format, -a/--account default=account_name\n + -la | --list-accounts - Print all configured accounts in the config files.\n + -ca | --create-account 'account name' - To create a new account with the given name if does not already exists.\n + -da | --delete-account 'account name' - To delete an account information from config file. \n -c | -C | --create-dir - option to create directory. Will provide folder id. Can be used to provide input folder, see README.\n -r | --root-dir or - google folder ID/URL to which the file/directory is going to upload. If you want to change the default value, then use this format, -r/--root-dir default=root_folder_id/root_folder_url\n @@ -1049,21 +1601,22 @@ _setup_arguments() { [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 # Internal variables # De-initialize if any variables set already. - unset FOLDERNAME LOCAL_INPUT_ARRAY ID_INPUT_ARRAY + unset LIST_ACCOUNTS UPDATE_DEFAULT_ACCOUNT CUSTOM_ACCOUNT_NAME NEW_ACCOUNT_NAME DELETE_ACCOUNT_NAME ACCOUNT_ONLY_RUN + unset FOLDERNAME LOCAL_INPUT_ARRAY ID_INPUT_ARRAY CONTINUE_WITH_NO_INPUT unset PARALLEL NO_OF_PARALLEL_JOBS SHARE SHARE_EMAIL OVERWRITE SKIP_DUPLICATES SKIP_SUBDIRS ROOTDIR QUIET unset VERBOSE VERBOSE_PROGRESS DEBUG LOG_FILE_ID CURL_SPEED RETRY - CURL_PROGRESS="-s" EXTRA_LOG=":" CURL_PROGRESS_EXTRA="-s" + export CURL_PROGRESS="-s" EXTRA_LOG=":" CURL_PROGRESS_EXTRA="-s" INFO_PATH="${HOME}/.google-drive-upload" CONFIG_INFO="${INFO_PATH}/google-drive-upload.configpath" [[ -f ${CONFIG_INFO} ]] && . "${CONFIG_INFO}" CONFIG="${CONFIG:-${HOME}/.googledrive.conf}" # Configuration variables # Remote gDrive variables - unset ROOT_FOLDER CLIENT_ID CLIENT_SECRET REFRESH_TOKEN ACCESS_TOKEN - API_URL="https://www.googleapis.com" - API_VERSION="v3" - SCOPE="${API_URL}/auth/drive" - REDIRECT_URI="urn:ietf:wg:oauth:2.0:oob" - TOKEN_URL="https://accounts.google.com/o/oauth2/token" + unset ROOT_FOLDER ROOT_FOLDER_NAME CLIENT_ID CLIENT_SECRET REFRESH_TOKEN ACCESS_TOKEN + export API_URL="https://www.googleapis.com" + export API_VERSION="v3" \ + SCOPE="${API_URL}/auth/drive" \ + REDIRECT_URI="urn:ietf:wg:oauth:2.0:oob" \ + TOKEN_URL="https://accounts.google.com/o/oauth2/token" _check_config() { [[ ${1} = default* ]] && export UPDATE_DEFAULT_CONFIG="_update_config" @@ -1085,6 +1638,21 @@ _setup_arguments() { -h | --help) _usage ;; -D | --debug) DEBUG="true" && export DEBUG ;; --info) _version_info ;; + -a | --account) + _check_longoptions "${1}" "${2}" + export CUSTOM_ACCOUNT_NAME="${2/default=/}" && shift + [[ ${2} = default* ]] && export UPDATE_DEFAULT_ACCOUNT="_update_config" + ;; + -la | --list-account) export LIST_ACCOUNTS="true" ;; + # this flag is preferred over --account + -ca | --create-account) + _check_longoptions "${1}" "${2}" + export NEW_ACCOUNT_NAME="${2}" && shift + ;; + -da | --delete-account) + _check_longoptions "${1}" "${2}" + export DELETE_ACCOUNT_NAME="${2}" && shift + ;; -c | -C | --create-dir) _check_longoptions "${1}" "${2}" FOLDERNAME="${2}" && shift @@ -1102,9 +1670,9 @@ _setup_arguments() { ;; -i | --save-info) _check_longoptions "${1}" "${2}" - LOG_FILE_ID="${2}" && shift + export LOG_FILE_ID="${2}" && shift ;; - -s | --skip-subdirs) SKIP_SUBDIRS="true" ;; + -s | --skip-subdirs) export SKIP_SUBDIRS="true" ;; -p | --parallel) _check_longoptions "${1}" "${2}" NO_OF_PARALLEL_JOBS="${2}" @@ -1114,10 +1682,10 @@ _setup_arguments() { printf "\nError: -p/--parallel value ranges between 1 to 10.\n" exit 1 fi - PARALLEL_UPLOAD="parallel" && shift + export PARALLEL_UPLOAD="parallel" && shift ;; - -o | --overwrite) OVERWRITE="Overwrite" && UPLOAD_MODE="update" ;; - -d | --skip-duplicates) SKIP_DUPLICATES="Skip Existing" && UPLOAD_MODE="update" ;; + -o | --overwrite) export OVERWRITE="Overwrite" UPLOAD_MODE="update" ;; + -d | --skip-duplicates) export SKIP_DUPLICATES="Skip Existing" UPLOAD_MODE="update" ;; -f | --file | --folder) _check_longoptions "${1}" "${2}" LOCAL_INPUT_ARRAY+=("${2}") && shift @@ -1133,12 +1701,13 @@ _setup_arguments() { ! [[ ${SHARE_EMAIL} =~ ${EMAIL_REGEX} ]] && printf "\nError: Provided email address for share option is invalid.\n" && exit 1 shift } + export SHARE_EMAIL ;; --speed) _check_longoptions "${1}" "${2}" regex='^([0-9]+)([k,K]|[m,M]|[g,G])+$' if [[ ${2} =~ ${regex} ]]; then - CURL_SPEED="--limit-rate ${2}" && shift + export CURL_SPEED="--limit-rate ${2}" && shift else printf "Error: Wrong speed limit format, supported formats: 1K , 1M and 1G\n" 1>&2 exit 1 @@ -1147,7 +1716,7 @@ _setup_arguments() { -R | --retry) _check_longoptions "${1}" "${2}" if [[ ${2} -gt 0 ]]; then - RETRY="${2}" && shift + export RETRY="${2}" && shift else printf "Error: -R/--retry only takes positive integers as arguments, min = 1, max = infinity.\n" exit 1 @@ -1162,9 +1731,9 @@ _setup_arguments() { EXCLUDE_FILES="${EXCLUDE_FILES} ! -name '${2}' " && shift ;; --hide) HIDE_INFO=":" ;; - -q | --quiet) QUIET="_print_center_quiet" ;; - -v | --verbose) VERBOSE="true" ;; - -V | --verbose-progress) VERBOSE_PROGRESS="true" ;; + -q | --quiet) export QUIET="_print_center_quiet" ;; + -v | --verbose) export VERBOSE="true" ;; + -V | --verbose-progress) export VERBOSE_PROGRESS="true" ;; --skip-internet-check) SKIP_INTERNET_CHECK=":" ;; '') shorthelp ;; *) # Check if user meant it to be a flag @@ -1195,8 +1764,11 @@ _setup_arguments() { _check_debug - [[ -n ${VERBOSE_PROGRESS} ]] && unset VERBOSE && CURL_PROGRESS="" - [[ -n ${QUIET} ]] && CURL_PROGRESS="-s" + [[ -n ${VERBOSE_PROGRESS} ]] && unset VERBOSE && export CURL_PROGRESS="" + [[ -n ${QUIET} ]] && export CURL_PROGRESS="-s" + + # create info path folder, can be missing if gupload was not installed with install.sh + mkdir -p "${INFO_PATH}" unset Aseen && declare -A Aseen for input in "${LOCAL_INPUT_ARRAY[@]}"; do @@ -1207,162 +1779,34 @@ _setup_arguments() { } done - # If no input, then check if -C option was used or not. - [[ -z ${FINAL_LOCAL_INPUT_ARRAY[*]:-${FINAL_ID_INPUT_ARRAY[*]:-${FOLDERNAME}}} ]] && _short_help - - # create info path folder, can be missing if gupload was not installed with install.sh - mkdir -p "${INFO_PATH}" - - return 0 -} - -################################################### -# Check Oauth credentials and create/update config file -# Client ID, Client Secret, Refesh Token and Access Token -# Globals: 10 variables, 3 functions -# Variables - API_URL, API_VERSION, TOKEN URL, -# CONFIG, UPDATE_DEFAULT_CONFIG, INFO_PATH, -# CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN and ACCESS_TOKEN -# Functions - _update_config, _update_value, _json_value and _print_center -# Arguments: None -# Result: read description -################################################### -_check_credentials() { - # Config file is created automatically after first run - [[ -r ${CONFIG} ]] && . "${CONFIG}" - "${UPDATE_DEFAULT_CONFIG:-:}" CONFIG "${CONFIG}" "${CONFIG_INFO}" - - ! [[ -t 1 ]] && [[ -z ${CLIENT_ID:+${CLIENT_SECRET:+${REFRESH_TOKEN}}} ]] && { - printf "%s\n" "Error: Script is not running in a terminal, cannot ask for credentials." - printf "%s\n" "Add in config manually if terminal is not accessible. CLIENT_ID, CLIENT_SECRET and REFRESH_TOKEN is required." && return 1 - } - - # Following https://developers.google.com/identity/protocols/oauth2#size - CLIENT_ID_REGEX='[0-9]+-[0-9A-Za-z_]{32}\.apps\.googleusercontent\.com' - CLIENT_SECRET_REGEX='[0-9A-Za-z_-]+' - REFRESH_TOKEN_REGEX='[0-9]//[0-9A-Za-z_-]+' # 512 bytes - ACCESS_TOKEN_REGEX='ya29\.[0-9A-Za-z_-]+' # 2048 bytes - AUTHORIZATION_CODE_REGEX='[0-9]/[0-9A-Za-z_-]+' # 256 bytes - - until [[ -n ${CLIENT_ID} && -n ${CLIENT_ID_VALID} ]]; do - [[ -n ${CLIENT_ID} ]] && { - if [[ ${CLIENT_ID} =~ ${CLIENT_ID_REGEX} ]]; then - [[ -n ${client_id} ]] && _update_config CLIENT_ID "${CLIENT_ID}" "${CONFIG}" - CLIENT_ID_VALID="true" && continue - else - { [[ -n ${client_id} ]] && message="- Try again"; } || message="in config ( ${CONFIG} )" - "${QUIET:-_print_center}" "normal" " Invalid Client ID ${message} " "-" && unset CLIENT_ID client_id - fi - } - [[ -z ${client_id} ]] && printf "\n" && "${QUIET:-_print_center}" "normal" " Enter Client ID " "-" - [[ -n ${client_id} ]] && _clear_line 1 - printf -- "-> " - read -r CLIENT_ID && client_id=1 - done - - until [[ -n ${CLIENT_SECRET} && -n ${CLIENT_SECRET_VALID} ]]; do - [[ -n ${CLIENT_SECRET} ]] && { - if [[ ${CLIENT_SECRET} =~ ${CLIENT_SECRET_REGEX} ]]; then - [[ -n ${client_secret} ]] && _update_config CLIENT_SECRET "${CLIENT_SECRET}" "${CONFIG}" - CLIENT_SECRET_VALID="true" && continue - else - { [[ -n ${client_secret} ]] && message="- Try again"; } || message="in config ( ${CONFIG} )" - "${QUIET:-_print_center}" "normal" " Invalid Client Secret ${message} " "-" && unset CLIENT_SECRET client_secret - fi - } - [[ -z ${client_secret} ]] && printf "\n" && "${QUIET:-_print_center}" "normal" " Enter Client Secret " "-" - [[ -n ${client_secret} ]] && _clear_line 1 - printf -- "-> " - read -r CLIENT_SECRET && client_secret=1 - done - - [[ -n ${REFRESH_TOKEN} ]] && { - ! [[ ${REFRESH_TOKEN} =~ ${REFRESH_TOKEN_REGEX} ]] && - "${QUIET:-_print_center}" "normal" " Error: Invalid Refresh token in config file, follow below steps.. " "-" && unset REFRESH_TOKEN + # handle account related flags here as we want to use the flags independenlty even with no normal valid inputs + # delete account, --delete-account flag + # TODO: add support for deleting multiple accounts + [[ -n ${DELETE_ACCOUNT_NAME} ]] && _delete_account "${DELETE_ACCOUNT_NAME}" + # list all configured accounts, --list-accounts flag + [[ -n ${LIST_ACCOUNTS} ]] && _all_accounts + + # If no input, then check if either -C option was used. + [[ -z ${FINAL_LOCAL_INPUT_ARRAY[*]:-${FINAL_ID_INPUT_ARRAY[*]:-${FOLDERNAME:-}}} ]] && { + # if any account related option was used then don't show short help + [[ -z ${DELETE_ACCOUNT_NAME:-${LIST_ACCOUNTS:-${NEW_ACCOUNT_NAME}}} ]] && _short_help + # exit right away if --list-accounts or --delete-account flag was used + [[ -n ${DELETE_ACCOUNT_NAME:-${LIST_ACCOUNTS:-}} ]] && exit 0 + # don't exit right away when new account is created but also let the rootdir stuff execute + [[ -n ${NEW_ACCOUNT_NAME} ]] && CONTINUE_WITH_NO_INPUT="true" } - [[ -z ${REFRESH_TOKEN} ]] && { - printf "\n" && "${QUIET:-_print_center}" "normal" "If you have a refresh token generated, then type the token, else leave blank and press return key.." " " - printf "\n" && "${QUIET:-_print_center}" "normal" " Refresh Token " "-" && printf -- "-> " - read -r REFRESH_TOKEN - if [[ -n ${REFRESH_TOKEN} ]]; then - "${QUIET:-_print_center}" "normal" " Checking refresh token.. " "-" - if [[ ${REFRESH_TOKEN} =~ ${REFRESH_TOKEN_REGEX} ]]; then - { _get_access_token_and_update && _update_config REFRESH_TOKEN "${REFRESH_TOKEN}" "${CONFIG}"; } || check_error=true - else - check_error=true - fi - [[ -n ${check_error} ]] && "${QUIET:-_print_center}" "normal" " Error: Invalid Refresh token given, follow below steps to generate.. " "-" && unset REFRESH_TOKEN - else - "${QUIET:-_print_center}" "normal" " No Refresh token given, follow below steps to generate.. " "-" - fi - - [[ -z ${REFRESH_TOKEN} ]] && { - printf "\n" && "${QUIET:-_print_center}" "normal" "Visit the below URL, tap on allow and then enter the code obtained" " " - URL="https://accounts.google.com/o/oauth2/auth?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&scope=${SCOPE}&response_type=code&prompt=consent" - printf "\n%s\n" "${URL}" - until [[ -n ${AUTHORIZATION_CODE} && -n ${AUTHORIZATION_CODE_VALID} ]]; do - [[ -n ${AUTHORIZATION_CODE} ]] && { - if [[ ${AUTHORIZATION_CODE} =~ ${AUTHORIZATION_CODE_REGEX} ]]; then - AUTHORIZATION_CODE_VALID="true" && continue - else - "${QUIET:-_print_center}" "normal" " Invalid CODE given, try again.. " "-" && unset AUTHORIZATION_CODE authorization_code - fi - } - { [[ -z ${authorization_code} ]] && printf "\n" && "${QUIET:-_print_center}" "normal" " Enter the authorization code " "-"; } || _clear_line 1 - printf -- "-> " - read -r AUTHORIZATION_CODE && authorization_code=1 - done - RESPONSE="$(curl --compressed "${CURL_PROGRESS}" -X POST \ - --data "code=${AUTHORIZATION_CODE}&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&redirect_uri=${REDIRECT_URI}&grant_type=authorization_code" "${TOKEN_URL}")" || : - _clear_line 1 1>&2 - - REFRESH_TOKEN="$(_json_value refresh_token 1 1 <<< "${RESPONSE}" || :)" - { _get_access_token_and_update "${RESPONSE}" && _update_config REFRESH_TOKEN "${REFRESH_TOKEN}" "${CONFIG}"; } || return 1 - } - printf "\n" - } - - [[ -z ${ACCESS_TOKEN} || ${ACCESS_TOKEN_EXPIRY:-0} -lt "$(printf "%(%s)T\\n" "-1")" ]] || ! [[ ${ACCESS_TOKEN} =~ ${ACCESS_TOKEN_REGEX} ]] && - { _get_access_token_and_update || return 1; } - printf "%b\n" "ACCESS_TOKEN=\"${ACCESS_TOKEN}\"\nACCESS_TOKEN_EXPIRY=\"${ACCESS_TOKEN_EXPIRY}\"" >| "${TMPFILE}_ACCESS_TOKEN" - - # launch a background service to check access token and update it - # checks ACCESS_TOKEN_EXPIRY, try to update before 5 mins of expiry, a fresh token gets 60 mins - # process will be killed when script exits or "${MAIN_PID}" is killed - { - until ! kill -0 "${MAIN_PID}" 2>| /dev/null 1>&2; do - . "${TMPFILE}_ACCESS_TOKEN" - CURRENT_TIME="$(printf "%(%s)T\\n" "-1")" - REMAINING_TOKEN_TIME="$((ACCESS_TOKEN_EXPIRY - CURRENT_TIME))" - if [[ ${REMAINING_TOKEN_TIME} -le 300 ]]; then - # timeout after 30 seconds, it shouldn't take too long anyway, and update tmp config - CONFIG="${TMPFILE}_ACCESS_TOKEN" _timeout 30 _get_access_token_and_update || : - else - TOKEN_PROCESS_TIME_TO_SLEEP="$(if [[ ${REMAINING_TOKEN_TIME} -le 301 ]]; then - printf "0\n" - else - printf "%s\n" "$((REMAINING_TOKEN_TIME - 300))" - fi)" - sleep "${TOKEN_PROCESS_TIME_TO_SLEEP}" - fi - sleep 1 - done - } & - ACCESS_TOKEN_SERVICE_PID="${!}" - return 0 } ################################################### # Setup root directory where all file/folders will be uploaded/updated -# Globals: 5 variables, 5 functions +# Globals: 5 variables, 6 functions # Variables - ROOTDIR, ROOT_FOLDER, UPDATE_DEFAULT_ROOTDIR, CONFIG, QUIET -# Functions - _print_center, _drive_info, _extract_id, _update_config, _json_value -# Arguments: 1 -# ${1} = Positive integer ( amount of time in seconds to sleep ) +# Functions - _print_center, _drive_info, _extract_id, _update_config, _json_value, _set_value +# Arguments: None # Result: read description -# If root id not found then pribt message and exit +# If root id not found then print message and exit # Update config with root id and root id name if specified # Reference: # https://github.com/dylanaraps/pure-bash-bible#use-read-as-an-alternative-to-the-sleep-command @@ -1378,29 +1822,32 @@ _setup_root_dir() { return 1 fi ROOT_FOLDER="${rootid}" - "${1:-:}" ROOT_FOLDER "${ROOT_FOLDER}" "${CONFIG}" + "${1:-:}" "ACCOUNT_${ACCOUNT_NAME}_ROOT_FOLDER" "${ROOT_FOLDER}" "${CONFIG}" || return 1 return 0 } _check_root_id_name() { ROOT_FOLDER_NAME="$(_drive_info "$(_extract_id "${ROOT_FOLDER}")" "name" | _json_value name || :)" - "${1:-:}" ROOT_FOLDER_NAME "${ROOT_FOLDER_NAME}" "${CONFIG}" + "${1:-:}" "ACCOUNT_${ACCOUNT_NAME}_ROOT_FOLDER_NAME" "${ROOT_FOLDER_NAME}" "${CONFIG}" || return 1 return 0 } + _set_value indirect ROOT_FOLDER "ACCOUNT_${ACCOUNT_NAME}_ROOT_FOLDER" + _set_value indirect ROOT_FOLDER_NAME "ACCOUNT_${ACCOUNT_NAME}_ROOT_FOLDER_NAME" + if [[ -n ${ROOTDIR:-} ]]; then ROOT_FOLDER="${ROOTDIR}" && { _check_root_id "${UPDATE_DEFAULT_ROOTDIR}" || return 1; } && unset ROOT_FOLDER_NAME elif [[ -z ${ROOT_FOLDER} ]]; then { [[ -t 1 ]] && "${QUIET:-_print_center}" "normal" "Enter root folder ID or URL, press enter for default ( root )" " " && printf -- "-> " && read -r ROOT_FOLDER && [[ -n ${ROOT_FOLDER} ]] && { _check_root_id _update_config || return 1; }; } || { ROOT_FOLDER="root" - _update_config ROOT_FOLDER "${ROOT_FOLDER}" "${CONFIG}" + _update_config "ACCOUNT_${ACCOUNT_NAME}_ROOT_FOLDER" "${ROOT_FOLDER}" "${CONFIG}" || return 1 } elif [[ -z ${ROOT_FOLDER_NAME} ]]; then - _check_root_id_name _update_config # update default root folder name if not available + _check_root_id_name _update_config || return 1 # update default root folder name if not available fi # fetch root folder name if rootdir different than default - [[ -z ${ROOT_FOLDER_NAME} ]] && _check_root_id_name "${UPDATE_DEFAULT_ROOTDIR}" + [[ -z ${ROOT_FOLDER_NAME} ]] && { _check_root_id_name "${UPDATE_DEFAULT_ROOTDIR}" || return 1; } return 0 } @@ -1444,14 +1891,6 @@ _setup_workspace() { # Result: Upload/Clone all the input files/folders, if a folder is empty, print Error message. ################################################### _process_arguments() { - export API_URL API_VERSION TOKEN_URL ACCESS_TOKEN \ - LOG_FILE_ID OVERWRITE UPLOAD_MODE SKIP_DUPLICATES CURL_SPEED RETRY UTILS_FOLDER TMPFILE \ - QUIET VERBOSE VERBOSE_PROGRESS CURL_PROGRESS CURL_PROGRESS_EXTRA CURL_PROGRESS_EXTRA_CLEAR COLUMNS EXTRA_LOG PARALLEL_UPLOAD - - export -f _bytes_to_human _dirname _json_value _url_encode _support_ansi_escapes _newline _print_center_quiet _print_center _clear_line \ - _api_request _get_access_token_and_update _check_existing_file _upload_file _upload_file_main _clone_file _collect_file_info _generate_upload_link _upload_file_from_uri _full_upload \ - _normal_logging_upload _error_logging_upload _log_upload_session _remove_upload_session _upload_folder _share_id _get_rootdir_id - # on successful uploads _share_and_print_link() { "${SHARE:-:}" "${1:-}" "${SHARE_EMAIL}" @@ -1476,8 +1915,8 @@ _process_arguments() { for _ in 1 2; do _clear_line 1; done && continue fi elif [[ -d ${input} ]]; then - input="$(cd "${input}" && pwd)" # to handle _dirname when current directory (.) is given as input. - unset EMPTY # Used when input folder is empty + input="$(cd "${input}" && pwd)" || return 1 # to handle _dirname when current directory (.) is given as input. + unset EMPTY # Used when input folder is empty _print_center "justify" "Given Input" ": FOLDER" "-" _print_center "justify" "Upload Method" ": ${SKIP_DUPLICATES:-${OVERWRITE:-Create}}" "=" && _newline "\n" @@ -1498,8 +1937,6 @@ _process_arguments() { mapfile -t FILENAMES <<< "$(_tmp='find "'${input}'" -type f -name "*" '${INCLUDE_FILES}' '${EXCLUDE_FILES}'' && eval "${_tmp}")" _clear_line 1 - ERROR_STATUS=0 SUCCESS_STATUS=0 - # Skip the sub folders and find recursively all the files and upload them. if [[ -n ${SKIP_SUBDIRS} ]]; then if [[ -n ${FILENAMES[0]} ]]; then @@ -1627,30 +2064,32 @@ main() { [[ $# = 0 ]] && _short_help [[ -z ${SELF_SOURCE} ]] && { - UTILS_FOLDER="${UTILS_FOLDER:-${PWD}}" - { . "${UTILS_FOLDER}"/common-utils.bash && . "${UTILS_FOLDER}"/drive-utils.bash && . "${UTILS_FOLDER}"/upload-utils.bash; } || + export UTILS_FOLDER="${UTILS_FOLDER:-${PWD}}" + { . "${UTILS_FOLDER}"/auth-utils.bash && . "${UTILS_FOLDER}"/common-utils.bash && . "${UTILS_FOLDER}"/drive-utils.bash && . "${UTILS_FOLDER}"/upload-utils.bash; } || { printf "Error: Unable to source util files.\n" && exit 1; } } _check_bash_version && set -o errexit -o noclobber -o pipefail - _setup_arguments "${@}" - "${SKIP_INTERNET_CHECK:-_check_internet}" + _setup_arguments "${@}" || exit 1 + "${SKIP_INTERNET_CHECK:-_check_internet}" || exit 1 - { command -v mktemp 1>| /dev/null && TMPFILE="$(mktemp -u)"; } || TMPFILE="${PWD}/.$(_t="$(printf "%(%s)T\\n" "-1")" && printf "%s\n" "$((_t * _t))").LOG" + { { command -v mktemp 1>| /dev/null && TMPFILE="$(mktemp -u)"; } || + TMPFILE="${PWD}/.$(_t="$(printf "%(%s)T\\n" "-1")" && printf "%s\n" "$((_t * _t))").LOG"; } || exit 1 + export TMPFILE _cleanup() { # unhide the cursor if hidden - [[ -n ${SUPPORT_ANSI_ESCAPES} ]] && printf "\e[?25h" + [[ -n ${SUPPORT_ANSI_ESCAPES} ]] && printf "\e[?25h\e[?7h" { [[ -f ${TMPFILE}_ACCESS_TOKEN ]] && { # update the config with latest ACCESS_TOKEN and ACCESS_TOKEN_EXPIRY only if changed . "${TMPFILE}_ACCESS_TOKEN" [[ ${INITIAL_ACCESS_TOKEN} = "${ACCESS_TOKEN}" ]] || { - _update_config ACCESS_TOKEN "${ACCESS_TOKEN}" "${CONFIG}" - _update_config ACCESS_TOKEN_EXPIRY "${ACCESS_TOKEN_EXPIRY}" "${CONFIG}" + _update_config "ACCOUNT_${ACCOUNT_NAME}_ACCESS_TOKEN" "${ACCESS_TOKEN}" "${CONFIG}" + _update_config "ACCOUNT_${ACCOUNT_NAME}_ACCESS_TOKEN_EXPIRY" "${ACCESS_TOKEN_EXPIRY}" "${CONFIG}" } - } 1>| /dev/null + } || : 1>| /dev/null # grab all chidren processes of access token service # https://askubuntu.com/a/512872 @@ -1658,7 +2097,7 @@ main() { token_service_pids="$(ps --ppid="${ACCESS_TOKEN_SERVICE_PID}" -o pid=)" # first kill parent id, then children processes kill "${ACCESS_TOKEN_SERVICE_PID}" - } 1>| /dev/null + } || : 1>| /dev/null # grab all script children pids script_children_pids="$(ps --ppid="${MAIN_PID}" -o pid=)" @@ -1686,18 +2125,20 @@ main() { export MAIN_PID="$$" START="$(printf "%(%s)T\\n" "-1")" - "${EXTRA_LOG}" "justify" "Starting script" "-" "${EXTRA_LOG}" "justify" "Checking credentials.." "-" - { _check_credentials && for _ in 1 2; do _clear_line 1; done; } || + { _check_credentials && _clear_line 1; } || { "${QUIET:-_print_center}" "normal" "[ Error: Credentials checking failed ]" "=" && exit 1; } - _print_center "justify" "Required credentials available." "=" + "${QUIET:-_print_center}" "normal" " Account: ${ACCOUNT_NAME} " "=" - "${EXTRA_LOG}" "justify" "Checking root dir and workspace folder.." "-" - { _setup_root_dir && for _ in 1 2; do _clear_line 1; done; } || + "${EXTRA_LOG}" "justify" "Checking root dir.." "-" + { _setup_root_dir && _clear_line 1; } || { "${QUIET:-_print_center}" "normal" "[ Error: Rootdir setup failed ]" "=" && exit 1; } _print_center "justify" "Root dir properly configured." "=" + # only execute next blocks if there was some input + [[ -n ${CONTINUE_WITH_NO_INPUT} ]] && exit 0 + "${EXTRA_LOG}" "justify" "Checking Workspace Folder.." "-" { _setup_workspace && for _ in 1 2; do _clear_line 1; done; } || { "${QUIET:-_print_center}" "normal" "[ Error: Workspace setup failed ]" "=" && exit 1; } diff --git a/bash/upload-utils.bash b/bash/upload-utils.bash index d8a5d2b..3e5121c 100755 --- a/bash/upload-utils.bash +++ b/bash/upload-utils.bash @@ -121,7 +121,7 @@ _upload_file_main() { _upload_folder() { [[ $# -lt 3 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 declare mode="${1}" files="${3}" && PARSE_MODE="${2}" ID="${4:-}" && export PARSE_MODE ID - unset SUCCESS_STATUS SUCCESS_FILES ERROR_STATUS ERROR_FILES + SUCCESS_STATUS=0 SUCCESS_FILES="" ERROR_STATUS=0 ERROR_FILES="" case "${mode}" in normal) [[ ${PARSE_MODE} = parse ]] && _clear_line 1 && _newline "\n" @@ -144,7 +144,7 @@ _upload_folder() { [[ -f "${TMPFILE}"ERROR ]] && rm "${TMPFILE}"ERROR # shellcheck disable=SC2016 - printf "%s\n" "${files}" | xargs -n1 -P"${NO_OF_PARALLEL_JOBS_FINAL}" -I {} bash -c ' + printf "%s\n" "${files}" | xargs -P"${NO_OF_PARALLEL_JOBS_FINAL}" -I "{}" -n 1 bash -c ' _upload_file_main "${PARSE_MODE}" "{}" "${ID}" true ' 1>| "${TMPFILE}"SUCCESS 2>| "${TMPFILE}"ERROR & pid="${!}" @@ -167,3 +167,11 @@ _upload_folder() { esac return 0 } + +ALL_FUNCTIONS=(_api_request + _collect_file_info + _error_logging_upload + _get_rootdir_id + _upload_file_main + _upload_folder) +export -f "${ALL_FUNCTIONS[@]}" diff --git a/bash/upload.bash b/bash/upload.bash index 30b30dc..83aed2b 100755 --- a/bash/upload.bash +++ b/bash/upload.bash @@ -9,6 +9,11 @@ Usage:\n ${0##*/} [options.. ] \n Foldername argument is optional. If not provided, the file will be uploaded to preconfigured google drive.\n File name argument is optional if create directory option is used.\n Options:\n + -a | --account 'account name' - Use different account than the default one.\n + To change the default account name, use this format, -a/--account default=account_name\n + -la | --list-accounts - Print all configured accounts in the config files.\n + -ca | --create-account 'account name' - To create a new account with the given name if does not already exists.\n + -da | --delete-account 'account name' - To delete an account information from config file. \n -c | -C | --create-dir - option to create directory. Will provide folder id. Can be used to provide input folder, see README.\n -r | --root-dir or - google folder ID/URL to which the file/directory is going to upload. If you want to change the default value, then use this format, -r/--root-dir default=root_folder_id/root_folder_url\n @@ -109,21 +114,22 @@ _setup_arguments() { [[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1 # Internal variables # De-initialize if any variables set already. - unset FOLDERNAME LOCAL_INPUT_ARRAY ID_INPUT_ARRAY + unset LIST_ACCOUNTS UPDATE_DEFAULT_ACCOUNT CUSTOM_ACCOUNT_NAME NEW_ACCOUNT_NAME DELETE_ACCOUNT_NAME ACCOUNT_ONLY_RUN + unset FOLDERNAME LOCAL_INPUT_ARRAY ID_INPUT_ARRAY CONTINUE_WITH_NO_INPUT unset PARALLEL NO_OF_PARALLEL_JOBS SHARE SHARE_EMAIL OVERWRITE SKIP_DUPLICATES SKIP_SUBDIRS ROOTDIR QUIET unset VERBOSE VERBOSE_PROGRESS DEBUG LOG_FILE_ID CURL_SPEED RETRY - CURL_PROGRESS="-s" EXTRA_LOG=":" CURL_PROGRESS_EXTRA="-s" + export CURL_PROGRESS="-s" EXTRA_LOG=":" CURL_PROGRESS_EXTRA="-s" INFO_PATH="${HOME}/.google-drive-upload" CONFIG_INFO="${INFO_PATH}/google-drive-upload.configpath" [[ -f ${CONFIG_INFO} ]] && . "${CONFIG_INFO}" CONFIG="${CONFIG:-${HOME}/.googledrive.conf}" # Configuration variables # Remote gDrive variables - unset ROOT_FOLDER CLIENT_ID CLIENT_SECRET REFRESH_TOKEN ACCESS_TOKEN - API_URL="https://www.googleapis.com" - API_VERSION="v3" - SCOPE="${API_URL}/auth/drive" - REDIRECT_URI="urn:ietf:wg:oauth:2.0:oob" - TOKEN_URL="https://accounts.google.com/o/oauth2/token" + unset ROOT_FOLDER ROOT_FOLDER_NAME CLIENT_ID CLIENT_SECRET REFRESH_TOKEN ACCESS_TOKEN + export API_URL="https://www.googleapis.com" + export API_VERSION="v3" \ + SCOPE="${API_URL}/auth/drive" \ + REDIRECT_URI="urn:ietf:wg:oauth:2.0:oob" \ + TOKEN_URL="https://accounts.google.com/o/oauth2/token" _check_config() { [[ ${1} = default* ]] && export UPDATE_DEFAULT_CONFIG="_update_config" @@ -145,6 +151,21 @@ _setup_arguments() { -h | --help) _usage ;; -D | --debug) DEBUG="true" && export DEBUG ;; --info) _version_info ;; + -a | --account) + _check_longoptions "${1}" "${2}" + export CUSTOM_ACCOUNT_NAME="${2/default=/}" && shift + [[ ${2} = default* ]] && export UPDATE_DEFAULT_ACCOUNT="_update_config" + ;; + -la | --list-account) export LIST_ACCOUNTS="true" ;; + # this flag is preferred over --account + -ca | --create-account) + _check_longoptions "${1}" "${2}" + export NEW_ACCOUNT_NAME="${2}" && shift + ;; + -da | --delete-account) + _check_longoptions "${1}" "${2}" + export DELETE_ACCOUNT_NAME="${2}" && shift + ;; -c | -C | --create-dir) _check_longoptions "${1}" "${2}" FOLDERNAME="${2}" && shift @@ -162,9 +183,9 @@ _setup_arguments() { ;; -i | --save-info) _check_longoptions "${1}" "${2}" - LOG_FILE_ID="${2}" && shift + export LOG_FILE_ID="${2}" && shift ;; - -s | --skip-subdirs) SKIP_SUBDIRS="true" ;; + -s | --skip-subdirs) export SKIP_SUBDIRS="true" ;; -p | --parallel) _check_longoptions "${1}" "${2}" NO_OF_PARALLEL_JOBS="${2}" @@ -174,10 +195,10 @@ _setup_arguments() { printf "\nError: -p/--parallel value ranges between 1 to 10.\n" exit 1 fi - PARALLEL_UPLOAD="parallel" && shift + export PARALLEL_UPLOAD="parallel" && shift ;; - -o | --overwrite) OVERWRITE="Overwrite" && UPLOAD_MODE="update" ;; - -d | --skip-duplicates) SKIP_DUPLICATES="Skip Existing" && UPLOAD_MODE="update" ;; + -o | --overwrite) export OVERWRITE="Overwrite" UPLOAD_MODE="update" ;; + -d | --skip-duplicates) export SKIP_DUPLICATES="Skip Existing" UPLOAD_MODE="update" ;; -f | --file | --folder) _check_longoptions "${1}" "${2}" LOCAL_INPUT_ARRAY+=("${2}") && shift @@ -193,12 +214,13 @@ _setup_arguments() { ! [[ ${SHARE_EMAIL} =~ ${EMAIL_REGEX} ]] && printf "\nError: Provided email address for share option is invalid.\n" && exit 1 shift } + export SHARE_EMAIL ;; --speed) _check_longoptions "${1}" "${2}" regex='^([0-9]+)([k,K]|[m,M]|[g,G])+$' if [[ ${2} =~ ${regex} ]]; then - CURL_SPEED="--limit-rate ${2}" && shift + export CURL_SPEED="--limit-rate ${2}" && shift else printf "Error: Wrong speed limit format, supported formats: 1K , 1M and 1G\n" 1>&2 exit 1 @@ -207,7 +229,7 @@ _setup_arguments() { -R | --retry) _check_longoptions "${1}" "${2}" if [[ ${2} -gt 0 ]]; then - RETRY="${2}" && shift + export RETRY="${2}" && shift else printf "Error: -R/--retry only takes positive integers as arguments, min = 1, max = infinity.\n" exit 1 @@ -222,9 +244,9 @@ _setup_arguments() { EXCLUDE_FILES="${EXCLUDE_FILES} ! -name '${2}' " && shift ;; --hide) HIDE_INFO=":" ;; - -q | --quiet) QUIET="_print_center_quiet" ;; - -v | --verbose) VERBOSE="true" ;; - -V | --verbose-progress) VERBOSE_PROGRESS="true" ;; + -q | --quiet) export QUIET="_print_center_quiet" ;; + -v | --verbose) export VERBOSE="true" ;; + -V | --verbose-progress) export VERBOSE_PROGRESS="true" ;; --skip-internet-check) SKIP_INTERNET_CHECK=":" ;; '') shorthelp ;; *) # Check if user meant it to be a flag @@ -255,8 +277,11 @@ _setup_arguments() { _check_debug - [[ -n ${VERBOSE_PROGRESS} ]] && unset VERBOSE && CURL_PROGRESS="" - [[ -n ${QUIET} ]] && CURL_PROGRESS="-s" + [[ -n ${VERBOSE_PROGRESS} ]] && unset VERBOSE && export CURL_PROGRESS="" + [[ -n ${QUIET} ]] && export CURL_PROGRESS="-s" + + # create info path folder, can be missing if gupload was not installed with install.sh + mkdir -p "${INFO_PATH}" unset Aseen && declare -A Aseen for input in "${LOCAL_INPUT_ARRAY[@]}"; do @@ -267,162 +292,34 @@ _setup_arguments() { } done - # If no input, then check if -C option was used or not. - [[ -z ${FINAL_LOCAL_INPUT_ARRAY[*]:-${FINAL_ID_INPUT_ARRAY[*]:-${FOLDERNAME}}} ]] && _short_help - - # create info path folder, can be missing if gupload was not installed with install.sh - mkdir -p "${INFO_PATH}" - - return 0 -} - -################################################### -# Check Oauth credentials and create/update config file -# Client ID, Client Secret, Refesh Token and Access Token -# Globals: 10 variables, 3 functions -# Variables - API_URL, API_VERSION, TOKEN URL, -# CONFIG, UPDATE_DEFAULT_CONFIG, INFO_PATH, -# CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN and ACCESS_TOKEN -# Functions - _update_config, _update_value, _json_value and _print_center -# Arguments: None -# Result: read description -################################################### -_check_credentials() { - # Config file is created automatically after first run - [[ -r ${CONFIG} ]] && . "${CONFIG}" - "${UPDATE_DEFAULT_CONFIG:-:}" CONFIG "${CONFIG}" "${CONFIG_INFO}" - - ! [[ -t 1 ]] && [[ -z ${CLIENT_ID:+${CLIENT_SECRET:+${REFRESH_TOKEN}}} ]] && { - printf "%s\n" "Error: Script is not running in a terminal, cannot ask for credentials." - printf "%s\n" "Add in config manually if terminal is not accessible. CLIENT_ID, CLIENT_SECRET and REFRESH_TOKEN is required." && return 1 - } - - # Following https://developers.google.com/identity/protocols/oauth2#size - CLIENT_ID_REGEX='[0-9]+-[0-9A-Za-z_]{32}\.apps\.googleusercontent\.com' - CLIENT_SECRET_REGEX='[0-9A-Za-z_-]+' - REFRESH_TOKEN_REGEX='[0-9]//[0-9A-Za-z_-]+' # 512 bytes - ACCESS_TOKEN_REGEX='ya29\.[0-9A-Za-z_-]+' # 2048 bytes - AUTHORIZATION_CODE_REGEX='[0-9]/[0-9A-Za-z_-]+' # 256 bytes - - until [[ -n ${CLIENT_ID} && -n ${CLIENT_ID_VALID} ]]; do - [[ -n ${CLIENT_ID} ]] && { - if [[ ${CLIENT_ID} =~ ${CLIENT_ID_REGEX} ]]; then - [[ -n ${client_id} ]] && _update_config CLIENT_ID "${CLIENT_ID}" "${CONFIG}" - CLIENT_ID_VALID="true" && continue - else - { [[ -n ${client_id} ]] && message="- Try again"; } || message="in config ( ${CONFIG} )" - "${QUIET:-_print_center}" "normal" " Invalid Client ID ${message} " "-" && unset CLIENT_ID client_id - fi - } - [[ -z ${client_id} ]] && printf "\n" && "${QUIET:-_print_center}" "normal" " Enter Client ID " "-" - [[ -n ${client_id} ]] && _clear_line 1 - printf -- "-> " - read -r CLIENT_ID && client_id=1 - done - - until [[ -n ${CLIENT_SECRET} && -n ${CLIENT_SECRET_VALID} ]]; do - [[ -n ${CLIENT_SECRET} ]] && { - if [[ ${CLIENT_SECRET} =~ ${CLIENT_SECRET_REGEX} ]]; then - [[ -n ${client_secret} ]] && _update_config CLIENT_SECRET "${CLIENT_SECRET}" "${CONFIG}" - CLIENT_SECRET_VALID="true" && continue - else - { [[ -n ${client_secret} ]] && message="- Try again"; } || message="in config ( ${CONFIG} )" - "${QUIET:-_print_center}" "normal" " Invalid Client Secret ${message} " "-" && unset CLIENT_SECRET client_secret - fi - } - [[ -z ${client_secret} ]] && printf "\n" && "${QUIET:-_print_center}" "normal" " Enter Client Secret " "-" - [[ -n ${client_secret} ]] && _clear_line 1 - printf -- "-> " - read -r CLIENT_SECRET && client_secret=1 - done - - [[ -n ${REFRESH_TOKEN} ]] && { - ! [[ ${REFRESH_TOKEN} =~ ${REFRESH_TOKEN_REGEX} ]] && - "${QUIET:-_print_center}" "normal" " Error: Invalid Refresh token in config file, follow below steps.. " "-" && unset REFRESH_TOKEN + # handle account related flags here as we want to use the flags independenlty even with no normal valid inputs + # delete account, --delete-account flag + # TODO: add support for deleting multiple accounts + [[ -n ${DELETE_ACCOUNT_NAME} ]] && _delete_account "${DELETE_ACCOUNT_NAME}" + # list all configured accounts, --list-accounts flag + [[ -n ${LIST_ACCOUNTS} ]] && _all_accounts + + # If no input, then check if either -C option was used. + [[ -z ${FINAL_LOCAL_INPUT_ARRAY[*]:-${FINAL_ID_INPUT_ARRAY[*]:-${FOLDERNAME:-}}} ]] && { + # if any account related option was used then don't show short help + [[ -z ${DELETE_ACCOUNT_NAME:-${LIST_ACCOUNTS:-${NEW_ACCOUNT_NAME}}} ]] && _short_help + # exit right away if --list-accounts or --delete-account flag was used + [[ -n ${DELETE_ACCOUNT_NAME:-${LIST_ACCOUNTS:-}} ]] && exit 0 + # don't exit right away when new account is created but also let the rootdir stuff execute + [[ -n ${NEW_ACCOUNT_NAME} ]] && CONTINUE_WITH_NO_INPUT="true" } - [[ -z ${REFRESH_TOKEN} ]] && { - printf "\n" && "${QUIET:-_print_center}" "normal" "If you have a refresh token generated, then type the token, else leave blank and press return key.." " " - printf "\n" && "${QUIET:-_print_center}" "normal" " Refresh Token " "-" && printf -- "-> " - read -r REFRESH_TOKEN - if [[ -n ${REFRESH_TOKEN} ]]; then - "${QUIET:-_print_center}" "normal" " Checking refresh token.. " "-" - if [[ ${REFRESH_TOKEN} =~ ${REFRESH_TOKEN_REGEX} ]]; then - { _get_access_token_and_update && _update_config REFRESH_TOKEN "${REFRESH_TOKEN}" "${CONFIG}"; } || check_error=true - else - check_error=true - fi - [[ -n ${check_error} ]] && "${QUIET:-_print_center}" "normal" " Error: Invalid Refresh token given, follow below steps to generate.. " "-" && unset REFRESH_TOKEN - else - "${QUIET:-_print_center}" "normal" " No Refresh token given, follow below steps to generate.. " "-" - fi - - [[ -z ${REFRESH_TOKEN} ]] && { - printf "\n" && "${QUIET:-_print_center}" "normal" "Visit the below URL, tap on allow and then enter the code obtained" " " - URL="https://accounts.google.com/o/oauth2/auth?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&scope=${SCOPE}&response_type=code&prompt=consent" - printf "\n%s\n" "${URL}" - until [[ -n ${AUTHORIZATION_CODE} && -n ${AUTHORIZATION_CODE_VALID} ]]; do - [[ -n ${AUTHORIZATION_CODE} ]] && { - if [[ ${AUTHORIZATION_CODE} =~ ${AUTHORIZATION_CODE_REGEX} ]]; then - AUTHORIZATION_CODE_VALID="true" && continue - else - "${QUIET:-_print_center}" "normal" " Invalid CODE given, try again.. " "-" && unset AUTHORIZATION_CODE authorization_code - fi - } - { [[ -z ${authorization_code} ]] && printf "\n" && "${QUIET:-_print_center}" "normal" " Enter the authorization code " "-"; } || _clear_line 1 - printf -- "-> " - read -r AUTHORIZATION_CODE && authorization_code=1 - done - RESPONSE="$(curl --compressed "${CURL_PROGRESS}" -X POST \ - --data "code=${AUTHORIZATION_CODE}&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&redirect_uri=${REDIRECT_URI}&grant_type=authorization_code" "${TOKEN_URL}")" || : - _clear_line 1 1>&2 - - REFRESH_TOKEN="$(_json_value refresh_token 1 1 <<< "${RESPONSE}" || :)" - { _get_access_token_and_update "${RESPONSE}" && _update_config REFRESH_TOKEN "${REFRESH_TOKEN}" "${CONFIG}"; } || return 1 - } - printf "\n" - } - - [[ -z ${ACCESS_TOKEN} || ${ACCESS_TOKEN_EXPIRY:-0} -lt "$(printf "%(%s)T\\n" "-1")" ]] || ! [[ ${ACCESS_TOKEN} =~ ${ACCESS_TOKEN_REGEX} ]] && - { _get_access_token_and_update || return 1; } - printf "%b\n" "ACCESS_TOKEN=\"${ACCESS_TOKEN}\"\nACCESS_TOKEN_EXPIRY=\"${ACCESS_TOKEN_EXPIRY}\"" >| "${TMPFILE}_ACCESS_TOKEN" - - # launch a background service to check access token and update it - # checks ACCESS_TOKEN_EXPIRY, try to update before 5 mins of expiry, a fresh token gets 60 mins - # process will be killed when script exits or "${MAIN_PID}" is killed - { - until ! kill -0 "${MAIN_PID}" 2>| /dev/null 1>&2; do - . "${TMPFILE}_ACCESS_TOKEN" - CURRENT_TIME="$(printf "%(%s)T\\n" "-1")" - REMAINING_TOKEN_TIME="$((ACCESS_TOKEN_EXPIRY - CURRENT_TIME))" - if [[ ${REMAINING_TOKEN_TIME} -le 300 ]]; then - # timeout after 30 seconds, it shouldn't take too long anyway, and update tmp config - CONFIG="${TMPFILE}_ACCESS_TOKEN" _timeout 30 _get_access_token_and_update || : - else - TOKEN_PROCESS_TIME_TO_SLEEP="$(if [[ ${REMAINING_TOKEN_TIME} -le 301 ]]; then - printf "0\n" - else - printf "%s\n" "$((REMAINING_TOKEN_TIME - 300))" - fi)" - sleep "${TOKEN_PROCESS_TIME_TO_SLEEP}" - fi - sleep 1 - done - } & - ACCESS_TOKEN_SERVICE_PID="${!}" - return 0 } ################################################### # Setup root directory where all file/folders will be uploaded/updated -# Globals: 5 variables, 5 functions +# Globals: 5 variables, 6 functions # Variables - ROOTDIR, ROOT_FOLDER, UPDATE_DEFAULT_ROOTDIR, CONFIG, QUIET -# Functions - _print_center, _drive_info, _extract_id, _update_config, _json_value -# Arguments: 1 -# ${1} = Positive integer ( amount of time in seconds to sleep ) +# Functions - _print_center, _drive_info, _extract_id, _update_config, _json_value, _set_value +# Arguments: None # Result: read description -# If root id not found then pribt message and exit +# If root id not found then print message and exit # Update config with root id and root id name if specified # Reference: # https://github.com/dylanaraps/pure-bash-bible#use-read-as-an-alternative-to-the-sleep-command @@ -438,29 +335,32 @@ _setup_root_dir() { return 1 fi ROOT_FOLDER="${rootid}" - "${1:-:}" ROOT_FOLDER "${ROOT_FOLDER}" "${CONFIG}" + "${1:-:}" "ACCOUNT_${ACCOUNT_NAME}_ROOT_FOLDER" "${ROOT_FOLDER}" "${CONFIG}" || return 1 return 0 } _check_root_id_name() { ROOT_FOLDER_NAME="$(_drive_info "$(_extract_id "${ROOT_FOLDER}")" "name" | _json_value name || :)" - "${1:-:}" ROOT_FOLDER_NAME "${ROOT_FOLDER_NAME}" "${CONFIG}" + "${1:-:}" "ACCOUNT_${ACCOUNT_NAME}_ROOT_FOLDER_NAME" "${ROOT_FOLDER_NAME}" "${CONFIG}" || return 1 return 0 } + _set_value indirect ROOT_FOLDER "ACCOUNT_${ACCOUNT_NAME}_ROOT_FOLDER" + _set_value indirect ROOT_FOLDER_NAME "ACCOUNT_${ACCOUNT_NAME}_ROOT_FOLDER_NAME" + if [[ -n ${ROOTDIR:-} ]]; then ROOT_FOLDER="${ROOTDIR}" && { _check_root_id "${UPDATE_DEFAULT_ROOTDIR}" || return 1; } && unset ROOT_FOLDER_NAME elif [[ -z ${ROOT_FOLDER} ]]; then { [[ -t 1 ]] && "${QUIET:-_print_center}" "normal" "Enter root folder ID or URL, press enter for default ( root )" " " && printf -- "-> " && read -r ROOT_FOLDER && [[ -n ${ROOT_FOLDER} ]] && { _check_root_id _update_config || return 1; }; } || { ROOT_FOLDER="root" - _update_config ROOT_FOLDER "${ROOT_FOLDER}" "${CONFIG}" + _update_config "ACCOUNT_${ACCOUNT_NAME}_ROOT_FOLDER" "${ROOT_FOLDER}" "${CONFIG}" || return 1 } elif [[ -z ${ROOT_FOLDER_NAME} ]]; then - _check_root_id_name _update_config # update default root folder name if not available + _check_root_id_name _update_config || return 1 # update default root folder name if not available fi # fetch root folder name if rootdir different than default - [[ -z ${ROOT_FOLDER_NAME} ]] && _check_root_id_name "${UPDATE_DEFAULT_ROOTDIR}" + [[ -z ${ROOT_FOLDER_NAME} ]] && { _check_root_id_name "${UPDATE_DEFAULT_ROOTDIR}" || return 1; } return 0 } @@ -504,14 +404,6 @@ _setup_workspace() { # Result: Upload/Clone all the input files/folders, if a folder is empty, print Error message. ################################################### _process_arguments() { - export API_URL API_VERSION TOKEN_URL ACCESS_TOKEN \ - LOG_FILE_ID OVERWRITE UPLOAD_MODE SKIP_DUPLICATES CURL_SPEED RETRY UTILS_FOLDER TMPFILE \ - QUIET VERBOSE VERBOSE_PROGRESS CURL_PROGRESS CURL_PROGRESS_EXTRA CURL_PROGRESS_EXTRA_CLEAR COLUMNS EXTRA_LOG PARALLEL_UPLOAD - - export -f _bytes_to_human _dirname _json_value _url_encode _support_ansi_escapes _newline _print_center_quiet _print_center _clear_line \ - _api_request _get_access_token_and_update _check_existing_file _upload_file _upload_file_main _clone_file _collect_file_info _generate_upload_link _upload_file_from_uri _full_upload \ - _normal_logging_upload _error_logging_upload _log_upload_session _remove_upload_session _upload_folder _share_id _get_rootdir_id - # on successful uploads _share_and_print_link() { "${SHARE:-:}" "${1:-}" "${SHARE_EMAIL}" @@ -536,8 +428,8 @@ _process_arguments() { for _ in 1 2; do _clear_line 1; done && continue fi elif [[ -d ${input} ]]; then - input="$(cd "${input}" && pwd)" # to handle _dirname when current directory (.) is given as input. - unset EMPTY # Used when input folder is empty + input="$(cd "${input}" && pwd)" || return 1 # to handle _dirname when current directory (.) is given as input. + unset EMPTY # Used when input folder is empty _print_center "justify" "Given Input" ": FOLDER" "-" _print_center "justify" "Upload Method" ": ${SKIP_DUPLICATES:-${OVERWRITE:-Create}}" "=" && _newline "\n" @@ -558,8 +450,6 @@ _process_arguments() { mapfile -t FILENAMES <<< "$(_tmp='find "'${input}'" -type f -name "*" '${INCLUDE_FILES}' '${EXCLUDE_FILES}'' && eval "${_tmp}")" _clear_line 1 - ERROR_STATUS=0 SUCCESS_STATUS=0 - # Skip the sub folders and find recursively all the files and upload them. if [[ -n ${SKIP_SUBDIRS} ]]; then if [[ -n ${FILENAMES[0]} ]]; then @@ -687,30 +577,32 @@ main() { [[ $# = 0 ]] && _short_help [[ -z ${SELF_SOURCE} ]] && { - UTILS_FOLDER="${UTILS_FOLDER:-${PWD}}" - { . "${UTILS_FOLDER}"/common-utils.bash && . "${UTILS_FOLDER}"/drive-utils.bash && . "${UTILS_FOLDER}"/upload-utils.bash; } || + export UTILS_FOLDER="${UTILS_FOLDER:-${PWD}}" + { . "${UTILS_FOLDER}"/auth-utils.bash && . "${UTILS_FOLDER}"/common-utils.bash && . "${UTILS_FOLDER}"/drive-utils.bash && . "${UTILS_FOLDER}"/upload-utils.bash; } || { printf "Error: Unable to source util files.\n" && exit 1; } } _check_bash_version && set -o errexit -o noclobber -o pipefail - _setup_arguments "${@}" - "${SKIP_INTERNET_CHECK:-_check_internet}" + _setup_arguments "${@}" || exit 1 + "${SKIP_INTERNET_CHECK:-_check_internet}" || exit 1 - { command -v mktemp 1>| /dev/null && TMPFILE="$(mktemp -u)"; } || TMPFILE="${PWD}/.$(_t="$(printf "%(%s)T\\n" "-1")" && printf "%s\n" "$((_t * _t))").LOG" + { { command -v mktemp 1>| /dev/null && TMPFILE="$(mktemp -u)"; } || + TMPFILE="${PWD}/.$(_t="$(printf "%(%s)T\\n" "-1")" && printf "%s\n" "$((_t * _t))").LOG"; } || exit 1 + export TMPFILE _cleanup() { # unhide the cursor if hidden - [[ -n ${SUPPORT_ANSI_ESCAPES} ]] && printf "\e[?25h" + [[ -n ${SUPPORT_ANSI_ESCAPES} ]] && printf "\e[?25h\e[?7h" { [[ -f ${TMPFILE}_ACCESS_TOKEN ]] && { # update the config with latest ACCESS_TOKEN and ACCESS_TOKEN_EXPIRY only if changed . "${TMPFILE}_ACCESS_TOKEN" [[ ${INITIAL_ACCESS_TOKEN} = "${ACCESS_TOKEN}" ]] || { - _update_config ACCESS_TOKEN "${ACCESS_TOKEN}" "${CONFIG}" - _update_config ACCESS_TOKEN_EXPIRY "${ACCESS_TOKEN_EXPIRY}" "${CONFIG}" + _update_config "ACCOUNT_${ACCOUNT_NAME}_ACCESS_TOKEN" "${ACCESS_TOKEN}" "${CONFIG}" + _update_config "ACCOUNT_${ACCOUNT_NAME}_ACCESS_TOKEN_EXPIRY" "${ACCESS_TOKEN_EXPIRY}" "${CONFIG}" } - } 1>| /dev/null + } || : 1>| /dev/null # grab all chidren processes of access token service # https://askubuntu.com/a/512872 @@ -718,7 +610,7 @@ main() { token_service_pids="$(ps --ppid="${ACCESS_TOKEN_SERVICE_PID}" -o pid=)" # first kill parent id, then children processes kill "${ACCESS_TOKEN_SERVICE_PID}" - } 1>| /dev/null + } || : 1>| /dev/null # grab all script children pids script_children_pids="$(ps --ppid="${MAIN_PID}" -o pid=)" @@ -746,18 +638,20 @@ main() { export MAIN_PID="$$" START="$(printf "%(%s)T\\n" "-1")" - "${EXTRA_LOG}" "justify" "Starting script" "-" "${EXTRA_LOG}" "justify" "Checking credentials.." "-" - { _check_credentials && for _ in 1 2; do _clear_line 1; done; } || + { _check_credentials && _clear_line 1; } || { "${QUIET:-_print_center}" "normal" "[ Error: Credentials checking failed ]" "=" && exit 1; } - _print_center "justify" "Required credentials available." "=" + "${QUIET:-_print_center}" "normal" " Account: ${ACCOUNT_NAME} " "=" - "${EXTRA_LOG}" "justify" "Checking root dir and workspace folder.." "-" - { _setup_root_dir && for _ in 1 2; do _clear_line 1; done; } || + "${EXTRA_LOG}" "justify" "Checking root dir.." "-" + { _setup_root_dir && _clear_line 1; } || { "${QUIET:-_print_center}" "normal" "[ Error: Rootdir setup failed ]" "=" && exit 1; } _print_center "justify" "Root dir properly configured." "=" + # only execute next blocks if there was some input + [[ -n ${CONTINUE_WITH_NO_INPUT} ]] && exit 0 + "${EXTRA_LOG}" "justify" "Checking Workspace Folder.." "-" { _setup_workspace && for _ in 1 2; do _clear_line 1; done; } || { "${QUIET:-_print_center}" "normal" "[ Error: Workspace setup failed ]" "=" && exit 1; } diff --git a/merge.sh b/merge.sh index def7ab5..7dd950f 100755 --- a/merge.sh +++ b/merge.sh @@ -15,7 +15,10 @@ _merge() ( sed -n 1p "${file}.${shell}" printf "%s\n" "SELF_SOURCE=\"true\"" sed 1d common-utils."${shell}" - [ "${file}" = upload ] && sed 1d drive-utils."${shell}" && sed 1d upload-utils."${shell}" + [ "${file}" = upload ] && + sed 1d auth-utils."${shell}" && + sed 1d drive-utils."${shell}" && + sed 1d upload-utils."${shell}" sed 1d "${file}.${shell}" } >| "release/g${file}" chmod +x "release/g${file}" diff --git a/sh/auth-utils.sh b/sh/auth-utils.sh new file mode 100755 index 0000000..1ba3eeb --- /dev/null +++ b/sh/auth-utils.sh @@ -0,0 +1,487 @@ +#!/usr/bin/env sh +# auth utils for Google Drive +# shellcheck source=/dev/null + +################################################### +# Check if account name is valid by a regex expression +# Globals: None +# Arguments: 1 +# ${1} = Account name +# Result: read description and return 1 or 0 +################################################### +_account_name_valid() { + name_account_name_valid="${1:?}" account_name_regex_account_name_valid='^([A-Za-z0-9_])+$' + printf "%s\n" "${name_account_name_valid}" | grep -qE "${account_name_regex_account_name_valid}" || return 1 + return 0 +} + +################################################### +# Check if account exists +# First check if the given account is in correct format +# Globals: 2 functions +# _set_value, _account_name_valid +# Arguments: 1 +# ${1} = Account name +# Result: read description and return 1 or 0 +################################################### +_account_exists() { + name_account_exists="${1:-}" client_id_account_exists="" client_secret_account_exists="" refresh_token_account_exists="" + _account_name_valid "${name_account_exists}" || return 1 + _set_value indirect client_id_account_exists "ACCOUNT_${name_account_exists}_CLIENT_ID" + _set_value indirect client_secret_account_exists "ACCOUNT_${name_account_exists}_CLIENT_SECRET" + _set_value indirect refresh_token_account_exists "ACCOUNT_${name_account_exists}_REFRESH_TOKEN" + [ -z "${client_id_account_exists:+${client_secret_account_exists:+${refresh_token_account_exists}}}" ] && return 1 + return 0 +} + +################################################### +# Show all accounts configured in config file +# Globals: 2 variables, 4 functions +# Variable - CONFIG, QUIET +# Functions - _account_exists, _set_value, _print_center, _reload_config +# Arguments: None +# Result: SHOW all accounts, export COUNT and ACC_${count}_ACC dynamic variables +# or print "No accounts configured yet." +################################################### +_all_accounts() { + { _reload_config && _handle_old_config; } || return 1 + COUNT=0 + while read -r account <&4 && [ -n "${account}" ]; do + _account_exists "${account}" && + { [ "${COUNT}" = 0 ] && "${QUIET:-_print_center}" "normal" " All available accounts. " "=" || :; } && + printf "%b" "$((COUNT += 1)). ${account} \n" && _set_value direct "ACC_${COUNT}_ACC" "${account}" + done 4<< EOF +$(grep -oE '^ACCOUNT_.*_CLIENT_ID' "${CONFIG}" | sed -e "s/ACCOUNT_//g" -e "s/_CLIENT_ID//g") +EOF + { [ "${COUNT}" -le 0 ] && "${QUIET:-_print_center}" "normal" " No accounts configured yet. " "=" 1>&2; } || printf '\n' + return 0 +} + +################################################### +# Setup a new account name +# If given account name is configured already, then ask for name +# after name has been properly setup, export ACCOUNT_NAME var +# Globals: 1 variable, 5 functions +# Variable - QUIET +# Functions - _print_center, _account_exists, _clear_line, _account_name_valid, _reload_config +# Arguments: 1 +# ${1} = Account name ( optional ) +# Result: read description and export ACCOUNT_NAME NEW_ACCOUNT_NAME +################################################### +_set_new_account_name() { + _reload_config || return 1 + new_account_name_set_new_account_name="${1:-}" && unset name_valid_set_new_account_name + [ -z "${new_account_name_set_new_account_name}" ] && { + _all_accounts 2>| /dev/null + "${QUIET:-_print_center}" "normal" " New account name: " "=" + "${QUIET:-_print_center}" "normal" "Info: Account names can only contain alphabets / numbers / dashes." " " && printf '\n' + } + until [ -n "${name_valid_set_new_account_name}" ]; do + if [ -n "${new_account_name_set_new_account_name}" ]; then + if _account_name_valid "${new_account_name_set_new_account_name}"; then + if _account_exists "${new_account_name_set_new_account_name}"; then + "${QUIET:-_print_center}" "normal" " Warning: Given account ( ${new_account_name_set_new_account_name} ) already exists, input different name. " "-" 1>&2 + unset new_account_name_set_new_account_name && continue + else + export new_account_name_set_new_account_name="${new_account_name_set_new_account_name}" ACCOUNT_NAME="${new_account_name_set_new_account_name}" && + name_valid_set_new_account_name="true" && continue + fi + else + "${QUIET:-_print_center}" "normal" " Warning: Given account name ( ${new_account_name_set_new_account_name} ) invalid, input different name. " "-" + unset new_account_name_set_new_account_name && continue + fi + else + [ -t 1 ] || { "${QUIET:-_print_center}" "normal" " Error: Not running in an interactive terminal, cannot ask for new account name. " 1>&2 && return 1; } + printf -- "-> \e[?7l" + read -r new_account_name_set_new_account_name + printf '\e[?7h' + fi + _clear_line 1 + done + "${QUIET:-_print_center}" "normal" " Given account name: ${NEW_ACCOUNT_NAME} " "=" + export ACCOUNT_NAME="${NEW_ACCOUNT_NAME}" + return 0 +} + +################################################### +# Delete a account from config file +# Globals: 2 variables, 3 functions +# Variables - CONFIG, QUIET +# Functions - _account_exists, _print_center, _reload_config +# Arguments: None +# Result: check if account exists and delete from config, else print error message +################################################### +_delete_account() { + { _reload_config && _handle_old_config; } || return 1 + account_delete_account="${1:?Error: give account name}" && unset regex_delete_account config_without_values_delete_account + if _account_exists "${account_delete_account}"; then + regex_delete_account="^ACCOUNT_${account_delete_account}_(CLIENT_ID=|CLIENT_SECRET=|REFRESH_TOKEN=|ROOT_FOLDER=|ROOT_FOLDER_NAME=|ACCESS_TOKEN=|ACCESS_TOKEN_EXPIRY=)|DEFAULT_ACCOUNT=\"${account_delete_account}\"" + config_without_values_delete_account="$(grep -vE "${regex_delete_account}" "${CONFIG}")" + chmod u+w "${CONFIG}" || return 1 # change perms to edit + printf "%s\n" "${config_without_values_delete_account}" >| "${CONFIG}" || return 1 + chmod "a-w-r-x,u+r" "${CONFIG}" || return 1 # restore perms + "${QUIET:-_print_center}" "normal" " Successfully deleted account ( ${account_delete_account} ) from config. " "-" + else + "${QUIET:-_print_center}" "normal" " Error: Cannot delete account ( ${account_delete_account} ) from config. No such account exists " "-" 1>&2 + fi + return 0 +} + +################################################### +# handle legacy config +# this will be triggered only if old config values are present, convert to new format +# new account will be created with "default" name, if default already taken, then add a number as suffix +# Globals: 7 variables, 2 functions +# Variables - CLIENT_ID CLIENT_SECRET, REFRESH_TOKEN, ROOT_FOLDER, ROOT_FOLDER_NAME CONFIG, ACCOUNT_NAME +# Functions - _account_exists, _reload_config +# Arguments: None +################################################### +_handle_old_config() { + export CLIENT_ID CLIENT_SECRET REFRESH_TOKEN # to handle a shellcheck warning + # only try to convert the if all three values are present + [ -n "${CLIENT_ID:+${CLIENT_SECRET:+${REFRESH_TOKEN}}}" ] && { + account_name_handle_old_config="default" regex_check_handle_old_config config_without_values_handle_old_config count_handle_old_config + # first try to name the new account as default, otherwise try to add numbers as suffix + until ! _account_exists "${account_name_handle_old_config}"; do + account_name_handle_old_config="${account_name_handle_old_config}$((count_handle_old_config += 1))" + done + regex_check_handle_old_config="^(CLIENT_ID=|CLIENT_SECRET=|REFRESH_TOKEN=|ROOT_FOLDER=|ROOT_FOLDER_NAME=|ACCESS_TOKEN=|ACCESS_TOKEN_EXPIRY=)" + config_without_values_handle_old_config="$(grep -vE "${regex_check_handle_old_config}" "${CONFIG}")" + chmod u+w "${CONFIG}" || return 1 # change perms to edit + printf "%s\n%s\n%s\n%s\n%s\n%s\n" \ + "ACCOUNT_${account_name_handle_old_config}_CLIENT_ID=\"${CLIENT_ID}\"" \ + "ACCOUNT_${account_name_handle_old_config}_CLIENT_SECRET=\"${CLIENT_SECRET}\"" \ + "ACCOUNT_${account_name_handle_old_config}_REFRESH_TOKEN=\"${REFRESH_TOKEN}\"" \ + "ACCOUNT_${account_name_handle_old_config}_ROOT_FOLDER=\"${ROOT_FOLDER}\"" \ + "ACCOUNT_${account_name_handle_old_config}_ROOT_FOLDER_NAME=\"${ROOT_FOLDER_NAME}\"" \ + "${config_without_values_handle_old_config}" >| "${CONFIG}" || return 1 + + chmod "a-w-r-x,u+r" "${CONFIG}" || return 1 # restore perms + + _reload_config || return 1 # reload config file + } + return 0 +} + +################################################### +# handle old config values, new account creation, custom account name, updating default config and account +# start token service if applicable +# Globals: 12 variables, 7 functions +# Variables - DEFAULT_CONFIG, NEW_ACCOUNT_NAME, CUSTOM_ACCOUNT_NAME, DELETE_ACCOUNT_NAME, LIST_ACCOUNTS, QUIET +# UPDATE_DEFAULT_ACCOUNT, UPDATE_DEFAULT_CONFIG, CONFIG_INFO, CONTINUE_WITH_NO_INPUT +# Functions - _reload_config, _handle_old_config, _set_new_account_name, _account_exists, _all_accounts +# _check_account_credentials, _token_bg_service, _print_center, _update_config, _set_value +# Arguments: None +# Result: read description and start access token check in bg if required +################################################### +_check_credentials() { + { _reload_config && _handle_old_config; } || return 1 + # set account name to default account name + ACCOUNT_NAME="${DEFAULT_ACCOUNT}" + # if old values exist in config + + if [ -n "${NEW_ACCOUNT_NAME}" ]; then + # create new account, --create-account flag + _set_new_account_name "${NEW_ACCOUNT_NAME}" || return 1 + _check_account_credentials "${ACCOUNT_NAME}" || return 1 + else + if [ -n "${CUSTOM_ACCOUNT_NAME}" ]; then + if _account_exists "${CUSTOM_ACCOUNT_NAME}"; then + ACCOUNT_NAME="${CUSTOM_ACCOUNT_NAME}" + else + # error out in case CUSTOM_ACCOUNT_NAME is invalid + "${QUIET:-_print_center}" "normal" " Error: No such account ( ${CUSTOM_ACCOUNT_NAME} ) exists. " "-" && return 1 + fi + elif [ -n "${DEFAULT_ACCOUNT}" ]; then + # check if default account if valid or not, else set account name to nothing and remove default account in config + _account_exists "${DEFAULT_ACCOUNT}" || { + _update_config DEFAULT_ACCOUNT "" "${CONFIG}" && unset DEFAULT_ACCOUNT ACCOUNT_NAME && UPDATE_DEFAULT_ACCOUNT="_update_config" + } + # UPDATE_DEFAULT_ACCOUNT to true so that default config is updated later + else + UPDATE_DEFAULT_ACCOUNT="_update_config" # as default account doesn't exist + fi + + # in case no account name was set + if [ -z "${ACCOUNT_NAME}" ]; then + # if accounts are configured but default account is not set + if _all_accounts 2>| /dev/null && [ "${COUNT}" -gt 0 ]; then + # when only 1 account is configured, then set it as default + if [ "${COUNT}" -eq 1 ]; then + _set_value indirect ACCOUNT_NAME "ACC_1_ACC" # ACC_1_ACC comes from _all_accounts function + else + "${QUIET:-_print_center}" "normal" " Above accounts are configured, but default one not set. " "=" + if [ -t 1 ]; then + "${QUIET:-_print_center}" "normal" " Choose default account: " "-" + until [ -n "${ACCOUNT_NAME}" ]; do + printf -- "-> \e[?7l" + read -r account_name_check_credentials + printf '\e[?7h' + if [ "${account_name_check_credentials}" -gt 0 ] && [ "${account_name_check_credentials}" -le "${COUNT}" ]; then + _set_value indirect ACCOUNT_NAME "ACC_${COUNT}_ACC" + else + _clear_line 1 + fi + done + else + # if not running in a terminal then choose 1st one as default + printf "%s\n" "Warning: Script is not running in a terminal, choosing first account as default." + _set_value indirect ACCOUNT_NAME "ACC_1_ACC" # ACC_1_ACC comes from _all_accounts function + fi + fi + else + _set_new_account_name "" || return 1 + _check_account_credentials "${ACCOUNT_NAME}" || return 1 + fi + fi + _check_account_credentials "${ACCOUNT_NAME}" || return 1 + fi + + "${UPDATE_DEFAULT_ACCOUNT:-:}" DEFAULT_ACCOUNT "${ACCOUNT_NAME}" "${CONFIG}" # update default account if required + "${UPDATE_DEFAULT_CONFIG:-:}" CONFIG "${CONFIG}" "${CONFIG_INFO}" # update default config if required + + [ -n "${CONTINUE_WITH_NO_INPUT}" ] || _token_bg_service # launch token bg service + return 0 +} + +################################################### +# check credentials for a given account name +# Globals: 3 functions +# Functions - _check_client, _check_refresh_token, _check_access_token +# Arguments: 2 +# ${1} = Account name +# Result: read description, return 1 or 0 +################################################### +_check_account_credentials() { + account_name_check_account_credentials="${1:?Give account name}" + { + _check_client ID "${account_name_check_account_credentials}" && + _check_client SECRET "${account_name_check_account_credentials}" && + _check_refresh_token "${account_name_check_account_credentials}" && + _check_access_token "${account_name_check_account_credentials}" check + } || return 1 + return 0 +} + +################################################### +# Check client id or secret and ask if required +# Globals: 4 variables, 3 functions +# Variables - CONFIG, QUIET, CLIENT_ID_${ACCOUNT_NAME}, CLIENT_SECRET_${ACCOUNT_NAME} +# Functions - _print_center, _update_config, _set_value +# Arguments: 2 +# ${1} = ID or SECRET +# ${2} = Account name ( optional - if not given, then just CLIENT_[ID|SECRET] var is used ) +# Result: read description and export ACCOUNT_name_CLIENT_[ID|SECRET] CLIENT_[ID|SECRET] +################################################### +_check_client() { + type_check_client="CLIENT_${1:?Error: ID or SECRET}" account_name_check_client="${2:-}" + type_value_check_client="" type_regex_check_client="" && + unset type_name_check_client valid_check_client client_check_client message_check_client + export client_id_regex='[0-9]+-[0-9A-Za-z_]{32}\.apps\.googleusercontent\.com' client_secret_regex='[0-9A-Za-z_-]+' + type_name_check_client="${account_name_check_client:+ACCOUNT_${account_name_check_client}_}${type_check_client}" + + # set the type_value to the actual value of ACCOUNT_${account_name}_[ID|SECRET] + _set_value indirect type_value_check_client "${type_name_check_client}" + # set the type_regex to the actual value of client_id_regex or client_secret_regex + _set_value indirect type_regex_check_client "${type_check_client}_regex" + + until [ -n "${type_value_check_client}" ] && [ -n "${valid_check_client}" ]; do + [ -n "${type_value_check_client}" ] && { + if printf "%s\n" "${type_value_check_client}" | grep -qE "${type_regex_check_client}"; then + [ -n "${client_check_client}" ] && { _update_config "${type_name_check_client}" "${type_value_check_client}" "${CONFIG}" || return 1; } + valid_check_client="true" && continue + else + { [ -n "${client_check_client}" ] && message_check_client="- Try again"; } || message_check_client="in config ( ${CONFIG} )" + "${QUIET:-_print_center}" "normal" " Invalid Client ${1} ${message_check_client} " "-" && unset "${type_name_check_client}" client + fi + } + [ -z "${client_check_client}" ] && printf "\n" && "${QUIET:-_print_center}" "normal" " Enter Client ${1} " "-" + [ -n "${client_check_client}" ] && _clear_line 1 + printf -- "-> " + read -r "${type_name_check_client?}" && client_check_client=1 + _set_value indirect type_value_check_client "${type_name_check_client}" + done + + # export ACCOUNT_name_CLIENT_[ID|SECRET] + _set_value direct "${type_name_check_client}" "${type_value_check_client}" + # export CLIENT_[ID|SECRET] + _set_value direct "${type_check_client}" "${type_value_check_client}" + + return 0 +} + +################################################### +# Check refresh token and ask if required +# Globals: 8 variables, 4 functions +# Variables - CLIENT_ID, CLIENT_SECRET, REDIRECT_URI, TOKEN_URL, CONFIG, QUIET +# Functions - _set_value, _print_center, _update_config, _check_access_token +# Arguments: 1 +# ${1} = Account name ( optional - if not given, then just REFRESH_TOKEN var is used ) +# Result: read description & export REFRESH_TOKEN ACCOUNT_${account_name}_REFRESH_TOKEN +################################################### +_check_refresh_token() { + # bail out before doing anything if client id and secret is not present, unlikely to happen but just in case + [ -z "${CLIENT_ID:+${CLIENT_SECRET}}" ] && return 1 + account_name_check_refresh_token="${1:-}" + refresh_token_regex='[0-9]//[0-9A-Za-z_-]+' authorization_code_regex='[0-9]/[0-9A-Za-z_-]+' + refresh_token_name_check_refresh_token="${account_name_check_refresh_token:+ACCOUNT_${account_name_check_refresh_token}_}REFRESH_TOKEN" + + _set_value indirect refresh_token_value_check_refresh_token "${refresh_token_name_check_refresh_token}" + + [ -n "${refresh_token_value_check_refresh_token}" ] && { + ! printf "%s\n" "${refresh_token_value_check_refresh_token}" | grep -qE "${refresh_token_regex}" && + "${QUIET:-_print_center}" "normal" " Error: Invalid Refresh token in config file, follow below steps.. " "-" && unset refresh_token_value_check_refresh_token + } + + [ -z "${refresh_token_value_check_refresh_token}" ] && { + printf "\n" && "${QUIET:-_print_center}" "normal" "If you have a refresh token generated, then type the token, else leave blank and press return key.." " " + printf "\n" && "${QUIET:-_print_center}" "normal" " Refresh Token " "-" && printf -- "-> " + read -r refresh_token_value_check_refresh_token + if [ -n "${refresh_token_value_check_refresh_token}" ]; then + "${QUIET:-_print_center}" "normal" " Checking refresh token.. " "-" + if printf "%s\n" "${refresh_token_value_check_refresh_token}" | grep -qE "${refresh_token_regex}"; then + _set_value direct REFRESH_TOKEN "${refresh_token_value_check_refresh_token}" + { _check_access_token "${account_name_check_refresh_token}" skip_check && + _update_config "${refresh_token_name_check_refresh_token}" "${refresh_token_value_check_refresh_token}" "${CONFIG}" && + _clear_line 1; } || check_error_check_refresh_token=true + else + check_error_check_refresh_token=true + fi + [ -n "${check_error_check_refresh_token}" ] && "${QUIET:-_print_center}" "normal" " Error: Invalid Refresh token given, follow below steps to generate.. " "-" && unset refresh_token_value_check_refresh_token + else + "${QUIET:-_print_center}" "normal" " No Refresh token given, follow below steps to generate.. " "-" && unset refresh_token_value_check_refresh_token + fi + + [ -z "${refresh_token_value_check_refresh_token}" ] && { + printf "\n" && "${QUIET:-_print_center}" "normal" "Visit the below URL, tap on allow and then enter the code obtained" " " + URL="https://accounts.google.com/o/oauth2/auth?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&scope=${SCOPE}&response_type=code&prompt=consent" + printf "\n%s\n" "${URL}" + unset AUTHORIZATION_CODE authorization_code AUTHORIZATION_CODE_VALID response + until [ -n "${AUTHORIZATION_CODE}" ] && [ -n "${AUTHORIZATION_CODE_VALID}" ]; do + [ -n "${AUTHORIZATION_CODE}" ] && { + if printf "%s\n" "${AUTHORIZATION_CODE}" | grep -qE "${authorization_code_regex}"; then + AUTHORIZATION_CODE_VALID="true" && continue + else + "${QUIET:-_print_center}" "normal" " Invalid CODE given, try again.. " "-" && unset AUTHORIZATION_CODE authorization_code + fi + } + { [ -z "${authorization_code}" ] && printf "\n" && "${QUIET:-_print_center}" "normal" " Enter the authorization code " "-"; } || _clear_line 1 + printf -- "-> \e[?7l" + read -r AUTHORIZATION_CODE && authorization_code=1 + printf '\e[?7h' + done + response_check_refresh_token="$(curl --compressed "${CURL_PROGRESS}" -X POST \ + --data "code=${AUTHORIZATION_CODE}&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&redirect_uri=${REDIRECT_URI}&grant_type=authorization_code" "${TOKEN_URL}")" || : + _clear_line 1 1>&2 + + refresh_token_value_check_refresh_token="$(printf "%s\n" "${response_check_refresh_token}" | _json_value refresh_token 1 1)" || + { printf "%s\n" "Error: Cannot fetch refresh token, make sure the authorization code was correct." && return 1; } + + _set_value direct REFRESH_TOKEN "${refresh_token_value_check_refresh_token}" + { _check_access_token "${account_name_check_refresh_token}" skip_check "${response_check_refresh_token}" && + _update_config "${refresh_token_name_check_refresh_token}" "${refresh_token_value_check_refresh_token}" "${CONFIG}"; } || return 1 + } + printf "\n" + } + + # export account_name_check_refresh_token_REFRESH_TOKEN + _set_value direct "${refresh_token_name_check_refresh_token}" "${refresh_token_value_check_refresh_token}" + # export REFRESH_TOKEN + _set_value direct REFRESH_TOKEN "${refresh_token_value_check_refresh_token}" + + return 0 +} + +################################################### +# Check access token and create/update if required +# Also update in config +# Globals: 9 variables, 3 functions +# Variables - CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN, TOKEN_URL, CONFIG, API_URL, API_VERSION, QUIET +# Functions - _print_center, _update_config, _set_value +# Arguments: 2 +# ${1} = Account name ( if not given, then just ACCESS_TOKEN var is used ) +# ${2} = if skip_check, then force create access token, else check with regex and expiry +# ${3} = json response ( optional ) +# Result: read description & export ACCESS_TOKEN ACCESS_TOKEN_EXPIRY +################################################### +_check_access_token() { + # bail out before doing anything if client id|secret or refresh token is not present, unlikely to happen but just in case + [ -z "${CLIENT_ID:+${CLIENT_SECRET:+${REFRESH_TOKEN}}}" ] && return 1 + + account_name_check_access_token="${1:-}" no_check_check_access_token="${2:-false}" response_json_check_access_token="${3:-}" + unset token_name_check_access_token token_expiry_name_check_access_token token_value_check_access_token token_expiry_value_check_access_token response_check_access_token + access_token_regex='ya29\.[0-9A-Za-z_-]+' + token_name_check_access_token="${account_name_check_access_token:+ACCOUNT_${account_name_check_access_token}_}ACCESS_TOKEN" + token_expiry_name_check_access_token="${token_name_check_access_token}_EXPIRY" + + _set_value indirect token_value_check_access_token "${token_name_check_access_token}" + _set_value indirect token_expiry_value_check_access_token "${token_expiry_name_check_access_token}" + + [ "${no_check_check_access_token}" = skip_check ] || [ -z "${token_value_check_access_token}" ] || [ "${token_expiry_value_check_access_token:-0}" -lt "$(date +"%s")" ] || ! printf "%s\n" "${token_value_check_access_token}" | grep -qE "${access_token_regex}" && { + response_check_access_token="${response_json_check_access_token:-$(curl --compressed -s -X POST --data \ + "client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&refresh_token=${REFRESH_TOKEN}&grant_type=refresh_token" "${TOKEN_URL}")}" || : + + if token_value_check_access_token="$(printf "%s\n" "${response_check_access_token}" | _json_value access_token 1 1)"; then + token_expiry_value_check_access_token="$(($(date +"%s") + $(printf "%s\n" "${response_check_access_token}" | _json_value expires_in 1 1) - 1))" + _update_config "${token_name_check_access_token}" "${token_value_check_access_token}" "${CONFIG}" || return 1 + _update_config "${token_expiry_name_check_access_token}" "${token_expiry_value_check_access_token}" "${CONFIG}" || return 1 + else + "${QUIET:-_print_center}" "justify" "Error: Something went wrong" ", printing error." "=" 1>&2 + printf "%s\n" "${response_check_access_token}" 1>&2 + return 1 + fi + } + + # export ACCESS_TOKEN and ACCESS_TOKEN_EXPIRY + _set_value direct ACCESS_TOKEN "${token_value_check_access_token}" + _set_value direct ACCESS_TOKEN_EXPIRY "${token_expiry_value_check_access_token}" + + # export INITIAL_ACCESS_TOKEN which is used on script cleanup + _set_value direct INITIAL_ACCESS_TOKEN "${ACCESS_TOKEN}" + return 0 +} + +################################################### +# load config file if available, else create a empty file +# uses global variable CONFIG +################################################### +_reload_config() { + { [ -r "${CONFIG}" ] && . "${CONFIG}"; } || { printf "" >> "${CONFIG}" || return 1; } + return 0 +} + +################################################### +# launch a background service to check access token and update it +# checks ACCESS_TOKEN_EXPIRY, try to update before 5 mins of expiry, a fresh token gets 60 mins +# process will be killed when script exits or "${MAIN_PID}" is killed +# Globals: 4 variables, 1 function +# Variables - ACCESS_TOKEN, ACCESS_TOKEN_EXPIRY, MAIN_PID, TMPFILE +# Functions - _check_access_token +# Arguments: None +# Result: read description & export ACCESS_TOKEN_SERVICE_PID +################################################### +_token_bg_service() { + [ -z "${MAIN_PID}" ] && return 0 # don't start if MAIN_PID is empty + printf "%b\n" "ACCESS_TOKEN=\"${ACCESS_TOKEN}\"\nACCESS_TOKEN_EXPIRY=\"${ACCESS_TOKEN_EXPIRY}\"" >| "${TMPFILE}_ACCESS_TOKEN" + { + until ! kill -0 "${MAIN_PID}" 2>| /dev/null 1>&2; do + . "${TMPFILE}_ACCESS_TOKEN" + CURRENT_TIME="$(date +"%s")" + REMAINING_TOKEN_TIME="$((ACCESS_TOKEN_EXPIRY - CURRENT_TIME))" + if [ "${REMAINING_TOKEN_TIME}" -le 300 ]; then + # timeout after 30 seconds, it shouldn't take too long anyway, and update tmp config + CONFIG="${TMPFILE}_ACCESS_TOKEN" _timeout 30 _check_access_token "" skip_check || : + else + TOKEN_PROCESS_TIME_TO_SLEEP="$(if [ "${REMAINING_TOKEN_TIME}" -le 301 ]; then + printf "0\n" + else + printf "%s\n" "$((REMAINING_TOKEN_TIME - 300))" + fi)" + sleep "${TOKEN_PROCESS_TIME_TO_SLEEP}" + fi + sleep 1 + done + } & + export ACCESS_TOKEN_SERVICE_PID="${!}" + return 0 +} diff --git a/sh/common-utils.sh b/sh/common-utils.sh index 2e12501..68c0e27 100755 --- a/sh/common-utils.sh +++ b/sh/common-utils.sh @@ -37,7 +37,6 @@ _bytes_to_human() { # Check QUIET, then check terminal size and enable print functions accordingly. ################################################### _check_debug() { - _print_center_quiet() { { [ $# = 3 ] && printf "%s\n" "${2}"; } || { printf "%s%s\n" "${2}" "${3}"; }; } if [ -n "${DEBUG}" ]; then set -x && PS4='-> ' _print_center() { { [ $# = 3 ] && printf "%s\n" "${2}"; } || { printf "%s%s\n" "${2}" "${3}"; }; } @@ -76,7 +75,7 @@ _check_internet() { if ! _timeout 10 curl -Is google.com --compressed; then _clear_line 1 "${QUIET:-_print_center}" "justify" "Error: Internet connection" " not available." "=" - exit 1 + return 1 fi _clear_line 1 } @@ -254,6 +253,30 @@ _print_center() { return 0 } +################################################### +# Quiet version of _print_center +################################################### +_print_center_quiet() { + { [ $# = 3 ] && printf "%s\n" "${2}"; } || printf "%s%s\n" "${2}" "${3}" +} + +################################################### +# Evaluates value1=value2 +# Globals: None +# Arguments: 3 +# ${1} = direct ( d ) or indirect ( i ) - ( evaluation mode ) +# ${2} = var name +# ${3} = var value +# Result: export value1=value2 +################################################### +_set_value() { + mode_set_value="${1:?}" var_set_value="${2:?}" value_set_value="${3:?}" + case "${mode_set_value}" in + d | direct) export "${var_set_value}=${value_set_value}" ;; + i | indirect) export "${var_set_value}=$(eval printf "%s" \"\$"${value_set_value}"\")" ;; + esac +} + ################################################### # Check if script terminal supports ansi escapes # Globals: 1 variable @@ -307,10 +330,11 @@ _update_config() { [ $# -lt 3 ] && printf "Missing arguments\n" && return 1 value_name_update_config="${1}" value_update_config="${2}" config_path_update_config="${3}" ! [ -f "${config_path_update_config}" ] && : >| "${config_path_update_config}" # If config file doesn't exist. - chmod u+w "${config_path_update_config}" + chmod u+w "${config_path_update_config}" || return 1 printf "%s\n%s\n" "$(grep -v -e "^$" -e "^${value_name_update_config}=" "${config_path_update_config}" || :)" \ - "${value_name_update_config}=\"${value_update_config}\"" >| "${config_path_update_config}" - chmod a-w-r-x,u+r "${config_path_update_config}" + "${value_name_update_config}=\"${value_update_config}\"" >| "${config_path_update_config}" || return 1 + chmod a-w-r-x,u+r "${config_path_update_config}" || return 1 + return 0 } ################################################### diff --git a/sh/drive-utils.sh b/sh/drive-utils.sh index fea8955..e9d9629 100755 --- a/sh/drive-utils.sh +++ b/sh/drive-utils.sh @@ -180,28 +180,6 @@ _extract_id() { printf "%b" "${id_extract_id:+${id_extract_id}\n}" } -################################################### -# Method to regenerate access_token ( also updates in config ). -# Make a request on https://www.googleapis.com/oauth2/""${API_VERSION}""/tokeninfo?access_token=${ACCESS_TOKEN} url and check if the given token is valid, if not generate one. -# Globals: 9 variables, 2 functions -# Variables - CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN, TOKEN_URL, CONFIG, API_URL, API_VERSION, QUIET, NO_UPDATE_TOKEN -# Functions - _update_config and _print_center -# Result: Update access_token and expiry else print error -################################################### -_get_access_token_and_update() { - RESPONSE="${1:-$(curl --compressed -s -X POST --data "client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&refresh_token=${REFRESH_TOKEN}&grant_type=refresh_token" "${TOKEN_URL}")}" || : - if ACCESS_TOKEN="$(printf "%s\n" "${RESPONSE}" | _json_value access_token 1 1)"; then - ACCESS_TOKEN_EXPIRY="$(($(date +"%s") + $(printf "%s\n" "${RESPONSE}" | _json_value expires_in 1 1) - 1))" - _update_config ACCESS_TOKEN "${ACCESS_TOKEN}" "${CONFIG}" - _update_config ACCESS_TOKEN_EXPIRY "${ACCESS_TOKEN_EXPIRY}" "${CONFIG}" - else - "${QUIET:-_print_center}" "justify" "Error: Something went wrong" ", printing error." "=" 1>&2 - printf "%s\n" "${RESPONSE}" 1>&2 - return 1 - fi - return 0 -} - ################################################### # Upload ( Create/Update ) files on gdrive. # Interrupted uploads can be resumed. diff --git a/sh/release/gsync b/sh/release/gsync index c178c02..7b3614e 100755 --- a/sh/release/gsync +++ b/sh/release/gsync @@ -38,7 +38,6 @@ _bytes_to_human() { # Check QUIET, then check terminal size and enable print functions accordingly. ################################################### _check_debug() { - _print_center_quiet() { { [ $# = 3 ] && printf "%s\n" "${2}"; } || { printf "%s%s\n" "${2}" "${3}"; }; } if [ -n "${DEBUG}" ]; then set -x && PS4='-> ' _print_center() { { [ $# = 3 ] && printf "%s\n" "${2}"; } || { printf "%s%s\n" "${2}" "${3}"; }; } @@ -77,7 +76,7 @@ _check_internet() { if ! _timeout 10 curl -Is google.com --compressed; then _clear_line 1 "${QUIET:-_print_center}" "justify" "Error: Internet connection" " not available." "=" - exit 1 + return 1 fi _clear_line 1 } @@ -255,6 +254,30 @@ _print_center() { return 0 } +################################################### +# Quiet version of _print_center +################################################### +_print_center_quiet() { + { [ $# = 3 ] && printf "%s\n" "${2}"; } || printf "%s%s\n" "${2}" "${3}" +} + +################################################### +# Evaluates value1=value2 +# Globals: None +# Arguments: 3 +# ${1} = direct ( d ) or indirect ( i ) - ( evaluation mode ) +# ${2} = var name +# ${3} = var value +# Result: export value1=value2 +################################################### +_set_value() { + mode_set_value="${1:?}" var_set_value="${2:?}" value_set_value="${3:?}" + case "${mode_set_value}" in + d | direct) export "${var_set_value}=${value_set_value}" ;; + i | indirect) export "${var_set_value}=$(eval printf "%s" \"\$"${value_set_value}"\")" ;; + esac +} + ################################################### # Check if script terminal supports ansi escapes # Globals: 1 variable @@ -308,10 +331,11 @@ _update_config() { [ $# -lt 3 ] && printf "Missing arguments\n" && return 1 value_name_update_config="${1}" value_update_config="${2}" config_path_update_config="${3}" ! [ -f "${config_path_update_config}" ] && : >| "${config_path_update_config}" # If config file doesn't exist. - chmod u+w "${config_path_update_config}" + chmod u+w "${config_path_update_config}" || return 1 printf "%s\n%s\n" "$(grep -v -e "^$" -e "^${value_name_update_config}=" "${config_path_update_config}" || :)" \ - "${value_name_update_config}=\"${value_update_config}\"" >| "${config_path_update_config}" - chmod a-w-r-x,u+r "${config_path_update_config}" + "${value_name_update_config}=\"${value_update_config}\"" >| "${config_path_update_config}" || return 1 + chmod a-w-r-x,u+r "${config_path_update_config}" || return 1 + return 0 } ################################################### diff --git a/sh/release/gupload b/sh/release/gupload index 55e06eb..74607ff 100755 --- a/sh/release/gupload +++ b/sh/release/gupload @@ -38,7 +38,6 @@ _bytes_to_human() { # Check QUIET, then check terminal size and enable print functions accordingly. ################################################### _check_debug() { - _print_center_quiet() { { [ $# = 3 ] && printf "%s\n" "${2}"; } || { printf "%s%s\n" "${2}" "${3}"; }; } if [ -n "${DEBUG}" ]; then set -x && PS4='-> ' _print_center() { { [ $# = 3 ] && printf "%s\n" "${2}"; } || { printf "%s%s\n" "${2}" "${3}"; }; } @@ -77,7 +76,7 @@ _check_internet() { if ! _timeout 10 curl -Is google.com --compressed; then _clear_line 1 "${QUIET:-_print_center}" "justify" "Error: Internet connection" " not available." "=" - exit 1 + return 1 fi _clear_line 1 } @@ -255,6 +254,30 @@ _print_center() { return 0 } +################################################### +# Quiet version of _print_center +################################################### +_print_center_quiet() { + { [ $# = 3 ] && printf "%s\n" "${2}"; } || printf "%s%s\n" "${2}" "${3}" +} + +################################################### +# Evaluates value1=value2 +# Globals: None +# Arguments: 3 +# ${1} = direct ( d ) or indirect ( i ) - ( evaluation mode ) +# ${2} = var name +# ${3} = var value +# Result: export value1=value2 +################################################### +_set_value() { + mode_set_value="${1:?}" var_set_value="${2:?}" value_set_value="${3:?}" + case "${mode_set_value}" in + d | direct) export "${var_set_value}=${value_set_value}" ;; + i | indirect) export "${var_set_value}=$(eval printf "%s" \"\$"${value_set_value}"\")" ;; + esac +} + ################################################### # Check if script terminal supports ansi escapes # Globals: 1 variable @@ -308,10 +331,11 @@ _update_config() { [ $# -lt 3 ] && printf "Missing arguments\n" && return 1 value_name_update_config="${1}" value_update_config="${2}" config_path_update_config="${3}" ! [ -f "${config_path_update_config}" ] && : >| "${config_path_update_config}" # If config file doesn't exist. - chmod u+w "${config_path_update_config}" + chmod u+w "${config_path_update_config}" || return 1 printf "%s\n%s\n" "$(grep -v -e "^$" -e "^${value_name_update_config}=" "${config_path_update_config}" || :)" \ - "${value_name_update_config}=\"${value_update_config}\"" >| "${config_path_update_config}" - chmod a-w-r-x,u+r "${config_path_update_config}" + "${value_name_update_config}=\"${value_update_config}\"" >| "${config_path_update_config}" || return 1 + chmod a-w-r-x,u+r "${config_path_update_config}" || return 1 + return 0 } ################################################### @@ -330,6 +354,492 @@ _url_encode() ( q = y ~ /[[:alnum:]]_.!~*\47()-]/ ? q y : q sprintf("%%%02X", z[y]) print q}' "${1}" ) +# auth utils for Google Drive +# shellcheck source=/dev/null + +################################################### +# Check if account name is valid by a regex expression +# Globals: None +# Arguments: 1 +# ${1} = Account name +# Result: read description and return 1 or 0 +################################################### +_account_name_valid() { + name_account_name_valid="${1:?}" account_name_regex_account_name_valid='^([A-Za-z0-9_])+$' + printf "%s\n" "${name_account_name_valid}" | grep -qE "${account_name_regex_account_name_valid}" || return 1 + return 0 +} + +################################################### +# Check if account exists +# First check if the given account is in correct format +# Globals: 2 functions +# _set_value, _account_name_valid +# Arguments: 1 +# ${1} = Account name +# Result: read description and return 1 or 0 +################################################### +_account_exists() { + name_account_exists="${1:-}" client_id_account_exists="" client_secret_account_exists="" refresh_token_account_exists="" + _account_name_valid "${name_account_exists}" || return 1 + _set_value indirect client_id_account_exists "ACCOUNT_${name_account_exists}_CLIENT_ID" + _set_value indirect client_secret_account_exists "ACCOUNT_${name_account_exists}_CLIENT_SECRET" + _set_value indirect refresh_token_account_exists "ACCOUNT_${name_account_exists}_REFRESH_TOKEN" + [ -z "${client_id_account_exists:+${client_secret_account_exists:+${refresh_token_account_exists}}}" ] && return 1 + return 0 +} + +################################################### +# Show all accounts configured in config file +# Globals: 2 variables, 4 functions +# Variable - CONFIG, QUIET +# Functions - _account_exists, _set_value, _print_center, _reload_config +# Arguments: None +# Result: SHOW all accounts, export COUNT and ACC_${count}_ACC dynamic variables +# or print "No accounts configured yet." +################################################### +_all_accounts() { + { _reload_config && _handle_old_config; } || return 1 + COUNT=0 + while read -r account <&4 && [ -n "${account}" ]; do + _account_exists "${account}" && + { [ "${COUNT}" = 0 ] && "${QUIET:-_print_center}" "normal" " All available accounts. " "=" || :; } && + printf "%b" "$((COUNT += 1)). ${account} \n" && _set_value direct "ACC_${COUNT}_ACC" "${account}" + done 4<< EOF +$(grep -oE '^ACCOUNT_.*_CLIENT_ID' "${CONFIG}" | sed -e "s/ACCOUNT_//g" -e "s/_CLIENT_ID//g") +EOF + { [ "${COUNT}" -le 0 ] && "${QUIET:-_print_center}" "normal" " No accounts configured yet. " "=" 1>&2; } || printf '\n' + return 0 +} + +################################################### +# Setup a new account name +# If given account name is configured already, then ask for name +# after name has been properly setup, export ACCOUNT_NAME var +# Globals: 1 variable, 5 functions +# Variable - QUIET +# Functions - _print_center, _account_exists, _clear_line, _account_name_valid, _reload_config +# Arguments: 1 +# ${1} = Account name ( optional ) +# Result: read description and export ACCOUNT_NAME NEW_ACCOUNT_NAME +################################################### +_set_new_account_name() { + _reload_config || return 1 + new_account_name_set_new_account_name="${1:-}" && unset name_valid_set_new_account_name + [ -z "${new_account_name_set_new_account_name}" ] && { + _all_accounts 2>| /dev/null + "${QUIET:-_print_center}" "normal" " New account name: " "=" + "${QUIET:-_print_center}" "normal" "Info: Account names can only contain alphabets / numbers / dashes." " " && printf '\n' + } + until [ -n "${name_valid_set_new_account_name}" ]; do + if [ -n "${new_account_name_set_new_account_name}" ]; then + if _account_name_valid "${new_account_name_set_new_account_name}"; then + if _account_exists "${new_account_name_set_new_account_name}"; then + "${QUIET:-_print_center}" "normal" " Warning: Given account ( ${new_account_name_set_new_account_name} ) already exists, input different name. " "-" 1>&2 + unset new_account_name_set_new_account_name && continue + else + export new_account_name_set_new_account_name="${new_account_name_set_new_account_name}" ACCOUNT_NAME="${new_account_name_set_new_account_name}" && + name_valid_set_new_account_name="true" && continue + fi + else + "${QUIET:-_print_center}" "normal" " Warning: Given account name ( ${new_account_name_set_new_account_name} ) invalid, input different name. " "-" + unset new_account_name_set_new_account_name && continue + fi + else + [ -t 1 ] || { "${QUIET:-_print_center}" "normal" " Error: Not running in an interactive terminal, cannot ask for new account name. " 1>&2 && return 1; } + printf -- "-> \e[?7l" + read -r new_account_name_set_new_account_name + printf '\e[?7h' + fi + _clear_line 1 + done + "${QUIET:-_print_center}" "normal" " Given account name: ${NEW_ACCOUNT_NAME} " "=" + export ACCOUNT_NAME="${NEW_ACCOUNT_NAME}" + return 0 +} + +################################################### +# Delete a account from config file +# Globals: 2 variables, 3 functions +# Variables - CONFIG, QUIET +# Functions - _account_exists, _print_center, _reload_config +# Arguments: None +# Result: check if account exists and delete from config, else print error message +################################################### +_delete_account() { + { _reload_config && _handle_old_config; } || return 1 + account_delete_account="${1:?Error: give account name}" && unset regex_delete_account config_without_values_delete_account + if _account_exists "${account_delete_account}"; then + regex_delete_account="^ACCOUNT_${account_delete_account}_(CLIENT_ID=|CLIENT_SECRET=|REFRESH_TOKEN=|ROOT_FOLDER=|ROOT_FOLDER_NAME=|ACCESS_TOKEN=|ACCESS_TOKEN_EXPIRY=)|DEFAULT_ACCOUNT=\"${account_delete_account}\"" + config_without_values_delete_account="$(grep -vE "${regex_delete_account}" "${CONFIG}")" + chmod u+w "${CONFIG}" || return 1 # change perms to edit + printf "%s\n" "${config_without_values_delete_account}" >| "${CONFIG}" || return 1 + chmod "a-w-r-x,u+r" "${CONFIG}" || return 1 # restore perms + "${QUIET:-_print_center}" "normal" " Successfully deleted account ( ${account_delete_account} ) from config. " "-" + else + "${QUIET:-_print_center}" "normal" " Error: Cannot delete account ( ${account_delete_account} ) from config. No such account exists " "-" 1>&2 + fi + return 0 +} + +################################################### +# handle legacy config +# this will be triggered only if old config values are present, convert to new format +# new account will be created with "default" name, if default already taken, then add a number as suffix +# Globals: 7 variables, 2 functions +# Variables - CLIENT_ID CLIENT_SECRET, REFRESH_TOKEN, ROOT_FOLDER, ROOT_FOLDER_NAME CONFIG, ACCOUNT_NAME +# Functions - _account_exists, _reload_config +# Arguments: None +################################################### +_handle_old_config() { + export CLIENT_ID CLIENT_SECRET REFRESH_TOKEN # to handle a shellcheck warning + # only try to convert the if all three values are present + [ -n "${CLIENT_ID:+${CLIENT_SECRET:+${REFRESH_TOKEN}}}" ] && { + account_name_handle_old_config="default" regex_check_handle_old_config config_without_values_handle_old_config count_handle_old_config + # first try to name the new account as default, otherwise try to add numbers as suffix + until ! _account_exists "${account_name_handle_old_config}"; do + account_name_handle_old_config="${account_name_handle_old_config}$((count_handle_old_config += 1))" + done + regex_check_handle_old_config="^(CLIENT_ID=|CLIENT_SECRET=|REFRESH_TOKEN=|ROOT_FOLDER=|ROOT_FOLDER_NAME=|ACCESS_TOKEN=|ACCESS_TOKEN_EXPIRY=)" + config_without_values_handle_old_config="$(grep -vE "${regex_check_handle_old_config}" "${CONFIG}")" + chmod u+w "${CONFIG}" || return 1 # change perms to edit + printf "%s\n%s\n%s\n%s\n%s\n%s\n" \ + "ACCOUNT_${account_name_handle_old_config}_CLIENT_ID=\"${CLIENT_ID}\"" \ + "ACCOUNT_${account_name_handle_old_config}_CLIENT_SECRET=\"${CLIENT_SECRET}\"" \ + "ACCOUNT_${account_name_handle_old_config}_REFRESH_TOKEN=\"${REFRESH_TOKEN}\"" \ + "ACCOUNT_${account_name_handle_old_config}_ROOT_FOLDER=\"${ROOT_FOLDER}\"" \ + "ACCOUNT_${account_name_handle_old_config}_ROOT_FOLDER_NAME=\"${ROOT_FOLDER_NAME}\"" \ + "${config_without_values_handle_old_config}" >| "${CONFIG}" || return 1 + + chmod "a-w-r-x,u+r" "${CONFIG}" || return 1 # restore perms + + _reload_config || return 1 # reload config file + } + return 0 +} + +################################################### +# handle old config values, new account creation, custom account name, updating default config and account +# start token service if applicable +# Globals: 12 variables, 7 functions +# Variables - DEFAULT_CONFIG, NEW_ACCOUNT_NAME, CUSTOM_ACCOUNT_NAME, DELETE_ACCOUNT_NAME, LIST_ACCOUNTS, QUIET +# UPDATE_DEFAULT_ACCOUNT, UPDATE_DEFAULT_CONFIG, CONFIG_INFO, CONTINUE_WITH_NO_INPUT +# Functions - _reload_config, _handle_old_config, _set_new_account_name, _account_exists, _all_accounts +# _check_account_credentials, _token_bg_service, _print_center, _update_config, _set_value +# Arguments: None +# Result: read description and start access token check in bg if required +################################################### +_check_credentials() { + { _reload_config && _handle_old_config; } || return 1 + # set account name to default account name + ACCOUNT_NAME="${DEFAULT_ACCOUNT}" + # if old values exist in config + + if [ -n "${NEW_ACCOUNT_NAME}" ]; then + # create new account, --create-account flag + _set_new_account_name "${NEW_ACCOUNT_NAME}" || return 1 + _check_account_credentials "${ACCOUNT_NAME}" || return 1 + else + if [ -n "${CUSTOM_ACCOUNT_NAME}" ]; then + if _account_exists "${CUSTOM_ACCOUNT_NAME}"; then + ACCOUNT_NAME="${CUSTOM_ACCOUNT_NAME}" + else + # error out in case CUSTOM_ACCOUNT_NAME is invalid + "${QUIET:-_print_center}" "normal" " Error: No such account ( ${CUSTOM_ACCOUNT_NAME} ) exists. " "-" && return 1 + fi + elif [ -n "${DEFAULT_ACCOUNT}" ]; then + # check if default account if valid or not, else set account name to nothing and remove default account in config + _account_exists "${DEFAULT_ACCOUNT}" || { + _update_config DEFAULT_ACCOUNT "" "${CONFIG}" && unset DEFAULT_ACCOUNT ACCOUNT_NAME && UPDATE_DEFAULT_ACCOUNT="_update_config" + } + # UPDATE_DEFAULT_ACCOUNT to true so that default config is updated later + else + UPDATE_DEFAULT_ACCOUNT="_update_config" # as default account doesn't exist + fi + + # in case no account name was set + if [ -z "${ACCOUNT_NAME}" ]; then + # if accounts are configured but default account is not set + if _all_accounts 2>| /dev/null && [ "${COUNT}" -gt 0 ]; then + # when only 1 account is configured, then set it as default + if [ "${COUNT}" -eq 1 ]; then + _set_value indirect ACCOUNT_NAME "ACC_1_ACC" # ACC_1_ACC comes from _all_accounts function + else + "${QUIET:-_print_center}" "normal" " Above accounts are configured, but default one not set. " "=" + if [ -t 1 ]; then + "${QUIET:-_print_center}" "normal" " Choose default account: " "-" + until [ -n "${ACCOUNT_NAME}" ]; do + printf -- "-> \e[?7l" + read -r account_name_check_credentials + printf '\e[?7h' + if [ "${account_name_check_credentials}" -gt 0 ] && [ "${account_name_check_credentials}" -le "${COUNT}" ]; then + _set_value indirect ACCOUNT_NAME "ACC_${COUNT}_ACC" + else + _clear_line 1 + fi + done + else + # if not running in a terminal then choose 1st one as default + printf "%s\n" "Warning: Script is not running in a terminal, choosing first account as default." + _set_value indirect ACCOUNT_NAME "ACC_1_ACC" # ACC_1_ACC comes from _all_accounts function + fi + fi + else + _set_new_account_name "" || return 1 + _check_account_credentials "${ACCOUNT_NAME}" || return 1 + fi + fi + _check_account_credentials "${ACCOUNT_NAME}" || return 1 + fi + + "${UPDATE_DEFAULT_ACCOUNT:-:}" DEFAULT_ACCOUNT "${ACCOUNT_NAME}" "${CONFIG}" # update default account if required + "${UPDATE_DEFAULT_CONFIG:-:}" CONFIG "${CONFIG}" "${CONFIG_INFO}" # update default config if required + + [ -n "${CONTINUE_WITH_NO_INPUT}" ] || _token_bg_service # launch token bg service + return 0 +} + +################################################### +# check credentials for a given account name +# Globals: 3 functions +# Functions - _check_client, _check_refresh_token, _check_access_token +# Arguments: 2 +# ${1} = Account name +# Result: read description, return 1 or 0 +################################################### +_check_account_credentials() { + account_name_check_account_credentials="${1:?Give account name}" + { + _check_client ID "${account_name_check_account_credentials}" && + _check_client SECRET "${account_name_check_account_credentials}" && + _check_refresh_token "${account_name_check_account_credentials}" && + _check_access_token "${account_name_check_account_credentials}" check + } || return 1 + return 0 +} + +################################################### +# Check client id or secret and ask if required +# Globals: 4 variables, 3 functions +# Variables - CONFIG, QUIET, CLIENT_ID_${ACCOUNT_NAME}, CLIENT_SECRET_${ACCOUNT_NAME} +# Functions - _print_center, _update_config, _set_value +# Arguments: 2 +# ${1} = ID or SECRET +# ${2} = Account name ( optional - if not given, then just CLIENT_[ID|SECRET] var is used ) +# Result: read description and export ACCOUNT_name_CLIENT_[ID|SECRET] CLIENT_[ID|SECRET] +################################################### +_check_client() { + type_check_client="CLIENT_${1:?Error: ID or SECRET}" account_name_check_client="${2:-}" + type_value_check_client="" type_regex_check_client="" && + unset type_name_check_client valid_check_client client_check_client message_check_client + export client_id_regex='[0-9]+-[0-9A-Za-z_]{32}\.apps\.googleusercontent\.com' client_secret_regex='[0-9A-Za-z_-]+' + type_name_check_client="${account_name_check_client:+ACCOUNT_${account_name_check_client}_}${type_check_client}" + + # set the type_value to the actual value of ACCOUNT_${account_name}_[ID|SECRET] + _set_value indirect type_value_check_client "${type_name_check_client}" + # set the type_regex to the actual value of client_id_regex or client_secret_regex + _set_value indirect type_regex_check_client "${type_check_client}_regex" + + until [ -n "${type_value_check_client}" ] && [ -n "${valid_check_client}" ]; do + [ -n "${type_value_check_client}" ] && { + if printf "%s\n" "${type_value_check_client}" | grep -qE "${type_regex_check_client}"; then + [ -n "${client_check_client}" ] && { _update_config "${type_name_check_client}" "${type_value_check_client}" "${CONFIG}" || return 1; } + valid_check_client="true" && continue + else + { [ -n "${client_check_client}" ] && message_check_client="- Try again"; } || message_check_client="in config ( ${CONFIG} )" + "${QUIET:-_print_center}" "normal" " Invalid Client ${1} ${message_check_client} " "-" && unset "${type_name_check_client}" client + fi + } + [ -z "${client_check_client}" ] && printf "\n" && "${QUIET:-_print_center}" "normal" " Enter Client ${1} " "-" + [ -n "${client_check_client}" ] && _clear_line 1 + printf -- "-> " + read -r "${type_name_check_client?}" && client_check_client=1 + _set_value indirect type_value_check_client "${type_name_check_client}" + done + + # export ACCOUNT_name_CLIENT_[ID|SECRET] + _set_value direct "${type_name_check_client}" "${type_value_check_client}" + # export CLIENT_[ID|SECRET] + _set_value direct "${type_check_client}" "${type_value_check_client}" + + return 0 +} + +################################################### +# Check refresh token and ask if required +# Globals: 8 variables, 4 functions +# Variables - CLIENT_ID, CLIENT_SECRET, REDIRECT_URI, TOKEN_URL, CONFIG, QUIET +# Functions - _set_value, _print_center, _update_config, _check_access_token +# Arguments: 1 +# ${1} = Account name ( optional - if not given, then just REFRESH_TOKEN var is used ) +# Result: read description & export REFRESH_TOKEN ACCOUNT_${account_name}_REFRESH_TOKEN +################################################### +_check_refresh_token() { + # bail out before doing anything if client id and secret is not present, unlikely to happen but just in case + [ -z "${CLIENT_ID:+${CLIENT_SECRET}}" ] && return 1 + account_name_check_refresh_token="${1:-}" + refresh_token_regex='[0-9]//[0-9A-Za-z_-]+' authorization_code_regex='[0-9]/[0-9A-Za-z_-]+' + refresh_token_name_check_refresh_token="${account_name_check_refresh_token:+ACCOUNT_${account_name_check_refresh_token}_}REFRESH_TOKEN" + + _set_value indirect refresh_token_value_check_refresh_token "${refresh_token_name_check_refresh_token}" + + [ -n "${refresh_token_value_check_refresh_token}" ] && { + ! printf "%s\n" "${refresh_token_value_check_refresh_token}" | grep -qE "${refresh_token_regex}" && + "${QUIET:-_print_center}" "normal" " Error: Invalid Refresh token in config file, follow below steps.. " "-" && unset refresh_token_value_check_refresh_token + } + + [ -z "${refresh_token_value_check_refresh_token}" ] && { + printf "\n" && "${QUIET:-_print_center}" "normal" "If you have a refresh token generated, then type the token, else leave blank and press return key.." " " + printf "\n" && "${QUIET:-_print_center}" "normal" " Refresh Token " "-" && printf -- "-> " + read -r refresh_token_value_check_refresh_token + if [ -n "${refresh_token_value_check_refresh_token}" ]; then + "${QUIET:-_print_center}" "normal" " Checking refresh token.. " "-" + if printf "%s\n" "${refresh_token_value_check_refresh_token}" | grep -qE "${refresh_token_regex}"; then + _set_value direct REFRESH_TOKEN "${refresh_token_value_check_refresh_token}" + { _check_access_token "${account_name_check_refresh_token}" skip_check && + _update_config "${refresh_token_name_check_refresh_token}" "${refresh_token_value_check_refresh_token}" "${CONFIG}" && + _clear_line 1; } || check_error_check_refresh_token=true + else + check_error_check_refresh_token=true + fi + [ -n "${check_error_check_refresh_token}" ] && "${QUIET:-_print_center}" "normal" " Error: Invalid Refresh token given, follow below steps to generate.. " "-" && unset refresh_token_value_check_refresh_token + else + "${QUIET:-_print_center}" "normal" " No Refresh token given, follow below steps to generate.. " "-" && unset refresh_token_value_check_refresh_token + fi + + [ -z "${refresh_token_value_check_refresh_token}" ] && { + printf "\n" && "${QUIET:-_print_center}" "normal" "Visit the below URL, tap on allow and then enter the code obtained" " " + URL="https://accounts.google.com/o/oauth2/auth?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&scope=${SCOPE}&response_type=code&prompt=consent" + printf "\n%s\n" "${URL}" + unset AUTHORIZATION_CODE authorization_code AUTHORIZATION_CODE_VALID response + until [ -n "${AUTHORIZATION_CODE}" ] && [ -n "${AUTHORIZATION_CODE_VALID}" ]; do + [ -n "${AUTHORIZATION_CODE}" ] && { + if printf "%s\n" "${AUTHORIZATION_CODE}" | grep -qE "${authorization_code_regex}"; then + AUTHORIZATION_CODE_VALID="true" && continue + else + "${QUIET:-_print_center}" "normal" " Invalid CODE given, try again.. " "-" && unset AUTHORIZATION_CODE authorization_code + fi + } + { [ -z "${authorization_code}" ] && printf "\n" && "${QUIET:-_print_center}" "normal" " Enter the authorization code " "-"; } || _clear_line 1 + printf -- "-> \e[?7l" + read -r AUTHORIZATION_CODE && authorization_code=1 + printf '\e[?7h' + done + response_check_refresh_token="$(curl --compressed "${CURL_PROGRESS}" -X POST \ + --data "code=${AUTHORIZATION_CODE}&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&redirect_uri=${REDIRECT_URI}&grant_type=authorization_code" "${TOKEN_URL}")" || : + _clear_line 1 1>&2 + + refresh_token_value_check_refresh_token="$(printf "%s\n" "${response_check_refresh_token}" | _json_value refresh_token 1 1)" || + { printf "%s\n" "Error: Cannot fetch refresh token, make sure the authorization code was correct." && return 1; } + + _set_value direct REFRESH_TOKEN "${refresh_token_value_check_refresh_token}" + { _check_access_token "${account_name_check_refresh_token}" skip_check "${response_check_refresh_token}" && + _update_config "${refresh_token_name_check_refresh_token}" "${refresh_token_value_check_refresh_token}" "${CONFIG}"; } || return 1 + } + printf "\n" + } + + # export account_name_check_refresh_token_REFRESH_TOKEN + _set_value direct "${refresh_token_name_check_refresh_token}" "${refresh_token_value_check_refresh_token}" + # export REFRESH_TOKEN + _set_value direct REFRESH_TOKEN "${refresh_token_value_check_refresh_token}" + + return 0 +} + +################################################### +# Check access token and create/update if required +# Also update in config +# Globals: 9 variables, 3 functions +# Variables - CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN, TOKEN_URL, CONFIG, API_URL, API_VERSION, QUIET +# Functions - _print_center, _update_config, _set_value +# Arguments: 2 +# ${1} = Account name ( if not given, then just ACCESS_TOKEN var is used ) +# ${2} = if skip_check, then force create access token, else check with regex and expiry +# ${3} = json response ( optional ) +# Result: read description & export ACCESS_TOKEN ACCESS_TOKEN_EXPIRY +################################################### +_check_access_token() { + # bail out before doing anything if client id|secret or refresh token is not present, unlikely to happen but just in case + [ -z "${CLIENT_ID:+${CLIENT_SECRET:+${REFRESH_TOKEN}}}" ] && return 1 + + account_name_check_access_token="${1:-}" no_check_check_access_token="${2:-false}" response_json_check_access_token="${3:-}" + unset token_name_check_access_token token_expiry_name_check_access_token token_value_check_access_token token_expiry_value_check_access_token response_check_access_token + access_token_regex='ya29\.[0-9A-Za-z_-]+' + token_name_check_access_token="${account_name_check_access_token:+ACCOUNT_${account_name_check_access_token}_}ACCESS_TOKEN" + token_expiry_name_check_access_token="${token_name_check_access_token}_EXPIRY" + + _set_value indirect token_value_check_access_token "${token_name_check_access_token}" + _set_value indirect token_expiry_value_check_access_token "${token_expiry_name_check_access_token}" + + [ "${no_check_check_access_token}" = skip_check ] || [ -z "${token_value_check_access_token}" ] || [ "${token_expiry_value_check_access_token:-0}" -lt "$(date +"%s")" ] || ! printf "%s\n" "${token_value_check_access_token}" | grep -qE "${access_token_regex}" && { + response_check_access_token="${response_json_check_access_token:-$(curl --compressed -s -X POST --data \ + "client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&refresh_token=${REFRESH_TOKEN}&grant_type=refresh_token" "${TOKEN_URL}")}" || : + + if token_value_check_access_token="$(printf "%s\n" "${response_check_access_token}" | _json_value access_token 1 1)"; then + token_expiry_value_check_access_token="$(($(date +"%s") + $(printf "%s\n" "${response_check_access_token}" | _json_value expires_in 1 1) - 1))" + _update_config "${token_name_check_access_token}" "${token_value_check_access_token}" "${CONFIG}" || return 1 + _update_config "${token_expiry_name_check_access_token}" "${token_expiry_value_check_access_token}" "${CONFIG}" || return 1 + else + "${QUIET:-_print_center}" "justify" "Error: Something went wrong" ", printing error." "=" 1>&2 + printf "%s\n" "${response_check_access_token}" 1>&2 + return 1 + fi + } + + # export ACCESS_TOKEN and ACCESS_TOKEN_EXPIRY + _set_value direct ACCESS_TOKEN "${token_value_check_access_token}" + _set_value direct ACCESS_TOKEN_EXPIRY "${token_expiry_value_check_access_token}" + + # export INITIAL_ACCESS_TOKEN which is used on script cleanup + _set_value direct INITIAL_ACCESS_TOKEN "${ACCESS_TOKEN}" + return 0 +} + +################################################### +# load config file if available, else create a empty file +# uses global variable CONFIG +################################################### +_reload_config() { + { [ -r "${CONFIG}" ] && . "${CONFIG}"; } || { printf "" >> "${CONFIG}" || return 1; } + return 0 +} + +################################################### +# launch a background service to check access token and update it +# checks ACCESS_TOKEN_EXPIRY, try to update before 5 mins of expiry, a fresh token gets 60 mins +# process will be killed when script exits or "${MAIN_PID}" is killed +# Globals: 4 variables, 1 function +# Variables - ACCESS_TOKEN, ACCESS_TOKEN_EXPIRY, MAIN_PID, TMPFILE +# Functions - _check_access_token +# Arguments: None +# Result: read description & export ACCESS_TOKEN_SERVICE_PID +################################################### +_token_bg_service() { + [ -z "${MAIN_PID}" ] && return 0 # don't start if MAIN_PID is empty + printf "%b\n" "ACCESS_TOKEN=\"${ACCESS_TOKEN}\"\nACCESS_TOKEN_EXPIRY=\"${ACCESS_TOKEN_EXPIRY}\"" >| "${TMPFILE}_ACCESS_TOKEN" + { + until ! kill -0 "${MAIN_PID}" 2>| /dev/null 1>&2; do + . "${TMPFILE}_ACCESS_TOKEN" + CURRENT_TIME="$(date +"%s")" + REMAINING_TOKEN_TIME="$((ACCESS_TOKEN_EXPIRY - CURRENT_TIME))" + if [ "${REMAINING_TOKEN_TIME}" -le 300 ]; then + # timeout after 30 seconds, it shouldn't take too long anyway, and update tmp config + CONFIG="${TMPFILE}_ACCESS_TOKEN" _timeout 30 _check_access_token "" skip_check || : + else + TOKEN_PROCESS_TIME_TO_SLEEP="$(if [ "${REMAINING_TOKEN_TIME}" -le 301 ]; then + printf "0\n" + else + printf "%s\n" "$((REMAINING_TOKEN_TIME - 300))" + fi)" + sleep "${TOKEN_PROCESS_TIME_TO_SLEEP}" + fi + sleep 1 + done + } & + export ACCESS_TOKEN_SERVICE_PID="${!}" + return 0 +} ################################################### # Search for an existing file on gdrive with write permission. @@ -511,28 +1021,6 @@ _extract_id() { printf "%b" "${id_extract_id:+${id_extract_id}\n}" } -################################################### -# Method to regenerate access_token ( also updates in config ). -# Make a request on https://www.googleapis.com/oauth2/""${API_VERSION}""/tokeninfo?access_token=${ACCESS_TOKEN} url and check if the given token is valid, if not generate one. -# Globals: 9 variables, 2 functions -# Variables - CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN, TOKEN_URL, CONFIG, API_URL, API_VERSION, QUIET, NO_UPDATE_TOKEN -# Functions - _update_config and _print_center -# Result: Update access_token and expiry else print error -################################################### -_get_access_token_and_update() { - RESPONSE="${1:-$(curl --compressed -s -X POST --data "client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&refresh_token=${REFRESH_TOKEN}&grant_type=refresh_token" "${TOKEN_URL}")}" || : - if ACCESS_TOKEN="$(printf "%s\n" "${RESPONSE}" | _json_value access_token 1 1)"; then - ACCESS_TOKEN_EXPIRY="$(($(date +"%s") + $(printf "%s\n" "${RESPONSE}" | _json_value expires_in 1 1) - 1))" - _update_config ACCESS_TOKEN "${ACCESS_TOKEN}" "${CONFIG}" - _update_config ACCESS_TOKEN_EXPIRY "${ACCESS_TOKEN_EXPIRY}" "${CONFIG}" - else - "${QUIET:-_print_center}" "justify" "Error: Something went wrong" ", printing error." "=" 1>&2 - printf "%s\n" "${RESPONSE}" 1>&2 - return 1 - fi - return 0 -} - ################################################### # Upload ( Create/Update ) files on gdrive. # Interrupted uploads can be resumed. @@ -875,7 +1363,7 @@ _upload_file_main() { _upload_folder() { [ $# -lt 3 ] && printf "Missing arguments\n" && return 1 mode_upload_folder="${1}" PARSE_MODE="${2}" files_upload_folder="${3}" ID="${4:-}" && export PARSE_MODE ID - unset SUCCESS_STATUS SUCCESS_FILES ERROR_STATUS ERROR_FILES + SUCCESS_STATUS=0 SUCCESS_FILES="" ERROR_STATUS=0 ERROR_FILES="" case "${mode_upload_folder}" in normal) [ "${PARSE_MODE}" = parse ] && _clear_line 1 && _newline "\n" @@ -900,7 +1388,7 @@ EOF [ -f "${TMPFILE}"ERROR ] && rm "${TMPFILE}"ERROR # shellcheck disable=SC2016 - (printf "%s\n" "${files_upload_folder}" | xargs -n1 -P"${NO_OF_PARALLEL_JOBS_FINAL}" -I {} sh -c ' + (printf "%s\n" "${files_upload_folder}" | xargs -P"${NO_OF_PARALLEL_JOBS_FINAL}" -I "{}" -n 1 sh -c ' eval "${SOURCE_UTILS}" _upload_file_main "${PARSE_MODE}" "{}" "${ID}" true ' 1>| "${TMPFILE}"SUCCESS 2>| "${TMPFILE}"ERROR) & @@ -935,6 +1423,11 @@ Usage:\n ${0##*/} [options.. ] \n Foldername argument is optional. If not provided, the file will be uploaded to preconfigured google drive.\n File name argument is optional if create directory option is used.\n Options:\n + -a | --account 'account name' - Use different account than the default one.\n + To change the default account name, use this format, -a/--account default=account_name\n + -la | --list-accounts - Print all configured accounts in the config files.\n + -ca | --create-account 'account name' - To create a new account with the given name if does not already exists.\n + -da | --delete-account 'account name' - To delete an account information from config file. \n -c | -C | --create-dir - option to create directory. Will provide folder id. Can be used to provide input folder, see README.\n -r | --root-dir or - google folder ID/URL to which the file/directory is going to upload. If you want to change the default value, then use this format, -r/--root-dir default=root_folder_id/root_folder_url\n @@ -1037,24 +1530,25 @@ _setup_arguments() { [ $# = 0 ] && printf "Missing arguments\n" && return 1 # Internal variables # De-initialize if any variables set already. - unset FOLDERNAME FINAL_LOCAL_INPUT_ARRAY FINAL_ID_INPUT_ARRAY + unset LIST_ACCOUNTS UPDATE_DEFAULT_ACCOUNT CUSTOM_ACCOUNT_NAME NEW_ACCOUNT_NAME DELETE_ACCOUNT_NAME ACCOUNT_ONLY_RUN + unset FOLDERNAME FINAL_LOCAL_INPUT_ARRAY FINAL_ID_INPUT_ARRAY CONTINUE_WITH_NO_INPUT unset PARALLEL NO_OF_PARALLEL_JOBS SHARE SHARE_EMAIL OVERWRITE SKIP_DUPLICATES SKIP_SUBDIRS ROOTDIR QUIET unset VERBOSE VERBOSE_PROGRESS DEBUG LOG_FILE_ID CURL_SPEED RETRY - CURL_PROGRESS="-s" EXTRA_LOG=":" CURL_PROGRESS_EXTRA="-s" + export CURL_PROGRESS="-s" EXTRA_LOG=":" CURL_PROGRESS_EXTRA="-s" INFO_PATH="${HOME}/.google-drive-upload" CONFIG_INFO="${INFO_PATH}/google-drive-upload.configpath" [ -f "${CONFIG_INFO}" ] && . "${CONFIG_INFO}" CONFIG="${CONFIG:-${HOME}/.googledrive.conf}" # Configuration variables # Remote gDrive variables unset ROOT_FOLDER CLIENT_ID CLIENT_SECRET REFRESH_TOKEN ACCESS_TOKEN - API_URL="https://www.googleapis.com" - API_VERSION="v3" - SCOPE="${API_URL}/auth/drive" - REDIRECT_URI="urn:ietf:wg:oauth:2.0:oob" - TOKEN_URL="https://accounts.google.com/o/oauth2/token" + export API_URL="https://www.googleapis.com" + export API_VERSION="v3" \ + SCOPE="${API_URL}/auth/drive" \ + REDIRECT_URI="urn:ietf:wg:oauth:2.0:oob" \ + TOKEN_URL="https://accounts.google.com/o/oauth2/token" _check_config() { - [ -z "${1##default=*}" ] && UPDATE_DEFAULT_CONFIG="_update_config" + [ -z "${1##default=*}" ] && export UPDATE_DEFAULT_CONFIG="_update_config" { [ -r "${2}" ] && CONFIG="${2}"; } || { printf "Error: Given config file (%s) doesn't exist/not readable,..\n" "${1}" 1>&2 && exit 1 } @@ -1073,6 +1567,21 @@ _setup_arguments() { -h | --help) _usage ;; -D | --debug) DEBUG="true" && export DEBUG ;; --info) _version_info ;; + -a | --account) + _check_longoptions "${1}" "${2}" + export CUSTOM_ACCOUNT_NAME="${2##default=}" && shift + [ -z "${2##default=*}" ] && export UPDATE_DEFAULT_ACCOUNT="_update_config" + ;; + -la | --list-accounts) export LIST_ACCOUNTS="true" ;; + # this flag is preferred over --account + -ca | --create-account) + _check_longoptions "${1}" "${2}" + export NEW_ACCOUNT_NAME="${2}" && shift + ;; + -da | --delete-account) + _check_longoptions "${1}" "${2}" + export DELETE_ACCOUNT_NAME="${2}" && shift + ;; -c | -C | --create-dir) _check_longoptions "${1}" "${2}" FOLDERNAME="${2}" && shift @@ -1080,7 +1589,7 @@ _setup_arguments() { -r | --root-dir) _check_longoptions "${1}" "${2}" ROOTDIR="${2##default=}" - [ -z "${2##default=*}" ] && UPDATE_DEFAULT_ROOTDIR="_update_config" + [ -z "${2##default=*}" ] && export UPDATE_DEFAULT_ROOTDIR="_update_config" shift ;; -z | --config) @@ -1090,9 +1599,9 @@ _setup_arguments() { ;; -i | --save-info) _check_longoptions "${1}" "${2}" - LOG_FILE_ID="${2}" && shift + export LOG_FILE_ID="${2}" && shift ;; - -s | --skip-subdirs) SKIP_SUBDIRS="true" ;; + -s | --skip-subdirs) export SKIP_SUBDIRS="true" ;; -p | --parallel) _check_longoptions "${1}" "${2}" NO_OF_PARALLEL_JOBS="${2}" @@ -1102,10 +1611,10 @@ _setup_arguments() { printf "\nError: -p/--parallel value ranges between 1 to 10.\n" exit 1 fi - PARALLEL_UPLOAD="parallel" && shift + export PARALLEL_UPLOAD="parallel" && shift ;; - -o | --overwrite) OVERWRITE="Overwrite" && UPLOAD_MODE="update" ;; - -d | --skip-duplicates) SKIP_DUPLICATES="Skip Existing" && UPLOAD_MODE="update" ;; + -o | --overwrite) export OVERWRITE="Overwrite" UPLOAD_MODE="update" ;; + -d | --skip-duplicates) export SKIP_DUPLICATES="Skip Existing" UPLOAD_MODE="update" ;; -f | --file | --folder) _check_longoptions "${1}" "${2}" LOCAL_INPUT_ARRAY="${LOCAL_INPUT_ARRAY} @@ -1127,12 +1636,13 @@ _setup_arguments() { shift ;; esac + export SHARE_EMAIL ;; --speed) _check_longoptions "${1}" "${2}" regex='^([0-9]+)([k,K]|[m,M]|[g,G])+$' if printf "%s\n" "${2}" | grep -qE "${regex}"; then - CURL_SPEED="--limit-rate ${2}" && shift + export CURL_SPEED="--limit-rate ${2}" && shift else printf "Error: Wrong speed limit format, supported formats: 1K , 1M and 1G\n" 1>&2 exit 1 @@ -1141,7 +1651,7 @@ _setup_arguments() { -R | --retry) _check_longoptions "${1}" "${2}" if [ "$((2))" -gt 0 ] 2>| /dev/null 1>&2; then - RETRY="${2}" && shift + export RETRY="${2}" && shift else printf "Error: -R/--retry only takes positive integers as arguments, min = 1, max = infinity.\n" exit 1 @@ -1156,10 +1666,10 @@ _setup_arguments() { EXCLUDE_FILES="${EXCLUDE_FILES} ! -name '${2}' " && shift ;; --hide) HIDE_INFO=":" ;; - -q | --quiet) QUIET="_print_center_quiet" ;; - -v | --verbose) VERBOSE="true" ;; - -V | --verbose-progress) VERBOSE_PROGRESS="true" ;; - --skip-internet-check) SKIP_INTERNET_CHECK=":" ;; + -q | --quiet) export QUIET="_print_center_quiet" ;; + -v | --verbose) export VERBOSE="true" ;; + -V | --verbose-progress) export VERBOSE_PROGRESS="true" ;; + --skip-internet-check) export SKIP_INTERNET_CHECK=":" ;; '') shorthelp ;; *) # Check if user meant it to be a flag if [ -z "${1##-*}" ]; then @@ -1193,8 +1703,18 @@ _setup_arguments() { _check_debug - [ -n "${VERBOSE_PROGRESS}" ] && unset VERBOSE && CURL_PROGRESS="" - [ -n "${QUIET}" ] && CURL_PROGRESS="-s" + [ -n "${VERBOSE_PROGRESS}" ] && unset VERBOSE && export CURL_PROGRESS="" + [ -n "${QUIET}" ] && export CURL_PROGRESS="-s" + + # create info path folder, can be missing if gupload was not installed with install.sh + mkdir -p "${INFO_PATH}" || return 1 + + # handle account related flags here as we want to use the flags independenlty even with no normal valid inputs + # delete account, --delete-account flag + # TODO: add support for deleting multiple accounts + [ -n "${DELETE_ACCOUNT_NAME}" ] && _delete_account "${DELETE_ACCOUNT_NAME}" + # list all configured accounts, --list-accounts flag + [ -n "${LIST_ACCOUNTS}" ] && _all_accounts # If no input, then check if -C option was used or not. # check if given input exists ( file/folder ) @@ -1205,164 +1725,30 @@ _setup_arguments() { } done)" - [ -z "${FINAL_LOCAL_INPUT_ARRAY:-${FINAL_ID_INPUT_ARRAY:-${FOLDERNAME}}}" ] && _short_help - - # create info path folder, can be missing if gupload was not installed with install.sh - mkdir -p "${INFO_PATH}" - - return 0 -} - -################################################### -# Check Oauth credentials and create/update config file -# Client ID, Client Secret, Refesh Token and Access Token -# Globals: 10 variables, 3 functions -# Variables - API_URL, API_VERSION, TOKEN URL, -# CONFIG, UPDATE_DEFAULT_CONFIG, INFO_PATH, -# CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN and ACCESS_TOKEN -# Functions - _update_config, _json_value and _print -# Arguments: None -# Result: read description -################################################### -_check_credentials() { - # Config file is created automatically after first run - [ -r "${CONFIG}" ] && . "${CONFIG}" - "${UPDATE_DEFAULT_CONFIG:-:}" CONFIG "${CONFIG}" "${CONFIG_INFO}" - - ! [ -t 1 ] && [ -z "${CLIENT_ID:+${CLIENT_SECRET:+${REFRESH_TOKEN}}}" ] && { - printf "%s\n" "Error: Script is not running in a terminal, cannot ask for credentials." - printf "%s\n" "Add in config manually if terminal is not accessible. CLIENT_ID, CLIENT_SECRET and REFRESH_TOKEN is required." && return 1 - } - - # Following https://developers.google.com/identity/protocols/oauth2#size - CLIENT_ID_REGEX='[0-9]+-[0-9A-Za-z_]{32}\.apps\.googleusercontent\.com' - CLIENT_SECRET_REGEX='[0-9A-Za-z_-]+' - REFRESH_TOKEN_REGEX='[0-9]//[0-9A-Za-z_-]+' # 512 bytes - ACCESS_TOKEN_REGEX='ya29\.[0-9A-Za-z_-]+' # 2048 bytes - AUTHORIZATION_CODE_REGEX='[0-9]/[0-9A-Za-z_-]+' # 256 bytes - - until [ -n "${CLIENT_ID}" ] && [ -n "${CLIENT_ID_VALID}" ]; do - [ -n "${CLIENT_ID}" ] && { - if printf "%s\n" "${CLIENT_ID}" | grep -qE "${CLIENT_ID_REGEX}"; then - [ -n "${client_id}" ] && _update_config CLIENT_ID "${CLIENT_ID}" "${CONFIG}" - CLIENT_ID_VALID="true" && continue - else - { [ -n "${client_id}" ] && message="- Try again"; } || message="in config ( ${CONFIG} )" - "${QUIET:-_print_center}" "normal" " Invalid Client ID ${message} " "-" && unset CLIENT_ID client_id - fi - } - [ -z "${client_id}" ] && printf "\n" && "${QUIET:-_print_center}" "normal" " Enter Client ID " "-" - [ -n "${client_id}" ] && _clear_line 1 - printf -- "-> " - read -r CLIENT_ID && client_id=1 - done - - until [ -n "${CLIENT_SECRET}" ] && [ -n "${CLIENT_SECRET_VALID}" ]; do - [ -n "${CLIENT_SECRET}" ] && { - if printf "%s\n" "${CLIENT_SECRET}" | grep -qE "${CLIENT_SECRET_REGEX}"; then - [ -n "${client_secret}" ] && _update_config CLIENT_SECRET "${CLIENT_SECRET}" "${CONFIG}" - CLIENT_SECRET_VALID="true" && continue - else - { [ -n "${client_secret}" ] && message="- Try again"; } || message="in config ( ${CONFIG} )" - "${QUIET:-_print_center}" "normal" " Invalid Client Secret ${message} " "-" && unset CLIENT_SECRET client_secret - fi - } - [ -z "${client_secret}" ] && printf "\n" && "${QUIET:-_print_center}" "normal" " Enter Client Secret " "-" - [ -n "${client_secret}" ] && _clear_line 1 - printf -- "-> " - read -r CLIENT_SECRET && client_secret=1 - done - - [ -n "${REFRESH_TOKEN}" ] && { - ! printf "%s\n" "${REFRESH_TOKEN}" | grep -qE "${REFRESH_TOKEN_REGEX}" && - "${QUIET:-_print_center}" "normal" " Error: Invalid Refresh token in config file, follow below steps.. " "-" && unset REFRESH_TOKEN - } - - [ -z "${REFRESH_TOKEN}" ] && { - printf "\n" && "${QUIET:-_print_center}" "normal" "If you have a refresh token generated, then type the token, else leave blank and press return key.." " " - printf "\n" && "${QUIET:-_print_center}" "normal" " Refresh Token " "-" && printf -- "-> " - read -r REFRESH_TOKEN - if [ -n "${REFRESH_TOKEN}" ]; then - "${QUIET:-_print_center}" "normal" " Checking refresh token.. " "-" - if ! printf "%s\n" "${REFRESH_TOKEN}" | grep -qE "${REFRESH_TOKEN_REGEX}"; then - { _get_access_token_and_update && _update_config REFRESH_TOKEN "${REFRESH_TOKEN}" "${CONFIG}"; } || check_error=1 - else - check_error=true - fi - [ -n "${check_error}" ] && "${QUIET:-_print_center}" "normal" " Error: Invalid Refresh token given, follow below steps to generate.. " "-" && unset REFRESH_TOKEN - else - "${QUIET:-_print_center}" "normal" " No Refresh token given, follow below steps to generate.. " "-" - fi - - [ -z "${REFRESH_TOKEN}" ] && { - printf "\n" && "${QUIET:-_print_center}" "normal" "Visit the below URL, tap on allow and then enter the code obtained" " " - URL="https://accounts.google.com/o/oauth2/auth?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&scope=${SCOPE}&response_type=code&prompt=consent" - printf "\n%s\n" "${URL}" - until [ -n "${AUTHORIZATION_CODE}" ] && [ -n "${AUTHORIZATION_CODE_VALID}" ]; do - [ -n "${AUTHORIZATION_CODE}" ] && { - if printf "%s\n" "${AUTHORIZATION_CODE}" | grep -qE "${AUTHORIZATION_CODE_REGEX}"; then - AUTHORIZATION_CODE_VALID="true" && continue - else - "${QUIET:-_print_center}" "normal" " Invalid CODE given, try again.. " "-" && unset AUTHORIZATION_CODE authorization_code - fi - } - { [ -z "${authorization_code}" ] && printf "\n" && "${QUIET:-_print_center}" "normal" " Enter the authorization code " "-"; } || _clear_line 1 - printf -- "-> " - read -r AUTHORIZATION_CODE && authorization_code=1 - done - RESPONSE="$(curl --compressed "${CURL_PROGRESS}" -X POST \ - --data "code=${AUTHORIZATION_CODE}&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&redirect_uri=${REDIRECT_URI}&grant_type=authorization_code" "${TOKEN_URL}")" || : - _clear_line 1 1>&2 - - REFRESH_TOKEN="$(printf "%s\n" "${RESPONSE}" | _json_value refresh_token 1 1 || :)" - { _get_access_token_and_update "${RESPONSE}" && _update_config REFRESH_TOKEN "${REFRESH_TOKEN}" "${CONFIG}"; } || return 1 - } - printf "\n" + # If no input, then check if either -C option was used. + [ -z "${FINAL_LOCAL_INPUT_ARRAY:-${FINAL_ID_INPUT_ARRAY:-${FOLDERNAME}}}" ] && { + # if any account related option was used then don't show short help + [ -z "${DELETE_ACCOUNT_NAME:-${LIST_ACCOUNTS:-${NEW_ACCOUNT_NAME}}}" ] && _short_help + # exit right away if --list-accounts or --delete-account flag was used + [ -n "${DELETE_ACCOUNT_NAME:-${LIST_ACCOUNTS:-}}" ] && exit 0 + # don't exit right away when new account is created but also let the rootdir stuff execute + [ -n "${NEW_ACCOUNT_NAME}" ] && CONTINUE_WITH_NO_INPUT="true" } - { [ -z "${ACCESS_TOKEN}" ] || ! printf "%s\n" "${ACCESS_TOKEN}" | grep -qE "${ACCESS_TOKEN_REGEX}" || [ "${ACCESS_TOKEN_EXPIRY:-0}" -lt "$(date +'%s')" ]; } && - { _get_access_token_and_update || return 1; } - printf "%b\n" "ACCESS_TOKEN=\"${ACCESS_TOKEN}\"\nACCESS_TOKEN_EXPIRY=\"${ACCESS_TOKEN_EXPIRY}\"" >| "${TMPFILE}_ACCESS_TOKEN" - - # launch a background service to check access token and update it - # checks ACCESS_TOKEN_EXPIRY, try to update before 5 mins of expiry, a fresh token gets 60 mins - # process will be killed when script exits or "${MAIN_PID}" is killed - { - until ! kill -0 "${MAIN_PID}" 2>| /dev/null 1>&2; do - . "${TMPFILE}_ACCESS_TOKEN" - CURRENT_TIME="$(date +'%s')" - REMAINING_TOKEN_TIME="$((CURRENT_TIME - ACCESS_TOKEN_EXPIRY))" - if [ "${REMAINING_TOKEN_TIME}" -le 300 ]; then - # timeout after 30 seconds, it shouldn't take too long anyway, and update tmp config - CONFIG="${TMPFILE}_ACCESS_TOKEN" _timeout 30 _get_access_token_and_update || : - else - TOKEN_PROCESS_TIME_TO_SLEEP="$(if [ "${REMAINING_TOKEN_TIME}" -le 301 ]; then - printf "0\n" - else - printf "%s\n" "$((REMAINING_TOKEN_TIME - 300))" - fi)" - sleep "${TOKEN_PROCESS_TIME_TO_SLEEP}" - fi - sleep 1 - done - } & - ACCESS_TOKEN_SERVICE_PID="${!}" - return 0 } ################################################### # Setup root directory where all file/folders will be uploaded/updated -# Globals: 5 variables, 5 functions +# Globals: 5 variables, 6 functions # Variables - ROOTDIR, ROOT_FOLDER, UPDATE_DEFAULT_ROOTDIR, CONFIG, QUIET -# Functions - _print, _drive_info, _extract_id, _update_config, _json_value -# Arguments: 1 -# ${1}" = Positive integer ( amount of time in seconds to sleep ) +# Functions - _print_center, _drive_info, _extract_id, _update_config, _json_value, _set_value +# Arguments: None # Result: read description -# If root id not found then pribt message and exit +# If root id not found then print message and exit # Update config with root id and root id name if specified # Reference: -# https://github.com/dylanaraps/pure-sh-bible#use-read-as-an-alternative-to-the-sleep-command +# https://github.com/dylanaraps/pure-bash-bible#use-read-as-an-alternative-to-the-sleep-command ################################################### _setup_root_dir() { _check_root_id() { @@ -1377,29 +1763,32 @@ _setup_root_dir() { fi ROOT_FOLDER="${rootid_setup_root_dir}" - "${1:-:}" ROOT_FOLDER "${ROOT_FOLDER}" "${CONFIG}" + "${1:-:}" "ACCOUNT_${ACCOUNT_NAME}_ROOT_FOLDER" "${ROOT_FOLDER}" "${CONFIG}" || return 1 return 0 } _check_root_id_name() { ROOT_FOLDER_NAME="$(_drive_info "$(_extract_id "${ROOT_FOLDER}")" "name" | _json_value name 1 1 || :)" - "${1:-:}" ROOT_FOLDER_NAME "${ROOT_FOLDER_NAME}" "${CONFIG}" + "${1:-:}" "ACCOUNT_${ACCOUNT_NAME}_ROOT_FOLDER_NAME" "${ROOT_FOLDER_NAME}" "${CONFIG}" || return 1 return 0 } + _set_value indirect ROOT_FOLDER "ACCOUNT_${ACCOUNT_NAME}_ROOT_FOLDER" + _set_value indirect ROOT_FOLDER_NAME "ACCOUNT_${ACCOUNT_NAME}_ROOT_FOLDER_NAME" + if [ -n "${ROOTDIR:-}" ]; then ROOT_FOLDER="${ROOTDIR}" && { _check_root_id "${UPDATE_DEFAULT_ROOTDIR}" || return 1; } && unset ROOT_FOLDER_NAME elif [ -z "${ROOT_FOLDER}" ]; then { [ -t 1 ] && "${QUIET:-_print_center}" "normal" "Enter root folder ID or URL, press enter for default ( root )" " " && printf -- "-> " && read -r ROOT_FOLDER && [ -n "${ROOT_FOLDER}" ] && { _check_root_id _update_config || return 1; }; } || { ROOT_FOLDER="root" - _update_config ROOT_FOLDER "${ROOT_FOLDER}" "${CONFIG}" + _update_config "ACCOUNT_${ACCOUNT_NAME}_ROOT_FOLDER" "${ROOT_FOLDER}" "${CONFIG}" || return 1 } && printf "\n\n" elif [ -z "${ROOT_FOLDER_NAME}" ]; then - _check_root_id_name _update_config # update default root folder name if not available + _check_root_id_name _update_config || return 1 # update default root folder name if not available fi # fetch root folder name if rootdir different than default - [ -z "${ROOT_FOLDER_NAME}" ] && _check_root_id_name "${UPDATE_DEFAULT_ROOTDIR}" + [ -z "${ROOT_FOLDER_NAME}" ] && { _check_root_id_name "${UPDATE_DEFAULT_ROOTDIR}" || return 1; } return 0 } @@ -1443,10 +1832,7 @@ _setup_workspace() { # Result: Upload/Clone all the input files/folders, if a folder is empty, print Error message. ################################################### _process_arguments() { - export API_URL API_VERSION TOKEN_URL ACCESS_TOKEN \ - LOG_FILE_ID OVERWRITE UPLOAD_MODE SKIP_DUPLICATES CURL_SPEED RETRY SOURCE_UTILS UTILS_FOLDER TMPFILE \ - QUIET VERBOSE VERBOSE_PROGRESS CURL_PROGRESS CURL_PROGRESS_EXTRA CURL_PROGRESS_EXTRA_CLEAR COLUMNS EXTRA_LOG PARALLEL_UPLOAD - + export SOURCE_UTILS # on successful uploads _share_and_print_link() { "${SHARE:-:}" "${1:-}" "${SHARE_EMAIL}" @@ -1475,8 +1861,8 @@ _process_arguments() { for _ in 1 2; do _clear_line 1; done && continue fi elif [ -d "${input}" ]; then - input="$(cd "${input}" && pwd)" # to handle dirname when current directory (.) is given as input. - unset EMPTY # Used when input folder is empty + input="$(cd "${input}" && pwd)" || return 1 # to handle dirname when current directory (.) is given as input. + unset EMPTY # Used when input folder is empty _print_center "justify" "Given Input" ": FOLDER" "-" _print_center "justify" "Upload Method" ": ${SKIP_DUPLICATES:-${OVERWRITE:-Create}}" "=" && _newline "\n" @@ -1497,8 +1883,6 @@ _process_arguments() { FILENAMES="$(_tmp='find "'${input}'" -type f -name "*" '${INCLUDE_FILES}' '${EXCLUDE_FILES}'' && eval "${_tmp}")" _clear_line 1 - ERROR_STATUS=0 SUCCESS_STATUS=0 - # Skip the sub folders and find recursively all the files and upload them. if [ -n "${SKIP_SUBDIRS}" ]; then if [ -n "${FILENAMES}" ]; then @@ -1637,31 +2021,35 @@ main() { [ $# = 0 ] && _short_help if [ -z "${SELF_SOURCE}" ]; then - UTILS_FOLDER="${UTILS_FOLDER:-${PWD}}" && SOURCE_UTILS=". '${UTILS_FOLDER}/common-utils.sh' && . '${UTILS_FOLDER}/drive-utils.sh' && . '${UTILS_FOLDER}/upload-utils.sh'" - eval "${SOURCE_UTILS}" || { printf "Error: Unable to source util files.\n" && exit 1; } + export UTILS_FOLDER="${UTILS_FOLDER:-${PWD}}" + export SOURCE_UTILS='. '${UTILS_FOLDER}/auth-utils.sh' && . '${UTILS_FOLDER}/common-utils.sh' && . '${UTILS_FOLDER}/drive-utils.sh' && . '${UTILS_FOLDER}/upload-utils.sh'' else - SOURCE_UTILS="SOURCED_GUPLOAD=true . \"$(cd "$(_dirname "${0}")" && pwd)/${0##*\/}\"" && eval "${SOURCE_UTILS}" + SCRIPT_PATH="$(cd "$(_dirname "${0}")" && pwd)/${0##*\/}" && export SCRIPT_PATH + export SOURCE_UTILS='SOURCED_GUPLOAD=true . '${SCRIPT_PATH}'' fi + eval "${SOURCE_UTILS}" || { printf "Error: Unable to source util files.\n" && exit 1; } set -o errexit -o noclobber - _setup_arguments "${@}" - "${SKIP_INTERNET_CHECK:-_check_internet}" + _setup_arguments "${@}" || exit 1 + "${SKIP_INTERNET_CHECK:-_check_internet}" || exit 1 - { command -v mktemp 1>| /dev/null && TMPFILE="$(mktemp -u)"; } || TMPFILE="$(pwd)/.$(_t="$(date +'%s')" && printf "%s\n" "$((_t * _t))").LOG" + { { command -v mktemp 1>| /dev/null && TMPFILE="$(mktemp -u)"; } || + TMPFILE="$(pwd)/.$(_t="$(date +'%s')" && printf "%s\n" "$((_t * _t))").LOG"; } || exit 1 + export TMPFILE _cleanup() { # unhide the cursor if hidden - [ -n "${SUPPORT_ANSI_ESCAPES}" ] && printf "\e[?25h" + [ -n "${SUPPORT_ANSI_ESCAPES}" ] && printf "\e[?25h\e[?7h" { # update the config with latest ACCESS_TOKEN and ACCESS_TOKEN_EXPIRY only if changed [ -f "${TMPFILE}_ACCESS_TOKEN" ] && { . "${TMPFILE}_ACCESS_TOKEN" [ "${INITIAL_ACCESS_TOKEN}" = "${ACCESS_TOKEN}" ] || { - _update_config ACCESS_TOKEN "${ACCESS_TOKEN}" "${CONFIG}" - _update_config ACCESS_TOKEN_EXPIRY "${ACCESS_TOKEN_EXPIRY}" "${CONFIG}" + _update_config "ACCOUNT_${ACCOUNT_NAME}_ACCESS_TOKEN" "${ACCESS_TOKEN}" "${CONFIG}" + _update_config "ACCOUNT_${ACCOUNT_NAME}_ACCESS_TOKEN_EXPIRY" "${ACCESS_TOKEN_EXPIRY}" "${CONFIG}" } - } 1>| /dev/null + } || : 1>| /dev/null # grab all chidren processes of access token service # https://askubuntu.com/a/512872 @@ -1669,7 +2057,7 @@ main() { token_service_pids="$(ps --ppid="${ACCESS_TOKEN_SERVICE_PID}" -o pid=)" # first kill parent id, then children processes kill "${ACCESS_TOKEN_SERVICE_PID}" - } 1>| /dev/null + } || : 1>| /dev/null # grab all script children pids script_children_pids="$(ps --ppid="${MAIN_PID}" -o pid=)" @@ -1697,18 +2085,20 @@ main() { export MAIN_PID="$$" START="$(date +'%s')" - "${EXTRA_LOG}" "justify" "Starting script" "-" "${EXTRA_LOG}" "justify" "Checking credentials.." "-" - { _check_credentials && for _ in 1 2; do _clear_line 1; done; } || + { _check_credentials && _clear_line 1; } || { "${QUIET:-_print_center}" "normal" "[ Error: Credentials checking failed ]" "=" && exit 1; } - _print_center "justify" "Required credentials available." "=" + "${QUIET:-_print_center}" "normal" " Account: ${ACCOUNT_NAME} " "=" - "${EXTRA_LOG}" "justify" "Checking root dir and workspace folder.." "-" - { _setup_root_dir && for _ in 1 2; do _clear_line 1; done; } || + "${EXTRA_LOG}" "justify" "Checking root dir.." "-" + { _setup_root_dir && _clear_line 1; } || { "${QUIET:-_print_center}" "normal" "[ Error: Rootdir setup failed ]" "=" && exit 1; } _print_center "justify" "Root dir properly configured." "=" + # only execute next blocks if there was some input + [ -n "${CONTINUE_WITH_NO_INPUT}" ] && exit 0 + "${EXTRA_LOG}" "justify" "Checking Workspace Folder.." "-" { _setup_workspace && for _ in 1 2; do _clear_line 1; done; } || { "${QUIET:-_print_center}" "normal" "[ Error: Workspace setup failed ]" "=" && exit 1; } diff --git a/sh/upload-utils.sh b/sh/upload-utils.sh index 7532f99..583d4bf 100755 --- a/sh/upload-utils.sh +++ b/sh/upload-utils.sh @@ -121,7 +121,7 @@ _upload_file_main() { _upload_folder() { [ $# -lt 3 ] && printf "Missing arguments\n" && return 1 mode_upload_folder="${1}" PARSE_MODE="${2}" files_upload_folder="${3}" ID="${4:-}" && export PARSE_MODE ID - unset SUCCESS_STATUS SUCCESS_FILES ERROR_STATUS ERROR_FILES + SUCCESS_STATUS=0 SUCCESS_FILES="" ERROR_STATUS=0 ERROR_FILES="" case "${mode_upload_folder}" in normal) [ "${PARSE_MODE}" = parse ] && _clear_line 1 && _newline "\n" @@ -146,7 +146,7 @@ EOF [ -f "${TMPFILE}"ERROR ] && rm "${TMPFILE}"ERROR # shellcheck disable=SC2016 - (printf "%s\n" "${files_upload_folder}" | xargs -n1 -P"${NO_OF_PARALLEL_JOBS_FINAL}" -I {} sh -c ' + (printf "%s\n" "${files_upload_folder}" | xargs -P"${NO_OF_PARALLEL_JOBS_FINAL}" -I "{}" -n 1 sh -c ' eval "${SOURCE_UTILS}" _upload_file_main "${PARSE_MODE}" "{}" "${ID}" true ' 1>| "${TMPFILE}"SUCCESS 2>| "${TMPFILE}"ERROR) & diff --git a/sh/upload.sh b/sh/upload.sh index dc90299..1ce9033 100755 --- a/sh/upload.sh +++ b/sh/upload.sh @@ -9,6 +9,11 @@ Usage:\n ${0##*/} [options.. ] \n Foldername argument is optional. If not provided, the file will be uploaded to preconfigured google drive.\n File name argument is optional if create directory option is used.\n Options:\n + -a | --account 'account name' - Use different account than the default one.\n + To change the default account name, use this format, -a/--account default=account_name\n + -la | --list-accounts - Print all configured accounts in the config files.\n + -ca | --create-account 'account name' - To create a new account with the given name if does not already exists.\n + -da | --delete-account 'account name' - To delete an account information from config file. \n -c | -C | --create-dir - option to create directory. Will provide folder id. Can be used to provide input folder, see README.\n -r | --root-dir or - google folder ID/URL to which the file/directory is going to upload. If you want to change the default value, then use this format, -r/--root-dir default=root_folder_id/root_folder_url\n @@ -111,24 +116,25 @@ _setup_arguments() { [ $# = 0 ] && printf "Missing arguments\n" && return 1 # Internal variables # De-initialize if any variables set already. - unset FOLDERNAME FINAL_LOCAL_INPUT_ARRAY FINAL_ID_INPUT_ARRAY + unset LIST_ACCOUNTS UPDATE_DEFAULT_ACCOUNT CUSTOM_ACCOUNT_NAME NEW_ACCOUNT_NAME DELETE_ACCOUNT_NAME ACCOUNT_ONLY_RUN + unset FOLDERNAME FINAL_LOCAL_INPUT_ARRAY FINAL_ID_INPUT_ARRAY CONTINUE_WITH_NO_INPUT unset PARALLEL NO_OF_PARALLEL_JOBS SHARE SHARE_EMAIL OVERWRITE SKIP_DUPLICATES SKIP_SUBDIRS ROOTDIR QUIET unset VERBOSE VERBOSE_PROGRESS DEBUG LOG_FILE_ID CURL_SPEED RETRY - CURL_PROGRESS="-s" EXTRA_LOG=":" CURL_PROGRESS_EXTRA="-s" + export CURL_PROGRESS="-s" EXTRA_LOG=":" CURL_PROGRESS_EXTRA="-s" INFO_PATH="${HOME}/.google-drive-upload" CONFIG_INFO="${INFO_PATH}/google-drive-upload.configpath" [ -f "${CONFIG_INFO}" ] && . "${CONFIG_INFO}" CONFIG="${CONFIG:-${HOME}/.googledrive.conf}" # Configuration variables # Remote gDrive variables unset ROOT_FOLDER CLIENT_ID CLIENT_SECRET REFRESH_TOKEN ACCESS_TOKEN - API_URL="https://www.googleapis.com" - API_VERSION="v3" - SCOPE="${API_URL}/auth/drive" - REDIRECT_URI="urn:ietf:wg:oauth:2.0:oob" - TOKEN_URL="https://accounts.google.com/o/oauth2/token" + export API_URL="https://www.googleapis.com" + export API_VERSION="v3" \ + SCOPE="${API_URL}/auth/drive" \ + REDIRECT_URI="urn:ietf:wg:oauth:2.0:oob" \ + TOKEN_URL="https://accounts.google.com/o/oauth2/token" _check_config() { - [ -z "${1##default=*}" ] && UPDATE_DEFAULT_CONFIG="_update_config" + [ -z "${1##default=*}" ] && export UPDATE_DEFAULT_CONFIG="_update_config" { [ -r "${2}" ] && CONFIG="${2}"; } || { printf "Error: Given config file (%s) doesn't exist/not readable,..\n" "${1}" 1>&2 && exit 1 } @@ -147,6 +153,21 @@ _setup_arguments() { -h | --help) _usage ;; -D | --debug) DEBUG="true" && export DEBUG ;; --info) _version_info ;; + -a | --account) + _check_longoptions "${1}" "${2}" + export CUSTOM_ACCOUNT_NAME="${2##default=}" && shift + [ -z "${2##default=*}" ] && export UPDATE_DEFAULT_ACCOUNT="_update_config" + ;; + -la | --list-accounts) export LIST_ACCOUNTS="true" ;; + # this flag is preferred over --account + -ca | --create-account) + _check_longoptions "${1}" "${2}" + export NEW_ACCOUNT_NAME="${2}" && shift + ;; + -da | --delete-account) + _check_longoptions "${1}" "${2}" + export DELETE_ACCOUNT_NAME="${2}" && shift + ;; -c | -C | --create-dir) _check_longoptions "${1}" "${2}" FOLDERNAME="${2}" && shift @@ -154,7 +175,7 @@ _setup_arguments() { -r | --root-dir) _check_longoptions "${1}" "${2}" ROOTDIR="${2##default=}" - [ -z "${2##default=*}" ] && UPDATE_DEFAULT_ROOTDIR="_update_config" + [ -z "${2##default=*}" ] && export UPDATE_DEFAULT_ROOTDIR="_update_config" shift ;; -z | --config) @@ -164,9 +185,9 @@ _setup_arguments() { ;; -i | --save-info) _check_longoptions "${1}" "${2}" - LOG_FILE_ID="${2}" && shift + export LOG_FILE_ID="${2}" && shift ;; - -s | --skip-subdirs) SKIP_SUBDIRS="true" ;; + -s | --skip-subdirs) export SKIP_SUBDIRS="true" ;; -p | --parallel) _check_longoptions "${1}" "${2}" NO_OF_PARALLEL_JOBS="${2}" @@ -176,10 +197,10 @@ _setup_arguments() { printf "\nError: -p/--parallel value ranges between 1 to 10.\n" exit 1 fi - PARALLEL_UPLOAD="parallel" && shift + export PARALLEL_UPLOAD="parallel" && shift ;; - -o | --overwrite) OVERWRITE="Overwrite" && UPLOAD_MODE="update" ;; - -d | --skip-duplicates) SKIP_DUPLICATES="Skip Existing" && UPLOAD_MODE="update" ;; + -o | --overwrite) export OVERWRITE="Overwrite" UPLOAD_MODE="update" ;; + -d | --skip-duplicates) export SKIP_DUPLICATES="Skip Existing" UPLOAD_MODE="update" ;; -f | --file | --folder) _check_longoptions "${1}" "${2}" LOCAL_INPUT_ARRAY="${LOCAL_INPUT_ARRAY} @@ -201,12 +222,13 @@ _setup_arguments() { shift ;; esac + export SHARE_EMAIL ;; --speed) _check_longoptions "${1}" "${2}" regex='^([0-9]+)([k,K]|[m,M]|[g,G])+$' if printf "%s\n" "${2}" | grep -qE "${regex}"; then - CURL_SPEED="--limit-rate ${2}" && shift + export CURL_SPEED="--limit-rate ${2}" && shift else printf "Error: Wrong speed limit format, supported formats: 1K , 1M and 1G\n" 1>&2 exit 1 @@ -215,7 +237,7 @@ _setup_arguments() { -R | --retry) _check_longoptions "${1}" "${2}" if [ "$((2))" -gt 0 ] 2>| /dev/null 1>&2; then - RETRY="${2}" && shift + export RETRY="${2}" && shift else printf "Error: -R/--retry only takes positive integers as arguments, min = 1, max = infinity.\n" exit 1 @@ -230,10 +252,10 @@ _setup_arguments() { EXCLUDE_FILES="${EXCLUDE_FILES} ! -name '${2}' " && shift ;; --hide) HIDE_INFO=":" ;; - -q | --quiet) QUIET="_print_center_quiet" ;; - -v | --verbose) VERBOSE="true" ;; - -V | --verbose-progress) VERBOSE_PROGRESS="true" ;; - --skip-internet-check) SKIP_INTERNET_CHECK=":" ;; + -q | --quiet) export QUIET="_print_center_quiet" ;; + -v | --verbose) export VERBOSE="true" ;; + -V | --verbose-progress) export VERBOSE_PROGRESS="true" ;; + --skip-internet-check) export SKIP_INTERNET_CHECK=":" ;; '') shorthelp ;; *) # Check if user meant it to be a flag if [ -z "${1##-*}" ]; then @@ -267,8 +289,18 @@ _setup_arguments() { _check_debug - [ -n "${VERBOSE_PROGRESS}" ] && unset VERBOSE && CURL_PROGRESS="" - [ -n "${QUIET}" ] && CURL_PROGRESS="-s" + [ -n "${VERBOSE_PROGRESS}" ] && unset VERBOSE && export CURL_PROGRESS="" + [ -n "${QUIET}" ] && export CURL_PROGRESS="-s" + + # create info path folder, can be missing if gupload was not installed with install.sh + mkdir -p "${INFO_PATH}" || return 1 + + # handle account related flags here as we want to use the flags independenlty even with no normal valid inputs + # delete account, --delete-account flag + # TODO: add support for deleting multiple accounts + [ -n "${DELETE_ACCOUNT_NAME}" ] && _delete_account "${DELETE_ACCOUNT_NAME}" + # list all configured accounts, --list-accounts flag + [ -n "${LIST_ACCOUNTS}" ] && _all_accounts # If no input, then check if -C option was used or not. # check if given input exists ( file/folder ) @@ -279,164 +311,30 @@ _setup_arguments() { } done)" - [ -z "${FINAL_LOCAL_INPUT_ARRAY:-${FINAL_ID_INPUT_ARRAY:-${FOLDERNAME}}}" ] && _short_help - - # create info path folder, can be missing if gupload was not installed with install.sh - mkdir -p "${INFO_PATH}" - - return 0 -} - -################################################### -# Check Oauth credentials and create/update config file -# Client ID, Client Secret, Refesh Token and Access Token -# Globals: 10 variables, 3 functions -# Variables - API_URL, API_VERSION, TOKEN URL, -# CONFIG, UPDATE_DEFAULT_CONFIG, INFO_PATH, -# CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN and ACCESS_TOKEN -# Functions - _update_config, _json_value and _print -# Arguments: None -# Result: read description -################################################### -_check_credentials() { - # Config file is created automatically after first run - [ -r "${CONFIG}" ] && . "${CONFIG}" - "${UPDATE_DEFAULT_CONFIG:-:}" CONFIG "${CONFIG}" "${CONFIG_INFO}" - - ! [ -t 1 ] && [ -z "${CLIENT_ID:+${CLIENT_SECRET:+${REFRESH_TOKEN}}}" ] && { - printf "%s\n" "Error: Script is not running in a terminal, cannot ask for credentials." - printf "%s\n" "Add in config manually if terminal is not accessible. CLIENT_ID, CLIENT_SECRET and REFRESH_TOKEN is required." && return 1 - } - - # Following https://developers.google.com/identity/protocols/oauth2#size - CLIENT_ID_REGEX='[0-9]+-[0-9A-Za-z_]{32}\.apps\.googleusercontent\.com' - CLIENT_SECRET_REGEX='[0-9A-Za-z_-]+' - REFRESH_TOKEN_REGEX='[0-9]//[0-9A-Za-z_-]+' # 512 bytes - ACCESS_TOKEN_REGEX='ya29\.[0-9A-Za-z_-]+' # 2048 bytes - AUTHORIZATION_CODE_REGEX='[0-9]/[0-9A-Za-z_-]+' # 256 bytes - - until [ -n "${CLIENT_ID}" ] && [ -n "${CLIENT_ID_VALID}" ]; do - [ -n "${CLIENT_ID}" ] && { - if printf "%s\n" "${CLIENT_ID}" | grep -qE "${CLIENT_ID_REGEX}"; then - [ -n "${client_id}" ] && _update_config CLIENT_ID "${CLIENT_ID}" "${CONFIG}" - CLIENT_ID_VALID="true" && continue - else - { [ -n "${client_id}" ] && message="- Try again"; } || message="in config ( ${CONFIG} )" - "${QUIET:-_print_center}" "normal" " Invalid Client ID ${message} " "-" && unset CLIENT_ID client_id - fi - } - [ -z "${client_id}" ] && printf "\n" && "${QUIET:-_print_center}" "normal" " Enter Client ID " "-" - [ -n "${client_id}" ] && _clear_line 1 - printf -- "-> " - read -r CLIENT_ID && client_id=1 - done - - until [ -n "${CLIENT_SECRET}" ] && [ -n "${CLIENT_SECRET_VALID}" ]; do - [ -n "${CLIENT_SECRET}" ] && { - if printf "%s\n" "${CLIENT_SECRET}" | grep -qE "${CLIENT_SECRET_REGEX}"; then - [ -n "${client_secret}" ] && _update_config CLIENT_SECRET "${CLIENT_SECRET}" "${CONFIG}" - CLIENT_SECRET_VALID="true" && continue - else - { [ -n "${client_secret}" ] && message="- Try again"; } || message="in config ( ${CONFIG} )" - "${QUIET:-_print_center}" "normal" " Invalid Client Secret ${message} " "-" && unset CLIENT_SECRET client_secret - fi - } - [ -z "${client_secret}" ] && printf "\n" && "${QUIET:-_print_center}" "normal" " Enter Client Secret " "-" - [ -n "${client_secret}" ] && _clear_line 1 - printf -- "-> " - read -r CLIENT_SECRET && client_secret=1 - done - - [ -n "${REFRESH_TOKEN}" ] && { - ! printf "%s\n" "${REFRESH_TOKEN}" | grep -qE "${REFRESH_TOKEN_REGEX}" && - "${QUIET:-_print_center}" "normal" " Error: Invalid Refresh token in config file, follow below steps.. " "-" && unset REFRESH_TOKEN - } - - [ -z "${REFRESH_TOKEN}" ] && { - printf "\n" && "${QUIET:-_print_center}" "normal" "If you have a refresh token generated, then type the token, else leave blank and press return key.." " " - printf "\n" && "${QUIET:-_print_center}" "normal" " Refresh Token " "-" && printf -- "-> " - read -r REFRESH_TOKEN - if [ -n "${REFRESH_TOKEN}" ]; then - "${QUIET:-_print_center}" "normal" " Checking refresh token.. " "-" - if ! printf "%s\n" "${REFRESH_TOKEN}" | grep -qE "${REFRESH_TOKEN_REGEX}"; then - { _get_access_token_and_update && _update_config REFRESH_TOKEN "${REFRESH_TOKEN}" "${CONFIG}"; } || check_error=1 - else - check_error=true - fi - [ -n "${check_error}" ] && "${QUIET:-_print_center}" "normal" " Error: Invalid Refresh token given, follow below steps to generate.. " "-" && unset REFRESH_TOKEN - else - "${QUIET:-_print_center}" "normal" " No Refresh token given, follow below steps to generate.. " "-" - fi - - [ -z "${REFRESH_TOKEN}" ] && { - printf "\n" && "${QUIET:-_print_center}" "normal" "Visit the below URL, tap on allow and then enter the code obtained" " " - URL="https://accounts.google.com/o/oauth2/auth?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&scope=${SCOPE}&response_type=code&prompt=consent" - printf "\n%s\n" "${URL}" - until [ -n "${AUTHORIZATION_CODE}" ] && [ -n "${AUTHORIZATION_CODE_VALID}" ]; do - [ -n "${AUTHORIZATION_CODE}" ] && { - if printf "%s\n" "${AUTHORIZATION_CODE}" | grep -qE "${AUTHORIZATION_CODE_REGEX}"; then - AUTHORIZATION_CODE_VALID="true" && continue - else - "${QUIET:-_print_center}" "normal" " Invalid CODE given, try again.. " "-" && unset AUTHORIZATION_CODE authorization_code - fi - } - { [ -z "${authorization_code}" ] && printf "\n" && "${QUIET:-_print_center}" "normal" " Enter the authorization code " "-"; } || _clear_line 1 - printf -- "-> " - read -r AUTHORIZATION_CODE && authorization_code=1 - done - RESPONSE="$(curl --compressed "${CURL_PROGRESS}" -X POST \ - --data "code=${AUTHORIZATION_CODE}&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&redirect_uri=${REDIRECT_URI}&grant_type=authorization_code" "${TOKEN_URL}")" || : - _clear_line 1 1>&2 - - REFRESH_TOKEN="$(printf "%s\n" "${RESPONSE}" | _json_value refresh_token 1 1 || :)" - { _get_access_token_and_update "${RESPONSE}" && _update_config REFRESH_TOKEN "${REFRESH_TOKEN}" "${CONFIG}"; } || return 1 - } - printf "\n" + # If no input, then check if either -C option was used. + [ -z "${FINAL_LOCAL_INPUT_ARRAY:-${FINAL_ID_INPUT_ARRAY:-${FOLDERNAME}}}" ] && { + # if any account related option was used then don't show short help + [ -z "${DELETE_ACCOUNT_NAME:-${LIST_ACCOUNTS:-${NEW_ACCOUNT_NAME}}}" ] && _short_help + # exit right away if --list-accounts or --delete-account flag was used + [ -n "${DELETE_ACCOUNT_NAME:-${LIST_ACCOUNTS:-}}" ] && exit 0 + # don't exit right away when new account is created but also let the rootdir stuff execute + [ -n "${NEW_ACCOUNT_NAME}" ] && CONTINUE_WITH_NO_INPUT="true" } - { [ -z "${ACCESS_TOKEN}" ] || ! printf "%s\n" "${ACCESS_TOKEN}" | grep -qE "${ACCESS_TOKEN_REGEX}" || [ "${ACCESS_TOKEN_EXPIRY:-0}" -lt "$(date +'%s')" ]; } && - { _get_access_token_and_update || return 1; } - printf "%b\n" "ACCESS_TOKEN=\"${ACCESS_TOKEN}\"\nACCESS_TOKEN_EXPIRY=\"${ACCESS_TOKEN_EXPIRY}\"" >| "${TMPFILE}_ACCESS_TOKEN" - - # launch a background service to check access token and update it - # checks ACCESS_TOKEN_EXPIRY, try to update before 5 mins of expiry, a fresh token gets 60 mins - # process will be killed when script exits or "${MAIN_PID}" is killed - { - until ! kill -0 "${MAIN_PID}" 2>| /dev/null 1>&2; do - . "${TMPFILE}_ACCESS_TOKEN" - CURRENT_TIME="$(date +'%s')" - REMAINING_TOKEN_TIME="$((CURRENT_TIME - ACCESS_TOKEN_EXPIRY))" - if [ "${REMAINING_TOKEN_TIME}" -le 300 ]; then - # timeout after 30 seconds, it shouldn't take too long anyway, and update tmp config - CONFIG="${TMPFILE}_ACCESS_TOKEN" _timeout 30 _get_access_token_and_update || : - else - TOKEN_PROCESS_TIME_TO_SLEEP="$(if [ "${REMAINING_TOKEN_TIME}" -le 301 ]; then - printf "0\n" - else - printf "%s\n" "$((REMAINING_TOKEN_TIME - 300))" - fi)" - sleep "${TOKEN_PROCESS_TIME_TO_SLEEP}" - fi - sleep 1 - done - } & - ACCESS_TOKEN_SERVICE_PID="${!}" - return 0 } ################################################### # Setup root directory where all file/folders will be uploaded/updated -# Globals: 5 variables, 5 functions +# Globals: 5 variables, 6 functions # Variables - ROOTDIR, ROOT_FOLDER, UPDATE_DEFAULT_ROOTDIR, CONFIG, QUIET -# Functions - _print, _drive_info, _extract_id, _update_config, _json_value -# Arguments: 1 -# ${1}" = Positive integer ( amount of time in seconds to sleep ) +# Functions - _print_center, _drive_info, _extract_id, _update_config, _json_value, _set_value +# Arguments: None # Result: read description -# If root id not found then pribt message and exit +# If root id not found then print message and exit # Update config with root id and root id name if specified # Reference: -# https://github.com/dylanaraps/pure-sh-bible#use-read-as-an-alternative-to-the-sleep-command +# https://github.com/dylanaraps/pure-bash-bible#use-read-as-an-alternative-to-the-sleep-command ################################################### _setup_root_dir() { _check_root_id() { @@ -451,29 +349,32 @@ _setup_root_dir() { fi ROOT_FOLDER="${rootid_setup_root_dir}" - "${1:-:}" ROOT_FOLDER "${ROOT_FOLDER}" "${CONFIG}" + "${1:-:}" "ACCOUNT_${ACCOUNT_NAME}_ROOT_FOLDER" "${ROOT_FOLDER}" "${CONFIG}" || return 1 return 0 } _check_root_id_name() { ROOT_FOLDER_NAME="$(_drive_info "$(_extract_id "${ROOT_FOLDER}")" "name" | _json_value name 1 1 || :)" - "${1:-:}" ROOT_FOLDER_NAME "${ROOT_FOLDER_NAME}" "${CONFIG}" + "${1:-:}" "ACCOUNT_${ACCOUNT_NAME}_ROOT_FOLDER_NAME" "${ROOT_FOLDER_NAME}" "${CONFIG}" || return 1 return 0 } + _set_value indirect ROOT_FOLDER "ACCOUNT_${ACCOUNT_NAME}_ROOT_FOLDER" + _set_value indirect ROOT_FOLDER_NAME "ACCOUNT_${ACCOUNT_NAME}_ROOT_FOLDER_NAME" + if [ -n "${ROOTDIR:-}" ]; then ROOT_FOLDER="${ROOTDIR}" && { _check_root_id "${UPDATE_DEFAULT_ROOTDIR}" || return 1; } && unset ROOT_FOLDER_NAME elif [ -z "${ROOT_FOLDER}" ]; then { [ -t 1 ] && "${QUIET:-_print_center}" "normal" "Enter root folder ID or URL, press enter for default ( root )" " " && printf -- "-> " && read -r ROOT_FOLDER && [ -n "${ROOT_FOLDER}" ] && { _check_root_id _update_config || return 1; }; } || { ROOT_FOLDER="root" - _update_config ROOT_FOLDER "${ROOT_FOLDER}" "${CONFIG}" + _update_config "ACCOUNT_${ACCOUNT_NAME}_ROOT_FOLDER" "${ROOT_FOLDER}" "${CONFIG}" || return 1 } && printf "\n\n" elif [ -z "${ROOT_FOLDER_NAME}" ]; then - _check_root_id_name _update_config # update default root folder name if not available + _check_root_id_name _update_config || return 1 # update default root folder name if not available fi # fetch root folder name if rootdir different than default - [ -z "${ROOT_FOLDER_NAME}" ] && _check_root_id_name "${UPDATE_DEFAULT_ROOTDIR}" + [ -z "${ROOT_FOLDER_NAME}" ] && { _check_root_id_name "${UPDATE_DEFAULT_ROOTDIR}" || return 1; } return 0 } @@ -517,10 +418,7 @@ _setup_workspace() { # Result: Upload/Clone all the input files/folders, if a folder is empty, print Error message. ################################################### _process_arguments() { - export API_URL API_VERSION TOKEN_URL ACCESS_TOKEN \ - LOG_FILE_ID OVERWRITE UPLOAD_MODE SKIP_DUPLICATES CURL_SPEED RETRY SOURCE_UTILS UTILS_FOLDER TMPFILE \ - QUIET VERBOSE VERBOSE_PROGRESS CURL_PROGRESS CURL_PROGRESS_EXTRA CURL_PROGRESS_EXTRA_CLEAR COLUMNS EXTRA_LOG PARALLEL_UPLOAD - + export SOURCE_UTILS # on successful uploads _share_and_print_link() { "${SHARE:-:}" "${1:-}" "${SHARE_EMAIL}" @@ -549,8 +447,8 @@ _process_arguments() { for _ in 1 2; do _clear_line 1; done && continue fi elif [ -d "${input}" ]; then - input="$(cd "${input}" && pwd)" # to handle dirname when current directory (.) is given as input. - unset EMPTY # Used when input folder is empty + input="$(cd "${input}" && pwd)" || return 1 # to handle dirname when current directory (.) is given as input. + unset EMPTY # Used when input folder is empty _print_center "justify" "Given Input" ": FOLDER" "-" _print_center "justify" "Upload Method" ": ${SKIP_DUPLICATES:-${OVERWRITE:-Create}}" "=" && _newline "\n" @@ -571,8 +469,6 @@ _process_arguments() { FILENAMES="$(_tmp='find "'${input}'" -type f -name "*" '${INCLUDE_FILES}' '${EXCLUDE_FILES}'' && eval "${_tmp}")" _clear_line 1 - ERROR_STATUS=0 SUCCESS_STATUS=0 - # Skip the sub folders and find recursively all the files and upload them. if [ -n "${SKIP_SUBDIRS}" ]; then if [ -n "${FILENAMES}" ]; then @@ -711,31 +607,35 @@ main() { [ $# = 0 ] && _short_help if [ -z "${SELF_SOURCE}" ]; then - UTILS_FOLDER="${UTILS_FOLDER:-${PWD}}" && SOURCE_UTILS=". '${UTILS_FOLDER}/common-utils.sh' && . '${UTILS_FOLDER}/drive-utils.sh' && . '${UTILS_FOLDER}/upload-utils.sh'" - eval "${SOURCE_UTILS}" || { printf "Error: Unable to source util files.\n" && exit 1; } + export UTILS_FOLDER="${UTILS_FOLDER:-${PWD}}" + export SOURCE_UTILS='. '${UTILS_FOLDER}/auth-utils.sh' && . '${UTILS_FOLDER}/common-utils.sh' && . '${UTILS_FOLDER}/drive-utils.sh' && . '${UTILS_FOLDER}/upload-utils.sh'' else - SOURCE_UTILS="SOURCED_GUPLOAD=true . \"$(cd "$(_dirname "${0}")" && pwd)/${0##*\/}\"" && eval "${SOURCE_UTILS}" + SCRIPT_PATH="$(cd "$(_dirname "${0}")" && pwd)/${0##*\/}" && export SCRIPT_PATH + export SOURCE_UTILS='SOURCED_GUPLOAD=true . '${SCRIPT_PATH}'' fi + eval "${SOURCE_UTILS}" || { printf "Error: Unable to source util files.\n" && exit 1; } set -o errexit -o noclobber - _setup_arguments "${@}" - "${SKIP_INTERNET_CHECK:-_check_internet}" + _setup_arguments "${@}" || exit 1 + "${SKIP_INTERNET_CHECK:-_check_internet}" || exit 1 - { command -v mktemp 1>| /dev/null && TMPFILE="$(mktemp -u)"; } || TMPFILE="$(pwd)/.$(_t="$(date +'%s')" && printf "%s\n" "$((_t * _t))").LOG" + { { command -v mktemp 1>| /dev/null && TMPFILE="$(mktemp -u)"; } || + TMPFILE="$(pwd)/.$(_t="$(date +'%s')" && printf "%s\n" "$((_t * _t))").LOG"; } || exit 1 + export TMPFILE _cleanup() { # unhide the cursor if hidden - [ -n "${SUPPORT_ANSI_ESCAPES}" ] && printf "\e[?25h" + [ -n "${SUPPORT_ANSI_ESCAPES}" ] && printf "\e[?25h\e[?7h" { # update the config with latest ACCESS_TOKEN and ACCESS_TOKEN_EXPIRY only if changed [ -f "${TMPFILE}_ACCESS_TOKEN" ] && { . "${TMPFILE}_ACCESS_TOKEN" [ "${INITIAL_ACCESS_TOKEN}" = "${ACCESS_TOKEN}" ] || { - _update_config ACCESS_TOKEN "${ACCESS_TOKEN}" "${CONFIG}" - _update_config ACCESS_TOKEN_EXPIRY "${ACCESS_TOKEN_EXPIRY}" "${CONFIG}" + _update_config "ACCOUNT_${ACCOUNT_NAME}_ACCESS_TOKEN" "${ACCESS_TOKEN}" "${CONFIG}" + _update_config "ACCOUNT_${ACCOUNT_NAME}_ACCESS_TOKEN_EXPIRY" "${ACCESS_TOKEN_EXPIRY}" "${CONFIG}" } - } 1>| /dev/null + } || : 1>| /dev/null # grab all chidren processes of access token service # https://askubuntu.com/a/512872 @@ -743,7 +643,7 @@ main() { token_service_pids="$(ps --ppid="${ACCESS_TOKEN_SERVICE_PID}" -o pid=)" # first kill parent id, then children processes kill "${ACCESS_TOKEN_SERVICE_PID}" - } 1>| /dev/null + } || : 1>| /dev/null # grab all script children pids script_children_pids="$(ps --ppid="${MAIN_PID}" -o pid=)" @@ -771,18 +671,20 @@ main() { export MAIN_PID="$$" START="$(date +'%s')" - "${EXTRA_LOG}" "justify" "Starting script" "-" "${EXTRA_LOG}" "justify" "Checking credentials.." "-" - { _check_credentials && for _ in 1 2; do _clear_line 1; done; } || + { _check_credentials && _clear_line 1; } || { "${QUIET:-_print_center}" "normal" "[ Error: Credentials checking failed ]" "=" && exit 1; } - _print_center "justify" "Required credentials available." "=" + "${QUIET:-_print_center}" "normal" " Account: ${ACCOUNT_NAME} " "=" - "${EXTRA_LOG}" "justify" "Checking root dir and workspace folder.." "-" - { _setup_root_dir && for _ in 1 2; do _clear_line 1; done; } || + "${EXTRA_LOG}" "justify" "Checking root dir.." "-" + { _setup_root_dir && _clear_line 1; } || { "${QUIET:-_print_center}" "normal" "[ Error: Rootdir setup failed ]" "=" && exit 1; } _print_center "justify" "Root dir properly configured." "=" + # only execute next blocks if there was some input + [ -n "${CONTINUE_WITH_NO_INPUT}" ] && exit 0 + "${EXTRA_LOG}" "justify" "Checking Workspace Folder.." "-" { _setup_workspace && for _ in 1 2; do _clear_line 1; done; } || { "${QUIET:-_print_center}" "normal" "[ Error: Workspace setup failed ]" "=" && exit 1; }