Skip to content

Commit

Permalink
dev(cspell-io): implement CSpellIONode methods (#3309)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jason3S committed Jul 28, 2022
1 parent f89c101 commit 1a94896
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 23 deletions.
64 changes: 54 additions & 10 deletions packages/cspell-io/src/CSpellIONode.test.ts
@@ -1,5 +1,6 @@
import { CSpellIONode } from './CSpellIONode';
import { pathToSample as ps } from './test/helper';
import { makePathToFile, pathToSample as ps, pathToTemp } from './test/helper';
import { promises as fs } from 'fs';

const sc = expect.stringContaining;
const oc = expect.objectContaining;
Expand Down Expand Up @@ -63,13 +64,56 @@ describe('CSpellIONode', () => {
expect(r).toEqual(expected);
});

// writeFile(_uriOrFilename: string, _content: string): Promise<void> {
// throw new ErrorNotImplemented('writeFile');
// }
// getStat(_uriOrFilename: string): Promise<Stats> {
// throw new ErrorNotImplemented('getStat');
// }
// getStatSync(_uriOrFilename: string): Stats {
// throw new ErrorNotImplemented('getStatSync');
// }
test.each`
url | expected
${'https://raw.githubusercontent.com/streetsidesoftware/cspell/main/tsconfig.json'} | ${oc({ eTag: sc('W/') })}
${__filename} | ${oc({ mtimeMs: expect.any(Number) })}
`('getStat $url', async ({ url, expected }) => {
const cspellIo = new CSpellIONode();
const r = await cspellIo.getStat(url);
expect(r).toEqual(expected);
});

test.each`
url | expected
${'https://raw.gitubusrcotent.com/streetsidesoftware/cspell/main/tsconfig.json'} | ${oc({ code: 'ENOTFOUND' })}
${ps(__dirname, 'not-found.nf')} | ${oc({ code: 'ENOENT' })}
`('getStat with error $url', async ({ url, expected }) => {
const cspellIo = new CSpellIONode();
const r = cspellIo.getStat(url);
await expect(r).rejects.toEqual(expected);
});

test.each`
url | expected
${__filename} | ${oc({ mtimeMs: expect.any(Number) })}
`('getStatSync $url', ({ url, expected }) => {
const cspellIo = new CSpellIONode();
const r = cspellIo.getStatSync(url);
expect(r).toEqual(expected);
});

test.each`
url | expected
${'https://raw.githubusercontent.com/streetsidesoftware/cspell/main/tsconfig.json'} | ${'The URL must be of scheme file'}
${ps(__dirname, 'not-found.nf')} | ${oc({ code: 'ENOENT' })}
`('getStatSync with error $url', async ({ url, expected }) => {
const cspellIo = new CSpellIONode();
expect(() => cspellIo.getStatSync(url)).toThrow(expected);
});

test.each`
filename
${pathToTemp('cities.txt')}
${pathToTemp('cities.txt.gz')}
`('writeFile $filename', async ({ filename }) => {
const content = await fs.readFile(ps('cities.txt'), 'utf-8');
const cspellIo = new CSpellIONode();
await makePathToFile(filename);
await cspellIo.writeFile(filename, content);
expect(await cspellIo.readFile(filename)).toEqual(content);
});
});

