@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.
Files changed (85) hide show
  1. package/README.md +130 -62
  2. package/dist/backup-dashboard.css +18 -1
  3. package/dist/collections/BackupTasks.js +8 -1
  4. package/dist/collections/BackupTasks.js.map +1 -1
  5. package/dist/components/BackupDashboard/BackupSettingsModal.client.js +203 -115
  6. package/dist/components/BackupDashboard/BackupSettingsModal.client.js.map +1 -1
  7. package/dist/components/BackupDashboard/RestoreBackupDialog.client.js +1 -1
  8. package/dist/components/BackupDashboard/RestoreBackupDialog.client.js.map +1 -1
  9. package/dist/components/BackupDashboard/TaskActionButton.client.d.ts +1 -1
  10. package/dist/components/BackupDashboard/TaskActionButton.client.js +59 -26
  11. package/dist/components/BackupDashboard/TaskActionButton.client.js.map +1 -1
  12. package/dist/components/BackupDashboard/backupDashboardInlineCss.js +1 -1
  13. package/dist/components/BackupDashboard/backupDashboardInlineCss.js.map +1 -1
  14. package/dist/components/BackupDashboard/index.js +27 -3
  15. package/dist/components/BackupDashboard/index.js.map +1 -1
  16. package/dist/core/backup.d.ts +4 -3
  17. package/dist/core/backup.js +50 -43
  18. package/dist/core/backup.js.map +1 -1
  19. package/dist/core/backupArchiveRead.d.ts +15 -0
  20. package/dist/core/backupArchiveRead.js +30 -0
  21. package/dist/core/backupArchiveRead.js.map +1 -0
  22. package/dist/core/backupSettings.js +5 -0
  23. package/dist/core/backupSettings.js.map +1 -1
  24. package/dist/core/restore.d.ts +5 -2
  25. package/dist/core/restore.js +133 -89
  26. package/dist/core/restore.js.map +1 -1
  27. package/dist/core/restorePreview.d.ts +12 -3
  28. package/dist/core/restorePreview.js +17 -13
  29. package/dist/core/restorePreview.js.map +1 -1
  30. package/dist/core/restoreResult.d.ts +34 -0
  31. package/dist/core/restoreResult.js +45 -0
  32. package/dist/core/restoreResult.js.map +1 -0
  33. package/dist/core/storage/config.d.ts +31 -0
  34. package/dist/core/storage/config.js +60 -0
  35. package/dist/core/storage/config.js.map +1 -0
  36. package/dist/core/storage/index.d.ts +23 -0
  37. package/dist/core/storage/index.js +25 -0
  38. package/dist/core/storage/index.js.map +1 -0
  39. package/dist/core/storage/s3.d.ts +35 -0
  40. package/dist/core/storage/s3.js +200 -0
  41. package/dist/core/storage/s3.js.map +1 -0
  42. package/dist/core/storage/types.d.ts +60 -0
  43. package/dist/core/storage/types.js +6 -0
  44. package/dist/core/storage/types.js.map +1 -0
  45. package/dist/core/storage/vercelBlob.d.ts +25 -0
  46. package/dist/core/storage/vercelBlob.js +53 -0
  47. package/dist/core/storage/vercelBlob.js.map +1 -0
  48. package/dist/core/taskProgress.d.ts +10 -4
  49. package/dist/core/taskProgress.js +11 -2
  50. package/dist/core/taskProgress.js.map +1 -1
  51. package/dist/endpoints/index.js +0 -5
  52. package/dist/endpoints/index.js.map +1 -1
  53. package/dist/endpoints/paths/admin-backup-download.js +8 -7
  54. package/dist/endpoints/paths/admin-backup-download.js.map +1 -1
  55. package/dist/endpoints/paths/admin-delete.js +11 -5
  56. package/dist/endpoints/paths/admin-delete.js.map +1 -1
  57. package/dist/endpoints/paths/admin-manual.js +2 -1
  58. package/dist/endpoints/paths/admin-manual.js.map +1 -1
  59. package/dist/endpoints/paths/admin-preview.js +9 -2
  60. package/dist/endpoints/paths/admin-preview.js.map +1 -1
  61. package/dist/endpoints/paths/admin-restore.js +7 -4
  62. package/dist/endpoints/paths/admin-restore.js.map +1 -1
  63. package/dist/endpoints/paths/admin-settings.js +35 -2
  64. package/dist/endpoints/paths/admin-settings.js.map +1 -1
  65. package/dist/endpoints/paths/cron-list.js +2 -1
  66. package/dist/endpoints/paths/cron-list.js.map +1 -1
  67. package/dist/endpoints/paths/cron-restore.js +14 -4
  68. package/dist/endpoints/paths/cron-restore.js.map +1 -1
  69. package/dist/endpoints/paths/cron-run.js +2 -1
  70. package/dist/endpoints/paths/cron-run.js.map +1 -1
  71. package/dist/endpoints/shared.d.ts +0 -1
  72. package/dist/endpoints/shared.js +0 -6
  73. package/dist/endpoints/shared.js.map +1 -1
  74. package/dist/index.d.ts +5 -3
  75. package/dist/index.js +3 -2
  76. package/dist/index.js.map +1 -1
  77. package/dist/publicApiPaths.d.ts +0 -1
  78. package/dist/publicApiPaths.js +0 -1
  79. package/dist/publicApiPaths.js.map +1 -1
  80. package/dist/types.d.ts +0 -5
  81. package/dist/types.js.map +1 -1
  82. package/package.json +22 -4
  83. package/dist/endpoints/paths/admin-seed.d.ts +0 -3
  84. package/dist/endpoints/paths/admin-seed.js +0 -47
  85. package/dist/endpoints/paths/admin-seed.js.map +0 -1
@@ -1,9 +1,8 @@
1
1
  import { EJSON } from 'bson';
2
- import fs from 'node:fs/promises';
3
- import path from 'node:path';
4
2
  import { resolveTarGzip } from './archive';
5
3
  import { COLLECTION_FILE_NAME } from './backup';
6
- import { putBackupBlobContent, readBackupBlobContentFlexible } from './backupBlobIO';
4
+ import { readBackupArchiveBytes, resolveArchiveFileReference } from './backupArchiveRead';
5
+ import { putBackupBlobContent } from './backupBlobIO';
7
6
  import { getDb } from './db';
8
7
  import { updateBackupTask } from './taskProgress';
