aboutsummaryrefslogtreecommitdiff
path: root/cordova/node_modules/cordova-common/src/FileUpdater.js
diff options
context:
space:
mode:
Diffstat (limited to 'cordova/node_modules/cordova-common/src/FileUpdater.js')
-rwxr-xr-xcordova/node_modules/cordova-common/src/FileUpdater.js415
1 files changed, 415 insertions, 0 deletions
diff --git a/cordova/node_modules/cordova-common/src/FileUpdater.js b/cordova/node_modules/cordova-common/src/FileUpdater.js
new file mode 100755
index 0000000..ea5d9e2
--- /dev/null
+++ b/cordova/node_modules/cordova-common/src/FileUpdater.js
@@ -0,0 +1,415 @@
+/**
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+*/
+
+'use strict';
+
+var fs = require('fs');
+var path = require('path');
+var shell = require('shelljs');
+var minimatch = require('minimatch');
+
+/**
+ * Logging callback used in the FileUpdater methods.
+ * @callback loggingCallback
+ * @param {string} message A message describing a single file update operation.
+ */
+
+/**
+ * Updates a target file or directory with a source file or directory. (Directory updates are
+ * not recursive.) Stats for target and source items must be passed in. This is an internal
+ * helper function used by other methods in this module.
+ *
+ * @param {?string} sourcePath Source file or directory to be used to update the
+ * destination. If the source is null, then the destination is deleted if it exists.
+ * @param {?fs.Stats} sourceStats An instance of fs.Stats for the source path, or null if
+ * the source does not exist.
+ * @param {string} targetPath Required destination file or directory to be updated. If it does
+ * not exist, it will be created.
+ * @param {?fs.Stats} targetStats An instance of fs.Stats for the target path, or null if
+ * the target does not exist.
+ * @param {Object} [options] Optional additional parameters for the update.
+ * @param {string} [options.rootDir] Optional root directory (such as a project) to which target
+ * and source path parameters are relative; may be omitted if the paths are absolute. The
+ * rootDir is always omitted from any logged paths, to make the logs easier to read.
+ * @param {boolean} [options.all] If true, all files are copied regardless of last-modified times.
+ * Otherwise, a file is copied if the source's last-modified time is greather than or
+ * equal to the target's last-modified time, or if the file sizes are different.
+ * @param {loggingCallback} [log] Optional logging callback that takes a string message
+ * describing any file operations that are performed.
+ * @return {boolean} true if any changes were made, or false if the force flag is not set
+ * and everything was up to date
+ */
+function updatePathWithStats (sourcePath, sourceStats, targetPath, targetStats, options, log) {
+ var updated = false;
+
+ var rootDir = (options && options.rootDir) || '';
+ var copyAll = (options && options.all) || false;
+
+ var targetFullPath = path.join(rootDir || '', targetPath);
+
+ if (sourceStats) {
+ var sourceFullPath = path.join(rootDir || '', sourcePath);
+
+ if (targetStats) {
+ // The target exists. But if the directory status doesn't match the source, delete it.
+ if (targetStats.isDirectory() && !sourceStats.isDirectory()) {
+ log('rmdir ' + targetPath + ' (source is a file)');
+ shell.rm('-rf', targetFullPath);
+ targetStats = null;
+ updated = true;
+ } else if (!targetStats.isDirectory() && sourceStats.isDirectory()) {
+ log('delete ' + targetPath + ' (source is a directory)');
+ shell.rm('-f', targetFullPath);
+ targetStats = null;
+ updated = true;
+ }
+ }
+
+ if (!targetStats) {
+ if (sourceStats.isDirectory()) {
+ // The target directory does not exist, so it should be created.
+ log('mkdir ' + targetPath);
+ shell.mkdir('-p', targetFullPath);
+ updated = true;
+ } else if (sourceStats.isFile()) {
+ // The target file does not exist, so it should be copied from the source.
+ log('copy ' + sourcePath + ' ' + targetPath + (copyAll ? '' : ' (new file)'));
+ shell.cp('-f', sourceFullPath, targetFullPath);
+ updated = true;
+ }
+ } else if (sourceStats.isFile() && targetStats.isFile()) {
+ // The source and target paths both exist and are files.
+ if (copyAll) {
+ // The caller specified all files should be copied.
+ log('copy ' + sourcePath + ' ' + targetPath);
+ shell.cp('-f', sourceFullPath, targetFullPath);
+ updated = true;
+ } else {
+ // Copy if the source has been modified since it was copied to the target, or if
+ // the file sizes are different. (The latter catches most cases in which something
+ // was done to the file after copying.) Comparison is >= rather than > to allow
+ // for timestamps lacking sub-second precision in some filesystems.
+ if (sourceStats.mtime.getTime() >= targetStats.mtime.getTime() ||
+ sourceStats.size !== targetStats.size) {
+ log('copy ' + sourcePath + ' ' + targetPath + ' (updated file)');
+ shell.cp('-f', sourceFullPath, targetFullPath);
+ updated = true;
+ }
+ }
+ }
+ } else if (targetStats) {
+ // The target exists but the source is null, so the target should be deleted.
+ if (targetStats.isDirectory()) {
+ log('rmdir ' + targetPath + (copyAll ? '' : ' (no source)'));
+ shell.rm('-rf', targetFullPath);
+ } else {
+ log('delete ' + targetPath + (copyAll ? '' : ' (no source)'));
+ shell.rm('-f', targetFullPath);
+ }
+ updated = true;
+ }
+
+ return updated;
+}
+
+/**
+ * Helper for updatePath and updatePaths functions. Queries stats for source and target
+ * and ensures target directory exists before copying a file.
+ */
+function updatePathInternal (sourcePath, targetPath, options, log) {
+ var rootDir = (options && options.rootDir) || '';
+ var targetFullPath = path.join(rootDir, targetPath);
+ var targetStats = fs.existsSync(targetFullPath) ? fs.statSync(targetFullPath) : null;
+ var sourceStats = null;
+
+ if (sourcePath) {
+ // A non-null source path was specified. It should exist.
+ var sourceFullPath = path.join(rootDir, sourcePath);
+ if (!fs.existsSync(sourceFullPath)) {
+ throw new Error('Source path does not exist: ' + sourcePath);
+ }
+
+ sourceStats = fs.statSync(sourceFullPath);
+
+ // Create the target's parent directory if it doesn't exist.
+ var parentDir = path.dirname(targetFullPath);
+ if (!fs.existsSync(parentDir)) {
+ shell.mkdir('-p', parentDir);
+ }
+ }
+
+ return updatePathWithStats(sourcePath, sourceStats, targetPath, targetStats, options, log);
+}
+
+/**
+ * Updates a target file or directory with a source file or directory. (Directory updates are
+ * not recursive.)
+ *
+ * @param {?string} sourcePath Source file or directory to be used to update the
+ * destination. If the source is null, then the destination is deleted if it exists.
+ * @param {string} targetPath Required destination file or directory to be updated. If it does
+ * not exist, it will be created.
+ * @param {Object} [options] Optional additional parameters for the update.
+ * @param {string} [options.rootDir] Optional root directory (such as a project) to which target
+ * and source path parameters are relative; may be omitted if the paths are absolute. The
+ * rootDir is always omitted from any logged paths, to make the logs easier to read.
+ * @param {boolean} [options.all] If true, all files are copied regardless of last-modified times.
+ * Otherwise, a file is copied if the source's last-modified time is greather than or
+ * equal to the target's last-modified time, or if the file sizes are different.
+ * @param {loggingCallback} [log] Optional logging callback that takes a string message
+ * describing any file operations that are performed.
+ * @return {boolean} true if any changes were made, or false if the force flag is not set
+ * and everything was up to date
+ */
+function updatePath (sourcePath, targetPath, options, log) {
+ if (sourcePath !== null && typeof sourcePath !== 'string') {
+ throw new Error('A source path (or null) is required.');
+ }
+
+ if (!targetPath || typeof targetPath !== 'string') {
+ throw new Error('A target path is required.');
+ }
+
+ log = log || function () { };
+
+ return updatePathInternal(sourcePath, targetPath, options, log);
+}
+
+/**
+ * Updates files and directories based on a mapping from target paths to source paths. Targets
+ * with null sources in the map are deleted.
+ *
+ * @param {Object} pathMap A dictionary mapping from target paths to source paths.
+ * @param {Object} [options] Optional additional parameters for the update.
+ * @param {string} [options.rootDir] Optional root directory (such as a project) to which target
+ * and source path parameters are relative; may be omitted if the paths are absolute. The
+ * rootDir is always omitted from any logged paths, to make the logs easier to read.
+ * @param {boolean} [options.all] If true, all files are copied regardless of last-modified times.
+ * Otherwise, a file is copied if the source's last-modified time is greather than or
+ * equal to the target's last-modified time, or if the file sizes are different.
+ * @param {loggingCallback} [log] Optional logging callback that takes a string message
+ * describing any file operations that are performed.
+ * @return {boolean} true if any changes were made, or false if the force flag is not set
+ * and everything was up to date
+ */
+function updatePaths (pathMap, options, log) {
+ if (!pathMap || typeof pathMap !== 'object' || Array.isArray(pathMap)) {
+ throw new Error('An object mapping from target paths to source paths is required.');
+ }
+
+ log = log || function () { };
+
+ var updated = false;
+
+ // Iterate in sorted order to ensure directories are created before files under them.
+ Object.keys(pathMap).sort().forEach(function (targetPath) {
+ var sourcePath = pathMap[targetPath];
+ updated = updatePathInternal(sourcePath, targetPath, options, log) || updated;
+ });
+
+ return updated;
+}
+
+/**
+ * Updates a target directory with merged files and subdirectories from source directories.
+ *
+ * @param {string|string[]} sourceDirs Required source directory or array of source directories
+ * to be merged into the target. The directories are listed in order of precedence; files in
+ * directories later in the array supersede files in directories earlier in the array
+ * (regardless of timestamps).
+ * @param {string} targetDir Required destination directory to be updated. If it does not exist,
+ * it will be created. If it exists, newer files from source directories will be copied over,
+ * and files missing in the source directories will be deleted.
+ * @param {Object} [options] Optional additional parameters for the update.
+ * @param {string} [options.rootDir] Optional root directory (such as a project) to which target
+ * and source path parameters are relative; may be omitted if the paths are absolute. The
+ * rootDir is always omitted from any logged paths, to make the logs easier to read.
+ * @param {boolean} [options.all] If true, all files are copied regardless of last-modified times.
+ * Otherwise, a file is copied if the source's last-modified time is greather than or
+ * equal to the target's last-modified time, or if the file sizes are different.
+ * @param {string|string[]} [options.include] Optional glob string or array of glob strings that
+ * are tested against both target and source relative paths to determine if they are included
+ * in the merge-and-update. If unspecified, all items are included.
+ * @param {string|string[]} [options.exclude] Optional glob string or array of glob strings that
+ * are tested against both target and source relative paths to determine if they are excluded
+ * from the merge-and-update. Exclusions override inclusions. If unspecified, no items are
+ * excluded.
+ * @param {loggingCallback} [log] Optional logging callback that takes a string message
+ * describing any file operations that are performed.
+ * @return {boolean} true if any changes were made, or false if the force flag is not set
+ * and everything was up to date
+ */
+function mergeAndUpdateDir (sourceDirs, targetDir, options, log) {
+ if (sourceDirs && typeof sourceDirs === 'string') {
+ sourceDirs = [ sourceDirs ];
+ } else if (!Array.isArray(sourceDirs)) {
+ throw new Error('A source directory path or array of paths is required.');
+ }
+
+ if (!targetDir || typeof targetDir !== 'string') {
+ throw new Error('A target directory path is required.');
+ }
+
+ log = log || function () { };
+
+ var rootDir = (options && options.rootDir) || '';
+
+ var include = (options && options.include) || [ '**' ];
+ if (typeof include === 'string') {
+ include = [ include ];
+ } else if (!Array.isArray(include)) {
+ throw new Error('Include parameter must be a glob string or array of glob strings.');
+ }
+
+ var exclude = (options && options.exclude) || [];
+ if (typeof exclude === 'string') {
+ exclude = [ exclude ];
+ } else if (!Array.isArray(exclude)) {
+ throw new Error('Exclude parameter must be a glob string or array of glob strings.');
+ }
+
+ // Scan the files in each of the source directories.
+ var sourceMaps = sourceDirs.map(function (sourceDir) {
+ return path.join(rootDir, sourceDir);
+ }).map(function (sourcePath) {
+ if (!fs.existsSync(sourcePath)) {
+ throw new Error('Source directory does not exist: ' + sourcePath);
+ }
+ return mapDirectory(rootDir, path.relative(rootDir, sourcePath), include, exclude);
+ });
+
+ // Scan the files in the target directory, if it exists.
+ var targetMap = {};
+ var targetFullPath = path.join(rootDir, targetDir);
+ if (fs.existsSync(targetFullPath)) {
+ targetMap = mapDirectory(rootDir, targetDir, include, exclude);
+ }
+
+ var pathMap = mergePathMaps(sourceMaps, targetMap, targetDir);
+
+ var updated = false;
+
+ // Iterate in sorted order to ensure directories are created before files under them.
+ Object.keys(pathMap).sort().forEach(function (subPath) {
+ var entry = pathMap[subPath];
+ updated = updatePathWithStats(
+ entry.sourcePath,
+ entry.sourceStats,
+ entry.targetPath,
+ entry.targetStats,
+ options,
+ log) || updated;
+ });
+
+ return updated;
+}
+
+/**
+ * Creates a dictionary map of all files and directories under a path.
+ */
+function mapDirectory (rootDir, subDir, include, exclude) {
+ var dirMap = { '': { subDir: subDir, stats: fs.statSync(path.join(rootDir, subDir)) } };
+ mapSubdirectory(rootDir, subDir, '', include, exclude, dirMap);
+ return dirMap;
+
+ function mapSubdirectory (rootDir, subDir, relativeDir, include, exclude, dirMap) {
+ var itemMapped = false;
+ var items = fs.readdirSync(path.join(rootDir, subDir, relativeDir));
+
+ items.forEach(function (item) {
+ var relativePath = path.join(relativeDir, item);
+ if (!matchGlobArray(relativePath, exclude)) {
+ // Stats obtained here (required at least to know where to recurse in directories)
+ // are saved for later, where the modified times may also be used. This minimizes
+ // the number of file I/O operations performed.
+ var fullPath = path.join(rootDir, subDir, relativePath);
+ var stats = fs.statSync(fullPath);
+
+ if (stats.isDirectory()) {
+ // Directories are included if either something under them is included or they
+ // match an include glob.
+ if (mapSubdirectory(rootDir, subDir, relativePath, include, exclude, dirMap) ||
+ matchGlobArray(relativePath, include)) {
+ dirMap[relativePath] = { subDir: subDir, stats: stats };
+ itemMapped = true;
+ }
+ } else if (stats.isFile()) {
+ // Files are included only if they match an include glob.
+ if (matchGlobArray(relativePath, include)) {
+ dirMap[relativePath] = { subDir: subDir, stats: stats };
+ itemMapped = true;
+ }
+ }
+ }
+ });
+ return itemMapped;
+ }
+
+ function matchGlobArray (path, globs) {
+ return globs.some(function (elem) {
+ return minimatch(path, elem, {dot: true});
+ });
+ }
+}
+
+/**
+ * Merges together multiple source maps and a target map into a single mapping from
+ * relative paths to objects with target and source paths and stats.
+ */
+function mergePathMaps (sourceMaps, targetMap, targetDir) {
+ // Merge multiple source maps together, along with target path info.
+ // Entries in later source maps override those in earlier source maps.
+ // Target stats will be filled in below for targets that exist.
+ var pathMap = {};
+ sourceMaps.forEach(function (sourceMap) {
+ Object.keys(sourceMap).forEach(function (sourceSubPath) {
+ var sourceEntry = sourceMap[sourceSubPath];
+ pathMap[sourceSubPath] = {
+ targetPath: path.join(targetDir, sourceSubPath),
+ targetStats: null,
+ sourcePath: path.join(sourceEntry.subDir, sourceSubPath),
+ sourceStats: sourceEntry.stats
+ };
+ });
+ });
+
+ // Fill in target stats for targets that exist, and create entries
+ // for targets that don't have any corresponding sources.
+ Object.keys(targetMap).forEach(function (subPath) {
+ var entry = pathMap[subPath];
+ if (entry) {
+ entry.targetStats = targetMap[subPath].stats;
+ } else {
+ pathMap[subPath] = {
+ targetPath: path.join(targetDir, subPath),
+ targetStats: targetMap[subPath].stats,
+ sourcePath: null,
+ sourceStats: null
+ };
+ }
+ });
+
+ return pathMap;
+}
+
+module.exports = {
+ updatePath: updatePath,
+ updatePaths: updatePaths,
+ mergeAndUpdateDir: mergeAndUpdateDir
+};