UNPKG

@lyra/export

Version:

Export Lyra documents and assets

281 lines (219 loc) 9.8 kB
'use strict'; var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } const path = require('path'); const crypto = require('crypto'); const fse = require('fs-extra'); const miss = require('mississippi'); const PQueue = require('p-queue'); var _require = require('lodash'); const omit = _require.omit; const pkg = require('../package.json'); const requestStream = require('./requestStream'); const debug = require('./debug'); const EXCLUDE_PROPS = ['_id', '_type', 'assetId', 'extension', 'mimeType', 'path', 'url']; const ACTION_REMOVE = 'remove'; const ACTION_REWRITE = 'rewrite'; class AssetHandler { constructor(options) { var _this = this; this.rewriteAssets = miss.through.obj((() => { var _ref = _asyncToGenerator(function* (doc, enc, callback) { if (['lyra.imageAsset', 'lyra.fileAsset'].includes(doc._type)) { const type = doc._type === 'lyra.imageAsset' ? 'image' : 'file'; const filePath = `${type}s/${generateFilename(doc._id)}`; _this.assetsSeen.set(doc._id, type); _this.queueAssetDownload(doc, filePath, type); callback(); return; } callback(null, (yield _this.findAndModify(doc, ACTION_REWRITE))); }); return function (_x, _x2, _x3) { return _ref.apply(this, arguments); }; })()); this.stripAssets = miss.through.obj((() => { var _ref2 = _asyncToGenerator(function* (doc, enc, callback) { if (['lyra.imageAsset', 'lyra.fileAsset'].includes(doc._type)) { callback(); return; } callback(null, (yield _this.findAndModify(doc, ACTION_REMOVE))); }); return function (_x4, _x5, _x6) { return _ref2.apply(this, arguments); }; })()); this.skipAssets = miss.through.obj((doc, enc, callback) => { const isAsset = ['lyra.imageAsset', 'lyra.fileAsset'].includes(doc._type); if (isAsset) { callback(); return; } callback(null, doc); }); this.noop = miss.through.obj((doc, enc, callback) => callback(null, doc)); this.findAndModify = (() => { var _ref3 = _asyncToGenerator(function* (item, action) { if (Array.isArray(item)) { const children = yield Promise.all(item.map(function (child) { return _this.findAndModify(child, action); })); return children.filter(Boolean); } if (!item || typeof item !== 'object') { return item; } const isAsset = isAssetField(item); if (isAsset && action === ACTION_REMOVE) { return undefined; } if (isAsset && action === ACTION_REWRITE) { const asset = item.asset, other = _objectWithoutProperties(item, ['asset']); const assetId = asset._ref; if (isModernAsset(assetId)) { const assetType = getAssetType(item); const filePath = `${assetType}s/${generateFilename(assetId)}`; return _extends({ _lyraAsset: `${assetType}@file://./${filePath}` }, other); } // Legacy asset const type = _this.assetsSeen.get(assetId) || (yield _this.lookupAssetType(assetId)); const filePath = `${type}s/${generateFilename(assetId)}`; return _extends({ _lyraAsset: `${type}@file://./${filePath}` }, other); } const newItem = {}; const keys = Object.keys(item); for (let i = 0; i < keys.length; i++) { const key = keys[i]; const value = item[key]; // eslint-disable-next-line no-await-in-loop newItem[key] = yield _this.findAndModify(value, action); if (typeof newItem[key] === 'undefined') { delete newItem[key]; } } return newItem; }); return function (_x7, _x8) { return _ref3.apply(this, arguments); }; })(); this.lookupAssetType = (() => { var _ref4 = _asyncToGenerator(function* (assetId) { const docType = yield _this.client.fetch('*[_id == $id][0]._type', { id: assetId }); return docType === 'lyra.imageAsset' ? 'image' : 'file'; }); return function (_x9) { return _ref4.apply(this, arguments); }; })(); this.client = options.client; this.tmpDir = options.tmpDir; this.assetDirsCreated = false; this.assetsSeen = new Map(); this.assetMap = {}; this.filesWritten = 0; this.queueSize = 0; this.queue = options.queue || new PQueue({ concurrency: 3 }); this.reject = () => { throw new Error('Asset handler errored before `finish()` was called'); }; } clear() { this.assetsSeen.clear(); this.queue.clear(); this.queueSize = 0; } finish() { return new Promise((resolve, reject) => { this.reject = reject; this.queue.onIdle().then(() => resolve(this.assetMap)); }); } // Called when we want to download all assets to local filesystem and rewrite documents to hold // placeholder asset references (_lyraAsset: 'image@file:///local/path') // Called in the case where we don't _want_ assets, so basically just remove all asset documents // as well as references to assets (*.asset._ref ^= (image|file)-) // Called when we are using raw export mode along with `assets: false`, where we simply // want to skip asset documents but retain asset references (useful for data mangling) queueAssetDownload(assetDoc, dstPath, type) { if (!assetDoc.url) { debug('Asset document "%s" does not have a URL property, skipping', assetDoc._id); return; } debug('Adding download task for %s (destination: %s)', assetDoc._id, dstPath); this.queueSize++; this.queue.add(() => this.downloadAsset(assetDoc, dstPath)); } downloadAsset(assetDoc, dstPath) { var _this2 = this; return _asyncToGenerator(function* () { const url = assetDoc.url; const headers = { 'User-Agent': `${pkg.name}@${pkg.version}` }; const stream = yield requestStream({ url, headers }); if (stream.statusCode !== 200) { _this2.queue.clear(); _this2.reject(new Error(`Referenced asset URL "${url}" returned HTTP ${stream.statusCode}`)); return; } if (!_this2.assetDirsCreated) { /* eslint-disable no-sync */ fse.ensureDirSync(path.join(_this2.tmpDir, 'files')); fse.ensureDirSync(path.join(_this2.tmpDir, 'images')); /* eslint-enable no-sync */ _this2.assetDirsCreated = true; } debug('Asset stream ready, writing to filesystem at %s', dstPath); const hash = yield writeHashedStream(path.join(_this2.tmpDir, dstPath), stream); const type = assetDoc._type === 'lyra.imageAsset' ? 'image' : 'file'; const id = `${type}-${hash}`; const metaProps = omit(assetDoc, EXCLUDE_PROPS); if (Object.keys(metaProps).length > 0) { _this2.assetMap[id] = metaProps; } _this2.filesWritten++; })(); } // eslint-disable-next-line complexity } function isAssetField(item) { return item.asset && item.asset._ref; } function getAssetType(item) { if (!item.asset || typeof item.asset._ref !== 'string') { return null; } var _ref5 = item.asset._ref.match(/^(image|file)-/) || [], _ref6 = _slicedToArray(_ref5, 2); const type = _ref6[1]; return type || null; } function isModernAsset(assetId) { return (/^(image|file)/.test(assetId) ); } function generateFilename(assetId) { var _ref7 = assetId.match(/^(image|file)-(.*?)(-[a-z]+)?$/) || [], _ref8 = _slicedToArray(_ref7, 4); const asset = _ref8[2], ext = _ref8[3]; const extension = (ext || 'bin').replace(/^-/, ''); return asset ? `${asset}.${extension}` : `${assetId}.bin`; } function writeHashedStream(filePath, stream) { const hash = crypto.createHash('sha1'); const hasher = miss.through((chunk, enc, cb) => { hash.update(chunk); cb(null, chunk); }); return new Promise((resolve, reject) => miss.pipe(stream, hasher, fse.createWriteStream(filePath), err => { return err ? reject(err) : resolve(hash.digest('hex')); })); } module.exports = AssetHandler;