mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
feat: add server-side logging to backup upload inspection
This commit is contained in:
@@ -2,16 +2,17 @@ import { Readable } from "node:stream";
|
|||||||
import { createGunzip } from "node:zlib";
|
import { createGunzip } from "node:zlib";
|
||||||
import * as tar from "tar-stream";
|
import * as tar from "tar-stream";
|
||||||
import { getArchiveEntryMatcher, getFileExtension } from "@/utils/common";
|
import { getArchiveEntryMatcher, getFileExtension } from "@/utils/common";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
|
|
||||||
|
const log = logger.child({ module: "features/database/archive-inspect" });
|
||||||
|
|
||||||
export type UploadKind = "raw" | "targz";
|
export type UploadKind = "raw" | "targz";
|
||||||
|
|
||||||
export type InspectResult = {
|
export type InspectResult = {
|
||||||
kind: UploadKind;
|
kind: UploadKind;
|
||||||
/** Extension to use when naming the stored file, incl. leading dot. */
|
|
||||||
storeExtension: string;
|
storeExtension: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Thrown when a real tarball does not hold a valid backup for the dbms. */
|
|
||||||
export class InvalidUploadError extends Error {
|
export class InvalidUploadError extends Error {
|
||||||
constructor(message: string) {
|
constructor(message: string) {
|
||||||
super(message);
|
super(message);
|
||||||
@@ -30,11 +31,6 @@ function isGzip(buffer: Buffer): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Streams the gzip buffer through a tar parser and returns the entry names.
|
|
||||||
* File data is drained without buffering, so memory stays flat regardless of
|
|
||||||
* archive size. Rejects if the stream is not a valid gzip-of-tar.
|
|
||||||
*/
|
|
||||||
function listTarEntries(buffer: Buffer): Promise<string[]> {
|
function listTarEntries(buffer: Buffer): Promise<string[]> {
|
||||||
return new Promise<string[]>((resolve, reject) => {
|
return new Promise<string[]>((resolve, reject) => {
|
||||||
const names: string[] = [];
|
const names: string[] = [];
|
||||||
@@ -56,43 +52,49 @@ function listTarEntries(buffer: Buffer): Promise<string[]> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines how an uploaded backup file should be treated.
|
|
||||||
*
|
|
||||||
* - Not gzip → raw dump, stored with the dbms's normal extension (unchanged
|
|
||||||
* behavior).
|
|
||||||
* - Gzip that is a valid tar → must contain an entry matching the dbms;
|
|
||||||
* otherwise InvalidUploadError. Stored as `.tar.gz`.
|
|
||||||
* - Gzip that is NOT a tar (e.g. mongodump `--archive --gzip`) → treated as a
|
|
||||||
* raw upload, preserving today's behavior. No regression.
|
|
||||||
*/
|
|
||||||
export async function inspectUpload(
|
export async function inspectUpload(
|
||||||
buffer: Buffer,
|
buffer: Buffer,
|
||||||
dbms: string,
|
dbms: string,
|
||||||
): Promise<InspectResult> {
|
): Promise<InspectResult> {
|
||||||
|
log.info({ dbms, size: buffer.length }, "Inspecting uploaded backup");
|
||||||
|
|
||||||
if (!isGzip(buffer)) {
|
if (!isGzip(buffer)) {
|
||||||
|
log.info({ dbms }, "Upload is not gzip, treating as raw dump");
|
||||||
return { kind: "raw", storeExtension: getFileExtension(dbms) };
|
return { kind: "raw", storeExtension: getFileExtension(dbms) };
|
||||||
}
|
}
|
||||||
|
|
||||||
let entries: string[];
|
let entries: string[];
|
||||||
try {
|
try {
|
||||||
entries = await listTarEntries(buffer);
|
entries = await listTarEntries(buffer);
|
||||||
} catch {
|
} catch (error) {
|
||||||
// Gzip but not a tar → fall back to raw (unchanged behavior).
|
// Gzip but not a tar → fall back to raw (unchanged behavior).
|
||||||
|
log.info(
|
||||||
|
{ dbms, error: error instanceof Error ? error.message : "unknown" },
|
||||||
|
"Gzip is not a tar archive, treating as raw dump",
|
||||||
|
);
|
||||||
return { kind: "raw", storeExtension: getFileExtension(dbms) };
|
return { kind: "raw", storeExtension: getFileExtension(dbms) };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entries.length === 0) {
|
if (entries.length === 0) {
|
||||||
|
log.info({ dbms }, "Tar archive has no entries, treating as raw dump");
|
||||||
return { kind: "raw", storeExtension: getFileExtension(dbms) };
|
return { kind: "raw", storeExtension: getFileExtension(dbms) };
|
||||||
}
|
}
|
||||||
|
|
||||||
const matcher = getArchiveEntryMatcher(dbms);
|
const matcher = getArchiveEntryMatcher(dbms);
|
||||||
const hasValidEntry = entries.some((name) => matcher.test(name));
|
const hasValidEntry = entries.some((name) => matcher.test(name));
|
||||||
if (!hasValidEntry) {
|
if (!hasValidEntry) {
|
||||||
|
log.warn(
|
||||||
|
{ dbms, entries },
|
||||||
|
"Tar archive does not contain a valid backup for dbms",
|
||||||
|
);
|
||||||
throw new InvalidUploadError(
|
throw new InvalidUploadError(
|
||||||
`Archive does not contain a valid ${dbms} backup.`,
|
`Archive does not contain a valid ${dbms} backup.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
{ dbms, entryCount: entries.length },
|
||||||
|
"Valid tar.gz backup detected",
|
||||||
|
);
|
||||||
return { kind: "targz", storeExtension: ".tar.gz" };
|
return { kind: "targz", storeExtension: ".tar.gz" };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user