Compare commits

..

1 commit

Author SHA1 Message Date
Cursor Origin
ce3e704ab7 Pre-computed merge for change #1 1970-01-01 00:00:00 +00:00
5 changed files with 16 additions and 54 deletions

View file

@ -78,10 +78,10 @@ export const config = {
redisUrlCore: isDev ? (Deno.env.get("REDIS_URL_CORE") ?? "") : required("REDIS_URL_CORE"), redisUrlCore: isDev ? (Deno.env.get("REDIS_URL_CORE") ?? "") : required("REDIS_URL_CORE"),
// --- Contabo Object Storage (S3-compatible) -- expedientes/PDFs/logos --- // --- Contabo Object Storage (S3-compatible) -- expedientes/PDFs/logos ---
s3Endpoint: (Deno.env.get("S3_ENDPOINT") ?? "").trim().replace(/\/$/, ""), s3Endpoint: Deno.env.get("S3_ENDPOINT") ?? "",
s3Bucket: (Deno.env.get("S3_BUCKET") ?? "").trim(), s3Bucket: Deno.env.get("S3_BUCKET") ?? "",
s3Region: (Deno.env.get("S3_REGION") ?? "auto").trim() || "auto", s3Region: Deno.env.get("S3_REGION") ?? "us-east-1",
s3AccessKeyId: (Deno.env.get("S3_ACCESS_KEY_ID") ?? "").trim(), s3AccessKeyId: Deno.env.get("S3_ACCESS_KEY_ID") ?? "",
s3SecretAccessKey: Deno.env.get("S3_SECRET_ACCESS_KEY") ?? "", s3SecretAccessKey: Deno.env.get("S3_SECRET_ACCESS_KEY") ?? "",
isDev, isDev,

View file

@ -1277,9 +1277,7 @@ await Promise.all([
} }
})), })),
startup("storage S3/R2", pingStorage().then((s) => { startup("storage S3/R2", pingStorage().then((s) => {
if (!s.ok) { if (!s.ok) throw new Error("Storage no responde (HeadBucket R2 o disco local)");
throw new Error(`Storage no responde: ${s.error ?? "sin detalle"}`);
}
if (!config.isDev && !s.configured) { if (!config.isDev && !s.configured) {
throw new Error("S3_ENDPOINT/S3_BUCKET/S3_* son obligatorios fuera de desarrollo"); throw new Error("S3_ENDPOINT/S3_BUCKET/S3_* son obligatorios fuera de desarrollo");
} }

View file

@ -1,6 +1,5 @@
/** /**
* Sonda S3/R2: ListObjects (o put/get/delete). HeadBucket no se usa: * Sonda de Contabo (S3): HeadBucket + put/get/delete.
* R2 suele devolver 403 con tokens acotados al bucket.
* *
* Desde api/: * Desde api/:
* deno run --allow-net --allow-env --allow-read --allow-write scripts/verify-storage.ts * deno run --allow-net --allow-env --allow-read --allow-write scripts/verify-storage.ts
@ -28,9 +27,9 @@ if (!s3Configured()) {
console.log(`S3 endpoint=${config.s3Endpoint} bucket=${config.s3Bucket} region=${config.s3Region}`); console.log(`S3 endpoint=${config.s3Endpoint} bucket=${config.s3Bucket} region=${config.s3Region}`);
const ping = await pingStorage(); const ping = await pingStorage();
if (!ping.ok) { if (!ping.ok) {
console.error(`FAIL - ping bucket: ${ping.error ?? "sin detalle"}`); console.error("FAIL - HeadBucket: no se pudo alcanzar el bucket (credenciales, red o nombre).");
Deno.exit(1); Deno.exit(1);
} }
console.log("OK - ping bucket (ListObjects o sonda write)"); console.log("OK - HeadBucket");
await probeStorageReadWrite(); await probeStorageReadWrite();
console.log("OK - put/get/delete de objeto sonda"); console.log("OK - put/get/delete de objeto sonda");

View file

@ -2,7 +2,7 @@ import {
S3Client, S3Client,
PutObjectCommand, PutObjectCommand,
GetObjectCommand, GetObjectCommand,
ListObjectsV2Command, HeadBucketCommand,
DeleteObjectCommand, DeleteObjectCommand,
} from "npm:@aws-sdk/client-s3@3"; } from "npm:@aws-sdk/client-s3@3";
import { mkdir, readFile, writeFile } from "node:fs/promises"; import { mkdir, readFile, writeFile } from "node:fs/promises";
@ -32,58 +32,23 @@ export type StoragePing = {
configured: boolean; configured: boolean;
ok: boolean; ok: boolean;
backend: "s3" | "local"; backend: "s3" | "local";
error?: string;
}; };
function s3Err(e: unknown): string { /** HeadBucket (S3) o escritura de prueba en disco local. */
if (e && typeof e === "object") {
const o = e as {
name?: string;
message?: string;
Code?: string;
$metadata?: { httpStatusCode?: number };
};
return [o.name, o.Code, o.$metadata?.httpStatusCode, o.message].filter(Boolean).join(": ");
}
return String(e);
}
/**
* R2: HeadBucket suele dar 403 con token de Account API acotado al bucket.
* ListObjectsV2 (o un put/get sonda) es lo que realmente usamos en runtime.
*/
export async function pingStorage(): Promise<StoragePing> { export async function pingStorage(): Promise<StoragePing> {
if (!s3Configured()) { if (!s3Configured()) {
try { try {
await mkdir(join(DATA_DIR, "local-objects"), { recursive: true }); await mkdir(join(DATA_DIR, "local-objects"), { recursive: true });
return { configured: false, ok: true, backend: "local" }; return { configured: false, ok: true, backend: "local" };
} catch (e) { } catch {
return { configured: false, ok: false, backend: "local", error: s3Err(e) }; return { configured: false, ok: false, backend: "local" };
} }
} }
try { try {
await getClient().send( await getClient().send(new HeadBucketCommand({ Bucket: config.s3Bucket }));
new ListObjectsV2Command({ Bucket: config.s3Bucket, MaxKeys: 1 }),
);
return { configured: true, ok: true, backend: "s3" }; return { configured: true, ok: true, backend: "s3" };
} catch (listErr) { } catch {
try { return { configured: true, ok: false, backend: "s3" };
await probeStorageReadWrite();
return { configured: true, ok: true, backend: "s3" };
} catch (writeErr) {
const error = `list=${s3Err(listErr)}; write=${s3Err(writeErr)}`;
console.error(
"[storage] ping failed",
error,
"endpoint=",
config.s3Endpoint,
"bucket=",
config.s3Bucket,
"region=",
config.s3Region,
);
return { configured: true, ok: false, backend: "s3", error };
}
} }
} }

View file

@ -131,7 +131,7 @@ sudo docker logs api-<uuid>-<timestamp>
Busca `[startup] FAIL …`. Causas típicas: Busca `[startup] FAIL …`. Causas típicas:
- **Redis:** `REDIS_URL_IAM` / `REDIS_URL_CORE` con el UUID de **Postgres**. Redis tiene el suyo (Redis URL internal). - **Redis:** `REDIS_URL_IAM` / `REDIS_URL_CORE` con el UUID de **Postgres**. Redis tiene el suyo (Redis URL internal).
- **S3/R2:** endpoint `https://<ACCOUNT_ID>.r2.cloudflarestorage.com` (sin barra final, **sin** el nombre del bucket), `S3_REGION=auto`, token **Account API** con Object Read & Write al bucket. R2 a menudo responde 403 a HeadBucket; el ping de la API usa ListObjects. - **S3/R2:** faltan `S3_*` o HeadBucket falla (token Account API, Object Read & Write, bucket correcto, `S3_REGION=auto`).
- **Postgres `_app`:** Liquibase usa `*_OWNER`; el runtime usa `DATABASE_URL_PLATFORM`, `DATABASE_URL_IAM`, `DATABASE_URL_CORE` (passwords distintos). - **Postgres `_app`:** Liquibase usa `*_OWNER`; el runtime usa `DATABASE_URL_PLATFORM`, `DATABASE_URL_IAM`, `DATABASE_URL_CORE` (passwords distintos).
### Coolify pide `PLATFORM_OWNER_PASSWORD`, `IAM_APP_PASSWORD`, `*_REDIS_PASSWORD`… ### Coolify pide `PLATFORM_OWNER_PASSWORD`, `IAM_APP_PASSWORD`, `*_REDIS_PASSWORD`…