@trieb.work/payload-plugin-backup-mongodb 0.1.1
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 +319 -0
- package/dist/backup-dashboard.css +1132 -0
- package/dist/client.d.ts +3 -0
- package/dist/client.js +3 -0
- package/dist/client.js.map +1 -0
- package/dist/collections/BackupSettings.d.ts +6 -0
- package/dist/collections/BackupSettings.js +91 -0
- package/dist/collections/BackupSettings.js.map +1 -0
- package/dist/collections/BackupTasks.d.ts +2 -0
- package/dist/collections/BackupTasks.js +56 -0
- package/dist/collections/BackupTasks.js.map +1 -0
- package/dist/components/BackupDashboard/BackupItemActions.client.d.ts +7 -0
- package/dist/components/BackupDashboard/BackupItemActions.client.js +90 -0
- package/dist/components/BackupDashboard/BackupItemActions.client.js.map +1 -0
- package/dist/components/BackupDashboard/BackupListCollapsible.client.d.ts +16 -0
- package/dist/components/BackupDashboard/BackupListCollapsible.client.js +597 -0
- package/dist/components/BackupDashboard/BackupListCollapsible.client.js.map +1 -0
- package/dist/components/BackupDashboard/BackupSettingsModal.client.d.ts +2 -0
- package/dist/components/BackupDashboard/BackupSettingsModal.client.js +939 -0
- package/dist/components/BackupDashboard/BackupSettingsModal.client.js.map +1 -0
- package/dist/components/BackupDashboard/CollectionBackupPreviewBody.client.d.ts +18 -0
- package/dist/components/BackupDashboard/CollectionBackupPreviewBody.client.js +267 -0
- package/dist/components/BackupDashboard/CollectionBackupPreviewBody.client.js.map +1 -0
- package/dist/components/BackupDashboard/ManualBackupDialog.client.d.ts +1 -0
- package/dist/components/BackupDashboard/ManualBackupDialog.client.js +276 -0
- package/dist/components/BackupDashboard/ManualBackupDialog.client.js.map +1 -0
- package/dist/components/BackupDashboard/RestoreBackupDialog.client.d.ts +6 -0
- package/dist/components/BackupDashboard/RestoreBackupDialog.client.js +551 -0
- package/dist/components/BackupDashboard/RestoreBackupDialog.client.js.map +1 -0
- package/dist/components/BackupDashboard/TaskActionButton.client.d.ts +29 -0
- package/dist/components/BackupDashboard/TaskActionButton.client.js +240 -0
- package/dist/components/BackupDashboard/TaskActionButton.client.js.map +1 -0
- package/dist/components/BackupDashboard/backupDashboardInlineCss.d.ts +1 -0
- package/dist/components/BackupDashboard/backupDashboardInlineCss.js +3 -0
- package/dist/components/BackupDashboard/backupDashboardInlineCss.js.map +1 -0
- package/dist/components/BackupDashboard/index.client.d.ts +8 -0
- package/dist/components/BackupDashboard/index.client.js +9 -0
- package/dist/components/BackupDashboard/index.client.js.map +1 -0
- package/dist/components/BackupDashboard/index.d.ts +8 -0
- package/dist/components/BackupDashboard/index.js +196 -0
- package/dist/components/BackupDashboard/index.js.map +1 -0
- package/dist/core/archive.d.ts +8 -0
- package/dist/core/archive.js +47 -0
- package/dist/core/archive.js.map +1 -0
- package/dist/core/backup.d.ts +41 -0
- package/dist/core/backup.js +188 -0
- package/dist/core/backup.js.map +1 -0
- package/dist/core/backupBlobIO.d.ts +54 -0
- package/dist/core/backupBlobIO.js +179 -0
- package/dist/core/backupBlobIO.js.map +1 -0
- package/dist/core/backupBlobTransfer.d.ts +29 -0
- package/dist/core/backupBlobTransfer.js +85 -0
- package/dist/core/backupBlobTransfer.js.map +1 -0
- package/dist/core/backupSettings.d.ts +35 -0
- package/dist/core/backupSettings.js +95 -0
- package/dist/core/backupSettings.js.map +1 -0
- package/dist/core/backupSourcePreview.d.ts +10 -0
- package/dist/core/backupSourcePreview.js +33 -0
- package/dist/core/backupSourcePreview.js.map +1 -0
- package/dist/core/blobTokenValidate.d.ts +14 -0
- package/dist/core/blobTokenValidate.js +75 -0
- package/dist/core/blobTokenValidate.js.map +1 -0
- package/dist/core/db.d.ts +24 -0
- package/dist/core/db.js +12 -0
- package/dist/core/db.js.map +1 -0
- package/dist/core/restore.d.ts +27 -0
- package/dist/core/restore.js +199 -0
- package/dist/core/restore.js.map +1 -0
- package/dist/core/restorePreview.d.ts +64 -0
- package/dist/core/restorePreview.js +266 -0
- package/dist/core/restorePreview.js.map +1 -0
- package/dist/core/taskProgress.d.ts +26 -0
- package/dist/core/taskProgress.js +73 -0
- package/dist/core/taskProgress.js.map +1 -0
- package/dist/core/vercelBackupCron.d.ts +8 -0
- package/dist/core/vercelBackupCron.js +38 -0
- package/dist/core/vercelBackupCron.js.map +1 -0
- package/dist/endpoints/createBackupEndpoints.d.ts +1 -0
- package/dist/endpoints/createBackupEndpoints.js +3 -0
- package/dist/endpoints/createBackupEndpoints.js.map +1 -0
- package/dist/endpoints/index.d.ts +3 -0
- package/dist/endpoints/index.js +34 -0
- package/dist/endpoints/index.js.map +1 -0
- package/dist/endpoints/paths/admin-backup-download.d.ts +8 -0
- package/dist/endpoints/paths/admin-backup-download.js +72 -0
- package/dist/endpoints/paths/admin-backup-download.js.map +1 -0
- package/dist/endpoints/paths/admin-delete.d.ts +3 -0
- package/dist/endpoints/paths/admin-delete.js +55 -0
- package/dist/endpoints/paths/admin-delete.js.map +1 -0
- package/dist/endpoints/paths/admin-manual.d.ts +3 -0
- package/dist/endpoints/paths/admin-manual.js +61 -0
- package/dist/endpoints/paths/admin-manual.js.map +1 -0
- package/dist/endpoints/paths/admin-preview.d.ts +6 -0
- package/dist/endpoints/paths/admin-preview.js +102 -0
- package/dist/endpoints/paths/admin-preview.js.map +1 -0
- package/dist/endpoints/paths/admin-restore.d.ts +3 -0
- package/dist/endpoints/paths/admin-restore.js +74 -0
- package/dist/endpoints/paths/admin-restore.js.map +1 -0
- package/dist/endpoints/paths/admin-seed.d.ts +3 -0
- package/dist/endpoints/paths/admin-seed.js +47 -0
- package/dist/endpoints/paths/admin-seed.js.map +1 -0
- package/dist/endpoints/paths/admin-settings.d.ts +3 -0
- package/dist/endpoints/paths/admin-settings.js +230 -0
- package/dist/endpoints/paths/admin-settings.js.map +1 -0
- package/dist/endpoints/paths/admin-task.d.ts +3 -0
- package/dist/endpoints/paths/admin-task.js +34 -0
- package/dist/endpoints/paths/admin-task.js.map +1 -0
- package/dist/endpoints/paths/admin-validate-blob-token.d.ts +8 -0
- package/dist/endpoints/paths/admin-validate-blob-token.js +26 -0
- package/dist/endpoints/paths/admin-validate-blob-token.js.map +1 -0
- package/dist/endpoints/paths/cron-list.d.ts +2 -0
- package/dist/endpoints/paths/cron-list.js +33 -0
- package/dist/endpoints/paths/cron-list.js.map +1 -0
- package/dist/endpoints/paths/cron-restore.d.ts +2 -0
- package/dist/endpoints/paths/cron-restore.js +61 -0
- package/dist/endpoints/paths/cron-restore.js.map +1 -0
- package/dist/endpoints/paths/cron-run.d.ts +3 -0
- package/dist/endpoints/paths/cron-run.js +45 -0
- package/dist/endpoints/paths/cron-run.js.map +1 -0
- package/dist/endpoints/shared.d.ts +13 -0
- package/dist/endpoints/shared.js +58 -0
- package/dist/endpoints/shared.js.map +1 -0
- package/dist/exports/client.d.ts +1 -0
- package/dist/exports/client.js +3 -0
- package/dist/exports/client.js.map +1 -0
- package/dist/exports/rsc.d.ts +1 -0
- package/dist/exports/rsc.js +3 -0
- package/dist/exports/rsc.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.d.ts +3 -0
- package/dist/plugin.js +82 -0
- package/dist/plugin.js.map +1 -0
- package/dist/publicApiPaths.d.ts +19 -0
- package/dist/publicApiPaths.js +20 -0
- package/dist/publicApiPaths.js.map +1 -0
- package/dist/types.d.ts +21 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/backupSelection.d.ts +6 -0
- package/dist/utils/backupSelection.js +16 -0
- package/dist/utils/backupSelection.js.map +1 -0
- package/dist/utils/blobName.d.ts +32 -0
- package/dist/utils/blobName.js +99 -0
- package/dist/utils/blobName.js.map +1 -0
- package/dist/utils/dashboardRoleAccess.d.ts +23 -0
- package/dist/utils/dashboardRoleAccess.js +64 -0
- package/dist/utils/dashboardRoleAccess.js.map +1 -0
- package/dist/utils/dialogBackdrop.d.ts +9 -0
- package/dist/utils/dialogBackdrop.js +17 -0
- package/dist/utils/dialogBackdrop.js.map +1 -0
- package/dist/utils/formatBytes.d.ts +2 -0
- package/dist/utils/formatBytes.js +20 -0
- package/dist/utils/formatBytes.js.map +1 -0
- package/dist/utils/hostname.d.ts +2 -0
- package/dist/utils/hostname.js +17 -0
- package/dist/utils/hostname.js.map +1 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.js +7 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/maskBlobToken.d.ts +11 -0
- package/dist/utils/maskBlobToken.js +42 -0
- package/dist/utils/maskBlobToken.js.map +1 -0
- package/package.json +140 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import configPromise from '@payload-config';
|
|
3
|
+
import { getPayload } from 'payload';
|
|
4
|
+
import { listBackups, resolveBackupListToken } from '../../core/backup';
|
|
5
|
+
import { getBackupSortTimeMs, getCurrentDbName, getCurrentHostname, isUserAllowedByEnvRoles, transformBlobName } from '../../utils/index';
|
|
6
|
+
import { backupDashboardInlineCss } from './backupDashboardInlineCss';
|
|
7
|
+
import { BackupListCollapsible, BackupSettingsModal, ManualBackupDialog } from './index.client';
|
|
8
|
+
function defaultIsHidden(user, access) {
|
|
9
|
+
if (user === null) {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
if (user === undefined) {
|
|
13
|
+
if (access) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
if (access) {
|
|
19
|
+
return !access(user);
|
|
20
|
+
}
|
|
21
|
+
// Role allow-list via `PAYLOAD_BACKUP_ALLOWED_ROLES`. When unset, falls back to the
|
|
22
|
+
// historical default (admin role, or "visible" when the project has no roles field).
|
|
23
|
+
return !isUserAllowedByEnvRoles(user);
|
|
24
|
+
}
|
|
25
|
+
export const BackupDashboard = async ({ i18n, user })=>{
|
|
26
|
+
if (defaultIsHidden(user)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const hasMongoConnection = Boolean(process.env.MONGODB_URI?.trim()) || Boolean(process.env.DATABASE_URL?.trim());
|
|
30
|
+
if (!hasMongoConnection) {
|
|
31
|
+
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
32
|
+
children: [
|
|
33
|
+
/*#__PURE__*/ _jsx("style", {
|
|
34
|
+
dangerouslySetInnerHTML: {
|
|
35
|
+
__html: backupDashboardInlineCss
|
|
36
|
+
}
|
|
37
|
+
}),
|
|
38
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
39
|
+
className: "backup-dashboard",
|
|
40
|
+
children: [
|
|
41
|
+
/*#__PURE__*/ _jsxs("h2", {
|
|
42
|
+
children: [
|
|
43
|
+
"Backups ",
|
|
44
|
+
/*#__PURE__*/ _jsx("span", {
|
|
45
|
+
className: "experimental",
|
|
46
|
+
children: "(experimental)"
|
|
47
|
+
})
|
|
48
|
+
]
|
|
49
|
+
}),
|
|
50
|
+
/*#__PURE__*/ _jsxs("p", {
|
|
51
|
+
className: "backup-dashboard__setup-hint",
|
|
52
|
+
role: "status",
|
|
53
|
+
children: [
|
|
54
|
+
"Set ",
|
|
55
|
+
/*#__PURE__*/ _jsx("code", {
|
|
56
|
+
className: "backup-dashboard__setup-hint-code",
|
|
57
|
+
children: "MONGODB_URI"
|
|
58
|
+
}),
|
|
59
|
+
" or",
|
|
60
|
+
' ',
|
|
61
|
+
/*#__PURE__*/ _jsx("code", {
|
|
62
|
+
className: "backup-dashboard__setup-hint-code",
|
|
63
|
+
children: "DATABASE_URL"
|
|
64
|
+
}),
|
|
65
|
+
" so Payload can connect to MongoDB. The in-memory dev database sets both automatically when neither is provided."
|
|
66
|
+
]
|
|
67
|
+
})
|
|
68
|
+
]
|
|
69
|
+
})
|
|
70
|
+
]
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const payload = await getPayload({
|
|
74
|
+
config: configPromise
|
|
75
|
+
});
|
|
76
|
+
const backupBlobToken = await resolveBackupListToken(payload);
|
|
77
|
+
const hasBlobToken = backupBlobToken.trim().length > 0;
|
|
78
|
+
const blobs = hasBlobToken ? await listBackups(payload, {
|
|
79
|
+
blobToken: backupBlobToken
|
|
80
|
+
}) : [];
|
|
81
|
+
const sortedBlobs = [
|
|
82
|
+
...blobs
|
|
83
|
+
].sort((a, b)=>{
|
|
84
|
+
const ta = getBackupSortTimeMs(transformBlobName(a.pathname), new Date(a.uploadedAt));
|
|
85
|
+
const tb = getBackupSortTimeMs(transformBlobName(b.pathname), new Date(b.uploadedAt));
|
|
86
|
+
return tb - ta;
|
|
87
|
+
});
|
|
88
|
+
const currentHostname = getCurrentHostname();
|
|
89
|
+
const currentDbName = getCurrentDbName();
|
|
90
|
+
const countOtherDb = sortedBlobs.filter((blob)=>{
|
|
91
|
+
const { dbName } = transformBlobName(blob.pathname);
|
|
92
|
+
return currentDbName !== dbName;
|
|
93
|
+
}).length;
|
|
94
|
+
const countOtherHostname = sortedBlobs.filter((blob)=>{
|
|
95
|
+
const { hostname } = transformBlobName(blob.pathname);
|
|
96
|
+
return currentHostname !== hostname;
|
|
97
|
+
}).length;
|
|
98
|
+
const lastBackup = sortedBlobs[0];
|
|
99
|
+
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
100
|
+
children: [
|
|
101
|
+
/*#__PURE__*/ _jsx("style", {
|
|
102
|
+
dangerouslySetInnerHTML: {
|
|
103
|
+
__html: backupDashboardInlineCss
|
|
104
|
+
},
|
|
105
|
+
"data-payload-backup-mongodb": "1"
|
|
106
|
+
}),
|
|
107
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
108
|
+
className: "backup-dashboard",
|
|
109
|
+
children: [
|
|
110
|
+
/*#__PURE__*/ _jsxs("h2", {
|
|
111
|
+
children: [
|
|
112
|
+
"Backups ",
|
|
113
|
+
/*#__PURE__*/ _jsx("span", {
|
|
114
|
+
className: "experimental",
|
|
115
|
+
children: "(experimental)"
|
|
116
|
+
})
|
|
117
|
+
]
|
|
118
|
+
}),
|
|
119
|
+
!hasBlobToken && /*#__PURE__*/ _jsxs("p", {
|
|
120
|
+
className: "backup-dashboard__setup-hint",
|
|
121
|
+
role: "status",
|
|
122
|
+
children: [
|
|
123
|
+
"Add a Vercel Blob read/write token: set environment variable",
|
|
124
|
+
' ',
|
|
125
|
+
/*#__PURE__*/ _jsx("code", {
|
|
126
|
+
className: "backup-dashboard__setup-hint-code",
|
|
127
|
+
children: "BLOB_READ_WRITE_TOKEN"
|
|
128
|
+
}),
|
|
129
|
+
", or open ",
|
|
130
|
+
/*#__PURE__*/ _jsx("strong", {
|
|
131
|
+
children: "Backup settings"
|
|
132
|
+
}),
|
|
133
|
+
" and paste a token. Until then, the list stays empty and cron or manual backups cannot run."
|
|
134
|
+
]
|
|
135
|
+
}),
|
|
136
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
137
|
+
className: "backup-dashboard__toolbar",
|
|
138
|
+
children: [
|
|
139
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
140
|
+
className: "backup-dashboard__toolbar-meta",
|
|
141
|
+
children: [
|
|
142
|
+
/*#__PURE__*/ _jsxs("span", {
|
|
143
|
+
className: "backup-dashboard__toolbar-pill",
|
|
144
|
+
children: [
|
|
145
|
+
/*#__PURE__*/ _jsx("span", {
|
|
146
|
+
className: "backup-dashboard__toolbar-key",
|
|
147
|
+
children: "Total"
|
|
148
|
+
}),
|
|
149
|
+
sortedBlobs.length,
|
|
150
|
+
" backup",
|
|
151
|
+
sortedBlobs.length === 1 ? '' : 's'
|
|
152
|
+
]
|
|
153
|
+
}),
|
|
154
|
+
/*#__PURE__*/ _jsxs("span", {
|
|
155
|
+
className: "backup-dashboard__toolbar-pill",
|
|
156
|
+
children: [
|
|
157
|
+
/*#__PURE__*/ _jsx("span", {
|
|
158
|
+
className: "backup-dashboard__toolbar-key",
|
|
159
|
+
children: "Last backup"
|
|
160
|
+
}),
|
|
161
|
+
lastBackup ? new Date(getBackupSortTimeMs(transformBlobName(lastBackup.pathname), new Date(lastBackup.uploadedAt))).toLocaleString(i18n?.language || 'en') : 'No backups yet'
|
|
162
|
+
]
|
|
163
|
+
})
|
|
164
|
+
]
|
|
165
|
+
}),
|
|
166
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
167
|
+
className: "make-backup-actions",
|
|
168
|
+
children: [
|
|
169
|
+
/*#__PURE__*/ _jsx(BackupSettingsModal, {}),
|
|
170
|
+
/*#__PURE__*/ _jsx(ManualBackupDialog, {})
|
|
171
|
+
]
|
|
172
|
+
})
|
|
173
|
+
]
|
|
174
|
+
}),
|
|
175
|
+
/*#__PURE__*/ _jsx(BackupListCollapsible, {
|
|
176
|
+
blobs: sortedBlobs.map((b)=>({
|
|
177
|
+
downloadUrl: b.downloadUrl,
|
|
178
|
+
pathname: b.pathname,
|
|
179
|
+
size: b.size,
|
|
180
|
+
uploadedAt: new Date(b.uploadedAt).toISOString(),
|
|
181
|
+
url: b.url
|
|
182
|
+
})),
|
|
183
|
+
countOtherDb: countOtherDb,
|
|
184
|
+
countOtherHostname: countOtherHostname,
|
|
185
|
+
currentDbName: currentDbName,
|
|
186
|
+
currentHostname: currentHostname,
|
|
187
|
+
i18nLanguage: i18n?.language || 'en'
|
|
188
|
+
})
|
|
189
|
+
]
|
|
190
|
+
})
|
|
191
|
+
]
|
|
192
|
+
});
|
|
193
|
+
};
|
|
194
|
+
export default BackupDashboard;
|
|
195
|
+
|
|
196
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/BackupDashboard/index.tsx"],"sourcesContent":["import type { I18n } from '@payloadcms/translations'\n\nimport configPromise from '@payload-config'\nimport { getPayload } 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 /** 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, 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 const payload = await getPayload({ config: configPromise })\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":["configPromise","getPayload","listBackups","resolveBackupListToken","getBackupSortTimeMs","getCurrentDbName","getCurrentHostname","isUserAllowedByEnvRoles","transformBlobName","backupDashboardInlineCss","BackupListCollapsible","BackupSettingsModal","ManualBackupDialog","defaultIsHidden","user","access","undefined","BackupDashboard","i18n","hasMongoConnection","Boolean","process","env","MONGODB_URI","trim","DATABASE_URL","style","dangerouslySetInnerHTML","__html","div","className","h2","span","p","role","code","payload","config","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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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
import zlib from 'node:zlib';
|
|
3
|
+
import tar from 'tar-stream';
|
|
4
|
+
export function createTarGzip(files) {
|
|
5
|
+
return new Promise((resolve, reject)=>{
|
|
6
|
+
const pack = tar.pack();
|
|
7
|
+
const gzip = zlib.createGzip();
|
|
8
|
+
const chunks = [];
|
|
9
|
+
files.forEach(({ name, content })=>{
|
|
10
|
+
pack.entry({
|
|
11
|
+
name
|
|
12
|
+
}, content);
|
|
13
|
+
});
|
|
14
|
+
pack.finalize();
|
|
15
|
+
const compressedStream = pack.pipe(gzip);
|
|
16
|
+
compressedStream.on('data', (chunk)=>chunks.push(chunk));
|
|
17
|
+
compressedStream.on('end', ()=>resolve(Buffer.concat(chunks)));
|
|
18
|
+
compressedStream.on('error', reject);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export function resolveTarGzip(fileBuffer) {
|
|
22
|
+
return new Promise((resolve, reject)=>{
|
|
23
|
+
const gunzip = zlib.createGunzip();
|
|
24
|
+
const extract = tar.extract();
|
|
25
|
+
const files = [];
|
|
26
|
+
extract.on('entry', (header, stream, next)=>{
|
|
27
|
+
const chunks = [];
|
|
28
|
+
stream.on('data', (chunk)=>chunks.push(chunk));
|
|
29
|
+
stream.on('end', ()=>{
|
|
30
|
+
files.push({
|
|
31
|
+
name: header.name,
|
|
32
|
+
content: Buffer.concat(chunks)
|
|
33
|
+
});
|
|
34
|
+
next();
|
|
35
|
+
});
|
|
36
|
+
stream.resume();
|
|
37
|
+
});
|
|
38
|
+
extract.on('finish', ()=>{
|
|
39
|
+
resolve(files);
|
|
40
|
+
});
|
|
41
|
+
extract.on('error', reject);
|
|
42
|
+
const stream = Readable.from(fileBuffer);
|
|
43
|
+
stream.pipe(gunzip).pipe(extract);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
//# sourceMappingURL=archive.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/core/archive.ts"],"sourcesContent":["import { Readable } from 'node:stream'\nimport zlib from 'node:zlib'\nimport tar from 'tar-stream'\n\nexport function createTarGzip(files: { content: Buffer; name: string }[]): Promise<Buffer> {\n return new Promise<Buffer>((resolve, reject) => {\n const pack = tar.pack()\n const gzip = zlib.createGzip()\n const chunks: Buffer[] = []\n\n files.forEach(({ name, content }) => {\n pack.entry({ name }, content)\n })\n\n pack.finalize()\n\n const compressedStream = pack.pipe(gzip)\n\n compressedStream.on('data', (chunk: Buffer) => chunks.push(chunk))\n compressedStream.on('end', () => resolve(Buffer.concat(chunks)))\n compressedStream.on('error', reject)\n })\n}\n\nexport function resolveTarGzip(\n fileBuffer: Buffer,\n): Promise<{ content: Buffer; name: string }[]> {\n return new Promise<{ content: Buffer; name: string }[]>((resolve, reject) => {\n const gunzip = zlib.createGunzip()\n const extract = tar.extract()\n\n const files: { content: Buffer; name: string }[] = []\n\n extract.on('entry', (header, stream, next) => {\n const chunks: Buffer[] = []\n stream.on('data', (chunk) => chunks.push(chunk))\n stream.on('end', () => {\n files.push({\n name: header.name,\n content: Buffer.concat(chunks),\n })\n next()\n })\n stream.resume()\n })\n extract.on('finish', () => {\n resolve(files)\n })\n extract.on('error', reject)\n\n const stream = Readable.from(fileBuffer)\n stream.pipe(gunzip).pipe(extract)\n })\n}\n"],"names":["Readable","zlib","tar","createTarGzip","files","Promise","resolve","reject","pack","gzip","createGzip","chunks","forEach","name","content","entry","finalize","compressedStream","pipe","on","chunk","push","Buffer","concat","resolveTarGzip","fileBuffer","gunzip","createGunzip","extract","header","stream","next","resume","from"],"mappings":"AAAA,SAASA,QAAQ,QAAQ,cAAa;AACtC,OAAOC,UAAU,YAAW;AAC5B,OAAOC,SAAS,aAAY;AAE5B,OAAO,SAASC,cAAcC,KAA0C;IACtE,OAAO,IAAIC,QAAgB,CAACC,SAASC;QACnC,MAAMC,OAAON,IAAIM,IAAI;QACrB,MAAMC,OAAOR,KAAKS,UAAU;QAC5B,MAAMC,SAAmB,EAAE;QAE3BP,MAAMQ,OAAO,CAAC,CAAC,EAAEC,IAAI,EAAEC,OAAO,EAAE;YAC9BN,KAAKO,KAAK,CAAC;gBAAEF;YAAK,GAAGC;QACvB;QAEAN,KAAKQ,QAAQ;QAEb,MAAMC,mBAAmBT,KAAKU,IAAI,CAACT;QAEnCQ,iBAAiBE,EAAE,CAAC,QAAQ,CAACC,QAAkBT,OAAOU,IAAI,CAACD;QAC3DH,iBAAiBE,EAAE,CAAC,OAAO,IAAMb,QAAQgB,OAAOC,MAAM,CAACZ;QACvDM,iBAAiBE,EAAE,CAAC,SAASZ;IAC/B;AACF;AAEA,OAAO,SAASiB,eACdC,UAAkB;IAElB,OAAO,IAAIpB,QAA6C,CAACC,SAASC;QAChE,MAAMmB,SAASzB,KAAK0B,YAAY;QAChC,MAAMC,UAAU1B,IAAI0B,OAAO;QAE3B,MAAMxB,QAA6C,EAAE;QAErDwB,QAAQT,EAAE,CAAC,SAAS,CAACU,QAAQC,QAAQC;YACnC,MAAMpB,SAAmB,EAAE;YAC3BmB,OAAOX,EAAE,CAAC,QAAQ,CAACC,QAAUT,OAAOU,IAAI,CAACD;YACzCU,OAAOX,EAAE,CAAC,OAAO;gBACff,MAAMiB,IAAI,CAAC;oBACTR,MAAMgB,OAAOhB,IAAI;oBACjBC,SAASQ,OAAOC,MAAM,CAACZ;gBACzB;gBACAoB;YACF;YACAD,OAAOE,MAAM;QACf;QACAJ,QAAQT,EAAE,CAAC,UAAU;YACnBb,QAAQF;QACV;QACAwB,QAAQT,EAAE,CAAC,SAASZ;QAEpB,MAAMuB,SAAS9B,SAASiC,IAAI,CAACR;QAC7BK,OAAOZ,IAAI,CAACQ,QAAQR,IAAI,CAACU;IAC3B;AACF"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Payload } from 'payload';
|
|
2
|
+
import { type BackupBlobAccessLevel } from './backupBlobIO';
|
|
3
|
+
export declare const COLLECTION_FILE_NAME = "collections.json";
|
|
4
|
+
/**
|
|
5
|
+
* Resolves the Vercel Blob read/write token the same way as backup endpoints and
|
|
6
|
+
* {@link createBackup} / {@link restoreBackup}: settings document override, else `BLOB_READ_WRITE_TOKEN`.
|
|
7
|
+
* Pass `explicitToken` to skip reading settings (same as `options.blobToken` in those calls).
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolveBackupListToken(payload: Payload, explicitToken?: string): Promise<string>;
|
|
10
|
+
export declare function listBackups(payload: Payload, options?: {
|
|
11
|
+
/**
|
|
12
|
+
* When set, used as the Vercel Blob token (e.g. tests or a pre-resolved value).
|
|
13
|
+
* When omitted, uses {@link resolveBackupListToken} (settings + env), same as other backup APIs.
|
|
14
|
+
*/
|
|
15
|
+
blobToken?: string;
|
|
16
|
+
}): Promise<import("@vercel/blob").ListBlobResultBlob[]>;
|
|
17
|
+
/**
|
|
18
|
+
* @param mediaListToken Token for listing/fetching **Payload media** blobs (usually
|
|
19
|
+
* `BLOB_READ_WRITE_TOKEN`). When omitted, uses env then falls back to `backupBlobToken`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function createMediaBackupFile(collectionBackupFile: string, mediaCollection: {
|
|
22
|
+
filename: string;
|
|
23
|
+
}[], backupBlobToken?: string, mediaListToken?: string, payload?: Payload): Promise<Buffer>;
|
|
24
|
+
export declare function createBackup(payload: Payload, options?: {
|
|
25
|
+
backupsToKeep?: number;
|
|
26
|
+
/** Public (default) or private Vercel Blob access for the uploaded backup archive. */
|
|
27
|
+
blobAccess?: BackupBlobAccessLevel;
|
|
28
|
+
/** Optional explicit Vercel Blob token (falls back to env inside @vercel/blob). */
|
|
29
|
+
blobToken?: string;
|
|
30
|
+
cron?: boolean;
|
|
31
|
+
includeMedia?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Optional human-readable label for manual backups (appears in the backup list and is
|
|
34
|
+
* searchable via the label filter). Ignored for cron backups. Whitespace is collapsed,
|
|
35
|
+
* length is capped and the stored value is URL-encoded in the blob pathname.
|
|
36
|
+
*/
|
|
37
|
+
label?: string;
|
|
38
|
+
/** Mongo collection names to omit from the dump (e.g. manual backup UI). */
|
|
39
|
+
skipCollections?: string[];
|
|
40
|
+
taskId?: string;
|
|
41
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { del, list } from '@vercel/blob';
|
|
2
|
+
import { EJSON } from 'bson';
|
|
3
|
+
import { createBlobName, getCurrentDbName, getCurrentHostname, sanitizeBackupLabel } from '../utils/index';
|
|
4
|
+
import { createTarGzip } from './archive';
|
|
5
|
+
import { putBackupBlobContent, readBackupBlobContentFlexible } from './backupBlobIO';
|
|
6
|
+
import { getResolvedCronBackupSettings, resolveBackupBlobToken } from './backupSettings';
|
|
7
|
+
import { getDb } from './db';
|
|
8
|
+
import { updateBackupTask } from './taskProgress';
|
|
9
|
+
export const COLLECTION_FILE_NAME = 'collections.json';
|
|
10
|
+
/**
|
|
11
|
+
* Resolves the Vercel Blob read/write token the same way as backup endpoints and
|
|
12
|
+
* {@link createBackup} / {@link restoreBackup}: settings document override, else `BLOB_READ_WRITE_TOKEN`.
|
|
13
|
+
* Pass `explicitToken` to skip reading settings (same as `options.blobToken` in those calls).
|
|
14
|
+
*/ export async function resolveBackupListToken(payload, explicitToken) {
|
|
15
|
+
const t = explicitToken?.trim();
|
|
16
|
+
if (t) {
|
|
17
|
+
return t;
|
|
18
|
+
}
|
|
19
|
+
return resolveBackupBlobToken(await getResolvedCronBackupSettings(payload)).trim();
|
|
20
|
+
}
|
|
21
|
+
export async function listBackups(payload, options = {}) {
|
|
22
|
+
const token = await resolveBackupListToken(payload, options.blobToken);
|
|
23
|
+
if (!token) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
const { blobs } = await list({
|
|
27
|
+
limit: 1000,
|
|
28
|
+
prefix: 'backups/',
|
|
29
|
+
token
|
|
30
|
+
});
|
|
31
|
+
return blobs;
|
|
32
|
+
}
|
|
33
|
+
function resolveBlobToken(blobToken) {
|
|
34
|
+
if (blobToken && blobToken.trim().length > 0) {
|
|
35
|
+
return blobToken;
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* @param mediaListToken Token for listing/fetching **Payload media** blobs (usually
|
|
41
|
+
* `BLOB_READ_WRITE_TOKEN`). When omitted, uses env then falls back to `backupBlobToken`.
|
|
42
|
+
*/ export async function createMediaBackupFile(collectionBackupFile, mediaCollection, backupBlobToken, mediaListToken, payload) {
|
|
43
|
+
const envMedia = (process.env.BLOB_READ_WRITE_TOKEN || '').trim();
|
|
44
|
+
const tokenForMedia = resolveBlobToken(mediaListToken) ?? (envMedia.length > 0 ? envMedia : undefined) ?? resolveBlobToken(backupBlobToken);
|
|
45
|
+
const mediaFiles = await Promise.all(mediaCollection.map(async (media)=>{
|
|
46
|
+
const matchingFiles = await list({
|
|
47
|
+
limit: 2,
|
|
48
|
+
prefix: media.filename,
|
|
49
|
+
token: tokenForMedia
|
|
50
|
+
});
|
|
51
|
+
const blob = matchingFiles.blobs.find((blob)=>blob.pathname === media.filename);
|
|
52
|
+
if (!blob) {
|
|
53
|
+
payload?.logger.warn({
|
|
54
|
+
filename: media.filename
|
|
55
|
+
}, '[backup] File was in collection but not in blob storage');
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
const content = await readBackupBlobContentFlexible(blob.pathname, blob.downloadUrl, tokenForMedia ?? '');
|
|
59
|
+
return {
|
|
60
|
+
name: media.filename,
|
|
61
|
+
content
|
|
62
|
+
};
|
|
63
|
+
}));
|
|
64
|
+
return await createTarGzip([
|
|
65
|
+
{
|
|
66
|
+
name: COLLECTION_FILE_NAME,
|
|
67
|
+
content: Buffer.from(collectionBackupFile)
|
|
68
|
+
},
|
|
69
|
+
...mediaFiles.filter(Boolean)
|
|
70
|
+
]);
|
|
71
|
+
}
|
|
72
|
+
export async function createBackup(payload, options = {}) {
|
|
73
|
+
const { backupsToKeep, blobToken, cron = false, includeMedia = false, skipCollections, taskId } = options;
|
|
74
|
+
const label = cron ? '' : sanitizeBackupLabel(options.label);
|
|
75
|
+
const blobAccess = options.blobAccess ?? 'public';
|
|
76
|
+
const envMedia = (process.env.BLOB_READ_WRITE_TOKEN || '').trim();
|
|
77
|
+
const token = resolveBlobToken(blobToken);
|
|
78
|
+
const skip = new Set(skipCollections ?? []);
|
|
79
|
+
const resolvedBackupsToKeep = backupsToKeep ?? (Number(process.env.BACKUPS_TO_KEEP) || 10);
|
|
80
|
+
const currentHostname = getCurrentHostname();
|
|
81
|
+
const currentDbName = getCurrentDbName();
|
|
82
|
+
const type = cron ? 'cron' : 'manual';
|
|
83
|
+
const t0 = Date.now();
|
|
84
|
+
payload.logger.info({
|
|
85
|
+
type,
|
|
86
|
+
blacklist: skipCollections?.length ?? 0,
|
|
87
|
+
db: currentDbName,
|
|
88
|
+
host: currentHostname,
|
|
89
|
+
includeMedia
|
|
90
|
+
}, '[backup] Starting backup');
|
|
91
|
+
if (taskId) {
|
|
92
|
+
await updateBackupTask(payload, taskId, {
|
|
93
|
+
message: 'Starting backup',
|
|
94
|
+
status: 'running'
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (cron) {
|
|
98
|
+
const { blobs } = await list({
|
|
99
|
+
limit: 1000,
|
|
100
|
+
prefix: 'backups/cron-',
|
|
101
|
+
token
|
|
102
|
+
});
|
|
103
|
+
const sorted = blobs.sort((a, b)=>new Date(b.uploadedAt).getTime() - new Date(a.uploadedAt).getTime());
|
|
104
|
+
const oldest = sorted.slice(resolvedBackupsToKeep - 1);
|
|
105
|
+
if (oldest.length > 0) {
|
|
106
|
+
payload.logger.info({
|
|
107
|
+
count: oldest.length
|
|
108
|
+
}, '[backup] Pruning old cron backups');
|
|
109
|
+
if (taskId) {
|
|
110
|
+
await updateBackupTask(payload, taskId, {
|
|
111
|
+
message: `Pruning ${oldest.length} old cron backup${oldest.length === 1 ? '' : 's'}`
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (const blob of oldest){
|
|
116
|
+
await del(blob.url, {
|
|
117
|
+
token
|
|
118
|
+
});
|
|
119
|
+
payload.logger.info({
|
|
120
|
+
pathname: blob.pathname
|
|
121
|
+
}, '[backup] Deleted old backup');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const db = getDb(payload);
|
|
125
|
+
const collections = await db.listCollections().toArray();
|
|
126
|
+
payload.logger.info({
|
|
127
|
+
collections: collections.length
|
|
128
|
+
}, '[backup] Dumping collections');
|
|
129
|
+
if (taskId) {
|
|
130
|
+
await updateBackupTask(payload, taskId, {
|
|
131
|
+
message: `Dumping ${collections.length} collection${collections.length === 1 ? '' : 's'}`
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
const allData = {};
|
|
135
|
+
for (const collection of collections){
|
|
136
|
+
if (skip.has(collection.name)) {
|
|
137
|
+
payload.logger.debug({
|
|
138
|
+
collection: collection.name
|
|
139
|
+
}, '[backup] Skipping blacklisted collection');
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
allData[collection.name] = await db.collection(collection.name).find({}).toArray();
|
|
143
|
+
payload.logger.debug({
|
|
144
|
+
collection: collection.name,
|
|
145
|
+
docs: allData[collection.name].length
|
|
146
|
+
}, '[backup] Collection dumped');
|
|
147
|
+
if (taskId) {
|
|
148
|
+
await updateBackupTask(payload, taskId, {
|
|
149
|
+
message: `Dumped collection ${collection.name} (${allData[collection.name].length} docs)`
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const collectionBackupFile = EJSON.stringify(allData);
|
|
154
|
+
const includedCollectionCount = Object.keys(allData).length;
|
|
155
|
+
const backupTimestampMs = Date.now();
|
|
156
|
+
if (includeMedia) {
|
|
157
|
+
payload.logger.info('[backup] Bundling media files into archive');
|
|
158
|
+
if (taskId) {
|
|
159
|
+
await updateBackupTask(payload, taskId, {
|
|
160
|
+
message: 'Bundling media files into archive'
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const backupFile = includeMedia ? await createMediaBackupFile(collectionBackupFile, allData?.['media'] || [], token, envMedia.length > 0 ? envMedia : undefined, payload) : collectionBackupFile;
|
|
165
|
+
const name = `backups/${createBlobName(type, currentDbName, currentHostname, includedCollectionCount, backupTimestampMs, includeMedia ? 'tar.gz' : 'json', label || undefined)}`;
|
|
166
|
+
payload.logger.info({
|
|
167
|
+
name
|
|
168
|
+
}, '[backup] Uploading backup to blob storage');
|
|
169
|
+
if (taskId) {
|
|
170
|
+
await updateBackupTask(payload, taskId, {
|
|
171
|
+
message: 'Uploading backup to blob storage'
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
const effectiveAccess = await putBackupBlobContent(name, backupFile, token, blobAccess);
|
|
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
|
+
}
|
|
182
|
+
payload.logger.info({
|
|
183
|
+
name,
|
|
184
|
+
durationMs: Date.now() - t0
|
|
185
|
+
}, '[backup] Backup complete');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
//# sourceMappingURL=backup.js.map
|
|
@@ -0,0 +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\n .collection(collection.name)\n .find({})\n .toArray()) as Record<string, unknown>[]\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 = 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":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Vercel Blob access for backup archives under `backups/`. */
|
|
2
|
+
export type BackupBlobAccessLevel = 'private' | 'public';
|
|
3
|
+
/**
|
|
4
|
+
* Returns the blob pathname (no leading slash) for a trusted Vercel Blob HTTPS URL, or null.
|
|
5
|
+
* The URL's path component is percent-decoded once so the result matches the literal pathname
|
|
6
|
+
* stored by Vercel Blob (e.g. blobs whose own pathname contains `%2F` are served from a URL that
|
|
7
|
+
* double-encodes it to `%252F`).
|
|
8
|
+
*/
|
|
9
|
+
export declare function vercelBlobPathnameFromUrl(urlStr: string): null | string;
|
|
10
|
+
export declare function isTrustedBackupBlobReference(urlStr: string, expectedPathname: string): boolean;
|
|
11
|
+
export type BackupBlobDownloadStream = {
|
|
12
|
+
contentType: string;
|
|
13
|
+
stream: ReadableStream<Uint8Array>;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Opens a readable stream for a backup object (public or private). Prefers the `downloadUrl` /
|
|
17
|
+
* `blobUrl` returned by `list()` because Vercel Blob already encodes the pathname correctly there
|
|
18
|
+
* (some legacy backups store literal `%2F` sequences inside the pathname, which the SDK's
|
|
19
|
+
* pathname-to-URL construction would misinterpret).
|
|
20
|
+
*/
|
|
21
|
+
export declare function streamBackupBlobForDownload(options: {
|
|
22
|
+
blobUrl?: null | string;
|
|
23
|
+
downloadUrl?: null | string;
|
|
24
|
+
pathname: string;
|
|
25
|
+
preferredAccess: BackupBlobAccessLevel;
|
|
26
|
+
token: string;
|
|
27
|
+
}): Promise<BackupBlobDownloadStream | null>;
|
|
28
|
+
export declare function bufferFromWebReadableStream(stream: ReadableStream<Uint8Array>): Promise<Buffer>;
|
|
29
|
+
/**
|
|
30
|
+
* Read a backup blob using the trusted `downloadUrl` returned by `list()`. The Bearer token is
|
|
31
|
+
* always attached for Vercel Blob hosts; this works for public blobs (token ignored) and is
|
|
32
|
+
* required for private blobs.
|
|
33
|
+
*/
|
|
34
|
+
export declare function readBackupBlobContent(options: {
|
|
35
|
+
/** Kept for API compatibility — the fetch path is the same for public and private stores. */
|
|
36
|
+
access: BackupBlobAccessLevel;
|
|
37
|
+
downloadUrl: string;
|
|
38
|
+
pathname: string;
|
|
39
|
+
token: string;
|
|
40
|
+
}): Promise<Buffer>;
|
|
41
|
+
/**
|
|
42
|
+
* Flexible read used during restore/preview/transfer where the source store visibility may differ
|
|
43
|
+
* from the new one. Always tries authenticated access first (correct for private stores and
|
|
44
|
+
* harmless for public ones). Falls back to anonymous fetch only when the URL is not a Vercel
|
|
45
|
+
* Blob URL, so tokens never leak to arbitrary hosts.
|
|
46
|
+
*/
|
|
47
|
+
export declare function readBackupBlobContentFlexible(pathname: string, downloadUrl: string, token: string, _sourceAccessHint?: BackupBlobAccessLevel): Promise<Buffer>;
|
|
48
|
+
/**
|
|
49
|
+
* Uploads a backup blob with the preferred access level and transparently retries the other level
|
|
50
|
+
* if the Vercel Blob store rejects the preferred one (e.g. legacy public-only stores reject
|
|
51
|
+
* `access: 'private'`). Returns the effective access level that succeeded so callers can log/track
|
|
52
|
+
* it; throws the original error if both attempts fail.
|
|
53
|
+
*/
|
|
54
|
+
export declare function putBackupBlobContent(pathname: string, body: Buffer | string | Uint8Array, token: string | undefined, access: BackupBlobAccessLevel): Promise<BackupBlobAccessLevel>;
|