mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
fix: Prevent FileSystemResourceLocker from writing to ./
This commit is contained in:
parent
6985fb3af3
commit
a99616acf2
@ -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,
|
||||||
|
@ -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();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user