190 lines
7.5 KiB
Bash
Executable File
190 lines
7.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
FILE=${1:?usage: upload-openlist-artifact.sh file [remote-directory]}
|
|
REMOTE_DIR=${2:-/yidongpan/构建产物/liverecorder}
|
|
OPENLIST_BASE_URL=${OPENLIST_BASE_URL:-https://openlist.nxsir.cn}
|
|
OPENLIST_USERNAME=${OPENLIST_USERNAME:?OPENLIST_USERNAME is required}
|
|
OPENLIST_PASSWORD=${OPENLIST_PASSWORD:?OPENLIST_PASSWORD is required}
|
|
OPENLIST_UPLOAD_ATTEMPTS=${OPENLIST_UPLOAD_ATTEMPTS:-6}
|
|
|
|
test -f "$FILE" || { printf 'artifact does not exist: %s\n' "$FILE" >&2; exit 1; }
|
|
for required_command in curl python3 stat basename sleep; do
|
|
command -v "$required_command" >/dev/null 2>&1 || {
|
|
printf 'required upload command is missing: %s\n' "$required_command" >&2
|
|
exit 1
|
|
}
|
|
done
|
|
|
|
OPENLIST_BASE_URL=${OPENLIST_BASE_URL%/}
|
|
REMOTE_DIR="/${REMOTE_DIR#/}"
|
|
REMOTE_DIR=${REMOTE_DIR%/}
|
|
FILE_NAME=$(basename -- "$FILE")
|
|
BUILD_MARKER=${BUILD_TAG:-${BUILD_ID:-$$}}
|
|
BUILD_MARKER=$(printf '%s' "$BUILD_MARKER" | tr -c 'A-Za-z0-9._-' '-')
|
|
TEMP_NAME=".${FILE_NAME}.uploading-${BUILD_MARKER}"
|
|
TEMP_PATH="$REMOTE_DIR/$TEMP_NAME"
|
|
FINAL_PATH="$REMOTE_DIR/$FILE_NAME"
|
|
LOCAL_SIZE=$(stat -c '%s' "$FILE")
|
|
TOKEN=
|
|
|
|
json_string() {
|
|
python3 -c 'import json,sys; print(json.dumps(sys.argv[1], ensure_ascii=False))' "$1"
|
|
}
|
|
|
|
envelope_code() {
|
|
python3 -c 'import json,sys
|
|
try:
|
|
value=json.load(sys.stdin)
|
|
print(value.get("code", -1))
|
|
except Exception:
|
|
print(-1)'
|
|
}
|
|
|
|
envelope_message() {
|
|
python3 -c 'import json,sys
|
|
try:
|
|
value=json.load(sys.stdin)
|
|
print(value.get("message", "unknown OpenList response"))
|
|
except Exception:
|
|
print("invalid OpenList response")'
|
|
}
|
|
|
|
login() {
|
|
local payload response code
|
|
payload=$(printf '{"username":%s,"password":%s}' \
|
|
"$(json_string "$OPENLIST_USERNAME")" \
|
|
"$(json_string "$OPENLIST_PASSWORD")")
|
|
response=$(curl --silent --show-error --location --fail-with-body \
|
|
--connect-timeout 30 --max-time 120 \
|
|
--request POST "$OPENLIST_BASE_URL/api/auth/login" \
|
|
--header 'Content-Type: application/json' \
|
|
--data-raw "$payload") || return 1
|
|
code=$(printf '%s' "$response" | envelope_code)
|
|
[ "$code" = "200" ] || {
|
|
printf 'OpenList login failed: %s\n' "$(printf '%s' "$response" | envelope_message)" >&2
|
|
return 1
|
|
}
|
|
TOKEN=$(printf '%s' "$response" | python3 -c 'import json,sys; print((json.load(sys.stdin).get("data") or {}).get("token", ""))')
|
|
[ -n "$TOKEN" ]
|
|
}
|
|
|
|
post_json() {
|
|
local endpoint=$1 payload=$2
|
|
curl --silent --show-error --location --fail-with-body \
|
|
--connect-timeout 30 --max-time 180 \
|
|
--request POST "$OPENLIST_BASE_URL$endpoint" \
|
|
--header "Authorization: $TOKEN" \
|
|
--header 'Content-Type: application/json' \
|
|
--data-raw "$payload"
|
|
}
|
|
|
|
remote_size() {
|
|
local path=$1 response code
|
|
response=$(post_json /api/fs/get "{\"path\":$(json_string "$path"),\"password\":\"\"}") || return 1
|
|
code=$(printf '%s' "$response" | envelope_code)
|
|
[ "$code" = "200" ] || return 1
|
|
printf '%s' "$response" | python3 -c 'import json,sys; print((json.load(sys.stdin).get("data") or {}).get("size", -1))'
|
|
}
|
|
|
|
ensure_remote_directory() {
|
|
local current= segment response code
|
|
while IFS= read -r segment; do
|
|
[ -n "$segment" ] || continue
|
|
current="$current/$segment"
|
|
response=$(post_json /api/fs/list "{\"path\":$(json_string "$current"),\"password\":\"\",\"refresh\":false,\"page\":1,\"per_page\":1}" || true)
|
|
code=$(printf '%s' "$response" | envelope_code)
|
|
if [ "$code" = "200" ]; then
|
|
continue
|
|
fi
|
|
response=$(post_json /api/fs/mkdir "{\"path\":$(json_string "$current")}") || return 1
|
|
code=$(printf '%s' "$response" | envelope_code)
|
|
[ "$code" = "200" ] || {
|
|
printf 'OpenList mkdir failed for %s: %s\n' "$current" "$(printf '%s' "$response" | envelope_message)" >&2
|
|
return 1
|
|
}
|
|
done < <(python3 -c 'import sys; print("\n".join(part for part in sys.argv[1].split("/") if part))' "$REMOTE_DIR")
|
|
}
|
|
|
|
remove_temporary_file() {
|
|
local response
|
|
[ -n "$TOKEN" ] || login >/dev/null 2>&1 || return 0
|
|
response=$(post_json /api/fs/remove "{\"dir\":$(json_string "$REMOTE_DIR"),\"names\":[$(json_string "$TEMP_NAME")]}" 2>/dev/null || true)
|
|
[ "$(printf '%s' "$response" | envelope_code)" = "200" ] || true
|
|
}
|
|
|
|
publish_temporary_file() {
|
|
local response code
|
|
response=$(post_json /api/fs/rename "{\"path\":$(json_string "$TEMP_PATH"),\"name\":$(json_string "$FILE_NAME")}") || return 1
|
|
code=$(printf '%s' "$response" | envelope_code)
|
|
[ "$code" = "200" ] || {
|
|
printf 'OpenList rename failed: %s\n' "$(printf '%s' "$response" | envelope_message)" >&2
|
|
return 1
|
|
}
|
|
}
|
|
|
|
upload_once() {
|
|
local encoded_path response code uploaded_size
|
|
encoded_path=$(python3 -c 'import sys,urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$TEMP_PATH")
|
|
response=$(curl --silent --show-error --location --fail-with-body \
|
|
--connect-timeout 30 --max-time 3600 --speed-limit 1024 --speed-time 120 \
|
|
--request PUT "$OPENLIST_BASE_URL/api/fs/put" \
|
|
--header "Authorization: $TOKEN" \
|
|
--header "File-Path: $encoded_path" \
|
|
--header 'As-Task: false' \
|
|
--header 'Content-Type: application/octet-stream' \
|
|
--header 'Expect:' \
|
|
--data-binary "@$FILE") || return 1
|
|
code=$(printf '%s' "$response" | envelope_code)
|
|
[ "$code" = "200" ] || {
|
|
printf 'OpenList upload failed: %s\n' "$(printf '%s' "$response" | envelope_message)" >&2
|
|
return 1
|
|
}
|
|
uploaded_size=$(remote_size "$TEMP_PATH") || return 1
|
|
[ "$uploaded_size" = "$LOCAL_SIZE" ] || {
|
|
printf 'OpenList size mismatch for temporary upload: local=%s remote=%s\n' "$LOCAL_SIZE" "$uploaded_size" >&2
|
|
return 1
|
|
}
|
|
}
|
|
|
|
printf 'Uploading %s (%s bytes) to OpenList %s\n' "$FILE_NAME" "$LOCAL_SIZE" "$REMOTE_DIR"
|
|
|
|
for attempt in $(seq 1 "$OPENLIST_UPLOAD_ATTEMPTS"); do
|
|
TOKEN=
|
|
upload_ready=1
|
|
if login && ensure_remote_directory; then
|
|
existing_size=$(remote_size "$FINAL_PATH" 2>/dev/null || true)
|
|
if [ "$existing_size" = "$LOCAL_SIZE" ]; then
|
|
printf 'OpenList artifact already exists with matching size: %s\n' "$FINAL_PATH"
|
|
exit 0
|
|
fi
|
|
if [ -n "$existing_size" ]; then
|
|
remove_response=$(post_json /api/fs/remove "{\"dir\":$(json_string "$REMOTE_DIR"),\"names\":[$(json_string "$FILE_NAME")]}" || true)
|
|
if [ "$(printf '%s' "$remove_response" | envelope_code)" != "200" ]; then
|
|
printf 'Unable to remove mismatched OpenList artifact before retry: %s\n' "$FINAL_PATH" >&2
|
|
upload_ready=0
|
|
fi
|
|
fi
|
|
|
|
if [ "$upload_ready" = "1" ] && upload_once && publish_temporary_file; then
|
|
final_size=$(remote_size "$FINAL_PATH") || final_size=-1
|
|
if [ "$final_size" = "$LOCAL_SIZE" ]; then
|
|
printf 'OpenList upload verified: %s (%s bytes)\n' "$FINAL_PATH" "$final_size"
|
|
exit 0
|
|
fi
|
|
printf 'OpenList final size mismatch: local=%s remote=%s\n' "$LOCAL_SIZE" "$final_size" >&2
|
|
fi
|
|
fi
|
|
|
|
if [ "$attempt" -lt "$OPENLIST_UPLOAD_ATTEMPTS" ]; then
|
|
delay=$((5 * (1 << (attempt - 1))))
|
|
[ "$delay" -le 120 ] || delay=120
|
|
printf 'OpenList upload attempt %s/%s failed; retrying in %ss\n' "$attempt" "$OPENLIST_UPLOAD_ATTEMPTS" "$delay" >&2
|
|
sleep "$delay"
|
|
fi
|
|
done
|
|
|
|
remove_temporary_file
|
|
printf 'OpenList upload failed after %s attempts: %s\n' "$OPENLIST_UPLOAD_ATTEMPTS" "$FILE_NAME" >&2
|
|
exit 1
|