9
8
  export async function restoreBackup(payload, downloadUrl, collectionBlacklist = [], mergeData = false, taskId, options) {
@@ -11,8 +10,12 @@ export async function restoreBackup(payload, downloadUrl, collectionBlacklist =
11
10
  const blobToken = options?.blobToken;
12
11
  const blobAccess = options?.blobAccess ?? 'public';
13
12
  const backupRead = options?.backupRead;
13
+ const archivePathname = options?.archivePathname;
14
14
  const t0 = Date.now();
15
- const urlBase = downloadUrl.split('?')?.[0];
15
+ const archiveRef = resolveArchiveFileReference(downloadUrl, archivePathname);
16
+ const collectionOutcomes = [];
17
+ let mediaOutcome = null;
18
+ let archiveKind;
16
19
  // Progress is stored in `backup-tasks`. Restoring that collection from the file
17
20
  // would delete/replace the active task doc and break GET .../admin/task/:id polling.
18
21
  const effectiveBlacklist = taskId ? Array.from(new Set([
@@ -24,7 +27,7 @@ export async function restoreBackup(payload, downloadUrl, collectionBlacklist =
24
27
  payload.logger.info({
25
28
  blacklist: effectiveBlacklist,
26
29
  mergeData,
27
- url: urlBase
30
+ url: archiveRef
28
31
  }, '[restore] Starting restore');
29
32
  if (taskId) {
30
33
  await updateBackupTask(payload, taskId, {
@@ -33,15 +36,15 @@ export async function restoreBackup(payload, downloadUrl, collectionBlacklist =
33
36
  });
34
37
  }
35
38
  const db = getDb(payload);
36
- const archiveBytes = backupRead ? await readBackupBlobContentFlexible(backupRead.pathname, downloadUrl, backupRead.token) : await (async ()=>{
37
- const res = await fetch(downloadUrl);
38
- if (!res.ok) {
39
- throw new Error(`Failed to download backup (${res.status})`);
40
- }
41
- return Buffer.from(await res.arrayBuffer());
42
- })();
39
+ const archiveBytes = await readBackupArchiveBytes(downloadUrl, {
40
+ archivePathname,
41
+ backupRead,
42
+ blobAccess,
43
+ blobToken
44
+ });
43
45
  let collections = {};
44
- if (urlBase?.endsWith('.json')) {
46
+ if (archiveRef.endsWith('.json')) {
47
+ archiveKind = 'json';
45
48
  payload.logger.info('[restore] Parsing JSON backup');
46
49
  if (taskId) {
47
50
  await updateBackupTask(payload, taskId, {
@@ -49,7 +52,8 @@ export async function restoreBackup(payload, downloadUrl, collectionBlacklist =
49
52
  });
50
53
  }
51
54
  collections = EJSON.parse(archiveBytes.toString('utf8'));
52
- } else if (urlBase?.endsWith('.gz')) {
55
+ } else if (archiveRef.endsWith('.gz')) {
56
+ archiveKind = 'tar-gzip';
53
57
  payload.logger.info('[restore] Extracting tar.gz backup');
54
58
  if (taskId) {
55
59
  await updateBackupTask(payload, taskId, {
@@ -59,34 +63,75 @@ export async function restoreBackup(payload, downloadUrl, collectionBlacklist =
59
63
  const files = await resolveTarGzip(archiveBytes);
60
64
  collections = EJSON.parse(files.find((file)=>file.name === COLLECTION_FILE_NAME)?.content?.toString() || '{}');
61
65
  const medias = restoreArchiveMedia ? files.filter((file)=>file.name !== COLLECTION_FILE_NAME) : [];
66
+ mediaOutcome = {
67
+ accessFallbackCount: 0,
68
+ attempted: medias.length,
69
+ failed: [],
70
+ restored: 0,
71
+ skippedByOption: !restoreArchiveMedia
72
+ };
62
73
  if (!restoreArchiveMedia) {
63
74
  payload.logger.info('[restore] Skipping archive media blob upload (restoreArchiveMedia=false)');
75
+ if (taskId) {
76
+ await updateBackupTask(payload, taskId, {
77
+ message: 'Skipped media files from archive'
78
+ });
79
+ }
80
+ } else {
81
+ payload.logger.info({
82
+ count: medias.length
83
+ }, '[restore] Restoring media files to blob storage');
84
+ if (taskId) {
85
+ await updateBackupTask(payload, taskId, {
86
+ message: `Restoring ${medias.length} media file${medias.length === 1 ? '' : 's'}`
87
+ });
88
+ }
89
+ const mediaResults = await Promise.all(medias.map(async (media)=>{
90
+ try {
91
+ const effectiveAccess = await putBackupBlobContent(media.name, media.content, blobToken, blobAccess);
92
+ return {
93
+ name: media.name,
94
+ effectiveAccess,
95
+ error: undefined,
96
+ ok: true
97
+ };
98
+ } catch (err) {
99
+ const error = err instanceof Error ? err.message : String(err);
100
+ payload.logger.warn({
101
+ err,
102
+ filename: media.name
103
+ }, '[restore] Failed to upload media file to blob storage');
104
+ return {
105
+ name: media.name,
106
+ error,
107
+ ok: false
108
+ };
109
+ }
110
+ }));
111
+ for (const result of mediaResults){
112
+ if (result.ok) {
113
+ mediaOutcome.restored += 1;
114
+ if (result.effectiveAccess !== blobAccess) {
115
+ mediaOutcome.accessFallbackCount += 1;
116
+ }
117
+ payload.logger.debug({
118
+ name: result.name,
119
+ access: result.effectiveAccess
120
+ }, '[restore] Media file uploaded');
121
+ } else {
122
+ mediaOutcome.failed.push({
123
+ name: result.name,
124
+ error: result.error
125
+ });
126
+ }
127
+ }
128
+ if (mediaOutcome.accessFallbackCount > 0) {
129
+ payload.logger.warn({
130
+ count: mediaOutcome.accessFallbackCount,
131
+ preferredAccess: blobAccess
132
+ }, '[restore] Blob store rejected preferred access level for media; uploaded with fallback');
133
+ }
64
134
  }
65
- payload.logger.info({
66
- count: medias.length
67
- }, '[restore] Restoring media files to blob storage');
68
- if (taskId) {
69
- await updateBackupTask(payload, taskId, {
70
- message: restoreArchiveMedia ? `Restoring ${medias.length} media file${medias.length === 1 ? '' : 's'}` : 'Skipped media files from archive'
71
- });
72
- }
73
- const mediaResults = await Promise.all(medias.map((media)=>putBackupBlobContent(media.name, media.content, blobToken, blobAccess).then((effectiveAccess)=>({
74
- name: media.name,
75
- effectiveAccess
76
- }))));
77
- const mismatched = mediaResults.filter((r)=>r.effectiveAccess !== blobAccess);
78
- if (mismatched.length > 0) {
79
- payload.logger.warn({
80
- count: mismatched.length,
81
- preferredAccess: blobAccess
82
- }, '[restore] Blob store rejected preferred access level for media; uploaded with fallback');
83
- }
84
- mediaResults.forEach((result)=>{
85
- payload.logger.debug({
86
- name: result.name,
87
- access: result.effectiveAccess
88
- }, '[restore] Media file uploaded');
89
- });
90
135
  } else {
91
136
  throw new Error(`File type of backup ${downloadUrl} not supported`);
92
137
  }
@@ -105,26 +150,39 @@ export async function restoreBackup(payload, downloadUrl, collectionBlacklist =
105
150
  payload.logger.debug({
106
151
  collection: collectionName
107
152
  }, '[restore] Skipping blacklisted collection');
153
+ collectionOutcomes.push({
154
+ name: collectionName,
155
+ kind: 'skipped',
156
+ reason: 'blacklist'
157
+ });
108
158
  continue;
109
159
  }
110
160
  const collectionData = collections[collectionName];
111
- if (collectionData.length > 0) {
112
- payload.logger.info({
113
- collection: collectionName,
114
- docs: collectionData.length,
115
- mergeData
116
- }, '[restore] Restoring collection');
161
+ if (collectionData.length === 0) {
162
+ collectionOutcomes.push({
163
+ name: collectionName,
164
+ kind: 'skipped',
165
+ reason: 'empty'
166
+ });
167
+ continue;
168
+ }
169
+ payload.logger.info({
170
+ collection: collectionName,
171
+ docs: collectionData.length,
172
+ mergeData
173
+ }, '[restore] Restoring collection');
174
+ if (taskId) {
175
+ await updateBackupTask(payload, taskId, {
176
+ message: `Restoring collection ${collectionName} (${collectionData.length} docs)`
177
+ });
178
+ }
179
+ try {
117
180
  const collection = db.collection(collectionName);
118
181
  const indexes = await collection.indexes();
119
182
  const uniqueIndexes = indexes.filter((idx)=>idx.unique).flatMap((idx)=>Object.keys(idx.key));
120
183
  if (!mergeData) {
121
184
  await collection.deleteMany({});
122
185
  }
123
- if (taskId) {
124
- await updateBackupTask(payload, taskId, {
125
- message: `Restoring collection ${collectionName} (${collectionData.length} docs)`
126
- });
127
- }
128
186
  const res = await collection.bulkWrite(collectionData.map((doc)=>({
129
187
  updateOne: {
130
188
  filter: uniqueIndexes.length > 0 ? {
@@ -150,50 +208,36 @@ export async function restoreBackup(payload, downloadUrl, collectionBlacklist =
150
208
  modified: res.modifiedCount,
151
209
  upserted: res.upsertedCount
152
210
  }, '[restore] Collection restored');
153
- }
154
- }
155
- payload.logger.info({
156
- durationMs: Date.now() - t0
157
- }, '[restore] Restore complete');
158
- }
159
- export async function restoreSeedMedia(payload, taskId) {
160
- const files = await fs.readdir(path.join(process.cwd(), 'public/seed/media'));
161
- if (taskId) {
162
- await updateBackupTask(payload, taskId, {
163
- message: `Restoring ${files.length} seed media file${files.length === 1 ? '' : 's'}`,
164
- status: 'running'
165
- });
166
- }
167
- for (const file of files){
168
- const data = await fs.readFile(path.join(process.cwd(), 'public/seed/media', file));
169
- if (process.env.BLOB_READ_WRITE_TOKEN) {
170
- const effectiveAccess = await putBackupBlobContent(file, data, process.env.BLOB_READ_WRITE_TOKEN, 'public');
171
- payload.logger.info({
172
- access: effectiveAccess,
173
- file
174
- }, '[restore] Restored seed media to Vercel Blob storage');
175
- } else {
176
- const folderPath = path.join(process.cwd(), 'public/media');
177
- const publicPath = path.join(folderPath, file);
178
- await fs.mkdir(folderPath, {
179
- recursive: true
211
+ collectionOutcomes.push({
212
+ name: collectionName,
213
+ docCount: collectionData.length,
214
+ kind: 'restored'
180
215
  });
181
- await fs.writeFile(publicPath, data);
182
- payload.logger.info({
183
- file,
184
- publicPath
185
- }, '[restore] Restored seed media to public directory');
186
- }
187
- if (taskId) {
188
- await updateBackupTask(payload, taskId, {
189
- message: `Restored seed media file ${file}`
216
+ } catch (err) {
217
+ const error = err instanceof Error ? err.message : String(err);
218
+ payload.logger.error({
219
+ collection: collectionName,
220
+ err
221
+ }, '[restore] Failed to restore collection');
222
+ collectionOutcomes.push({
223
+ name: collectionName,
224
+ error,
225
+ kind: 'failed'
190
226
  });
191
227
  }
192
228
  }
229
+ const durationMs = Date.now() - t0;
230
+ const result = {
231
+ archiveKind,
232
+ collections: collectionOutcomes,
233
+ durationMs,
234
+ media: mediaOutcome
235
+ };
193
236
  payload.logger.info({
194
- count: files.length
195
- }, '[restore] Restored all seed media files');
196
- return files;
237
+ durationMs,
238
+ result
239
+ }, '[restore] Restore complete');
240
+ return result;
197
241
  }
198
242
 
199
243
  //# sourceMappingURL=restore.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/restore.ts"],"sourcesContent":["import type { Payload } from 'payload'\n\nimport { EJSON } from 'bson'\nimport fs from 'node:fs/promises'\nimport path from 'node:path'\n\nimport { resolveTarGzip } from './archive'\nimport { COLLECTION_FILE_NAME } from './backup'\nimport {\n type BackupBlobAccessLevel,\n putBackupBlobContent,\n readBackupBlobContentFlexible,\n} from './backupBlobIO'\nimport { getDb } from './db'\nimport { updateBackupTask } from './taskProgress'\n\nexport interface RestoreBackupOptions {\n /**\n * When set, the archive is loaded with the backup token (fetch + SDK fallback; works for\n * public and private blobs). Otherwise `downloadUrl` is fetched anonymously.\n */\n backupRead?: { pathname: string; token: string }\n /**\n * Preferred Vercel Blob access for re-uploaded media files. When the store rejects the\n * preferred level (e.g. a private-only store rejects `public`), the other level is tried\n * automatically. Defaults to `public`.\n */\n blobAccess?: BackupBlobAccessLevel\n /** Optional explicit Vercel Blob token used for archive media blob uploads. */\n blobToken?: string\n /**\n * When false, skip uploading media blobs from a tar.gz archive (Mongo `media` and other\n * collections still restore). Default true.\n */\n restoreArchiveMedia?: boolean\n}\n\nexport async function restoreBackup(\n payload: Payload,\n downloadUrl: string,\n collectionBlacklist: string[] = [],\n mergeData = false,\n taskId?: string,\n options?: RestoreBackupOptions,\n): Promise<void> {\n const restoreArchiveMedia = options?.restoreArchiveMedia !== false\n const blobToken = options?.blobToken\n const blobAccess: BackupBlobAccessLevel = options?.blobAccess ?? 'public'\n const backupRead = options?.backupRead\n const t0 = Date.now()\n const urlBase = downloadUrl.split('?')?.[0]\n\n // Progress is stored in `backup-tasks`. Restoring that collection from the file\n // would delete/replace the active task doc and break GET .../admin/task/:id polling.\n const effectiveBlacklist =\n taskId ?\n Array.from(new Set(['backup-tasks', ...collectionBlacklist]))\n : [...collectionBlacklist]\n\n payload.logger.info(\n { blacklist: effectiveBlacklist, mergeData, url: urlBase },\n '[restore] Starting restore',\n )\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: 'Starting restore',\n status: 'running',\n })\n }\n\n const db = getDb(payload)\n const archiveBytes =\n backupRead ?\n await readBackupBlobContentFlexible(backupRead.pathname, downloadUrl, backupRead.token)\n : await (async () => {\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 let collections: Record<string, Record<string, unknown>[]> = {}\n\n if (urlBase?.endsWith('.json')) {\n payload.logger.info('[restore] Parsing JSON backup')\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: 'Parsing JSON backup',\n })\n }\n collections = EJSON.parse(archiveBytes.toString('utf8'))\n } else if (urlBase?.endsWith('.gz')) {\n payload.logger.info('[restore] Extracting tar.gz backup')\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: 'Extracting tar.gz backup',\n })\n }\n const files = await resolveTarGzip(archiveBytes)\n collections = EJSON.parse(\n files.find((file) => file.name === COLLECTION_FILE_NAME)?.content?.toString() || '{}',\n )\n const medias =\n restoreArchiveMedia ? files.filter((file) => file.name !== COLLECTION_FILE_NAME) : []\n if (!restoreArchiveMedia) {\n payload.logger.info(\n '[restore] Skipping archive media blob upload (restoreArchiveMedia=false)',\n )\n }\n payload.logger.info({ count: medias.length }, '[restore] Restoring media files to blob storage')\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message:\n restoreArchiveMedia ?\n `Restoring ${medias.length} media file${medias.length === 1 ? '' : 's'}`\n : 'Skipped media files from archive',\n })\n }\n const mediaResults = await Promise.all(\n medias.map((media) =>\n putBackupBlobContent(media.name, media.content, blobToken, blobAccess).then(\n (effectiveAccess) => ({ name: media.name, effectiveAccess }),\n ),\n ),\n )\n const mismatched = mediaResults.filter((r) => r.effectiveAccess !== blobAccess)\n if (mismatched.length > 0) {\n payload.logger.warn(\n { count: mismatched.length, preferredAccess: blobAccess },\n '[restore] Blob store rejected preferred access level for media; uploaded with fallback',\n )\n }\n mediaResults.forEach((result) => {\n payload.logger.debug(\n { name: result.name, access: result.effectiveAccess },\n '[restore] Media file uploaded',\n )\n })\n } else {\n throw new Error(`File type of backup ${downloadUrl} not supported`)\n }\n\n const collectionNames = Object.keys(collections)\n payload.logger.info(\n { blacklisted: effectiveBlacklist.length, total: collectionNames.length },\n '[restore] Restoring collections',\n )\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: `Restoring ${collectionNames.length} collection${collectionNames.length === 1 ? '' : 's'}`,\n })\n }\n\n for (const collectionName of collectionNames) {\n if (effectiveBlacklist.includes(collectionName)) {\n payload.logger.debug(\n { collection: collectionName },\n '[restore] Skipping blacklisted collection',\n )\n continue\n }\n const collectionData = collections[collectionName]\n if (collectionData.length > 0) {\n payload.logger.info(\n { collection: collectionName, docs: collectionData.length, mergeData },\n '[restore] Restoring collection',\n )\n const collection = db.collection(collectionName)\n const indexes = await collection.indexes()\n const uniqueIndexes = indexes\n .filter((idx) => idx.unique)\n .flatMap((idx) => Object.keys(idx.key))\n if (!mergeData) {\n await collection.deleteMany({})\n }\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: `Restoring collection ${collectionName} (${collectionData.length} docs)`,\n })\n }\n const res = await collection.bulkWrite(\n collectionData.map((doc) => ({\n updateOne: {\n filter:\n uniqueIndexes.length > 0 ?\n {\n $or: [\n { _id: doc._id },\n ...uniqueIndexes.map((field) => ({ [field]: doc[field] })),\n ],\n }\n : { _id: doc._id },\n update: { $set: doc },\n upsert: true,\n },\n })),\n )\n payload.logger.debug(\n { collection: collectionName, modified: res.modifiedCount, upserted: res.upsertedCount },\n '[restore] Collection restored',\n )\n }\n }\n\n payload.logger.info({ durationMs: Date.now() - t0 }, '[restore] Restore complete')\n}\n\nexport async function restoreSeedMedia(payload: Payload, taskId?: string): Promise<string[]> {\n const files = await fs.readdir(path.join(process.cwd(), 'public/seed/media'))\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: `Restoring ${files.length} seed media file${files.length === 1 ? '' : 's'}`,\n status: 'running',\n })\n }\n for (const file of files) {\n const data = await fs.readFile(path.join(process.cwd(), 'public/seed/media', file))\n if (process.env.BLOB_READ_WRITE_TOKEN) {\n const effectiveAccess = await putBackupBlobContent(\n file,\n data,\n process.env.BLOB_READ_WRITE_TOKEN,\n 'public',\n )\n payload.logger.info(\n { access: effectiveAccess, file },\n '[restore] Restored seed media to Vercel Blob storage',\n )\n } else {\n const folderPath = path.join(process.cwd(), 'public/media')\n const publicPath = path.join(folderPath, file)\n await fs.mkdir(folderPath, { recursive: true })\n await fs.writeFile(publicPath, data)\n payload.logger.info({ file, publicPath }, '[restore] Restored seed media to public directory')\n }\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: `Restored seed media file ${file}`,\n })\n }\n }\n payload.logger.info({ count: files.length }, '[restore] Restored all seed media files')\n return files\n}\n"],"names":["EJSON","fs","path","resolveTarGzip","COLLECTION_FILE_NAME","putBackupBlobContent","readBackupBlobContentFlexible","getDb","updateBackupTask","restoreBackup","payload","downloadUrl","collectionBlacklist","mergeData","taskId","options","restoreArchiveMedia","blobToken","blobAccess","backupRead","t0","Date","now","urlBase","split","effectiveBlacklist","Array","from","Set","logger","info","blacklist","url","message","status","db","archiveBytes","pathname","token","res","fetch","ok","Error","Buffer","arrayBuffer","collections","endsWith","parse","toString","files","find","file","name","content","medias","filter","count","length","mediaResults","Promise","all","map","media","then","effectiveAccess","mismatched","r","warn","preferredAccess","forEach","result","debug","access","collectionNames","Object","keys","blacklisted","total","collectionName","includes","collection","collectionData","docs","indexes","uniqueIndexes","idx","unique","flatMap","key","deleteMany","bulkWrite","doc","updateOne","$or","_id","field","update","$set","upsert","modified","modifiedCount","upserted","upsertedCount","durationMs","restoreSeedMedia","readdir","join","process","cwd","data","readFile","env","BLOB_READ_WRITE_TOKEN","folderPath","publicPath","mkdir","recursive","writeFile"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/core/restore.ts"],"sourcesContent":["import type { Payload } from 'payload'\n\nimport { EJSON } from 'bson'\n\nimport type {\n RestoreArchiveKind,\n RestoreBackupResult,\n RestoreCollectionOutcome,\n RestoreMediaOutcome,\n} from './restoreResult'\n\nimport { resolveTarGzip } from './archive'\nimport { COLLECTION_FILE_NAME } from './backup'\nimport { readBackupArchiveBytes, resolveArchiveFileReference } from './backupArchiveRead'\nimport { type BackupBlobAccessLevel, putBackupBlobContent } from './backupBlobIO'\nimport { getDb } from './db'\nimport { updateBackupTask } from './taskProgress'\n\nexport type { RestoreBackupResult } from './restoreResult'\n\nexport interface RestoreBackupOptions {\n /** When set with S3, the archive is read server-side and list URLs may be stale. */\n archivePathname?: string\n /**\n * When set, the archive is loaded with the backup token (fetch + SDK fallback; works for\n * public and private blobs). Otherwise `downloadUrl` is fetched anonymously.\n */\n backupRead?: { pathname: string; token: string }\n /**\n * Preferred Vercel Blob access for re-uploaded media files. When the store rejects the\n * preferred level (e.g. a private-only store rejects `public`), the other level is tried\n * automatically. Defaults to `public`.\n */\n blobAccess?: BackupBlobAccessLevel\n /** Optional explicit Vercel Blob token used for archive media blob uploads. */\n blobToken?: string\n /**\n * When false, skip uploading media blobs from a tar.gz archive (Mongo `media` and other\n * collections still restore). Default true.\n */\n restoreArchiveMedia?: boolean\n}\n\nexport async function restoreBackup(\n payload: Payload,\n downloadUrl: string,\n collectionBlacklist: string[] = [],\n mergeData = false,\n taskId?: string,\n options?: RestoreBackupOptions,\n): Promise<RestoreBackupResult> {\n const restoreArchiveMedia = options?.restoreArchiveMedia !== false\n const blobToken = options?.blobToken\n const blobAccess: BackupBlobAccessLevel = options?.blobAccess ?? 'public'\n const backupRead = options?.backupRead\n const archivePathname = options?.archivePathname\n const t0 = Date.now()\n const archiveRef = resolveArchiveFileReference(downloadUrl, archivePathname)\n const collectionOutcomes: RestoreCollectionOutcome[] = []\n let mediaOutcome: null | RestoreMediaOutcome = null\n let archiveKind: RestoreArchiveKind\n\n // Progress is stored in `backup-tasks`. Restoring that collection from the file\n // would delete/replace the active task doc and break GET .../admin/task/:id polling.\n const effectiveBlacklist =\n taskId ?\n Array.from(new Set(['backup-tasks', ...collectionBlacklist]))\n : [...collectionBlacklist]\n\n payload.logger.info(\n { blacklist: effectiveBlacklist, mergeData, url: archiveRef },\n '[restore] Starting restore',\n )\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: 'Starting restore',\n status: 'running',\n })\n }\n\n const db = getDb(payload)\n const archiveBytes = await readBackupArchiveBytes(downloadUrl, {\n archivePathname,\n backupRead,\n blobAccess,\n blobToken,\n })\n let collections: Record<string, Record<string, unknown>[]> = {}\n\n if (archiveRef.endsWith('.json')) {\n archiveKind = 'json'\n payload.logger.info('[restore] Parsing JSON backup')\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: 'Parsing JSON backup',\n })\n }\n collections = EJSON.parse(archiveBytes.toString('utf8'))\n } else if (archiveRef.endsWith('.gz')) {\n archiveKind = 'tar-gzip'\n payload.logger.info('[restore] Extracting tar.gz backup')\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: 'Extracting tar.gz backup',\n })\n }\n const files = await resolveTarGzip(archiveBytes)\n collections = EJSON.parse(\n files.find((file) => file.name === COLLECTION_FILE_NAME)?.content?.toString() || '{}',\n )\n const medias =\n restoreArchiveMedia ? files.filter((file) => file.name !== COLLECTION_FILE_NAME) : []\n\n mediaOutcome = {\n accessFallbackCount: 0,\n attempted: medias.length,\n failed: [],\n restored: 0,\n skippedByOption: !restoreArchiveMedia,\n }\n\n if (!restoreArchiveMedia) {\n payload.logger.info(\n '[restore] Skipping archive media blob upload (restoreArchiveMedia=false)',\n )\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: 'Skipped media files from archive',\n })\n }\n } else {\n payload.logger.info(\n { count: medias.length },\n '[restore] Restoring media files to blob storage',\n )\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: `Restoring ${medias.length} media file${medias.length === 1 ? '' : 's'}`,\n })\n }\n\n const mediaResults = await Promise.all(\n medias.map(async (media) => {\n try {\n const effectiveAccess = await putBackupBlobContent(\n media.name,\n media.content,\n blobToken,\n blobAccess,\n )\n return {\n name: media.name,\n effectiveAccess,\n error: undefined as string | undefined,\n ok: true as const,\n }\n } catch (err) {\n const error = err instanceof Error ? err.message : String(err)\n payload.logger.warn(\n { err, filename: media.name },\n '[restore] Failed to upload media file to blob storage',\n )\n return { name: media.name, error, ok: false as const }\n }\n }),\n )\n\n for (const result of mediaResults) {\n if (result.ok) {\n mediaOutcome.restored += 1\n if (result.effectiveAccess !== blobAccess) {\n mediaOutcome.accessFallbackCount += 1\n }\n payload.logger.debug(\n { name: result.name, access: result.effectiveAccess },\n '[restore] Media file uploaded',\n )\n } else {\n mediaOutcome.failed.push({ name: result.name, error: result.error })\n }\n }\n\n if (mediaOutcome.accessFallbackCount > 0) {\n payload.logger.warn(\n { count: mediaOutcome.accessFallbackCount, preferredAccess: blobAccess },\n '[restore] Blob store rejected preferred access level for media; uploaded with fallback',\n )\n }\n }\n } else {\n throw new Error(`File type of backup ${downloadUrl} not supported`)\n }\n\n const collectionNames = Object.keys(collections)\n payload.logger.info(\n { blacklisted: effectiveBlacklist.length, total: collectionNames.length },\n '[restore] Restoring collections',\n )\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: `Restoring ${collectionNames.length} collection${collectionNames.length === 1 ? '' : 's'}`,\n })\n }\n\n for (const collectionName of collectionNames) {\n if (effectiveBlacklist.includes(collectionName)) {\n payload.logger.debug(\n { collection: collectionName },\n '[restore] Skipping blacklisted collection',\n )\n collectionOutcomes.push({ name: collectionName, kind: 'skipped', reason: 'blacklist' })\n continue\n }\n const collectionData = collections[collectionName]\n if (collectionData.length === 0) {\n collectionOutcomes.push({ name: collectionName, kind: 'skipped', reason: 'empty' })\n continue\n }\n\n payload.logger.info(\n { collection: collectionName, docs: collectionData.length, mergeData },\n '[restore] Restoring collection',\n )\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: `Restoring collection ${collectionName} (${collectionData.length} docs)`,\n })\n }\n\n try {\n const collection = db.collection(collectionName)\n const indexes = await collection.indexes()\n const uniqueIndexes = indexes\n .filter((idx) => idx.unique)\n .flatMap((idx) => Object.keys(idx.key))\n if (!mergeData) {\n await collection.deleteMany({})\n }\n const res = await collection.bulkWrite(\n collectionData.map((doc) => ({\n updateOne: {\n filter:\n uniqueIndexes.length > 0 ?\n {\n $or: [\n { _id: doc._id },\n ...uniqueIndexes.map((field) => ({ [field]: doc[field] })),\n ],\n }\n : { _id: doc._id },\n update: { $set: doc },\n upsert: true,\n },\n })),\n )\n payload.logger.debug(\n { collection: collectionName, modified: res.modifiedCount, upserted: res.upsertedCount },\n '[restore] Collection restored',\n )\n collectionOutcomes.push({\n name: collectionName,\n docCount: collectionData.length,\n kind: 'restored',\n })\n } catch (err) {\n const error = err instanceof Error ? err.message : String(err)\n payload.logger.error(\n { collection: collectionName, err },\n '[restore] Failed to restore collection',\n )\n collectionOutcomes.push({ name: collectionName, error, kind: 'failed' })\n }\n }\n\n const durationMs = Date.now() - t0\n const result: RestoreBackupResult = {\n archiveKind,\n collections: collectionOutcomes,\n durationMs,\n media: mediaOutcome,\n }\n\n payload.logger.info({ durationMs, result }, '[restore] Restore complete')\n return result\n}\n"],"names":["EJSON","resolveTarGzip","COLLECTION_FILE_NAME","readBackupArchiveBytes","resolveArchiveFileReference","putBackupBlobContent","getDb","updateBackupTask","restoreBackup","payload","downloadUrl","collectionBlacklist","mergeData","taskId","options","restoreArchiveMedia","blobToken","blobAccess","backupRead","archivePathname","t0","Date","now","archiveRef","collectionOutcomes","mediaOutcome","archiveKind","effectiveBlacklist","Array","from","Set","logger","info","blacklist","url","message","status","db","archiveBytes","collections","endsWith","parse","toString","files","find","file","name","content","medias","filter","accessFallbackCount","attempted","length","failed","restored","skippedByOption","count","mediaResults","Promise","all","map","media","effectiveAccess","error","undefined","ok","err","Error","String","warn","filename","result","debug","access","push","preferredAccess","collectionNames","Object","keys","blacklisted","total","collectionName","includes","collection","kind","reason","collectionData","docs","indexes","uniqueIndexes","idx","unique","flatMap","key","deleteMany","res","bulkWrite","doc","updateOne","$or","_id","field","update","$set","upsert","modified","modifiedCount","upserted","upsertedCount","docCount","durationMs"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import type { Payload } from 'payload';
2
+ import { type BackupBlobAccessLevel } from './backupBlobIO';
2
3
  export type RestorePreviewFileKind = 'json' | 'tar-gzip';
3
4
  export type RestorePreviewAdminHiddenReason = 'collection-config' | 'version-history';
4
5
  export type RestorePreviewGroup = {
@@ -30,9 +31,14 @@ export type RestorePreviewResponse = {
30
31
  * (hidden config, unknown mongo name, etc.).
31
32
  */
32
33
  export declare function navVisibleCollectionOrderIndex(payload: Payload, groupId: string): null | number;
33
- export declare function loadRestoreBackupIndex(downloadUrl: string, readAuth?: {
34
- pathname: string;
35
- token: string;
34
+ export declare function loadRestoreBackupIndex(downloadUrl: string, options?: {
35
+ archivePathname?: string;
36
+ blobAccess?: BackupBlobAccessLevel;
37
+ blobToken?: string;
38
+ readAuth?: {
39
+ pathname: string;
40
+ token: string;
41
+ };
36
42
  }): Promise<{
37
43
  byName: Record<string, unknown[]>;
38
44
  fileKind: RestorePreviewFileKind;
@@ -56,9 +62,12 @@ export declare function buildRestorePreviewGroups(payload: Payload, parsed: {
56
62
  preferredLocales?: string[];
57
63
  }): RestorePreviewResponse;
58
64
  export declare function getRestorePreviewForAdminRestore(payload: Payload, downloadUrl: string, options?: {
65
+ archivePathname?: string;
59
66
  backupRead?: {
60
67
  pathname: string;
61
68
  token: string;
62
69
  };
70
+ blobAccess?: BackupBlobAccessLevel;
71
+ blobToken?: string;
63
72
  preferredLocales?: string[];
64
73
  }): Promise<RestorePreviewResponse>;
@@ -1,7 +1,7 @@
1
1
  import { EJSON } from 'bson';
2
2
  import { resolveTarGzip } from './archive';
3
3
  import { COLLECTION_FILE_NAME } from './backup';
4
- import { readBackupBlobContentFlexible } from './backupBlobIO';
4
+ import { readBackupArchiveBytes, resolveArchiveFileReference } from './backupArchiveRead';
5
5
  /** Mongo collection names that invalidate the current admin session when replaced. */ const AUTH_SESSION_MONGO_NAMES = new Set([
6
6
  'payload-preferences',
7
7
  'roles',
@@ -101,22 +101,21 @@ function buildDisplayTitle(payload, groupSlug, preferredLocales) {
101
101
  const label = pickLocalizedLabel(col?.labels, preferredLocales) || humanizeSlug(groupSlug);
102
102
  return `${label} (${groupSlug})`;
103
103
  }
104
- export async function loadRestoreBackupIndex(downloadUrl, readAuth) {
105
- const urlBase = downloadUrl.split('?')?.[0];
106
- const bytes = readAuth ? await readBackupBlobContentFlexible(readAuth.pathname, downloadUrl, readAuth.token) : await (async ()=>{
107
- const res = await fetch(downloadUrl);
108
- if (!res.ok) {
109
- throw new Error(`Failed to download backup (${res.status})`);
110
- }
111
- return Buffer.from(await res.arrayBuffer());
112
- })();
104
+ export async function loadRestoreBackupIndex(downloadUrl, options = {}) {
105
+ const archiveRef = resolveArchiveFileReference(downloadUrl, options.archivePathname);
106
+ const bytes = await readBackupArchiveBytes(downloadUrl, {
107
+ archivePathname: options.archivePathname,
108
+ backupRead: options.readAuth,
109
+ blobAccess: options.blobAccess,
110
+ blobToken: options.blobToken
111
+ });
113
112
  let byName = {};
114
113
  let mediaBlobCount = 0;
115
114
  let fileKind;
116
- if (urlBase?.endsWith('.json')) {
115
+ if (archiveRef.endsWith('.json')) {
117
116
  fileKind = 'json';
118
117
  byName = EJSON.parse(bytes.toString('utf8'));
119
- } else if (urlBase?.endsWith('.gz')) {
118
+ } else if (archiveRef.endsWith('.gz')) {
120
119
  fileKind = 'tar-gzip';
121
120
  const files = await resolveTarGzip(bytes);
122
121
  byName = EJSON.parse(files.find((f)=>f.name === COLLECTION_FILE_NAME)?.content?.toString() || '{}');
@@ -259,7 +258,12 @@ export function buildRestorePreviewGroups(payload, parsed, options = {}) {
259
258
  };
260
259
  }
261
260
  export async function getRestorePreviewForAdminRestore(payload, downloadUrl, options = {}) {
262
- const parsed = await loadRestoreBackupIndex(downloadUrl, options.backupRead);
261
+ const parsed = await loadRestoreBackupIndex(downloadUrl, {
262
+ archivePathname: options.archivePathname,
263
+ blobAccess: options.blobAccess,
264
+ blobToken: options.blobToken,
265
+ readAuth: options.backupRead
266
+ });
263
267
  return buildRestorePreviewGroups(payload, parsed, options);
264
268
  }
265
269
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/restorePreview.ts"],"sourcesContent":["import type { Payload } from 'payload'\n\nimport { EJSON } from 'bson'\n\nimport { resolveTarGzip } from './archive'\nimport { COLLECTION_FILE_NAME } from './backup'\nimport { readBackupBlobContentFlexible } from './backupBlobIO'\n\n/** Mongo collection names that invalidate the current admin session when replaced. */\nconst AUTH_SESSION_MONGO_NAMES = new Set(['payload-preferences', 'roles', 'sessions', 'users'])\n\nconst MONGO_VERSIONS = /^_(.+)_versions$/i\n\nexport type RestorePreviewFileKind = 'json' | 'tar-gzip'\n\nexport type RestorePreviewAdminHiddenReason = 'collection-config' | 'version-history'\n\nexport type RestorePreviewGroup = {\n adminHidden: boolean\n adminHiddenReasons: RestorePreviewAdminHiddenReason[]\n affectsAuthSession: boolean\n /** e.g. \"Seiten (pages)\" */\n displayTitle: string\n /** Stable id for UI + skip payload (Payload collection slug when known, else mongo name). */\n groupId: string\n main?: { docCount: number; mongoName: string }\n /** All Mongo names restored when this group is checked. */\n mongoNames: string[]\n versions?: { docCount: number; mongoName: string }\n}\n\nexport type RestorePreviewResponse = {\n fileKind: RestorePreviewFileKind\n groups: RestorePreviewGroup[]\n mediaBlobCount: number\n}\n\nfunction docCount(byName: Record<string, unknown[]>, name: string): number {\n const docs = byName[name]\n return Array.isArray(docs) ? docs.length : 0\n}\n\nfunction isVersionsMongoName(name: string): boolean {\n return MONGO_VERSIONS.test(name)\n}\n\nfunction resolveAdminHidden(\n payload: Payload,\n mongoName: string,\n): { adminHidden: boolean; adminHiddenReason: null | RestorePreviewAdminHiddenReason } {\n const col = payload.config.collections.find((c) => {\n const dbName = (c as { dbName?: string }).dbName ?? c.slug\n return dbName === mongoName || c.slug === mongoName\n })\n if (col?.admin?.hidden) {\n return { adminHidden: true, adminHiddenReason: 'collection-config' }\n }\n if (isVersionsMongoName(mongoName)) {\n return { adminHidden: true, adminHiddenReason: 'version-history' }\n }\n return { adminHidden: false, adminHiddenReason: null }\n}\n\nfunction pickLocalizedLabel(labels: unknown, preferredLocales: string[]): string | undefined {\n if (!labels || typeof labels !== 'object') {\n return undefined\n }\n const l = labels as { plural?: unknown; singular?: unknown }\n\n const pickField = (field: unknown): string | undefined => {\n if (typeof field === 'string') {\n return field\n }\n if (field && typeof field === 'object') {\n const map = field as Record<string, string>\n for (const loc of preferredLocales) {\n const v = map[loc]\n if (v) {\n return v\n }\n }\n const first = Object.values(map).find(Boolean)\n return first\n }\n return undefined\n }\n\n return pickField(l.plural) || pickField(l.singular)\n}\n\nfunction humanizeSlug(slug: string): string {\n return slug\n .split('-')\n .map((w) => (w ? w.charAt(0).toUpperCase() + w.slice(1) : ''))\n .join(' ')\n}\n\n/** Collections shown in the default Payload admin sidebar: config order, skipping `admin.hidden`. */\nfunction getNavVisibleCollections(payload: Payload) {\n return payload.config.collections.filter((c) => c.admin?.hidden !== true)\n}\n\n/**\n * Index in sidebar order, or `null` if the group is not a visible nav collection\n * (hidden config, unknown mongo name, etc.).\n */\nexport function navVisibleCollectionOrderIndex(payload: Payload, groupId: string): null | number {\n const visible = getNavVisibleCollections(payload)\n const idx = visible.findIndex((c) => {\n const dbName = (c as { dbName?: string }).dbName ?? c.slug\n return c.slug === groupId || dbName === groupId\n })\n return idx === -1 ? null : idx\n}\n\nfunction sortPreviewGroupsLikeAdminNav(\n payload: Payload,\n groups: RestorePreviewGroup[],\n preferredLocales: string[],\n): void {\n const locale = preferredLocales[0] ?? 'en'\n groups.sort((a, b) => {\n const ia = navVisibleCollectionOrderIndex(payload, a.groupId)\n const ib = navVisibleCollectionOrderIndex(payload, b.groupId)\n if (ia !== null && ib !== null && ia !== ib) {\n return ia - ib\n }\n if (ia !== null && ib === null) {\n return -1\n }\n if (ia === null && ib !== null) {\n return 1\n }\n return a.displayTitle.localeCompare(b.displayTitle, locale)\n })\n}\n\nfunction buildDisplayTitle(\n payload: Payload,\n groupSlug: string,\n preferredLocales: string[],\n): string {\n const col = payload.config.collections.find((c) => c.slug === groupSlug)\n const label = pickLocalizedLabel(col?.labels, preferredLocales) || humanizeSlug(groupSlug)\n return `${label} (${groupSlug})`\n}\n\nexport async function loadRestoreBackupIndex(\n downloadUrl: string,\n readAuth?: { pathname: string; token: string },\n): Promise<{\n byName: Record<string, unknown[]>\n fileKind: RestorePreviewFileKind\n mediaBlobCount: number\n}> {\n const urlBase = downloadUrl.split('?')?.[0]\n const bytes =\n readAuth ?\n await readBackupBlobContentFlexible(readAuth.pathname, downloadUrl, readAuth.token)\n : await (async () => {\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\n let byName: Record<string, unknown[]> = {}\n let mediaBlobCount = 0\n let fileKind: RestorePreviewFileKind\n\n if (urlBase?.endsWith('.json')) {\n fileKind = 'json'\n byName = EJSON.parse(bytes.toString('utf8')) as Record<string, unknown[]>\n } else if (urlBase?.endsWith('.gz')) {\n fileKind = 'tar-gzip'\n const files = await resolveTarGzip(bytes)\n byName = EJSON.parse(\n files.find((f) => f.name === COLLECTION_FILE_NAME)?.content?.toString() || '{}',\n ) as Record<string, unknown[]>\n mediaBlobCount = files.filter((f) => f.name !== COLLECTION_FILE_NAME).length\n } else {\n throw new Error('Unsupported backup file type (expected .json or .gz)')\n }\n\n return { byName, fileKind, mediaBlobCount }\n}\n\ntype GroupAgg = {\n groupId: string\n main?: { docCount: number; mongoName: string }\n versions?: { docCount: number; mongoName: string }\n}\n\nfunction rebuildMongoNames(g: GroupAgg): string[] {\n return [g.main?.mongoName, g.versions?.mongoName].filter(Boolean) as string[]\n}\n\nfunction countsFromByName(byName: Record<string, unknown[]>): Record<string, number> {\n const out: Record<string, number> = {}\n for (const k of Object.keys(byName)) {\n out[k] = docCount(byName, k)\n }\n return out\n}\n\n/** Ensures every configured Payload collection appears (0 if absent from backup JSON), like live-DB preview. */\nfunction mergeConfiguredCollectionZeros(\n payload: Payload,\n fileCounts: Record<string, number>,\n excludeMongo: Set<string>,\n): Record<string, number> {\n const merged = { ...fileCounts }\n for (const col of payload.config.collections) {\n const mongo = (col as { dbName?: string }).dbName ?? col.slug\n if (excludeMongo.has(mongo)) {\n continue\n }\n if (!(mongo in merged)) {\n merged[mongo] = 0\n }\n }\n return merged\n}\n\n/** Group Mongo collection names + doc counts (live DB or backup file) for admin UI lists. */\nexport function buildCollectionPreviewGroups(\n payload: Payload,\n countsByMongo: Record<string, number>,\n options: {\n excludeMongo?: Set<string>\n /** When true, list collections with 0 documents (e.g. manual backup UI). Default false. */\n includeEmptyCollections?: boolean\n preferredLocales?: string[]\n /** When true, order like Payload admin sidebar; unknown / hidden-only groups last by title. */\n sortLikeAdminNav?: boolean\n } = {},\n): RestorePreviewGroup[] {\n const preferredLocales =\n options.preferredLocales?.length ? options.preferredLocales : ['de', 'en']\n const excludeMongo = options.excludeMongo ?? new Set(['backup-tasks'])\n const includeEmptyCollections = options.includeEmptyCollections ?? false\n const sortLikeAdminNav = options.sortLikeAdminNav ?? false\n\n const aggs = new Map<string, GroupAgg>()\n\n for (const [mongoName, count] of Object.entries(countsByMongo)) {\n if (excludeMongo.has(mongoName)) {\n continue\n }\n if (!includeEmptyCollections && count === 0) {\n continue\n }\n\n const versionMatch = mongoName.match(MONGO_VERSIONS)\n if (versionMatch) {\n const groupId = versionMatch[1]\n const prev = aggs.get(groupId) ?? { groupId }\n aggs.set(groupId, {\n ...prev,\n groupId,\n versions: { docCount: count, mongoName },\n })\n } else {\n const groupId = mongoName\n const prev = aggs.get(groupId) ?? { groupId }\n aggs.set(groupId, {\n ...prev,\n groupId,\n main: { docCount: count, mongoName },\n })\n }\n }\n\n const groups: RestorePreviewGroup[] = [...aggs.values()].map((agg) => {\n const mongoNames = rebuildMongoNames(agg)\n const reasons = new Set<RestorePreviewAdminHiddenReason>()\n let adminHidden = false\n for (const mn of mongoNames) {\n const r = resolveAdminHidden(payload, mn)\n if (r.adminHidden && r.adminHiddenReason) {\n adminHidden = true\n reasons.add(r.adminHiddenReason)\n }\n }\n const affectsAuthSession = mongoNames.some((n) => AUTH_SESSION_MONGO_NAMES.has(n))\n\n return {\n adminHidden,\n adminHiddenReasons: [...reasons],\n affectsAuthSession,\n displayTitle: buildDisplayTitle(payload, agg.groupId, preferredLocales),\n groupId: agg.groupId,\n main: agg.main,\n mongoNames,\n versions: agg.versions,\n }\n })\n\n if (sortLikeAdminNav) {\n sortPreviewGroupsLikeAdminNav(payload, groups, preferredLocales)\n } else {\n const locale = preferredLocales[0] ?? 'en'\n groups.sort((a, b) => a.displayTitle.localeCompare(b.displayTitle, locale))\n }\n return groups\n}\n\nexport function buildRestorePreviewGroups(\n payload: Payload,\n parsed: {\n byName: Record<string, unknown[]>\n fileKind: RestorePreviewFileKind\n mediaBlobCount: number\n },\n options: { excludeMongo?: Set<string>; preferredLocales?: string[] } = {},\n): RestorePreviewResponse {\n const excludeMongo = options.excludeMongo ?? new Set(['backup-tasks'])\n const rawCounts = countsFromByName(parsed.byName)\n const counts = mergeConfiguredCollectionZeros(payload, rawCounts, excludeMongo)\n const groups = buildCollectionPreviewGroups(payload, counts, {\n excludeMongo,\n includeEmptyCollections: true,\n preferredLocales: options.preferredLocales,\n sortLikeAdminNav: true,\n })\n\n return {\n fileKind: parsed.fileKind,\n groups,\n mediaBlobCount: parsed.mediaBlobCount,\n }\n}\n\nexport async function getRestorePreviewForAdminRestore(\n payload: Payload,\n downloadUrl: string,\n options: {\n backupRead?: { pathname: string; token: string }\n preferredLocales?: string[]\n } = {},\n): Promise<RestorePreviewResponse> {\n const parsed = await loadRestoreBackupIndex(downloadUrl, options.backupRead)\n return buildRestorePreviewGroups(payload, parsed, options)\n}\n"],"names":["EJSON","resolveTarGzip","COLLECTION_FILE_NAME","readBackupBlobContentFlexible","AUTH_SESSION_MONGO_NAMES","Set","MONGO_VERSIONS","docCount","byName","name","docs","Array","isArray","length","isVersionsMongoName","test","resolveAdminHidden","payload","mongoName","col","config","collections","find","c","dbName","slug","admin","hidden","adminHidden","adminHiddenReason","pickLocalizedLabel","labels","preferredLocales","undefined","l","pickField","field","map","loc","v","first","Object","values","Boolean","plural","singular","humanizeSlug","split","w","charAt","toUpperCase","slice","join","getNavVisibleCollections","filter","navVisibleCollectionOrderIndex","groupId","visible","idx","findIndex","sortPreviewGroupsLikeAdminNav","groups","locale","sort","a","b","ia","ib","displayTitle","localeCompare","buildDisplayTitle","groupSlug","label","loadRestoreBackupIndex","downloadUrl","readAuth","urlBase","bytes","pathname","token","res","fetch","ok","Error","status","Buffer","from","arrayBuffer","mediaBlobCount","fileKind","endsWith","parse","toString","files","f","content","rebuildMongoNames","g","main","versions","countsFromByName","out","k","keys","mergeConfiguredCollectionZeros","fileCounts","excludeMongo","merged","mongo","has","buildCollectionPreviewGroups","countsByMongo","options","includeEmptyCollections","sortLikeAdminNav","aggs","Map","count","entries","versionMatch","match","prev","get","set","agg","mongoNames","reasons","mn","r","add","affectsAuthSession","some","n","adminHiddenReasons","buildRestorePreviewGroups","parsed","rawCounts","counts","getRestorePreviewForAdminRestore","backupRead"],"mappings":"AAEA,SAASA,KAAK,QAAQ,OAAM;AAE5B,SAASC,cAAc,QAAQ,YAAW;AAC1C,SAASC,oBAAoB,QAAQ,WAAU;AAC/C,SAASC,6BAA6B,QAAQ,iBAAgB;AAE9D,oFAAoF,GACpF,MAAMC,2BAA2B,IAAIC,IAAI;IAAC;IAAuB;IAAS;IAAY;CAAQ;AAE9F,MAAMC,iBAAiB;AA0BvB,SAASC,SAASC,MAAiC,EAAEC,IAAY;IAC/D,MAAMC,OAAOF,MAAM,CAACC,KAAK;IACzB,OAAOE,MAAMC,OAAO,CAACF,QAAQA,KAAKG,MAAM,GAAG;AAC7C;AAEA,SAASC,oBAAoBL,IAAY;IACvC,OAAOH,eAAeS,IAAI,CAACN;AAC7B;AAEA,SAASO,mBACPC,OAAgB,EAChBC,SAAiB;IAEjB,MAAMC,MAAMF,QAAQG,MAAM,CAACC,WAAW,CAACC,IAAI,CAAC,CAACC;QAC3C,MAAMC,SAAS,AAACD,EAA0BC,MAAM,IAAID,EAAEE,IAAI;QAC1D,OAAOD,WAAWN,aAAaK,EAAEE,IAAI,KAAKP;IAC5C;IACA,IAAIC,KAAKO,OAAOC,QAAQ;QACtB,OAAO;YAAEC,aAAa;YAAMC,mBAAmB;QAAoB;IACrE;IACA,IAAIf,oBAAoBI,YAAY;QAClC,OAAO;YAAEU,aAAa;YAAMC,mBAAmB;QAAkB;IACnE;IACA,OAAO;QAAED,aAAa;QAAOC,mBAAmB;IAAK;AACvD;AAEA,SAASC,mBAAmBC,MAAe,EAAEC,gBAA0B;IACrE,IAAI,CAACD,UAAU,OAAOA,WAAW,UAAU;QACzC,OAAOE;IACT;IACA,MAAMC,IAAIH;IAEV,MAAMI,YAAY,CAACC;QACjB,IAAI,OAAOA,UAAU,UAAU;YAC7B,OAAOA;QACT;QACA,IAAIA,SAAS,OAAOA,UAAU,UAAU;YACtC,MAAMC,MAAMD;YACZ,KAAK,MAAME,OAAON,iBAAkB;gBAClC,MAAMO,IAAIF,GAAG,CAACC,IAAI;gBAClB,IAAIC,GAAG;oBACL,OAAOA;gBACT;YACF;YACA,MAAMC,QAAQC,OAAOC,MAAM,CAACL,KAAKf,IAAI,CAACqB;YACtC,OAAOH;QACT;QACA,OAAOP;IACT;IAEA,OAAOE,UAAUD,EAAEU,MAAM,KAAKT,UAAUD,EAAEW,QAAQ;AACpD;AAEA,SAASC,aAAarB,IAAY;IAChC,OAAOA,KACJsB,KAAK,CAAC,KACNV,GAAG,CAAC,CAACW,IAAOA,IAAIA,EAAEC,MAAM,CAAC,GAAGC,WAAW,KAAKF,EAAEG,KAAK,CAAC,KAAK,IACzDC,IAAI,CAAC;AACV;AAEA,mGAAmG,GACnG,SAASC,yBAAyBpC,OAAgB;IAChD,OAAOA,QAAQG,MAAM,CAACC,WAAW,CAACiC,MAAM,CAAC,CAAC/B,IAAMA,EAAEG,KAAK,EAAEC,WAAW;AACtE;AAEA;;;CAGC,GACD,OAAO,SAAS4B,+BAA+BtC,OAAgB,EAAEuC,OAAe;IAC9E,MAAMC,UAAUJ,yBAAyBpC;IACzC,MAAMyC,MAAMD,QAAQE,SAAS,CAAC,CAACpC;QAC7B,MAAMC,SAAS,AAACD,EAA0BC,MAAM,IAAID,EAAEE,IAAI;QAC1D,OAAOF,EAAEE,IAAI,KAAK+B,WAAWhC,WAAWgC;IAC1C;IACA,OAAOE,QAAQ,CAAC,IAAI,OAAOA;AAC7B;AAEA,SAASE,8BACP3C,OAAgB,EAChB4C,MAA6B,EAC7B7B,gBAA0B;IAE1B,MAAM8B,SAAS9B,gBAAgB,CAAC,EAAE,IAAI;IACtC6B,OAAOE,IAAI,CAAC,CAACC,GAAGC;QACd,MAAMC,KAAKX,+BAA+BtC,SAAS+C,EAAER,OAAO;QAC5D,MAAMW,KAAKZ,+BAA+BtC,SAASgD,EAAET,OAAO;QAC5D,IAAIU,OAAO,QAAQC,OAAO,QAAQD,OAAOC,IAAI;YAC3C,OAAOD,KAAKC;QACd;QACA,IAAID,OAAO,QAAQC,OAAO,MAAM;YAC9B,OAAO,CAAC;QACV;QACA,IAAID,OAAO,QAAQC,OAAO,MAAM;YAC9B,OAAO;QACT;QACA,OAAOH,EAAEI,YAAY,CAACC,aAAa,CAACJ,EAAEG,YAAY,EAAEN;IACtD;AACF;AAEA,SAASQ,kBACPrD,OAAgB,EAChBsD,SAAiB,EACjBvC,gBAA0B;IAE1B,MAAMb,MAAMF,QAAQG,MAAM,CAACC,WAAW,CAACC,IAAI,CAAC,CAACC,IAAMA,EAAEE,IAAI,KAAK8C;IAC9D,MAAMC,QAAQ1C,mBAAmBX,KAAKY,QAAQC,qBAAqBc,aAAayB;IAChF,OAAO,GAAGC,MAAM,EAAE,EAAED,UAAU,CAAC,CAAC;AAClC;AAEA,OAAO,eAAeE,uBACpBC,WAAmB,EACnBC,QAA8C;IAM9C,MAAMC,UAAUF,YAAY3B,KAAK,CAAC,MAAM,CAAC,EAAE;IAC3C,MAAM8B,QACJF,WACE,MAAMxE,8BAA8BwE,SAASG,QAAQ,EAAEJ,aAAaC,SAASI,KAAK,IAClF,MAAM,AAAC,CAAA;QACL,MAAMC,MAAM,MAAMC,MAAMP;QACxB,IAAI,CAACM,IAAIE,EAAE,EAAE;YACX,MAAM,IAAIC,MAAM,CAAC,2BAA2B,EAAEH,IAAII,MAAM,CAAC,CAAC,CAAC;QAC7D;QACA,OAAOC,OAAOC,IAAI,CAAC,MAAMN,IAAIO,WAAW;IAC1C,CAAA;IAEJ,IAAI/E,SAAoC,CAAC;IACzC,IAAIgF,iBAAiB;IACrB,IAAIC;IAEJ,IAAIb,SAASc,SAAS,UAAU;QAC9BD,WAAW;QACXjF,SAASR,MAAM2F,KAAK,CAACd,MAAMe,QAAQ,CAAC;IACtC,OAAO,IAAIhB,SAASc,SAAS,QAAQ;QACnCD,WAAW;QACX,MAAMI,QAAQ,MAAM5F,eAAe4E;QACnCrE,SAASR,MAAM2F,KAAK,CAClBE,MAAMvE,IAAI,CAAC,CAACwE,IAAMA,EAAErF,IAAI,KAAKP,uBAAuB6F,SAASH,cAAc;QAE7EJ,iBAAiBK,MAAMvC,MAAM,CAAC,CAACwC,IAAMA,EAAErF,IAAI,KAAKP,sBAAsBW,MAAM;IAC9E,OAAO;QACL,MAAM,IAAIsE,MAAM;IAClB;IAEA,OAAO;QAAE3E;QAAQiF;QAAUD;IAAe;AAC5C;AAQA,SAASQ,kBAAkBC,CAAW;IACpC,OAAO;QAACA,EAAEC,IAAI,EAAEhF;QAAW+E,EAAEE,QAAQ,EAAEjF;KAAU,CAACoC,MAAM,CAACX;AAC3D;AAEA,SAASyD,iBAAiB5F,MAAiC;IACzD,MAAM6F,MAA8B,CAAC;IACrC,KAAK,MAAMC,KAAK7D,OAAO8D,IAAI,CAAC/F,QAAS;QACnC6F,GAAG,CAACC,EAAE,GAAG/F,SAASC,QAAQ8F;IAC5B;IACA,OAAOD;AACT;AAEA,8GAA8G,GAC9G,SAASG,+BACPvF,OAAgB,EAChBwF,UAAkC,EAClCC,YAAyB;IAEzB,MAAMC,SAAS;QAAE,GAAGF,UAAU;IAAC;IAC/B,KAAK,MAAMtF,OAAOF,QAAQG,MAAM,CAACC,WAAW,CAAE;QAC5C,MAAMuF,QAAQ,AAACzF,IAA4BK,MAAM,IAAIL,IAAIM,IAAI;QAC7D,IAAIiF,aAAaG,GAAG,CAACD,QAAQ;YAC3B;QACF;QACA,IAAI,CAAEA,CAAAA,SAASD,MAAK,GAAI;YACtBA,MAAM,CAACC,MAAM,GAAG;QAClB;IACF;IACA,OAAOD;AACT;AAEA,2FAA2F,GAC3F,OAAO,SAASG,6BACd7F,OAAgB,EAChB8F,aAAqC,EACrCC,UAOI,CAAC,CAAC;IAEN,MAAMhF,mBACJgF,QAAQhF,gBAAgB,EAAEnB,SAASmG,QAAQhF,gBAAgB,GAAG;QAAC;QAAM;KAAK;IAC5E,MAAM0E,eAAeM,QAAQN,YAAY,IAAI,IAAIrG,IAAI;QAAC;KAAe;IACrE,MAAM4G,0BAA0BD,QAAQC,uBAAuB,IAAI;IACnE,MAAMC,mBAAmBF,QAAQE,gBAAgB,IAAI;IAErD,MAAMC,OAAO,IAAIC;IAEjB,KAAK,MAAM,CAAClG,WAAWmG,MAAM,IAAI5E,OAAO6E,OAAO,CAACP,eAAgB;QAC9D,IAAIL,aAAaG,GAAG,CAAC3F,YAAY;YAC/B;QACF;QACA,IAAI,CAAC+F,2BAA2BI,UAAU,GAAG;YAC3C;QACF;QAEA,MAAME,eAAerG,UAAUsG,KAAK,CAAClH;QACrC,IAAIiH,cAAc;YAChB,MAAM/D,UAAU+D,YAAY,CAAC,EAAE;YAC/B,MAAME,OAAON,KAAKO,GAAG,CAAClE,YAAY;gBAAEA;YAAQ;YAC5C2D,KAAKQ,GAAG,CAACnE,SAAS;gBAChB,GAAGiE,IAAI;gBACPjE;gBACA2C,UAAU;oBAAE5F,UAAU8G;oBAAOnG;gBAAU;YACzC;QACF,OAAO;YACL,MAAMsC,UAAUtC;YAChB,MAAMuG,OAAON,KAAKO,GAAG,CAAClE,YAAY;gBAAEA;YAAQ;YAC5C2D,KAAKQ,GAAG,CAACnE,SAAS;gBAChB,GAAGiE,IAAI;gBACPjE;gBACA0C,MAAM;oBAAE3F,UAAU8G;oBAAOnG;gBAAU;YACrC;QACF;IACF;IAEA,MAAM2C,SAAgC;WAAIsD,KAAKzE,MAAM;KAAG,CAACL,GAAG,CAAC,CAACuF;QAC5D,MAAMC,aAAa7B,kBAAkB4B;QACrC,MAAME,UAAU,IAAIzH;QACpB,IAAIuB,cAAc;QAClB,KAAK,MAAMmG,MAAMF,WAAY;YAC3B,MAAMG,IAAIhH,mBAAmBC,SAAS8G;YACtC,IAAIC,EAAEpG,WAAW,IAAIoG,EAAEnG,iBAAiB,EAAE;gBACxCD,cAAc;gBACdkG,QAAQG,GAAG,CAACD,EAAEnG,iBAAiB;YACjC;QACF;QACA,MAAMqG,qBAAqBL,WAAWM,IAAI,CAAC,CAACC,IAAMhI,yBAAyByG,GAAG,CAACuB;QAE/E,OAAO;YACLxG;YACAyG,oBAAoB;mBAAIP;aAAQ;YAChCI;YACA9D,cAAcE,kBAAkBrD,SAAS2G,IAAIpE,OAAO,EAAExB;YACtDwB,SAASoE,IAAIpE,OAAO;YACpB0C,MAAM0B,IAAI1B,IAAI;YACd2B;YACA1B,UAAUyB,IAAIzB,QAAQ;QACxB;IACF;IAEA,IAAIe,kBAAkB;QACpBtD,8BAA8B3C,SAAS4C,QAAQ7B;IACjD,OAAO;QACL,MAAM8B,SAAS9B,gBAAgB,CAAC,EAAE,IAAI;QACtC6B,OAAOE,IAAI,CAAC,CAACC,GAAGC,IAAMD,EAAEI,YAAY,CAACC,aAAa,CAACJ,EAAEG,YAAY,EAAEN;IACrE;IACA,OAAOD;AACT;AAEA,OAAO,SAASyE,0BACdrH,OAAgB,EAChBsH,MAIC,EACDvB,UAAuE,CAAC,CAAC;IAEzE,MAAMN,eAAeM,QAAQN,YAAY,IAAI,IAAIrG,IAAI;QAAC;KAAe;IACrE,MAAMmI,YAAYpC,iBAAiBmC,OAAO/H,MAAM;IAChD,MAAMiI,SAASjC,+BAA+BvF,SAASuH,WAAW9B;IAClE,MAAM7C,SAASiD,6BAA6B7F,SAASwH,QAAQ;QAC3D/B;QACAO,yBAAyB;QACzBjF,kBAAkBgF,QAAQhF,gBAAgB;QAC1CkF,kBAAkB;IACpB;IAEA,OAAO;QACLzB,UAAU8C,OAAO9C,QAAQ;QACzB5B;QACA2B,gBAAgB+C,OAAO/C,cAAc;IACvC;AACF;AAEA,OAAO,eAAekD,iCACpBzH,OAAgB,EAChByD,WAAmB,EACnBsC,UAGI,CAAC,CAAC;IAEN,MAAMuB,SAAS,MAAM9D,uBAAuBC,aAAasC,QAAQ2B,UAAU;IAC3E,OAAOL,0BAA0BrH,SAASsH,QAAQvB;AACpD"}
1
+ {"version":3,"sources":["../../src/core/restorePreview.ts"],"sourcesContent":["import type { Payload } from 'payload'\n\nimport { EJSON } from 'bson'\n\nimport { resolveTarGzip } from './archive'\nimport { COLLECTION_FILE_NAME } from './backup'\nimport { readBackupArchiveBytes, resolveArchiveFileReference } from './backupArchiveRead'\nimport { type BackupBlobAccessLevel } from './backupBlobIO'\n\n/** Mongo collection names that invalidate the current admin session when replaced. */\nconst AUTH_SESSION_MONGO_NAMES = new Set(['payload-preferences', 'roles', 'sessions', 'users'])\n\nconst MONGO_VERSIONS = /^_(.+)_versions$/i\n\nexport type RestorePreviewFileKind = 'json' | 'tar-gzip'\n\nexport type RestorePreviewAdminHiddenReason = 'collection-config' | 'version-history'\n\nexport type RestorePreviewGroup = {\n adminHidden: boolean\n adminHiddenReasons: RestorePreviewAdminHiddenReason[]\n affectsAuthSession: boolean\n /** e.g. \"Seiten (pages)\" */\n displayTitle: string\n /** Stable id for UI + skip payload (Payload collection slug when known, else mongo name). */\n groupId: string\n main?: { docCount: number; mongoName: string }\n /** All Mongo names restored when this group is checked. */\n mongoNames: string[]\n versions?: { docCount: number; mongoName: string }\n}\n\nexport type RestorePreviewResponse = {\n fileKind: RestorePreviewFileKind\n groups: RestorePreviewGroup[]\n mediaBlobCount: number\n}\n\nfunction docCount(byName: Record<string, unknown[]>, name: string): number {\n const docs = byName[name]\n return Array.isArray(docs) ? docs.length : 0\n}\n\nfunction isVersionsMongoName(name: string): boolean {\n return MONGO_VERSIONS.test(name)\n}\n\nfunction resolveAdminHidden(\n payload: Payload,\n mongoName: string,\n): { adminHidden: boolean; adminHiddenReason: null | RestorePreviewAdminHiddenReason } {\n const col = payload.config.collections.find((c) => {\n const dbName = (c as { dbName?: string }).dbName ?? c.slug\n return dbName === mongoName || c.slug === mongoName\n })\n if (col?.admin?.hidden) {\n return { adminHidden: true, adminHiddenReason: 'collection-config' }\n }\n if (isVersionsMongoName(mongoName)) {\n return { adminHidden: true, adminHiddenReason: 'version-history' }\n }\n return { adminHidden: false, adminHiddenReason: null }\n}\n\nfunction pickLocalizedLabel(labels: unknown, preferredLocales: string[]): string | undefined {\n if (!labels || typeof labels !== 'object') {\n return undefined\n }\n const l = labels as { plural?: unknown; singular?: unknown }\n\n const pickField = (field: unknown): string | undefined => {\n if (typeof field === 'string') {\n return field\n }\n if (field && typeof field === 'object') {\n const map = field as Record<string, string>\n for (const loc of preferredLocales) {\n const v = map[loc]\n if (v) {\n return v\n }\n }\n const first = Object.values(map).find(Boolean)\n return first\n }\n return undefined\n }\n\n return pickField(l.plural) || pickField(l.singular)\n}\n\nfunction humanizeSlug(slug: string): string {\n return slug\n .split('-')\n .map((w) => (w ? w.charAt(0).toUpperCase() + w.slice(1) : ''))\n .join(' ')\n}\n\n/** Collections shown in the default Payload admin sidebar: config order, skipping `admin.hidden`. */\nfunction getNavVisibleCollections(payload: Payload) {\n return payload.config.collections.filter((c) => c.admin?.hidden !== true)\n}\n\n/**\n * Index in sidebar order, or `null` if the group is not a visible nav collection\n * (hidden config, unknown mongo name, etc.).\n */\nexport function navVisibleCollectionOrderIndex(payload: Payload, groupId: string): null | number {\n const visible = getNavVisibleCollections(payload)\n const idx = visible.findIndex((c) => {\n const dbName = (c as { dbName?: string }).dbName ?? c.slug\n return c.slug === groupId || dbName === groupId\n })\n return idx === -1 ? null : idx\n}\n\nfunction sortPreviewGroupsLikeAdminNav(\n payload: Payload,\n groups: RestorePreviewGroup[],\n preferredLocales: string[],\n): void {\n const locale = preferredLocales[0] ?? 'en'\n groups.sort((a, b) => {\n const ia = navVisibleCollectionOrderIndex(payload, a.groupId)\n const ib = navVisibleCollectionOrderIndex(payload, b.groupId)\n if (ia !== null && ib !== null && ia !== ib) {\n return ia - ib\n }\n if (ia !== null && ib === null) {\n return -1\n }\n if (ia === null && ib !== null) {\n return 1\n }\n return a.displayTitle.localeCompare(b.displayTitle, locale)\n })\n}\n\nfunction buildDisplayTitle(\n payload: Payload,\n groupSlug: string,\n preferredLocales: string[],\n): string {\n const col = payload.config.collections.find((c) => c.slug === groupSlug)\n const label = pickLocalizedLabel(col?.labels, preferredLocales) || humanizeSlug(groupSlug)\n return `${label} (${groupSlug})`\n}\n\nexport async function loadRestoreBackupIndex(\n downloadUrl: string,\n options: {\n archivePathname?: string\n blobAccess?: BackupBlobAccessLevel\n blobToken?: string\n readAuth?: { pathname: string; token: string }\n } = {},\n): Promise<{\n byName: Record<string, unknown[]>\n fileKind: RestorePreviewFileKind\n mediaBlobCount: number\n}> {\n const archiveRef = resolveArchiveFileReference(downloadUrl, options.archivePathname)\n const bytes = await readBackupArchiveBytes(downloadUrl, {\n archivePathname: options.archivePathname,\n backupRead: options.readAuth,\n blobAccess: options.blobAccess,\n blobToken: options.blobToken,\n })\n\n let byName: Record<string, unknown[]> = {}\n let mediaBlobCount = 0\n let fileKind: RestorePreviewFileKind\n\n if (archiveRef.endsWith('.json')) {\n fileKind = 'json'\n byName = EJSON.parse(bytes.toString('utf8')) as Record<string, unknown[]>\n } else if (archiveRef.endsWith('.gz')) {\n fileKind = 'tar-gzip'\n const files = await resolveTarGzip(bytes)\n byName = EJSON.parse(\n files.find((f) => f.name === COLLECTION_FILE_NAME)?.content?.toString() || '{}',\n ) as Record<string, unknown[]>\n mediaBlobCount = files.filter((f) => f.name !== COLLECTION_FILE_NAME).length\n } else {\n throw new Error('Unsupported backup file type (expected .json or .gz)')\n }\n\n return { byName, fileKind, mediaBlobCount }\n}\n\ntype GroupAgg = {\n groupId: string\n main?: { docCount: number; mongoName: string }\n versions?: { docCount: number; mongoName: string }\n}\n\nfunction rebuildMongoNames(g: GroupAgg): string[] {\n return [g.main?.mongoName, g.versions?.mongoName].filter(Boolean) as string[]\n}\n\nfunction countsFromByName(byName: Record<string, unknown[]>): Record<string, number> {\n const out: Record<string, number> = {}\n for (const k of Object.keys(byName)) {\n out[k] = docCount(byName, k)\n }\n return out\n}\n\n/** Ensures every configured Payload collection appears (0 if absent from backup JSON), like live-DB preview. */\nfunction mergeConfiguredCollectionZeros(\n payload: Payload,\n fileCounts: Record<string, number>,\n excludeMongo: Set<string>,\n): Record<string, number> {\n const merged = { ...fileCounts }\n for (const col of payload.config.collections) {\n const mongo = (col as { dbName?: string }).dbName ?? col.slug\n if (excludeMongo.has(mongo)) {\n continue\n }\n if (!(mongo in merged)) {\n merged[mongo] = 0\n }\n }\n return merged\n}\n\n/** Group Mongo collection names + doc counts (live DB or backup file) for admin UI lists. */\nexport function buildCollectionPreviewGroups(\n payload: Payload,\n countsByMongo: Record<string, number>,\n options: {\n excludeMongo?: Set<string>\n /** When true, list collections with 0 documents (e.g. manual backup UI). Default false. */\n includeEmptyCollections?: boolean\n preferredLocales?: string[]\n /** When true, order like Payload admin sidebar; unknown / hidden-only groups last by title. */\n sortLikeAdminNav?: boolean\n } = {},\n): RestorePreviewGroup[] {\n const preferredLocales =\n options.preferredLocales?.length ? options.preferredLocales : ['de', 'en']\n const excludeMongo = options.excludeMongo ?? new Set(['backup-tasks'])\n const includeEmptyCollections = options.includeEmptyCollections ?? false\n const sortLikeAdminNav = options.sortLikeAdminNav ?? false\n\n const aggs = new Map<string, GroupAgg>()\n\n for (const [mongoName, count] of Object.entries(countsByMongo)) {\n if (excludeMongo.has(mongoName)) {\n continue\n }\n if (!includeEmptyCollections && count === 0) {\n continue\n }\n\n const versionMatch = mongoName.match(MONGO_VERSIONS)\n if (versionMatch) {\n const groupId = versionMatch[1]\n const prev = aggs.get(groupId) ?? { groupId }\n aggs.set(groupId, {\n ...prev,\n groupId,\n versions: { docCount: count, mongoName },\n })\n } else {\n const groupId = mongoName\n const prev = aggs.get(groupId) ?? { groupId }\n aggs.set(groupId, {\n ...prev,\n groupId,\n main: { docCount: count, mongoName },\n })\n }\n }\n\n const groups: RestorePreviewGroup[] = [...aggs.values()].map((agg) => {\n const mongoNames = rebuildMongoNames(agg)\n const reasons = new Set<RestorePreviewAdminHiddenReason>()\n let adminHidden = false\n for (const mn of mongoNames) {\n const r = resolveAdminHidden(payload, mn)\n if (r.adminHidden && r.adminHiddenReason) {\n adminHidden = true\n reasons.add(r.adminHiddenReason)\n }\n }\n const affectsAuthSession = mongoNames.some((n) => AUTH_SESSION_MONGO_NAMES.has(n))\n\n return {\n adminHidden,\n adminHiddenReasons: [...reasons],\n affectsAuthSession,\n displayTitle: buildDisplayTitle(payload, agg.groupId, preferredLocales),\n groupId: agg.groupId,\n main: agg.main,\n mongoNames,\n versions: agg.versions,\n }\n })\n\n if (sortLikeAdminNav) {\n sortPreviewGroupsLikeAdminNav(payload, groups, preferredLocales)\n } else {\n const locale = preferredLocales[0] ?? 'en'\n groups.sort((a, b) => a.displayTitle.localeCompare(b.displayTitle, locale))\n }\n return groups\n}\n\nexport function buildRestorePreviewGroups(\n payload: Payload,\n parsed: {\n byName: Record<string, unknown[]>\n fileKind: RestorePreviewFileKind\n mediaBlobCount: number\n },\n options: { excludeMongo?: Set<string>; preferredLocales?: string[] } = {},\n): RestorePreviewResponse {\n const excludeMongo = options.excludeMongo ?? new Set(['backup-tasks'])\n const rawCounts = countsFromByName(parsed.byName)\n const counts = mergeConfiguredCollectionZeros(payload, rawCounts, excludeMongo)\n const groups = buildCollectionPreviewGroups(payload, counts, {\n excludeMongo,\n includeEmptyCollections: true,\n preferredLocales: options.preferredLocales,\n sortLikeAdminNav: true,\n })\n\n return {\n fileKind: parsed.fileKind,\n groups,\n mediaBlobCount: parsed.mediaBlobCount,\n }\n}\n\nexport async function getRestorePreviewForAdminRestore(\n payload: Payload,\n downloadUrl: string,\n options: {\n archivePathname?: string\n backupRead?: { pathname: string; token: string }\n blobAccess?: BackupBlobAccessLevel\n blobToken?: string\n preferredLocales?: string[]\n } = {},\n): Promise<RestorePreviewResponse> {\n const parsed = await loadRestoreBackupIndex(downloadUrl, {\n archivePathname: options.archivePathname,\n blobAccess: options.blobAccess,\n blobToken: options.blobToken,\n readAuth: options.backupRead,\n })\n return buildRestorePreviewGroups(payload, parsed, options)\n}\n"],"names":["EJSON","resolveTarGzip","COLLECTION_FILE_NAME","readBackupArchiveBytes","resolveArchiveFileReference","AUTH_SESSION_MONGO_NAMES","Set","MONGO_VERSIONS","docCount","byName","name","docs","Array","isArray","length","isVersionsMongoName","test","resolveAdminHidden","payload","mongoName","col","config","collections","find","c","dbName","slug","admin","hidden","adminHidden","adminHiddenReason","pickLocalizedLabel","labels","preferredLocales","undefined","l","pickField","field","map","loc","v","first","Object","values","Boolean","plural","singular","humanizeSlug","split","w","charAt","toUpperCase","slice","join","getNavVisibleCollections","filter","navVisibleCollectionOrderIndex","groupId","visible","idx","findIndex","sortPreviewGroupsLikeAdminNav","groups","locale","sort","a","b","ia","ib","displayTitle","localeCompare","buildDisplayTitle","groupSlug","label","loadRestoreBackupIndex","downloadUrl","options","archiveRef","archivePathname","bytes","backupRead","readAuth","blobAccess","blobToken","mediaBlobCount","fileKind","endsWith","parse","toString","files","f","content","Error","rebuildMongoNames","g","main","versions","countsFromByName","out","k","keys","mergeConfiguredCollectionZeros","fileCounts","excludeMongo","merged","mongo","has","buildCollectionPreviewGroups","countsByMongo","includeEmptyCollections","sortLikeAdminNav","aggs","Map","count","entries","versionMatch","match","prev","get","set","agg","mongoNames","reasons","mn","r","add","affectsAuthSession","some","n","adminHiddenReasons","buildRestorePreviewGroups","parsed","rawCounts","counts","getRestorePreviewForAdminRestore"],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ export type RestoreArchiveKind = 'json' | 'tar-gzip';
2
+ export type RestoreCollectionOutcome = {
3
+ docCount: number;
4
+ kind: 'restored';
5
+ name: string;
6
+ } | {
7
+ error: string;
8
+ kind: 'failed';
9
+ name: string;
10
+ } | {
11
+ kind: 'skipped';
12
+ name: string;
13
+ reason: 'blacklist' | 'empty';
14
+ };
15
+ export type RestoreMediaOutcome = {
16
+ accessFallbackCount: number;
17
+ attempted: number;
18
+ failed: Array<{
19
+ error: string;
20
+ name: string;
21
+ }>;
22
+ restored: number;
23
+ skippedByOption: boolean;
24
+ };
25
+ /** Structured outcome of a restore run (returned to endpoints and task progress). */
26
+ export type RestoreBackupResult = {
27
+ archiveKind: RestoreArchiveKind;
28
+ collections: RestoreCollectionOutcome[];
29
+ durationMs: number;
30
+ media: null | RestoreMediaOutcome;
31
+ };
32
+ export declare function buildRestoreWarnings(result: RestoreBackupResult): string[];
33
+ export declare function formatRestoreSummary(result: RestoreBackupResult): string;
34
+ export declare function restoreTaskStatus(result: RestoreBackupResult): 'completed' | 'completed_with_warnings';