@trieb.work/payload-plugin-backup-mongodb 0.1.7 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -62
- package/dist/backup-dashboard.css +18 -1
- package/dist/collections/BackupTasks.js +8 -1
- package/dist/collections/BackupTasks.js.map +1 -1
- package/dist/components/BackupDashboard/BackupSettingsModal.client.js +203 -115
- package/dist/components/BackupDashboard/BackupSettingsModal.client.js.map +1 -1
- package/dist/components/BackupDashboard/RestoreBackupDialog.client.js +1 -1
- package/dist/components/BackupDashboard/RestoreBackupDialog.client.js.map +1 -1
- package/dist/components/BackupDashboard/TaskActionButton.client.d.ts +1 -1
- package/dist/components/BackupDashboard/TaskActionButton.client.js +59 -26
- package/dist/components/BackupDashboard/TaskActionButton.client.js.map +1 -1
- package/dist/components/BackupDashboard/backupDashboardInlineCss.js +1 -1
- package/dist/components/BackupDashboard/backupDashboardInlineCss.js.map +1 -1
- package/dist/components/BackupDashboard/index.js +27 -3
- package/dist/components/BackupDashboard/index.js.map +1 -1
- package/dist/core/backup.d.ts +4 -3
- package/dist/core/backup.js +50 -43
- package/dist/core/backup.js.map +1 -1
- package/dist/core/backupArchiveRead.d.ts +15 -0
- package/dist/core/backupArchiveRead.js +30 -0
- package/dist/core/backupArchiveRead.js.map +1 -0
- package/dist/core/backupSettings.js +5 -0
- package/dist/core/backupSettings.js.map +1 -1
- package/dist/core/restore.d.ts +5 -2
- package/dist/core/restore.js +133 -89
- package/dist/core/restore.js.map +1 -1
- package/dist/core/restorePreview.d.ts +12 -3
- package/dist/core/restorePreview.js +17 -13
- package/dist/core/restorePreview.js.map +1 -1
- package/dist/core/restoreResult.d.ts +34 -0
- package/dist/core/restoreResult.js +45 -0
- package/dist/core/restoreResult.js.map +1 -0
- package/dist/core/storage/config.d.ts +31 -0
- package/dist/core/storage/config.js +60 -0
- package/dist/core/storage/config.js.map +1 -0
- package/dist/core/storage/index.d.ts +23 -0
- package/dist/core/storage/index.js +25 -0
- package/dist/core/storage/index.js.map +1 -0
- package/dist/core/storage/s3.d.ts +35 -0
- package/dist/core/storage/s3.js +200 -0
- package/dist/core/storage/s3.js.map +1 -0
- package/dist/core/storage/types.d.ts +60 -0
- package/dist/core/storage/types.js +6 -0
- package/dist/core/storage/types.js.map +1 -0
- package/dist/core/storage/vercelBlob.d.ts +25 -0
- package/dist/core/storage/vercelBlob.js +53 -0
- package/dist/core/storage/vercelBlob.js.map +1 -0
- package/dist/core/taskProgress.d.ts +10 -4
- package/dist/core/taskProgress.js +11 -2
- package/dist/core/taskProgress.js.map +1 -1
- package/dist/endpoints/index.js +0 -5
- package/dist/endpoints/index.js.map +1 -1
- package/dist/endpoints/paths/admin-backup-download.js +8 -7
- package/dist/endpoints/paths/admin-backup-download.js.map +1 -1
- package/dist/endpoints/paths/admin-delete.js +11 -5
- package/dist/endpoints/paths/admin-delete.js.map +1 -1
- package/dist/endpoints/paths/admin-manual.js +2 -1
- package/dist/endpoints/paths/admin-manual.js.map +1 -1
- package/dist/endpoints/paths/admin-preview.js +9 -2
- package/dist/endpoints/paths/admin-preview.js.map +1 -1
- package/dist/endpoints/paths/admin-restore.js +7 -4
- package/dist/endpoints/paths/admin-restore.js.map +1 -1
- package/dist/endpoints/paths/admin-settings.js +35 -2
- package/dist/endpoints/paths/admin-settings.js.map +1 -1
- package/dist/endpoints/paths/cron-list.js +2 -1
- package/dist/endpoints/paths/cron-list.js.map +1 -1
- package/dist/endpoints/paths/cron-restore.js +14 -4
- package/dist/endpoints/paths/cron-restore.js.map +1 -1
- package/dist/endpoints/paths/cron-run.js +2 -1
- package/dist/endpoints/paths/cron-run.js.map +1 -1
- package/dist/endpoints/shared.d.ts +0 -1
- package/dist/endpoints/shared.js +0 -6
- package/dist/endpoints/shared.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/publicApiPaths.d.ts +0 -1
- package/dist/publicApiPaths.js +0 -1
- package/dist/publicApiPaths.js.map +1 -1
- package/dist/types.d.ts +0 -5
- package/dist/types.js.map +1 -1
- package/package.json +22 -4
- package/dist/endpoints/paths/admin-seed.d.ts +0 -3
- package/dist/endpoints/paths/admin-seed.js +0 -47
- package/dist/endpoints/paths/admin-seed.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/BackupDashboard/index.tsx"],"sourcesContent":["import type { I18n } from '@payloadcms/translations'\nimport type { Payload } from 'payload'\n\nimport type { BackupPluginOptions } from '../../types'\n\nimport { listBackups, resolveBackupListToken } from '../../core/backup'\nimport {\n getBackupSortTimeMs,\n getCurrentDbName,\n getCurrentHostname,\n isUserAllowedByEnvRoles,\n transformBlobName,\n} from '../../utils/index'\nimport { backupDashboardInlineCss } from './backupDashboardInlineCss'\nimport { BackupListCollapsible, BackupSettingsModal, ManualBackupDialog } from './index.client'\n\ninterface BackupDashboardProps {\n i18n: I18n\n /**\n * Payload instance injected by Payload's admin `RenderServerComponent` as a server prop\n * for `afterDashboard` components. Avoids importing `@payload-config` from the plugin,\n * which does not resolve when the plugin runs from `node_modules`.\n */\n payload?: Payload\n /** Optional on admin server props; omitted does not mean logged out (see `defaultIsHidden`). */\n user?: null | Record<string, unknown>\n}\n\nfunction defaultIsHidden(\n user: null | Record<string, unknown> | undefined,\n access?: BackupPluginOptions['access'],\n): boolean {\n if (user === null) {\n return true\n }\n if (user === undefined) {\n if (access) {\n return true\n }\n return false\n }\n if (access) {\n return !access(user)\n }\n // Role allow-list via `PAYLOAD_BACKUP_ALLOWED_ROLES`. When unset, falls back to the\n // historical default (admin role, or \"visible\" when the project has no roles field).\n return !isUserAllowedByEnvRoles(user)\n}\n\nexport const BackupDashboard: React.FC<BackupDashboardProps> = async ({ i18n, payload, user }) => {\n if (defaultIsHidden(user)) {\n return null\n }\n\n const hasMongoConnection =\n Boolean(process.env.MONGODB_URI?.trim()) || Boolean(process.env.DATABASE_URL?.trim())\n if (!hasMongoConnection) {\n return (\n <>\n <style dangerouslySetInnerHTML={{ __html: backupDashboardInlineCss }} />\n <div className=\"backup-dashboard\">\n <h2>\n Backups <span className=\"experimental\">(experimental)</span>\n </h2>\n <p className=\"backup-dashboard__setup-hint\" role=\"status\">\n Set <code className=\"backup-dashboard__setup-hint-code\">MONGODB_URI</code> or{' '}\n <code className=\"backup-dashboard__setup-hint-code\">DATABASE_URL</code> so Payload can\n connect to MongoDB. The in-memory dev database sets both automatically when neither is\n provided.\n </p>\n </div>\n </>\n )\n }\n\n if (!payload) {\n // Should not happen in a normal Payload admin render; guard for safety.\n return (\n <>\n <style dangerouslySetInnerHTML={{ __html: backupDashboardInlineCss }} />\n <div className=\"backup-dashboard\">\n <h2>\n Backups <span className=\"experimental\">(experimental)</span>\n </h2>\n <p className=\"backup-dashboard__setup-hint\" role=\"status\">\n Backup dashboard could not initialise: Payload instance was not provided by the admin\n server props.\n </p>\n </div>\n </>\n )\n }\n\n const backupBlobToken = await resolveBackupListToken(payload)\n const hasBlobToken = backupBlobToken.trim().length > 0\n\n const blobs = hasBlobToken ? await listBackups(payload, { blobToken: backupBlobToken }) : []\n const sortedBlobs = [...blobs].sort((a, b) => {\n const ta = getBackupSortTimeMs(transformBlobName(a.pathname), new Date(a.uploadedAt))\n const tb = getBackupSortTimeMs(transformBlobName(b.pathname), new Date(b.uploadedAt))\n return tb - ta\n })\n const currentHostname = getCurrentHostname()\n const currentDbName = getCurrentDbName()\n\n const countOtherDb = sortedBlobs.filter((blob) => {\n const { dbName } = transformBlobName(blob.pathname)\n return currentDbName !== dbName\n }).length\n\n const countOtherHostname = sortedBlobs.filter((blob) => {\n const { hostname } = transformBlobName(blob.pathname)\n return currentHostname !== hostname\n }).length\n const lastBackup = sortedBlobs[0]\n\n return (\n <>\n <style\n dangerouslySetInnerHTML={{ __html: backupDashboardInlineCss }}\n data-payload-backup-mongodb=\"1\"\n />\n <div className=\"backup-dashboard\">\n <h2>\n Backups <span className=\"experimental\">(experimental)</span>\n </h2>\n\n {!hasBlobToken && (\n <p className=\"backup-dashboard__setup-hint\" role=\"status\">\n Add a Vercel Blob read/write token: set environment variable{' '}\n <code className=\"backup-dashboard__setup-hint-code\">BLOB_READ_WRITE_TOKEN</code>, or\n open <strong>Backup settings</strong> and paste a token. Until then, the list stays\n empty and cron or manual backups cannot run.\n </p>\n )}\n\n <div className=\"backup-dashboard__toolbar\">\n <div className=\"backup-dashboard__toolbar-meta\">\n <span className=\"backup-dashboard__toolbar-pill\">\n <span className=\"backup-dashboard__toolbar-key\">Total</span>\n {sortedBlobs.length} backup{sortedBlobs.length === 1 ? '' : 's'}\n </span>\n <span className=\"backup-dashboard__toolbar-pill\">\n <span className=\"backup-dashboard__toolbar-key\">Last backup</span>\n {lastBackup ?\n new Date(\n getBackupSortTimeMs(\n transformBlobName(lastBackup.pathname),\n new Date(lastBackup.uploadedAt),\n ),\n ).toLocaleString(i18n?.language || 'en')\n : 'No backups yet'}\n </span>\n </div>\n <div className=\"make-backup-actions\">\n <BackupSettingsModal />\n <ManualBackupDialog />\n </div>\n </div>\n\n <BackupListCollapsible\n blobs={sortedBlobs.map((b) => ({\n downloadUrl: b.downloadUrl,\n pathname: b.pathname,\n size: b.size,\n uploadedAt: new Date(b.uploadedAt).toISOString(),\n url: b.url,\n }))}\n countOtherDb={countOtherDb}\n countOtherHostname={countOtherHostname}\n currentDbName={currentDbName}\n currentHostname={currentHostname}\n i18nLanguage={i18n?.language || 'en'}\n />\n </div>\n </>\n )\n}\n\nexport default BackupDashboard\n"],"names":["listBackups","resolveBackupListToken","getBackupSortTimeMs","getCurrentDbName","getCurrentHostname","isUserAllowedByEnvRoles","transformBlobName","backupDashboardInlineCss","BackupListCollapsible","BackupSettingsModal","ManualBackupDialog","defaultIsHidden","user","access","undefined","BackupDashboard","i18n","payload","hasMongoConnection","Boolean","process","env","MONGODB_URI","trim","DATABASE_URL","style","dangerouslySetInnerHTML","__html","div","className","h2","span","p","role","code","backupBlobToken","hasBlobToken","length","blobs","blobToken","sortedBlobs","sort","a","b","ta","pathname","Date","uploadedAt","tb","currentHostname","currentDbName","countOtherDb","filter","blob","dbName","countOtherHostname","hostname","lastBackup","data-payload-backup-mongodb","strong","toLocaleString","language","map","downloadUrl","size","toISOString","url","i18nLanguage"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/BackupDashboard/index.tsx"],"sourcesContent":["import type { I18n } from '@payloadcms/translations'\nimport type { Payload } from 'payload'\n\nimport type { BackupPluginOptions } from '../../types'\n\nimport { listBackups, resolveBackupListToken } from '../../core/backup'\nimport { getBackupStorageKind, isBackupStorageConfigured } from '../../core/storage'\nimport {\n getBackupSortTimeMs,\n getCurrentDbName,\n getCurrentHostname,\n isUserAllowedByEnvRoles,\n transformBlobName,\n} from '../../utils/index'\nimport { backupDashboardInlineCss } from './backupDashboardInlineCss'\nimport { BackupListCollapsible, BackupSettingsModal, ManualBackupDialog } from './index.client'\n\ninterface BackupDashboardProps {\n i18n: I18n\n /**\n * Payload instance injected by Payload's admin `RenderServerComponent` as a server prop\n * for `afterDashboard` components. Avoids importing `@payload-config` from the plugin,\n * which does not resolve when the plugin runs from `node_modules`.\n */\n payload?: Payload\n /** Optional on admin server props; omitted does not mean logged out (see `defaultIsHidden`). */\n user?: null | Record<string, unknown>\n}\n\nfunction defaultIsHidden(\n user: null | Record<string, unknown> | undefined,\n access?: BackupPluginOptions['access'],\n): boolean {\n if (user === null) {\n return true\n }\n if (user === undefined) {\n if (access) {\n return true\n }\n return false\n }\n if (access) {\n return !access(user)\n }\n // Role allow-list via `PAYLOAD_BACKUP_ALLOWED_ROLES`. When unset, falls back to the\n // historical default (admin role, or \"visible\" when the project has no roles field).\n return !isUserAllowedByEnvRoles(user)\n}\n\nexport const BackupDashboard: React.FC<BackupDashboardProps> = async ({ i18n, payload, user }) => {\n if (defaultIsHidden(user)) {\n return null\n }\n\n const hasMongoConnection =\n Boolean(process.env.MONGODB_URI?.trim()) || Boolean(process.env.DATABASE_URL?.trim())\n if (!hasMongoConnection) {\n return (\n <>\n <style dangerouslySetInnerHTML={{ __html: backupDashboardInlineCss }} />\n <div className=\"backup-dashboard\">\n <h2>\n Backups <span className=\"experimental\">(experimental)</span>\n </h2>\n <p className=\"backup-dashboard__setup-hint\" role=\"status\">\n Set <code className=\"backup-dashboard__setup-hint-code\">MONGODB_URI</code> or{' '}\n <code className=\"backup-dashboard__setup-hint-code\">DATABASE_URL</code> so Payload can\n connect to MongoDB. The in-memory dev database sets both automatically when neither is\n provided.\n </p>\n </div>\n </>\n )\n }\n\n if (!payload) {\n // Should not happen in a normal Payload admin render; guard for safety.\n return (\n <>\n <style dangerouslySetInnerHTML={{ __html: backupDashboardInlineCss }} />\n <div className=\"backup-dashboard\">\n <h2>\n Backups <span className=\"experimental\">(experimental)</span>\n </h2>\n <p className=\"backup-dashboard__setup-hint\" role=\"status\">\n Backup dashboard could not initialise: Payload instance was not provided by the admin\n server props.\n </p>\n </div>\n </>\n )\n }\n\n const storageKind = getBackupStorageKind()\n const backupBlobToken = await resolveBackupListToken(payload)\n const storageConfigured = isBackupStorageConfigured(backupBlobToken)\n\n const blobs = storageConfigured ? await listBackups(payload, { blobToken: backupBlobToken }) : []\n const sortedBlobs = [...blobs].sort((a, b) => {\n const ta = getBackupSortTimeMs(transformBlobName(a.pathname), new Date(a.uploadedAt))\n const tb = getBackupSortTimeMs(transformBlobName(b.pathname), new Date(b.uploadedAt))\n return tb - ta\n })\n const currentHostname = getCurrentHostname()\n const currentDbName = getCurrentDbName()\n\n const countOtherDb = sortedBlobs.filter((blob) => {\n const { dbName } = transformBlobName(blob.pathname)\n return currentDbName !== dbName\n }).length\n\n const countOtherHostname = sortedBlobs.filter((blob) => {\n const { hostname } = transformBlobName(blob.pathname)\n return currentHostname !== hostname\n }).length\n const lastBackup = sortedBlobs[0]\n\n return (\n <>\n <style\n dangerouslySetInnerHTML={{ __html: backupDashboardInlineCss }}\n data-payload-backup-mongodb=\"1\"\n />\n <div className=\"backup-dashboard\">\n <h2>\n Backups <span className=\"experimental\">(experimental)</span>\n </h2>\n\n {!storageConfigured && storageKind === 's3' && (\n <p className=\"backup-dashboard__setup-hint\" role=\"status\">\n Set <code className=\"backup-dashboard__setup-hint-code\">BACKUP_S3_BUCKET</code> (and\n credentials) to enable the S3 backup target. Until then, the list stays empty and cron\n or manual backups cannot run.\n </p>\n )}\n {!storageConfigured && storageKind === 'vercel-blob' && (\n <p className=\"backup-dashboard__setup-hint\" role=\"status\">\n Add a Vercel Blob read/write token: set environment variable{' '}\n <code className=\"backup-dashboard__setup-hint-code\">BLOB_READ_WRITE_TOKEN</code>, or\n open <strong>Backup settings</strong> and paste a token. Until then, the list stays\n empty and cron or manual backups cannot run.\n </p>\n )}\n\n <div className=\"backup-dashboard__toolbar\">\n <div className=\"backup-dashboard__toolbar-meta\">\n <span className=\"backup-dashboard__toolbar-pill\">\n <span className=\"backup-dashboard__toolbar-key\">Storage</span>\n {storageKind === 's3' ? 'AWS S3' : 'Vercel Blob'}\n </span>\n <span className=\"backup-dashboard__toolbar-pill\">\n <span className=\"backup-dashboard__toolbar-key\">Total</span>\n {sortedBlobs.length} backup{sortedBlobs.length === 1 ? '' : 's'}\n </span>\n <span className=\"backup-dashboard__toolbar-pill\">\n <span className=\"backup-dashboard__toolbar-key\">Last backup</span>\n {lastBackup ?\n new Date(\n getBackupSortTimeMs(\n transformBlobName(lastBackup.pathname),\n new Date(lastBackup.uploadedAt),\n ),\n ).toLocaleString(i18n?.language || 'en')\n : 'No backups yet'}\n </span>\n </div>\n <div className=\"make-backup-actions\">\n <BackupSettingsModal />\n <ManualBackupDialog />\n </div>\n </div>\n\n <BackupListCollapsible\n blobs={sortedBlobs.map((b) => ({\n downloadUrl: b.downloadUrl,\n pathname: b.pathname,\n size: b.size,\n uploadedAt: new Date(b.uploadedAt).toISOString(),\n url: b.url,\n }))}\n countOtherDb={countOtherDb}\n countOtherHostname={countOtherHostname}\n currentDbName={currentDbName}\n currentHostname={currentHostname}\n i18nLanguage={i18n?.language || 'en'}\n />\n </div>\n </>\n )\n}\n\nexport default BackupDashboard\n"],"names":["listBackups","resolveBackupListToken","getBackupStorageKind","isBackupStorageConfigured","getBackupSortTimeMs","getCurrentDbName","getCurrentHostname","isUserAllowedByEnvRoles","transformBlobName","backupDashboardInlineCss","BackupListCollapsible","BackupSettingsModal","ManualBackupDialog","defaultIsHidden","user","access","undefined","BackupDashboard","i18n","payload","hasMongoConnection","Boolean","process","env","MONGODB_URI","trim","DATABASE_URL","style","dangerouslySetInnerHTML","__html","div","className","h2","span","p","role","code","storageKind","backupBlobToken","storageConfigured","blobs","blobToken","sortedBlobs","sort","a","b","ta","pathname","Date","uploadedAt","tb","currentHostname","currentDbName","countOtherDb","filter","blob","dbName","length","countOtherHostname","hostname","lastBackup","data-payload-backup-mongodb","strong","toLocaleString","language","map","downloadUrl","size","toISOString","url","i18nLanguage"],"mappings":"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"}
|
package/dist/core/backup.d.ts
CHANGED
|
@@ -13,10 +13,11 @@ export declare function listBackups(payload: Payload, options?: {
|
|
|
13
13
|
* When omitted, uses {@link resolveBackupListToken} (settings + env), same as other backup APIs.
|
|
14
14
|
*/
|
|
15
15
|
blobToken?: string;
|
|
16
|
-
}): Promise<import("
|
|
16
|
+
}): Promise<import("./storage").BackupStorageObject[]>;
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* Builds a tar.gz archive containing the collection dump plus Payload upload-store media files.
|
|
19
|
+
* Media always comes from the Vercel Blob upload store (via token/env), independent of the
|
|
20
|
+
* configured backup archive target (S3 vs Vercel Blob).
|
|
20
21
|
*/
|
|
21
22
|
export declare function createMediaBackupFile(collectionBackupFile: string, mediaCollection: {
|
|
22
23
|
filename: string;
|
package/dist/core/backup.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { list } from '@vercel/blob';
|
|
2
2
|
import { EJSON } from 'bson';
|
|
3
3
|
import { createBlobName, getCurrentDbName, getCurrentHostname, sanitizeBackupLabel } from '../utils/index';
|
|
4
4
|
import { createTarGzip } from './archive';
|
|
5
|
-
import {
|
|
5
|
+
import { readBackupBlobContentFlexible } from './backupBlobIO';
|
|
6
6
|
import { getResolvedCronBackupSettings, resolveBackupBlobToken } from './backupSettings';
|
|
7
7
|
import { getDb } from './db';
|
|
8
|
+
import { getBackupStorageKind, resolveBackupStorage } from './storage';
|
|
8
9
|
import { updateBackupTask } from './taskProgress';
|
|
9
10
|
export const COLLECTION_FILE_NAME = 'collections.json';
|
|
10
11
|
/**
|
|
@@ -19,16 +20,20 @@ export const COLLECTION_FILE_NAME = 'collections.json';
|
|
|
19
20
|
return resolveBackupBlobToken(await getResolvedCronBackupSettings(payload)).trim();
|
|
20
21
|
}
|
|
21
22
|
export async function listBackups(payload, options = {}) {
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
24
|
-
|
|
23
|
+
const kind = getBackupStorageKind();
|
|
24
|
+
if (kind === 'vercel-blob') {
|
|
25
|
+
const token = await resolveBackupListToken(payload, options.blobToken);
|
|
26
|
+
if (!token) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
return resolveBackupStorage({
|
|
30
|
+
blobToken: token,
|
|
31
|
+
kind
|
|
32
|
+
}).list('backups/');
|
|
25
33
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
token
|
|
30
|
-
});
|
|
31
|
-
return blobs;
|
|
34
|
+
return resolveBackupStorage({
|
|
35
|
+
kind
|
|
36
|
+
}).list('backups/');
|
|
32
37
|
}
|
|
33
38
|
function resolveBlobToken(blobToken) {
|
|
34
39
|
if (blobToken && blobToken.trim().length > 0) {
|
|
@@ -37,29 +42,38 @@ function resolveBlobToken(blobToken) {
|
|
|
37
42
|
return undefined;
|
|
38
43
|
}
|
|
39
44
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
45
|
+
* Builds a tar.gz archive containing the collection dump plus Payload upload-store media files.
|
|
46
|
+
* Media always comes from the Vercel Blob upload store (via token/env), independent of the
|
|
47
|
+
* configured backup archive target (S3 vs Vercel Blob).
|
|
42
48
|
*/ export async function createMediaBackupFile(collectionBackupFile, mediaCollection, backupBlobToken, mediaListToken, payload) {
|
|
43
49
|
const envMedia = (process.env.BLOB_READ_WRITE_TOKEN || '').trim();
|
|
44
50
|
const tokenForMedia = resolveBlobToken(mediaListToken) ?? (envMedia.length > 0 ? envMedia : undefined) ?? resolveBlobToken(backupBlobToken);
|
|
45
51
|
const mediaFiles = await Promise.all(mediaCollection.map(async (media)=>{
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
try {
|
|
53
|
+
const matchingFiles = await list({
|
|
54
|
+
limit: 2,
|
|
55
|
+
prefix: media.filename,
|
|
56
|
+
token: tokenForMedia
|
|
57
|
+
});
|
|
58
|
+
const blob = matchingFiles.blobs.find((blob)=>blob.pathname === media.filename);
|
|
59
|
+
if (!blob) {
|
|
60
|
+
payload?.logger.warn({
|
|
61
|
+
filename: media.filename
|
|
62
|
+
}, '[backup] File was in collection but not in blob storage');
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
const content = await readBackupBlobContentFlexible(blob.pathname, blob.downloadUrl, tokenForMedia ?? '');
|
|
66
|
+
return {
|
|
67
|
+
name: media.filename,
|
|
68
|
+
content
|
|
69
|
+
};
|
|
70
|
+
} catch (err) {
|
|
53
71
|
payload?.logger.warn({
|
|
72
|
+
err,
|
|
54
73
|
filename: media.filename
|
|
55
|
-
}, '[backup]
|
|
74
|
+
}, '[backup] Failed to read media file from blob storage');
|
|
56
75
|
return undefined;
|
|
57
76
|
}
|
|
58
|
-
const content = await readBackupBlobContentFlexible(blob.pathname, blob.downloadUrl, tokenForMedia ?? '');
|
|
59
|
-
return {
|
|
60
|
-
name: media.filename,
|
|
61
|
-
content
|
|
62
|
-
};
|
|
63
77
|
}));
|
|
64
78
|
return await createTarGzip([
|
|
65
79
|
{
|
|
@@ -73,10 +87,13 @@ export async function createBackup(payload, options = {}) {
|
|
|
73
87
|
const { backupsToKeep, blobToken, cron = false, includeMedia = false, skipCollections, taskId } = options;
|
|
74
88
|
const label = cron ? '' : sanitizeBackupLabel(options.label);
|
|
75
89
|
const blobAccess = options.blobAccess ?? 'public';
|
|
76
|
-
const envMedia = (process.env.BLOB_READ_WRITE_TOKEN || '').trim();
|
|
77
90
|
const token = resolveBlobToken(blobToken);
|
|
78
91
|
const skip = new Set(skipCollections ?? []);
|
|
79
92
|
const resolvedBackupsToKeep = backupsToKeep ?? (Number(process.env.BACKUPS_TO_KEEP) || 10);
|
|
93
|
+
const storage = resolveBackupStorage({
|
|
94
|
+
blobAccess,
|
|
95
|
+
blobToken: token
|
|
96
|
+
});
|
|
80
97
|
const currentHostname = getCurrentHostname();
|
|
81
98
|
const currentDbName = getCurrentDbName();
|
|
82
99
|
const type = cron ? 'cron' : 'manual';
|
|
@@ -95,11 +112,7 @@ export async function createBackup(payload, options = {}) {
|
|
|
95
112
|
});
|
|
96
113
|
}
|
|
97
114
|
if (cron) {
|
|
98
|
-
const
|
|
99
|
-
limit: 1000,
|
|
100
|
-
prefix: 'backups/cron-',
|
|
101
|
-
token
|
|
102
|
-
});
|
|
115
|
+
const blobs = await storage.list('backups/cron-');
|
|
103
116
|
const sorted = blobs.sort((a, b)=>new Date(b.uploadedAt).getTime() - new Date(a.uploadedAt).getTime());
|
|
104
117
|
const oldest = sorted.slice(resolvedBackupsToKeep - 1);
|
|
105
118
|
if (oldest.length > 0) {
|
|
@@ -113,8 +126,9 @@ export async function createBackup(payload, options = {}) {
|
|
|
113
126
|
}
|
|
114
127
|
}
|
|
115
128
|
for (const blob of oldest){
|
|
116
|
-
await del(
|
|
117
|
-
|
|
129
|
+
await storage.del({
|
|
130
|
+
pathname: blob.pathname,
|
|
131
|
+
url: blob.url
|
|
118
132
|
});
|
|
119
133
|
payload.logger.info({
|
|
120
134
|
pathname: blob.pathname
|
|
@@ -161,7 +175,7 @@ export async function createBackup(payload, options = {}) {
|
|
|
161
175
|
});
|
|
162
176
|
}
|
|
163
177
|
}
|
|
164
|
-
const backupFile = includeMedia ? await createMediaBackupFile(collectionBackupFile, allData?.['media'] || [], token,
|
|
178
|
+
const backupFile = includeMedia ? await createMediaBackupFile(collectionBackupFile, allData?.['media'] || [], token, undefined, payload) : collectionBackupFile;
|
|
165
179
|
const name = `backups/${createBlobName(type, currentDbName, currentHostname, includedCollectionCount, backupTimestampMs, includeMedia ? 'tar.gz' : 'json', label || undefined)}`;
|
|
166
180
|
payload.logger.info({
|
|
167
181
|
name
|
|
@@ -171,14 +185,7 @@ export async function createBackup(payload, options = {}) {
|
|
|
171
185
|
message: 'Uploading backup to blob storage'
|
|
172
186
|
});
|
|
173
187
|
}
|
|
174
|
-
|
|
175
|
-
if (effectiveAccess !== blobAccess) {
|
|
176
|
-
payload.logger.warn({
|
|
177
|
-
name,
|
|
178
|
-
effectiveAccess,
|
|
179
|
-
preferredAccess: blobAccess
|
|
180
|
-
}, '[backup] Blob store rejected preferred access level; uploaded with fallback');
|
|
181
|
-
}
|
|
188
|
+
await storage.put(name, backupFile);
|
|
182
189
|
payload.logger.info({
|
|
183
190
|
name,
|
|
184
191
|
durationMs: Date.now() - t0
|
package/dist/core/backup.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/backup.ts"],"sourcesContent":["import type { Payload } from 'payload'\n\nimport { del, list } from '@vercel/blob'\nimport { EJSON } from 'bson'\n\nimport {\n createBlobName,\n getCurrentDbName,\n getCurrentHostname,\n sanitizeBackupLabel,\n} from '../utils/index'\nimport { createTarGzip } from './archive'\nimport {\n type BackupBlobAccessLevel,\n putBackupBlobContent,\n readBackupBlobContentFlexible,\n} from './backupBlobIO'\nimport { getResolvedCronBackupSettings, resolveBackupBlobToken } from './backupSettings'\nimport { getDb } from './db'\nimport { updateBackupTask } from './taskProgress'\n\nexport const COLLECTION_FILE_NAME = 'collections.json'\n\n/**\n * Resolves the Vercel Blob read/write token the same way as backup endpoints and\n * {@link createBackup} / {@link restoreBackup}: settings document override, else `BLOB_READ_WRITE_TOKEN`.\n * Pass `explicitToken` to skip reading settings (same as `options.blobToken` in those calls).\n */\nexport async function resolveBackupListToken(\n payload: Payload,\n explicitToken?: string,\n): Promise<string> {\n const t = explicitToken?.trim()\n if (t) {\n return t\n }\n return resolveBackupBlobToken(await getResolvedCronBackupSettings(payload)).trim()\n}\n\nexport async function listBackups(\n payload: Payload,\n options: {\n /**\n * When set, used as the Vercel Blob token (e.g. tests or a pre-resolved value).\n * When omitted, uses {@link resolveBackupListToken} (settings + env), same as other backup APIs.\n */\n blobToken?: string\n } = {},\n) {\n const token = await resolveBackupListToken(payload, options.blobToken)\n if (!token) {\n return []\n }\n const { blobs } = await list({\n limit: 1000,\n prefix: 'backups/',\n token,\n })\n return blobs\n}\n\nfunction resolveBlobToken(blobToken?: string): string | undefined {\n if (blobToken && blobToken.trim().length > 0) {\n return blobToken\n }\n return undefined\n}\n\n/**\n * @param mediaListToken Token for listing/fetching **Payload media** blobs (usually\n * `BLOB_READ_WRITE_TOKEN`). When omitted, uses env then falls back to `backupBlobToken`.\n */\nexport async function createMediaBackupFile(\n collectionBackupFile: string,\n mediaCollection: { filename: string }[],\n backupBlobToken?: string,\n mediaListToken?: string,\n payload?: Payload,\n): Promise<Buffer> {\n const envMedia = (process.env.BLOB_READ_WRITE_TOKEN || '').trim()\n const tokenForMedia =\n resolveBlobToken(mediaListToken) ??\n (envMedia.length > 0 ? envMedia : undefined) ??\n resolveBlobToken(backupBlobToken)\n const mediaFiles = await Promise.all(\n mediaCollection.map(async (media) => {\n const matchingFiles = await list({ limit: 2, prefix: media.filename, token: tokenForMedia })\n const blob = matchingFiles.blobs.find((blob) => blob.pathname === media.filename)\n if (!blob) {\n payload?.logger.warn(\n { filename: media.filename },\n '[backup] File was in collection but not in blob storage',\n )\n return undefined\n }\n const content = await readBackupBlobContentFlexible(\n blob.pathname,\n blob.downloadUrl,\n tokenForMedia ?? '',\n )\n return { name: media.filename, content }\n }),\n )\n return await createTarGzip([\n { name: COLLECTION_FILE_NAME, content: Buffer.from(collectionBackupFile) },\n ...(mediaFiles.filter(Boolean) as { content: Buffer; name: string }[]),\n ])\n}\n\nexport async function createBackup(\n payload: Payload,\n options: {\n backupsToKeep?: number\n /** Public (default) or private Vercel Blob access for the uploaded backup archive. */\n blobAccess?: BackupBlobAccessLevel\n /** Optional explicit Vercel Blob token (falls back to env inside @vercel/blob). */\n blobToken?: string\n cron?: boolean\n includeMedia?: boolean\n /**\n * Optional human-readable label for manual backups (appears in the backup list and is\n * searchable via the label filter). Ignored for cron backups. Whitespace is collapsed,\n * length is capped and the stored value is URL-encoded in the blob pathname.\n */\n label?: string\n /** Mongo collection names to omit from the dump (e.g. manual backup UI). */\n skipCollections?: string[]\n taskId?: string\n } = {},\n): Promise<void> {\n const {\n backupsToKeep,\n blobToken,\n cron = false,\n includeMedia = false,\n skipCollections,\n taskId,\n } = options\n const label = cron ? '' : sanitizeBackupLabel(options.label)\n const blobAccess: BackupBlobAccessLevel = options.blobAccess ?? 'public'\n const envMedia = (process.env.BLOB_READ_WRITE_TOKEN || '').trim()\n const token = resolveBlobToken(blobToken)\n const skip = new Set(skipCollections ?? [])\n const resolvedBackupsToKeep = backupsToKeep ?? (Number(process.env.BACKUPS_TO_KEEP) || 10)\n\n const currentHostname = getCurrentHostname()\n const currentDbName = getCurrentDbName()\n const type = cron ? 'cron' : 'manual'\n const t0 = Date.now()\n\n payload.logger.info(\n {\n type,\n blacklist: skipCollections?.length ?? 0,\n db: currentDbName,\n host: currentHostname,\n includeMedia,\n },\n '[backup] Starting backup',\n )\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: 'Starting backup',\n status: 'running',\n })\n }\n\n if (cron) {\n const { blobs } = await list({\n limit: 1000,\n prefix: 'backups/cron-',\n token,\n })\n const sorted = blobs.sort(\n (a, b) => new Date(b.uploadedAt).getTime() - new Date(a.uploadedAt).getTime(),\n )\n const oldest = sorted.slice(resolvedBackupsToKeep - 1)\n if (oldest.length > 0) {\n payload.logger.info({ count: oldest.length }, '[backup] Pruning old cron backups')\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: `Pruning ${oldest.length} old cron backup${oldest.length === 1 ? '' : 's'}`,\n })\n }\n }\n for (const blob of oldest) {\n await del(blob.url, { token })\n payload.logger.info({ pathname: blob.pathname }, '[backup] Deleted old backup')\n }\n }\n\n const db = getDb(payload)\n const collections = await db.listCollections().toArray()\n payload.logger.info({ collections: collections.length }, '[backup] Dumping collections')\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: `Dumping ${collections.length} collection${collections.length === 1 ? '' : 's'}`,\n })\n }\n\n const allData: Record<string, Record<string, unknown>[]> = {}\n for (const collection of collections) {\n if (skip.has(collection.name)) {\n payload.logger.debug(\n { collection: collection.name },\n '[backup] Skipping blacklisted collection',\n )\n continue\n }\n allData[collection.name] = (await db.collection(collection.name).find({}).toArray()) as Record<\n string,\n unknown\n >[]\n payload.logger.debug(\n { collection: collection.name, docs: allData[collection.name].length },\n '[backup] Collection dumped',\n )\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: `Dumped collection ${collection.name} (${allData[collection.name].length} docs)`,\n })\n }\n }\n\n const collectionBackupFile = EJSON.stringify(allData)\n const includedCollectionCount = Object.keys(allData).length\n const backupTimestampMs = Date.now()\n\n if (includeMedia) {\n payload.logger.info('[backup] Bundling media files into archive')\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: 'Bundling media files into archive',\n })\n }\n }\n\n const backupFile =\n includeMedia ?\n await createMediaBackupFile(\n collectionBackupFile,\n (allData?.['media'] as { filename: string }[] | undefined) || [],\n token,\n envMedia.length > 0 ? envMedia : undefined,\n payload,\n )\n : collectionBackupFile\n const name = `backups/${createBlobName(\n type,\n currentDbName,\n currentHostname,\n includedCollectionCount,\n backupTimestampMs,\n includeMedia ? 'tar.gz' : 'json',\n label || undefined,\n )}`\n\n payload.logger.info({ name }, '[backup] Uploading backup to blob storage')\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: 'Uploading backup to blob storage',\n })\n }\n const effectiveAccess = await putBackupBlobContent(name, backupFile, token, blobAccess)\n if (effectiveAccess !== blobAccess) {\n payload.logger.warn(\n { name, effectiveAccess, preferredAccess: blobAccess },\n '[backup] Blob store rejected preferred access level; uploaded with fallback',\n )\n }\n\n payload.logger.info({ name, durationMs: Date.now() - t0 }, '[backup] Backup complete')\n}\n"],"names":["del","list","EJSON","createBlobName","getCurrentDbName","getCurrentHostname","sanitizeBackupLabel","createTarGzip","putBackupBlobContent","readBackupBlobContentFlexible","getResolvedCronBackupSettings","resolveBackupBlobToken","getDb","updateBackupTask","COLLECTION_FILE_NAME","resolveBackupListToken","payload","explicitToken","t","trim","listBackups","options","token","blobToken","blobs","limit","prefix","resolveBlobToken","length","undefined","createMediaBackupFile","collectionBackupFile","mediaCollection","backupBlobToken","mediaListToken","envMedia","process","env","BLOB_READ_WRITE_TOKEN","tokenForMedia","mediaFiles","Promise","all","map","media","matchingFiles","filename","blob","find","pathname","logger","warn","content","downloadUrl","name","Buffer","from","filter","Boolean","createBackup","backupsToKeep","cron","includeMedia","skipCollections","taskId","label","blobAccess","skip","Set","resolvedBackupsToKeep","Number","BACKUPS_TO_KEEP","currentHostname","currentDbName","type","t0","Date","now","info","blacklist","db","host","message","status","sorted","sort","a","b","uploadedAt","getTime","oldest","slice","count","url","collections","listCollections","toArray","allData","collection","has","debug","docs","stringify","includedCollectionCount","Object","keys","backupTimestampMs","backupFile","effectiveAccess","preferredAccess","durationMs"],"mappings":"AAEA,SAASA,GAAG,EAAEC,IAAI,QAAQ,eAAc;AACxC,SAASC,KAAK,QAAQ,OAAM;AAE5B,SACEC,cAAc,EACdC,gBAAgB,EAChBC,kBAAkB,EAClBC,mBAAmB,QACd,iBAAgB;AACvB,SAASC,aAAa,QAAQ,YAAW;AACzC,SAEEC,oBAAoB,EACpBC,6BAA6B,QACxB,iBAAgB;AACvB,SAASC,6BAA6B,EAAEC,sBAAsB,QAAQ,mBAAkB;AACxF,SAASC,KAAK,QAAQ,OAAM;AAC5B,SAASC,gBAAgB,QAAQ,iBAAgB;AAEjD,OAAO,MAAMC,uBAAuB,mBAAkB;AAEtD;;;;CAIC,GACD,OAAO,eAAeC,uBACpBC,OAAgB,EAChBC,aAAsB;IAEtB,MAAMC,IAAID,eAAeE;IACzB,IAAID,GAAG;QACL,OAAOA;IACT;IACA,OAAOP,uBAAuB,MAAMD,8BAA8BM,UAAUG,IAAI;AAClF;AAEA,OAAO,eAAeC,YACpBJ,OAAgB,EAChBK,UAMI,CAAC,CAAC;IAEN,MAAMC,QAAQ,MAAMP,uBAAuBC,SAASK,QAAQE,SAAS;IACrE,IAAI,CAACD,OAAO;QACV,OAAO,EAAE;IACX;IACA,MAAM,EAAEE,KAAK,EAAE,GAAG,MAAMvB,KAAK;QAC3BwB,OAAO;QACPC,QAAQ;QACRJ;IACF;IACA,OAAOE;AACT;AAEA,SAASG,iBAAiBJ,SAAkB;IAC1C,IAAIA,aAAaA,UAAUJ,IAAI,GAAGS,MAAM,GAAG,GAAG;QAC5C,OAAOL;IACT;IACA,OAAOM;AACT;AAEA;;;CAGC,GACD,OAAO,eAAeC,sBACpBC,oBAA4B,EAC5BC,eAAuC,EACvCC,eAAwB,EACxBC,cAAuB,EACvBlB,OAAiB;IAEjB,MAAMmB,WAAW,AAACC,CAAAA,QAAQC,GAAG,CAACC,qBAAqB,IAAI,EAAC,EAAGnB,IAAI;IAC/D,MAAMoB,gBACJZ,iBAAiBO,mBAChBC,CAAAA,SAASP,MAAM,GAAG,IAAIO,WAAWN,SAAQ,KAC1CF,iBAAiBM;IACnB,MAAMO,aAAa,MAAMC,QAAQC,GAAG,CAClCV,gBAAgBW,GAAG,CAAC,OAAOC;QACzB,MAAMC,gBAAgB,MAAM5C,KAAK;YAAEwB,OAAO;YAAGC,QAAQkB,MAAME,QAAQ;YAAExB,OAAOiB;QAAc;QAC1F,MAAMQ,OAAOF,cAAcrB,KAAK,CAACwB,IAAI,CAAC,CAACD,OAASA,KAAKE,QAAQ,KAAKL,MAAME,QAAQ;QAChF,IAAI,CAACC,MAAM;YACT/B,SAASkC,OAAOC,KACd;gBAAEL,UAAUF,MAAME,QAAQ;YAAC,GAC3B;YAEF,OAAOjB;QACT;QACA,MAAMuB,UAAU,MAAM3C,8BACpBsC,KAAKE,QAAQ,EACbF,KAAKM,WAAW,EAChBd,iBAAiB;QAEnB,OAAO;YAAEe,MAAMV,MAAME,QAAQ;YAAEM;QAAQ;IACzC;IAEF,OAAO,MAAM7C,cAAc;QACzB;YAAE+C,MAAMxC;YAAsBsC,SAASG,OAAOC,IAAI,CAACzB;QAAsB;WACrES,WAAWiB,MAAM,CAACC;KACvB;AACH;AAEA,OAAO,eAAeC,aACpB3C,OAAgB,EAChBK,UAiBI,CAAC,CAAC;IAEN,MAAM,EACJuC,aAAa,EACbrC,SAAS,EACTsC,OAAO,KAAK,EACZC,eAAe,KAAK,EACpBC,eAAe,EACfC,MAAM,EACP,GAAG3C;IACJ,MAAM4C,QAAQJ,OAAO,KAAKvD,oBAAoBe,QAAQ4C,KAAK;IAC3D,MAAMC,aAAoC7C,QAAQ6C,UAAU,IAAI;IAChE,MAAM/B,WAAW,AAACC,CAAAA,QAAQC,GAAG,CAACC,qBAAqB,IAAI,EAAC,EAAGnB,IAAI;IAC/D,MAAMG,QAAQK,iBAAiBJ;IAC/B,MAAM4C,OAAO,IAAIC,IAAIL,mBAAmB,EAAE;IAC1C,MAAMM,wBAAwBT,iBAAkBU,CAAAA,OAAOlC,QAAQC,GAAG,CAACkC,eAAe,KAAK,EAAC;IAExF,MAAMC,kBAAkBnE;IACxB,MAAMoE,gBAAgBrE;IACtB,MAAMsE,OAAOb,OAAO,SAAS;IAC7B,MAAMc,KAAKC,KAAKC,GAAG;IAEnB7D,QAAQkC,MAAM,CAAC4B,IAAI,CACjB;QACEJ;QACAK,WAAWhB,iBAAiBnC,UAAU;QACtCoD,IAAIP;QACJQ,MAAMT;QACNV;IACF,GACA;IAEF,IAAIE,QAAQ;QACV,MAAMnD,iBAAiBG,SAASgD,QAAQ;YACtCkB,SAAS;YACTC,QAAQ;QACV;IACF;IAEA,IAAItB,MAAM;QACR,MAAM,EAAErC,KAAK,EAAE,GAAG,MAAMvB,KAAK;YAC3BwB,OAAO;YACPC,QAAQ;YACRJ;QACF;QACA,MAAM8D,SAAS5D,MAAM6D,IAAI,CACvB,CAACC,GAAGC,IAAM,IAAIX,KAAKW,EAAEC,UAAU,EAAEC,OAAO,KAAK,IAAIb,KAAKU,EAAEE,UAAU,EAAEC,OAAO;QAE7E,MAAMC,SAASN,OAAOO,KAAK,CAACtB,wBAAwB;QACpD,IAAIqB,OAAO9D,MAAM,GAAG,GAAG;YACrBZ,QAAQkC,MAAM,CAAC4B,IAAI,CAAC;gBAAEc,OAAOF,OAAO9D,MAAM;YAAC,GAAG;YAC9C,IAAIoC,QAAQ;gBACV,MAAMnD,iBAAiBG,SAASgD,QAAQ;oBACtCkB,SAAS,CAAC,QAAQ,EAAEQ,OAAO9D,MAAM,CAAC,gBAAgB,EAAE8D,OAAO9D,MAAM,KAAK,IAAI,KAAK,KAAK;gBACtF;YACF;QACF;QACA,KAAK,MAAMmB,QAAQ2C,OAAQ;YACzB,MAAM1F,IAAI+C,KAAK8C,GAAG,EAAE;gBAAEvE;YAAM;YAC5BN,QAAQkC,MAAM,CAAC4B,IAAI,CAAC;gBAAE7B,UAAUF,KAAKE,QAAQ;YAAC,GAAG;QACnD;IACF;IAEA,MAAM+B,KAAKpE,MAAMI;IACjB,MAAM8E,cAAc,MAAMd,GAAGe,eAAe,GAAGC,OAAO;IACtDhF,QAAQkC,MAAM,CAAC4B,IAAI,CAAC;QAAEgB,aAAaA,YAAYlE,MAAM;IAAC,GAAG;IACzD,IAAIoC,QAAQ;QACV,MAAMnD,iBAAiBG,SAASgD,QAAQ;YACtCkB,SAAS,CAAC,QAAQ,EAAEY,YAAYlE,MAAM,CAAC,WAAW,EAAEkE,YAAYlE,MAAM,KAAK,IAAI,KAAK,KAAK;QAC3F;IACF;IAEA,MAAMqE,UAAqD,CAAC;IAC5D,KAAK,MAAMC,cAAcJ,YAAa;QACpC,IAAI3B,KAAKgC,GAAG,CAACD,WAAW5C,IAAI,GAAG;YAC7BtC,QAAQkC,MAAM,CAACkD,KAAK,CAClB;gBAAEF,YAAYA,WAAW5C,IAAI;YAAC,GAC9B;YAEF;QACF;QACA2C,OAAO,CAACC,WAAW5C,IAAI,CAAC,GAAI,MAAM0B,GAAGkB,UAAU,CAACA,WAAW5C,IAAI,EAAEN,IAAI,CAAC,CAAC,GAAGgD,OAAO;QAIjFhF,QAAQkC,MAAM,CAACkD,KAAK,CAClB;YAAEF,YAAYA,WAAW5C,IAAI;YAAE+C,MAAMJ,OAAO,CAACC,WAAW5C,IAAI,CAAC,CAAC1B,MAAM;QAAC,GACrE;QAEF,IAAIoC,QAAQ;YACV,MAAMnD,iBAAiBG,SAASgD,QAAQ;gBACtCkB,SAAS,CAAC,kBAAkB,EAAEgB,WAAW5C,IAAI,CAAC,EAAE,EAAE2C,OAAO,CAACC,WAAW5C,IAAI,CAAC,CAAC1B,MAAM,CAAC,MAAM,CAAC;YAC3F;QACF;IACF;IAEA,MAAMG,uBAAuB7B,MAAMoG,SAAS,CAACL;IAC7C,MAAMM,0BAA0BC,OAAOC,IAAI,CAACR,SAASrE,MAAM;IAC3D,MAAM8E,oBAAoB9B,KAAKC,GAAG;IAElC,IAAIf,cAAc;QAChB9C,QAAQkC,MAAM,CAAC4B,IAAI,CAAC;QACpB,IAAId,QAAQ;YACV,MAAMnD,iBAAiBG,SAASgD,QAAQ;gBACtCkB,SAAS;YACX;QACF;IACF;IAEA,MAAMyB,aACJ7C,eACE,MAAMhC,sBACJC,sBACA,AAACkE,SAAS,CAAC,QAAQ,IAA2C,EAAE,EAChE3E,OACAa,SAASP,MAAM,GAAG,IAAIO,WAAWN,WACjCb,WAEFe;IACJ,MAAMuB,OAAO,CAAC,QAAQ,EAAEnD,eACtBuE,MACAD,eACAD,iBACA+B,yBACAG,mBACA5C,eAAe,WAAW,QAC1BG,SAASpC,YACR;IAEHb,QAAQkC,MAAM,CAAC4B,IAAI,CAAC;QAAExB;IAAK,GAAG;IAC9B,IAAIU,QAAQ;QACV,MAAMnD,iBAAiBG,SAASgD,QAAQ;YACtCkB,SAAS;QACX;IACF;IACA,MAAM0B,kBAAkB,MAAMpG,qBAAqB8C,MAAMqD,YAAYrF,OAAO4C;IAC5E,IAAI0C,oBAAoB1C,YAAY;QAClClD,QAAQkC,MAAM,CAACC,IAAI,CACjB;YAAEG;YAAMsD;YAAiBC,iBAAiB3C;QAAW,GACrD;IAEJ;IAEAlD,QAAQkC,MAAM,CAAC4B,IAAI,CAAC;QAAExB;QAAMwD,YAAYlC,KAAKC,GAAG,KAAKF;IAAG,GAAG;AAC7D"}
|
|
1
|
+
{"version":3,"sources":["../../src/core/backup.ts"],"sourcesContent":["import type { Payload } from 'payload'\n\nimport { list } from '@vercel/blob'\nimport { EJSON } from 'bson'\n\nimport {\n createBlobName,\n getCurrentDbName,\n getCurrentHostname,\n sanitizeBackupLabel,\n} from '../utils/index'\nimport { createTarGzip } from './archive'\nimport { type BackupBlobAccessLevel, readBackupBlobContentFlexible } from './backupBlobIO'\nimport { getResolvedCronBackupSettings, resolveBackupBlobToken } from './backupSettings'\nimport { getDb } from './db'\nimport { getBackupStorageKind, resolveBackupStorage } from './storage'\nimport { updateBackupTask } from './taskProgress'\n\nexport const COLLECTION_FILE_NAME = 'collections.json'\n\n/**\n * Resolves the Vercel Blob read/write token the same way as backup endpoints and\n * {@link createBackup} / {@link restoreBackup}: settings document override, else `BLOB_READ_WRITE_TOKEN`.\n * Pass `explicitToken` to skip reading settings (same as `options.blobToken` in those calls).\n */\nexport async function resolveBackupListToken(\n payload: Payload,\n explicitToken?: string,\n): Promise<string> {\n const t = explicitToken?.trim()\n if (t) {\n return t\n }\n return resolveBackupBlobToken(await getResolvedCronBackupSettings(payload)).trim()\n}\n\nexport async function listBackups(\n payload: Payload,\n options: {\n /**\n * When set, used as the Vercel Blob token (e.g. tests or a pre-resolved value).\n * When omitted, uses {@link resolveBackupListToken} (settings + env), same as other backup APIs.\n */\n blobToken?: string\n } = {},\n) {\n const kind = getBackupStorageKind()\n if (kind === 'vercel-blob') {\n const token = await resolveBackupListToken(payload, options.blobToken)\n if (!token) {\n return []\n }\n return resolveBackupStorage({ blobToken: token, kind }).list('backups/')\n }\n return resolveBackupStorage({ kind }).list('backups/')\n}\n\nfunction resolveBlobToken(blobToken?: string): string | undefined {\n if (blobToken && blobToken.trim().length > 0) {\n return blobToken\n }\n return undefined\n}\n\n/**\n * Builds a tar.gz archive containing the collection dump plus Payload upload-store media files.\n * Media always comes from the Vercel Blob upload store (via token/env), independent of the\n * configured backup archive target (S3 vs Vercel Blob).\n */\nexport async function createMediaBackupFile(\n collectionBackupFile: string,\n mediaCollection: { filename: string }[],\n backupBlobToken?: string,\n mediaListToken?: string,\n payload?: Payload,\n): Promise<Buffer> {\n const envMedia = (process.env.BLOB_READ_WRITE_TOKEN || '').trim()\n const tokenForMedia =\n resolveBlobToken(mediaListToken) ??\n (envMedia.length > 0 ? envMedia : undefined) ??\n resolveBlobToken(backupBlobToken)\n const mediaFiles = await Promise.all(\n mediaCollection.map(async (media) => {\n try {\n const matchingFiles = await list({ limit: 2, prefix: media.filename, token: tokenForMedia })\n const blob = matchingFiles.blobs.find((blob) => blob.pathname === media.filename)\n if (!blob) {\n payload?.logger.warn(\n { filename: media.filename },\n '[backup] File was in collection but not in blob storage',\n )\n return undefined\n }\n const content = await readBackupBlobContentFlexible(\n blob.pathname,\n blob.downloadUrl,\n tokenForMedia ?? '',\n )\n return { name: media.filename, content }\n } catch (err) {\n payload?.logger.warn(\n { err, filename: media.filename },\n '[backup] Failed to read media file from blob storage',\n )\n return undefined\n }\n }),\n )\n return await createTarGzip([\n { name: COLLECTION_FILE_NAME, content: Buffer.from(collectionBackupFile) },\n ...(mediaFiles.filter(Boolean) as { content: Buffer; name: string }[]),\n ])\n}\n\nexport async function createBackup(\n payload: Payload,\n options: {\n backupsToKeep?: number\n /** Public (default) or private Vercel Blob access for the uploaded backup archive. */\n blobAccess?: BackupBlobAccessLevel\n /** Optional explicit Vercel Blob token (falls back to env inside @vercel/blob). */\n blobToken?: string\n cron?: boolean\n includeMedia?: boolean\n /**\n * Optional human-readable label for manual backups (appears in the backup list and is\n * searchable via the label filter). Ignored for cron backups. Whitespace is collapsed,\n * length is capped and the stored value is URL-encoded in the blob pathname.\n */\n label?: string\n /** Mongo collection names to omit from the dump (e.g. manual backup UI). */\n skipCollections?: string[]\n taskId?: string\n } = {},\n): Promise<void> {\n const {\n backupsToKeep,\n blobToken,\n cron = false,\n includeMedia = false,\n skipCollections,\n taskId,\n } = options\n const label = cron ? '' : sanitizeBackupLabel(options.label)\n const blobAccess: BackupBlobAccessLevel = options.blobAccess ?? 'public'\n const token = resolveBlobToken(blobToken)\n const skip = new Set(skipCollections ?? [])\n const resolvedBackupsToKeep = backupsToKeep ?? (Number(process.env.BACKUPS_TO_KEEP) || 10)\n const storage = resolveBackupStorage({ blobAccess, blobToken: token })\n\n const currentHostname = getCurrentHostname()\n const currentDbName = getCurrentDbName()\n const type = cron ? 'cron' : 'manual'\n const t0 = Date.now()\n\n payload.logger.info(\n {\n type,\n blacklist: skipCollections?.length ?? 0,\n db: currentDbName,\n host: currentHostname,\n includeMedia,\n },\n '[backup] Starting backup',\n )\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: 'Starting backup',\n status: 'running',\n })\n }\n\n if (cron) {\n const blobs = await storage.list('backups/cron-')\n const sorted = blobs.sort(\n (a, b) => new Date(b.uploadedAt).getTime() - new Date(a.uploadedAt).getTime(),\n )\n const oldest = sorted.slice(resolvedBackupsToKeep - 1)\n if (oldest.length > 0) {\n payload.logger.info({ count: oldest.length }, '[backup] Pruning old cron backups')\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: `Pruning ${oldest.length} old cron backup${oldest.length === 1 ? '' : 's'}`,\n })\n }\n }\n for (const blob of oldest) {\n await storage.del({ pathname: blob.pathname, url: blob.url })\n payload.logger.info({ pathname: blob.pathname }, '[backup] Deleted old backup')\n }\n }\n\n const db = getDb(payload)\n const collections = await db.listCollections().toArray()\n payload.logger.info({ collections: collections.length }, '[backup] Dumping collections')\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: `Dumping ${collections.length} collection${collections.length === 1 ? '' : 's'}`,\n })\n }\n\n const allData: Record<string, Record<string, unknown>[]> = {}\n for (const collection of collections) {\n if (skip.has(collection.name)) {\n payload.logger.debug(\n { collection: collection.name },\n '[backup] Skipping blacklisted collection',\n )\n continue\n }\n allData[collection.name] = (await db.collection(collection.name).find({}).toArray()) as Record<\n string,\n unknown\n >[]\n payload.logger.debug(\n { collection: collection.name, docs: allData[collection.name].length },\n '[backup] Collection dumped',\n )\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: `Dumped collection ${collection.name} (${allData[collection.name].length} docs)`,\n })\n }\n }\n\n const collectionBackupFile = EJSON.stringify(allData)\n const includedCollectionCount = Object.keys(allData).length\n const backupTimestampMs = Date.now()\n\n if (includeMedia) {\n payload.logger.info('[backup] Bundling media files into archive')\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: 'Bundling media files into archive',\n })\n }\n }\n\n const backupFile =\n includeMedia ?\n await createMediaBackupFile(\n collectionBackupFile,\n (allData?.['media'] as { filename: string }[] | undefined) || [],\n token,\n undefined,\n payload,\n )\n : collectionBackupFile\n const name = `backups/${createBlobName(\n type,\n currentDbName,\n currentHostname,\n includedCollectionCount,\n backupTimestampMs,\n includeMedia ? 'tar.gz' : 'json',\n label || undefined,\n )}`\n\n payload.logger.info({ name }, '[backup] Uploading backup to blob storage')\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: 'Uploading backup to blob storage',\n })\n }\n await storage.put(name, backupFile)\n\n payload.logger.info({ name, durationMs: Date.now() - t0 }, '[backup] Backup complete')\n}\n"],"names":["list","EJSON","createBlobName","getCurrentDbName","getCurrentHostname","sanitizeBackupLabel","createTarGzip","readBackupBlobContentFlexible","getResolvedCronBackupSettings","resolveBackupBlobToken","getDb","getBackupStorageKind","resolveBackupStorage","updateBackupTask","COLLECTION_FILE_NAME","resolveBackupListToken","payload","explicitToken","t","trim","listBackups","options","kind","token","blobToken","resolveBlobToken","length","undefined","createMediaBackupFile","collectionBackupFile","mediaCollection","backupBlobToken","mediaListToken","envMedia","process","env","BLOB_READ_WRITE_TOKEN","tokenForMedia","mediaFiles","Promise","all","map","media","matchingFiles","limit","prefix","filename","blob","blobs","find","pathname","logger","warn","content","downloadUrl","name","err","Buffer","from","filter","Boolean","createBackup","backupsToKeep","cron","includeMedia","skipCollections","taskId","label","blobAccess","skip","Set","resolvedBackupsToKeep","Number","BACKUPS_TO_KEEP","storage","currentHostname","currentDbName","type","t0","Date","now","info","blacklist","db","host","message","status","sorted","sort","a","b","uploadedAt","getTime","oldest","slice","count","del","url","collections","listCollections","toArray","allData","collection","has","debug","docs","stringify","includedCollectionCount","Object","keys","backupTimestampMs","backupFile","put","durationMs"],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type BackupBlobAccessLevel } from './backupBlobIO';
|
|
2
|
+
export interface ReadBackupArchiveBytesOptions {
|
|
3
|
+
/** Provider-agnostic archive pathname (e.g. `backups/manual---db---host---1.json`). */
|
|
4
|
+
archivePathname?: string;
|
|
5
|
+
backupRead?: {
|
|
6
|
+
pathname: string;
|
|
7
|
+
token: string;
|
|
8
|
+
};
|
|
9
|
+
blobAccess?: BackupBlobAccessLevel;
|
|
10
|
+
blobToken?: string;
|
|
11
|
+
}
|
|
12
|
+
/** Prefer `archivePathname` for file-type detection when the download URL may be stale or opaque. */
|
|
13
|
+
export declare function resolveArchiveFileReference(downloadUrl: string, archivePathname?: string): string;
|
|
14
|
+
/** Loads a backup archive into memory (Vercel token read or S3 pathname read, else fetch). */
|
|
15
|
+
export declare function readBackupArchiveBytes(downloadUrl: string, options?: ReadBackupArchiveBytesOptions): Promise<Buffer>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readBackupBlobContentFlexible } from './backupBlobIO';
|
|
2
|
+
import { getBackupStorageKind, resolveBackupStorage } from './storage';
|
|
3
|
+
/** Prefer `archivePathname` for file-type detection when the download URL may be stale or opaque. */ export function resolveArchiveFileReference(downloadUrl, archivePathname) {
|
|
4
|
+
if (typeof archivePathname === 'string' && archivePathname.startsWith('backups/')) {
|
|
5
|
+
return archivePathname;
|
|
6
|
+
}
|
|
7
|
+
return downloadUrl.split('?')[0] ?? downloadUrl;
|
|
8
|
+
}
|
|
9
|
+
/** Loads a backup archive into memory (Vercel token read or S3 pathname read, else fetch). */ export async function readBackupArchiveBytes(downloadUrl, options = {}) {
|
|
10
|
+
if (options.backupRead) {
|
|
11
|
+
return readBackupBlobContentFlexible(options.backupRead.pathname, downloadUrl, options.backupRead.token);
|
|
12
|
+
}
|
|
13
|
+
const archivePathname = options.archivePathname;
|
|
14
|
+
if (getBackupStorageKind() === 's3' && typeof archivePathname === 'string' && archivePathname.startsWith('backups/')) {
|
|
15
|
+
const storage = resolveBackupStorage({
|
|
16
|
+
blobAccess: options.blobAccess,
|
|
17
|
+
blobToken: options.blobToken
|
|
18
|
+
});
|
|
19
|
+
return storage.read({
|
|
20
|
+
pathname: archivePathname
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const res = await fetch(downloadUrl);
|
|
24
|
+
if (!res.ok) {
|
|
25
|
+
throw new Error(`Failed to download backup (${res.status})`);
|
|
26
|
+
}
|
|
27
|
+
return Buffer.from(await res.arrayBuffer());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
//# sourceMappingURL=backupArchiveRead.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/core/backupArchiveRead.ts"],"sourcesContent":["import { type BackupBlobAccessLevel, readBackupBlobContentFlexible } from './backupBlobIO'\nimport { getBackupStorageKind, resolveBackupStorage } from './storage'\n\nexport interface ReadBackupArchiveBytesOptions {\n /** Provider-agnostic archive pathname (e.g. `backups/manual---db---host---1.json`). */\n archivePathname?: string\n backupRead?: { pathname: string; token: string }\n blobAccess?: BackupBlobAccessLevel\n blobToken?: string\n}\n\n/** Prefer `archivePathname` for file-type detection when the download URL may be stale or opaque. */\nexport function resolveArchiveFileReference(downloadUrl: string, archivePathname?: string): string {\n if (typeof archivePathname === 'string' && archivePathname.startsWith('backups/')) {\n return archivePathname\n }\n return downloadUrl.split('?')[0] ?? downloadUrl\n}\n\n/** Loads a backup archive into memory (Vercel token read or S3 pathname read, else fetch). */\nexport async function readBackupArchiveBytes(\n downloadUrl: string,\n options: ReadBackupArchiveBytesOptions = {},\n): Promise<Buffer> {\n if (options.backupRead) {\n return readBackupBlobContentFlexible(\n options.backupRead.pathname,\n downloadUrl,\n options.backupRead.token,\n )\n }\n\n const archivePathname = options.archivePathname\n if (\n getBackupStorageKind() === 's3' &&\n typeof archivePathname === 'string' &&\n archivePathname.startsWith('backups/')\n ) {\n const storage = resolveBackupStorage({\n blobAccess: options.blobAccess,\n blobToken: options.blobToken,\n })\n return storage.read({ pathname: archivePathname })\n }\n\n const res = await fetch(downloadUrl)\n if (!res.ok) {\n throw new Error(`Failed to download backup (${res.status})`)\n }\n return Buffer.from(await res.arrayBuffer())\n}\n"],"names":["readBackupBlobContentFlexible","getBackupStorageKind","resolveBackupStorage","resolveArchiveFileReference","downloadUrl","archivePathname","startsWith","split","readBackupArchiveBytes","options","backupRead","pathname","token","storage","blobAccess","blobToken","read","res","fetch","ok","Error","status","Buffer","from","arrayBuffer"],"mappings":"AAAA,SAAqCA,6BAA6B,QAAQ,iBAAgB;AAC1F,SAASC,oBAAoB,EAAEC,oBAAoB,QAAQ,YAAW;AAUtE,mGAAmG,GACnG,OAAO,SAASC,4BAA4BC,WAAmB,EAAEC,eAAwB;IACvF,IAAI,OAAOA,oBAAoB,YAAYA,gBAAgBC,UAAU,CAAC,aAAa;QACjF,OAAOD;IACT;IACA,OAAOD,YAAYG,KAAK,CAAC,IAAI,CAAC,EAAE,IAAIH;AACtC;AAEA,4FAA4F,GAC5F,OAAO,eAAeI,uBACpBJ,WAAmB,EACnBK,UAAyC,CAAC,CAAC;IAE3C,IAAIA,QAAQC,UAAU,EAAE;QACtB,OAAOV,8BACLS,QAAQC,UAAU,CAACC,QAAQ,EAC3BP,aACAK,QAAQC,UAAU,CAACE,KAAK;IAE5B;IAEA,MAAMP,kBAAkBI,QAAQJ,eAAe;IAC/C,IACEJ,2BAA2B,QAC3B,OAAOI,oBAAoB,YAC3BA,gBAAgBC,UAAU,CAAC,aAC3B;QACA,MAAMO,UAAUX,qBAAqB;YACnCY,YAAYL,QAAQK,UAAU;YAC9BC,WAAWN,QAAQM,SAAS;QAC9B;QACA,OAAOF,QAAQG,IAAI,CAAC;YAAEL,UAAUN;QAAgB;IAClD;IAEA,MAAMY,MAAM,MAAMC,MAAMd;IACxB,IAAI,CAACa,IAAIE,EAAE,EAAE;QACX,MAAM,IAAIC,MAAM,CAAC,2BAA2B,EAAEH,IAAII,MAAM,CAAC,CAAC,CAAC;IAC7D;IACA,OAAOC,OAAOC,IAAI,CAAC,MAAMN,IAAIO,WAAW;AAC1C"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getBackupStorageKind } from './storage/config';
|
|
1
2
|
export const BACKUP_SETTINGS_SLUG = 'backup-settings';
|
|
2
3
|
function defaultBackupsToKeep() {
|
|
3
4
|
const n = Number(process.env.BACKUPS_TO_KEEP);
|
|
@@ -79,6 +80,10 @@ export function resolveBackupBlobToken(settings) {
|
|
|
79
80
|
* When `pathname` is sent, restore/preview read via token + flexible SDK/fetch (works for public
|
|
80
81
|
* and private blobs). Returns undefined if pathname is missing or invalid.
|
|
81
82
|
*/ export function resolveBackupArchiveRead(settings, pathname) {
|
|
83
|
+
// Vercel-specific authenticated archive reads — S3 restores load archives via pathname server-side.
|
|
84
|
+
if (getBackupStorageKind() !== 'vercel-blob') {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
82
87
|
if (typeof pathname !== 'string' || !pathname.startsWith('backups/')) {
|
|
83
88
|
return undefined;
|
|
84
89
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/backupSettings.ts"],"sourcesContent":["import type { Payload } from 'payload'\n\nexport const BACKUP_SETTINGS_SLUG = 'backup-settings'\n\nexport interface ResolvedCronBackupSettings {\n /**\n * Validated access level of the current backup blob store, populated via\n * `validateBackupBlobToken`. `null` when validation has never run (default token use is still\n * treated as `public`, dedicated token without validation falls back to heuristic private).\n */\n backupBlobAccess: 'private' | 'public' | null\n backupBlobReadWriteToken: string\n backupsToKeep: number\n id: string\n includeMediaForCron: boolean\n skipMongoCollections: string[]\n}\n\nfunction defaultBackupsToKeep(): number {\n const n = Number(process.env.BACKUPS_TO_KEEP)\n return Number.isFinite(n) && n > 0 ? Math.floor(n) : 10\n}\n\nexport function normalizeSkipMongoCollections(raw: unknown): string[] {\n if (!Array.isArray(raw)) {\n return []\n }\n const out: string[] = []\n for (const entry of raw) {\n if (typeof entry === 'string' && entry.length > 0 && entry.length < 512) {\n out.push(entry)\n continue\n }\n if (entry && typeof entry === 'object' && 'name' in entry) {\n const name = (entry as { name?: unknown }).name\n if (typeof name === 'string' && name.length > 0 && name.length < 512) {\n out.push(name)\n }\n }\n }\n return out\n}\n\nexport function toPayloadSkipRows(names: string[]): { name: string }[] {\n return names.map((name) => ({ name }))\n}\n\nexport async function getResolvedCronBackupSettings(\n payload: Payload,\n): Promise<ResolvedCronBackupSettings> {\n const res = await payload.find({\n collection: BACKUP_SETTINGS_SLUG,\n depth: 0,\n limit: 1,\n overrideAccess: true,\n })\n const doc = res.docs[0] as unknown as Record<string, unknown> | undefined\n if (!doc) {\n return {\n id: '',\n backupBlobAccess: null,\n backupBlobReadWriteToken: '',\n backupsToKeep: defaultBackupsToKeep(),\n includeMediaForCron: true,\n skipMongoCollections: [],\n }\n }\n const rawKeep = doc.backupsToKeep\n const backupsToKeep =\n typeof rawKeep === 'number' && Number.isFinite(rawKeep) ?\n Math.min(365, Math.max(1, Math.floor(rawKeep)))\n : defaultBackupsToKeep()\n\n const rawAccess = typeof doc.backupBlobAccess === 'string' ? doc.backupBlobAccess : null\n const backupBlobAccess: 'private' | 'public' | null =\n rawAccess === 'public' || rawAccess === 'private' ? rawAccess : null\n\n return {\n id: String(doc.id),\n backupBlobAccess,\n backupBlobReadWriteToken:\n typeof doc.backupBlobReadWriteToken === 'string' ? doc.backupBlobReadWriteToken : '',\n backupsToKeep,\n includeMediaForCron: doc.includeMediaForCron === true,\n skipMongoCollections: normalizeSkipMongoCollections(doc.skipMongoCollections),\n }\n}\n\nexport function resolveBackupBlobToken(settings: ResolvedCronBackupSettings): string {\n const fromSettings = settings.backupBlobReadWriteToken.trim()\n if (fromSettings.length > 0) {\n return fromSettings\n }\n return process.env.BLOB_READ_WRITE_TOKEN || ''\n}\n\n/**\n * Vercel Blob `access` for new backup archives. Prefers the detected + persisted access level from\n * {@link validateBackupBlobToken}; otherwise heuristically treats a dedicated backup token as\n * `private` and the default env token as `public`.\n */\nexport function resolveBackupBlobAccess(\n settings: ResolvedCronBackupSettings,\n): 'private' | 'public' {\n if (settings.backupBlobAccess === 'public' || settings.backupBlobAccess === 'private') {\n return settings.backupBlobAccess\n }\n return settings.backupBlobReadWriteToken.trim().length > 0 ? 'private' : 'public'\n}\n\n/**\n * When `pathname` is sent, restore/preview read via token + flexible SDK/fetch (works for public\n * and private blobs). Returns undefined if pathname is missing or invalid.\n */\nexport function resolveBackupArchiveRead(\n settings: ResolvedCronBackupSettings,\n pathname: unknown,\n): { pathname: string; token: string } | undefined {\n if (typeof pathname !== 'string' || !pathname.startsWith('backups/')) {\n return undefined\n }\n const token = resolveBackupBlobToken(settings).trim()\n if (!token) {\n return undefined\n }\n return { pathname, token }\n}\n"],"names":["BACKUP_SETTINGS_SLUG","defaultBackupsToKeep","n","Number","process","env","BACKUPS_TO_KEEP","isFinite","Math","floor","normalizeSkipMongoCollections","raw","Array","isArray","out","entry","length","push","name","toPayloadSkipRows","names","map","getResolvedCronBackupSettings","payload","res","find","collection","depth","limit","overrideAccess","doc","docs","id","backupBlobAccess","backupBlobReadWriteToken","backupsToKeep","includeMediaForCron","skipMongoCollections","rawKeep","min","max","rawAccess","String","resolveBackupBlobToken","settings","fromSettings","trim","BLOB_READ_WRITE_TOKEN","resolveBackupBlobAccess","resolveBackupArchiveRead","pathname","
|
|
1
|
+
{"version":3,"sources":["../../src/core/backupSettings.ts"],"sourcesContent":["import type { Payload } from 'payload'\n\nimport { getBackupStorageKind } from './storage/config'\n\nexport const BACKUP_SETTINGS_SLUG = 'backup-settings'\n\nexport interface ResolvedCronBackupSettings {\n /**\n * Validated access level of the current backup blob store, populated via\n * `validateBackupBlobToken`. `null` when validation has never run (default token use is still\n * treated as `public`, dedicated token without validation falls back to heuristic private).\n */\n backupBlobAccess: 'private' | 'public' | null\n backupBlobReadWriteToken: string\n backupsToKeep: number\n id: string\n includeMediaForCron: boolean\n skipMongoCollections: string[]\n}\n\nfunction defaultBackupsToKeep(): number {\n const n = Number(process.env.BACKUPS_TO_KEEP)\n return Number.isFinite(n) && n > 0 ? Math.floor(n) : 10\n}\n\nexport function normalizeSkipMongoCollections(raw: unknown): string[] {\n if (!Array.isArray(raw)) {\n return []\n }\n const out: string[] = []\n for (const entry of raw) {\n if (typeof entry === 'string' && entry.length > 0 && entry.length < 512) {\n out.push(entry)\n continue\n }\n if (entry && typeof entry === 'object' && 'name' in entry) {\n const name = (entry as { name?: unknown }).name\n if (typeof name === 'string' && name.length > 0 && name.length < 512) {\n out.push(name)\n }\n }\n }\n return out\n}\n\nexport function toPayloadSkipRows(names: string[]): { name: string }[] {\n return names.map((name) => ({ name }))\n}\n\nexport async function getResolvedCronBackupSettings(\n payload: Payload,\n): Promise<ResolvedCronBackupSettings> {\n const res = await payload.find({\n collection: BACKUP_SETTINGS_SLUG,\n depth: 0,\n limit: 1,\n overrideAccess: true,\n })\n const doc = res.docs[0] as unknown as Record<string, unknown> | undefined\n if (!doc) {\n return {\n id: '',\n backupBlobAccess: null,\n backupBlobReadWriteToken: '',\n backupsToKeep: defaultBackupsToKeep(),\n includeMediaForCron: true,\n skipMongoCollections: [],\n }\n }\n const rawKeep = doc.backupsToKeep\n const backupsToKeep =\n typeof rawKeep === 'number' && Number.isFinite(rawKeep) ?\n Math.min(365, Math.max(1, Math.floor(rawKeep)))\n : defaultBackupsToKeep()\n\n const rawAccess = typeof doc.backupBlobAccess === 'string' ? doc.backupBlobAccess : null\n const backupBlobAccess: 'private' | 'public' | null =\n rawAccess === 'public' || rawAccess === 'private' ? rawAccess : null\n\n return {\n id: String(doc.id),\n backupBlobAccess,\n backupBlobReadWriteToken:\n typeof doc.backupBlobReadWriteToken === 'string' ? doc.backupBlobReadWriteToken : '',\n backupsToKeep,\n includeMediaForCron: doc.includeMediaForCron === true,\n skipMongoCollections: normalizeSkipMongoCollections(doc.skipMongoCollections),\n }\n}\n\nexport function resolveBackupBlobToken(settings: ResolvedCronBackupSettings): string {\n const fromSettings = settings.backupBlobReadWriteToken.trim()\n if (fromSettings.length > 0) {\n return fromSettings\n }\n return process.env.BLOB_READ_WRITE_TOKEN || ''\n}\n\n/**\n * Vercel Blob `access` for new backup archives. Prefers the detected + persisted access level from\n * {@link validateBackupBlobToken}; otherwise heuristically treats a dedicated backup token as\n * `private` and the default env token as `public`.\n */\nexport function resolveBackupBlobAccess(\n settings: ResolvedCronBackupSettings,\n): 'private' | 'public' {\n if (settings.backupBlobAccess === 'public' || settings.backupBlobAccess === 'private') {\n return settings.backupBlobAccess\n }\n return settings.backupBlobReadWriteToken.trim().length > 0 ? 'private' : 'public'\n}\n\n/**\n * When `pathname` is sent, restore/preview read via token + flexible SDK/fetch (works for public\n * and private blobs). Returns undefined if pathname is missing or invalid.\n */\nexport function resolveBackupArchiveRead(\n settings: ResolvedCronBackupSettings,\n pathname: unknown,\n): { pathname: string; token: string } | undefined {\n // Vercel-specific authenticated archive reads — S3 restores load archives via pathname server-side.\n if (getBackupStorageKind() !== 'vercel-blob') {\n return undefined\n }\n if (typeof pathname !== 'string' || !pathname.startsWith('backups/')) {\n return undefined\n }\n const token = resolveBackupBlobToken(settings).trim()\n if (!token) {\n return undefined\n }\n return { pathname, token }\n}\n"],"names":["getBackupStorageKind","BACKUP_SETTINGS_SLUG","defaultBackupsToKeep","n","Number","process","env","BACKUPS_TO_KEEP","isFinite","Math","floor","normalizeSkipMongoCollections","raw","Array","isArray","out","entry","length","push","name","toPayloadSkipRows","names","map","getResolvedCronBackupSettings","payload","res","find","collection","depth","limit","overrideAccess","doc","docs","id","backupBlobAccess","backupBlobReadWriteToken","backupsToKeep","includeMediaForCron","skipMongoCollections","rawKeep","min","max","rawAccess","String","resolveBackupBlobToken","settings","fromSettings","trim","BLOB_READ_WRITE_TOKEN","resolveBackupBlobAccess","resolveBackupArchiveRead","pathname","undefined","startsWith","token"],"mappings":"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"}
|
package/dist/core/restore.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { Payload } from 'payload';
|
|
2
|
+
import type { RestoreBackupResult } from './restoreResult';
|
|
2
3
|
import { type BackupBlobAccessLevel } from './backupBlobIO';
|
|
4
|
+
export type { RestoreBackupResult } from './restoreResult';
|
|
3
5
|
export interface RestoreBackupOptions {
|
|
6
|
+
/** When set with S3, the archive is read server-side and list URLs may be stale. */
|
|
7
|
+
archivePathname?: string;
|
|
4
8
|
/**
|
|
5
9
|
* When set, the archive is loaded with the backup token (fetch + SDK fallback; works for
|
|
6
10
|
* public and private blobs). Otherwise `downloadUrl` is fetched anonymously.
|
|
@@ -23,5 +27,4 @@ export interface RestoreBackupOptions {
|
|
|
23
27
|
*/
|
|
24
28
|
restoreArchiveMedia?: boolean;
|
|
25
29
|
}
|
|
26
|
-
export declare function restoreBackup(payload: Payload, downloadUrl: string, collectionBlacklist?: string[], mergeData?: boolean, taskId?: string, options?: RestoreBackupOptions): Promise<
|
|
27
|
-
export declare function restoreSeedMedia(payload: Payload, taskId?: string): Promise<string[]>;
|
|
30
|
+
export declare function restoreBackup(payload: Payload, downloadUrl: string, collectionBlacklist?: string[], mergeData?: boolean, taskId?: string, options?: RestoreBackupOptions): Promise<RestoreBackupResult>;
|