Skip to content

Commit

Permalink
feat: add lock and unlock method support
Browse files Browse the repository at this point in the history
  • Loading branch information
hperrin committed Aug 4, 2022
1 parent 7560d19 commit 92ae750
Show file tree
Hide file tree
Showing 30 changed files with 1,081 additions and 176 deletions.
51 changes: 49 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
"@types/express": "^4.17.13",
"@types/jest": "^27.5.0",
"@types/mmmagic": "^0.4.30",
"@types/uuid": "^8.3.4",
"@types/vary": "^1.1.0",
"@types/xml2js": "^0.4.11",
"authenticate-pam": "^1.0.5",
"basic-auth": "^2.0.1",
Expand All @@ -67,6 +69,8 @@
"debug": "^4.3.4",
"express": "^4.18.1",
"nanoid": "^4.0.0",
"uuid": "^8.3.2",
"vary": "^1.1.2",
"xml2js": "^0.4.23"
},
"engines": {
Expand Down
1 change: 1 addition & 0 deletions src/Errors/MethodNotImplementedError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class MethodNotImplementedError extends Error {}
1 change: 1 addition & 0 deletions src/Errors/ServiceUnavailableError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class ServiceUnavailableError extends Error {}
2 changes: 2 additions & 0 deletions src/Errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from './ForbiddenError.js';
export * from './InsufficientStorageError.js';
export * from './LockedError.js';
export * from './MediaTypeNotSupportedError.js';
export * from './MethodNotImplementedError.js';
export * from './MethodNotSupportedError.js';
export * from './NotAcceptableError.js';
export * from './PreconditionFailedError.js';
Expand All @@ -16,5 +17,6 @@ export * from './RequestURITooLongError.js';
export * from './ResourceExistsError.js';
export * from './ResourceNotFoundError.js';
export * from './ResourceTreeNotCompleteError.js';
export * from './ServiceUnavailableError.js';
export * from './UnauthorizedError.js';
export * from './UnprocessableEntityError.js';
56 changes: 54 additions & 2 deletions src/FileSystemAdapter/Adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ import type {
} from '../index.js';
import {
BadGatewayError,
MethodNotImplementedError,
MethodNotSupportedError,
ResourceNotFoundError,
} from '../index.js';

import type Lock from './Lock.js';
import {
userReadBit,
userWriteBit,
Expand Down Expand Up @@ -359,8 +361,58 @@ export default class Adapter implements AdapterInterface {
});
}

getMethod(_method: string): typeof Method {
async formatLocks(locks: Lock[], baseUrl: URL) {
const xml = { activelock: [] as any[] };

if (locks != null) {
for (let lock of locks) {
const secondsLeft =
lock.timeout === Infinity
? Infinity
: (lock.date.getTime() + lock.timeout - new Date().getTime()) /
1000;

if (secondsLeft <= 0) {
continue;
}

xml.activelock.push({
locktype: {
write: {},
},
lockscope: {
[lock.scope]: {},
},
depth: {
_: `${lock.depth}`,
},
owner: lock.owner,
timeout:
secondsLeft === Infinity
? { _: 'Infinite' }
: { _: `Second-${secondsLeft}` },
locktoken: { href: { _: lock.token } },
lockroot: {
href: {
_: (await lock.resource.getCanonicalUrl(baseUrl)).pathname,
},
},
});
}
}

if (!xml.activelock.length) {
return {};
}

return xml;
}

getMethod(method: string): typeof Method {
// No additional methods to handle.
throw new MethodNotSupportedError('Method not allowed.');
if (method === 'POST' || method === 'PATCH') {
throw new MethodNotSupportedError('Method not supported.');
}
throw new MethodNotImplementedError('Method not implemented.');
}
}
8 changes: 5 additions & 3 deletions src/FileSystemAdapter/Lock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import Resource from './Resource.js';
import User from './User.js';

