From baab060628f011110026989c724524a662a85c51 Mon Sep 17 00:00:00 2001 From: Charles GTE Date: Sat, 20 Jun 2026 19:35:40 +0200 Subject: [PATCH] fix: azurite and refactoring --- Makefile | 10 +++- docker-compose.yml | 24 +++++++++ seeds/azurite/azurite-list.sh | 51 +++++++++++++++++++ seeds/azurite/azurite-seed.sh | 46 +++++++++++++++++ src/features/channel/channel-form.schema.ts | 2 +- src/features/channel/channels-helpers.tsx | 2 +- .../{Blob.form.tsx => az-blob.form.tsx} | 0 .../{Blob.schema.ts => az-blob.schema.ts} | 5 +- .../channel/storages/{Blob.ts => az-blob.ts} | 0 src/features/channel/storages/index.ts | 2 +- 10 files changed, 137 insertions(+), 5 deletions(-) create mode 100755 seeds/azurite/azurite-list.sh create mode 100755 seeds/azurite/azurite-seed.sh rename src/features/channel/storages/{Blob.form.tsx => az-blob.form.tsx} (100%) rename src/features/channel/storages/{Blob.schema.ts => az-blob.schema.ts} (73%) rename src/features/channel/storages/{Blob.ts => az-blob.ts} (100%) diff --git a/Makefile b/Makefile index 57081fac..02ceac45 100644 --- a/Makefile +++ b/Makefile @@ -45,4 +45,12 @@ export-keycloak: @docker cp kc-exporter:/tmp/kc-export/. ./seeds/keycloak/ @docker rm -f kc-exporter >/dev/null 2>&1 @docker compose -f docker-compose.func.yml start keycloak >/dev/null 2>&1 - @echo "Keycloak configuration and users exported to seeds/keycloak/*.json" \ No newline at end of file + @echo "Keycloak configuration and users exported to seeds/keycloak/*.json" + +seed-blob: + @chmod +x seeds/azurite/azurite-seed.sh + @bash seeds/azurite/azurite-seed.sh + +list-blob: + @chmod +x seeds/azurite/azurite-list.sh + @bash seeds/azurite/azurite-list.sh \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index f81c2324..b74a7f39 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -41,5 +41,29 @@ services: - "8025:8025" - "1025:1025" + azurite: + image: mcr.microsoft.com/azure-storage/azurite + command: azurite --blobHost 0.0.0.0 --skipApiVersionCheck + ports: + - "10000:10000" + volumes: + - azurite-data:/data + extra_hosts: + - "localhost:host-gateway" + networks: + - portabase + # DefaultEndpointsProtocol=http; + # AccountName=devstoreaccount1; + # AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==; + # BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1; + # QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1; + # TableEndpoint=http://127.0.0.1:10002/devstoreaccount1; + volumes: postgres-data: + azurite-data: + +networks: + portabase: + name: portabase_network + external: true diff --git a/seeds/azurite/azurite-list.sh b/seeds/azurite/azurite-list.sh new file mode 100755 index 00000000..456c999c --- /dev/null +++ b/seeds/azurite/azurite-list.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# +# azurite-list.sh — enumerate every container and blob in a local Azurite emulator. +# +# Usage: +# ./azurite-list.sh # list containers + blobs +# ./azurite-list.sh -v # also print blob size, last-modified, content-type +# + +set -euo pipefail + +export AZURE_STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;" + +VERBOSE=0 +[[ "${1:-}" == "-v" || "${1:-}" == "--verbose" ]] && VERBOSE=1 + +command -v az >/dev/null 2>&1 || { echo "ERROR: 'az' CLI not found in PATH." >&2; exit 1; } +command -v jq >/dev/null 2>&1 || { echo "ERROR: 'jq' not found in PATH." >&2; exit 1; } + +if ! az storage container list --num-results 1 >/dev/null 2>&1; then + echo "ERROR: Cannot reach Azurite at 127.0.0.1:10000. Is the container running?" >&2 + echo " Start it with: docker start azurite" >&2 + exit 1 +fi + +containers=$(az storage container list --query "[].name" -o tsv) + +if [[ -z "$containers" ]]; then + echo "(no containers found in this emulator)" + exit 0 +fi + +while IFS= read -r container; do + echo "📦 container: $container" + + if [[ "$VERBOSE" -eq 1 ]]; then + az storage blob list --container-name "$container" \ + --query "[].{name:name, bytes:properties.contentLength, modified:properties.lastModified, type:properties.contentSettings.contentType}" \ + -o json \ + | jq -r '.[] | " • \(.name) [\(.bytes) bytes] \(.type // "?") \(.modified)"' + else + az storage blob list --container-name "$container" \ + --query "[].{name:name, bytes:properties.contentLength}" \ + -o json \ + | jq -r '.[] | " • \(.name) [\(.bytes) bytes]"' + fi + + count=$(az storage blob list --container-name "$container" --query "length(@)" -o tsv) + [[ "$count" -eq 0 ]] && echo " (empty)" + echo +done <<< "$containers" \ No newline at end of file diff --git a/seeds/azurite/azurite-seed.sh b/seeds/azurite/azurite-seed.sh new file mode 100755 index 00000000..c3b80595 --- /dev/null +++ b/seeds/azurite/azurite-seed.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# +# azurite-seed.sh — seed a local Azurite emulator with a container + sample blob. +# +# Usage: +# ./azurite-seed.sh # create container "portabase" + sample blob +# ./azurite-seed.sh my-container # override container name +# + +set -euo pipefail + +export AZURE_STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;" + +CONTAINER="${1:-portabase}" +BLOB_NAME="hello.txt" + +command -v az >/dev/null 2>&1 || { echo "ERROR: 'az' CLI not found in PATH." >&2; exit 1; } + +# Reachability check — surface the real az error instead of guessing "container down". +if ! err=$(az storage container list --num-results 1 2>&1 >/dev/null); then + echo "ERROR: Azurite query failed:" >&2 + printf '%s\n' "$err" | sed 's/^/ /' >&2 + echo " Is Azurite up? Start it with: docker compose up -d azurite" >&2 + exit 1 +fi + +# Create container (idempotent — az returns created:false if it already exists). +az storage container create --name "$CONTAINER" -o none +echo "📦 container ready: $CONTAINER" + +# Upload a sample blob from a temp file. +tmp=$(mktemp) +trap 'rm -f "$tmp"' EXIT +printf 'hello from azurite seed — %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$tmp" + +az storage blob upload \ + --container-name "$CONTAINER" \ + --name "$BLOB_NAME" \ + --file "$tmp" \ + --overwrite true \ + --no-progress \ + -o none +echo " • uploaded blob: $BLOB_NAME" + +echo +echo "Seed complete. Verify with: make list-blob" diff --git a/src/features/channel/channel-form.schema.ts b/src/features/channel/channel-form.schema.ts index ee8e1cc5..1fe5d6fe 100644 --- a/src/features/channel/channel-form.schema.ts +++ b/src/features/channel/channel-form.schema.ts @@ -10,9 +10,9 @@ import {NextcloudChannelConfigSchema} from "@/features/channel/notifications/nex import {PushoverChannelConfigSchema} from "@/features/channel/notifications/pushover.schema"; import {S3ChannelConfigSchema} from "@/features/channel/storages/s3.schema"; import {GoogleDriveChannelConfigSchema} from "@/features/channel/storages/google-drive/google-drive.schema"; -import { BlobChannelConfigSchema } from "@/features/channel/storages/Blob.schema"; import {LocalChannelConfigSchema} from "@/features/channel/storages/local.schema"; import {TeamsChannelConfigSchema} from "@/features/channel/notifications/teams.schema"; +import {BlobChannelConfigSchema} from "@/features/channel/storages/az-blob.schema"; const BaseChannelFormSchema = z.object({ diff --git a/src/features/channel/channels-helpers.tsx b/src/features/channel/channels-helpers.tsx index 822a3724..5d1f6b6b 100644 --- a/src/features/channel/channels-helpers.tsx +++ b/src/features/channel/channels-helpers.tsx @@ -43,7 +43,7 @@ import { } from "@/features/channel/storages/google-drive/google-drive.form"; import { StorageBlobForm -} from "@/features/channel/storages/Blob.form"; +} from "@/features/channel/storages/az-blob.form"; export type ChannelKind = "notification" | "storage"; diff --git a/src/features/channel/storages/Blob.form.tsx b/src/features/channel/storages/az-blob.form.tsx similarity index 100% rename from src/features/channel/storages/Blob.form.tsx rename to src/features/channel/storages/az-blob.form.tsx diff --git a/src/features/channel/storages/Blob.schema.ts b/src/features/channel/storages/az-blob.schema.ts similarity index 73% rename from src/features/channel/storages/Blob.schema.ts rename to src/features/channel/storages/az-blob.schema.ts index a0dcda4c..dc9aa75e 100644 --- a/src/features/channel/storages/Blob.schema.ts +++ b/src/features/channel/storages/az-blob.schema.ts @@ -5,7 +5,10 @@ export const BlobChannelConfigSchema = z.object({ accountKey: z.string().optional(), connectionString: z.string().optional(), containerName: z.string().min(1, "Container name is required"), - endpointUrl: z.string().url("Endpoint URL must be a valid URL").optional(), + endpointUrl: z.preprocess( + (v) => (v === "" ? undefined : v), + z.string().url("Endpoint URL must be a valid URL").optional(), + ), }).refine( (data) => data.accountKey || data.connectionString, {message: "Either account key or connection string is required"} diff --git a/src/features/channel/storages/Blob.ts b/src/features/channel/storages/az-blob.ts similarity index 100% rename from src/features/channel/storages/Blob.ts rename to src/features/channel/storages/az-blob.ts diff --git a/src/features/channel/storages/index.ts b/src/features/channel/storages/index.ts index e322ce98..851b6bf2 100644 --- a/src/features/channel/storages/index.ts +++ b/src/features/channel/storages/index.ts @@ -6,7 +6,7 @@ import { import {uploadLocal, getLocal, deleteLocal, pingLocal, copyLocal} from './local'; import {copyS3, deleteS3, getS3, pingS3, uploadS3} from "@/features/channel/storages/s3"; -import {copyBlob, deleteBlob, getBlob, pingBlob, uploadBlob} from "@/features/channel/storages/Blob"; +import {copyBlob, deleteBlob, getBlob, pingBlob, uploadBlob} from "@/features/channel/storages/az-blob"; import { copyGoogleDrive, deleteGoogleDrive,