1
0
forked from baron/baron-sso
Files
baron-sso/scripts/docker-image/upload_works_drive.sh

774 lines
26 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd "$script_dir/../.." && pwd)"
source "$repo_root/scripts/backup/lib/common.sh"
if [[ -f "$repo_root/.env" ]]; then
env_override_keys=(
DOCKER_IMAGE_REF
IMAGE_REF
WORKS_DOCKER_COMMIT_CONTAINER
DOCKER_COMMIT_CONTAINER
WORKS_DOCKER_IMAGE_ARCHIVE_DIR
WORKS_SHAREDRIVE_DOCKER_IMAGE_DIR
WORKS_DRIVE_TARGET
WORKS_DRIVE_SHARED_DRIVE_ID
WORKS_DRIVE_PARENT_FILE_ID
WORKS_DRIVE_USER_ID
WORKS_DRIVE_GROUP_ID
WORKS_DRIVE_SHARED_FOLDER_ID
WORKS_DRIVE_ACCESS_TOKEN
WORKS_DRIVE_ACCESS_TOKEN_FILE
WORKS_DRIVE_ACCESS_TOKEN_CMD
WORKS_DRIVE_OAUTH_SCOPE
WORKS_DRIVE_API_BASE_URL
WORKS_DRIVE_OAUTH_TOKEN_URL
WORKS_DRIVE_OVERWRITE
WORKS_DRIVE_DRY_RUN
WORKS_DRIVE_CURL_BIN
WORKS_DRIVE_SHAREDRIVE_ID
WORKS_DRIVE_OAUTH_CLIENT_ID
WORKS_DRIVE_OAUTH_CLIENT_SECRET
WORKS_DRIVE_OAUTH_CLIENT_SERVICE_ACCOUNT
WORKS_DRIVE_OAUTH_CLIENT_PRIVATE_KEY
WORKS_DRIVE_OAUTH_CLIENT_PRIVATE_KEY_FILE
WORKS_DRIVE_OAUTH_REFRESH_TOKEN
WORKS_SHAREDRIVE_ID
WORKS_ADMIN_API_BASE_URL
WORKS_ADMIN_OAUTH_TOKEN_URL
)
declare -A env_override_values=()
env_override_set=()
for env_key in "${env_override_keys[@]}"; do
if [[ -v "$env_key" ]]; then
env_override_set+=("$env_key")
env_override_values["$env_key"]="${!env_key}"
fi
done
set -a
# shellcheck source=/dev/null
source "$repo_root/.env"
set +a
for env_key in "${env_override_set[@]}"; do
printf -v "$env_key" '%s' "${env_override_values[$env_key]}"
export "$env_key"
done
fi
backup_require_command docker
backup_require_command jq
backup_require_command sha256sum
backup_require_command stat
backup_require_command zstd
image_ref="${DOCKER_IMAGE_REF:-${IMAGE_REF:-}}"
[[ -n "$image_ref" ]] || backup_die "DOCKER_IMAGE_REF is required. Example: DOCKER_IMAGE_REF=registry.example/baron_sso/backend:v1.2606.ab12"
commit_container="${WORKS_DOCKER_COMMIT_CONTAINER:-${DOCKER_COMMIT_CONTAINER:-}}"
archive_root="${WORKS_DOCKER_IMAGE_ARCHIVE_DIR:-/tmp/baron-sso-docker-image-upload}"
folder_cache_file="${WORKS_DOCKER_IMAGE_FOLDER_CACHE_FILE:-${archive_root}/.works-folder-cache.json}"
image_root_dir="${WORKS_DRIVE_DOCKER_IMAGE_DIR:-${WORKS_SHAREDRIVE_DOCKER_IMAGE_DIR:-baron-sso}}"
dry_run="${WORKS_DRIVE_DRY_RUN:-false}"
target="${WORKS_DRIVE_TARGET:-sharedrive}"
api_base_url="${WORKS_DRIVE_API_BASE_URL:-${WORKS_ADMIN_API_BASE_URL:-https://www.worksapis.com}}"
curl_bin="${WORKS_DRIVE_CURL_BIN:-curl}"
overwrite="${WORKS_DRIVE_OVERWRITE:-true}"
upload_scope="${WORKS_DRIVE_OAUTH_SCOPE:-file}"
WORKS_DRIVE_SHARED_DRIVE_ID="${WORKS_DRIVE_SHARED_DRIVE_ID:-${WORKS_DRIVE_SHAREDRIVE_ID:-${WORKS_SHAREDRIVE_ID:-}}}"
folder_cache_scope() {
printf '%s:%s:%s' "$target" "${WORKS_DRIVE_SHARED_DRIVE_ID:-${WORKS_DRIVE_USER_ID:-${WORKS_DRIVE_GROUP_ID:-${WORKS_DRIVE_SHARED_FOLDER_ID:-}}}}" "${WORKS_DRIVE_PARENT_FILE_ID:-root}"
}
read_cached_folder_id() {
local path="$1"
local key
[[ -f "$folder_cache_file" ]] || return 0
key="$(folder_cache_scope):${path}"
jq -er --arg key "$key" '.[$key] // empty' "$folder_cache_file" 2>/dev/null || true
}
write_cached_folder_id() {
local path="$1"
local folder_id="$2"
local key
local tmp_file
[[ -n "$folder_id" ]] || return 0
mkdir -p "$(dirname "$folder_cache_file")"
[[ -f "$folder_cache_file" ]] || printf '{}\n' >"$folder_cache_file"
key="$(folder_cache_scope):${path}"
tmp_file="${folder_cache_file}.tmp"
jq --arg key "$key" --arg folderId "$folder_id" '. + {($key): $folderId}' "$folder_cache_file" >"$tmp_file"
mv "$tmp_file" "$folder_cache_file"
}
urlencode_path() {
jq -nr --arg value "$1" '$value|@uri'
}
split_curl_response() {
local response="$1"
local __body_var="$2"
local __status_var="$3"
local status
local body
status="$(tail -n 1 <<<"$response")"
if [[ "$status" =~ ^[0-9][0-9][0-9]$ ]]; then
body="$(sed '$d' <<<"$response")"
else
status="200"
body="$response"
fi
printf -v "$__body_var" '%s' "$body"
printf -v "$__status_var" '%s' "$status"
}
redact_for_log() {
sed -E 's/("(access_token|refresh_token|assertion|client_secret|Authorization)"[[:space:]]*:[[:space:]]*)"[^"]*"/\1"REDACTED"/Ig'
}
resolve_target_upload_endpoint() {
local parent_file_id="${1:-${WORKS_DRIVE_PARENT_FILE_ID:-}}"
local encoded_parent=""
if [[ -n "$parent_file_id" ]]; then
encoded_parent="$(urlencode_path "$parent_file_id")"
fi
case "$target" in
sharedrive)
[[ -n "${WORKS_DRIVE_SHARED_DRIVE_ID:-}" ]] || backup_die "WORKS_DRIVE_SHARED_DRIVE_ID is required when WORKS_DRIVE_TARGET=sharedrive."
local shared_drive_id
shared_drive_id="$(urlencode_path "$WORKS_DRIVE_SHARED_DRIVE_ID")"
if [[ -n "$encoded_parent" ]]; then
printf '%s/v1.0/sharedrives/%s/files/%s\n' "$api_base_url" "$shared_drive_id" "$encoded_parent"
else
printf '%s/v1.0/sharedrives/%s/files\n' "$api_base_url" "$shared_drive_id"
fi
;;
mydrive)
local user_id="${WORKS_DRIVE_USER_ID:-me}"
user_id="$(urlencode_path "$user_id")"
if [[ -n "$encoded_parent" ]]; then
printf '%s/v1.0/users/%s/drive/files/%s\n' "$api_base_url" "$user_id" "$encoded_parent"
else
printf '%s/v1.0/users/%s/drive/files\n' "$api_base_url" "$user_id"
fi
;;
group)
[[ -n "${WORKS_DRIVE_GROUP_ID:-}" ]] || backup_die "WORKS_DRIVE_GROUP_ID is required when WORKS_DRIVE_TARGET=group."
local group_id
group_id="$(urlencode_path "$WORKS_DRIVE_GROUP_ID")"
if [[ -n "$encoded_parent" ]]; then
printf '%s/v1.0/groups/%s/folder/files/%s\n' "$api_base_url" "$group_id" "$encoded_parent"
else
printf '%s/v1.0/groups/%s/folder/files\n' "$api_base_url" "$group_id"
fi
;;
sharedfolder)
[[ -n "${WORKS_DRIVE_SHARED_FOLDER_ID:-}" ]] || backup_die "WORKS_DRIVE_SHARED_FOLDER_ID is required when WORKS_DRIVE_TARGET=sharedfolder."
local user_id="${WORKS_DRIVE_USER_ID:-me}"
local shared_folder_id
user_id="$(urlencode_path "$user_id")"
shared_folder_id="$(urlencode_path "$WORKS_DRIVE_SHARED_FOLDER_ID")"
if [[ -n "$encoded_parent" ]]; then
printf '%s/v1.0/users/%s/drive/sharedfolders/%s/files/%s\n' "$api_base_url" "$user_id" "$shared_folder_id" "$encoded_parent"
else
printf '%s/v1.0/users/%s/drive/sharedfolders/%s/files\n' "$api_base_url" "$user_id" "$shared_folder_id"
fi
;;
*)
backup_die "unknown WORKS_DRIVE_TARGET: $target"
;;
esac
}
resolve_target_children_endpoint() {
local parent_file_id="${1:-${WORKS_DRIVE_PARENT_FILE_ID:-}}"
if [[ -n "$parent_file_id" ]]; then
printf '%s/children\n' "$(resolve_target_upload_endpoint "$parent_file_id")"
else
resolve_target_upload_endpoint
fi
}
resolve_target_create_folder_endpoint() {
local parent_file_id="${1:-${WORKS_DRIVE_PARENT_FILE_ID:-}}"
printf '%s/createfolder\n' "$(resolve_target_upload_endpoint "$parent_file_id")"
}
base64url() {
openssl base64 -A | tr '+/' '-_' | tr -d '='
}
build_jwt_assertion() {
backup_require_command openssl
local client_id="${WORKS_DRIVE_OAUTH_CLIENT_ID:-}"
local service_account="${WORKS_DRIVE_OAUTH_CLIENT_SERVICE_ACCOUNT:-}"
local private_key="${WORKS_DRIVE_OAUTH_CLIENT_PRIVATE_KEY:-}"
local private_key_file="${WORKS_DRIVE_OAUTH_CLIENT_PRIVATE_KEY_FILE:-}"
local key_file=""
local temp_key_file=""
local now
local exp
local header
local payload
local signing_input
[[ -n "$client_id" ]] || backup_die "WORKS_DRIVE_OAUTH_CLIENT_ID is required for service-account token mode."
[[ -n "$service_account" ]] || backup_die "WORKS_DRIVE_OAUTH_CLIENT_SERVICE_ACCOUNT is required for service-account token mode."
if [[ -n "$private_key" ]]; then
temp_key_file="$(mktemp /tmp/baron-sso-works-key.XXXXXX)"
printf '%s\n' "$private_key" >"$temp_key_file"
key_file="$temp_key_file"
elif [[ -n "$private_key_file" ]]; then
if [[ "$private_key_file" != /* ]]; then
private_key_file="$repo_root/$private_key_file"
fi
backup_require_path "$private_key_file"
key_file="$private_key_file"
else
backup_die "WORKS_DRIVE_OAUTH_CLIENT_PRIVATE_KEY or WORKS_DRIVE_OAUTH_CLIENT_PRIVATE_KEY_FILE is required for service-account token mode."
fi
now="$(date +%s)"
exp="$((now + 3600))"
header="$(printf '{"alg":"RS256","typ":"JWT"}' | base64url)"
payload="$(jq -cn \
--arg iss "$client_id" \
--arg sub "$service_account" \
--argjson iat "$now" \
--argjson exp "$exp" \
'{iss:$iss, sub:$sub, iat:$iat, exp:$exp}' | base64url)"
signing_input="${header}.${payload}"
printf '%s' "$signing_input" \
| openssl dgst -sha256 -sign "$key_file" -binary \
| base64url \
| while IFS= read -r signature; do
printf '%s.%s\n' "$signing_input" "$signature"
done
if [[ -n "$temp_key_file" ]]; then
rm -f "$temp_key_file"
fi
}
request_service_account_token() {
local client_id="${WORKS_DRIVE_OAUTH_CLIENT_ID:-}"
local client_secret="${WORKS_DRIVE_OAUTH_CLIENT_SECRET:-}"
local token_url="${WORKS_DRIVE_OAUTH_TOKEN_URL:-${WORKS_ADMIN_OAUTH_TOKEN_URL:-https://auth.worksmobile.com/oauth2/v2.0/token}}"
local assertion
local response
local response_body
local http_status
[[ -n "$client_secret" ]] || backup_die "WORKS_DRIVE_OAUTH_CLIENT_SECRET is required for service-account token mode."
assertion="$(build_jwt_assertion)"
response="$("$curl_bin" -sS -w $'\n%{http_code}' -X POST \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
--data-urlencode "assertion=$assertion" \
--data-urlencode "client_id=$client_id" \
--data-urlencode "client_secret=$client_secret" \
--data-urlencode "scope=$upload_scope" \
"$token_url")"
split_curl_response "$response" response_body http_status
if [[ "$http_status" -lt 200 || "$http_status" -ge 300 ]]; then
backup_die "WORKS token request failed (HTTP $http_status): $(printf '%s' "$response_body" | redact_for_log)"
fi
jq -er '.access_token' <<<"$response_body"
}
request_refresh_access_token() {
local client_id="${WORKS_DRIVE_OAUTH_CLIENT_ID:-}"
local client_secret="${WORKS_DRIVE_OAUTH_CLIENT_SECRET:-}"
local refresh_token="${WORKS_DRIVE_OAUTH_REFRESH_TOKEN:-}"
local token_url="${WORKS_DRIVE_OAUTH_TOKEN_URL:-${WORKS_ADMIN_OAUTH_TOKEN_URL:-https://auth.worksmobile.com/oauth2/v2.0/token}}"
local response
local response_body
local http_status
[[ -n "$client_id" ]] || backup_die "WORKS_DRIVE_OAUTH_CLIENT_ID is required for refresh-token mode."
[[ -n "$client_secret" ]] || backup_die "WORKS_DRIVE_OAUTH_CLIENT_SECRET is required for refresh-token mode."
[[ -n "$refresh_token" ]] || backup_die "WORKS_DRIVE_OAUTH_REFRESH_TOKEN is required for refresh-token mode."
response="$("$curl_bin" -sS -w $'\n%{http_code}' -X POST \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=refresh_token" \
--data-urlencode "refresh_token=$refresh_token" \
--data-urlencode "client_id=$client_id" \
--data-urlencode "client_secret=$client_secret" \
"$token_url")"
split_curl_response "$response" response_body http_status
if [[ "$http_status" -lt 200 || "$http_status" -ge 300 ]]; then
backup_die "WORKS refresh token request failed (HTTP $http_status): $(printf '%s' "$response_body" | redact_for_log)"
fi
jq -er '.access_token' <<<"$response_body"
}
resolve_access_token() {
if [[ -n "${WORKS_DRIVE_ACCESS_TOKEN:-}" ]]; then
printf '%s\n' "$WORKS_DRIVE_ACCESS_TOKEN"
return
fi
if [[ -n "${WORKS_DRIVE_ACCESS_TOKEN_FILE:-}" ]]; then
backup_require_path "$WORKS_DRIVE_ACCESS_TOKEN_FILE"
sed -n '1p' "$WORKS_DRIVE_ACCESS_TOKEN_FILE"
return
fi
if [[ -n "${WORKS_DRIVE_ACCESS_TOKEN_CMD:-}" ]]; then
sh -c "$WORKS_DRIVE_ACCESS_TOKEN_CMD"
return
fi
if [[ -n "${WORKS_DRIVE_OAUTH_REFRESH_TOKEN:-}" ]]; then
request_refresh_access_token
return
fi
request_service_account_token
}
list_child_folders() {
local access_token="$1"
local endpoint="$2"
local response
local response_body
local http_status
response="$("$curl_bin" -sS -w $'\n%{http_code}' -X GET \
-H "Authorization: Bearer $access_token" \
"$endpoint")"
split_curl_response "$response" response_body http_status
if [[ "$http_status" -lt 200 || "$http_status" -ge 300 ]]; then
backup_die "WORKS folder list request failed (HTTP $http_status): $(printf '%s' "$response_body" | redact_for_log)"
fi
printf '%s\n' "$response_body"
}
find_folder_id_in_listing() {
local listing_json="$1"
local folder_name="$2"
local strict_type="${3:-true}"
jq -er --arg name "$folder_name" --arg strictType "$strict_type" '
[
(.files // .children // .items // .data // .contents // [])[]
| select((.fileName // .name // .displayName // .title) == $name)
| select(
$strictType != "true"
or (
((.fileType // .type // .resourceType // "") | ascii_downcase) as $type
| ($type == "" or $type == "folder" or $type == "dir" or $type == "directory")
)
)
| .fileId // .id
][0] // empty
' <<<"$listing_json" 2>/dev/null || true
}
create_child_folder() {
local access_token="$1"
local endpoint="$2"
local folder_name="$3"
local payload
local response
local response_body
local http_status
payload="$(jq -cn --arg fileName "$folder_name" '{fileName:$fileName}')"
response="$("$curl_bin" -sS -w $'\n%{http_code}' -X POST \
-H "Authorization: Bearer $access_token" \
-H "Content-Type: application/json; charset=UTF-8" \
-d "$payload" \
"$endpoint")"
split_curl_response "$response" response_body http_status
if [[ "$http_status" -eq 409 ]]; then
printf 'WORKS_CONFLICT\n'
return 2
fi
if [[ "$http_status" -lt 200 || "$http_status" -ge 300 ]]; then
backup_die "WORKS folder create request failed (HTTP $http_status): $(printf '%s' "$response_body" | redact_for_log)"
fi
jq -er '.fileId // .id' <<<"$response_body"
}
ensure_child_folder() {
local access_token="$1"
local parent_file_id="$2"
local folder_name="$3"
local children_endpoint
local create_folder_endpoint
local children_json
local refreshed_children_json
local folder_id
local create_status
children_endpoint="$(resolve_target_children_endpoint "$parent_file_id")"
create_folder_endpoint="$(resolve_target_create_folder_endpoint "$parent_file_id")"
backup_log "Checking WORKS folder: parent=${parent_file_id:-root} name=$folder_name" >&2
if ! children_json="$(list_child_folders "$access_token" "$children_endpoint")"; then
return 1
fi
folder_id="$(find_folder_id_in_listing "$children_json" "$folder_name" "true")"
if [[ -n "$folder_id" ]]; then
backup_log "Found existing WORKS folder: $folder_name -> $folder_id" >&2
printf '%s\n' "$folder_id"
return
fi
backup_log "Creating WORKS folder: parent=${parent_file_id:-root} name=$folder_name" >&2
if folder_id="$(create_child_folder "$access_token" "$create_folder_endpoint" "$folder_name")"; then
backup_log "Created WORKS folder: $folder_name -> $folder_id" >&2
printf '%s\n' "$folder_id"
return
else
create_status="$?"
fi
if [[ "$create_status" -eq 2 ]]; then
backup_log "WORKS folder already exists, resolving existing folder id: $folder_name" >&2
children_endpoint="$(resolve_target_children_endpoint "$parent_file_id")"
if ! refreshed_children_json="$(list_child_folders "$access_token" "$children_endpoint")"; then
return 1
fi
folder_id="$(find_folder_id_in_listing "$refreshed_children_json" "$folder_name" "false")"
if [[ -n "$folder_id" ]]; then
backup_log "Resolved existing WORKS folder after conflict: $folder_name -> $folder_id" >&2
printf '%s\n' "$folder_id"
return
fi
backup_die "WORKS folder already exists but its fileId could not be resolved: $folder_name"
fi
return 1
}
ensure_folder_path() {
local access_token="$1"
local path="$2"
local parent_file_id="${WORKS_DRIVE_PARENT_FILE_ID:-}"
local component
local accumulated_path=""
local cached_folder_id
IFS='/' read -r -a components <<<"$path"
for component in "${components[@]}"; do
[[ -n "$component" ]] || continue
accumulated_path="${accumulated_path:+$accumulated_path/}$component"
cached_folder_id="$(read_cached_folder_id "$accumulated_path")"
if [[ -n "$cached_folder_id" ]]; then
backup_log "Using cached WORKS folder: $accumulated_path -> $cached_folder_id" >&2
parent_file_id="$cached_folder_id"
continue
fi
backup_log "Resolving WORKS folder component: $accumulated_path" >&2
if ! parent_file_id="$(ensure_child_folder "$access_token" "$parent_file_id" "$component")"; then
return 1
fi
write_cached_folder_id "$accumulated_path" "$parent_file_id"
done
printf '%s\n' "$parent_file_id"
}
create_upload_url() {
local access_token="$1"
local endpoint="$2"
local file_path="$3"
local file_name
local file_size
local payload
local response
local response_body
local http_status
file_name="$(basename "$file_path")"
file_size="$(stat -c '%s' "$file_path")"
payload="$(jq -cn \
--arg fileName "$file_name" \
--argjson fileSize "$file_size" \
--argjson overwrite "$overwrite" \
'{fileName:$fileName, fileSize:$fileSize, overwrite:$overwrite}')"
response="$("$curl_bin" -sS -w $'\n%{http_code}' -X POST \
-H "Authorization: Bearer $access_token" \
-H "Content-Type: application/json; charset=UTF-8" \
-d "$payload" \
"$endpoint")"
split_curl_response "$response" response_body http_status
if [[ "$http_status" -lt 200 || "$http_status" -ge 300 ]]; then
backup_die "WORKS upload URL request failed (HTTP $http_status): $(printf '%s' "$response_body" | redact_for_log)"
fi
jq -er '.uploadUrl' <<<"$response_body"
}
upload_file_to_url() {
local access_token="$1"
local upload_url="$2"
local file_path="$3"
local file_name
local response
local response_body
local http_status
file_name="$(basename "$file_path")"
response="$("$curl_bin" -sS -w $'\n%{http_code}' -X POST \
-H "Authorization: Bearer $access_token" \
-F "Filedata=@${file_path};filename=${file_name};type=application/octet-stream" \
"$upload_url")"
split_curl_response "$response" response_body http_status
if [[ "$http_status" -lt 200 || "$http_status" -ge 300 ]]; then
backup_die "WORKS file upload failed (HTTP $http_status): $(printf '%s' "$response_body" | redact_for_log)"
fi
printf '%s\n' "$response_body"
}
derive_repository_and_tag() {
local ref="$1"
local ref_without_digest="${ref%@*}"
local last_slash_index=-1
local last_colon_index=-1
local i
local char
local repository_with_registry
local first_component
local rest
if [[ "$ref" == *@* ]]; then
backup_die "digest image refs are not supported for WORKS image archive upload. Use a tagged image ref."
fi
for ((i = 0; i < ${#ref_without_digest}; i += 1)); do
char="${ref_without_digest:i:1}"
[[ "$char" == "/" ]] && last_slash_index="$i"
[[ "$char" == ":" ]] && last_colon_index="$i"
done
if [[ "$last_colon_index" -le "$last_slash_index" ]]; then
backup_die "DOCKER_IMAGE_REF must include an explicit tag: $ref"
fi
repository_with_registry="${ref_without_digest:0:last_colon_index}"
image_tag="${ref_without_digest:last_colon_index + 1}"
first_component="${repository_with_registry%%/*}"
if [[ "$repository_with_registry" == */* ]]; then
rest="${repository_with_registry#*/}"
else
rest="$repository_with_registry"
fi
if [[ "$first_component" == *.* || "$first_component" == *:* || "$first_component" == "localhost" ]]; then
image_repository="$rest"
else
image_repository="$repository_with_registry"
fi
[[ -n "$image_repository" ]] || backup_die "image repository could not be derived from DOCKER_IMAGE_REF: $ref"
[[ "$image_repository" =~ ^[A-Za-z0-9._/-]+$ ]] || backup_die "image repository contains unsupported characters: $image_repository"
[[ "$image_tag" =~ ^[A-Za-z0-9._-]+$ ]] || backup_die "image tag contains unsupported characters: $image_tag"
}
derive_repository_and_tag "$image_ref"
image_name="${image_repository##*/}"
release_repository="${image_root_dir}"
remote_path="${release_repository}/${image_tag}"
artifact_dir="${archive_root}/${release_repository}/${image_tag}"
mkdir -p "$artifact_dir"
tar_file="$artifact_dir/${image_name}.${image_tag}.tar"
archive_file="$artifact_dir/${image_name}.${image_tag}.tar.zst"
checksum_file="$artifact_dir/${image_name}.${image_tag}.sha256"
manifest_file="$artifact_dir/manifest.${image_tag}.json"
upload_report_file="$artifact_dir/works-upload.json"
rm -f "$tar_file" "$archive_file" "$checksum_file" "$upload_report_file"
backup_log "Docker image archive context: image_ref=$image_ref remote_path=$remote_path artifact_dir=$artifact_dir"
if [[ -n "$commit_container" ]]; then
backup_log "Committing container $commit_container to $image_ref"
docker commit "$commit_container" "$image_ref" >/dev/null
fi
backup_log "Saving Docker image $image_ref"
docker save -o "$tar_file" "$image_ref"
backup_log "Compressing image archive with zstd"
zstd -f -19 -o "$archive_file" "$tar_file" >/dev/null
rm -f "$tar_file"
archive_sha256="$(sha256sum "$archive_file" | awk '{print $1}')"
printf '%s %s\n' "$archive_sha256" "$(basename "$archive_file")" >"$checksum_file"
image_id="$(docker image inspect "$image_ref" --format '{{.Id}}' 2>/dev/null || printf 'unknown')"
archive_size="$(stat -c '%s' "$archive_file")"
git_commit="$(backup_git_commit "$repo_root")"
manifest_tmp_file="${manifest_file}.tmp"
manifest_jq_filter='
def image_entry: {
image_ref: $imageRef,
repository: $repository,
image_name: $imageName,
source_container: $sourceContainer,
docker_image_id: $imageId,
archive: {
file_name: $archiveFile,
size_bytes: $archiveSize,
sha256: $archiveSha256
},
restore_command: ("zstd -d -c " + $archiveFile + " | docker load")
};
.schema_version = 1
| .format = "docker-save-zstd"
| .created_at = (.created_at // $createdAt)
| .updated_at = $createdAt
| .image_ref = $imageRef
| .repository = $repository
| .release_repository = $releaseRepository
| .image_name = $imageName
| .tag = $tag
| .source_container = $sourceContainer
| .docker_image_id = $imageId
| .git_commit = $gitCommit
| .remote_path = $remotePath
| .restore_command = ("zstd -d -c " + $archiveFile + " | docker load")
| .archive = {
file_name: $archiveFile,
size_bytes: $archiveSize,
sha256: $archiveSha256
}
| .images = ((.images // {}) + {($imageName): image_entry})
'
if [[ -f "$manifest_file" ]]; then
jq \
--arg createdAt "$(backup_utc_now)" \
--arg imageRef "$image_ref" \
--arg repository "$image_repository" \
--arg releaseRepository "$release_repository" \
--arg imageName "$image_name" \
--arg tag "$image_tag" \
--arg sourceContainer "$commit_container" \
--arg imageId "$image_id" \
--arg gitCommit "$git_commit" \
--arg remotePath "$remote_path" \
--arg archiveFile "$(basename "$archive_file")" \
--arg archiveSha256 "$archive_sha256" \
--argjson archiveSize "$archive_size" \
"$manifest_jq_filter" "$manifest_file" >"$manifest_tmp_file"
else
jq -n \
--arg createdAt "$(backup_utc_now)" \
--arg imageRef "$image_ref" \
--arg repository "$image_repository" \
--arg releaseRepository "$release_repository" \
--arg imageName "$image_name" \
--arg tag "$image_tag" \
--arg sourceContainer "$commit_container" \
--arg imageId "$image_id" \
--arg gitCommit "$git_commit" \
--arg remotePath "$remote_path" \
--arg archiveFile "$(basename "$archive_file")" \
--arg archiveSha256 "$archive_sha256" \
--argjson archiveSize "$archive_size" \
"$manifest_jq_filter" >"$manifest_tmp_file"
fi
mv "$manifest_tmp_file" "$manifest_file"
upload_files=("$archive_file" "$checksum_file" "$manifest_file")
if [[ "$dry_run" == "true" ]]; then
jq -n \
--arg createdAt "$(backup_utc_now)" \
--arg status "planned" \
--arg remotePath "$remote_path" \
--arg artifactDir "$artifact_dir" \
--argjson files "$(printf '%s\n' "${upload_files[@]}" | jq -R '{file_path:., file_name:(. | split("/")[-1]), status:"planned"}' | jq -s '.')" \
'{
created_at: $createdAt,
status: $status,
remote_path: $remotePath,
artifact_dir: $artifactDir,
files: $files
}' >"$upload_report_file"
backup_log "Dry run: packaged image artifacts at $artifact_dir"
backup_log "Dry run: would upload to WORKS Drive path $remote_path"
exit 0
fi
backup_require_command "$curl_bin"
access_token="$(resolve_access_token)"
backup_log "Resolving WORKS Drive folder path: $remote_path"
target_folder_id="$(ensure_folder_path "$access_token" "$remote_path")"
upload_endpoint="$(resolve_target_upload_endpoint "$target_folder_id")"
uploaded_items="[]"
for file_path in "${upload_files[@]}"; do
backup_require_path "$file_path"
backup_log "Creating WORKS Drive upload URL for $(basename "$file_path")"
upload_url="$(create_upload_url "$access_token" "$upload_endpoint" "$file_path")"
backup_log "Uploading $(basename "$file_path") to WORKS Drive"
upload_response="$(upload_file_to_url "$access_token" "$upload_url" "$file_path")"
upload_response_json="$(jq -c '.' <<<"${upload_response:-{}}" 2>/dev/null || printf '{}')"
uploaded_items="$(jq \
--arg fileName "$(basename "$file_path")" \
--arg filePath "$file_path" \
--argjson fileSize "$(stat -c '%s' "$file_path")" \
--arg status "uploaded" \
--arg response "$upload_response_json" \
'. + [{file_name:$fileName, file_path:$filePath, file_size:$fileSize, status:$status, response:($response | fromjson? // {})}]' \
<<<"$uploaded_items")"
done
jq -n \
--arg createdAt "$(backup_utc_now)" \
--arg status "uploaded" \
--arg remotePath "$remote_path" \
--arg artifactDir "$artifact_dir" \
--arg target "$target" \
--arg folderId "$target_folder_id" \
--argjson files "$uploaded_items" \
'{
created_at: $createdAt,
status: $status,
target: $target,
remote_path: $remotePath,
target_folder_id: $folderId,
artifact_dir: $artifactDir,
files: $files
}' >"$upload_report_file"
backup_log "Upload complete: $upload_report_file"