export default class Lock implements LockInterface {
token: string = '';
resource: Resource;
user: User;
token: string = '';
date: Date = new Date();
timeout: number = 1000 * 60 * 60 * 24 * 2; // Default to two day timeout.
exclusive: boolean = false;
scope: 'exclusive' | 'shared' = 'exclusive';
depth: '0' | 'infinity' = '0';
provisional: boolean = false;
owner: any = {};

constructor({ resource, user }: { resource: Resource; user: User }) {
this.resource = resource;
Expand All @@ -29,9 +30,10 @@ export default class Lock implements LockInterface {
username: this.user.username,
date: this.date.getTime(),
timeout: this.timeout,
exclusive: this.exclusive,
scope: this.scope,
depth: this.depth,
provisional: this.provisional,
owner: this.owner,
};

await this.resource.saveMetadataFile(meta);
Expand Down
14 changes: 7 additions & 7 deletions src/FileSystemAdapter/Properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,19 @@ export default class Properties implements PropertiesInterface {
case 'getcontentlength':
return `${await this.resource.getLength()}`;
case 'getcontenttype':
return await this.resource.getMediaType();
const mediaType = await this.resource.getMediaType();
if (mediaType == null) {
throw new PropertyNotFoundError(
`${name} property doesn't exist on resource.`
);
}
return mediaType;
case 'getetag':
return await this.resource.getEtag();
case 'getlastmodified': {
const stats = await this.resource.getStats();
return stats.mtime.toUTCString();
}
case 'lockdiscovery':
// TODO: Implement this. (Page 94)
return '';
case 'resourcetype':
if (await this.resource.isCollection()) {
return { collection: {} };
Expand Down Expand Up @@ -174,7 +177,6 @@ export default class Properties implements PropertiesInterface {
'getcontenttype',
'getetag',
'getlastmodified',
'lockdiscovery',
'resourcetype',
'supportedlock',
'quota-available-bytes',
Expand Down Expand Up @@ -272,7 +274,6 @@ export default class Properties implements PropertiesInterface {
getcontenttype: await this.get('getcontenttype'),
getetag: await this.get('getetag'),
getlastmodified: await this.get('getlastmodified'),
lockdiscovery: await this.get('lockdiscovery'),
resourcetype: await this.get('resourcetype'),
supportedlock: await this.get('supportedlock'),
'quota-available-bytes': await this.get('quota-available-bytes'),
Expand Down Expand Up @@ -306,7 +307,6 @@ export default class Properties implements PropertiesInterface {
'getcontenttype',
'getetag',
'getlastmodified',
'lockdiscovery',
'resourcetype',
'supportedlock',
'quota-available-bytes',
Expand Down
21 changes: 18 additions & 3 deletions src/FileSystemAdapter/Resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,10 @@ export type MetaStorage = {
username: string;
date: number;
timeout: number;
exclusive: boolean;
scope: 'exclusive' | 'shared';
depth: '0' | 'infinity';
provisional: boolean;
owner: any;
};
};
};
Expand Down Expand Up @@ -93,9 +94,10 @@ export default class Resource implements ResourceInterface {
lock.token = token;
lock.date = new Date(entry.date);
lock.timeout = entry.timeout;
lock.exclusive = entry.exclusive;
lock.scope = entry.scope;
lock.depth = entry.depth;
lock.provisional = entry.provisional;
lock.owner = entry.owner;

return lock;
});
Expand All @@ -116,9 +118,10 @@ export default class Resource implements ResourceInterface {
lock.token = token;
lock.date = new Date(entry.date);
lock.timeout = entry.timeout;
lock.exclusive = entry.exclusive;
lock.scope = entry.scope;
lock.depth = entry.depth;
lock.provisional = entry.provisional;
lock.owner = entry.owner;

return lock;
});
Expand Down Expand Up @@ -574,6 +577,10 @@ export default class Resource implements ResourceInterface {
}

async getMediaType() {
if (await this.isCollection()) {
return null;
}

const mediaType = await new Promise<string>((resolve, reject) => {
const magic = new Magic(mmm.MAGIC_MIME_TYPE);
magic.detectFile(this.absolutePath, function (err, result) {
Expand Down Expand Up @@ -727,6 +734,14 @@ export default class Resource implements ResourceInterface {
const filepath = await this.getMetadataFilePath();
let exists = true;

try {
await fsp.access(path.dirname(filepath), constants.F_OK);
} catch (e: any) {
throw new ResourceTreeNotCompleteError(
'One or more intermediate collections must be created before this resource.'
);
}

try {
await fsp.access(filepath, constants.F_OK);
} catch (e: any) {
Expand Down
13 changes: 10 additions & 3 deletions src/Interfaces/Adapter.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { Method } from '../index.js';

import type { Resource } from './Resource.js';
import type { User } from './User.js';
import type { Lock } from './Lock.js';

export type AuthResponse<
ResBody = any,
Expand Down Expand Up @@ -150,6 +151,11 @@ export interface Adapter {
*/
newCollection(url: URL, baseUrl: string): Promise<Resource>;

/**
* Format a list of locks into an object acceptable by xml2js.
*/
formatLocks(locks: Lock[], baseUrl: URL): Promise<any>;

/**
* Get a handler class for an additional method.
*
Expand All @@ -158,9 +164,10 @@ export interface Adapter {
* returning specific error codes in certain situations, you should handle
* errors within this class' `run` function.
*
* If the requested method is not supported (i.e. it isn't included in the
* output from `getAllowedMethods`), a MethodNotSupportedError should be
* thrown.
* If the requested method is not supported (i.e. it is purposefully excluded
* from the output from `getAllowedMethods`), a MethodNotSupportedError should
* be thrown. If the method is not recognized, a MethodNotImplementedError
* should be thrown.
*/
getMethod(method: string): typeof Method;
}
Loading

0 comments on commit 92ae750

Please sign in to comment.