33/* eslint-disable max-classes-per-file */
44
55import { existsSync } from 'node:fs' ;
6- import { PassThrough } from 'node:stream' ;
7- import { constants as FS_CONSTANTS , copyFile , mkdir } from 'node:fs/promises' ;
6+ import {
7+ constants as FS_CONSTANTS ,
8+ copyFile ,
9+ mkdir ,
10+ rename ,
11+ } from 'node:fs/promises' ;
812
913import * as durations from '../util/durations/index.std.js' ;
1014import { createLogger } from '../logging/log.std.js' ;
@@ -14,6 +18,7 @@ import { redactGenericText } from '../util/privacy.node.js';
1418import {
1519 getAbsoluteAttachmentPath ,
1620 getAbsoluteAttachmentPath as doGetAbsoluteAttachmentPath ,
21+ getAbsoluteTempPath ,
1722} from '../util/migrations.preload.js' ;
1823import {
1924 JobManager ,
@@ -36,6 +41,7 @@ import {
3641 getLocalBackupDirectoryForMediaName ,
3742 getLocalBackupPathForMediaName ,
3843} from '../services/backups/util/localBackup.node.js' ;
44+ import { createName } from '../util/attachmentPath.node.js' ;
3945
4046const log = createLogger ( 'AttachmentLocalBackupManager' ) ;
4147
@@ -219,7 +225,7 @@ async function runAttachmentBackupJobInner(
219225 log . info ( `${ logId } : starting` ) ;
220226
221227 const { backupsBaseDir, mediaName } = job ;
222- const { localKey , path, size } = job . data ;
228+ const { path } = job . data ;
223229
224230 if ( ! path ) {
225231 throw new AttachmentPermanentlyMissingError ( 'No path property' ) ;
@@ -230,45 +236,26 @@ async function runAttachmentBackupJobInner(
230236 throw new AttachmentPermanentlyMissingError ( 'No file at provided path' ) ;
231237 }
232238
233- if ( ! localKey ) {
234- throw new Error ( 'No localKey property, required for test decryption' ) ;
235- }
236-
237239 const localBackupFileDir = getLocalBackupDirectoryForMediaName ( {
238240 backupsBaseDir,
239241 mediaName,
240242 } ) ;
241243 await mkdir ( localBackupFileDir , { recursive : true } ) ;
242244
243- const localBackupFilePath = getLocalBackupPathForMediaName ( {
245+ const destinationLocalBackupFilePath = getLocalBackupPathForMediaName ( {
244246 backupsBaseDir,
245247 mediaName,
246248 } ) ;
247249
248- // TODO: Add check in local FS to prevent double backup
249-
250250 // File is already encrypted with localKey, so we just have to copy it to the backup dir
251- const attachmentPath = getAbsoluteAttachmentPath ( path ) ;
251+ const sourceAttachmentPath = getAbsoluteAttachmentPath ( path ) ;
252+ const tempPath = getAbsoluteTempPath ( createName ( ) ) ;
253+
254+ // A unique constraint on the DB table should enforce that only one job is writing to
255+ // the same mediaName at a time, but just to be safe, we copy to temp file and rename to
256+ // ensure the atomicity of the copy operation
252257
253258 // Set COPYFILE_FICLONE for Copy on Write (OS dependent, gracefully falls back to copy)
254- await copyFile (
255- attachmentPath ,
256- localBackupFilePath ,
257- FS_CONSTANTS . COPYFILE_FICLONE
258- ) ;
259-
260- // TODO: Optimize this check -- it can be expensive to test decrypt on every export
261- log . info ( `${ logId } : Verifying file in local backup` ) ;
262- const sink = new PassThrough ( ) ;
263- sink . resume ( ) ;
264- await decryptAttachmentV2ToSink (
265- {
266- ciphertextPath : localBackupFilePath ,
267- idForLogging : 'AttachmentLocalBackupManager' ,
268- keysBase64 : localKey ,
269- size,
270- type : 'local' ,
271- } ,
272- sink
273- ) ;
259+ await copyFile ( sourceAttachmentPath , tempPath , FS_CONSTANTS . COPYFILE_FICLONE ) ;
260+ await rename ( tempPath , destinationLocalBackupFilePath ) ;
274261}
0 commit comments