Some checks failed
Aduanasoft/PANEL_BASES_ANEXO24/pipeline/head There was a failure building this commit
integraciones de panel de respaldos fallidos, asignacion masiva de restaurador, trackeo de baks para nuevo sistema de cloudrestore Reviewed-on: #13 Co-authored-by: hreyes <hreyes@aduanasoft.com.mx> Co-committed-by: hreyes <hreyes@aduanasoft.com.mx>
104 lines
3.6 KiB
TypeScript
104 lines
3.6 KiB
TypeScript
/**
|
|
* Descarga de respaldos por restaurador, con ruta RELATIVA y SIN rutas fijas.
|
|
*
|
|
* La base de cada restaurador se DERIVA al vuelo de su Entrada reportada
|
|
* (`cloudrestore_status.input_folder`): hermana `Procesados`/`Fallados` (layout por defecto del
|
|
* install). El cliente solo envía `target` + `kind` + `relPath`; el servidor arma `base + relPath`
|
|
* y lo sirve leyendo la carpeta por filesystem (local o montada como share). CloudRestoreAS no
|
|
* reporta ni configura nada nuevo.
|
|
*/
|
|
import fs from 'node:fs/promises';
|
|
import { createReadStream } from 'node:fs';
|
|
import { Readable } from 'node:stream';
|
|
import { deriveSiblingFolder } from './backup-files';
|
|
import { getRestoreTargetForDownload, type RestoreTargetDownload } from './controldesk-pg';
|
|
|
|
export type BackupKind = 'procesados' | 'fallados';
|
|
|
|
export class BackupDownloadError extends Error {
|
|
constructor(
|
|
public status: number,
|
|
message: string
|
|
) {
|
|
super(message);
|
|
this.name = 'BackupDownloadError';
|
|
}
|
|
}
|
|
|
|
function baseSeparator(p: string): '\\' | '/' {
|
|
return p.includes('\\') ? '\\' : '/';
|
|
}
|
|
|
|
/** Carpeta base (procesados/fallados) del restaurador, SIEMPRE derivada de su Entrada. */
|
|
function resolveBaseFolder(t: RestoreTargetDownload, kind: BackupKind): string | null {
|
|
const input = t.input_folder;
|
|
if (!input) return null;
|
|
return deriveSiblingFolder(input, kind === 'procesados' ? 'Procesados' : 'Fallados');
|
|
}
|
|
|
|
/** Valida la ruta relativa: sin '..', sin absoluto, sin componentes vacíos. */
|
|
export function sanitizeRelPath(relPath: string): string | null {
|
|
const rel = String(relPath ?? '')
|
|
.replace(/\\/g, '/')
|
|
.trim();
|
|
if (!rel) return null;
|
|
if (rel.startsWith('/') || /^[a-zA-Z]:/.test(rel)) return null; // absoluto
|
|
const parts = rel.split('/');
|
|
if (parts.some((p) => p === '' || p === '.' || p === '..')) return null;
|
|
return parts.join('/');
|
|
}
|
|
|
|
function localJoin(base: string, rel: string): string {
|
|
const sep = baseSeparator(base);
|
|
const b = base.replace(/[\\/]+$/, '');
|
|
const relNative = rel.split('/').join(sep);
|
|
return `${b}${sep}${relNative}`;
|
|
}
|
|
|
|
export interface BackupDownload {
|
|
body: ReadableStream<Uint8Array>;
|
|
size: number | null;
|
|
filename: string;
|
|
cleanup: () => void;
|
|
}
|
|
|
|
/**
|
|
* Abre el stream de descarga de un respaldo desde la carpeta de procesados/fallados del
|
|
* restaurador (por filesystem). Lanza BackupDownloadError con el código HTTP apropiado.
|
|
*/
|
|
export async function openBackupDownload(
|
|
targetId: number,
|
|
kind: BackupKind,
|
|
relPathRaw: string
|
|
): Promise<BackupDownload> {
|
|
const rel = sanitizeRelPath(relPathRaw);
|
|
if (!rel) throw new BackupDownloadError(400, 'Ruta de archivo inválida');
|
|
|
|
const target = await getRestoreTargetForDownload(targetId);
|
|
if (!target) throw new BackupDownloadError(404, 'Restaurador no encontrado');
|
|
|
|
const base = resolveBaseFolder(target, kind);
|
|
if (!base) throw new BackupDownloadError(409, 'El restaurador aún no reportó su carpeta de entrada');
|
|
|
|
const filename = rel.split('/').pop() as string;
|
|
const localPath = localJoin(base, rel);
|
|
|
|
let st;
|
|
try {
|
|
st = await fs.stat(localPath);
|
|
} catch {
|
|
throw new BackupDownloadError(404, 'Archivo de respaldo no encontrado o carpeta inaccesible');
|
|
}
|
|
if (!st.isFile()) {
|
|
throw new BackupDownloadError(404, 'La ruta indicada no es un archivo');
|
|
}
|
|
|
|
const node = createReadStream(localPath, { highWaterMark: 1024 * 1024 });
|
|
return {
|
|
body: Readable.toWeb(node) as unknown as ReadableStream<Uint8Array>,
|
|
size: st.size,
|
|
filename,
|
|
cleanup: () => node.destroy()
|
|
};
|
|
}
|