11import { randomUUID } from "node:crypto" ;
2- import { constants as fsConstants } from "node:fs" ;
2+ import { constants as fsConstants , createWriteStream } from "node:fs" ;
33import fs from "node:fs/promises" ;
44import os from "node:os" ;
55import path from "node:path" ;
@@ -12,7 +12,8 @@ import {
1212 resolveBackupPlanFromDisk ,
1313} from "../commands/backup-shared.js" ;
1414import { isPathWithin } from "../commands/cleanup-utils.js" ;
15- import { resolveHomeDir , resolveUserPath } from "../utils.js" ;
15+ import { isNotFoundPathError } from "../infra/path-guards.js" ;
16+ import { resolveHomeDir , resolveUserPath , shortenHomePath } from "../utils.js" ;
1617import { resolveRuntimeServiceVersion } from "../version.js" ;
1718
1819export type BackupCreateOptions = {
@@ -127,10 +128,63 @@ function buildTempArchivePath(outputPath: string): string {
127128 return `${ outputPath } .${ randomUUID ( ) } .tmp` ;
128129}
129130
131+ async function createTarArchiveWithMissingSkip (
132+ params : {
133+ file : string ;
134+ gzip ?: boolean ;
135+ portable ?: boolean ;
136+ preservePaths ?: boolean ;
137+ onWriteEntry ?: ( entry : unknown ) => void ;
138+ } ,
139+ files : string [ ] ,
140+ ) : Promise < string [ ] > {
141+ const skipped : string [ ] = [ ] ;
142+ const pack = new tar . Pack ( {
143+ file : params . file ,
144+ gzip : params . gzip ,
145+ portable : params . portable ,
146+ preservePaths : params . preservePaths ,
147+ onWriteEntry : params . onWriteEntry ,
148+ } ) ;
149+ const stream = createWriteStream ( params . file ) ;
150+ pack . pipe ( stream ) ;
151+
152+ await new Promise < void > ( ( resolve , reject ) => {
153+ stream . on ( "error" , reject ) ;
154+ stream . on ( "close" , resolve ) ;
155+ pack . on ( "error" , ( err : unknown ) => {
156+ if ( isNotFoundPathError ( err ) ) {
157+ const p = ( err as NodeJS . ErrnoException ) . path ;
158+ if ( typeof p === "string" ) {
159+ skipped . push ( p ) ;
160+ }
161+ return ;
162+ }
163+ reject ( err instanceof Error ? err : new Error ( String ( err ) ) ) ;
164+ } ) ;
165+
166+ for ( const file of files ) {
167+ pack . add ( file ) ;
168+ }
169+ pack . end ( ) ;
170+ } ) ;
171+
172+ return skipped ;
173+ }
174+
130175function isLinkUnsupportedError ( code : string | undefined ) : boolean {
131176 return code === "ENOTSUP" || code === "EOPNOTSUPP" || code === "EPERM" ;
132177}
133178
179+ function resolveAssetKindForPath ( filePath : string , assets : BackupAsset [ ] ) : string {
180+ for ( const asset of assets ) {
181+ if ( filePath === asset . sourcePath || isPathWithin ( filePath , asset . sourcePath ) ) {
182+ return asset . kind ;
183+ }
184+ }
185+ return "state" ;
186+ }
187+
134188async function publishTempArchive ( params : {
135189 tempArchivePath : string ;
136190 outputPath : string ;
@@ -342,22 +396,30 @@ export async function createBackupArchive(
342396 } ) ;
343397 await fs . writeFile ( manifestPath , `${ JSON . stringify ( manifest , null , 2 ) } \n` , "utf8" ) ;
344398
345- await tar . c (
399+ const skippedPaths = await createTarArchiveWithMissingSkip (
346400 {
347401 file : tempArchivePath ,
348402 gzip : true ,
349403 portable : true ,
350404 preservePaths : true ,
351405 onWriteEntry : ( entry ) => {
352- entry . path = remapArchiveEntryPath ( {
353- entryPath : entry . path ,
406+ ( entry as { path : string } ) . path = remapArchiveEntryPath ( {
407+ entryPath : ( entry as { path : string } ) . path ,
354408 manifestPath,
355409 archiveRoot,
356410 } ) ;
357411 } ,
358412 } ,
359413 [ manifestPath , ...result . assets . map ( ( asset ) => asset . sourcePath ) ] ,
360414 ) ;
415+ for ( const skippedPath of skippedPaths ) {
416+ result . skipped . push ( {
417+ kind : resolveAssetKindForPath ( skippedPath , result . assets ) ,
418+ sourcePath : skippedPath ,
419+ displayPath : shortenHomePath ( skippedPath ) ,
420+ reason : "cleaned up during backup" ,
421+ } ) ;
422+ }
361423 await publishTempArchive ( { tempArchivePath, outputPath } ) ;
362424 } finally {
363425 await fs . rm ( tempArchivePath , { force : true } ) . catch ( ( ) => undefined ) ;
0 commit comments