fix: Prevent FileSystemResourceLocker from writing to ./

This commit is contained in:
Joachim Van Herwegen 2022-08-05 11:36:06 +02:00
parent 6985fb3af3
commit a99616acf2
2 changed files with 27 additions and 22 deletions

View File

@ -35,10 +35,10 @@ const attemptDefaults: Required<AttemptSettings> = { retryCount: -1, retryDelay:
* Argument interface of the FileSystemResourceLocker constructor. * Argument interface of the FileSystemResourceLocker constructor.
*/ */
interface FileSystemResourceLockerArgs { interface FileSystemResourceLockerArgs {
/** The rootPath of the filesystem _[default is the current dir `./`]_ */ /** The root filepath of where the server is allowed to write files */
rootFilePath?: string; rootFilePath: string;
/** /**
* The path to the directory where locks will be stored (appended to rootFilePath) * The path to the directory where locks will be stored (relative to rootFilePath)
* _[default is `/.internal/locks`]_ * _[default is `/.internal/locks`]_
*/ */
lockDirectory?: string; lockDirectory?: string;
@ -56,11 +56,12 @@ function isCodedError(err: unknown): err is { code: string } & Error {
* *
* This **proper-lockfile** library has its own retry mechanism for the operations, since a lock/unlock call will * This **proper-lockfile** library has its own retry mechanism for the operations, since a lock/unlock call will
* either resolve successfully or reject immediately with the causing error. The retry function of the library * either resolve successfully or reject immediately with the causing error. The retry function of the library
* however will be ignored and replaced by our own LockUtils' {@link retryFunctionUntil} function. * however will be ignored and replaced by our own LockUtils' {@link retryFunction} function.
*/ */
export class FileSystemResourceLocker implements ResourceLocker, Initializable, Finalizable { export class FileSystemResourceLocker implements ResourceLocker, Initializable, Finalizable {
protected readonly logger = getLoggerFor(this); protected readonly logger = getLoggerFor(this);
private readonly attemptSettings: Required<AttemptSettings>; private readonly attemptSettings: Required<AttemptSettings>;
private readonly lockOptions: LockOptions;
/** Folder that stores the locks */ /** Folder that stores the locks */
private readonly lockFolder: string; private readonly lockFolder: string;
private finalized = false; private finalized = false;
@ -69,17 +70,18 @@ export class FileSystemResourceLocker implements ResourceLocker, Initializable,
* Create a new FileSystemResourceLocker * Create a new FileSystemResourceLocker
* @param args - Configures the locker using the specified FileSystemResourceLockerArgs instance. * @param args - Configures the locker using the specified FileSystemResourceLockerArgs instance.
*/ */
public constructor(args: FileSystemResourceLockerArgs = {}) { public constructor(args: FileSystemResourceLockerArgs) {
const { rootFilePath, lockDirectory, attemptSettings } = args; const { rootFilePath, lockDirectory, attemptSettings } = args;
defaultLockOptions.onCompromised = this.customOnCompromised.bind(this); // Need to create lock options for this instance due to the custom `onCompromised`
this.lockOptions = { ...defaultLockOptions, onCompromised: this.customOnCompromised.bind(this) };
this.attemptSettings = { ...attemptDefaults, ...attemptSettings }; this.attemptSettings = { ...attemptDefaults, ...attemptSettings };
this.lockFolder = joinFilePath(rootFilePath ?? './', lockDirectory ?? '/.internal/locks'); this.lockFolder = joinFilePath(rootFilePath, lockDirectory ?? '/.internal/locks');
} }
/** /**
* Wrapper function for all (un)lock operations. Any errors coming from the `fn()` will be swallowed. * Wrapper function for all (un)lock operations. Any errors coming from the `fn()` will be swallowed.
* Only `ENOTACQUIRED` errors wills be thrown (trying to release lock that didn't exist). * Only `ENOTACQUIRED` errors wills be thrown (trying to release lock that didn't exist).
* This wrapper returns undefined because {@link retryFunction} expects that when a retry needs to happne.s * This wrapper returns undefined because {@link retryFunction} expects that when a retry needs to happen.
* @param fn - The function reference to swallow errors from. * @param fn - The function reference to swallow errors from.
* @returns Boolean or undefined. * @returns Boolean or undefined.
*/ */
@ -101,7 +103,7 @@ export class FileSystemResourceLocker implements ResourceLocker, Initializable,
const { path } = identifier; const { path } = identifier;
this.logger.debug(`Acquiring lock for ${path}`); this.logger.debug(`Acquiring lock for ${path}`);
try { try {
const opt = this.generateOptions(identifier, defaultLockOptions); const opt = this.generateOptions(identifier, this.lockOptions);
await retryFunction( await retryFunction(
this.swallowErrors(lock.bind(null, path, opt)), this.swallowErrors(lock.bind(null, path, opt)),
this.attemptSettings, this.attemptSettings,

View File

@ -1,14 +1,19 @@
import { readdir } from 'fs-extra'; import { readdir } from 'fs-extra';
import { InternalServerError, FileSystemResourceLocker } from '../../../../src'; import { InternalServerError, FileSystemResourceLocker, joinFilePath } from '../../../../src';
import { getTestFolder, removeFolder } from '../../../integration/Config';
const lockFolder = './.internal/locks/'; // Due to the nature of using a file locking library, this is a unit test that writes to disk.
// In the future ( = if someone has time) we might want to split this up into a unit test with a `proper-lockfile` mock,
// and an integration test that tests the behaviour of the library.
const rootFilePath = getTestFolder('FileSystemResourceLocker');
const lockFolder = joinFilePath(rootFilePath, '.internal/locks/');
describe('A FileSystemResourceLocker', (): void => { describe('A FileSystemResourceLocker', (): void => {
let locker: FileSystemResourceLocker; let locker: FileSystemResourceLocker;
const identifier = { path: 'http://test.com/foo' }; const identifier = { path: 'http://test.com/foo' };
beforeEach(async(): Promise<void> => { beforeEach(async(): Promise<void> => {
locker = new FileSystemResourceLocker({ attemptSettings: { retryCount: 19, retryDelay: 100 }}); locker = new FileSystemResourceLocker({ rootFilePath, attemptSettings: { retryCount: 19, retryDelay: 100 }});
await locker.initialize(); await locker.initialize();
}); });
@ -23,6 +28,7 @@ describe('A FileSystemResourceLocker', (): void => {
afterAll(async(): Promise<void> => { afterAll(async(): Promise<void> => {
await locker.finalize(); await locker.finalize();
await removeFolder(rootFilePath);
}); });
it('can lock and unlock a resource.', async(): Promise<void> => { it('can lock and unlock a resource.', async(): Promise<void> => {
@ -31,7 +37,7 @@ describe('A FileSystemResourceLocker', (): void => {
}); });
it('can lock and unlock a resource with a locker with indefinite retry.', async(): Promise<void> => { it('can lock and unlock a resource with a locker with indefinite retry.', async(): Promise<void> => {
const locker2 = new FileSystemResourceLocker({ attemptSettings: { retryCount: -1 }}); const locker2 = new FileSystemResourceLocker({ rootFilePath, attemptSettings: { retryCount: -1 }});
await expect(locker2.acquire(identifier)).resolves.toBeUndefined(); await expect(locker2.acquire(identifier)).resolves.toBeUndefined();
await expect(locker2.release(identifier)).resolves.toBeUndefined(); await expect(locker2.release(identifier)).resolves.toBeUndefined();
await locker2.finalize(); await locker2.finalize();
@ -116,14 +122,11 @@ describe('A FileSystemResourceLocker', (): void => {
await expect(readdir(lockFolder)).resolves.toHaveLength(0); await expect(readdir(lockFolder)).resolves.toHaveLength(0);
}); });
it('stops proper-lock from throwing errors onCompromise after finalize was called.', it('stops proper-lock from throwing errors after finalize was called.', async(): Promise<void> => {
async(): Promise<void> => { // Tests should never access private fields so we need to change this after splitting the test as mentioned above.
expect((): void => (locker as any).customOnCompromised(new Error('test'))).toThrow(); // Once we have a mock we can check which parameters `unlock` was called with and extract the function from there.
await locker.finalize(); expect((): void => (locker as any).customOnCompromised(new Error('test'))).toThrow();
expect((locker as any).customOnCompromised(new Error('test'))).toBeUndefined(); await locker.finalize();
}); expect((locker as any).customOnCompromised(new Error('test'))).toBeUndefined();
it('can create a locker with default AttemptSettings.', async(): Promise<void> => {
expect((): FileSystemResourceLocker => new FileSystemResourceLocker()).not.toThrow();
}); });
}); });