#!/usr/bin/env bash set -euo pipefail image="${IMAGE:-imagesilo:smoke}" platform="${PLATFORM:-linux/amd64}" port="${PORT:-18080}" suffix="${SMOKE_SUFFIX:-local}" if [[ ! "$suffix" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$ ]]; then printf 'SMOKE_SUFFIX must contain only 1-64 safe name characters\n' >&2 exit 1 fi container="imagesilo-smoke-${suffix}" restart_container="${container}-restart" permission_container="${container}-permission" volume="${container}-data" temporary_root="$(mktemp -d "${TMPDIR:-/tmp}/imagesilo-smoke.XXXXXX")" cookie_file="${temporary_root}/cookies" upload_response="${temporary_root}/upload.json" system_response="${temporary_root}/system.json" png_response="${temporary_root}/png.json" gif_response="${temporary_root}/gif.json" conversion_response="${temporary_root}/conversion.json" webp_response="${temporary_root}/webp.json" import_response="${temporary_root}/import.json" import_conflict_response="${temporary_root}/import-conflict.json" import_source="${temporary_root}/import-source.jpg" import_token_response="${temporary_root}/import-token.json" import_manifest="${temporary_root}/import.tsv" import_result="${temporary_root}/import-result.jsonl" overview_response="${temporary_root}/overview.json" runtime_before_response="${temporary_root}/runtime-before.json" runtime_after_response="${temporary_root}/runtime-after.json" converted_webp="${temporary_root}/converted.webp" delivery_headers="${temporary_root}/delivery.headers" range_body="${temporary_root}/range.bin" permission_log="${temporary_root}/permission.log" permission_directory="${temporary_root}/readonly-data" smoke_email="admin@example.com" smoke_password="ImageSilo-${suffix}-Smoke-Password!" cleanup_runtime() { docker rm --force "$container" "$restart_container" "$permission_container" >/dev/null 2>&1 || true docker volume rm "$volume" >/dev/null 2>&1 || true } cleanup() { cleanup_runtime rm -rf "$temporary_root" } trap cleanup EXIT INT TERM hash_stdin() { if command -v sha256sum >/dev/null 2>&1; then sha256sum | awk '{print $1}' else shasum -a 256 | awk '{print $1}' fi } wait_ready() { local base_url="$1" for _ in $(seq 1 60); do if curl --fail --silent "${base_url}/readyz" >/dev/null; then return 0 fi sleep 1 done docker logs "$container" || true return 1 } wait_healthy() { local name="$1" for _ in $(seq 1 60); do if test "$(docker inspect --format '{{.State.Health.Status}}' "$name" 2>/dev/null || true)" = "healthy"; then return 0 fi sleep 1 done docker inspect "$name" || true return 1 } cleanup_runtime docker volume create "$volume" >/dev/null printf '%s\n' "$smoke_password" | docker run --rm --interactive \ --platform "$platform" \ --volume "${volume}:/data" \ "$image" admin create --email "$smoke_email" --password-stdin docker run --detach \ --platform "$platform" \ --name "$container" \ --publish "127.0.0.1:${port}:8080" \ --env IMAGESILO_COOKIE_SECURE=false \ --volume "${volume}:/data" \ "$image" >/dev/null base_url="http://127.0.0.1:${port}" wait_ready "$base_url" wait_healthy "$container" test "$(docker inspect --format '{{.Config.User}}' "$container")" = "10001:10001" test "$(docker inspect --format '{{json .Config.Entrypoint}}' "$container")" = '["/usr/local/bin/imagesilo"]' test "$(docker inspect --format '{{json .Config.Healthcheck.Test}}' "$container")" = '["CMD","/usr/local/bin/imagesilo","healthcheck"]' docker exec "$container" sh -ec ' ! command -v node >/dev/null ! command -v go >/dev/null ! command -v cc >/dev/null ! command -v gcc >/dev/null test ! -e /src test ! -e /opt/vips/bin test ! -e /opt/vips/include test ! -e /opt/vips/lib/pkgconfig test ! -e /opt/vips/share test -n "$(find /opt/vips/lib -type f -name "libvips.so.*" -print -quit)" test -r /usr/share/doc/imagesilo/THIRD_PARTY_NOTICES.md grep -q TailAdmin /usr/share/doc/imagesilo/THIRD_PARTY_NOTICES.md grep -q Outfit /usr/share/doc/imagesilo/THIRD_PARTY_NOTICES.md ' mkdir -p "$permission_directory" chmod 0555 "$permission_directory" if docker run --name "$permission_container" \ --platform "$platform" \ --volume "${permission_directory}:/data:ro" \ "$image" serve >"$permission_log" 2>&1; then printf 'read-only /data unexpectedly allowed ImageSilo to start\n' >&2 exit 1 fi grep -Eqi 'read-only file system|permission denied' "$permission_log" docker rm "$permission_container" >/dev/null curl --fail --silent --show-error \ --cookie-jar "$cookie_file" \ --header 'Content-Type: application/json' \ --data "{\"email\":\"${smoke_email}\",\"password\":\"${smoke_password}\"}" \ "${base_url}/api/v1/auth/login" >/dev/null csrf_token="$(awk '$6 == "imagesilo_csrf" { print $7 }' "$cookie_file" | tail -n 1)" test -n "$csrf_token" curl --fail --silent --show-error \ --cookie "$cookie_file" \ "${base_url}/api/v1/system" >"$system_response" test "$(jq --raw-output '.vipsVersion' "$system_response")" = "8.18.4" test "$(jq '.supportedFormats | length' "$system_response")" = "4" test "$(jq '.processingConcurrency' "$system_response")" = "1" test "$(jq '.maxTotalPixels' "$system_response")" = "16000000" curl --fail --silent --show-error --cookie "$cookie_file" "${base_url}/api/v1/overview" >"$runtime_before_response" goroutines_before="$(jq '.goroutines' "$runtime_before_response")" fds_before="$(docker exec "$container" sh -c 'find /proc/1/fd -mindepth 1 -maxdepth 1 | wc -l')" go run ./tests/performance/jpeg_stdout -width 3000 -height 2000 -quality 90 | curl --fail --silent --show-error \ --cookie "$cookie_file" \ --header "X-CSRF-Token: ${csrf_token}" \ --form 'file=@-;filename=container-smoke.jpg;type=image/jpeg' \ "${base_url}/api/v1/images" >"$upload_response" image_id="$(jq --raw-output '.id' "$upload_response")" expected_hash="$(jq --raw-output '.storedSha256' "$upload_response")" test -n "$image_id" test "$expected_hash" != "null" actual_hash="$(curl --fail --silent --show-error "${base_url}/image/${image_id}" | hash_stdin)" test "$actual_hash" = "$expected_hash" curl --fail --silent --show-error \ --dump-header "$delivery_headers" \ --output "$range_body" \ --header 'Range: bytes=0-3' \ "${base_url}/image/${image_id}" test "$(wc -c <"$range_body" | tr -d ' ')" = "4" grep -Eqi '^Content-Range: bytes 0-3/[0-9]+' "$delivery_headers" curl --fail --silent --show-error \ --dump-header "$delivery_headers" \ --output "$range_body" \ --header 'Range: bytes=-4' \ "${base_url}/image/${image_id}" test "$(wc -c <"$range_body" | tr -d ' ')" = "4" grep -Eqi '^Content-Range: bytes [0-9]+-[0-9]+/[0-9]+' "$delivery_headers" etag="$(curl --fail --silent --show-error --head "${base_url}/image/${image_id}" | awk 'tolower($1) == "etag:" { print $2 }' | tr -d '\r')" test -n "$etag" test "$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' --header "If-None-Match: ${etag}" "${base_url}/image/${image_id}")" = "304" test "$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' --header 'Range: bytes=999999999-' "${base_url}/image/${image_id}")" = "416" curl --fail --silent --show-error --head "${base_url}/image/${image_id}" >"$delivery_headers" grep -Eqi "^Content-Length: $(jq '.storedSize' "$upload_response")" "$delivery_headers" go run ./tests/performance/image_stdout -format png -width 800 -height 600 | curl --fail --silent --show-error \ --cookie "$cookie_file" \ --header "X-CSRF-Token: ${csrf_token}" \ --form 'file=@-;filename=container-smoke.png;type=image/png' \ "${base_url}/api/v1/images" >"$png_response" test "$(jq --raw-output '.mimeType' "$png_response")" = "image/png" test "$(jq --raw-output '.sourceSha256' "$png_response")" = "$(jq --raw-output '.storedSha256' "$png_response")" go run ./tests/performance/image_stdout -format gif -width 320 -height 240 | curl --fail --silent --show-error \ --cookie "$cookie_file" \ --header "X-CSRF-Token: ${csrf_token}" \ --form 'file=@-;filename=container-smoke.gif;type=image/gif' \ "${base_url}/api/v1/images" >"$gif_response" test "$(jq --raw-output '.mimeType' "$gif_response")" = "image/gif" gif_hash="$(jq --raw-output '.sourceSha256' "$gif_response")" test "$gif_hash" = "$(jq --raw-output '.storedSha256' "$gif_response")" test "$gif_hash" = "$(curl --fail --silent --show-error "${base_url}$(jq --raw-output '.standardUrl' "$gif_response")" | hash_stdin)" curl --fail --silent --show-error \ --cookie "$cookie_file" \ --header "X-CSRF-Token: ${csrf_token}" \ --header 'Content-Type: application/json' \ --request PATCH \ --data '{"compressionEnabled":false,"jpegQuality":85,"webpQuality":82,"pngCompressionLevel":6,"conversionEnabled":true,"conversionWebpQuality":82,"conversionWebpLossless":false}' \ "${base_url}/api/v1/settings/processing" >/dev/null go run ./tests/performance/image_stdout -format jpeg -width 800 -height 600 -quality 95 | curl --fail --silent --show-error \ --cookie "$cookie_file" \ --header "X-CSRF-Token: ${csrf_token}" \ --form 'file=@-;filename=container-convert.jpg;type=image/jpeg' \ "${base_url}/api/v1/images" >"$conversion_response" test "$(jq --raw-output '.mimeType' "$conversion_response")" = "image/webp" test "$(jq --raw-output '.processingSummary.action' "$conversion_response")" = "convert" test "$(jq --raw-output '.sourceSha256' "$conversion_response")" != "$(jq --raw-output '.storedSha256' "$conversion_response")" curl --fail --silent --show-error \ --output "$converted_webp" \ "${base_url}$(jq --raw-output '.standardUrl' "$conversion_response")" curl --fail --silent --show-error \ --cookie "$cookie_file" \ --header "X-CSRF-Token: ${csrf_token}" \ --header 'Content-Type: application/json' \ --request PATCH \ --data '{"compressionEnabled":false,"jpegQuality":85,"webpQuality":82,"pngCompressionLevel":6,"conversionEnabled":false,"conversionWebpQuality":82,"conversionWebpLossless":false}' \ "${base_url}/api/v1/settings/processing" >/dev/null curl --fail --silent --show-error \ --cookie "$cookie_file" \ --header "X-CSRF-Token: ${csrf_token}" \ --form "file=@${converted_webp};filename=container-smoke.webp;type=image/webp" \ "${base_url}/api/v1/images" >"$webp_response" test "$(jq --raw-output '.mimeType' "$webp_response")" = "image/webp" test "$(jq --raw-output '.sourceSha256' "$webp_response")" = "$(jq --raw-output '.storedSha256' "$webp_response")" go run ./tests/performance/image_stdout -format jpeg -width 160 -height 120 -quality 90 >"$import_source" import_source_hash="$(hash_stdin <"$import_source")" curl --fail --silent --show-error \ --cookie "$cookie_file" \ --header "X-CSRF-Token: ${csrf_token}" \ --header 'Content-Type: application/json' \ --data '{"name":"container importer","scopes":["images:upload","aliases:write"]}' \ "${base_url}/api/v1/api-tokens" >"$import_token_response" import_token="$(jq --raw-output '.token' "$import_token_response")" printf '/legacy/container-import.jpg\t%s\n' "$import_source" >"$import_manifest" IMAGESILO_BASE_URL="$base_url" IMAGESILO_TOKEN="$import_token" \ scripts/import-manifest.sh --manifest "$import_manifest" --output "$import_result" --visibility public jq --raw-output '.response | fromjson' "$import_result" >"$import_response" import_id="$(jq --raw-output '.imageId' "$import_response")" test "$(jq --raw-output '.sha256' "$import_response")" = "$import_source_hash" alias_etag="$(curl --fail --silent --show-error --head "${base_url}/legacy/container-import.jpg" | awk 'tolower($1) == "etag:" { print $2 }' | tr -d '\r')" import_etag="$(curl --fail --silent --show-error --head "${base_url}/image/${import_id}" | awk 'tolower($1) == "etag:" { print $2 }' | tr -d '\r')" test -n "$alias_etag" test "$alias_etag" = "$import_etag" curl --fail --silent --show-error --cookie "$cookie_file" "${base_url}/api/v1/overview" >"$overview_response" images_before_conflict="$(jq '.imageCount' "$overview_response")" conflict_status="$(curl --silent --show-error \ --output "$import_conflict_response" \ --write-out '%{http_code}' \ --cookie "$cookie_file" \ --header "X-CSRF-Token: ${csrf_token}" \ --form "file=@${import_source};filename=legacy-import.jpg;type=image/jpeg" \ --form 'alias=/legacy/container-import.jpg' \ --form 'visibility=public' \ "${base_url}/api/v1/imports")" test "$conflict_status" = "409" curl --fail --silent --show-error --cookie "$cookie_file" "${base_url}/api/v1/overview" >"$overview_response" test "$(jq '.imageCount' "$overview_response")" = "$images_before_conflict" curl --fail --silent --show-error \ --cookie "$cookie_file" \ "${base_url}$(jq --raw-output '.thumbnailUrl' "$webp_response")" >/dev/null test "$(docker inspect --format '{{.State.Health.Status}}' "$container")" = "healthy" curl --fail --silent --show-error "${base_url}/admin/login" | grep -q '
' for _ in $(seq 1 4); do curl --fail --silent --show-error "${base_url}/image/${image_id}" >/dev/null done curl --fail --silent --show-error \ --cookie "$cookie_file" \ --header "X-CSRF-Token: ${csrf_token}" \ --request POST \ "${base_url}/api/v1/maintenance/inspect" >/dev/null curl --fail --silent --show-error \ --cookie "$cookie_file" \ --header "X-CSRF-Token: ${csrf_token}" \ --request POST \ "${base_url}/api/v1/maintenance/rebuild" >/dev/null curl --fail --silent --show-error --cookie "$cookie_file" "${base_url}/api/v1/overview" >"$runtime_after_response" goroutines_after="$(jq '.goroutines' "$runtime_after_response")" fds_after="$(docker exec "$container" sh -c 'find /proc/1/fd -mindepth 1 -maxdepth 1 | wc -l')" temporary_after="$(docker exec "$container" sh -c 'find /data/tmp -type f | wc -l')" test "$goroutines_after" -le "$((goroutines_before + 4))" test "$fds_after" -le "$((fds_before + 8))" test "$temporary_after" = "0" docker exec "$container" rm "/data/images/${import_id}" docker stop --timeout 15 "$container" >/dev/null test "$(docker inspect --format '{{.State.ExitCode}}' "$container")" = "0" docker logs "$container" 2>&1 | grep -q 'shutdown requested' docker rm "$container" >/dev/null docker run --detach \ --platform "$platform" \ --name "$restart_container" \ --publish "127.0.0.1:${port}:8080" \ --env IMAGESILO_COOKIE_SECURE=false \ --volume "${volume}:/data" \ "$image" >/dev/null container="$restart_container" wait_ready "$base_url" wait_healthy "$container" restarted_hash="$(curl --fail --silent --show-error "${base_url}/image/${image_id}" | hash_stdin)" test "$restarted_hash" = "$expected_hash" curl --fail --silent --show-error --cookie "$cookie_file" "${base_url}/api/v1/overview" >"$overview_response" test "$(jq '.missingImageCount' "$overview_response")" = "1" test "$(jq --raw-output --arg id "$import_id" '.missingImageIds | index($id) != null' "$overview_response")" = "true" test "$(curl --silent --output /dev/null --write-out '%{http_code}' "${base_url}/legacy/container-import.jpg")" = "404" docker stop --timeout 15 "$container" >/dev/null test "$(docker inspect --format '{{.State.ExitCode}}' "$container")" = "0" docker logs "$container" 2>&1 | grep -q 'shutdown requested' printf 'ImageSilo container smoke passed: platform=%s image_id=%s sha256=%s\n' "$platform" "$image_id" "$expected_hash"