Checkpoint: Ajout du secours de stockage réservé aux PDF de paie chiffrés : lorsque Forge est indisponible en recette, les octets AES-256-GCM sont stockés dans un volume hôte Docker persistant, jamais dans l'image ni la base. Import, réindexation et consultation passent par cette même abstraction, avec test de round-trip sans Forge. Compose transmet la clé AES et monte le volume dédié. Validation complète : 56 tests, TypeScript, build et format YAML.
All checks were successful
Validation applicative / TypeScript, tests et build (push) Successful in 2m9s
All checks were successful
Validation applicative / TypeScript, tests et build (push) Successful in 2m9s
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
// Uploads via Forge Server presigned URL to S3 (PUT direct).
|
||||
// Downloads return /manus-storage/{key} paths served via 307 redirect.
|
||||
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, relative, resolve } from "node:path";
|
||||
import { ENV } from "./_core/env";
|
||||
|
||||
function getForgeConfig() {
|
||||
@@ -107,3 +109,43 @@ export async function storageGetBuffer(relKey: string): Promise<Buffer> {
|
||||
if (!response.ok) throw new Error(`Storage download failed (${response.status})`);
|
||||
return Buffer.from(await response.arrayBuffer());
|
||||
}
|
||||
|
||||
/**
|
||||
* Répertoire persistant optionnel réservé aux PDF de paie déjà chiffrés.
|
||||
* Il n'est utilisé que sur les environnements sans configuration Forge, où le
|
||||
* volume Docker hôte survit aux reconstructions de l'image applicative.
|
||||
*/
|
||||
function getPayrollStoragePath(relKey: string): string | null {
|
||||
const configuredDirectory = process.env.PAYROLL_STORAGE_DIR?.trim();
|
||||
if (!configuredDirectory) return null;
|
||||
|
||||
const root = resolve(configuredDirectory);
|
||||
const destination = resolve(root, normalizeKey(relKey));
|
||||
const pathFromRoot = relative(root, destination);
|
||||
if (pathFromRoot.startsWith("..") || pathFromRoot === "") {
|
||||
throw new Error("Invalid payroll storage key");
|
||||
}
|
||||
return destination;
|
||||
}
|
||||
|
||||
export async function payrollStoragePut(
|
||||
relKey: string,
|
||||
data: Buffer | Uint8Array | string,
|
||||
contentType = "application/octet-stream",
|
||||
): Promise<{ key: string; url: string }> {
|
||||
const localKey = appendHashSuffix(normalizeKey(relKey));
|
||||
const localDestination = getPayrollStoragePath(localKey);
|
||||
if (!localDestination) return storagePut(relKey, data, contentType);
|
||||
|
||||
await mkdir(dirname(localDestination), { recursive: true, mode: 0o700 });
|
||||
await writeFile(localDestination, data, { mode: 0o600, flag: "wx" });
|
||||
// Les appels paie n'exposent jamais cette URL : la lecture passe exclusivement
|
||||
// par la route administrateur qui déchiffre côté serveur.
|
||||
return { key: localKey, url: `/manus-storage/${localKey}` };
|
||||
}
|
||||
|
||||
export async function payrollStorageGetBuffer(relKey: string): Promise<Buffer> {
|
||||
const localDestination = getPayrollStoragePath(relKey);
|
||||
if (!localDestination) return storageGetBuffer(relKey);
|
||||
return readFile(localDestination);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user