/* Copyright IBM Corp. 2018 */ import { readFile, writeFile } from 'graceful-fs'; import { define as mimeDefine, getType } from 'mime'; import { join, parse, ParsedPath } from 'path'; import { empty, merge, Observable, Observer } from 'rxjs'; import { filter, mergeMap, share } from 'rxjs/operators'; import { VError } from 'verror'; import { gzip, ZlibOptions } from 'zlib'; import { rxWalkFiles } from './rx.walk'; const brotli = require('brotli'); // register mimeDefine({ 'application/brotli': ['br'] }); function _isCompressed(aMimeType: string): boolean { return (aMimeType === 'application/gzip') || (aMimeType === 'application/gzip') || (aMimeType === 'application/brotli'); } const GZIP_OPTIONS: ZlibOptions = { level: 9 }; function _readFile(aPath: string): Observable { return Observable.create((observer: Observer) => { readFile(aPath, (errRead, data) => { if (errRead) { observer.error(new VError(errRead, 'Unable to read %s.', aPath)); } else { observer.next(data); observer.complete(); } }); }); } function _compressGzip(aFile: string, aData: Buffer): Observable { // compress return Observable.create((aObserver: Observer) => { // build the target file const parsed: ParsedPath = parse(aFile); const dstFile = join(parsed.dir, parsed.base + '.gz'); // compress gzip(aData, GZIP_OPTIONS, (zipError, zipData) => { if (zipError) { aObserver.error(new VError(zipError, 'Unable to compress %s.', aFile)); } else { writeFile(dstFile, zipData, writeErr => { if (writeErr) { aObserver.error(new VError(writeErr, 'Unable to write %s.', dstFile)); } else { aObserver.next(dstFile); aObserver.complete(); } }); } }); }); } const BROTLI_OPTIONS = { mode: 0, // 0 = generic, 1 = text, 2 = font (WOFF2) quality: 11, // 0 - 11 lgwin: 22 // window size }; function _compressBrotli(aFile: string, aData: Buffer): Observable { // read the file const type = getType(aFile); // otions let opts: any; // create the options if (type && type.startsWith('image/')) { opts = Object.assign({}, BROTLI_OPTIONS, { 'mode': 0 }); } else { opts = Object.assign({}, BROTLI_OPTIONS, { 'mode': 1 }); } // compress return Observable.create((aObserver: Observer) => { // build the target file const parsed: ParsedPath = parse(aFile); const dstFile = join(parsed.dir, parsed.base + '.br'); // compress const brotliData = brotli.compress(aData, opts); // write writeFile(dstFile, brotliData, writeErr => { if (writeErr) { aObserver.error(new VError(writeErr, 'Unable to write %s.', dstFile)); } else { aObserver.next(dstFile); aObserver.complete(); } }); }); } function _compressFile(aFile: string): Observable { // test the mime type const type = getType(aFile); if (type && _isCompressed(type)) { return empty(); } // read const rxRead = _readFile(aFile).pipe(share()); // gzip const rxGzip = rxRead.pipe(mergeMap(data => _compressGzip(aFile, data))); const rxBrotli = rxRead.pipe(mergeMap(data => _compressBrotli(aFile, data))); // compress return merge(rxGzip, rxBrotli); } function _compressDir(aDir: string): Observable { // walk the files return rxWalkFiles(aDir).pipe( filter(fd => fd.stats.isFile()), mergeMap(fd => _compressFile(fd.absPath)) ); } export { _compressFile as rxCompressFile, _compressDir as rxCompressDir };