diff --git a/docs/works-drive-docker-image-archive.md b/docs/works-drive-docker-image-archive.md index df659636..28bb5654 100644 --- a/docs/works-drive-docker-image-archive.md +++ b/docs/works-drive-docker-image-archive.md @@ -167,7 +167,7 @@ scripts/docker-image/verify_archive.sh /path/to/downloaded/archive - `zstd -t` 무결성 성공 - 선택적으로 `docker load` 성공 -현재 repo의 `scripts/docker-image/download_works_drive.sh`는 `WORKS_DRIVE_DOCKER_IMAGE_DRIVE_ID`와 `IMAGE_TAG`를 사용해 `baron-sso//`의 archive, checksum, manifest를 내려받고 checksum/manifest 검증 후 `docker load`를 수행한다. +현재 repo의 `scripts/docker-image/download_works_drive.sh`는 `WORKS_DRIVE_DOCKER_IMAGE_DRIVE_ID`와 `IMAGE_TAG`를 사용해 `baron-sso//`의 archive, checksum, manifest를 내려받고 checksum/manifest 검증 후 `docker load`를 수행한다. 공용 드라이브 root 목록은 `GET /v1.0/sharedrives//files`로 조회하고, 하위 폴더 목록만 `GET /v1.0/sharedrives//files//children`를 사용한다. ```bash WORKS_DRIVE_DOCKER_IMAGE_DRIVE_ID=@2001000000547281 \ @@ -176,7 +176,7 @@ IMAGE_TAG=v1.2606.ab12 \ scripts/docker-image/download_works_drive.sh ``` -API로 다운로드할 때는 대상 archive 폴더의 children을 조회해 각 파일의 `fileId`를 얻은 뒤 다음 endpoint를 호출한다. +API로 다운로드할 때는 root 목록에서 `baron-sso` 폴더의 `fileId`를 찾고, 이후 대상 archive 폴더의 children을 조회해 각 파일의 `fileId`를 얻은 뒤 다음 endpoint를 호출한다. ```text GET /v1.0/sharedrives//files//download diff --git a/scripts/docker-image/download_works_drive.sh b/scripts/docker-image/download_works_drive.sh index 6fc5fef4..7f53b2bf 100755 --- a/scripts/docker-image/download_works_drive.sh +++ b/scripts/docker-image/download_works_drive.sh @@ -59,7 +59,11 @@ list_children() { local response_body local http_status - endpoint="$(resolve_files_endpoint "$parent_file_id")/children" + if [[ -n "$parent_file_id" ]]; then + endpoint="$(resolve_files_endpoint "$parent_file_id")/children" + else + endpoint="$(resolve_files_endpoint)" + fi response="$("$curl_bin" -sS -w $'\n%{http_code}' \ -H "Authorization: Bearer $access_token" \ "$endpoint")" diff --git a/scripts/docker-image/upload_works_drive.sh b/scripts/docker-image/upload_works_drive.sh index 324a132f..13601b97 100755 --- a/scripts/docker-image/upload_works_drive.sh +++ b/scripts/docker-image/upload_works_drive.sh @@ -68,6 +68,7 @@ image_ref="${DOCKER_IMAGE_REF:-${IMAGE_REF:-}}" 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}" @@ -78,6 +79,34 @@ 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' } @@ -163,7 +192,11 @@ resolve_target_upload_endpoint() { resolve_target_children_endpoint() { local parent_file_id="${1:-${WORKS_DRIVE_PARENT_FILE_ID:-}}" - printf '%s/children\n' "$(resolve_target_upload_endpoint "$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() { @@ -365,24 +398,33 @@ ensure_child_folder() { local children_json local folder_id - children_endpoint="$(resolve_target_children_endpoint "$parent_file_id")" - create_folder_endpoint="$(resolve_target_create_folder_endpoint "$parent_file_id")" - children_json="$(list_child_folders "$access_token" "$children_endpoint")" - folder_id="$(jq -er --arg name "$folder_name" ' - [ - (.files // .children // .items // [])[] - | select((.fileName // .name) == $name) - | select(((.fileType // .type // "") | ascii_downcase) == "folder") - | .fileId // .id - ][0] // empty - ' <<<"$children_json" 2>/dev/null || true)" + if [[ -n "$parent_file_id" ]]; then + children_endpoint="$(resolve_target_children_endpoint "$parent_file_id")" + create_folder_endpoint="$(resolve_target_create_folder_endpoint "$parent_file_id")" + if ! children_json="$(list_child_folders "$access_token" "$children_endpoint")"; then + return 1 + fi + folder_id="$(jq -er --arg name "$folder_name" ' + [ + (.files // .children // .items // [])[] + | select((.fileName // .name) == $name) + | select(((.fileType // .type // "") | ascii_downcase) == "folder") + | .fileId // .id + ][0] // empty + ' <<<"$children_json" 2>/dev/null || true)" - if [[ -n "$folder_id" ]]; then - printf '%s\n' "$folder_id" - return + if [[ -n "$folder_id" ]]; then + printf '%s\n' "$folder_id" + return + fi + else + create_folder_endpoint="$(resolve_target_create_folder_endpoint "$parent_file_id")" fi - create_child_folder "$access_token" "$create_folder_endpoint" "$folder_name" + if ! folder_id="$(create_child_folder "$access_token" "$create_folder_endpoint" "$folder_name")"; then + return 1 + fi + printf '%s\n' "$folder_id" } ensure_folder_path() { @@ -390,11 +432,22 @@ ensure_folder_path() { 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 - parent_file_id="$(ensure_child_folder "$access_token" "$parent_file_id" "$component")" + accumulated_path="${accumulated_path:+$accumulated_path/}$component" + cached_folder_id="$(read_cached_folder_id "$accumulated_path")" + if [[ -n "$cached_folder_id" ]]; then + parent_file_id="$cached_folder_id" + continue + fi + 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" diff --git a/test/works_drive_docker_image_download_policy_test.sh b/test/works_drive_docker_image_download_policy_test.sh index 72f5b715..dce0f083 100644 --- a/test/works_drive_docker_image_download_policy_test.sh +++ b/test/works_drive_docker_image_download_policy_test.sh @@ -86,7 +86,7 @@ if [[ -n "$output_file" ]]; then fi case "$url" in - https://www.worksapis.com/v1.0/sharedrives/shared-drive-1/files/children) + https://www.worksapis.com/v1.0/sharedrives/shared-drive-1/files) printf '{"files":[{"fileId":"baron-sso-id","fileName":"baron-sso","fileType":"FOLDER"}]}\n200\n' ;; https://www.worksapis.com/v1.0/sharedrives/shared-drive-1/files/baron-sso-id/children) @@ -132,7 +132,7 @@ WORKS_DOCKER_IMAGE_DOWNLOAD_DIR="$tmp_root/downloaded" \ IMAGE_TAG="v1.2606.ab12" \ "$script" >/dev/null -grep -Fq "sharedrives/shared-drive-1/files/children" "$curl_log" \ +grep -Fq "sharedrives/shared-drive-1/files" "$curl_log" \ || fail "download script must list the shared drive root." grep -Fq "sharedrives/shared-drive-1/files/baron-sso-id/children" "$curl_log" \ || fail "download script must resolve the baron-sso folder." diff --git a/test/works_drive_docker_image_upload_policy_test.sh b/test/works_drive_docker_image_upload_policy_test.sh index ba957779..0d180841 100755 --- a/test/works_drive_docker_image_upload_policy_test.sh +++ b/test/works_drive_docker_image_upload_policy_test.sh @@ -205,4 +205,68 @@ report_file="$artifact_dir/works-upload.json" jq -e '.status == "uploaded" and (.files | length) == 3' "$report_file" >/dev/null \ || fail "upload report must include three uploaded artifact files." +root_curl_log="$tmp_dir/root-curl.log" +root_fake_curl="$tmp_dir/root-fake-curl.sh" +cat >"$root_fake_curl" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "$*" >>"${FAKE_CURL_LOG}" +last_arg="${!#}" + +case "$last_arg" in + https://www.worksapis.com/v1.0/sharedrives/root-drive/files) + printf '{"files":[]}\n200' + ;; + https://www.worksapis.com/v1.0/sharedrives/root-drive/files/createfolder) + create_count_file="${FAKE_CURL_LOG}.root-create-count" + create_count=0 + [[ -f "$create_count_file" ]] && create_count="$(cat "$create_count_file")" + create_count=$((create_count + 1)) + printf '%s' "$create_count" >"$create_count_file" + if [[ "$create_count" -eq 1 ]]; then + printf '{"fileId":"root-baron-sso-id","fileName":"baron-sso","fileType":"FOLDER"}\n200' + else + printf '{"code":"RESOURCE_ALREADY_EXIST","description":"Resource already exists."}\n409' + fi + ;; + https://www.worksapis.com/v1.0/sharedrives/root-drive/files/root-baron-sso-id/children) + printf '{"files":[]}\n200' + ;; + https://www.worksapis.com/v1.0/sharedrives/root-drive/files/root-baron-sso-id/createfolder) + printf '{"fileId":"root-tag-id","fileName":"v1.2606.ab12","fileType":"FOLDER"}\n200' + ;; + https://www.worksapis.com/v1.0/sharedrives/root-drive/files/root-tag-id) + printf '{"uploadUrl":"https://upload.example.test/root-docker-image"}\n200' + ;; + https://upload.example.test/root-docker-image) + printf '{"fileId":"uploaded-root-file-id"}\n200' + ;; + *) + echo "unexpected root curl URL: $last_arg" >&2 + exit 2 + ;; +esac +EOF +chmod +x "$root_fake_curl" + +root_archive_dir="$tmp_dir/root-archive" +for image in backend userfront; do + FAKE_DOCKER_LOG="$docker_log" \ + FAKE_CURL_LOG="$root_curl_log" \ + PATH="$fake_bin:$PATH" \ + WORKS_DRIVE_ACCESS_TOKEN="test-access-token" \ + WORKS_DRIVE_TARGET="sharedrive" \ + WORKS_DRIVE_SHARED_DRIVE_ID="root-drive" \ + WORKS_DRIVE_PARENT_FILE_ID="" \ + WORKS_DRIVE_CURL_BIN="$root_fake_curl" \ + WORKS_DOCKER_IMAGE_ARCHIVE_DIR="$root_archive_dir" \ + DOCKER_IMAGE_REF="baron_sso/${image}:v1.2606.ab12" \ + "$script" >"$tmp_dir/root-${image}.out" +done + +root_create_count="$(cat "${root_curl_log}.root-create-count")" +[[ "$root_create_count" == "1" ]] || fail "script must reuse the cached root archive folder id across image uploads in the same run." +grep -Fq "sharedrives/root-drive/files/root-tag-id" "$root_curl_log" \ + || fail "script must upload follow-up images into the cached tag folder." + echo "OK: WORKS Drive Docker image archive upload flow commits, packages, and uploads image artifacts"