Fix file retention after updates on macOS #417
This fixes issue #417 where autoupdate installer files were not deleted on macOS, leading to accumulation of old installers. Key changes: - Store update files in application-specific directory - Clear update files directory on every app launch Other supporting changes: - Refactor file system operations to be more testable and reusable - Improve separation of concerns in directory management - Enhance dependency injection for auto-update logic - Fix async completion to support `await` operations - Add additional logging and revise some log messages during updates
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
import { it, describe, expect } from 'vitest';
|
||||
import type { Logger } from '@/application/Common/Log/Logger';
|
||||
import type { ApplicationDirectoryProvider } from '@/infrastructure/FileSystem/Directory/ApplicationDirectoryProvider';
|
||||
import type { FileSystemOperations } from '@/infrastructure/FileSystem/FileSystemOperations';
|
||||
import { ApplicationDirectoryProviderStub } from '@tests/unit/shared/Stubs/ApplicationDirectoryProviderStub';
|
||||
import { LoggerStub } from '@tests/unit/shared/Stubs/LoggerStub';
|
||||
import { FileSystemOperationsStub } from '@tests/unit/shared/Stubs/FileSystemOperationsStub';
|
||||
import { clearUpdateInstallationFiles } from '@/presentation/electron/main/Update/ManualUpdater/InstallationFiles/InstallationFileCleaner';
|
||||
import { expectThrowsAsync } from '@tests/shared/Assertions/ExpectThrowsAsync';
|
||||
import { collectExceptionAsync } from '@tests/unit/shared/ExceptionCollector';
|
||||
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
import { indentText } from '@/application/Common/Text/IndentText';
|
||||
|
||||
describe('InstallationFileCleaner', () => {
|
||||
describe('clearUpdateInstallationFiles', () => {
|
||||
describe('deleting files', () => {
|
||||
it('deletes all update installation files and directories', async () => {
|
||||
// arrange
|
||||
const expectedDirectoryEntries = ['file1', 'file2', 'file3', 'directory1', 'directory2'];
|
||||
const directoryPath = 'directory-name';
|
||||
const pathSeparator = 'test-separator';
|
||||
const directoryProviderStub = new ApplicationDirectoryProviderStub()
|
||||
.withDirectoryPath('update-installation-files', directoryPath);
|
||||
const fileSystemStub = new FileSystemOperationsStub()
|
||||
.withDefaultSeparator(pathSeparator)
|
||||
.withDirectoryContents(directoryPath, expectedDirectoryEntries);
|
||||
const context = new TestContext()
|
||||
.withDirectoryProvider(directoryProviderStub)
|
||||
.withFileSystem(fileSystemStub);
|
||||
// act
|
||||
await context.run();
|
||||
// assert
|
||||
const actualDeletedEntries = fileSystemStub.callHistory
|
||||
.filter((c) => c.methodName === 'deletePath')
|
||||
.map((c) => c.args[0])
|
||||
.map((path) => path.split(pathSeparator).pop());
|
||||
expect(expectedDirectoryEntries.sort()).to.deep.equal(actualDeletedEntries.sort());
|
||||
});
|
||||
it('deletes files at the correct absolute paths', async () => {
|
||||
// arrange
|
||||
const directoryItemName = 'expected-item-name';
|
||||
const directoryPath = 'expected-directory';
|
||||
const pathSeparator = '[expected-separator]';
|
||||
const expectedFullPath = [directoryPath, directoryItemName].join(pathSeparator);
|
||||
const directoryProviderStub = new ApplicationDirectoryProviderStub()
|
||||
.withDirectoryPath('update-installation-files', directoryPath);
|
||||
const fileSystemStub = new FileSystemOperationsStub()
|
||||
.withDefaultSeparator(pathSeparator)
|
||||
.withDirectoryContents(directoryPath, [directoryItemName]);
|
||||
const context = new TestContext()
|
||||
.withDirectoryProvider(directoryProviderStub)
|
||||
.withFileSystem(fileSystemStub);
|
||||
// act
|
||||
await context.run();
|
||||
// assert
|
||||
const actualDeletedEntries = fileSystemStub.callHistory
|
||||
.filter((c) => c.methodName === 'deletePath')
|
||||
.map((c) => c.args[0]);
|
||||
expect(actualDeletedEntries).to.have.lengthOf(1);
|
||||
const actualFullPath = actualDeletedEntries[0];
|
||||
expect(actualFullPath).to.equal(expectedFullPath);
|
||||
});
|
||||
it('continues deleting other items if one cannot be deleted', async () => {
|
||||
// arrange
|
||||
const expectedDeletedItems = ['success-1', 'success-2', 'success-3'];
|
||||
const expectedDirectoryEntries = ['fail-1', ...expectedDeletedItems, 'fail-2'];
|
||||
const directoryPath = 'directory-name';
|
||||
const pathSeparator = 'test-separator';
|
||||
const directoryProviderStub = new ApplicationDirectoryProviderStub()
|
||||
.withDirectoryPath('update-installation-files', directoryPath);
|
||||
const fileSystemStub = new FileSystemOperationsStub()
|
||||
.withDefaultSeparator(pathSeparator)
|
||||
.withDirectoryContents(directoryPath, expectedDirectoryEntries);
|
||||
fileSystemStub.deletePath = async (path) => {
|
||||
await FileSystemOperationsStub.prototype
|
||||
.deletePath.call(fileSystemStub, path); // register call history
|
||||
if (expectedDeletedItems.some((item) => path.endsWith(item))) {
|
||||
return;
|
||||
}
|
||||
throw new Error(`Path is not configured to succeed, so it fails: ${path}`);
|
||||
};
|
||||
const context = new TestContext()
|
||||
.withDirectoryProvider(directoryProviderStub)
|
||||
.withFileSystem(fileSystemStub);
|
||||
// act
|
||||
try {
|
||||
await context.run();
|
||||
} catch { /* Swallow */ }
|
||||
// assert
|
||||
const actualDeletedEntries = fileSystemStub.callHistory
|
||||
.filter((c) => c.methodName === 'deletePath')
|
||||
.map((c) => c.args[0])
|
||||
.map((path) => path.split(pathSeparator).pop());
|
||||
expect(expectedDirectoryEntries.sort()).to.deep.equal(actualDeletedEntries.sort());
|
||||
});
|
||||
});
|
||||
it('does nothing if directory is empty', async () => {
|
||||
// arrange
|
||||
const directoryPath = 'directory-path';
|
||||
const directoryProviderStub = new ApplicationDirectoryProviderStub()
|
||||
.withDirectoryPath('update-installation-files', directoryPath);
|
||||
const fileSystemStub = new FileSystemOperationsStub()
|
||||
.withDirectoryContents(directoryPath, []);
|
||||
const context = new TestContext()
|
||||
.withDirectoryProvider(directoryProviderStub)
|
||||
.withFileSystem(fileSystemStub);
|
||||
// act
|
||||
await context.run();
|
||||
// assert
|
||||
const actualDeletedEntries = fileSystemStub.callHistory
|
||||
.filter((c) => c.methodName === 'deletePath');
|
||||
expect(actualDeletedEntries).to.have.lengthOf(0);
|
||||
});
|
||||
describe('error handling', () => {
|
||||
it('throws if installation directory cannot be provided', async () => {
|
||||
// arrange
|
||||
const expectedError = 'Cannot locate the installation files directory path';
|
||||
const directoryProviderStub = new ApplicationDirectoryProviderStub()
|
||||
.withFailure();
|
||||
const context = new TestContext()
|
||||
.withDirectoryProvider(directoryProviderStub);
|
||||
// act
|
||||
const act = () => context.run();
|
||||
// assert
|
||||
await expectThrowsAsync(act, expectedError);
|
||||
});
|
||||
it('throws if directory contents cannot be listed', async () => {
|
||||
// arrange
|
||||
const expectedError = 'Failed to read directory contents';
|
||||
const fileSystemStub = new FileSystemOperationsStub();
|
||||
fileSystemStub.listDirectoryContents = () => Promise.reject(new Error(expectedError));
|
||||
const context = new TestContext()
|
||||
.withFileSystem(fileSystemStub);
|
||||
// act
|
||||
const act = () => context.run();
|
||||
// assert
|
||||
await expectThrowsAsync(act, expectedError);
|
||||
});
|
||||
it('throws if all items cannot be deleted', async () => {
|
||||
// arrange
|
||||
const itemsWithErrors: Map<string, Error> = new Map([
|
||||
['item-1', new Error('Access Denied: item-1')],
|
||||
['item-2', new Error('Disk I/O Error: item-2')],
|
||||
]);
|
||||
const expectedErrorParts = [
|
||||
'Failed to delete some items',
|
||||
...[...itemsWithErrors.values()].map((item: Error) => item.message),
|
||||
];
|
||||
const loggerStub = new LoggerStub();
|
||||
const fileSystemStub = new FileSystemOperationsStub();
|
||||
fileSystemStub.listDirectoryContents = async () => {
|
||||
await FileSystemOperationsStub.prototype
|
||||
.listDirectoryContents.call(fileSystemStub); // register call history
|
||||
return [...itemsWithErrors.keys()];
|
||||
};
|
||||
fileSystemStub.deletePath = (path) => {
|
||||
const name = [...itemsWithErrors.keys()]
|
||||
.find((fileName) => path.endsWith(fileName));
|
||||
if (!name) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const error = itemsWithErrors.get(name)!;
|
||||
return Promise.reject(error);
|
||||
};
|
||||
const context = new TestContext()
|
||||
.withFileSystem(fileSystemStub)
|
||||
.withLogger(loggerStub);
|
||||
// act
|
||||
const act = () => context.run();
|
||||
// assert
|
||||
const error = await collectExceptionAsync(act);
|
||||
expectExists(error, formatAssertionMessage([
|
||||
`FileSystem calls: ${JSON.stringify(fileSystemStub.callHistory)}`,
|
||||
`Log calls: ${JSON.stringify(loggerStub.callHistory)}`,
|
||||
]));
|
||||
const errorMessage = error.message;
|
||||
const notExistingErrorMessageParts = expectedErrorParts.filter(
|
||||
(e) => !errorMessage.includes(e),
|
||||
);
|
||||
expect(notExistingErrorMessageParts).to.have.lengthOf(0, formatAssertionMessage([
|
||||
'Actual error message:',
|
||||
indentText(errorMessage),
|
||||
'Expected parts:',
|
||||
indentText(expectedErrorParts.map((part) => `- ${part}`).join('\n')),
|
||||
]));
|
||||
});
|
||||
it('throws if some items cannot be deleted', async () => {
|
||||
// arrange
|
||||
const itemsWithErrors: Map<string, Error> = new Map([
|
||||
['item-1', new Error('Access Denied: item-1')],
|
||||
['item-2', new Error('Disk I/O Error: item-2')],
|
||||
]);
|
||||
const expectedErrorParts = [
|
||||
'Failed to delete some items',
|
||||
...[...itemsWithErrors.values()].map((item: Error) => item.message),
|
||||
];
|
||||
const itemsWithSuccess = ['successful-item-1', 'successful-item-2'];
|
||||
const allItems = [
|
||||
itemsWithSuccess[0],
|
||||
...[...itemsWithErrors.keys()],
|
||||
itemsWithSuccess[1],
|
||||
];
|
||||
const fileSystemStub = new FileSystemOperationsStub();
|
||||
const loggerStub = new LoggerStub();
|
||||
fileSystemStub.listDirectoryContents = async () => {
|
||||
await FileSystemOperationsStub.prototype
|
||||
.listDirectoryContents.call(fileSystemStub); // register call history
|
||||
return allItems;
|
||||
};
|
||||
fileSystemStub.deletePath = async (path) => {
|
||||
await FileSystemOperationsStub.prototype
|
||||
.deletePath.call(fileSystemStub, path); // register call history
|
||||
const name = [...itemsWithErrors.keys()].find((n) => path.endsWith(n));
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
const error = itemsWithErrors.get(name)!;
|
||||
throw error;
|
||||
};
|
||||
const context = new TestContext()
|
||||
.withFileSystem(fileSystemStub)
|
||||
.withLogger(loggerStub);
|
||||
// act
|
||||
const act = () => context.run();
|
||||
// assert
|
||||
const error = await collectExceptionAsync(act);
|
||||
expectExists(error, formatAssertionMessage([
|
||||
`Calls: ${JSON.stringify(fileSystemStub.callHistory)}`,
|
||||
`Logs: ${JSON.stringify(loggerStub.callHistory)}`,
|
||||
]));
|
||||
const errorMessage = error.message;
|
||||
const notExistingErrorMessageParts = expectedErrorParts.filter(
|
||||
(e) => !error.message.includes(e),
|
||||
);
|
||||
expect(notExistingErrorMessageParts)
|
||||
.to.have.lengthOf(0, formatAssertionMessage([
|
||||
'Actual error message:',
|
||||
indentText(errorMessage),
|
||||
'Expected parts:',
|
||||
indentText(expectedErrorParts.map((part) => `- ${part}`).join('\n')),
|
||||
]));
|
||||
expect(itemsWithSuccess.some((item) => errorMessage.includes(item)))
|
||||
.to.equal(false, formatAssertionMessage([
|
||||
'Actual error message:',
|
||||
indentText(errorMessage),
|
||||
'Unexpected parts:',
|
||||
indentText(itemsWithSuccess.map((part) => `- ${part}`).join('\n')),
|
||||
]));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class TestContext {
|
||||
private logger: Logger = new LoggerStub();
|
||||
|
||||
private directoryProvider: ApplicationDirectoryProvider = new ApplicationDirectoryProviderStub();
|
||||
|
||||
private fileSystem: FileSystemOperations = new FileSystemOperationsStub();
|
||||
|
||||
public withDirectoryProvider(directoryProvider: ApplicationDirectoryProvider): this {
|
||||
this.directoryProvider = directoryProvider;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withFileSystem(fileSystem: FileSystemOperations): this {
|
||||
this.fileSystem = fileSystem;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withLogger(logger: Logger): this {
|
||||
this.logger = logger;
|
||||
return this;
|
||||
}
|
||||
|
||||
public run(): ReturnType<typeof clearUpdateInstallationFiles> {
|
||||
return clearUpdateInstallationFiles({
|
||||
logger: this.logger,
|
||||
directoryProvider: this.directoryProvider,
|
||||
fileSystem: this.fileSystem,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
import { it, describe, expect } from 'vitest';
|
||||
import type { Logger } from '@/application/Common/Log/Logger';
|
||||
import { LoggerStub } from '@tests/unit/shared/Stubs/LoggerStub';
|
||||
import { ApplicationDirectoryProviderStub } from '@tests/unit/shared/Stubs/ApplicationDirectoryProviderStub';
|
||||
import type { ApplicationDirectoryProvider } from '@/infrastructure/FileSystem/Directory/ApplicationDirectoryProvider';
|
||||
import type { FileSystemOperations } from '@/infrastructure/FileSystem/FileSystemOperations';
|
||||
import { FileSystemOperationsStub } from '@tests/unit/shared/Stubs/FileSystemOperationsStub';
|
||||
import type { FileSystemAccessorWithRetry } from '@/presentation/electron/main/Update/ManualUpdater/FileSystemAccessorWithRetry';
|
||||
import { FileSystemAccessorWithRetryStub } from '@tests/unit/shared/Stubs/FileSystemAccessorWithRetryStub';
|
||||
import { InstallerFileSuffix, provideUpdateInstallationFilepath } from '@/presentation/electron/main/Update/ManualUpdater/InstallationFiles/InstallationFilepathProvider';
|
||||
import { expectThrowsAsync } from '@tests/shared/Assertions/ExpectThrowsAsync';
|
||||
import { collectExceptionAsync } from '@tests/unit/shared/ExceptionCollector';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||
|
||||
describe('InstallationFilePathProvider', () => {
|
||||
describe('provideUpdateInstallationFilePath', () => {
|
||||
it('returns correct filepath', async () => {
|
||||
// arrange
|
||||
const version = '1.2.3';
|
||||
const baseDirectoryPath = '/updates';
|
||||
const pathSegmentSeparator = '/separator/';
|
||||
const expectedPath = [
|
||||
baseDirectoryPath, pathSegmentSeparator, version, InstallerFileSuffix,
|
||||
].join('');
|
||||
const fileSystemStub = new FileSystemOperationsStub()
|
||||
.withDefaultSeparator(pathSegmentSeparator);
|
||||
const directoryProviderStub = new ApplicationDirectoryProviderStub()
|
||||
.withDirectoryPath('update-installation-files', baseDirectoryPath);
|
||||
const context = new TestContext()
|
||||
.withFileSystem(fileSystemStub)
|
||||
.withDirectoryProvider(directoryProviderStub)
|
||||
.withVersion(version);
|
||||
// act
|
||||
const actualPath = await context.run();
|
||||
// assert
|
||||
expect(actualPath).to.equal(expectedPath);
|
||||
});
|
||||
it('checks if file exists at correct path', async () => {
|
||||
// arrange
|
||||
const version = '1.2.3';
|
||||
const baseDirectoryPath = '/updates';
|
||||
const pathSegmentSeparator = '/separator/';
|
||||
const expectedPath = [
|
||||
baseDirectoryPath, pathSegmentSeparator, version, InstallerFileSuffix,
|
||||
].join('');
|
||||
const fileSystemStub = new FileSystemOperationsStub()
|
||||
.withDefaultSeparator(pathSegmentSeparator);
|
||||
const directoryProviderStub = new ApplicationDirectoryProviderStub()
|
||||
.withDirectoryPath('update-installation-files', baseDirectoryPath);
|
||||
const context = new TestContext()
|
||||
.withFileSystem(fileSystemStub)
|
||||
.withDirectoryProvider(directoryProviderStub)
|
||||
.withVersion(version);
|
||||
// act
|
||||
await context.run();
|
||||
// assert
|
||||
const calls = fileSystemStub.callHistory.filter((c) => c.methodName === 'isFileAvailable');
|
||||
expect(calls).to.have.lengthOf(1);
|
||||
const [actualFilePath] = calls[0].args;
|
||||
expect(actualFilePath).to.equal(expectedPath);
|
||||
});
|
||||
it('deletes file at correct path', async () => {
|
||||
// arrange
|
||||
const version = '1.2.3';
|
||||
const baseDirectoryPath = '/updates';
|
||||
const pathSegmentSeparator = '/separator/';
|
||||
const expectedPath = [
|
||||
baseDirectoryPath, pathSegmentSeparator, version, InstallerFileSuffix,
|
||||
].join('');
|
||||
const isFileAvailable = true;
|
||||
const fileSystemStub = new FileSystemOperationsStub()
|
||||
.withFileAvailability(expectedPath, isFileAvailable)
|
||||
.withDefaultSeparator(pathSegmentSeparator);
|
||||
const directoryProviderStub = new ApplicationDirectoryProviderStub()
|
||||
.withDirectoryPath('update-installation-files', baseDirectoryPath);
|
||||
const context = new TestContext()
|
||||
.withFileSystem(fileSystemStub)
|
||||
.withDirectoryProvider(directoryProviderStub)
|
||||
.withVersion(version);
|
||||
// act
|
||||
await context.run();
|
||||
// assert
|
||||
const calls = fileSystemStub.callHistory.filter((c) => c.methodName === 'deletePath');
|
||||
expect(calls).to.have.lengthOf(1);
|
||||
const [deletedFilePath] = calls[0].args;
|
||||
expect(deletedFilePath).to.equal(expectedPath);
|
||||
});
|
||||
it('deletes existing file', async () => {
|
||||
// arrange
|
||||
const isFileAvailable = true;
|
||||
const fileSystemStub = new FileSystemOperationsStub();
|
||||
fileSystemStub.isFileAvailable = () => Promise.resolve(isFileAvailable);
|
||||
const context = new TestContext()
|
||||
.withFileSystem(fileSystemStub);
|
||||
// act
|
||||
await context.run();
|
||||
// assert
|
||||
const calls = fileSystemStub.callHistory.filter((c) => c.methodName === 'deletePath');
|
||||
expect(calls).to.have.lengthOf(1);
|
||||
});
|
||||
it('does not attempt to delete non-existent file', async () => {
|
||||
// arrange
|
||||
const isFileAvailable = false;
|
||||
const fileSystemStub = new FileSystemOperationsStub();
|
||||
fileSystemStub.isFileAvailable = () => Promise.resolve(isFileAvailable);
|
||||
const context = new TestContext()
|
||||
.withFileSystem(fileSystemStub);
|
||||
// act
|
||||
await context.run();
|
||||
// assert
|
||||
const calls = fileSystemStub.callHistory.filter((c) => c.methodName === 'deletePath');
|
||||
expect(calls).to.have.lengthOf(0);
|
||||
});
|
||||
describe('file system error handling', () => {
|
||||
it('retries on file deletion failure', async () => {
|
||||
// arrange
|
||||
const forcedRetries = 2;
|
||||
const expectedTotalCalls = forcedRetries + 1;
|
||||
const isFileAvailable = true;
|
||||
const accessorStub = new FileSystemAccessorWithRetryStub()
|
||||
.withAlwaysRetry(forcedRetries);
|
||||
const fileSystemStub = new FileSystemOperationsStub();
|
||||
fileSystemStub.isFileAvailable = () => Promise.resolve(isFileAvailable);
|
||||
const context = new TestContext()
|
||||
.withFileSystem(fileSystemStub)
|
||||
.withAccessor(accessorStub.get());
|
||||
// act
|
||||
await context.run();
|
||||
// assert
|
||||
const calls = fileSystemStub.callHistory
|
||||
.filter((c) => c.methodName === 'deletePath');
|
||||
expect(calls).to.have.lengthOf(expectedTotalCalls);
|
||||
});
|
||||
});
|
||||
describe('error handling', () => {
|
||||
it('throws when directory provision fails', async () => {
|
||||
// arrange
|
||||
const expectedErrorMessage = 'Failed to provide download directory.';
|
||||
const directoryProvider = new ApplicationDirectoryProviderStub()
|
||||
.withFailure();
|
||||
const context = new TestContext()
|
||||
.withDirectoryProvider(directoryProvider);
|
||||
// act
|
||||
const act = () => context.run();
|
||||
// assert
|
||||
await expectThrowsAsync(act, expectedErrorMessage);
|
||||
});
|
||||
it('throws on file availability check failure', async () => {
|
||||
// arrange
|
||||
const expectedErrorMessage = 'File availability check failed';
|
||||
const fileSystemStub = new FileSystemOperationsStub();
|
||||
fileSystemStub.isFileAvailable = () => {
|
||||
return Promise.reject(new Error(expectedErrorMessage));
|
||||
};
|
||||
const context = new TestContext()
|
||||
.withFileSystem(fileSystemStub);
|
||||
// act
|
||||
const act = () => context.run();
|
||||
// assert
|
||||
await expectThrowsAsync(act, expectedErrorMessage);
|
||||
});
|
||||
it('throws on existing file deletion failure', async () => {
|
||||
// arrange
|
||||
const expectedErrorMessagePart = 'Failed to prepare the file path for the installer';
|
||||
const fileSystemStub = new FileSystemOperationsStub();
|
||||
fileSystemStub.deletePath = () => {
|
||||
return Promise.reject(new Error('Internal error'));
|
||||
};
|
||||
const context = new TestContext()
|
||||
.withFileSystem(fileSystemStub);
|
||||
// act
|
||||
const act = () => context.run();
|
||||
// assert
|
||||
const error = await collectExceptionAsync(act);
|
||||
expectExists(error, formatAssertionMessage([
|
||||
`File system calls: ${fileSystemStub.methodCalls}`,
|
||||
]));
|
||||
expect(error.message).to.include(expectedErrorMessagePart);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class TestContext {
|
||||
private version = '3.5.5';
|
||||
|
||||
private logger: Logger = new LoggerStub();
|
||||
|
||||
private directoryProvider
|
||||
: ApplicationDirectoryProvider = new ApplicationDirectoryProviderStub();
|
||||
|
||||
private fileSystem: FileSystemOperations = new FileSystemOperationsStub();
|
||||
|
||||
private accessor: FileSystemAccessorWithRetry = new FileSystemAccessorWithRetryStub().get();
|
||||
|
||||
public withVersion(version: string): this {
|
||||
this.version = version;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withAccessor(accessor: FileSystemAccessorWithRetry): this {
|
||||
this.accessor = accessor;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withDirectoryProvider(directoryProvider: ApplicationDirectoryProvider): this {
|
||||
this.directoryProvider = directoryProvider;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withFileSystem(fileSystem: FileSystemOperations): this {
|
||||
this.fileSystem = fileSystem;
|
||||
return this;
|
||||
}
|
||||
|
||||
public run() {
|
||||
return provideUpdateInstallationFilepath(this.version, {
|
||||
logger: this.logger,
|
||||
directoryProvider: this.directoryProvider,
|
||||
fileSystem: this.fileSystem,
|
||||
accessFileSystemWithRetry: this.accessor,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user