2026-06-25 12:39:33 +02:00

703 lines
19 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
REMOTE_SSH_HOST="${REMOTE_SSH_HOST:-storage@172.235.163.25}"
REMOTE_API_PORT="${REMOTE_API_PORT:-18080}"
LOCAL_API_PORT="${LOCAL_API_PORT:-8080}"
TUNNEL_CONTROL_PATH="${TUNNEL_CONTROL_PATH:-/tmp/logos-storage-tunnel-${USER:-user}.ctl}"
CID_STATE_FILE="${CID_STATE_FILE:-${HOME}/.logos/storage/test/cids.log}"
TEST_FILES_DIR="${TEST_FILES_DIR:-${HOME}/.logos/storage/test/files}"
TEST_FILE_SIZES="${TEST_FILE_SIZES:-4K 1M 10M}"
TEST_KEEP_FILES="${TEST_KEEP_FILES:-0}"
STORAGE_LIB_CTL="${STORAGE_LIB_CTL:-${ROOT_DIR}/tools/libstorage-cpp/storage_lib_ctl}"
STORAGE_LIB_SOCKET="${STORAGE_LIB_SOCKET:-${HOME}/.logos/storage/libstorage/storage_lib.sock}"
LOCAL_API="http://127.0.0.1:${LOCAL_API_PORT}/api/storage/v1"
REMOTE_API="http://127.0.0.1:${REMOTE_API_PORT}/api/storage/v1"
usage() {
cat <<EOF
Usage: $0 <target> <command> [args]
$0 <global-command> [args]
Targets:
local Local REST node at $LOCAL_API
remote Remote REST node through SSH tunnel at $REMOTE_API
lib Local storage_lib daemon via Unix socket
Target commands:
<target> upload <file>
Upload a file and print the returned CID.
<target> upload-random <size> [--keep]
Create random content, upload it, print CID.
<target> download <cid> <output-file> [--local]
Download CID into output-file. For lib, --local means local store only.
<target> fetch <cid> [--wait]
Fetch CID from network into target local store. --wait is REST-only.
<target> list
List manifest CIDs stored locally by target.
<target> delete <cid>
Delete CID from target local storage.
<target> delete-all --yes
Delete every CID returned by list from target local storage.
<target> exists <cid>
Check whether target has CID locally.
<target> space
Show target storage space information.
<target> peerid
Show target peer ID.
<target> test
Upload random files to remote, download via local/lib target, validate hashes,
and clean up involved CIDs. Supported targets: local, lib.
Lib-only target commands:
lib spr
lib debug
lib manifest <cid>
lib connect <peer-id> [addr...]
Global commands:
help
Show this help.
tunnel start|stop|status
Manage SSH tunnel to the remote REST API.
make-file <size> [output-file]
Create random content with dd. Example: make-file 10M /tmp/logos-10M.bin
last-cid [target]
Print the most recent CID from CID_STATE_FILE, optionally filtered by target.
Environment:
REMOTE_SSH_HOST SSH host for the remote node [$REMOTE_SSH_HOST]
REMOTE_API_PORT Local tunnel port for remote API [$REMOTE_API_PORT]
LOCAL_API_PORT Local node API port [$LOCAL_API_PORT]
STORAGE_LIB_CTL Path to storage_lib_ctl [$STORAGE_LIB_CTL]
STORAGE_LIB_SOCKET Unix socket for storage_lib [$STORAGE_LIB_SOCKET]
CID_STATE_FILE Upload history log [$CID_STATE_FILE]
TEST_FILES_DIR Generated test file directory [$TEST_FILES_DIR]
TEST_FILE_SIZES Sizes used by '<target> test' [$TEST_FILE_SIZES]
TEST_KEEP_FILES Keep test workspace when set to 1 [$TEST_KEEP_FILES]
Examples:
$0 tunnel start
$0 remote upload-random 10M
$0 local fetch <CID> --wait
$0 local download <CID> /tmp/downloaded.bin
$0 lib download <CID> /tmp/downloaded.bin
$0 lib test
$0 local test
EOF
}
die() {
printf 'error: %s\n' "$*" >&2
exit 1
}
need() {
command -v "$1" >/dev/null 2>&1 || die "missing required command: $1"
}
check_common_deps() {
need curl
need jq
}
is_target() {
case "${1:-}" in
local|remote|lib) return 0 ;;
*) return 1 ;;
esac
}
target_api() {
case "${1:-}" in
local)
printf '%s\n' "$LOCAL_API"
;;
remote)
tunnel_start >/dev/null
printf '%s\n' "$REMOTE_API"
;;
*)
die "REST target must be 'local' or 'remote'"
;;
esac
}
tunnel_status() {
ssh -S "$TUNNEL_CONTROL_PATH" -O check "$REMOTE_SSH_HOST" >/dev/null 2>&1
}
tunnel_start() {
need ssh
if tunnel_status; then
printf 'tunnel already running: 127.0.0.1:%s -> %s:127.0.0.1:8080\n' \
"$REMOTE_API_PORT" "$REMOTE_SSH_HOST"
return 0
fi
ssh \
-M \
-S "$TUNNEL_CONTROL_PATH" \
-fN \
-L "127.0.0.1:${REMOTE_API_PORT}:127.0.0.1:8080" \
"$REMOTE_SSH_HOST"
printf 'started tunnel: 127.0.0.1:%s -> %s:127.0.0.1:8080\n' \
"$REMOTE_API_PORT" "$REMOTE_SSH_HOST"
}
tunnel_stop() {
need ssh
if tunnel_status; then
ssh -S "$TUNNEL_CONTROL_PATH" -O exit "$REMOTE_SSH_HOST" >/dev/null
printf 'stopped tunnel\n'
else
printf 'tunnel not running\n'
fi
}
make_file() {
local size="${1:-}"
local out="${2:-}"
[[ -n "$size" ]] || die 'make-file requires <size>'
if [[ -z "$out" ]]; then
mkdir -p "$TEST_FILES_DIR"
out="${TEST_FILES_DIR}/logos-test-${size}-$(date -u +%Y%m%dT%H%M%SZ).bin"
fi
dd if=/dev/urandom of="$out" bs="$size" count=1 status=progress
printf '%s\n' "$out"
}
record_cid() {
local target="$1"
local cid="$2"
local file="$3"
mkdir -p "$(dirname "$CID_STATE_FILE")"
printf '%s %s %s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$target" "$cid" "$file" >> "$CID_STATE_FILE"
}
last_cid() {
local target=""
while [[ $# -gt 0 ]]; do
case "$1" in
local|remote|lib)
[[ -z "$target" ]] || die 'target specified more than once'
target="$1"
;;
*)
die "unknown last-cid option: $1"
;;
esac
shift
done
[[ -f "$CID_STATE_FILE" ]] || die "CID state file not found: $CID_STATE_FILE"
local cid
if [[ -n "$target" ]]; then
cid="$(awk -v target="$target" '$2 == target { cid = $3 } END { print cid }' "$CID_STATE_FILE")"
else
cid="$(awk 'NF >= 3 { cid = $3 } END { print cid }' "$CID_STATE_FILE")"
fi
[[ -n "$cid" ]] || die 'no matching CID found'
printf '%s\n' "$cid"
}
lib_ctl_raw() {
[[ -x "$STORAGE_LIB_CTL" ]] || die "storage_lib_ctl not executable: $STORAGE_LIB_CTL"
"$STORAGE_LIB_CTL" --socket "$STORAGE_LIB_SOCKET" "$@"
}
lib_result() {
check_common_deps
local response
response="$(lib_ctl_raw "$@")"
if ! printf '%s\n' "$response" | jq -e '.ok == true' >/dev/null; then
printf '%s\n' "$response" >&2
return 1
fi
printf '%s\n' "$response" | jq -r '.result'
}
abs_existing_path() {
local path="$1"
[[ -e "$path" ]] || die "path not found: $path"
local dir base
dir="$(cd "$(dirname "$path")" && pwd)"
base="$(basename "$path")"
printf '%s/%s\n' "$dir" "$base"
}
abs_output_path() {
local path="$1"
local dir base
dir="$(dirname "$path")"
base="$(basename "$path")"
mkdir -p "$dir"
dir="$(cd "$dir" && pwd)"
printf '%s/%s\n' "$dir" "$base"
}
rest_upload_file() {
check_common_deps
local target="$1"
local file="$2"
[[ -f "$file" ]] || die "file not found: $file"
local api
api="$(target_api "$target")"
curl -fsS \
-H 'Content-Type: application/octet-stream' \
--data-binary "@${file}" \
"${api}/data"
}
target_upload_file() {
local target="$1"
local file="$2"
[[ -n "$file" ]] || die 'upload requires <file>'
local cid
if [[ "$target" == 'lib' ]]; then
cid="$(lib_result upload "$(abs_existing_path "$file")")"
else
cid="$(rest_upload_file "$target" "$file")"
fi
printf '%s\n' "$cid"
record_cid "$target" "$cid" "$file"
}
target_upload_random() {
local target="$1"
local size="${2:-}"
local keep=false
[[ -n "$size" ]] || die 'upload-random requires <size> [--keep]'
shift 2
while [[ $# -gt 0 ]]; do
case "$1" in
--keep) keep=true ;;
*) die "unknown upload-random option: $1" ;;
esac
shift
done
local tmp cid
tmp="$(mktemp "${TMPDIR:-/tmp}/logos-storage-test.XXXXXX")"
dd if=/dev/urandom of="$tmp" bs="$size" count=1 status=progress >&2
cid="$(target_upload_file "$target" "$tmp")"
printf '%s\n' "$cid"
if [[ "$keep" == true ]]; then
printf 'kept file: %s\n' "$tmp" >&2
else
rm -f "$tmp"
fi
}
target_list_cids() {
local target="$1"
if [[ "$target" == 'lib' ]]; then
lib_result list | jq -r '.[]?.cid'
return 0
fi
check_common_deps
local api
api="$(target_api "$target")"
curl -fsS "${api}/data" | jq -r '.content[]?.cid'
}
target_delete_cid() {
local target="$1"
local cid="${2:-}"
[[ -n "$cid" ]] || die 'delete requires <cid>'
if [[ "$target" == 'lib' ]]; then
lib_result delete "$cid" >/dev/null
else
check_common_deps
local api
api="$(target_api "$target")"
curl -fsS -X DELETE "${api}/data/${cid}" >/dev/null
fi
printf 'deleted %s from %s\n' "$cid" "$target"
}
target_delete_all() {
local target="$1"
local yes="${2:-}"
[[ "$yes" == '--yes' ]] || die 'delete-all requires --yes'
local cid count=0
while IFS= read -r cid; do
[[ -n "$cid" ]] || continue
target_delete_cid "$target" "$cid"
count=$((count + 1))
done < <(target_list_cids "$target")
printf 'deleted %d CID(s) from %s\n' "$count" "$target"
}
target_simple_get() {
local target="$1"
local path="$2"
if [[ "$target" == 'lib' ]]; then
case "$path" in
space) lib_result space ;;
peerid) lib_result peer-id ;;
*) die "unsupported lib get path: $path" ;;
esac
return 0
fi
check_common_deps
local api
api="$(target_api "$target")"
curl -fsS "${api}/${path}"
printf '\n'
}
target_exists_cid() {
local target="$1"
local cid="${2:-}"
[[ -n "$cid" ]] || die 'exists requires <cid>'
if [[ "$target" == 'lib' ]]; then
lib_result exists "$cid"
else
target_simple_get "$target" "data/${cid}/exists"
fi
}
target_fetch_cid() {
local target="$1"
local cid="${2:-}"
local wait="${3:-}"
[[ -n "$cid" ]] || die 'fetch requires <cid> [--wait]'
[[ -z "$wait" || "$wait" == '--wait' ]] || die 'only supported option is --wait'
if [[ "$target" == 'lib' ]]; then
[[ -z "$wait" ]] || printf 'warning: --wait is ignored for lib fetch\n' >&2
lib_result fetch "$cid"
return 0
fi
check_common_deps
local api response download_id
api="$(target_api "$target")"
response="$(curl -fsS -X POST "${api}/data/${cid}/network")"
printf '%s\n' "$response" | jq .
if [[ "$wait" != '--wait' ]]; then
return 0
fi
download_id="$(printf '%s\n' "$response" | jq -r '.downloadId // empty')"
[[ -n "$download_id" ]] || die 'response did not contain downloadId'
while true; do
local progress active received total
progress="$(curl -fsS "${api}/data/${cid}/network/progress/${download_id}")"
printf '%s\n' "$progress" | jq .
active="$(printf '%s\n' "$progress" | jq -r '.active')"
received="$(printf '%s\n' "$progress" | jq -r '.received // 0')"
total="$(printf '%s\n' "$progress" | jq -r '.total // 0')"
if [[ "$active" != 'true' || ( "$total" != '0' && "$received" == "$total" ) ]]; then
break
fi
sleep 2
done
}
target_download_cid() {
local target="$1"
local cid="${2:-}"
local out="${3:-}"
local local_only=false
[[ -n "$cid" && -n "$out" ]] || die 'download requires <cid> <output-file> [--local]'
if [[ "${4:-}" == '--local' ]]; then
local_only=true
elif [[ -n "${4:-}" ]]; then
die 'download only supports optional --local'
fi
if [[ "$target" == 'lib' ]]; then
lib_result download "$cid" "$(abs_output_path "$out")" "$local_only" >/dev/null
else
check_common_deps
local api
api="$(target_api "$target")"
curl -fL "${api}/data/${cid}/network/stream" -o "$out"
fi
printf '%s\n' "$out"
}
lib_only_command() {
local command="$1"
shift
case "$command" in
spr|debug)
[[ $# -eq 0 ]] || die "$command does not accept arguments"
lib_result "$command"
;;
manifest)
[[ $# -eq 1 ]] || die 'manifest requires <cid>'
lib_result manifest "$1"
;;
connect)
[[ $# -ge 1 ]] || die 'connect requires <peer-id> [addr...]'
lib_result connect "$@"
;;
*)
return 1
;;
esac
}
size_to_bytes() {
local value="$1"
local number suffix
number="${value%[KkMmGg]}"
suffix="${value:${#number}}"
[[ "$number" =~ ^[0-9]+$ ]] || die "invalid size: $value"
case "$suffix" in
K|k) printf '%s\n' $((number * 1024)) ;;
M|m) printf '%s\n' $((number * 1024 * 1024)) ;;
G|g) printf '%s\n' $((number * 1024 * 1024 * 1024)) ;;
'') printf '%s\n' "$number" ;;
*) die "invalid size suffix: $value" ;;
esac
}
target_test() {
local target="$1"
[[ "$target" == 'local' || "$target" == 'lib' ]] || die 'test is supported only for local and lib targets'
check_common_deps
need sha256sum
local max_bytes=$((10 * 1024 * 1024))
local workspace report_ts report_path start_time start_epoch cleanup_done=0 cleanup_failures=0
workspace="$(mktemp -d "${TMPDIR:-/tmp}/logos-storage-test.XXXXXX")"
report_ts="$(date +%Y-%m-%d_%H-%M-%S)"
report_path="./report-${report_ts}.md"
start_time="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
start_epoch="$(date +%s)"
local -a cids=()
local -a local_cids=()
local -a result_sizes=()
local -a result_bytes=()
local -a result_sources=()
local -a result_downloads=()
local -a result_cids=()
local -a result_hashes=()
local -a cleanup_messages=()
cleanup() {
if [[ "$cleanup_done" == '1' ]]; then
return 0
fi
cleanup_done=1
local cid
for cid in "${local_cids[@]:-}"; do
if target_delete_cid "$target" "$cid" >/dev/null 2>&1; then
cleanup_messages+=("deleted $cid from $target")
else
cleanup_messages+=("failed to delete $cid from $target")
cleanup_failures=$((cleanup_failures + 1))
fi
done
for cid in "${cids[@]:-}"; do
if target_delete_cid remote "$cid" >/dev/null 2>&1; then
cleanup_messages+=("deleted $cid from remote")
else
cleanup_messages+=("failed to delete $cid from remote")
cleanup_failures=$((cleanup_failures + 1))
fi
done
if [[ "$TEST_KEEP_FILES" != '1' ]]; then
rm -rf "$workspace"
cleanup_messages+=("removed workspace $workspace")
else
printf 'kept test workspace: %s\n' "$workspace" >&2
cleanup_messages+=("kept workspace $workspace")
fi
}
trap cleanup RETURN
write_report() {
local end_time end_epoch duration i
end_time="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
end_epoch="$(date +%s)"
duration=$((end_epoch - start_epoch))
{
printf '# Logos Storage Test Report\n\n'
printf '- **Status:** PASS\n'
printf '- **Started:** `%s`\n' "$start_time"
printf '- **Finished:** `%s`\n' "$end_time"
printf '- **Duration:** `%ss`\n' "$duration"
printf '- **Target:** `%s`\n' "$target"
printf '- **Remote source:** `remote`\n'
printf '- **File sizes:** `%s`\n' "$TEST_FILE_SIZES"
printf '- **Workspace:** `%s`\n' "$workspace"
printf '- **Cleanup failures:** `%s`\n\n' "$cleanup_failures"
printf '## Files\n\n'
printf '| # | Size | Bytes | CID | SHA-256 | Source | Download |\n'
printf '|---:|---:|---:|---|---|---|---|\n'
for i in "${!result_cids[@]}"; do
printf '| %d | `%s` | `%s` | `%s` | `%s` | `%s` | `%s` |\n' \
"$((i + 1))" \
"${result_sizes[$i]}" \
"${result_bytes[$i]}" \
"${result_cids[$i]}" \
"${result_hashes[$i]}" \
"${result_sources[$i]}" \
"${result_downloads[$i]}"
done
printf '\n## Cleanup\n\n'
if [[ ${#cleanup_messages[@]} -eq 0 ]]; then
printf '- No cleanup actions recorded.\n'
else
for i in "${!cleanup_messages[@]}"; do
printf '- %s\n' "${cleanup_messages[$i]}"
done
fi
} > "$report_path"
}
printf 'Starting Logos Storage remote-to-%s test\n' "$target"
printf ' workspace: %s\n' "$workspace"
printf ' report: %s\n' "$report_path"
printf ' sizes: %s\n' "$TEST_FILE_SIZES"
local size index=0
for size in $TEST_FILE_SIZES; do
local bytes src out cid src_hash out_hash
bytes="$(size_to_bytes "$size")"
(( bytes <= max_bytes )) || die "test file size exceeds 10MB limit: $size"
index=$((index + 1))
src="${workspace}/source-${index}-${size}.bin"
out="${workspace}/download-${index}-${size}.bin"
dd if=/dev/urandom of="$src" bs="$size" count=1 status=none
src_hash="$(sha256sum "$src" | awk '{ print $1 }')"
printf '\n[%d] Generate %s random file (%s bytes)\n' "$index" "$size" "$bytes"
printf '[%d] Upload to remote\n' "$index"
cid="$(target_upload_file remote "$src")"
cids+=("$cid")
printf '[%d] Download via %s: %s\n' "$index" "$target" "$cid"
if [[ "$target" == 'local' ]]; then
target_fetch_cid local "$cid" --wait >/dev/null
local_cids+=("$cid")
target_download_cid local "$cid" "$out" >/dev/null
else
target_download_cid lib "$cid" "$out" >/dev/null
local_cids+=("$cid")
fi
out_hash="$(sha256sum "$out" | awk '{ print $1 }')"
[[ "$src_hash" == "$out_hash" ]] || die "hash mismatch for cid $cid"
result_sizes+=("$size")
result_bytes+=("$bytes")
result_sources+=("$src")
result_downloads+=("$out")
result_cids+=("$cid")
result_hashes+=("$src_hash")
printf '[%d] OK sha256=%s\n' "$index" "$src_hash"
done
printf '\nCleaning up remote and %s CIDs...\n' "$target"
cleanup
trap - RETURN
write_report
printf '\nTest passed\n'
printf ' target: %s\n' "$target"
printf ' files validated: %d\n' "$index"
printf ' cleanup failures: %d\n' "$cleanup_failures"
printf ' report: %s\n' "$report_path"
}
target_command() {
local target="$1"
local command="${2:-help}"
shift 2 || true
case "$command" in
upload) target_upload_file "$target" "${1:-}" ;;
upload-random) target_upload_random "$target" "$@" ;;
download) target_download_cid "$target" "$@" ;;
fetch) target_fetch_cid "$target" "$@" ;;
list) [[ $# -eq 0 ]] || die 'list does not accept arguments'; target_list_cids "$target" ;;
delete) target_delete_cid "$target" "$@" ;;
delete-all) target_delete_all "$target" "$@" ;;
exists) target_exists_cid "$target" "$@" ;;
space) [[ $# -eq 0 ]] || die 'space does not accept arguments'; target_simple_get "$target" 'space' ;;
peerid) [[ $# -eq 0 ]] || die 'peerid does not accept arguments'; target_simple_get "$target" 'peerid' ;;
test) [[ $# -eq 0 ]] || die 'test does not accept arguments yet'; target_test "$target" ;;
spr|debug|manifest|connect)
[[ "$target" == 'lib' ]] || die "$command is currently supported only for lib target"
lib_only_command "$command" "$@"
;;
help|--help|-h)
usage
;;
*)
die "unknown command for target '$target': $command"
;;
esac
}
cmd="${1:-help}"
shift || true
if is_target "$cmd"; then
target_command "$cmd" "$@"
exit 0
fi
case "$cmd" in
help|--help|-h)
usage
;;
tunnel)
case "${1:-}" in
start) tunnel_start ;;
stop) tunnel_stop ;;
status)
if tunnel_status; then
printf 'tunnel running\n'
else
printf 'tunnel stopped\n'
exit 1
fi
;;
*) die 'usage: tunnel start|stop|status' ;;
esac
;;
make-file) make_file "$@" ;;
last-cid) last_cid "$@" ;;
*)
usage >&2
exit 1
;;
esac