@trieb.work/payload-plugin-backup-mongodb 0.1.4 → 0.1.7

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/BackupDashboard/TaskActionButton.client.tsx"],"sourcesContent":["'use client'\n\nimport { Button, Pill } from '@payloadcms/ui'\nimport { useRouter } from 'next/navigation'\nimport { useEffect, useMemo, useRef, useState } from 'react'\n\nimport type { BackupTaskProgress } from '../../core/taskProgress'\n\nimport { backupPluginPublicApiPaths } from '../../publicApiPaths'\nimport { closeNativeDialogOnBackdropPointer } from '../../utils/dialogBackdrop'\n\nexport interface TaskActionDangerConfirm {\n body: string\n cancelLabel?: string\n confirmLabel?: string\n title: string\n /** If true, show a second confirmation dialog before running the request (e.g. lock-out risk). */\n when: () => boolean\n}\n\ntype TaskActionButtonProps = {\n body?: Record<string, unknown>\n buttonStyle?: 'error' | 'primary' | 'secondary' | 'subtle'\n className?: string\n completeLabel?: string\n /** Extra confirmation dialog (danger) before POST — runs when `when()` returns true on click. */\n dangerConfirm?: TaskActionDangerConfirm\n endpoint: string\n /** When true, the idle (not-yet-running) action is disabled — e.g. while a restore preview loads. */\n idleDisabled?: boolean\n idleLabel: string\n kind: BackupTaskProgress['kind']\n /** Invoked once when the task reaches `completed` (after optional `refreshOnComplete`). Not called when `redirectOnComplete` is set. */\n onComplete?: () => void\n pendingLabel: string\n redirectOnComplete?: string\n refreshOnComplete?: boolean\n}\n\nexport const TaskActionButton: React.FC<TaskActionButtonProps> = ({\n body,\n buttonStyle = 'primary',\n className,\n completeLabel,\n dangerConfirm,\n endpoint,\n idleDisabled = false,\n idleLabel,\n kind,\n onComplete,\n pendingLabel,\n redirectOnComplete,\n refreshOnComplete = false,\n}) => {\n const router = useRouter()\n const dangerDialogRef = useRef<HTMLDialogElement>(null)\n const intervalRef = useRef<null | ReturnType<typeof setInterval>>(null)\n /** Avoid firing `onComplete` twice if the polling effect re-runs while status stays `completed`. */\n const completeNotifyForTaskIdRef = useRef<null | string>(null)\n /** Lets the task GET succeed after restore replaces `users` (session becomes invalid). */\n const pollSecretRef = useRef<null | string>(null)\n const [error, setError] = useState<null | string>(null)\n const [isStarting, setIsStarting] = useState(false)\n const [task, setTask] = useState<BackupTaskProgress | null>(null)\n\n const isBusy = isStarting || task?.status === 'queued' || task?.status === 'running'\n const message = error || task?.error || task?.message || ''\n\n const label = useMemo(() => {\n if (isBusy) {\n return pendingLabel\n }\n\n if (task?.status === 'completed' && completeLabel) {\n return completeLabel\n }\n\n return idleLabel\n }, [completeLabel, idleLabel, isBusy, pendingLabel, task?.status])\n\n const pillStyle = useMemo(() => {\n if (error || task?.status === 'failed') {\n return 'error' as const\n }\n\n if (task?.status === 'completed') {\n return 'success' as const\n }\n\n if (isBusy || task?.status === 'queued' || task?.status === 'running') {\n return 'warning' as const\n }\n\n return 'light-gray' as const\n }, [error, isBusy, task?.status])\n\n const statusLabel = useMemo(() => {\n if (error || task?.status === 'failed') {\n return 'Failed'\n }\n\n if (task?.status === 'completed') {\n return 'Done'\n }\n\n if (isBusy || task?.status === 'queued' || task?.status === 'running') {\n return 'Working'\n }\n\n return 'Idle'\n }, [error, isBusy, task?.status])\n\n useEffect(() => {\n return () => {\n if (intervalRef.current) {\n clearInterval(intervalRef.current)\n }\n }\n }, [])\n\n useEffect(() => {\n if (!task?.id || task.status === 'completed' || task.status === 'failed') {\n if (intervalRef.current) {\n clearInterval(intervalRef.current)\n intervalRef.current = null\n }\n\n if (task?.status === 'completed') {\n if (redirectOnComplete) {\n window.location.assign(redirectOnComplete)\n return\n }\n\n if (completeNotifyForTaskIdRef.current !== task.id) {\n completeNotifyForTaskIdRef.current = task.id\n if (refreshOnComplete) {\n router.refresh()\n }\n onComplete?.()\n }\n }\n\n return\n }\n\n intervalRef.current = setInterval(async () => {\n const response = await fetch(backupPluginPublicApiPaths.adminTask(task.id), {\n cache: 'no-store',\n ...(pollSecretRef.current\n ? { headers: { Authorization: `Bearer ${pollSecretRef.current}` } }\n : {}),\n })\n\n if (!response.ok) {\n clearInterval(intervalRef.current!)\n intervalRef.current = null\n setError('Could not load task progress')\n return\n }\n\n const nextTask = (await response.json()) as BackupTaskProgress\n setTask(nextTask)\n }, 1000)\n\n return () => {\n if (intervalRef.current) {\n clearInterval(intervalRef.current)\n intervalRef.current = null\n }\n }\n }, [onComplete, redirectOnComplete, refreshOnComplete, router, task?.id, task?.status])\n\n const runTask = async () => {\n try {\n setError(null)\n setIsStarting(true)\n pollSecretRef.current = null\n completeNotifyForTaskIdRef.current = null\n setTask(null)\n\n const response = await fetch(endpoint, {\n body: JSON.stringify(body ?? {}),\n headers: {\n 'Content-Type': 'application/json',\n },\n method: 'POST',\n })\n\n if (!response.ok) {\n const text = await response.text()\n throw new Error(text || 'Request failed')\n }\n\n const data = (await response.json()) as { pollSecret?: string; taskId: string }\n pollSecretRef.current = data.pollSecret ?? null\n const now = new Date().toISOString()\n setTask({\n id: data.taskId,\n createdAt: now,\n kind,\n message: 'Task queued',\n status: 'queued',\n updatedAt: now,\n })\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Request failed')\n } finally {\n setIsStarting(false)\n }\n }\n\n const handlePrimaryClick = async () => {\n if (dangerConfirm?.when()) {\n dangerDialogRef.current?.showModal()\n return\n }\n await runTask()\n }\n\n const handleDangerConfirm = async () => {\n dangerDialogRef.current?.close()\n await runTask()\n }\n\n return (\n <>\n <div className=\"backup-task-action\">\n <Button\n buttonStyle={buttonStyle}\n className={className}\n disabled={isBusy || idleDisabled}\n onClick={() => void handlePrimaryClick()}\n size=\"medium\"\n >\n {label}\n </Button>\n\n {(task !== null || error !== null) && (\n <div aria-live=\"polite\" className=\"backup-task-status-slot\">\n <Pill pillStyle={pillStyle} size=\"small\">\n {statusLabel}\n </Pill>\n <span className=\"backup-task-status__message\">{message}</span>\n </div>\n )}\n </div>\n\n {dangerConfirm ? (\n <>\n {/* Native <dialog>: backdrop dismiss; element not in jsx-a11y interactive list */}\n {/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */}\n <dialog\n className=\"backup-confirm-dialog backup-confirm-dialog--danger-confirm\"\n onMouseDown={(e) => closeNativeDialogOnBackdropPointer(e, dangerDialogRef)}\n ref={dangerDialogRef}\n >\n <p className=\"backup-confirm-dialog__title\">{dangerConfirm.title}</p>\n <p className=\"backup-confirm-dialog__body\">{dangerConfirm.body}</p>\n <div className=\"backup-confirm-dialog__actions\">\n <Button buttonStyle=\"error\" onClick={() => void handleDangerConfirm()} size=\"small\">\n {dangerConfirm.confirmLabel ?? 'I understand — continue'}\n </Button>\n <Button\n buttonStyle=\"secondary\"\n onClick={() => dangerDialogRef.current?.close()}\n size=\"small\"\n >\n {dangerConfirm.cancelLabel ?? 'Go back'}\n </Button>\n </div>\n </dialog>\n </>\n ) : null}\n </>\n )\n}\n"],"names":["Button","Pill","useRouter","useEffect","useMemo","useRef","useState","backupPluginPublicApiPaths","closeNativeDialogOnBackdropPointer","TaskActionButton","body","buttonStyle","className","completeLabel","dangerConfirm","endpoint","idleDisabled","idleLabel","kind","onComplete","pendingLabel","redirectOnComplete","refreshOnComplete","router","dangerDialogRef","intervalRef","completeNotifyForTaskIdRef","pollSecretRef","error","setError","isStarting","setIsStarting","task","setTask","isBusy","status","message","label","pillStyle","statusLabel","current","clearInterval","id","window","location","assign","refresh","setInterval","response","fetch","adminTask","cache","headers","Authorization","ok","nextTask","json","runTask","JSON","stringify","method","text","Error","data","pollSecret","now","Date","toISOString","taskId","createdAt","updatedAt","err","handlePrimaryClick","when","showModal","handleDangerConfirm","close","div","disabled","onClick","size","aria-live","span","dialog","onMouseDown","e","ref","p","title","confirmLabel","cancelLabel"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/components/BackupDashboard/TaskActionButton.client.tsx"],"sourcesContent":["'use client'\n\nimport { Button, Pill } from '@payloadcms/ui'\nimport { useRouter } from 'next/navigation'\nimport { useEffect, useMemo, useRef, useState } from 'react'\n\nimport type { BackupTaskProgress } from '../../core/taskProgress'\n\nimport { backupPluginPublicApiPaths } from '../../publicApiPaths'\nimport { closeNativeDialogOnBackdropPointer } from '../../utils/dialogBackdrop'\n\nexport interface TaskActionDangerConfirm {\n body: string\n cancelLabel?: string\n confirmLabel?: string\n title: string\n /** If true, show a second confirmation dialog before running the request (e.g. lock-out risk). */\n when: () => boolean\n}\n\ntype TaskActionButtonProps = {\n body?: Record<string, unknown>\n buttonStyle?: 'error' | 'primary' | 'secondary' | 'subtle'\n className?: string\n completeLabel?: string\n /** Extra confirmation dialog (danger) before POST — runs when `when()` returns true on click. */\n dangerConfirm?: TaskActionDangerConfirm\n endpoint: string\n /** When true, the idle (not-yet-running) action is disabled — e.g. while a restore preview loads. */\n idleDisabled?: boolean\n idleLabel: string\n kind: BackupTaskProgress['kind']\n /** Invoked once when the task reaches `completed` (after optional `refreshOnComplete`). Not called when `redirectOnComplete` is set. */\n onComplete?: () => void\n pendingLabel: string\n redirectOnComplete?: string\n refreshOnComplete?: boolean\n}\n\nexport const TaskActionButton: React.FC<TaskActionButtonProps> = ({\n body,\n buttonStyle = 'primary',\n className,\n completeLabel,\n dangerConfirm,\n endpoint,\n idleDisabled = false,\n idleLabel,\n kind,\n onComplete,\n pendingLabel,\n redirectOnComplete,\n refreshOnComplete = false,\n}) => {\n const router = useRouter()\n const dangerDialogRef = useRef<HTMLDialogElement>(null)\n const intervalRef = useRef<null | ReturnType<typeof setInterval>>(null)\n /** Avoid firing `onComplete` twice if the polling effect re-runs while status stays `completed`. */\n const completeNotifyForTaskIdRef = useRef<null | string>(null)\n /** Lets the task GET succeed after restore replaces `users` (session becomes invalid). */\n const pollSecretRef = useRef<null | string>(null)\n const [error, setError] = useState<null | string>(null)\n const [isStarting, setIsStarting] = useState(false)\n const [task, setTask] = useState<BackupTaskProgress | null>(null)\n\n const isBusy = isStarting || task?.status === 'queued' || task?.status === 'running'\n const message = error || task?.error || task?.message || ''\n\n const label = useMemo(() => {\n if (isBusy) {\n return pendingLabel\n }\n\n if (task?.status === 'completed' && completeLabel) {\n return completeLabel\n }\n\n return idleLabel\n }, [completeLabel, idleLabel, isBusy, pendingLabel, task?.status])\n\n const pillStyle = useMemo(() => {\n if (error || task?.status === 'failed') {\n return 'error' as const\n }\n\n if (task?.status === 'completed') {\n return 'success' as const\n }\n\n if (isBusy || task?.status === 'queued' || task?.status === 'running') {\n return 'warning' as const\n }\n\n return 'light-gray' as const\n }, [error, isBusy, task?.status])\n\n const statusLabel = useMemo(() => {\n if (error || task?.status === 'failed') {\n return 'Failed'\n }\n\n if (task?.status === 'completed') {\n return 'Done'\n }\n\n if (isBusy || task?.status === 'queued' || task?.status === 'running') {\n return 'Working'\n }\n\n return 'Idle'\n }, [error, isBusy, task?.status])\n\n useEffect(() => {\n return () => {\n if (intervalRef.current) {\n clearInterval(intervalRef.current)\n }\n }\n }, [])\n\n useEffect(() => {\n if (!task?.id || task.status === 'completed' || task.status === 'failed') {\n if (intervalRef.current) {\n clearInterval(intervalRef.current)\n intervalRef.current = null\n }\n\n if (task?.status === 'completed') {\n if (redirectOnComplete) {\n window.location.assign(redirectOnComplete)\n return\n }\n\n if (completeNotifyForTaskIdRef.current !== task.id) {\n completeNotifyForTaskIdRef.current = task.id\n if (refreshOnComplete) {\n router.refresh()\n }\n onComplete?.()\n }\n }\n\n return\n }\n\n intervalRef.current = setInterval(async () => {\n const response = await fetch(backupPluginPublicApiPaths.adminTask(task.id), {\n cache: 'no-store',\n ...(pollSecretRef.current ?\n { headers: { Authorization: `Bearer ${pollSecretRef.current}` } }\n : {}),\n })\n\n if (!response.ok) {\n clearInterval(intervalRef.current!)\n intervalRef.current = null\n setError('Could not load task progress')\n return\n }\n\n const nextTask = (await response.json()) as BackupTaskProgress\n setTask(nextTask)\n }, 1000)\n\n return () => {\n if (intervalRef.current) {\n clearInterval(intervalRef.current)\n intervalRef.current = null\n }\n }\n }, [onComplete, redirectOnComplete, refreshOnComplete, router, task?.id, task?.status])\n\n const runTask = async () => {\n try {\n setError(null)\n setIsStarting(true)\n pollSecretRef.current = null\n completeNotifyForTaskIdRef.current = null\n setTask(null)\n\n const response = await fetch(endpoint, {\n body: JSON.stringify(body ?? {}),\n headers: {\n 'Content-Type': 'application/json',\n },\n method: 'POST',\n })\n\n if (!response.ok) {\n const text = await response.text()\n throw new Error(text || 'Request failed')\n }\n\n const data = (await response.json()) as { pollSecret?: string; taskId: string }\n pollSecretRef.current = data.pollSecret ?? null\n const now = new Date().toISOString()\n setTask({\n id: data.taskId,\n createdAt: now,\n kind,\n message: 'Task queued',\n status: 'queued',\n updatedAt: now,\n })\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Request failed')\n } finally {\n setIsStarting(false)\n }\n }\n\n const handlePrimaryClick = async () => {\n if (dangerConfirm?.when()) {\n dangerDialogRef.current?.showModal()\n return\n }\n await runTask()\n }\n\n const handleDangerConfirm = async () => {\n dangerDialogRef.current?.close()\n await runTask()\n }\n\n return (\n <>\n <div className=\"backup-task-action\">\n <Button\n buttonStyle={buttonStyle}\n className={className}\n disabled={isBusy || idleDisabled}\n onClick={() => void handlePrimaryClick()}\n size=\"medium\"\n >\n {label}\n </Button>\n\n {(task !== null || error !== null) && (\n <div aria-live=\"polite\" className=\"backup-task-status-slot\">\n <Pill pillStyle={pillStyle} size=\"small\">\n {statusLabel}\n </Pill>\n <span className=\"backup-task-status__message\">{message}</span>\n </div>\n )}\n </div>\n\n {dangerConfirm ?\n <>\n {/* Native <dialog>: backdrop dismiss; element not in jsx-a11y interactive list */}\n {/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */}\n <dialog\n className=\"backup-confirm-dialog backup-confirm-dialog--danger-confirm\"\n onMouseDown={(e) => closeNativeDialogOnBackdropPointer(e, dangerDialogRef)}\n ref={dangerDialogRef}\n >\n <p className=\"backup-confirm-dialog__title\">{dangerConfirm.title}</p>\n <p className=\"backup-confirm-dialog__body\">{dangerConfirm.body}</p>\n <div className=\"backup-confirm-dialog__actions\">\n <Button buttonStyle=\"error\" onClick={() => void handleDangerConfirm()} size=\"small\">\n {dangerConfirm.confirmLabel ?? 'I understand — continue'}\n </Button>\n <Button\n buttonStyle=\"secondary\"\n onClick={() => dangerDialogRef.current?.close()}\n size=\"small\"\n >\n {dangerConfirm.cancelLabel ?? 'Go back'}\n </Button>\n </div>\n </dialog>\n </>\n : null}\n </>\n )\n}\n"],"names":["Button","Pill","useRouter","useEffect","useMemo","useRef","useState","backupPluginPublicApiPaths","closeNativeDialogOnBackdropPointer","TaskActionButton","body","buttonStyle","className","completeLabel","dangerConfirm","endpoint","idleDisabled","idleLabel","kind","onComplete","pendingLabel","redirectOnComplete","refreshOnComplete","router","dangerDialogRef","intervalRef","completeNotifyForTaskIdRef","pollSecretRef","error","setError","isStarting","setIsStarting","task","setTask","isBusy","status","message","label","pillStyle","statusLabel","current","clearInterval","id","window","location","assign","refresh","setInterval","response","fetch","adminTask","cache","headers","Authorization","ok","nextTask","json","runTask","JSON","stringify","method","text","Error","data","pollSecret","now","Date","toISOString","taskId","createdAt","updatedAt","err","handlePrimaryClick","when","showModal","handleDangerConfirm","close","div","disabled","onClick","size","aria-live","span","dialog","onMouseDown","e","ref","p","title","confirmLabel","cancelLabel"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/BackupDashboard/index.tsx"],"sourcesContent":["import type { I18n } from '@payloadcms/translations'\nimport type { Payload } from 'payload'\n\nimport type { BackupPluginOptions } from '../../types'\n\nimport { listBackups, resolveBackupListToken } from '../../core/backup'\nimport {\n getBackupSortTimeMs,\n getCurrentDbName,\n getCurrentHostname,\n isUserAllowedByEnvRoles,\n transformBlobName,\n} from '../../utils/index'\nimport { backupDashboardInlineCss } from './backupDashboardInlineCss'\nimport { BackupListCollapsible, BackupSettingsModal, ManualBackupDialog } from './index.client'\n\ninterface BackupDashboardProps {\n i18n: I18n\n /**\n * Payload instance injected by Payload's admin `RenderServerComponent` as a server prop\n * for `afterDashboard` components. Avoids importing `@payload-config` from the plugin,\n * which does not resolve when the plugin runs from `node_modules`.\n */\n payload?: Payload\n /** Optional on admin server props; omitted does not mean logged out (see `defaultIsHidden`). */\n user?: null | Record<string, unknown>\n}\n\nfunction defaultIsHidden(\n user: null | Record<string, unknown> | undefined,\n access?: BackupPluginOptions['access'],\n): boolean {\n if (user === null) {\n return true\n }\n if (user === undefined) {\n if (access) {\n return true\n }\n return false\n }\n if (access) {\n return !access(user)\n }\n // Role allow-list via `PAYLOAD_BACKUP_ALLOWED_ROLES`. When unset, falls back to the\n // historical default (admin role, or \"visible\" when the project has no roles field).\n return !isUserAllowedByEnvRoles(user)\n}\n\nexport const BackupDashboard: React.FC<BackupDashboardProps> = async ({ i18n, payload, user }) => {\n if (defaultIsHidden(user)) {\n return null\n }\n\n const hasMongoConnection =\n Boolean(process.env.MONGODB_URI?.trim()) || Boolean(process.env.DATABASE_URL?.trim())\n if (!hasMongoConnection) {\n return (\n <>\n <style dangerouslySetInnerHTML={{ __html: backupDashboardInlineCss }} />\n <div className=\"backup-dashboard\">\n <h2>\n Backups <span className=\"experimental\">(experimental)</span>\n </h2>\n <p className=\"backup-dashboard__setup-hint\" role=\"status\">\n Set <code className=\"backup-dashboard__setup-hint-code\">MONGODB_URI</code> or{' '}\n <code className=\"backup-dashboard__setup-hint-code\">DATABASE_URL</code> so Payload can\n connect to MongoDB. The in-memory dev database sets both automatically when neither is\n provided.\n </p>\n </div>\n </>\n )\n }\n\n if (!payload) {\n // Should not happen in a normal Payload admin render; guard for safety.\n return (\n <>\n <style dangerouslySetInnerHTML={{ __html: backupDashboardInlineCss }} />\n <div className=\"backup-dashboard\">\n <h2>\n Backups <span className=\"experimental\">(experimental)</span>\n </h2>\n <p className=\"backup-dashboard__setup-hint\" role=\"status\">\n Backup dashboard could not initialise: Payload instance was not provided by the admin\n server props.\n </p>\n </div>\n </>\n )\n }\n\n const backupBlobToken = await resolveBackupListToken(payload)\n const hasBlobToken = backupBlobToken.trim().length > 0\n\n const blobs = hasBlobToken ? await listBackups(payload, { blobToken: backupBlobToken }) : []\n const sortedBlobs = [...blobs].sort((a, b) => {\n const ta = getBackupSortTimeMs(transformBlobName(a.pathname), new Date(a.uploadedAt))\n const tb = getBackupSortTimeMs(transformBlobName(b.pathname), new Date(b.uploadedAt))\n return tb - ta\n })\n const currentHostname = getCurrentHostname()\n const currentDbName = getCurrentDbName()\n\n const countOtherDb = sortedBlobs.filter((blob) => {\n const { dbName } = transformBlobName(blob.pathname)\n return currentDbName !== dbName\n }).length\n\n const countOtherHostname = sortedBlobs.filter((blob) => {\n const { hostname } = transformBlobName(blob.pathname)\n return currentHostname !== hostname\n }).length\n const lastBackup = sortedBlobs[0]\n\n return (\n <>\n <style\n dangerouslySetInnerHTML={{ __html: backupDashboardInlineCss }}\n data-payload-backup-mongodb=\"1\"\n />\n <div className=\"backup-dashboard\">\n <h2>\n Backups <span className=\"experimental\">(experimental)</span>\n </h2>\n\n {!hasBlobToken && (\n <p className=\"backup-dashboard__setup-hint\" role=\"status\">\n Add a Vercel Blob read/write token: set environment variable{' '}\n <code className=\"backup-dashboard__setup-hint-code\">BLOB_READ_WRITE_TOKEN</code>, or\n open <strong>Backup settings</strong> and paste a token. Until then, the list stays\n empty and cron or manual backups cannot run.\n </p>\n )}\n\n <div className=\"backup-dashboard__toolbar\">\n <div className=\"backup-dashboard__toolbar-meta\">\n <span className=\"backup-dashboard__toolbar-pill\">\n <span className=\"backup-dashboard__toolbar-key\">Total</span>\n {sortedBlobs.length} backup{sortedBlobs.length === 1 ? '' : 's'}\n </span>\n <span className=\"backup-dashboard__toolbar-pill\">\n <span className=\"backup-dashboard__toolbar-key\">Last backup</span>\n {lastBackup\n ? new Date(\n getBackupSortTimeMs(\n transformBlobName(lastBackup.pathname),\n new Date(lastBackup.uploadedAt),\n ),\n ).toLocaleString(i18n?.language || 'en')\n : 'No backups yet'}\n </span>\n </div>\n <div className=\"make-backup-actions\">\n <BackupSettingsModal />\n <ManualBackupDialog />\n </div>\n </div>\n\n <BackupListCollapsible\n blobs={sortedBlobs.map((b) => ({\n downloadUrl: b.downloadUrl,\n pathname: b.pathname,\n size: b.size,\n uploadedAt: new Date(b.uploadedAt).toISOString(),\n url: b.url,\n }))}\n countOtherDb={countOtherDb}\n countOtherHostname={countOtherHostname}\n currentDbName={currentDbName}\n currentHostname={currentHostname}\n i18nLanguage={i18n?.language || 'en'}\n />\n </div>\n </>\n )\n}\n\nexport default BackupDashboard\n"],"names":["listBackups","resolveBackupListToken","getBackupSortTimeMs","getCurrentDbName","getCurrentHostname","isUserAllowedByEnvRoles","transformBlobName","backupDashboardInlineCss","BackupListCollapsible","BackupSettingsModal","ManualBackupDialog","defaultIsHidden","user","access","undefined","BackupDashboard","i18n","payload","hasMongoConnection","Boolean","process","env","MONGODB_URI","trim","DATABASE_URL","style","dangerouslySetInnerHTML","__html","div","className","h2","span","p","role","code","backupBlobToken","hasBlobToken","length","blobs","blobToken","sortedBlobs","sort","a","b","ta","pathname","Date","uploadedAt","tb","currentHostname","currentDbName","countOtherDb","filter","blob","dbName","countOtherHostname","hostname","lastBackup","data-payload-backup-mongodb","strong","toLocaleString","language","map","downloadUrl","size","toISOString","url","i18nLanguage"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/components/BackupDashboard/index.tsx"],"sourcesContent":["import type { I18n } from '@payloadcms/translations'\nimport type { Payload } from 'payload'\n\nimport type { BackupPluginOptions } from '../../types'\n\nimport { listBackups, resolveBackupListToken } from '../../core/backup'\nimport {\n getBackupSortTimeMs,\n getCurrentDbName,\n getCurrentHostname,\n isUserAllowedByEnvRoles,\n transformBlobName,\n} from '../../utils/index'\nimport { backupDashboardInlineCss } from './backupDashboardInlineCss'\nimport { BackupListCollapsible, BackupSettingsModal, ManualBackupDialog } from './index.client'\n\ninterface BackupDashboardProps {\n i18n: I18n\n /**\n * Payload instance injected by Payload's admin `RenderServerComponent` as a server prop\n * for `afterDashboard` components. Avoids importing `@payload-config` from the plugin,\n * which does not resolve when the plugin runs from `node_modules`.\n */\n payload?: Payload\n /** Optional on admin server props; omitted does not mean logged out (see `defaultIsHidden`). */\n user?: null | Record<string, unknown>\n}\n\nfunction defaultIsHidden(\n user: null | Record<string, unknown> | undefined,\n access?: BackupPluginOptions['access'],\n): boolean {\n if (user === null) {\n return true\n }\n if (user === undefined) {\n if (access) {\n return true\n }\n return false\n }\n if (access) {\n return !access(user)\n }\n // Role allow-list via `PAYLOAD_BACKUP_ALLOWED_ROLES`. When unset, falls back to the\n // historical default (admin role, or \"visible\" when the project has no roles field).\n return !isUserAllowedByEnvRoles(user)\n}\n\nexport const BackupDashboard: React.FC<BackupDashboardProps> = async ({ i18n, payload, user }) => {\n if (defaultIsHidden(user)) {\n return null\n }\n\n const hasMongoConnection =\n Boolean(process.env.MONGODB_URI?.trim()) || Boolean(process.env.DATABASE_URL?.trim())\n if (!hasMongoConnection) {\n return (\n <>\n <style dangerouslySetInnerHTML={{ __html: backupDashboardInlineCss }} />\n <div className=\"backup-dashboard\">\n <h2>\n Backups <span className=\"experimental\">(experimental)</span>\n </h2>\n <p className=\"backup-dashboard__setup-hint\" role=\"status\">\n Set <code className=\"backup-dashboard__setup-hint-code\">MONGODB_URI</code> or{' '}\n <code className=\"backup-dashboard__setup-hint-code\">DATABASE_URL</code> so Payload can\n connect to MongoDB. The in-memory dev database sets both automatically when neither is\n provided.\n </p>\n </div>\n </>\n )\n }\n\n if (!payload) {\n // Should not happen in a normal Payload admin render; guard for safety.\n return (\n <>\n <style dangerouslySetInnerHTML={{ __html: backupDashboardInlineCss }} />\n <div className=\"backup-dashboard\">\n <h2>\n Backups <span className=\"experimental\">(experimental)</span>\n </h2>\n <p className=\"backup-dashboard__setup-hint\" role=\"status\">\n Backup dashboard could not initialise: Payload instance was not provided by the admin\n server props.\n </p>\n </div>\n </>\n )\n }\n\n const backupBlobToken = await resolveBackupListToken(payload)\n const hasBlobToken = backupBlobToken.trim().length > 0\n\n const blobs = hasBlobToken ? await listBackups(payload, { blobToken: backupBlobToken }) : []\n const sortedBlobs = [...blobs].sort((a, b) => {\n const ta = getBackupSortTimeMs(transformBlobName(a.pathname), new Date(a.uploadedAt))\n const tb = getBackupSortTimeMs(transformBlobName(b.pathname), new Date(b.uploadedAt))\n return tb - ta\n })\n const currentHostname = getCurrentHostname()\n const currentDbName = getCurrentDbName()\n\n const countOtherDb = sortedBlobs.filter((blob) => {\n const { dbName } = transformBlobName(blob.pathname)\n return currentDbName !== dbName\n }).length\n\n const countOtherHostname = sortedBlobs.filter((blob) => {\n const { hostname } = transformBlobName(blob.pathname)\n return currentHostname !== hostname\n }).length\n const lastBackup = sortedBlobs[0]\n\n return (\n <>\n <style\n dangerouslySetInnerHTML={{ __html: backupDashboardInlineCss }}\n data-payload-backup-mongodb=\"1\"\n />\n <div className=\"backup-dashboard\">\n <h2>\n Backups <span className=\"experimental\">(experimental)</span>\n </h2>\n\n {!hasBlobToken && (\n <p className=\"backup-dashboard__setup-hint\" role=\"status\">\n Add a Vercel Blob read/write token: set environment variable{' '}\n <code className=\"backup-dashboard__setup-hint-code\">BLOB_READ_WRITE_TOKEN</code>, or\n open <strong>Backup settings</strong> and paste a token. Until then, the list stays\n empty and cron or manual backups cannot run.\n </p>\n )}\n\n <div className=\"backup-dashboard__toolbar\">\n <div className=\"backup-dashboard__toolbar-meta\">\n <span className=\"backup-dashboard__toolbar-pill\">\n <span className=\"backup-dashboard__toolbar-key\">Total</span>\n {sortedBlobs.length} backup{sortedBlobs.length === 1 ? '' : 's'}\n </span>\n <span className=\"backup-dashboard__toolbar-pill\">\n <span className=\"backup-dashboard__toolbar-key\">Last backup</span>\n {lastBackup ?\n new Date(\n getBackupSortTimeMs(\n transformBlobName(lastBackup.pathname),\n new Date(lastBackup.uploadedAt),\n ),\n ).toLocaleString(i18n?.language || 'en')\n : 'No backups yet'}\n </span>\n </div>\n <div className=\"make-backup-actions\">\n <BackupSettingsModal />\n <ManualBackupDialog />\n </div>\n </div>\n\n <BackupListCollapsible\n blobs={sortedBlobs.map((b) => ({\n downloadUrl: b.downloadUrl,\n pathname: b.pathname,\n size: b.size,\n uploadedAt: new Date(b.uploadedAt).toISOString(),\n url: b.url,\n }))}\n countOtherDb={countOtherDb}\n countOtherHostname={countOtherHostname}\n currentDbName={currentDbName}\n currentHostname={currentHostname}\n i18nLanguage={i18n?.language || 'en'}\n />\n </div>\n </>\n )\n}\n\nexport default BackupDashboard\n"],"names":["listBackups","resolveBackupListToken","getBackupSortTimeMs","getCurrentDbName","getCurrentHostname","isUserAllowedByEnvRoles","transformBlobName","backupDashboardInlineCss","BackupListCollapsible","BackupSettingsModal","ManualBackupDialog","defaultIsHidden","user","access","undefined","BackupDashboard","i18n","payload","hasMongoConnection","Boolean","process","env","MONGODB_URI","trim","DATABASE_URL","style","dangerouslySetInnerHTML","__html","div","className","h2","span","p","role","code","backupBlobToken","hasBlobToken","length","blobs","blobToken","sortedBlobs","sort","a","b","ta","pathname","Date","uploadedAt","tb","currentHostname","currentDbName","countOtherDb","filter","blob","dbName","countOtherHostname","hostname","lastBackup","data-payload-backup-mongodb","strong","toLocaleString","language","map","downloadUrl","size","toISOString","url","i18nLanguage"],"mappings":"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"}
@@ -1 +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"}
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(fileBuffer: Buffer): 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,eAAeC,UAAkB;IAC/C,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"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/backup.ts"],"sourcesContent":["import type { Payload } from 'payload'\n\nimport { del, list } from '@vercel/blob'\nimport { EJSON } from 'bson'\n\nimport {\n createBlobName,\n getCurrentDbName,\n getCurrentHostname,\n sanitizeBackupLabel,\n} from '../utils/index'\nimport { createTarGzip } from './archive'\nimport {\n type BackupBlobAccessLevel,\n putBackupBlobContent,\n readBackupBlobContentFlexible,\n} from './backupBlobIO'\nimport { getResolvedCronBackupSettings, resolveBackupBlobToken } from './backupSettings'\nimport { getDb } from './db'\nimport { updateBackupTask } from './taskProgress'\n\nexport const COLLECTION_FILE_NAME = 'collections.json'\n\n/**\n * Resolves the Vercel Blob read/write token the same way as backup endpoints and\n * {@link createBackup} / {@link restoreBackup}: settings document override, else `BLOB_READ_WRITE_TOKEN`.\n * Pass `explicitToken` to skip reading settings (same as `options.blobToken` in those calls).\n */\nexport async function resolveBackupListToken(\n payload: Payload,\n explicitToken?: string,\n): Promise<string> {\n const t = explicitToken?.trim()\n if (t) {\n return t\n }\n return resolveBackupBlobToken(await getResolvedCronBackupSettings(payload)).trim()\n}\n\nexport async function listBackups(\n payload: Payload,\n options: {\n /**\n * When set, used as the Vercel Blob token (e.g. tests or a pre-resolved value).\n * When omitted, uses {@link resolveBackupListToken} (settings + env), same as other backup APIs.\n */\n blobToken?: string\n } = {},\n) {\n const token = await resolveBackupListToken(payload, options.blobToken)\n if (!token) {\n return []\n }\n const { blobs } = await list({\n limit: 1000,\n prefix: 'backups/',\n token,\n })\n return blobs\n}\n\nfunction resolveBlobToken(blobToken?: string): string | undefined {\n if (blobToken && blobToken.trim().length > 0) {\n return blobToken\n }\n return undefined\n}\n\n/**\n * @param mediaListToken Token for listing/fetching **Payload media** blobs (usually\n * `BLOB_READ_WRITE_TOKEN`). When omitted, uses env then falls back to `backupBlobToken`.\n */\nexport async function createMediaBackupFile(\n collectionBackupFile: string,\n mediaCollection: { filename: string }[],\n backupBlobToken?: string,\n mediaListToken?: string,\n payload?: Payload,\n): Promise<Buffer> {\n const envMedia = (process.env.BLOB_READ_WRITE_TOKEN || '').trim()\n const tokenForMedia =\n resolveBlobToken(mediaListToken) ??\n (envMedia.length > 0 ? envMedia : undefined) ??\n resolveBlobToken(backupBlobToken)\n const mediaFiles = await Promise.all(\n mediaCollection.map(async (media) => {\n const matchingFiles = await list({ limit: 2, prefix: media.filename, token: tokenForMedia })\n const blob = matchingFiles.blobs.find((blob) => blob.pathname === media.filename)\n if (!blob) {\n payload?.logger.warn(\n { filename: media.filename },\n '[backup] File was in collection but not in blob storage',\n )\n return undefined\n }\n const content = await readBackupBlobContentFlexible(\n blob.pathname,\n blob.downloadUrl,\n tokenForMedia ?? '',\n )\n return { name: media.filename, content }\n }),\n )\n return await createTarGzip([\n { name: COLLECTION_FILE_NAME, content: Buffer.from(collectionBackupFile) },\n ...(mediaFiles.filter(Boolean) as { content: Buffer; name: string }[]),\n ])\n}\n\nexport async function createBackup(\n payload: Payload,\n options: {\n backupsToKeep?: number\n /** Public (default) or private Vercel Blob access for the uploaded backup archive. */\n blobAccess?: BackupBlobAccessLevel\n /** Optional explicit Vercel Blob token (falls back to env inside @vercel/blob). */\n blobToken?: string\n cron?: boolean\n includeMedia?: boolean\n /**\n * Optional human-readable label for manual backups (appears in the backup list and is\n * searchable via the label filter). Ignored for cron backups. Whitespace is collapsed,\n * length is capped and the stored value is URL-encoded in the blob pathname.\n */\n label?: string\n /** Mongo collection names to omit from the dump (e.g. manual backup UI). */\n skipCollections?: string[]\n taskId?: string\n } = {},\n): Promise<void> {\n const {\n backupsToKeep,\n blobToken,\n cron = false,\n includeMedia = false,\n skipCollections,\n taskId,\n } = options\n const label = cron ? '' : sanitizeBackupLabel(options.label)\n const blobAccess: BackupBlobAccessLevel = options.blobAccess ?? 'public'\n const envMedia = (process.env.BLOB_READ_WRITE_TOKEN || '').trim()\n const token = resolveBlobToken(blobToken)\n const skip = new Set(skipCollections ?? [])\n const resolvedBackupsToKeep = backupsToKeep ?? (Number(process.env.BACKUPS_TO_KEEP) || 10)\n\n const currentHostname = getCurrentHostname()\n const currentDbName = getCurrentDbName()\n const type = cron ? 'cron' : 'manual'\n const t0 = Date.now()\n\n payload.logger.info(\n {\n type,\n blacklist: skipCollections?.length ?? 0,\n db: currentDbName,\n host: currentHostname,\n includeMedia,\n },\n '[backup] Starting backup',\n )\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: 'Starting backup',\n status: 'running',\n })\n }\n\n if (cron) {\n const { blobs } = await list({\n limit: 1000,\n prefix: 'backups/cron-',\n token,\n })\n const sorted = blobs.sort(\n (a, b) => new Date(b.uploadedAt).getTime() - new Date(a.uploadedAt).getTime(),\n )\n const oldest = sorted.slice(resolvedBackupsToKeep - 1)\n if (oldest.length > 0) {\n payload.logger.info({ count: oldest.length }, '[backup] Pruning old cron backups')\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: `Pruning ${oldest.length} old cron backup${oldest.length === 1 ? '' : 's'}`,\n })\n }\n }\n for (const blob of oldest) {\n await del(blob.url, { token })\n payload.logger.info({ pathname: blob.pathname }, '[backup] Deleted old backup')\n }\n }\n\n const db = getDb(payload)\n const collections = await db.listCollections().toArray()\n payload.logger.info({ collections: collections.length }, '[backup] Dumping collections')\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: `Dumping ${collections.length} collection${collections.length === 1 ? '' : 's'}`,\n })\n }\n\n const allData: Record<string, Record<string, unknown>[]> = {}\n for (const collection of collections) {\n if (skip.has(collection.name)) {\n payload.logger.debug(\n { collection: collection.name },\n '[backup] Skipping blacklisted collection',\n )\n continue\n }\n allData[collection.name] = (await db\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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"}
1
+ {"version":3,"sources":["../../src/core/backup.ts"],"sourcesContent":["import type { Payload } from 'payload'\n\nimport { del, list } from '@vercel/blob'\nimport { EJSON } from 'bson'\n\nimport {\n createBlobName,\n getCurrentDbName,\n getCurrentHostname,\n sanitizeBackupLabel,\n} from '../utils/index'\nimport { createTarGzip } from './archive'\nimport {\n type BackupBlobAccessLevel,\n putBackupBlobContent,\n readBackupBlobContentFlexible,\n} from './backupBlobIO'\nimport { getResolvedCronBackupSettings, resolveBackupBlobToken } from './backupSettings'\nimport { getDb } from './db'\nimport { updateBackupTask } from './taskProgress'\n\nexport const COLLECTION_FILE_NAME = 'collections.json'\n\n/**\n * Resolves the Vercel Blob read/write token the same way as backup endpoints and\n * {@link createBackup} / {@link restoreBackup}: settings document override, else `BLOB_READ_WRITE_TOKEN`.\n * Pass `explicitToken` to skip reading settings (same as `options.blobToken` in those calls).\n */\nexport async function resolveBackupListToken(\n payload: Payload,\n explicitToken?: string,\n): Promise<string> {\n const t = explicitToken?.trim()\n if (t) {\n return t\n }\n return resolveBackupBlobToken(await getResolvedCronBackupSettings(payload)).trim()\n}\n\nexport async function listBackups(\n payload: Payload,\n options: {\n /**\n * When set, used as the Vercel Blob token (e.g. tests or a pre-resolved value).\n * When omitted, uses {@link resolveBackupListToken} (settings + env), same as other backup APIs.\n */\n blobToken?: string\n } = {},\n) {\n const token = await resolveBackupListToken(payload, options.blobToken)\n if (!token) {\n return []\n }\n const { blobs } = await list({\n limit: 1000,\n prefix: 'backups/',\n token,\n })\n return blobs\n}\n\nfunction resolveBlobToken(blobToken?: string): string | undefined {\n if (blobToken && blobToken.trim().length > 0) {\n return blobToken\n }\n return undefined\n}\n\n/**\n * @param mediaListToken Token for listing/fetching **Payload media** blobs (usually\n * `BLOB_READ_WRITE_TOKEN`). When omitted, uses env then falls back to `backupBlobToken`.\n */\nexport async function createMediaBackupFile(\n collectionBackupFile: string,\n mediaCollection: { filename: string }[],\n backupBlobToken?: string,\n mediaListToken?: string,\n payload?: Payload,\n): Promise<Buffer> {\n const envMedia = (process.env.BLOB_READ_WRITE_TOKEN || '').trim()\n const tokenForMedia =\n resolveBlobToken(mediaListToken) ??\n (envMedia.length > 0 ? envMedia : undefined) ??\n resolveBlobToken(backupBlobToken)\n const mediaFiles = await Promise.all(\n mediaCollection.map(async (media) => {\n const matchingFiles = await list({ limit: 2, prefix: media.filename, token: tokenForMedia })\n const blob = matchingFiles.blobs.find((blob) => blob.pathname === media.filename)\n if (!blob) {\n payload?.logger.warn(\n { filename: media.filename },\n '[backup] File was in collection but not in blob storage',\n )\n return undefined\n }\n const content = await readBackupBlobContentFlexible(\n blob.pathname,\n blob.downloadUrl,\n tokenForMedia ?? '',\n )\n return { name: media.filename, content }\n }),\n )\n return await createTarGzip([\n { name: COLLECTION_FILE_NAME, content: Buffer.from(collectionBackupFile) },\n ...(mediaFiles.filter(Boolean) as { content: Buffer; name: string }[]),\n ])\n}\n\nexport async function createBackup(\n payload: Payload,\n options: {\n backupsToKeep?: number\n /** Public (default) or private Vercel Blob access for the uploaded backup archive. */\n blobAccess?: BackupBlobAccessLevel\n /** Optional explicit Vercel Blob token (falls back to env inside @vercel/blob). */\n blobToken?: string\n cron?: boolean\n includeMedia?: boolean\n /**\n * Optional human-readable label for manual backups (appears in the backup list and is\n * searchable via the label filter). Ignored for cron backups. Whitespace is collapsed,\n * length is capped and the stored value is URL-encoded in the blob pathname.\n */\n label?: string\n /** Mongo collection names to omit from the dump (e.g. manual backup UI). */\n skipCollections?: string[]\n taskId?: string\n } = {},\n): Promise<void> {\n const {\n backupsToKeep,\n blobToken,\n cron = false,\n includeMedia = false,\n skipCollections,\n taskId,\n } = options\n const label = cron ? '' : sanitizeBackupLabel(options.label)\n const blobAccess: BackupBlobAccessLevel = options.blobAccess ?? 'public'\n const envMedia = (process.env.BLOB_READ_WRITE_TOKEN || '').trim()\n const token = resolveBlobToken(blobToken)\n const skip = new Set(skipCollections ?? [])\n const resolvedBackupsToKeep = backupsToKeep ?? (Number(process.env.BACKUPS_TO_KEEP) || 10)\n\n const currentHostname = getCurrentHostname()\n const currentDbName = getCurrentDbName()\n const type = cron ? 'cron' : 'manual'\n const t0 = Date.now()\n\n payload.logger.info(\n {\n type,\n blacklist: skipCollections?.length ?? 0,\n db: currentDbName,\n host: currentHostname,\n includeMedia,\n },\n '[backup] Starting backup',\n )\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: 'Starting backup',\n status: 'running',\n })\n }\n\n if (cron) {\n const { blobs } = await list({\n limit: 1000,\n prefix: 'backups/cron-',\n token,\n })\n const sorted = blobs.sort(\n (a, b) => new Date(b.uploadedAt).getTime() - new Date(a.uploadedAt).getTime(),\n )\n const oldest = sorted.slice(resolvedBackupsToKeep - 1)\n if (oldest.length > 0) {\n payload.logger.info({ count: oldest.length }, '[backup] Pruning old cron backups')\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: `Pruning ${oldest.length} old cron backup${oldest.length === 1 ? '' : 's'}`,\n })\n }\n }\n for (const blob of oldest) {\n await del(blob.url, { token })\n payload.logger.info({ pathname: blob.pathname }, '[backup] Deleted old backup')\n }\n }\n\n const db = getDb(payload)\n const collections = await db.listCollections().toArray()\n payload.logger.info({ collections: collections.length }, '[backup] Dumping collections')\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: `Dumping ${collections.length} collection${collections.length === 1 ? '' : 's'}`,\n })\n }\n\n const allData: Record<string, Record<string, unknown>[]> = {}\n for (const collection of collections) {\n if (skip.has(collection.name)) {\n payload.logger.debug(\n { collection: collection.name },\n '[backup] Skipping blacklisted collection',\n )\n continue\n }\n allData[collection.name] = (await db.collection(collection.name).find({}).toArray()) as Record<\n string,\n unknown\n >[]\n payload.logger.debug(\n { collection: collection.name, docs: allData[collection.name].length },\n '[backup] Collection dumped',\n )\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: `Dumped collection ${collection.name} (${allData[collection.name].length} docs)`,\n })\n }\n }\n\n const collectionBackupFile = EJSON.stringify(allData)\n const includedCollectionCount = Object.keys(allData).length\n const backupTimestampMs = Date.now()\n\n if (includeMedia) {\n payload.logger.info('[backup] Bundling media files into archive')\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: 'Bundling media files into archive',\n })\n }\n }\n\n const backupFile =\n includeMedia ?\n await createMediaBackupFile(\n collectionBackupFile,\n (allData?.['media'] as { filename: string }[] | undefined) || [],\n token,\n envMedia.length > 0 ? envMedia : undefined,\n payload,\n )\n : collectionBackupFile\n const name = `backups/${createBlobName(\n type,\n currentDbName,\n currentHostname,\n includedCollectionCount,\n backupTimestampMs,\n includeMedia ? 'tar.gz' : 'json',\n label || undefined,\n )}`\n\n payload.logger.info({ name }, '[backup] Uploading backup to blob storage')\n if (taskId) {\n await updateBackupTask(payload, taskId, {\n message: 'Uploading backup to blob storage',\n })\n }\n const effectiveAccess = await putBackupBlobContent(name, backupFile, token, blobAccess)\n if (effectiveAccess !== blobAccess) {\n payload.logger.warn(\n { name, effectiveAccess, preferredAccess: blobAccess },\n '[backup] Blob store rejected preferred access level; uploaded with fallback',\n )\n }\n\n payload.logger.info({ name, durationMs: Date.now() - t0 }, '[backup] Backup complete')\n}\n"],"names":["del","list","EJSON","createBlobName","getCurrentDbName","getCurrentHostname","sanitizeBackupLabel","createTarGzip","putBackupBlobContent","readBackupBlobContentFlexible","getResolvedCronBackupSettings","resolveBackupBlobToken","getDb","updateBackupTask","COLLECTION_FILE_NAME","resolveBackupListToken","payload","explicitToken","t","trim","listBackups","options","token","blobToken","blobs","limit","prefix","resolveBlobToken","length","undefined","createMediaBackupFile","collectionBackupFile","mediaCollection","backupBlobToken","mediaListToken","envMedia","process","env","BLOB_READ_WRITE_TOKEN","tokenForMedia","mediaFiles","Promise","all","map","media","matchingFiles","filename","blob","find","pathname","logger","warn","content","downloadUrl","name","Buffer","from","filter","Boolean","createBackup","backupsToKeep","cron","includeMedia","skipCollections","taskId","label","blobAccess","skip","Set","resolvedBackupsToKeep","Number","BACKUPS_TO_KEEP","currentHostname","currentDbName","type","t0","Date","now","info","blacklist","db","host","message","status","sorted","sort","a","b","uploadedAt","getTime","oldest","slice","count","url","collections","listCollections","toArray","allData","collection","has","debug","docs","stringify","includedCollectionCount","Object","keys","backupTimestampMs","backupFile","effectiveAccess","preferredAccess","durationMs"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/backupBlobIO.ts"],"sourcesContent":["import { put } from '@vercel/blob'\n\n/** Vercel Blob access for backup archives under `backups/`. */\nexport type BackupBlobAccessLevel = 'private' | 'public'\n\n/**\n * Returns the blob pathname (no leading slash) for a trusted Vercel Blob HTTPS URL, or null.\n * The URL's path component is percent-decoded once so the result matches the literal pathname\n * stored by Vercel Blob (e.g. blobs whose own pathname contains `%2F` are served from a URL that\n * double-encodes it to `%252F`).\n */\nexport function vercelBlobPathnameFromUrl(urlStr: string): null | string {\n try {\n const u = new URL(urlStr)\n if (u.protocol !== 'https:') {return null}\n if (!u.hostname.endsWith('.blob.vercel-storage.com')) {return null}\n const raw = u.pathname.replace(/^\\//, '')\n if (raw.length === 0) {return null}\n try {\n return decodeURIComponent(raw)\n } catch {\n return raw\n }\n } catch {\n return null\n }\n}\n\nexport function isTrustedBackupBlobReference(urlStr: string, expectedPathname: string): boolean {\n const fromUrl = vercelBlobPathnameFromUrl(urlStr)\n return Boolean(fromUrl && fromUrl === expectedPathname)\n}\n\n/** True when the URL points to any Vercel Blob host (public or private). */\nfunction isVercelBlobHost(urlStr: string): boolean {\n try {\n const u = new URL(urlStr)\n return u.protocol === 'https:' && u.hostname.endsWith('.blob.vercel-storage.com')\n } catch {\n return false\n }\n}\n\n/**\n * Fetches a Vercel Blob URL with the Bearer token attached when the host belongs to Vercel Blob.\n * Works transparently for public stores (token is ignored) and private stores (token is required).\n * Tokens are never sent to non-Vercel hosts.\n */\nasync function fetchBackupBlobUrl(url: string, token: string): Promise<Response> {\n const init: RequestInit | undefined =\n token && isVercelBlobHost(url)\n ? { headers: { authorization: `Bearer ${token}` } }\n : undefined\n return fetch(url, init)\n}\n\nexport type BackupBlobDownloadStream = {\n contentType: string\n stream: ReadableStream<Uint8Array>\n}\n\n/**\n * Opens a readable stream for a backup object (public or private). Prefers the `downloadUrl` /\n * `blobUrl` returned by `list()` because Vercel Blob already encodes the pathname correctly there\n * (some legacy backups store literal `%2F` sequences inside the pathname, which the SDK's\n * pathname-to-URL construction would misinterpret).\n */\nexport async function streamBackupBlobForDownload(options: {\n blobUrl?: null | string\n downloadUrl?: null | string\n pathname: string\n preferredAccess: BackupBlobAccessLevel\n token: string\n}): Promise<BackupBlobDownloadStream | null> {\n const { blobUrl, downloadUrl, pathname, token } = options\n const candidates: string[] = []\n if (downloadUrl && isTrustedBackupBlobReference(downloadUrl, pathname)) {\n candidates.push(downloadUrl)\n }\n if (blobUrl && isTrustedBackupBlobReference(blobUrl, pathname)) {\n candidates.push(blobUrl)\n }\n\n for (const target of candidates) {\n try {\n const res = await fetchBackupBlobUrl(target, token)\n if (res.ok && res.body) {\n return {\n contentType: res.headers.get('content-type') || 'application/octet-stream',\n stream: res.body,\n }\n }\n } catch {\n /* try next candidate */\n }\n }\n\n return null\n}\n\nexport async function bufferFromWebReadableStream(\n stream: ReadableStream<Uint8Array>,\n): Promise<Buffer> {\n const reader = stream.getReader()\n const chunks: Buffer[] = []\n while (true) {\n const { done, value } = await reader.read()\n if (done) {break}\n if (value) {chunks.push(Buffer.from(value))}\n }\n return Buffer.concat(chunks)\n}\n\n/**\n * Read a backup blob using the trusted `downloadUrl` returned by `list()`. The Bearer token is\n * always attached for Vercel Blob hosts; this works for public blobs (token ignored) and is\n * required for private blobs.\n */\nexport async function readBackupBlobContent(options: {\n /** Kept for API compatibility — the fetch path is the same for public and private stores. */\n access: BackupBlobAccessLevel\n downloadUrl: string\n pathname: string\n token: string\n}): Promise<Buffer> {\n const { downloadUrl, pathname, token } = options\n const res = await fetchBackupBlobUrl(downloadUrl, token)\n if (res.ok) {\n return Buffer.from(await res.arrayBuffer())\n }\n throw new Error(`Failed to read backup blob (${pathname}): HTTP ${res.status}`)\n}\n\n/**\n * Flexible read used during restore/preview/transfer where the source store visibility may differ\n * from the new one. Always tries authenticated access first (correct for private stores and\n * harmless for public ones). Falls back to anonymous fetch only when the URL is not a Vercel\n * Blob URL, so tokens never leak to arbitrary hosts.\n */\nexport async function readBackupBlobContentFlexible(\n pathname: string,\n downloadUrl: string,\n token: string,\n _sourceAccessHint?: BackupBlobAccessLevel,\n): Promise<Buffer> {\n try {\n const res = await fetchBackupBlobUrl(downloadUrl, token)\n if (res.ok) {return Buffer.from(await res.arrayBuffer())}\n } catch {\n /* try anonymous fallback */\n }\n\n if (!isVercelBlobHost(downloadUrl)) {\n try {\n const res = await fetch(downloadUrl)\n if (res.ok) {return Buffer.from(await res.arrayBuffer())}\n } catch {\n /* fall through to error */\n }\n }\n\n throw new Error(`Failed to read backup blob ${pathname}`)\n}\n\n/**\n * Uploads a backup blob with the preferred access level and transparently retries the other level\n * if the Vercel Blob store rejects the preferred one (e.g. legacy public-only stores reject\n * `access: 'private'`). Returns the effective access level that succeeded so callers can log/track\n * it; throws the original error if both attempts fail.\n */\nexport async function putBackupBlobContent(\n pathname: string,\n body: Buffer | string | Uint8Array,\n token: string | undefined,\n access: BackupBlobAccessLevel,\n): Promise<BackupBlobAccessLevel> {\n const order: BackupBlobAccessLevel[] =\n access === 'private' ? ['private', 'public'] : ['public', 'private']\n let firstError: unknown\n for (const attempt of order) {\n try {\n await put(pathname, body, {\n access: attempt,\n addRandomSuffix: false,\n allowOverwrite: true,\n ...(token ? { token } : {}),\n })\n return attempt\n } catch (error) {\n if (firstError === undefined) {firstError = error}\n if (!looksLikeAccessModeMismatch(error)) {throw error}\n }\n }\n throw firstError instanceof Error ? firstError : new Error('Failed to upload backup blob')\n}\n\n/** Heuristic for Vercel Blob errors that indicate the store rejects the chosen access level. */\nfunction looksLikeAccessModeMismatch(error: unknown): boolean {\n const msg =\n error instanceof Error\n ? error.message\n : typeof error === 'string'\n ? error\n : error == null\n ? ''\n : (() => {\n try {\n return JSON.stringify(error)\n } catch {\n return 'unknown'\n }\n })()\n const lower = msg.toLowerCase()\n return (\n lower.includes('access') &&\n (lower.includes('public') ||\n lower.includes('private') ||\n lower.includes('not support') ||\n lower.includes('invalid'))\n )\n}\n"],"names":["put","vercelBlobPathnameFromUrl","urlStr","u","URL","protocol","hostname","endsWith","raw","pathname","replace","length","decodeURIComponent","isTrustedBackupBlobReference","expectedPathname","fromUrl","Boolean","isVercelBlobHost","fetchBackupBlobUrl","url","token","init","headers","authorization","undefined","fetch","streamBackupBlobForDownload","options","blobUrl","downloadUrl","candidates","push","target","res","ok","body","contentType","get","stream","bufferFromWebReadableStream","reader","getReader","chunks","done","value","read","Buffer","from","concat","readBackupBlobContent","arrayBuffer","Error","status","readBackupBlobContentFlexible","_sourceAccessHint","putBackupBlobContent","access","order","firstError","attempt","addRandomSuffix","allowOverwrite","error","looksLikeAccessModeMismatch","msg","message","JSON","stringify","lower","toLowerCase","includes"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/core/backupBlobIO.ts"],"sourcesContent":["import { put } from '@vercel/blob'\n\n/** Vercel Blob access for backup archives under `backups/`. */\nexport type BackupBlobAccessLevel = 'private' | 'public'\n\n/**\n * Returns the blob pathname (no leading slash) for a trusted Vercel Blob HTTPS URL, or null.\n * The URL's path component is percent-decoded once so the result matches the literal pathname\n * stored by Vercel Blob (e.g. blobs whose own pathname contains `%2F` are served from a URL that\n * double-encodes it to `%252F`).\n */\nexport function vercelBlobPathnameFromUrl(urlStr: string): null | string {\n try {\n const u = new URL(urlStr)\n if (u.protocol !== 'https:') {\n return null\n }\n if (!u.hostname.endsWith('.blob.vercel-storage.com')) {\n return null\n }\n const raw = u.pathname.replace(/^\\//, '')\n if (raw.length === 0) {\n return null\n }\n try {\n return decodeURIComponent(raw)\n } catch {\n return raw\n }\n } catch {\n return null\n }\n}\n\nexport function isTrustedBackupBlobReference(urlStr: string, expectedPathname: string): boolean {\n const fromUrl = vercelBlobPathnameFromUrl(urlStr)\n return Boolean(fromUrl && fromUrl === expectedPathname)\n}\n\n/** True when the URL points to any Vercel Blob host (public or private). */\nfunction isVercelBlobHost(urlStr: string): boolean {\n try {\n const u = new URL(urlStr)\n return u.protocol === 'https:' && u.hostname.endsWith('.blob.vercel-storage.com')\n } catch {\n return false\n }\n}\n\n/**\n * Fetches a Vercel Blob URL with the Bearer token attached when the host belongs to Vercel Blob.\n * Works transparently for public stores (token is ignored) and private stores (token is required).\n * Tokens are never sent to non-Vercel hosts.\n */\nasync function fetchBackupBlobUrl(url: string, token: string): Promise<Response> {\n const init: RequestInit | undefined =\n token && isVercelBlobHost(url) ? { headers: { authorization: `Bearer ${token}` } } : undefined\n return fetch(url, init)\n}\n\nexport type BackupBlobDownloadStream = {\n contentType: string\n stream: ReadableStream<Uint8Array>\n}\n\n/**\n * Opens a readable stream for a backup object (public or private). Prefers the `downloadUrl` /\n * `blobUrl` returned by `list()` because Vercel Blob already encodes the pathname correctly there\n * (some legacy backups store literal `%2F` sequences inside the pathname, which the SDK's\n * pathname-to-URL construction would misinterpret).\n */\nexport async function streamBackupBlobForDownload(options: {\n blobUrl?: null | string\n downloadUrl?: null | string\n pathname: string\n preferredAccess: BackupBlobAccessLevel\n token: string\n}): Promise<BackupBlobDownloadStream | null> {\n const { blobUrl, downloadUrl, pathname, token } = options\n const candidates: string[] = []\n if (downloadUrl && isTrustedBackupBlobReference(downloadUrl, pathname)) {\n candidates.push(downloadUrl)\n }\n if (blobUrl && isTrustedBackupBlobReference(blobUrl, pathname)) {\n candidates.push(blobUrl)\n }\n\n for (const target of candidates) {\n try {\n const res = await fetchBackupBlobUrl(target, token)\n if (res.ok && res.body) {\n return {\n contentType: res.headers.get('content-type') || 'application/octet-stream',\n stream: res.body,\n }\n }\n } catch {\n /* try next candidate */\n }\n }\n\n return null\n}\n\nexport async function bufferFromWebReadableStream(\n stream: ReadableStream<Uint8Array>,\n): Promise<Buffer> {\n const reader = stream.getReader()\n const chunks: Buffer[] = []\n while (true) {\n const { done, value } = await reader.read()\n if (done) {\n break\n }\n if (value) {\n chunks.push(Buffer.from(value))\n }\n }\n return Buffer.concat(chunks)\n}\n\n/**\n * Read a backup blob using the trusted `downloadUrl` returned by `list()`. The Bearer token is\n * always attached for Vercel Blob hosts; this works for public blobs (token ignored) and is\n * required for private blobs.\n */\nexport async function readBackupBlobContent(options: {\n /** Kept for API compatibility — the fetch path is the same for public and private stores. */\n access: BackupBlobAccessLevel\n downloadUrl: string\n pathname: string\n token: string\n}): Promise<Buffer> {\n const { downloadUrl, pathname, token } = options\n const res = await fetchBackupBlobUrl(downloadUrl, token)\n if (res.ok) {\n return Buffer.from(await res.arrayBuffer())\n }\n throw new Error(`Failed to read backup blob (${pathname}): HTTP ${res.status}`)\n}\n\n/**\n * Flexible read used during restore/preview/transfer where the source store visibility may differ\n * from the new one. Always tries authenticated access first (correct for private stores and\n * harmless for public ones). Falls back to anonymous fetch only when the URL is not a Vercel\n * Blob URL, so tokens never leak to arbitrary hosts.\n */\nexport async function readBackupBlobContentFlexible(\n pathname: string,\n downloadUrl: string,\n token: string,\n _sourceAccessHint?: BackupBlobAccessLevel,\n): Promise<Buffer> {\n try {\n const res = await fetchBackupBlobUrl(downloadUrl, token)\n if (res.ok) {\n return Buffer.from(await res.arrayBuffer())\n }\n } catch {\n /* try anonymous fallback */\n }\n\n if (!isVercelBlobHost(downloadUrl)) {\n try {\n const res = await fetch(downloadUrl)\n if (res.ok) {\n return Buffer.from(await res.arrayBuffer())\n }\n } catch {\n /* fall through to error */\n }\n }\n\n throw new Error(`Failed to read backup blob ${pathname}`)\n}\n\n/**\n * Uploads a backup blob with the preferred access level and transparently retries the other level\n * if the Vercel Blob store rejects the preferred one (e.g. legacy public-only stores reject\n * `access: 'private'`). Returns the effective access level that succeeded so callers can log/track\n * it; throws the original error if both attempts fail.\n */\nexport async function putBackupBlobContent(\n pathname: string,\n body: Buffer | string | Uint8Array,\n token: string | undefined,\n access: BackupBlobAccessLevel,\n): Promise<BackupBlobAccessLevel> {\n const order: BackupBlobAccessLevel[] =\n access === 'private' ? ['private', 'public'] : ['public', 'private']\n let firstError: unknown\n for (const attempt of order) {\n try {\n await put(pathname, body, {\n access: attempt,\n addRandomSuffix: false,\n allowOverwrite: true,\n ...(token ? { token } : {}),\n })\n return attempt\n } catch (error) {\n if (firstError === undefined) {\n firstError = error\n }\n if (!looksLikeAccessModeMismatch(error)) {\n throw error\n }\n }\n }\n throw firstError instanceof Error ? firstError : new Error('Failed to upload backup blob')\n}\n\n/** Heuristic for Vercel Blob errors that indicate the store rejects the chosen access level. */\nfunction looksLikeAccessModeMismatch(error: unknown): boolean {\n const msg =\n error instanceof Error ? error.message\n : typeof error === 'string' ? error\n : error == null ? ''\n : (() => {\n try {\n return JSON.stringify(error)\n } catch {\n return 'unknown'\n }\n })()\n const lower = msg.toLowerCase()\n return (\n lower.includes('access') &&\n (lower.includes('public') ||\n lower.includes('private') ||\n lower.includes('not support') ||\n lower.includes('invalid'))\n )\n}\n"],"names":["put","vercelBlobPathnameFromUrl","urlStr","u","URL","protocol","hostname","endsWith","raw","pathname","replace","length","decodeURIComponent","isTrustedBackupBlobReference","expectedPathname","fromUrl","Boolean","isVercelBlobHost","fetchBackupBlobUrl","url","token","init","headers","authorization","undefined","fetch","streamBackupBlobForDownload","options","blobUrl","downloadUrl","candidates","push","target","res","ok","body","contentType","get","stream","bufferFromWebReadableStream","reader","getReader","chunks","done","value","read","Buffer","from","concat","readBackupBlobContent","arrayBuffer","Error","status","readBackupBlobContentFlexible","_sourceAccessHint","putBackupBlobContent","access","order","firstError","attempt","addRandomSuffix","allowOverwrite","error","looksLikeAccessModeMismatch","msg","message","JSON","stringify","lower","toLowerCase","includes"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/backupSettings.ts"],"sourcesContent":["import type { Payload } from 'payload'\n\nexport const BACKUP_SETTINGS_SLUG = 'backup-settings'\n\nexport interface ResolvedCronBackupSettings {\n /**\n * Validated access level of the current backup blob store, populated via\n * `validateBackupBlobToken`. `null` when validation has never run (default token use is still\n * treated as `public`, dedicated token without validation falls back to heuristic private).\n */\n backupBlobAccess: 'private' | 'public' | null\n backupBlobReadWriteToken: string\n backupsToKeep: number\n id: string\n includeMediaForCron: boolean\n skipMongoCollections: string[]\n}\n\nfunction defaultBackupsToKeep(): number {\n const n = Number(process.env.BACKUPS_TO_KEEP)\n return Number.isFinite(n) && n > 0 ? Math.floor(n) : 10\n}\n\nexport function normalizeSkipMongoCollections(raw: unknown): string[] {\n if (!Array.isArray(raw)) {return []}\n const out: string[] = []\n for (const entry of raw) {\n if (typeof entry === 'string' && entry.length > 0 && entry.length < 512) {\n out.push(entry)\n continue\n }\n if (entry && typeof entry === 'object' && 'name' in entry) {\n const name = (entry as { name?: unknown }).name\n if (typeof name === 'string' && name.length > 0 && name.length < 512) {out.push(name)}\n }\n }\n return out\n}\n\nexport function toPayloadSkipRows(names: string[]): { name: string }[] {\n return names.map((name) => ({ name }))\n}\n\nexport async function getResolvedCronBackupSettings(\n payload: Payload,\n): Promise<ResolvedCronBackupSettings> {\n const res = await payload.find({\n collection: BACKUP_SETTINGS_SLUG,\n depth: 0,\n limit: 1,\n overrideAccess: true,\n })\n const doc = res.docs[0] as unknown as Record<string, unknown> | undefined\n if (!doc) {\n return {\n id: '',\n backupBlobAccess: null,\n backupBlobReadWriteToken: '',\n backupsToKeep: defaultBackupsToKeep(),\n includeMediaForCron: true,\n skipMongoCollections: [],\n }\n }\n const rawKeep = doc.backupsToKeep\n const backupsToKeep =\n typeof rawKeep === 'number' && Number.isFinite(rawKeep)\n ? Math.min(365, Math.max(1, Math.floor(rawKeep)))\n : defaultBackupsToKeep()\n\n const rawAccess = typeof doc.backupBlobAccess === 'string' ? doc.backupBlobAccess : null\n const backupBlobAccess: 'private' | 'public' | null =\n rawAccess === 'public' || rawAccess === 'private' ? rawAccess : null\n\n return {\n id: String(doc.id),\n backupBlobAccess,\n backupBlobReadWriteToken:\n typeof doc.backupBlobReadWriteToken === 'string' ? doc.backupBlobReadWriteToken : '',\n backupsToKeep,\n includeMediaForCron: doc.includeMediaForCron === true,\n skipMongoCollections: normalizeSkipMongoCollections(doc.skipMongoCollections),\n }\n}\n\nexport function resolveBackupBlobToken(settings: ResolvedCronBackupSettings): string {\n const fromSettings = settings.backupBlobReadWriteToken.trim()\n if (fromSettings.length > 0) {return fromSettings}\n return process.env.BLOB_READ_WRITE_TOKEN || ''\n}\n\n/**\n * Vercel Blob `access` for new backup archives. Prefers the detected + persisted access level from\n * {@link validateBackupBlobToken}; otherwise heuristically treats a dedicated backup token as\n * `private` and the default env token as `public`.\n */\nexport function resolveBackupBlobAccess(\n settings: ResolvedCronBackupSettings,\n): 'private' | 'public' {\n if (settings.backupBlobAccess === 'public' || settings.backupBlobAccess === 'private') {\n return settings.backupBlobAccess\n }\n return settings.backupBlobReadWriteToken.trim().length > 0 ? 'private' : 'public'\n}\n\n/**\n * When `pathname` is sent, restore/preview read via token + flexible SDK/fetch (works for public\n * and private blobs). Returns undefined if pathname is missing or invalid.\n */\nexport function resolveBackupArchiveRead(\n settings: ResolvedCronBackupSettings,\n pathname: unknown,\n): { pathname: string; token: string } | undefined {\n if (typeof pathname !== 'string' || !pathname.startsWith('backups/')) {return undefined}\n const token = resolveBackupBlobToken(settings).trim()\n if (!token) {return undefined}\n return { pathname, token }\n}\n"],"names":["BACKUP_SETTINGS_SLUG","defaultBackupsToKeep","n","Number","process","env","BACKUPS_TO_KEEP","isFinite","Math","floor","normalizeSkipMongoCollections","raw","Array","isArray","out","entry","length","push","name","toPayloadSkipRows","names","map","getResolvedCronBackupSettings","payload","res","find","collection","depth","limit","overrideAccess","doc","docs","id","backupBlobAccess","backupBlobReadWriteToken","backupsToKeep","includeMediaForCron","skipMongoCollections","rawKeep","min","max","rawAccess","String","resolveBackupBlobToken","settings","fromSettings","trim","BLOB_READ_WRITE_TOKEN","resolveBackupBlobAccess","resolveBackupArchiveRead","pathname","startsWith","undefined","token"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/core/backupSettings.ts"],"sourcesContent":["import type { Payload } from 'payload'\n\nexport const BACKUP_SETTINGS_SLUG = 'backup-settings'\n\nexport interface ResolvedCronBackupSettings {\n /**\n * Validated access level of the current backup blob store, populated via\n * `validateBackupBlobToken`. `null` when validation has never run (default token use is still\n * treated as `public`, dedicated token without validation falls back to heuristic private).\n */\n backupBlobAccess: 'private' | 'public' | null\n backupBlobReadWriteToken: string\n backupsToKeep: number\n id: string\n includeMediaForCron: boolean\n skipMongoCollections: string[]\n}\n\nfunction defaultBackupsToKeep(): number {\n const n = Number(process.env.BACKUPS_TO_KEEP)\n return Number.isFinite(n) && n > 0 ? Math.floor(n) : 10\n}\n\nexport function normalizeSkipMongoCollections(raw: unknown): string[] {\n if (!Array.isArray(raw)) {\n return []\n }\n const out: string[] = []\n for (const entry of raw) {\n if (typeof entry === 'string' && entry.length > 0 && entry.length < 512) {\n out.push(entry)\n continue\n }\n if (entry && typeof entry === 'object' && 'name' in entry) {\n const name = (entry as { name?: unknown }).name\n if (typeof name === 'string' && name.length > 0 && name.length < 512) {\n out.push(name)\n }\n }\n }\n return out\n}\n\nexport function toPayloadSkipRows(names: string[]): { name: string }[] {\n return names.map((name) => ({ name }))\n}\n\nexport async function getResolvedCronBackupSettings(\n payload: Payload,\n): Promise<ResolvedCronBackupSettings> {\n const res = await payload.find({\n collection: BACKUP_SETTINGS_SLUG,\n depth: 0,\n limit: 1,\n overrideAccess: true,\n })\n const doc = res.docs[0] as unknown as Record<string, unknown> | undefined\n if (!doc) {\n return {\n id: '',\n backupBlobAccess: null,\n backupBlobReadWriteToken: '',\n backupsToKeep: defaultBackupsToKeep(),\n includeMediaForCron: true,\n skipMongoCollections: [],\n }\n }\n const rawKeep = doc.backupsToKeep\n const backupsToKeep =\n typeof rawKeep === 'number' && Number.isFinite(rawKeep) ?\n Math.min(365, Math.max(1, Math.floor(rawKeep)))\n : defaultBackupsToKeep()\n\n const rawAccess = typeof doc.backupBlobAccess === 'string' ? doc.backupBlobAccess : null\n const backupBlobAccess: 'private' | 'public' | null =\n rawAccess === 'public' || rawAccess === 'private' ? rawAccess : null\n\n return {\n id: String(doc.id),\n backupBlobAccess,\n backupBlobReadWriteToken:\n typeof doc.backupBlobReadWriteToken === 'string' ? doc.backupBlobReadWriteToken : '',\n backupsToKeep,\n includeMediaForCron: doc.includeMediaForCron === true,\n skipMongoCollections: normalizeSkipMongoCollections(doc.skipMongoCollections),\n }\n}\n\nexport function resolveBackupBlobToken(settings: ResolvedCronBackupSettings): string {\n const fromSettings = settings.backupBlobReadWriteToken.trim()\n if (fromSettings.length > 0) {\n return fromSettings\n }\n return process.env.BLOB_READ_WRITE_TOKEN || ''\n}\n\n/**\n * Vercel Blob `access` for new backup archives. Prefers the detected + persisted access level from\n * {@link validateBackupBlobToken}; otherwise heuristically treats a dedicated backup token as\n * `private` and the default env token as `public`.\n */\nexport function resolveBackupBlobAccess(\n settings: ResolvedCronBackupSettings,\n): 'private' | 'public' {\n if (settings.backupBlobAccess === 'public' || settings.backupBlobAccess === 'private') {\n return settings.backupBlobAccess\n }\n return settings.backupBlobReadWriteToken.trim().length > 0 ? 'private' : 'public'\n}\n\n/**\n * When `pathname` is sent, restore/preview read via token + flexible SDK/fetch (works for public\n * and private blobs). Returns undefined if pathname is missing or invalid.\n */\nexport function resolveBackupArchiveRead(\n settings: ResolvedCronBackupSettings,\n pathname: unknown,\n): { pathname: string; token: string } | undefined {\n if (typeof pathname !== 'string' || !pathname.startsWith('backups/')) {\n return undefined\n }\n const token = resolveBackupBlobToken(settings).trim()\n if (!token) {\n return undefined\n }\n return { pathname, token }\n}\n"],"names":["BACKUP_SETTINGS_SLUG","defaultBackupsToKeep","n","Number","process","env","BACKUPS_TO_KEEP","isFinite","Math","floor","normalizeSkipMongoCollections","raw","Array","isArray","out","entry","length","push","name","toPayloadSkipRows","names","map","getResolvedCronBackupSettings","payload","res","find","collection","depth","limit","overrideAccess","doc","docs","id","backupBlobAccess","backupBlobReadWriteToken","backupsToKeep","includeMediaForCron","skipMongoCollections","rawKeep","min","max","rawAccess","String","resolveBackupBlobToken","settings","fromSettings","trim","BLOB_READ_WRITE_TOKEN","resolveBackupBlobAccess","resolveBackupArchiveRead","pathname","startsWith","undefined","token"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/blobTokenValidate.ts"],"sourcesContent":["import { del, put } from '@vercel/blob'\n\nimport type { BackupBlobAccessLevel } from './backupBlobIO'\n\nexport interface BackupBlobTokenValidation {\n /** Detected access level the store accepts (set when ok). */\n access?: BackupBlobAccessLevel\n /** Short error description when validation fails. */\n error?: string\n ok: boolean\n}\n\nconst PROBE_PATH_PREFIX = 'backups/.backup-mongodb-probe-'\n\nfunction looksLikeAccessModeRejection(error: unknown): boolean {\n const msg =\n error instanceof Error\n ? error.message\n : typeof error === 'string'\n ? error\n : error == null\n ? ''\n : (() => {\n try {\n return JSON.stringify(error)\n } catch {\n return 'unknown'\n }\n })()\n const lower = msg.toLowerCase()\n if (!lower.includes('access')) {\n return false\n }\n return (\n lower.includes('public') ||\n lower.includes('private') ||\n lower.includes('not support') ||\n lower.includes('invalid')\n )\n}\n\n/**\n * Probes a Vercel Blob read/write token by uploading a tiny file and deleting it afterwards. Tries\n * `private` first and falls back to `public` so the caller sees which access modes the store\n * accepts. Non-access errors (auth, network, quota…) are reported as invalid.\n */\nexport async function validateBackupBlobToken(token: string): Promise<BackupBlobTokenValidation> {\n const trimmed = token.trim()\n if (!trimmed) {\n return { error: 'Token is empty', ok: false }\n }\n\n const probePath = `${PROBE_PATH_PREFIX}${Date.now().toString(36)}-${Math.random()\n .toString(36)\n .slice(2, 10)}.txt`\n const probeBody = 'backup-mongodb-plugin-probe'\n\n let firstError: unknown\n for (const access of ['private', 'public'] as const) {\n let uploadedUrl: string | undefined\n try {\n const result = await put(probePath, probeBody, {\n access,\n addRandomSuffix: false,\n token: trimmed,\n })\n uploadedUrl = result?.url\n return { access, ok: true }\n } catch (error) {\n if (firstError === undefined) {\n firstError = error\n }\n if (!looksLikeAccessModeRejection(error)) {\n return { error: error instanceof Error ? error.message : 'Token rejected', ok: false }\n }\n } finally {\n if (uploadedUrl) {\n try {\n await del(uploadedUrl, { token: trimmed })\n } catch {\n /* best-effort cleanup */\n }\n }\n }\n }\n\n return {\n error:\n firstError instanceof Error\n ? firstError.message\n : 'Token rejected for both public and private access',\n ok: false,\n }\n}\n"],"names":["del","put","PROBE_PATH_PREFIX","looksLikeAccessModeRejection","error","msg","Error","message","JSON","stringify","lower","toLowerCase","includes","validateBackupBlobToken","token","trimmed","trim","ok","probePath","Date","now","toString","Math","random","slice","probeBody","firstError","access","uploadedUrl","result","addRandomSuffix","url","undefined"],"mappings":"AAAA,SAASA,GAAG,EAAEC,GAAG,QAAQ,eAAc;AAYvC,MAAMC,oBAAoB;AAE1B,SAASC,6BAA6BC,KAAc;IAClD,MAAMC,MACJD,iBAAiBE,QACbF,MAAMG,OAAO,GACb,OAAOH,UAAU,WACfA,QACAA,SAAS,OACP,KACA,AAAC,CAAA;QACC,IAAI;YACF,OAAOI,KAAKC,SAAS,CAACL;QACxB,EAAE,OAAM;YACN,OAAO;QACT;IACF,CAAA;IACV,MAAMM,QAAQL,IAAIM,WAAW;IAC7B,IAAI,CAACD,MAAME,QAAQ,CAAC,WAAW;QAC7B,OAAO;IACT;IACA,OACEF,MAAME,QAAQ,CAAC,aACfF,MAAME,QAAQ,CAAC,cACfF,MAAME,QAAQ,CAAC,kBACfF,MAAME,QAAQ,CAAC;AAEnB;AAEA;;;;CAIC,GACD,OAAO,eAAeC,wBAAwBC,KAAa;IACzD,MAAMC,UAAUD,MAAME,IAAI;IAC1B,IAAI,CAACD,SAAS;QACZ,OAAO;YAAEX,OAAO;YAAkBa,IAAI;QAAM;IAC9C;IAEA,MAAMC,YAAY,GAAGhB,oBAAoBiB,KAAKC,GAAG,GAAGC,QAAQ,CAAC,IAAI,CAAC,EAAEC,KAAKC,MAAM,GAC5EF,QAAQ,CAAC,IACTG,KAAK,CAAC,GAAG,IAAI,IAAI,CAAC;IACrB,MAAMC,YAAY;IAElB,IAAIC;IACJ,KAAK,MAAMC,UAAU;QAAC;QAAW;KAAS,CAAW;QACnD,IAAIC;QACJ,IAAI;YACF,MAAMC,SAAS,MAAM5B,IAAIiB,WAAWO,WAAW;gBAC7CE;gBACAG,iBAAiB;gBACjBhB,OAAOC;YACT;YACAa,cAAcC,QAAQE;YACtB,OAAO;gBAAEJ;gBAAQV,IAAI;YAAK;QAC5B,EAAE,OAAOb,OAAO;YACd,IAAIsB,eAAeM,WAAW;gBAC5BN,aAAatB;YACf;YACA,IAAI,CAACD,6BAA6BC,QAAQ;gBACxC,OAAO;oBAAEA,OAAOA,iBAAiBE,QAAQF,MAAMG,OAAO,GAAG;oBAAkBU,IAAI;gBAAM;YACvF;QACF,SAAU;YACR,IAAIW,aAAa;gBACf,IAAI;oBACF,MAAM5B,IAAI4B,aAAa;wBAAEd,OAAOC;oBAAQ;gBAC1C,EAAE,OAAM;gBACN,uBAAuB,GACzB;YACF;QACF;IACF;IAEA,OAAO;QACLX,OACEsB,sBAAsBpB,QAClBoB,WAAWnB,OAAO,GAClB;QACNU,IAAI;IACN;AACF"}
1
+ {"version":3,"sources":["../../src/core/blobTokenValidate.ts"],"sourcesContent":["import { del, put } from '@vercel/blob'\n\nimport type { BackupBlobAccessLevel } from './backupBlobIO'\n\nexport interface BackupBlobTokenValidation {\n /** Detected access level the store accepts (set when ok). */\n access?: BackupBlobAccessLevel\n /** Short error description when validation fails. */\n error?: string\n ok: boolean\n}\n\nconst PROBE_PATH_PREFIX = 'backups/.backup-mongodb-probe-'\n\nfunction looksLikeAccessModeRejection(error: unknown): boolean {\n const msg =\n error instanceof Error ? error.message\n : typeof error === 'string' ? error\n : error == null ? ''\n : (() => {\n try {\n return JSON.stringify(error)\n } catch {\n return 'unknown'\n }\n })()\n const lower = msg.toLowerCase()\n if (!lower.includes('access')) {\n return false\n }\n return (\n lower.includes('public') ||\n lower.includes('private') ||\n lower.includes('not support') ||\n lower.includes('invalid')\n )\n}\n\n/**\n * Probes a Vercel Blob read/write token by uploading a tiny file and deleting it afterwards. Tries\n * `private` first and falls back to `public` so the caller sees which access modes the store\n * accepts. Non-access errors (auth, network, quota…) are reported as invalid.\n */\nexport async function validateBackupBlobToken(token: string): Promise<BackupBlobTokenValidation> {\n const trimmed = token.trim()\n if (!trimmed) {\n return { error: 'Token is empty', ok: false }\n }\n\n const probePath = `${PROBE_PATH_PREFIX}${Date.now().toString(36)}-${Math.random()\n .toString(36)\n .slice(2, 10)}.txt`\n const probeBody = 'backup-mongodb-plugin-probe'\n\n let firstError: unknown\n for (const access of ['private', 'public'] as const) {\n let uploadedUrl: string | undefined\n try {\n const result = await put(probePath, probeBody, {\n access,\n addRandomSuffix: false,\n token: trimmed,\n })\n uploadedUrl = result?.url\n return { access, ok: true }\n } catch (error) {\n if (firstError === undefined) {\n firstError = error\n }\n if (!looksLikeAccessModeRejection(error)) {\n return { error: error instanceof Error ? error.message : 'Token rejected', ok: false }\n }\n } finally {\n if (uploadedUrl) {\n try {\n await del(uploadedUrl, { token: trimmed })\n } catch {\n /* best-effort cleanup */\n }\n }\n }\n }\n\n return {\n error:\n firstError instanceof Error ?\n firstError.message\n : 'Token rejected for both public and private access',\n ok: false,\n }\n}\n"],"names":["del","put","PROBE_PATH_PREFIX","looksLikeAccessModeRejection","error","msg","Error","message","JSON","stringify","lower","toLowerCase","includes","validateBackupBlobToken","token","trimmed","trim","ok","probePath","Date","now","toString","Math","random","slice","probeBody","firstError","access","uploadedUrl","result","addRandomSuffix","url","undefined"],"mappings":"AAAA,SAASA,GAAG,EAAEC,GAAG,QAAQ,eAAc;AAYvC,MAAMC,oBAAoB;AAE1B,SAASC,6BAA6BC,KAAc;IAClD,MAAMC,MACJD,iBAAiBE,QAAQF,MAAMG,OAAO,GACpC,OAAOH,UAAU,WAAWA,QAC5BA,SAAS,OAAO,KAChB,AAAC,CAAA;QACC,IAAI;YACF,OAAOI,KAAKC,SAAS,CAACL;QACxB,EAAE,OAAM;YACN,OAAO;QACT;IACF,CAAA;IACJ,MAAMM,QAAQL,IAAIM,WAAW;IAC7B,IAAI,CAACD,MAAME,QAAQ,CAAC,WAAW;QAC7B,OAAO;IACT;IACA,OACEF,MAAME,QAAQ,CAAC,aACfF,MAAME,QAAQ,CAAC,cACfF,MAAME,QAAQ,CAAC,kBACfF,MAAME,QAAQ,CAAC;AAEnB;AAEA;;;;CAIC,GACD,OAAO,eAAeC,wBAAwBC,KAAa;IACzD,MAAMC,UAAUD,MAAME,IAAI;IAC1B,IAAI,CAACD,SAAS;QACZ,OAAO;YAAEX,OAAO;YAAkBa,IAAI;QAAM;IAC9C;IAEA,MAAMC,YAAY,GAAGhB,oBAAoBiB,KAAKC,GAAG,GAAGC,QAAQ,CAAC,IAAI,CAAC,EAAEC,KAAKC,MAAM,GAC5EF,QAAQ,CAAC,IACTG,KAAK,CAAC,GAAG,IAAI,IAAI,CAAC;IACrB,MAAMC,YAAY;IAElB,IAAIC;IACJ,KAAK,MAAMC,UAAU;QAAC;QAAW;KAAS,CAAW;QACnD,IAAIC;QACJ,IAAI;YACF,MAAMC,SAAS,MAAM5B,IAAIiB,WAAWO,WAAW;gBAC7CE;gBACAG,iBAAiB;gBACjBhB,OAAOC;YACT;YACAa,cAAcC,QAAQE;YACtB,OAAO;gBAAEJ;gBAAQV,IAAI;YAAK;QAC5B,EAAE,OAAOb,OAAO;YACd,IAAIsB,eAAeM,WAAW;gBAC5BN,aAAatB;YACf;YACA,IAAI,CAACD,6BAA6BC,QAAQ;gBACxC,OAAO;oBAAEA,OAAOA,iBAAiBE,QAAQF,MAAMG,OAAO,GAAG;oBAAkBU,IAAI;gBAAM;YACvF;QACF,SAAU;YACR,IAAIW,aAAa;gBACf,IAAI;oBACF,MAAM5B,IAAI4B,aAAa;wBAAEd,OAAOC;oBAAQ;gBAC1C,EAAE,OAAM;gBACN,uBAAuB,GACzB;YACF;QACF;IACF;IAEA,OAAO;QACLX,OACEsB,sBAAsBpB,QACpBoB,WAAWnB,OAAO,GAClB;QACJU,IAAI;IACN;AACF"}
@@ -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 = 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 = 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 = restoreArchiveMedia\n ? files.filter((file) => file.name !== COLLECTION_FILE_NAME)\n : []\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: 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'\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 +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 = 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 = options.preferredLocales?.length\n ? options.preferredLocales\n : ['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":"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"}
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":"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"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/vercelBackupCron.ts"],"sourcesContent":["import cronstrue from 'cronstrue'\nimport fs from 'node:fs'\nimport path from 'node:path'\n\nexport interface VercelBackupCronInfo {\n /** Path relative to the app root (where `vercel.json` lives). */\n configFileRelative: string\n path: string\n schedule: string\n}\n\nexport function readVercelBackupCronFromRepo(): null | VercelBackupCronInfo {\n const configFileRelative = 'vercel.json'\n try {\n const abs = path.join(process.cwd(), configFileRelative)\n if (!fs.existsSync(abs)) {return null}\n const raw = fs.readFileSync(abs, 'utf8')\n const parsed = JSON.parse(raw) as { crons?: Array<{ path?: string; schedule?: string }> }\n const crons = parsed.crons\n if (!Array.isArray(crons)) {return null}\n const hit = crons.find(\n (c) =>\n typeof c?.path === 'string' &&\n c.path.includes('backup-mongodb/cron') &&\n typeof c?.schedule === 'string',\n )\n if (!hit?.schedule || !hit.path) {return null}\n return { configFileRelative, path: hit.path, schedule: hit.schedule }\n } catch {\n return null\n }\n}\n\nexport function describeCronSchedule(schedule: string): null | string {\n try {\n return cronstrue.toString(schedule)\n } catch {\n return null\n }\n}\n"],"names":["cronstrue","fs","path","readVercelBackupCronFromRepo","configFileRelative","abs","join","process","cwd","existsSync","raw","readFileSync","parsed","JSON","parse","crons","Array","isArray","hit","find","c","includes","schedule","describeCronSchedule","toString"],"mappings":"AAAA,OAAOA,eAAe,YAAW;AACjC,OAAOC,QAAQ,UAAS;AACxB,OAAOC,UAAU,YAAW;AAS5B,OAAO,SAASC;IACd,MAAMC,qBAAqB;IAC3B,IAAI;QACF,MAAMC,MAAMH,KAAKI,IAAI,CAACC,QAAQC,GAAG,IAAIJ;QACrC,IAAI,CAACH,GAAGQ,UAAU,CAACJ,MAAM;YAAC,OAAO;QAAI;QACrC,MAAMK,MAAMT,GAAGU,YAAY,CAACN,KAAK;QACjC,MAAMO,SAASC,KAAKC,KAAK,CAACJ;QAC1B,MAAMK,QAAQH,OAAOG,KAAK;QAC1B,IAAI,CAACC,MAAMC,OAAO,CAACF,QAAQ;YAAC,OAAO;QAAI;QACvC,MAAMG,MAAMH,MAAMI,IAAI,CACpB,CAACC,IACC,OAAOA,GAAGlB,SAAS,YACnBkB,EAAElB,IAAI,CAACmB,QAAQ,CAAC,0BAChB,OAAOD,GAAGE,aAAa;QAE3B,IAAI,CAACJ,KAAKI,YAAY,CAACJ,IAAIhB,IAAI,EAAE;YAAC,OAAO;QAAI;QAC7C,OAAO;YAAEE;YAAoBF,MAAMgB,IAAIhB,IAAI;YAAEoB,UAAUJ,IAAII,QAAQ;QAAC;IACtE,EAAE,OAAM;QACN,OAAO;IACT;AACF;AAEA,OAAO,SAASC,qBAAqBD,QAAgB;IACnD,IAAI;QACF,OAAOtB,UAAUwB,QAAQ,CAACF;IAC5B,EAAE,OAAM;QACN,OAAO;IACT;AACF"}
1
+ {"version":3,"sources":["../../src/core/vercelBackupCron.ts"],"sourcesContent":["import cronstrue from 'cronstrue'\nimport fs from 'node:fs'\nimport path from 'node:path'\n\nexport interface VercelBackupCronInfo {\n /** Path relative to the app root (where `vercel.json` lives). */\n configFileRelative: string\n path: string\n schedule: string\n}\n\nexport function readVercelBackupCronFromRepo(): null | VercelBackupCronInfo {\n const configFileRelative = 'vercel.json'\n try {\n const abs = path.join(process.cwd(), configFileRelative)\n if (!fs.existsSync(abs)) {\n return null\n }\n const raw = fs.readFileSync(abs, 'utf8')\n const parsed = JSON.parse(raw) as { crons?: Array<{ path?: string; schedule?: string }> }\n const crons = parsed.crons\n if (!Array.isArray(crons)) {\n return null\n }\n const hit = crons.find(\n (c) =>\n typeof c?.path === 'string' &&\n c.path.includes('backup-mongodb/cron') &&\n typeof c?.schedule === 'string',\n )\n if (!hit?.schedule || !hit.path) {\n return null\n }\n return { configFileRelative, path: hit.path, schedule: hit.schedule }\n } catch {\n return null\n }\n}\n\nexport function describeCronSchedule(schedule: string): null | string {\n try {\n return cronstrue.toString(schedule)\n } catch {\n return null\n }\n}\n"],"names":["cronstrue","fs","path","readVercelBackupCronFromRepo","configFileRelative","abs","join","process","cwd","existsSync","raw","readFileSync","parsed","JSON","parse","crons","Array","isArray","hit","find","c","includes","schedule","describeCronSchedule","toString"],"mappings":"AAAA,OAAOA,eAAe,YAAW;AACjC,OAAOC,QAAQ,UAAS;AACxB,OAAOC,UAAU,YAAW;AAS5B,OAAO,SAASC;IACd,MAAMC,qBAAqB;IAC3B,IAAI;QACF,MAAMC,MAAMH,KAAKI,IAAI,CAACC,QAAQC,GAAG,IAAIJ;QACrC,IAAI,CAACH,GAAGQ,UAAU,CAACJ,MAAM;YACvB,OAAO;QACT;QACA,MAAMK,MAAMT,GAAGU,YAAY,CAACN,KAAK;QACjC,MAAMO,SAASC,KAAKC,KAAK,CAACJ;QAC1B,MAAMK,QAAQH,OAAOG,KAAK;QAC1B,IAAI,CAACC,MAAMC,OAAO,CAACF,QAAQ;YACzB,OAAO;QACT;QACA,MAAMG,MAAMH,MAAMI,IAAI,CACpB,CAACC,IACC,OAAOA,GAAGlB,SAAS,YACnBkB,EAAElB,IAAI,CAACmB,QAAQ,CAAC,0BAChB,OAAOD,GAAGE,aAAa;QAE3B,IAAI,CAACJ,KAAKI,YAAY,CAACJ,IAAIhB,IAAI,EAAE;YAC/B,OAAO;QACT;QACA,OAAO;YAAEE;YAAoBF,MAAMgB,IAAIhB,IAAI;YAAEoB,UAAUJ,IAAII,QAAQ;QAAC;IACtE,EAAE,OAAM;QACN,OAAO;IACT;AACF;AAEA,OAAO,SAASC,qBAAqBD,QAAgB;IACnD,IAAI;QACF,OAAOtB,UAAUwB,QAAQ,CAACF;IAC5B,EAAE,OAAM;QACN,OAAO;IACT;AACF"}