mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 12:43:18 +00:00
Compare commits
2 commits
ce3e704ab7
...
fc5c2c0791
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc5c2c0791 | ||
|
|
ab20701fea |
5 changed files with 54 additions and 16 deletions
|
|
@ -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") ?? "",
|
s3Endpoint: (Deno.env.get("S3_ENDPOINT") ?? "").trim().replace(/\/$/, ""),
|
||||||
s3Bucket: Deno.env.get("S3_BUCKET") ?? "",
|
s3Bucket: (Deno.env.get("S3_BUCKET") ?? "").trim(),
|
||||||
s3Region: Deno.env.get("S3_REGION") ?? "us-east-1",
|
s3Region: (Deno.env.get("S3_REGION") ?? "auto").trim() || "auto",
|
||||||
s3AccessKeyId: Deno.env.get("S3_ACCESS_KEY_ID") ?? "",
|
s3AccessKeyId: (Deno.env.get("S3_ACCESS_KEY_ID") ?? "").trim(),
|
||||||
s3SecretAccessKey: Deno.env.get("S3_SECRET_ACCESS_KEY") ?? "",
|
s3SecretAccessKey: Deno.env.get("S3_SECRET_ACCESS_KEY") ?? "",
|
||||||
|
|
||||||
isDev,
|
isDev,
|
||||||
|
|
|
||||||
|
|
@ -1277,7 +1277,9 @@ await Promise.all([
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
startup("storage S3/R2", pingStorage().then((s) => {
|
startup("storage S3/R2", pingStorage().then((s) => {
|
||||||
if (!s.ok) throw new Error("Storage no responde (HeadBucket R2 o disco local)");
|
if (!s.ok) {
|
||||||
|
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");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* Sonda de Contabo (S3): HeadBucket + put/get/delete.
|
* Sonda S3/R2: ListObjects (o put/get/delete). HeadBucket no se usa:
|
||||||
|
* 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
|
||||||
|
|
@ -27,9 +28,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 - HeadBucket: no se pudo alcanzar el bucket (credenciales, red o nombre).");
|
console.error(`FAIL - ping bucket: ${ping.error ?? "sin detalle"}`);
|
||||||
Deno.exit(1);
|
Deno.exit(1);
|
||||||
}
|
}
|
||||||
console.log("OK - HeadBucket");
|
console.log("OK - ping bucket (ListObjects o sonda write)");
|
||||||
await probeStorageReadWrite();
|
await probeStorageReadWrite();
|
||||||
console.log("OK - put/get/delete de objeto sonda");
|
console.log("OK - put/get/delete de objeto sonda");
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import {
|
||||||
S3Client,
|
S3Client,
|
||||||
PutObjectCommand,
|
PutObjectCommand,
|
||||||
GetObjectCommand,
|
GetObjectCommand,
|
||||||
HeadBucketCommand,
|
ListObjectsV2Command,
|
||||||
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,23 +32,58 @@ export type StoragePing = {
|
||||||
configured: boolean;
|
configured: boolean;
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
backend: "s3" | "local";
|
backend: "s3" | "local";
|
||||||
|
error?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** HeadBucket (S3) o escritura de prueba en disco local. */
|
function s3Err(e: unknown): string {
|
||||||
|
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 {
|
} catch (e) {
|
||||||
return { configured: false, ok: false, backend: "local" };
|
return { configured: false, ok: false, backend: "local", error: s3Err(e) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await getClient().send(new HeadBucketCommand({ Bucket: config.s3Bucket }));
|
await getClient().send(
|
||||||
|
new ListObjectsV2Command({ Bucket: config.s3Bucket, MaxKeys: 1 }),
|
||||||
|
);
|
||||||
return { configured: true, ok: true, backend: "s3" };
|
return { configured: true, ok: true, backend: "s3" };
|
||||||
} catch {
|
} catch (listErr) {
|
||||||
return { configured: true, ok: false, backend: "s3" };
|
try {
|
||||||
|
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 };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:** faltan `S3_*` o HeadBucket falla (token Account API, Object Read & Write, bucket correcto, `S3_REGION=auto`).
|
- **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.
|
||||||
- **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`…
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue