tc-source-content-updater
Version:
Module that updates source content for the desktop application translationCore.
656 lines (572 loc) • 24.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.isDirectory = exports.processResource = exports.unzipResource = exports.cleanReaddirSync = exports.TRANSLATION_HELPS_INDEX = undefined;
/**
* @description Processes a resource in the imports directory as needed
* @param {Object} resource Resource object
* @param {String} sourcePath Path to the source dictory of the resource
* @param {String} resourcesPath Path to user resources folder
* @return {String} Path to the directory of the processed files
* @param {Array} downloadErrors - parsed list of download errors with details such as if the download completed (vs. parsing error), error, and url
* @param {object} config - configuration object
*/
let processResource = exports.processResource = (() => {
var _ref2 = _asyncToGenerator(function* (resource, sourcePath, resourcesPath, downloadErrors, config = {}) {
try {
if (!resource || !(0, _util.isObject)(resource) || !resource.languageId || !resource.resourceId) {
throw Error(formatError(resource, errors.RESOURCE_NOT_GIVEN));
}
if (!sourcePath) {
throw Error(formatError(resource, errors.SOURCE_PATH_NOT_GIVEN));
}
if (!_fsExtra2.default.pathExistsSync(sourcePath)) {
throw Error(formatError(resource, errors.SOURCE_PATH_NOT_EXIST));
}
const processedFilesPath = sourcePath + '_processed';
_fsExtra2.default.ensureDirSync(processedFilesPath);
const subject = resource.subject && resource.subject.replaceAll(' ', '_'); // replace spaces with underscores;
switch (subject) {
case 'Translation_Words':
twArticleHelpers.processTranslationWords(resource, sourcePath, processedFilesPath);
break;
case 'TSV_Translation_Words_Links':
yield twArticleHelpers.processTranslationWordsTSV(resource, sourcePath, processedFilesPath, resourcesPath, downloadErrors, config);
break;
case 'TSV_Translation_Notes':
yield tnArticleHelpers.processTranslationNotes(resource, sourcePath, processedFilesPath, resourcesPath, downloadErrors, config);
break;
case 'Translation_Academy':
taArticleHelpers.processTranslationAcademy(resource, sourcePath, processedFilesPath);
break;
case 'Bible':
case 'Aligned_Bible':
case 'Greek_New_Testament':
case 'Hebrew_Old_Testament':
packageParseHelpers.parseBiblePackage(resource, sourcePath, processedFilesPath, config);
break;
default:
_fsExtra2.default.copySync(sourcePath, processedFilesPath);
}
let manifest = getResourceManifest(processedFilesPath);
if (!manifest) {
// if manifest not found, create
manifest = getResourceManifest(sourcePath);
}
const stage = resource.catalogEntry && resource.catalogEntry.resource && resource.catalogEntry.resource.stage;
if (manifest) {
if (resource.version) {
manifest.version = resource.version;
}
manifest.modifiedTime = manifest.catalog_modified_time = resource.remoteModifiedTime || resource.modified;
if (stage) {
manifest.stage = stage;
}
_fsExtra2.default.outputJsonSync(_pathExtra2.default.join(processedFilesPath, 'manifest.json'), manifest, { spaces: 2 });
}
if (stage) {
(0, _resourcesDownloadHelpers.setResourcesPreReleaseStateFromStage)(resourcesPath, stage);
}
return processedFilesPath;
} catch (error) {
console.error(error);
throw Error(appendError(errors.FAILED_TO_PROCESS_RESOURCE, error));
}
});
return function processResource(_x4, _x5, _x6, _x7) {
return _ref2.apply(this, arguments);
};
})();
/**
* encode owner to string without characters unsupported by file system
* @param {string} owner
* @return {string}
*/
exports.getVersionFromManifest = getVersionFromManifest;
exports.getResourceManifest = getResourceManifest;
exports.getResourceManifestFromJson = getResourceManifestFromJson;
exports.getResourceManifestFromYaml = getResourceManifestFromYaml;
exports.getVersionsInPath = getVersionsInPath;
exports.getVersionAndOwnerFromPath = getVersionAndOwnerFromPath;
exports.splitVersionAndOwner = splitVersionAndOwner;
exports.getLatestVersionsAndOwners = getLatestVersionsAndOwners;
exports.sortVersionsByNumber = sortVersionsByNumber;
exports.listVersions = listVersions;
exports.compareVersions = compareVersions;
exports.sortVersions = sortVersions;
exports.getLatestVersionInPath = getLatestVersionInPath;
exports.getLatestVersionFromList = getLatestVersionFromList;
exports.getSubdirOfUnzippedResource = getSubdirOfUnzippedResource;
exports.encodeOwnerStr = encodeOwnerStr;
exports.addOwnerToKey = addOwnerToKey;
exports.getActualResourcePath = getActualResourcePath;
exports.makeTwGroupDataResource = makeTwGroupDataResource;
exports.removeAllButLatestVersion = removeAllButLatestVersion;
exports.formatError = formatError;
exports.getErrorMessage = getErrorMessage;
exports.appendError = appendError;
var _fsExtra = require('fs-extra');
var _fsExtra2 = _interopRequireDefault(_fsExtra);
var _pathExtra = require('path-extra');
var _pathExtra2 = _interopRequireDefault(_pathExtra);
var _yamljs = require('yamljs');
var _yamljs2 = _interopRequireDefault(_yamljs);
var _util = require('util');
var _semver = require('semver');
var _semver2 = _interopRequireDefault(_semver);
var _zipFileHelpers = require('./zipFileHelpers');
var zipFileHelpers = _interopRequireWildcard(_zipFileHelpers);
var _twArticleHelpers = require('./translationHelps/twArticleHelpers');
var twArticleHelpers = _interopRequireWildcard(_twArticleHelpers);
var _tnArticleHelpers = require('./translationHelps/tnArticleHelpers');
var tnArticleHelpers = _interopRequireWildcard(_tnArticleHelpers);
var _taArticleHelpers = require('./translationHelps/taArticleHelpers');
var taArticleHelpers = _interopRequireWildcard(_taArticleHelpers);
var _twGroupDataHelpers = require('./translationHelps/twGroupDataHelpers');
var twGroupDataHelpers = _interopRequireWildcard(_twGroupDataHelpers);
var _packageParseHelpers = require('./packageParseHelpers');
var packageParseHelpers = _interopRequireWildcard(_packageParseHelpers);
var _errors = require('../resources/errors');
var errors = _interopRequireWildcard(_errors);
var _bible = require('../resources/bible');
var Bible = _interopRequireWildcard(_bible);
var _apiHelpers = require('./apiHelpers');
var _resourcesDownloadHelpers = require('./resourcesDownloadHelpers');
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
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"); }); }; } /* eslint-disable no-console,max-len,camelcase */
// helpers
// constants
const TRANSLATION_HELPS_INDEX = exports.TRANSLATION_HELPS_INDEX = {
ta: 'translationAcademy',
tn: 'translationNotes',
tw: 'translationWords',
twl: 'translationWordsLinks',
tq: 'translationQuestions'
};
/**
* @description - Gets the version from the manifest
* @param {String} resourcePath - folder for manifest.json or yaml
* @return {String} version
*/
function getVersionFromManifest(resourcePath) {
const manifest = getResourceManifest(resourcePath);
if (!manifest || !manifest.dublin_core || !manifest.dublin_core.version) {
return null;
}
return manifest.dublin_core.version;
}
/**
* @description Helper function to get manifest file from the resources folder. First
* it will try manifest.json, then manifest.yaml.
* @param {String} resourcePath - path to a resource folder which contains the manifest file in its root.
* @return {Object} manifest
*/
function getResourceManifest(resourcePath) {
const manifest = getResourceManifestFromJson(resourcePath);
if (!manifest) {
return getResourceManifestFromYaml(resourcePath);
}
return manifest;
}
/**
* @description - Turns a manifest.json file into an object and returns it, null if doesn't exist
* @param {String} resourcePath - folder for manifest.json
* @return {Object} manifest
*/
function getResourceManifestFromJson(resourcePath) {
const fileName = 'manifest.json';
const manifestPath = _pathExtra2.default.join(resourcePath, fileName);
let manifest = null;
if (_fsExtra2.default.existsSync(manifestPath)) {
manifest = _fsExtra2.default.readJsonSync(manifestPath);
}
return manifest;
}
/**
* @description - Turns a manifest.yaml file into an object and returns it, null if doesn't exist
* @param {String} resourcePath - folder for manifest.yaml
* @return {Object} manifest
*/
function getResourceManifestFromYaml(resourcePath) {
const fileName = 'manifest.yaml';
const manifestPath = _pathExtra2.default.join(resourcePath, fileName);
let manifest = null;
if (_fsExtra2.default.existsSync(manifestPath)) {
let yamlManifest = _fsExtra2.default.readFileSync(manifestPath, 'utf8').replace(/^\uFEFF/, '');
try {
manifest = _yamljs2.default.parse(yamlManifest);
} catch (e) {
console.error(`getResourceManifestFromYaml - manifest.yaml invalid ${manifestPath}`, e);
yamlManifest = yamlManifest.replace('---', '').trimStart();
try {
manifest = YAML.parse(yamlManifest);
// copy some data for more convenient access
console.log(`getResourceManifestFromYaml - manifest.yaml cleaned yaml worked`);
} catch (e) {
console.error(`getResourceManifestFromYaml - manifest.yaml invalid even without --- ${manifestPath}`, e);
manifest = null;
throw e;
}
}
}
return manifest;
}
/**
* Returns an array of versions found in the path that start with [vV]\d
* @param {String} resourcePath - base path to search for versions
* @param {string} ownerStr - optional owner to filter by
* @param {boolean} fallbackToDefaultOwner
* @return {Array} - array of versions, e.g. ['v1', 'v10', 'v1.1']
*/
function getVersionsInPath(resourcePath, ownerStr = _apiHelpers.DEFAULT_OWNER, fallbackToDefaultOwner = false) {
if (!resourcePath || !_fsExtra2.default.pathExistsSync(resourcePath)) {
return null;
}
ownerStr = encodeOwnerStr(ownerStr);
if (ownerStr[0] !== _apiHelpers.OWNER_SEPARATOR) {
// prefix the separator character if missing
ownerStr = _apiHelpers.OWNER_SEPARATOR + ownerStr;
}
const isVersionDirectory = name => {
const fullPath = _pathExtra2.default.join(resourcePath, name);
let isMatch = _fsExtra2.default.lstatSync(fullPath).isDirectory() && name.match(/^v\d/i);
if (isMatch && ownerStr) {
// if we need to filter by owner
isMatch = name.endsWith(ownerStr);
}
return isMatch;
};
let versions = sortVersions(_fsExtra2.default.readdirSync(resourcePath).filter(isVersionDirectory));
if ((!versions || !versions.length) && fallbackToDefaultOwner) {
versions = getVersionsInPath(resourcePath, _apiHelpers.DEFAULT_OWNER);
}
return versions;
}
/**
* reads files names from folder and remove system files
* @param {string} path
* @return {*[]}
*/
const cleanReaddirSync = exports.cleanReaddirSync = path => {
let cleanDirectories = [];
if (_fsExtra2.default.existsSync(path)) {
cleanDirectories = _fsExtra2.default.readdirSync(path).filter(file => file !== '.DS_Store');
}
return cleanDirectories;
};
/**
* takes path that has version and owner such as `~/resources/bible/v1.1_unfoldingWord` and extracts the version and owner.
* @param {string} versionPath
* @return {{owner: string, version: string}}
*/
function getVersionAndOwnerFromPath(versionPath) {
if (versionPath) {
const versionAndOwner = _pathExtra2.default.base(versionPath, true);
return splitVersionAndOwner(versionAndOwner);
}
return {};
}
/**
* takes string that has version and owner such as `v1.1_unfoldingWord` and splits into version and owner.
* @param {string} versionAndOwner
* @return {{owner: string, version: string}}
*/
function splitVersionAndOwner(versionAndOwner) {
let version = versionAndOwner;
let owner = '';
const pos = versionAndOwner && versionAndOwner.indexOf(_apiHelpers.OWNER_SEPARATOR);
if (pos >= 0) {
owner = decodeURIComponent(versionAndOwner.substr(pos + 1));
version = versionAndOwner.substr(0, pos);
}
return { version, owner };
}
/**
* Returns the versioned folder within the directory with the highest value.
* e.g. `v10` is greater than `v9`
* @param {string} dir - the directory to read
* @return {string} the full path to the latest version directory.
*/
function getLatestVersionsAndOwners(dir) {
const versionAndOwners = listVersions(dir, true);
const orgs = {};
for (const versionAndOwner of versionAndOwners) {
const { owner } = splitVersionAndOwner(versionAndOwner);
if (!orgs[owner]) {
orgs[owner] = [];
}
orgs[owner].push(versionAndOwner);
}
const orgsKeys = Object.keys(orgs);
for (const org of orgsKeys) {
const versions = orgs[org];
const latest = _pathExtra2.default.join(dir, versions[0]);
orgs[org] = latest;
}
if (orgsKeys.length > 0) {
return orgs;
} else {
return null;
}
}
/**
* sorts array by version number
* @param {Array} versionedDirs
* @param {boolean} inverted - if true then do reverse sort
* @return {*}
*/
function sortVersionsByNumber(versionedDirs, inverted = false) {
if (inverted) {
return versionedDirs.sort((a, b) => -compareVersions(a, b) // do inverted sort
);
}
return versionedDirs.sort((a, b) => compareVersions(a, b) // do regular sort
);
}
/**
* Returns an array of paths found in the directory filtered and sorted by version
* @param {string} dir
* @param {boolean} inverted - if true then do reverse sort
* @return {string[]}
*/
function listVersions(dir, inverted = false) {
if (_fsExtra2.default.pathExistsSync(dir)) {
const versionedDirs = _fsExtra2.default.readdirSync(dir).filter(file => _fsExtra2.default.lstatSync(_pathExtra2.default.join(dir, file)).isDirectory() && file.match(/^v\d/i));
return sortVersionsByNumber(versionedDirs, inverted);
}
return [];
}
/**
* compares version numbers, if a > b returns 1; if a < b return -1; else are equal and return 0
* @param {String} a
* @param {String} b
* @return {number}
*/
function compareVersions(a, b) {
const cleanA = _semver2.default.coerce(a);
const cleanB = _semver2.default.coerce(b);
if (_semver2.default.gt(cleanA, cleanB)) {
return 1;
} else if (_semver2.default.lt(cleanA, cleanB)) {
return -1;
} else {
return 0;
}
}
/**
* Returns a sorted an array of versions so that numeric parts are properly ordered (e.g. v10a < v100)
* @param {Array} versions - array of versions unsorted: ['v05.5.2', 'v5.5.1', 'V6.21.0', 'v4.22.0', 'v6.1.0', 'v6.1a.0', 'v5.1.0', 'V4.5.0']
* @return {Array} - array of versions sorted: ["V4.5.0", "v4.22.0", "v5.1.0", "v5.5.1", "v05.5.2", "v6.1.0", "v6.1a.0", "V6.21.0"]
*/
function sortVersions(versions) {
// Don't sort if null, empty or not an array
if (!versions || !Array.isArray(versions)) {
return versions;
}
// Only sort if all items are strings
for (let i = 0; i < versions.length; ++i) {
if (typeof versions[i] !== 'string') {
return versions;
}
}
versions = sortVersionsByNumber(versions); // sort by standard version order
return versions;
}
/**
* Return the full path to the highest version folder in resource path
* @param {String} resourcePath - base path to search for versions
* @param {string} ownerStr - optional owner to filter by
* @param {boolean} fallbackToDefaultOwner - if version not found for owner, then fall back to using default owner
* @return {String} - path to highest version
*/
function getLatestVersionInPath(resourcePath, ownerStr = _apiHelpers.DEFAULT_OWNER, fallbackToDefaultOwner = false) {
const versions = getVersionsInPath(resourcePath, ownerStr, fallbackToDefaultOwner);
if (versions && versions.length) {
// versions = sortVersions(versions);
return _pathExtra2.default.join(resourcePath, versions[versions.length - 1]);
}
return null; // return illegal path
}
/**
* returns the highest version for array that matches owner
* @param {Array} versions - base path to search for versions
* @param {string} ownerStr - optional owner to filter by, default to DOOR43_CATALOG
* @return {String} - highest version
*/
function getLatestVersionFromList(versions, ownerStr = _apiHelpers.DEFAULT_OWNER) {
if (Array.isArray(versions)) {
if (versions.length) {
ownerStr = encodeOwnerStr(ownerStr);
if (ownerStr[0] !== _apiHelpers.OWNER_SEPARATOR) {
// prefix the separator character if missing
ownerStr = _apiHelpers.OWNER_SEPARATOR + ownerStr;
}
versions = versions.filter(version => version.endsWith(ownerStr));
versions = sortVersions(versions); // sort by standard version order
return versions[versions.length - 1];
}
}
return null;
}
/**
* @description Unzips a resource's zip file to an imports directory for processing
* @param {Object} resource Resource object containing resourceId and languageId
* @param {String} zipFilePath Path to the zip file
* @param {string} resourcesPath Path to the resources directory
* @return {String} Path to the resource's import directory
*/
const unzipResource = exports.unzipResource = (() => {
var _ref = _asyncToGenerator(function* (resource, zipFilePath, resourcesPath) {
const importsPath = _pathExtra2.default.join(resourcesPath, 'imports');
_fsExtra2.default.ensureDirSync(importsPath);
const importPath = zipFilePath.split('.').slice(0, -1).join('.');
yield zipFileHelpers.extractZipFile(zipFilePath, importPath);
return importPath;
});
return function unzipResource(_x, _x2, _x3) {
return _ref.apply(this, arguments);
};
})();
/**
* Gets the single subdirectory of an extracted zip file path
* @param {String} extractedFilesPath Extracted files path
* @return {String} The subdir in the extracted path
*/
function getSubdirOfUnzippedResource(extractedFilesPath) {
const subdirs = _fsExtra2.default.readdirSync(extractedFilesPath);
if (subdirs.length === 1 && _fsExtra2.default.lstatSync(_pathExtra2.default.join(extractedFilesPath, subdirs[0])).isDirectory()) {
return _pathExtra2.default.join(extractedFilesPath, subdirs[0]);
}
return extractedFilesPath;
}function encodeOwnerStr(owner) {
const ownerStr = encodeURIComponent(owner || '');
return ownerStr;
}
/**
* combine owner with key
* @param {string} key
* @param {string} owner
* @return {string|*}
*/
function addOwnerToKey(key, owner) {
const ownerStr = encodeOwnerStr(owner);
const versionDir = ownerStr ? `${key}${_apiHelpers.OWNER_SEPARATOR}${ownerStr}` : key;
return versionDir;
}
/**
* @description Gets the actual path to a resource based on the resource object
* @param {Object} resource The resource object
* @param {String} resourcesPath The path to the resources directory
* @return {String} The resource path
*/
function getActualResourcePath(resource, resourcesPath) {
const languageId = resource.languageId;
let resourceName = resource.resourceId;
let type = 'bibles';
if (TRANSLATION_HELPS_INDEX[resourceName]) {
resourceName = TRANSLATION_HELPS_INDEX[resourceName];
type = 'translationHelps';
}
const version = 'v' + resource.version;
const versionDir = addOwnerToKey(version, resource.owner);
const actualResourcePath = _pathExtra2.default.join(resourcesPath, languageId, type, resourceName, versionDir);
_fsExtra2.default.ensureDirSync(actualResourcePath);
return actualResourcePath;
}
/**
* @description Downloads the resources that need to be updated for a given language using the DCS API
* @param {Object.<{
* languageId: String,
* resourceId: String,
* localModifiedTime: String,
* remoteModifiedTime: String,
* downloadUrl: String,
* version: String,
* subject: String,
* catalogEntry: {langResource, bookResource, format}
* }>} resource - resource to download
* @param {String} sourcePath Path to the Bible directory
* @return {String} Path to the processed tw Group Data files
*/
function makeTwGroupDataResource(resource, sourcePath) {
if (!resource) {
throw Error(formatError(resource, errors.RESOURCE_NOT_GIVEN));
}
if (!_fsExtra2.default.pathExistsSync(sourcePath)) {
throw Error(formatError(resource, errors.SOURCE_PATH_NOT_EXIST));
}
if (resource.languageId === Bible.NT_ORIG_LANG && resource.resourceId === Bible.NT_ORIG_LANG_BIBLE || resource.languageId === Bible.OT_ORIG_LANG && resource.resourceId === Bible.OT_ORIG_LANG_BIBLE) {
const twGroupDataPath = _pathExtra2.default.join(sourcePath + '_tw_group_data_' + resource.languageId + '_v' + resource.version);
const result = twGroupDataHelpers.generateTwGroupDataFromAlignedBible(resource, sourcePath, twGroupDataPath);
if (result) {
return twGroupDataPath;
}
}
}
/**
* Removes all version directories except the latest
* @param {String} resourcePath Path to the resource directory that has subdirs of versions
* @param {array} versionsToNotDelete List of versions not to be deleted.
* @param {string} ownerStr - optional owner to filter by
* @return {Boolean} True if versions were deleted, false if nothing was touched
*/
function removeAllButLatestVersion(resourcePath, versionsToNotDelete = [], ownerStr = '') {
// Remove the previous version(s)
const versionDirs = getVersionsInPath(resourcePath, ownerStr);
if (versionDirs && versionDirs.length > 1) {
const lastVersion = versionDirs[versionDirs.length - 1];
versionDirs.forEach(versionDir => {
if (versionDir !== lastVersion && !versionsToNotDelete.includes(versionDir)) {
const versionPath = _pathExtra2.default.join(resourcePath, versionDir);
console.log(`removeAllButLatestVersion() - removing old resource: ${versionPath}`);
_fsExtra2.default.removeSync(versionPath);
}
});
return true;
}
return false;
}
/**
* @description Formats an error for all resources to have a standard format
* @param {Object} resource Resource object
* @param {String} errMessage Error message
* @return {String} The formatted error message
*/
function formatError(resource, errMessage) {
if (!resource || !(0, _util.isObject)(resource) || !resource.languageId || !resource.resourceId) {
resource = {
languageId: 'unknown',
resourceId: 'unknown'
};
}
return resource.owner + '/' + resource.languageId + '_' + resource.resourceId + ': ' + errMessage;
}
/**
* converts error to string
* @param {Error|String} error - error to append
* @return {string} concatenated message
*/
function getErrorMessage(error) {
return error && error.message || error || 'UNDEFINED';
}
/**
* appends error message to string
* @param {string} str - string to use as prefix
* @param {Error|String} err - error to append
* @return {string} concatenated message
*/
function appendError(str, err) {
return str + ': ' + getErrorMessage(err);
}
/**
* Determines if the rootpath plus a filename is a directory.
* @param {string} rootPath
* @param {string} filename
* @return {bool} - Whether the path is a directory or not.
*/
const isDirectory = exports.isDirectory = (rootPath, filename) => {
const fullPath = _pathExtra2.default.join(rootPath, filename);
return _fsExtra2.default.lstatSync(fullPath).isDirectory();
};