// '/Users/jason/projects/cspell6/packages/cspell-io/temp/src/CSpellIONode.test.ts/test_._test/cities.txt'
// '/Users/jason/projects/cspell6/packages/cspell-io/temp/src/CSpellIONode.test.ts/test_._test/cities.txt'
4 changes: 2 additions & 2 deletions packages/cspell-io/src/CSpellIONode.ts
Expand Up @@ -34,9 +34,9 @@ export class CSpellIONode implements CSpellIO {
}
return res.value;
}
writeFile(uriOrFilename: string, _content: string): Promise<void> {
writeFile(uriOrFilename: string, content: string): Promise<void> {
const url = toURL(uriOrFilename);
const res = this.serviceBus.dispatch(RequestFsWriteFile.create({ url }));
const res = this.serviceBus.dispatch(RequestFsWriteFile.create({ url, content }));
if (!isServiceResponseSuccess(res)) {
throw genError(res.error, 'writeFile');
}
Expand Down
9 changes: 9 additions & 0 deletions packages/cspell-io/src/errors/index.ts
@@ -0,0 +1,9 @@
import { format } from 'util';

export function toError(e: unknown): Error {
if (e instanceof Error) return e;
if (typeof e === 'object' && e && (e as Error).message) {
return e as Error;
}
return Error(format(e));
}
85 changes: 83 additions & 2 deletions packages/cspell-io/src/handlers/node/file.ts
Expand Up @@ -7,17 +7,28 @@ import {
ServiceBus,
} from '@cspell/cspell-service-bus';
import assert from 'assert';
import { promises as fs, readFileSync } from 'fs';
import { gunzipSync } from 'zlib';
import { promises as fs, readFileSync, statSync } from 'fs';
import { gunzipSync, gzipSync } from 'zlib';
import { toError } from '../../errors';
import { fetchURL } from '../../node/file/fetch';
import { getStatHttp } from '../../node/file/stat';
import {
RequestFsReadBinaryFile,
RequestFsReadBinaryFileSync,
RequestFsReadFile,
RequestFsReadFileSync,
RequestFsStat,
RequestFsStatSync,
RequestFsWriteFile,
RequestZlibInflate,
} from '../../requests';

const isGzFileRegExp = /\.gz($|[?#])/;

function isGzFile(url: URL): boolean {
return isGzFileRegExp.test(url.pathname);
}

/**
* Handle Binary File Reads
*/
Expand Down Expand Up @@ -104,18 +115,88 @@ function bufferToText(buf: Buffer): string {
return buf[0] === 0x1f && buf[1] === 0x8b ? bufferToText(gunzipSync(buf)) : buf.toString('utf-8');
}

/**
* Handle fs:stat
*/
const handleRequestFsStat = createRequestHandler(
RequestFsStat,
({ params }) => createResponse(fs.stat(params.url)),
undefined,
'Node: fs.stat.'
);

/**
* Handle fs:statSync
*/
const handleRequestFsStatSync = createRequestHandler(
RequestFsStatSync,
(req) => {
const { params } = req;
try {
return createResponse(statSync(params.url));
} catch (e) {
return createResponseFail(req, toError(e));
}
},
undefined,
'Node: fs.stat.'
);

/**
* Handle deflating gzip data
*/
const handleRequestFsStatHttp = createRequestHandler(
RequestFsStat,
(req, next) => {
const { url } = req.params;
if (!(url.protocol in supportedFetchProtocols)) return next(req);
return createResponse(getStatHttp(url));
},
undefined,
'Node: http get stat'
);

/**
* Handle fs:writeFile
*/
const handleRequestFsWriteFile = createRequestHandler(
RequestFsWriteFile,
({ params }) => createResponse(fs.writeFile(params.url, params.content)),
undefined,
'Node: fs.writeFile'
);

/**
* Handle fs:writeFile compressed
*/
const handleRequestFsWriteFileGz = createRequestHandler(
RequestFsWriteFile,
(req, next) => {
const { url, content } = req.params;
if (!isGzFile(url)) return next(req);
return createResponse(fs.writeFile(url, gzipSync(content)));
},
undefined,
'Node: http get stat'
);

export function registerHandlers(serviceBus: ServiceBus) {
/**
* Handlers are in order of low to high level
* Order is VERY important.
*/
const handlers = [
handleRequestFsWriteFile,
handleRequestFsWriteFileGz,
handleRequestFsReadBinaryFile,
handleRequestFsReadBinaryFileSync,
handleRequestFsReadBinaryFileHttp,
handleRequestFsReadFile,
handleRequestFsReadFileSync,
handleRequestZlibInflate,
handleRequestFsStatSync,
handleRequestFsStat,
handleRequestFsStatHttp,
];

handlers.forEach((handler) => serviceBus.addHandler(handler));
Expand Down
19 changes: 18 additions & 1 deletion packages/cspell-io/src/node/file/stat.test.ts
@@ -1,4 +1,4 @@
import { getStat } from './stat';
import { getStat, getStatSync } from './stat';
import { join } from 'path';

const oc = expect.objectContaining;
Expand All @@ -22,4 +22,21 @@ describe('stat', () => {
const r = await getStat(url);
expect(r).toEqual(expected);
});

test.each`
url | expected
${__filename} | ${oc({ mtimeMs: expect.any(Number) })}
`('getStatSync $url', ({ url, expected }) => {
const r = getStatSync(url);
expect(r).toEqual(expected);
});

test.each`
url | expected
${'https://raw.githubusercontent.com/streetsidesoftware/cspell/main/tsconfig.json'} | ${oc({ code: 'ENOENT' })}
${join(__dirname, 'not-found.nf')} | ${oc({ code: 'ENOENT' })}
`('getStatSync with error $url', async ({ url, expected }) => {
const r = await getStatSync(url);
expect(r).toEqual(expected);
});
});
20 changes: 12 additions & 8 deletions packages/cspell-io/src/node/file/stat.ts
Expand Up @@ -9,14 +9,7 @@ export async function getStat(filenameOrUri: string): Promise<Stats | Error> {
const url = toURL(filenameOrUri);
if (!isFileURL(url)) {
try {
const headers = await fetchHead(url);
const eTag = headers.get('etag') || undefined;
const guessSize = Number.parseInt(headers.get('content-length') || '0', 10);
return {
size: eTag ? -1 : guessSize,
mtimeMs: 0,
eTag,
};
return await getStatHttp(url);
} catch (e) {
return toError(e);
}
Expand All @@ -33,6 +26,17 @@ export function getStatSync(uri: string): Stats | Error {
}
}

export async function getStatHttp(url: URL): Promise<Stats> {
const headers = await fetchHead(url);
const eTag = headers.get('etag') || undefined;
const guessSize = Number.parseInt(headers.get('content-length') || '0', 10);
return {
size: eTag ? -1 : guessSize,
mtimeMs: 0,
eTag,
};
}

function toError(e: unknown): Error {
if (isErrnoException(e) || e instanceof Error) return e;
return new Error(format(e));
Expand Down
1 change: 1 addition & 0 deletions packages/cspell-io/src/requests/RequestFsWriteFile.ts
Expand Up @@ -3,5 +3,6 @@ import { requestFactory } from '@cspell/cspell-service-bus';
const RequestType = 'fs:writeFile' as const;
interface RequestParams {
readonly url: URL;
readonly content: string;
}
export const RequestFsWriteFile = requestFactory<typeof RequestType, RequestParams, Promise<void>>(RequestType);

0 comments on commit 1a94896

Please sign in to comment.