diff --git a/package.json b/package.json index e013c838..7601b11f 100644 --- a/package.json +++ b/package.json @@ -36,13 +36,13 @@ "node": ">=10.1.0" }, "devDependencies": { - "@types/node": "^11.9.6", + "@types/node": "^11.11.0", "ava": "^1.0.1", "eslint": "^5.2.0", - "eslint-config-klasa": "github:dirigeants/klasa-lint", + "eslint-config-klasa": "dirigeants/klasa-lint", "nyc": "^13.0.1", - "tslint": "^5.7.0", "ts-node": "^8.0.3", + "tslint": "^5.7.0", "typedoc": "^0.14.2", "typescript": "^3.0.1" }, diff --git a/scripts/.azure-test-template.yml b/scripts/.azure-test-template.yml index 9d9162d6..b7020b0d 100644 --- a/scripts/.azure-test-template.yml +++ b/scripts/.azure-test-template.yml @@ -20,8 +20,11 @@ jobs: versionSpec: $(nodeVersion) displayName: 'Install Node.js' - script: 'yarn' + displayName: 'Install Dependencies' - script: 'yarn run test:coverage' + displayName: 'Run Tests' - script: 'yarn run coverage:azure' + displayName: 'Generate Reports' - task: PublishCodeCoverageResults@1 inputs: codeCoverageTool: 'cobertura' diff --git a/src/index.ts b/src/index.ts index f7bdb9f8..f3568e63 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,6 +21,10 @@ export { default as ensureLink } from './nextra/createLink'; export { default as ensureLinkAtomic } from './nextra/createLinkAtomic'; export { default as ensureSymlink } from './nextra/createSymlink'; export { default as ensureSymlinkAtomic } from './nextra/createSymlinkAtomic'; +export { default as gunzip } from './nextra/gunzip'; +export { default as gunzipAtomic } from './nextra/gunzipAtomic'; +export { default as gzip } from './nextra/gzip'; +export { default as gzipAtomic } from './nextra/gzip'; export { default as linkAtomic } from './nextra/linkAtomic'; export { default as mkdirp } from './nextra/mkdirs'; export { default as mkdirs } from './nextra/mkdirs'; @@ -37,6 +41,10 @@ export { default as readJson } from './nextra/readJSON'; export { default as remove } from './nextra/remove'; export { default as scan } from './nextra/scan'; export { default as symlinkAtomic } from './nextra/symlinkAtomic'; +export { default as targz } from './nextra/targz'; +export { default as targzAtomic } from './nextra/targzAtomic'; +export { default as unTargz } from './nextra/unTargz'; +export { default as unTargzAtomic } from './nextra/unTargzAtomic'; export { default as writeFileAtomic } from './nextra/writeFileAtomic'; export { default as writeJSON } from './nextra/writeJSON'; export { default as writeJSONAtomic } from './nextra/writeJSONAtomic'; diff --git a/src/nextra/copy.ts b/src/nextra/copy.ts index cdfee586..3edcf4bb 100644 --- a/src/nextra/copy.ts +++ b/src/nextra/copy.ts @@ -1,6 +1,6 @@ import { resolve, dirname, join, basename } from 'path'; -import { replaceEsc, isSrcKid } from '../util'; +import { replaceEsc, isSrcKid } from '../utils/util'; import { access, readlink, mkdir, symlink, copyFile, lstat, stat, chmod, readdir, Stats } from '../fs'; import mkdirs from './mkdirs'; diff --git a/src/nextra/copyFileAtomic.ts b/src/nextra/copyFileAtomic.ts index 785d4cd6..68a1fc3e 100644 --- a/src/nextra/copyFileAtomic.ts +++ b/src/nextra/copyFileAtomic.ts @@ -1,4 +1,4 @@ -import { tempFile } from '../util'; +import { tempFile } from '../utils/util'; import { copyFile } from '../fs'; import move from './move'; diff --git a/src/nextra/gunzip.ts b/src/nextra/gunzip.ts new file mode 100644 index 00000000..ea385b5c --- /dev/null +++ b/src/nextra/gunzip.ts @@ -0,0 +1,23 @@ +import { createGunzip } from 'zlib'; + +import { createWriteStream, createReadStream } from '../fs'; +import { pipelinePromise } from '../utils/util'; +import gunzipAtomic from './gunzipAtomic'; + +/** + * Un-Gzips a file + * @function gunzip + * @memberof fsn/nextra + * @param fileName The filename of the output file + * @param inputFile The filepath of the archive + * @param atomic If the unzip file should be created atomically + */ +export default async function gzip(fileName: string, inputFile: string, atomic: boolean = false): Promise { + if (atomic) return gunzipAtomic(fileName, inputFile); + + return pipelinePromise( + createReadStream(inputFile), + createGunzip(), + createWriteStream(fileName) + ); +} diff --git a/src/nextra/gunzipAtomic.ts b/src/nextra/gunzipAtomic.ts new file mode 100644 index 00000000..7ce8a45f --- /dev/null +++ b/src/nextra/gunzipAtomic.ts @@ -0,0 +1,16 @@ +import gunzip from './gunzip'; +import move from './move'; +import { tempFile } from '../utils/util'; + +/** + * Un-Gzips a file atomically. + * @function gunzipAtomic + * @memberof fsn/nextra + * @param fileName The filename of the output file + * @param inputFile The filepath of the archive + */ +export default async function gzipAtomic(fileName: string, inputFile: string): Promise { + const tempPath = tempFile(); + await gunzip(tempPath, inputFile); + return move(tempPath, fileName, { overwrite: true }); +} diff --git a/src/nextra/gzip.ts b/src/nextra/gzip.ts new file mode 100644 index 00000000..afa1986c --- /dev/null +++ b/src/nextra/gzip.ts @@ -0,0 +1,23 @@ +import { createGzip } from 'zlib'; + +import { createWriteStream, createReadStream } from '../fs'; +import { pipelinePromise } from '../utils/util'; +import gzipAtomic from './gzipAtomic'; + +/** + * Gzips a file + * @function gzip + * @memberof fsn/nextra + * @param fileName The filename of the archive + * @param inputFile The filepath of the input file + * @param atomic If the gzip file should be created + */ +export default async function gzip(fileName: string, inputFile: string, atomic: boolean = false): Promise { + if (atomic) return gzipAtomic(fileName, inputFile); + + return pipelinePromise( + createReadStream(inputFile), + createGzip(), + createWriteStream(fileName) + ); +} diff --git a/src/nextra/gzipAtomic.ts b/src/nextra/gzipAtomic.ts new file mode 100644 index 00000000..13be8fac --- /dev/null +++ b/src/nextra/gzipAtomic.ts @@ -0,0 +1,16 @@ +import gzip from './gzip'; +import move from './move'; +import { tempFile } from '../utils/util'; + +/** + * Gzips a file atomically. + * @function gzipAtomic + * @memberof fsn/nextra + * @param fileName The filename of the archive + * @param inputFile The filepath of the input file + */ +export default async function gzipAtomic(fileName: string, inputFile: string): Promise { + const tempPath = tempFile(); + await gzip(tempPath, inputFile); + return move(tempPath, fileName, { overwrite: true }); +} diff --git a/src/nextra/linkAtomic.ts b/src/nextra/linkAtomic.ts index 09f3f5ae..abee5fd3 100644 --- a/src/nextra/linkAtomic.ts +++ b/src/nextra/linkAtomic.ts @@ -1,4 +1,4 @@ -import { tempFile } from '../util'; +import { tempFile } from '../utils/util'; import { link } from '../fs'; import move from './move'; diff --git a/src/nextra/mkdirs.ts b/src/nextra/mkdirs.ts index daf99dcc..b7dca59e 100644 --- a/src/nextra/mkdirs.ts +++ b/src/nextra/mkdirs.ts @@ -1,6 +1,6 @@ import { resolve, dirname, normalize, sep } from 'path'; -import { isWindows } from '../util'; +import { isWindows } from '../utils/util'; import { stat, mkdir } from '../fs'; /** diff --git a/src/nextra/move.ts b/src/nextra/move.ts index bcccb5ac..a8d706fa 100644 --- a/src/nextra/move.ts +++ b/src/nextra/move.ts @@ -1,6 +1,6 @@ import { resolve, dirname } from 'path'; -import { isSrcKid } from '../util'; +import { isSrcKid } from '../utils/util'; import { access, rename, stat } from '../fs'; import remove from './remove'; diff --git a/src/nextra/remove.ts b/src/nextra/remove.ts index 7e764bc7..22913175 100644 --- a/src/nextra/remove.ts +++ b/src/nextra/remove.ts @@ -1,6 +1,6 @@ import { join } from 'path'; -import { isWindows, setTimeoutPromise } from '../util'; +import { isWindows, setTimeoutPromise } from '../utils/util'; import { lstat, unlink, rmdir, chmod, readdir } from '../fs'; /** diff --git a/src/nextra/symlinkAtomic.ts b/src/nextra/symlinkAtomic.ts index d9897efe..1212e7d6 100644 --- a/src/nextra/symlinkAtomic.ts +++ b/src/nextra/symlinkAtomic.ts @@ -1,4 +1,4 @@ -import { tempFile } from '../util'; +import { tempFile } from '../utils/util'; import { symlink } from '../fs'; import { SymLinkType } from './createSymlink'; diff --git a/src/nextra/targz.ts b/src/nextra/targz.ts new file mode 100644 index 00000000..304b8c52 --- /dev/null +++ b/src/nextra/targz.ts @@ -0,0 +1,35 @@ +import { createGzip } from 'zlib'; + +import { createWriteStream } from '../fs'; +import Tar from '../utils/Tar'; +import { pipelinePromise } from '../utils/util'; +import { dirname } from 'path'; +import scan from './scan'; +import targzAtomic from './targzAtomic'; + + +/** + * Tar/Gzips a directory or array of files. + * @function targz + * @memberof fsn/nextra + * @param fileName The filename of the archive + * @param inputFiles The directory or array of filepaths to .tar.gz + * @param options The options for this .tar.gz + */ +export default async function targz(fileName: string, inputFiles: string | string[], atomic: boolean = false): Promise { + if (atomic) return targzAtomic(fileName, inputFiles); + if (!Array.isArray(inputFiles)) inputFiles = [inputFiles]; + + const tar = new Tar(dirname(inputFiles[0])); + + for (const input of inputFiles) { + const files = await scan(input, { filter: stats => stats.isFile() }); + for (const [file, stats] of files) tar.append(file, stats); + } + + return pipelinePromise( + tar, + createGzip(), + createWriteStream(fileName) + ); +} diff --git a/src/nextra/targzAtomic.ts b/src/nextra/targzAtomic.ts new file mode 100644 index 00000000..4af834b4 --- /dev/null +++ b/src/nextra/targzAtomic.ts @@ -0,0 +1,16 @@ +import targz from './targz'; +import move from './move'; +import { tempFile } from '../utils/util'; + +/** + * Tar/Gzips a directory or array of files. + * @function targzAtomic + * @memberof fsn/nextra + * @param fileName The filename of the archive + * @param inputFiles The directory or array of filepaths to .tar.gz + */ +export default async function targzAtomic(fileName: string, inputFiles: string | string[]): Promise { + const tempPath = tempFile(); + await targz(tempPath, inputFiles); + return move(tempPath, fileName, { overwrite: true }); +} diff --git a/src/nextra/unTargz.ts b/src/nextra/unTargz.ts new file mode 100644 index 00000000..685f949f --- /dev/null +++ b/src/nextra/unTargz.ts @@ -0,0 +1,23 @@ +import { createGunzip } from 'zlib'; +import { resolve } from 'path'; + +import { createReadStream } from '../fs'; +import Untar from '../utils/Untar'; +import outputFile from './outputFile'; +import outputFileAtomic from './outputFileAtomic'; + + +/** + * Extracts files from .tar.gz archives. + * @function unTargz + * @memberof fsn/nextra + * @param outputDirectory The directory to extract to + * @param inputFile The archive file + * @param atomic The if the writes should be atomic + */ +export default async function unTargz(outputDirectory: string, inputFile: string, atomic: boolean = false): Promise { + const tar = createReadStream(inputFile).pipe(createGunzip()).pipe(new Untar()); + const writeMethod = atomic ? outputFile : outputFileAtomic; + + for await (const { header, file } of tar.files()) await writeMethod(resolve(outputDirectory, header.filename), file); +} diff --git a/src/nextra/unTargzAtomic.ts b/src/nextra/unTargzAtomic.ts new file mode 100644 index 00000000..4404a570 --- /dev/null +++ b/src/nextra/unTargzAtomic.ts @@ -0,0 +1,14 @@ +import unTargz from './unTargz'; + + +/** + * Extracts files from .tar.gz archives and writes them atomically. + * @function unTargzAtomic + * @memberof fsn/nextra + * @param outputDirectory The directory to extract to + * @param inputFile The archive file + * @param atomic The if the writes should be atomic + */ +export default async function unTargzAtomic(outputDirectory: string, inputFile: string): Promise { + return unTargz(outputDirectory, inputFile, true); +} diff --git a/src/nextra/writeFileAtomic.ts b/src/nextra/writeFileAtomic.ts index 280a0a29..e304afa9 100644 --- a/src/nextra/writeFileAtomic.ts +++ b/src/nextra/writeFileAtomic.ts @@ -1,4 +1,4 @@ -import { tempFile } from '../util'; +import { tempFile } from '../utils/util'; import { writeFile } from '../fs'; import move from './move'; diff --git a/src/utils/Tar.ts b/src/utils/Tar.ts new file mode 100644 index 00000000..3c28832e --- /dev/null +++ b/src/utils/Tar.ts @@ -0,0 +1,64 @@ +import { relative } from 'path'; +import { Readable } from 'stream'; +import { encodeHeader } from './header'; +import { createReadStream, Stats } from '../fs'; + +export default class Tar extends Readable { + + private base: string; + private written: number = 0; + private recordSize = 512; + private queue: Array<{ header: Buffer, file: Readable, size: number }> = []; + + public constructor(base: string) { + super(); + + this.base = base; + } + + public async _read(): Promise { + if (!this.queue.length) { + this.push(null); + return; + } + + const { header, file, size } = this.queue.shift(); + const { written } = this; + + this.written += header.length; + + const fileChunks = []; + + for await (const chunk of file) { + fileChunks.push(chunk); + this.written += chunk.length; + } + + // Hard to produce, requires a size perfectibly divisible by the recordSize + /* istanbul ignore next */ + const extraBytes = this.recordSize - (size % this.recordSize || this.recordSize); + this.written += extraBytes; + + this.push(Buffer.concat([header, ...fileChunks, Buffer.alloc(extraBytes)], this.written - written)); + } + + public append(filepath: string, stats: Stats): void { + this.queue.push({ + header: encodeHeader({ + filename: relative(this.base, filepath), + mode: stats.mode, + uid: stats.uid, + gid: stats.gid, + size: stats.size, + mtime: Math.trunc(stats.mtime.valueOf() / 1000), + type: '0', + ustar: 'ustar ', + owner: '', + group: '' + }), + file: createReadStream(filepath), + size: stats.size + }); + } + +} diff --git a/src/utils/Untar.ts b/src/utils/Untar.ts new file mode 100644 index 00000000..68e5833f --- /dev/null +++ b/src/utils/Untar.ts @@ -0,0 +1,74 @@ +import { Writable } from 'stream'; +import { decodeHeader, HeaderFormat } from './header'; + +export default class Untar extends Writable { + + private header: HeaderFormat = null; + private file: Buffer = Buffer.alloc(0); + private totalRead: number = 0; + private recordSize: number = 512; + private queue: Array<{ header: HeaderFormat, file: Buffer }> = []; + + public _write(data: Buffer, encoding: string, next: Function) { + this.file = Buffer.concat([this.file, data], this.file.length + data.length); + + if (this.file.length === 0) return next(); + + // file + + if (this.header) { + if (this.file.length >= this.header.size) { + this.send(); + return this._write(Buffer.alloc(0), encoding, next); + } + return next(); + } + + // Remove extra bits between files + + if (this.totalRead % this.recordSize) { + this.slice(Math.min(this.recordSize - (this.totalRead % this.recordSize), this.file.length)); + return this._write(Buffer.alloc(0), encoding, next); + } + + // Hard to test, requires the leftover of a chunk to be less than the size of a header block + /* istanbul ignore next */ + if (this.file.length < this.recordSize) return next(); + + // New Header + + this.header = decodeHeader(this.slice(this.recordSize)); + + return this._write(Buffer.alloc(0), encoding, next); + } + + private slice(length: number): Buffer { + const buffer = this.file.slice(0, length); + this.file = this.file.slice(length); + this.totalRead += length; + return buffer; + } + + private send() { + if (this.listenerCount('file')) this.emit('file', this.header, this.slice(this.header.size)); + else this.queue.push({ header: this.header, file: this.slice(this.header.size) }); + this.header = null; + } + + private next(): Promise<{ header: HeaderFormat, file: Buffer }> { + if (this.queue.length) return Promise.resolve(this.queue.shift()); + if (!this.writable) return Promise.resolve(null); + return new Promise((resolve) => { + this.once('file', (header: HeaderFormat, file: Buffer) => { + resolve({ header, file }); + }); + }); + } + + public async *files(): AsyncIterableIterator<{ header: HeaderFormat, file: Buffer }> { + let file: { header: HeaderFormat, file: Buffer }; + + while (file = await this.next()) yield file; + } + +} diff --git a/src/utils/header.ts b/src/utils/header.ts new file mode 100644 index 00000000..e8362f86 --- /dev/null +++ b/src/utils/header.ts @@ -0,0 +1,175 @@ +const headerFormat = [ + { + field: 'filename', + length: 100, + type: 'string' + }, + { + field: 'mode', + length: 8, + type: 'number' + }, + { + field: 'uid', + length: 8, + type: 'number' + }, + { + field: 'gid', + length: 8, + type: 'number' + }, + { + field: 'size', + length: 12, + type: 'number' + }, + { + field: 'mtime', + length: 12, + type: 'number' + }, + { + field: 'checksum', + length: 8, + type: 'number' + }, + { + field: 'type', + length: 1, + type: 'number' + }, + { + field: 'linkName', + length: 100, + type: 'string' + }, + { + field: 'ustar', + length: 8, + type: 'string' + }, + { + field: 'owner', + length: 32, + type: 'string' + }, + { + field: 'group', + length: 32, + type: 'string' + }, + { + field: 'majorNumber', + length: 8, + type: 'number' + }, + { + field: 'minorNumber', + length: 8, + type: 'number' + }, + { + field: 'filenamePrefix', + length: 155, + type: 'string' + }, + { + field: 'padding', + length: 12 + } +]; + +const pad = (num: number, bytes: number, base: number = 8): string => num.toString(base).padStart(bytes, '0'); + +const readInt = (value: string) => parseInt(value, 8) || 0; + +const readString = (buffer: Buffer) => { + for (let i = 0, length = buffer.length; i < length; i ++) if (buffer[i] === 0) return buffer.toString('utf8', 0, i); +}; + +const updateChecksum = (value: string, checksum: number): number => { + for (let i = 0, length = value.length; i < length; i++) checksum += value.charCodeAt(i); + return checksum; +}; + +export interface HeaderFormat { + filename?: string; + mode?: number; + uid?: number; + gid?: number; + size?: number; + mtime?: number; + checksum?: number; + type?: string; + linkName?: string; + ustar?: string; + owner?: string; + group?: string; + majorNumber?: string; + minorNumber?: string; + filenamePrefix?: string; + padding?: any; +} + +export function encodeHeader(data: HeaderFormat): Buffer { + const header = Buffer.alloc(512); + let offset = 0; + + const formatted = { + filename: data.filename, + mode: pad(data.mode, 7), + uid: pad(data.uid, 7), + gid: pad(data.gid, 7), + size: pad(data.size, 11), + mtime: pad(data.mtime, 11), + checksum: ' ', + type: data.type, + ustar: data.ustar, + owner: data.owner, + group: data.group + }; + + for (const { field, length } of headerFormat) { + header.write(formatted[field] || '', offset); + offset += length; + } + + let chksum = 0; + for (let i = 0, { length } = header; i < length; i++) chksum += header[i]; + + const checksum = pad(chksum, 6); + for (let i = 0, length = 6; i < length; i++) header[i + 148] = checksum.charCodeAt(i); + + header[154] = 0; + header[155] = 0x20; + + return header; +} + +export function decodeHeader(data: Buffer): HeaderFormat { + const header: HeaderFormat = {}; + let offset = 0; + let checksum = 0; + + for (const field of headerFormat) { + const tBuf = data.slice(offset, offset + field.length); + const tString = tBuf.toString(); + + offset += field.length; + + // No plans to test, requires a tar generated outside of this lib + /* istanbul ignore next */ + if (field.field === 'ustar' && !/ustar/.test(tString)) break; + + checksum = updateChecksum(field.field === 'checksum' ? ' ' : tString, checksum); + + if (field.type === 'string') header[field.field] = readString(tBuf); + else if (field.type === 'number') header[field.field] = readInt(tString); + } + + // No plans to test, requires a tar with a bad checksum (generated outside of this lib) + /* istanbul ignore next */ + if (checksum !== header.checksum) throw new Error(`Checksum not equal: ${checksum} != ${header.checksum}`); + return header; +} diff --git a/src/util.ts b/src/utils/util.ts similarity index 88% rename from src/util.ts rename to src/utils/util.ts index 03f1851e..526c182a 100644 --- a/src/util.ts +++ b/src/utils/util.ts @@ -2,11 +2,14 @@ import { sep, resolve, join } from 'path'; import { promisify } from 'util'; import { randomBytes } from 'crypto'; import { tmpdir } from 'os'; +import { pipeline } from 'stream'; export const isWindows: boolean = process.platform === 'win32'; export const setTimeoutPromise: typeof setTimeout.__promisify__ = promisify(setTimeout); +export const pipelinePromise: typeof pipeline.__promisify__ = promisify(pipeline); + export const replaceEsc = (str: string): string => str.replace(/\$/g, '$$'); export const isSrcKid = (source: string, destination: string): boolean => { @@ -21,3 +24,4 @@ export const uuid = (): string => { }; export const tempFile = (ext?: string): string => join(tmpdir(), uuid() + (ext || '')); + diff --git a/test/gunzip.ts b/test/gunzip.ts new file mode 100644 index 00000000..4ed16b5b --- /dev/null +++ b/test/gunzip.ts @@ -0,0 +1,36 @@ +import ava from 'ava'; +import { promises as fs } from 'fs'; +import { tempFileLoc } from './lib'; +import * as nextra from '../dist'; + +ava('File', async test => { + test.plan(3); + + const file = tempFileLoc(); + await nextra.writeFile(file, 'test', 'utf8'); + const fileName = `${tempFileLoc()}.gz`; + await nextra.gzip(fileName, file); + const output = tempFileLoc(); + const retVal = await nextra.gunzip(output, fileName); + const stats = await fs.stat(output); + + test.is(retVal, undefined); + test.true(stats.isFile()); + test.is(await nextra.readFile(output, 'utf8'), 'test'); +}); + +ava('File (Atomic Shortcut)', async test => { + test.plan(3); + + const file = tempFileLoc(); + await nextra.writeFile(file, 'test', 'utf8'); + const fileName = `${tempFileLoc()}.gz`; + await nextra.gzip(fileName, file); + const output = tempFileLoc(); + const retVal = await nextra.gunzip(output, fileName, true); + const stats = await fs.stat(output); + + test.is(retVal, undefined); + test.true(stats.isFile()); + test.is(await nextra.readFile(output, 'utf8'), 'test'); +}); diff --git a/test/gunzipAtomic.ts b/test/gunzipAtomic.ts new file mode 100644 index 00000000..fd38b52f --- /dev/null +++ b/test/gunzipAtomic.ts @@ -0,0 +1,20 @@ +import ava from 'ava'; +import { promises as fs } from 'fs'; +import { tempFileLoc } from './lib'; +import * as nextra from '../dist'; + +ava('File', async test => { + test.plan(3); + + const file = tempFileLoc(); + await nextra.writeFile(file, 'test', 'utf8'); + const fileName = `${tempFileLoc()}.gz`; + await nextra.gzip(fileName, file); + const output = tempFileLoc(); + const retVal = await nextra.gunzipAtomic(output, fileName); + const stats = await fs.stat(output); + + test.is(retVal, undefined); + test.true(stats.isFile()); + test.is(await nextra.readFile(output, 'utf8'), 'test'); +}); diff --git a/test/gzip.ts b/test/gzip.ts new file mode 100644 index 00000000..88f45032 --- /dev/null +++ b/test/gzip.ts @@ -0,0 +1,30 @@ +import ava from 'ava'; +import { promises as fs } from 'fs'; +import { tempFileLoc } from './lib'; +import * as nextra from '../dist'; + +ava('File', async test => { + test.plan(2); + + const file = tempFileLoc(); + await nextra.writeFile(file, 'test', 'utf8'); + const fileName = `${tempFileLoc()}.gz`; + const retVal = await nextra.gzip(fileName, file); + const stats = await fs.stat(fileName); + + test.is(retVal, undefined); + test.true(stats.isFile()); +}); + +ava('File (Atomic Shortcut)', async test => { + test.plan(2); + + const file = tempFileLoc(); + await nextra.writeFile(file, 'test', 'utf8'); + const fileName = `${tempFileLoc()}.gz`; + const retVal = await nextra.gzip(fileName, file, true); + const stats = await fs.stat(fileName); + + test.is(retVal, undefined); + test.true(stats.isFile()); +}); diff --git a/test/gzipAtomic.ts b/test/gzipAtomic.ts new file mode 100644 index 00000000..4949c0bd --- /dev/null +++ b/test/gzipAtomic.ts @@ -0,0 +1,17 @@ +import ava from 'ava'; +import { promises as fs } from 'fs'; +import { tempFileLoc } from './lib'; +import * as nextra from '../dist'; + +ava('File', async test => { + test.plan(2); + + const file = tempFileLoc(); + await nextra.writeFile(file, 'test', 'utf8'); + const fileName = `${tempFileLoc()}.tar.gz`; + const retVal = await nextra.gzipAtomic(fileName, file); + const stats = await fs.stat(fileName); + + test.is(retVal, undefined); + test.true(stats.isFile()); +}); diff --git a/test/targz.ts b/test/targz.ts new file mode 100644 index 00000000..5e600b20 --- /dev/null +++ b/test/targz.ts @@ -0,0 +1,88 @@ +import ava from 'ava'; +import { promises as fs } from 'fs'; +import { tempFileLoc, tempDir } from './lib'; +import * as nextra from '../dist'; + +ava('File', async test => { + test.plan(2); + + const file = tempFileLoc(); + await nextra.writeFile(file, 'test', 'utf8'); + const fileName = `${tempFileLoc()}.tar.gz`; + const retVal = await nextra.targz(fileName, file); + const stats = await fs.stat(fileName); + + test.is(retVal, undefined); + test.true(stats.isFile()); +}); + +ava('Files', async test => { + test.plan(2); + + const file1 = tempFileLoc(); + await nextra.writeFile(file1, 'test1', 'utf8'); + const file2 = tempFileLoc(); + await nextra.writeFile(file2, 'test2', 'utf8'); + const fileName = `${tempFileLoc()}.tar.gz`; + const retVal = await nextra.targz(fileName, [file1, file2]); + const stats = await fs.stat(fileName); + + test.is(retVal, undefined); + test.true(stats.isFile()); +}); + +ava('Directory', async test => { + test.plan(2); + + const dir = tempDir(); + await nextra.writeFile(tempFileLoc(dir), 'file1', 'utf8'); + await nextra.writeFile(tempFileLoc(dir), 'file2', 'utf8'); + const fileName = `${tempFileLoc()}.tar.gz`; + const retVal = await nextra.targz(fileName, dir); + const stats = await fs.stat(fileName); + + test.is(retVal, undefined); + test.true(stats.isFile()); +}); + +ava('File (Atomic Shortcut)', async test => { + test.plan(2); + + const file = tempFileLoc(); + await nextra.writeFile(file, 'test', 'utf8'); + const fileName = `${tempFileLoc()}.tar.gz`; + const retVal = await nextra.targz(fileName, file, true); + const stats = await fs.stat(fileName); + + test.is(retVal, undefined); + test.true(stats.isFile()); +}); + +ava('Files (Atomic Shortcut)', async test => { + test.plan(2); + + const file1 = tempFileLoc(); + await nextra.writeFile(file1, 'test1', 'utf8'); + const file2 = tempFileLoc(); + await nextra.writeFile(file2, 'test2', 'utf8'); + const fileName = `${tempFileLoc()}.tar.gz`; + const retVal = await nextra.targz(fileName, [file1, file2], true); + const stats = await fs.stat(fileName); + + test.is(retVal, undefined); + test.true(stats.isFile()); +}); + +ava('Directory (Atomic Shortcut)', async test => { + test.plan(2); + + const dir = tempDir(); + await nextra.writeFile(tempFileLoc(dir), 'file1', 'utf8'); + await nextra.writeFile(tempFileLoc(dir), 'file2', 'utf8'); + const fileName = `${tempFileLoc()}.tar.gz`; + const retVal = await nextra.targz(fileName, dir, true); + const stats = await fs.stat(fileName); + + test.is(retVal, undefined); + test.true(stats.isFile()); +}); diff --git a/test/targzAtomic.ts b/test/targzAtomic.ts new file mode 100644 index 00000000..f440da8b --- /dev/null +++ b/test/targzAtomic.ts @@ -0,0 +1,46 @@ +import ava from 'ava'; +import { promises as fs } from 'fs'; +import { tempFileLoc, tempDir } from './lib'; +import * as nextra from '../dist'; + +ava('File', async test => { + test.plan(2); + + const file = tempFileLoc(); + await nextra.writeFile(file, 'test', 'utf8'); + const fileName = `${tempFileLoc()}.tar.gz`; + const retVal = await nextra.targzAtomic(fileName, file); + const stats = await fs.stat(fileName); + + test.is(retVal, undefined); + test.true(stats.isFile()); +}); + +ava('Files', async test => { + test.plan(2); + + const file1 = tempFileLoc(); + await nextra.writeFile(file1, 'test1', 'utf8'); + const file2 = tempFileLoc(); + await nextra.writeFile(file2, 'test2', 'utf8'); + const fileName = `${tempFileLoc()}.tar.gz`; + const retVal = await nextra.targzAtomic(fileName, [file1, file2]); + const stats = await fs.stat(fileName); + + test.is(retVal, undefined); + test.true(stats.isFile()); +}); + +ava('Directory', async test => { + test.plan(2); + + const dir = tempDir(); + await nextra.writeFile(tempFileLoc(dir), 'file1', 'utf8'); + await nextra.writeFile(tempFileLoc(dir), 'file2', 'utf8'); + const fileName = `${tempFileLoc()}.tar.gz`; + const retVal = await nextra.targzAtomic(fileName, dir); + const stats = await fs.stat(fileName); + + test.is(retVal, undefined); + test.true(stats.isFile()); +}); diff --git a/test/unTargz.ts b/test/unTargz.ts new file mode 100644 index 00000000..6ffc15a4 --- /dev/null +++ b/test/unTargz.ts @@ -0,0 +1,148 @@ +import ava from 'ava'; +import { promises as fs } from 'fs'; +import { basename, resolve, relative } from 'path'; +import { tempFileLoc, tempDir, dir } from './lib'; +import * as nextra from '../dist'; + +ava('File', async test => { + test.plan(3); + + const file = tempFileLoc(); + await nextra.writeFile(file, 'test', 'utf8'); + const fileName = `${tempFileLoc()}.tar.gz`; + await nextra.targz(fileName, file); + const outputDirectory = tempDir(); + const retVal = await nextra.unTargz(outputDirectory, fileName); + const newFile = resolve(outputDirectory, basename(file)); + const stats = await fs.stat(newFile); + + test.is(retVal, undefined); + test.true(stats.isFile()); + test.is(await fs.readFile(newFile, 'utf8'), 'test'); +}); + +ava('Big File', async test => { + test.plan(3); + + const content = `${'big'.repeat(1000000)}file!`; + + const file = tempFileLoc(); + await nextra.writeFile(file, content, 'utf8'); + const fileName = `${tempFileLoc()}.tar.gz`; + await nextra.targz(fileName, file); + const outputDirectory = tempDir(); + const retVal = await nextra.unTargz(outputDirectory, fileName); + const newFile = resolve(outputDirectory, basename(file)); + const stats = await fs.stat(newFile); + + test.is(retVal, undefined); + test.true(stats.isFile()); + test.is(await fs.readFile(newFile, 'utf8'), content); +}); + +ava('Files', async test => { + test.plan(5); + + const file1 = tempFileLoc(); + await nextra.writeFile(file1, 'test1', 'utf8'); + const file2 = tempFileLoc(); + await nextra.writeFile(file2, 'test2', 'utf8'); + const fileName = `${tempFileLoc()}.tar.gz`; + await nextra.targz(fileName, [file1, file2]); + const outputDirectory = tempDir(); + const retVal = await nextra.unTargz(outputDirectory, fileName); + const newFile1 = resolve(outputDirectory, basename(file1)); + const newFile2 = resolve(outputDirectory, basename(file2)); + const [stats1, stats2] = await Promise.all([fs.stat(newFile1), fs.stat(newFile2)]); + + test.is(retVal, undefined); + test.true(stats1.isFile()); + test.true(stats2.isFile()); + test.is(await fs.readFile(newFile1, 'utf8'), 'test1'); + test.is(await fs.readFile(newFile2, 'utf8'), 'test2'); +}); + +ava('Directory', async test => { + test.plan(5); + + const directory = tempDir(); + const file1 = tempFileLoc(directory); + const file2 = tempFileLoc(directory); + await nextra.writeFile(file1, 'file1', 'utf8'); + await nextra.writeFile(file2, 'file2', 'utf8'); + const fileName = `${tempFileLoc()}.tar.gz`; + await nextra.targz(fileName, directory); + const outputDirectory = tempDir(); + const retVal = await nextra.unTargz(outputDirectory, fileName); + const newFile1 = resolve(outputDirectory, relative(dir, file1)); + const newFile2 = resolve(outputDirectory, relative(dir, file2)); + const [stats1, stats2] = await Promise.all([fs.stat(newFile1), fs.stat(newFile2)]); + + test.is(retVal, undefined); + test.true(stats1.isFile()); + test.true(stats2.isFile()); + test.is(await fs.readFile(newFile1, 'utf8'), 'file1'); + test.is(await fs.readFile(newFile2, 'utf8'), 'file2'); +}); + +ava('File (Atomic Shortcut)', async test => { + test.plan(3); + + const file = tempFileLoc(); + await nextra.writeFile(file, 'test', 'utf8'); + const fileName = `${tempFileLoc()}.tar.gz`; + await nextra.targz(fileName, file); + const outputDirectory = tempDir(); + const retVal = await nextra.unTargz(outputDirectory, fileName, true); + const newFile = resolve(outputDirectory, basename(file)); + const stats = await fs.stat(newFile); + + test.is(retVal, undefined); + test.true(stats.isFile()); + test.is(await fs.readFile(newFile, 'utf8'), 'test'); +}); + +ava('Files (Atomic Shortcut)', async test => { + test.plan(5); + + const file1 = tempFileLoc(); + await nextra.writeFile(file1, 'test1', 'utf8'); + const file2 = tempFileLoc(); + await nextra.writeFile(file2, 'test2', 'utf8'); + const fileName = `${tempFileLoc()}.tar.gz`; + await nextra.targz(fileName, [file1, file2]); + const outputDirectory = tempDir(); + const retVal = await nextra.unTargz(outputDirectory, fileName, true); + const newFile1 = resolve(outputDirectory, basename(file1)); + const newFile2 = resolve(outputDirectory, basename(file2)); + const [stats1, stats2] = await Promise.all([fs.stat(newFile1), fs.stat(newFile2)]); + + test.is(retVal, undefined); + test.true(stats1.isFile()); + test.true(stats2.isFile()); + test.is(await fs.readFile(newFile1, 'utf8'), 'test1'); + test.is(await fs.readFile(newFile2, 'utf8'), 'test2'); +}); + +ava('Directory (Atomic Shortcut)', async test => { + test.plan(5); + + const directory = tempDir(); + const file1 = tempFileLoc(directory); + const file2 = tempFileLoc(directory); + await nextra.writeFile(file1, 'file1', 'utf8'); + await nextra.writeFile(file2, 'file2', 'utf8'); + const fileName = `${tempFileLoc()}.tar.gz`; + await nextra.targz(fileName, directory); + const outputDirectory = tempDir(); + const retVal = await nextra.unTargz(outputDirectory, fileName, true); + const newFile1 = resolve(outputDirectory, relative(dir, file1)); + const newFile2 = resolve(outputDirectory, relative(dir, file2)); + const [stats1, stats2] = await Promise.all([fs.stat(newFile1), fs.stat(newFile2)]); + + test.is(retVal, undefined); + test.true(stats1.isFile()); + test.true(stats2.isFile()); + test.is(await fs.readFile(newFile1, 'utf8'), 'file1'); + test.is(await fs.readFile(newFile2, 'utf8'), 'file2'); +}); diff --git a/test/unTargzAtomic.ts b/test/unTargzAtomic.ts new file mode 100644 index 00000000..0d3d413a --- /dev/null +++ b/test/unTargzAtomic.ts @@ -0,0 +1,67 @@ +import ava from 'ava'; +import { promises as fs } from 'fs'; +import { basename, resolve, relative } from 'path'; +import { tempFileLoc, tempDir, dir } from './lib'; +import * as nextra from '../dist'; + +ava('File', async test => { + test.plan(3); + + const file = tempFileLoc(); + await nextra.writeFile(file, 'test', 'utf8'); + const fileName = `${tempFileLoc()}.tar.gz`; + await nextra.targz(fileName, file); + const outputDirectory = tempDir(); + const retVal = await nextra.unTargzAtomic(outputDirectory, fileName); + const newFile = resolve(outputDirectory, basename(file)); + const stats = await fs.stat(newFile); + + test.is(retVal, undefined); + test.true(stats.isFile()); + test.is(await fs.readFile(newFile, 'utf8'), 'test'); +}); + +ava('Files', async test => { + test.plan(5); + + const file1 = tempFileLoc(); + await nextra.writeFile(file1, 'test1', 'utf8'); + const file2 = tempFileLoc(); + await nextra.writeFile(file2, 'test2', 'utf8'); + const fileName = `${tempFileLoc()}.tar.gz`; + await nextra.targz(fileName, [file1, file2]); + const outputDirectory = tempDir(); + const retVal = await nextra.unTargzAtomic(outputDirectory, fileName); + const newFile1 = resolve(outputDirectory, basename(file1)); + const newFile2 = resolve(outputDirectory, basename(file2)); + const [stats1, stats2] = await Promise.all([fs.stat(newFile1), fs.stat(newFile2)]); + + test.is(retVal, undefined); + test.true(stats1.isFile()); + test.true(stats2.isFile()); + test.is(await fs.readFile(newFile1, 'utf8'), 'test1'); + test.is(await fs.readFile(newFile2, 'utf8'), 'test2'); +}); + +ava('Directory', async test => { + test.plan(5); + + const directory = tempDir(); + const file1 = tempFileLoc(directory); + const file2 = tempFileLoc(directory); + await nextra.writeFile(file1, 'file1', 'utf8'); + await nextra.writeFile(file2, 'file2', 'utf8'); + const fileName = `${tempFileLoc()}.tar.gz`; + await nextra.targz(fileName, directory); + const outputDirectory = tempDir(); + const retVal = await nextra.unTargzAtomic(outputDirectory, fileName); + const newFile1 = resolve(outputDirectory, relative(dir, file1)); + const newFile2 = resolve(outputDirectory, relative(dir, file2)); + const [stats1, stats2] = await Promise.all([fs.stat(newFile1), fs.stat(newFile2)]); + + test.is(retVal, undefined); + test.true(stats1.isFile()); + test.true(stats2.isFile()); + test.is(await fs.readFile(newFile1, 'utf8'), 'file1'); + test.is(await fs.readFile(newFile2, 'utf8'), 'file2'); +}); diff --git a/yarn.lock b/yarn.lock index ad3141be..9991d021 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1170,9 +1170,8 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.4, escape-string-regexp@^ version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" -"eslint-config-klasa@github:dirigeants/klasa-lint": +eslint-config-klasa@dirigeants/klasa-lint: version "0.0.1" - uid d24af720607fe149a7689c03cfd0e50c8284be15 resolved "https://codeload.github.com/dirigeants/klasa-lint/tar.gz/d24af720607fe149a7689c03cfd0e50c8284be15" eslint-scope@^4.0.2: