/** * Load extensions. * * @param {?Array.<string>} A list containing references to extension source * location. A source location may be either (a) a folder name inside * src/extensions or (b) an absolute path. * @return {!$.Promise} A promise object that is resolved when all extensions complete loading. */ function init(paths) { var params = new UrlParams(); if (_init) { // Only init once. Return a resolved promise. return new $.Deferred().resolve().promise(); } if (!paths) { params.parse(); if (params.get("reloadWithoutUserExts") === "true") { paths = ["default"]; } else { paths = [ getDefaultExtensionPath(), "dev", getUserExtensionPath() ]; } } // Load extensions before restoring the project // Get a Directory for the user extension directory and create it if it doesn't exist. // Note that this is an async call and there are no success or failure functions passed // in. If the directory *doesn't* exist, it will be created. Extension loading may happen // before the directory is finished being created, but that is okay, since the extension // loading will work correctly without this directory. // If the directory *does* exist, nothing else needs to be done. It will be scanned normally // during extension loading. var extensionPath = getUserExtensionPath(); FileSystem.getDirectoryForPath(extensionPath).create(); // Create the extensions/disabled directory, too. var disabledExtensionPath = extensionPath.replace(/\/user$/, "/disabled"); FileSystem.getDirectoryForPath(disabledExtensionPath).create(); var promise = Async.doSequentially(paths, function (item) { var extensionPath = item; // If the item has "/" in it, assume it is a full path. Otherwise, load // from our source path + "/extensions/". if (item.indexOf("/") === -1) { extensionPath = FileUtils.getNativeBracketsDirectoryPath() + "/extensions/" + item; } return loadAllExtensionsInNativeDirectory(extensionPath); }, false); promise.always(function () { _init = true; }); return promise; }
FileSystem.resolve(path, function (err) { if (!err) { // Item already exists, fail with error d.reject(FileSystemError.ALREADY_EXISTS); } else { if (isFolder) { var directory = FileSystem.getDirectoryForPath(path); directory.create(function (err) { if (err) { d.reject(err); } else { d.resolve(directory); } }); } else { // Create an empty file var file = FileSystem.getFileForPath(path); FileUtils.writeText(file, "").then(function () { d.resolve(file); }, d.reject); } } });
/** * @private * * Gets the FileSystem object (either a File or Directory) based on the path provided. * * @param {string} path Path to retrieve */ function _getFSObject(path) { if (!path) { return path; } else if (_pathIsFile(path)) { return FileSystem.getFileForPath(path); } return FileSystem.getDirectoryForPath(path); }
runs(function () { var dir = FileSystem.getDirectoryForPath(getTempDirectory()).create(function (err) { if (err && err !== FileSystemError.ALREADY_EXISTS) { deferred.reject(err); } else { deferred.resolve(); } }); });
ProjectModel.prototype._getDirectoryContents = function (path) { var d = new $.Deferred(); FileSystem.getDirectoryForPath(path).getContents(function (err, contents) { if (err) { d.reject(err); } else { d.resolve(contents); } }); return d.promise(); };
/** * Rename a file/folder. This will update the project tree data structures * and send notifications about the rename. * * @param {string} oldName Old item name * @param {string} newName New item name * @param {boolean} isFolder True if item is a folder; False if it is a file. * @return {$.Promise} A promise object that will be resolved or rejected when * the rename is finished. */ function _renameItem(oldName, newName, isFolder) { var result = new $.Deferred(); if (oldName === newName) { result.resolve(); } else if (!isValidFilename(FileUtils.getBaseName(newName), _invalidChars)) { result.reject(ERROR_INVALID_FILENAME); } else { var entry = isFolder ? FileSystem.getDirectoryForPath(oldName) : FileSystem.getFileForPath(oldName); entry.rename(newName, function (err) { if (err) { result.reject(err); } else { result.resolve(); } }); } return result.promise(); }
/** * @private * Loads a file entryPoint from each extension folder within the baseUrl into its own Require.js context * * @param {!string} directory, an absolute native path that contains a directory of extensions. * each subdirectory is interpreted as an independent extension * @param {!{baseUrl: string}} config object with baseUrl property containing absolute path of extension folder * @param {!string} entryPoint Module name to load (without .js suffix) * @param {function} processExtension * @return {!$.Promise} A promise object that is resolved when all extensions complete loading. */ function _loadAll(directory, config, entryPoint, processExtension) { var result = new $.Deferred(); FileSystem.getDirectoryForPath(directory).getContents(function (err, contents) { if (!err) { var i, extensions = []; for (i = 0; i < contents.length; i++) { if (contents[i].isDirectory) { // FUTURE (JRB): read package.json instead of just using the entrypoint "main". // Also, load sub-extensions defined in package.json. extensions.push(contents[i].name); } } if (extensions.length === 0) { result.resolve(); return; } Async.doInParallel(extensions, function (item) { var extConfig = { baseUrl: config.baseUrl + "/" + item, paths: config.paths }; return processExtension(item, extConfig, entryPoint); }).always(function () { // Always resolve the promise even if some extensions had errors result.resolve(); }); } else { console.error("[Extension] Error -- could not read native directory: " + directory); result.reject(); } }); return result.promise(); }
NativeFileSystem.DirectoryEntry = function (fullPath) { _warn("new NativeFileSystem.DirectoryEntry()", "FileSystem.getDirectoryForPath()"); return FileSystem.getDirectoryForPath(fullPath); };
/** * @private * Find valid extensions in specified path * @param {string} dirPath Directory with extensions * @param {Object} autoExtensions Object that maps names of previously auto-installed * extensions {string} to installed version {string}. * @return {$.Promise} Promise that resolves with arrays for extensions to update and install */ function _getAutoInstallFiles(dirPath, autoExtensions) { var zipFiles = [], installZips = [], updateZips = [], deferred = new $.Deferred(); FileSystem.getDirectoryForPath(dirPath).getContents(function (err, contents) { if (!err) { zipFiles = contents.filter(function (dirItem) { return (dirItem.isFile && FileUtils.getFileExtension(dirItem.fullPath) === "zip"); }); } // Parse zip files and separate new installs vs. updates Async.doInParallel_aggregateErrors(zipFiles, function (file) { var zipFilePromise = new $.Deferred(); // Call validate() so that we open the local zip file and parse the // package.json. We need the name to detect if this zip will be a // new install or an update. Package.validate(file.fullPath, { requirePackageJSON: true }).done(function (info) { if (info.errors.length) { zipFilePromise.reject(Package.formatError(info.errors)); return; } var extensionInfo, installedVersion, zipArray, existingItem, extensionName = info.metadata.name, autoExtVersion = autoExtensions[extensionName]; // Verify extension has not already been auto-installed/updated if (autoExtVersion && semver.lte(info.metadata.version, autoExtVersion)) { // Have already auto installed/updated version >= version of this extension zipFilePromise.reject(); return; } // Verify extension has not already been installed/updated by some other means extensionInfo = extensions[extensionName]; installedVersion = extensionInfo && extensionInfo.installInfo && extensionInfo.installInfo.metadata.version; if (installedVersion && semver.lte(info.metadata.version, installedVersion)) { // Have already manually installed/updated version >= version of this extension zipFilePromise.reject(); return; } // Update appropriate zip array. There could be multiple zip files for an // extension, so make sure only the latest is stored zipArray = (installedVersion) ? updateZips : installZips; zipArray.some(function (zip) { if (zip.info.metadata.name === extensionName) { existingItem = zip; return true; } return false; }); if (existingItem) { if (semver.lt(existingItem.info.metadata.version, info.metadata.version)) { existingItem.file = file; existingItem.info = info; } } else { zipArray.push({ file: file, info: info }); } zipFilePromise.resolve(); }).fail(function (err) { zipFilePromise.reject(Package.formatError(err)); }); return zipFilePromise.promise(); }).fail(function (errorArray) { // Async.doInParallel() fails if some are successful, so write errors // to console and always resolve errorArray.forEach(function (errorObj) { // If we rejected without an error argument, it means it was no problem // (e.g. same version of extension is already installed) if (errorObj.error) { if (errorObj.error.forEach) { console.error("Errors for", errorObj.item); errorObj.error.forEach(function (error) { console.error(Package.formatError(error)); }); } else { console.error("Error for", errorObj.item, errorObj); } } }); }).always(function () { deferred.resolve({ installZips: installZips, updateZips: updateZips }); }); }); return deferred.promise(); }
/** * Load extensions. * * @param {?Array.<string>} A list containing references to extension source * location. A source location may be either (a) a folder name inside * src/extensions or (b) an absolute path. * @return {!$.Promise} A promise object that is resolved when all extensions complete loading. */ function init(paths) { var params = new UrlParams(); if (_init) { // Only init once. Return a resolved promise. return new $.Deferred().resolve().promise(); } // Load *subset* of the usual builtin extensions list, and don't try to find any user/dev extensions if (brackets.inBrowser) { var basePath = PathUtils.directory(window.location.href) + "extensions/default/", defaultExtensions = [ // Core extensions we want to support in the browser "CSSCodeHints", "HTMLCodeHints", "HtmlEntityCodeHints", "InlineColorEditor", "JavaScriptQuickEdit", "JSLint", "LESSSupport", "QuickOpenCSS", "QuickOpenHTML", "QuickOpenJavaScript", "QuickView", "RecentProjects", "UrlCodeHints", "WebPlatformDocs", // Custom extensions we want loaded by default // NOTE: Maps to a folder inside /src/extensions/default/ "makedrive-sync-icon" // "ExampleExtension", ]; return Async.doInParallel(defaultExtensions, function (item) { var extConfig = { baseUrl: basePath + item }; return loadExtension(item, extConfig, "main"); }); } if (!paths) { params.parse(); if (params.get("reloadWithoutUserExts") === "true") { paths = ["default"]; } else { paths = ["default", "dev", getUserExtensionPath()]; } } // Load extensions before restoring the project // Get a Directory for the user extension directory and create it if it doesn't exist. // Note that this is an async call and there are no success or failure functions passed // in. If the directory *doesn't* exist, it will be created. Extension loading may happen // before the directory is finished being created, but that is okay, since the extension // loading will work correctly without this directory. // If the directory *does* exist, nothing else needs to be done. It will be scanned normally // during extension loading. var extensionPath = getUserExtensionPath(); FileSystem.getDirectoryForPath(extensionPath).create(); // Create the extensions/disabled directory, too. var disabledExtensionPath = extensionPath.replace(/\/user$/, "/disabled"); FileSystem.getDirectoryForPath(disabledExtensionPath).create(); var promise = Async.doSequentially(paths, function (item) { var extensionPath = item; // If the item has "/" in it, assume it is a full path. Otherwise, load // from our source path + "/extensions/". if (item.indexOf("/") === -1) { extensionPath = FileUtils.getNativeBracketsDirectoryPath() + "/extensions/" + item; } return loadAllExtensionsInNativeDirectory(extensionPath); }, false); promise.always(function () { _init = true; }); return promise; }
function _autoInstallBundles() { // Get list of extension bundles var validatePromise, dirPath = FileUtils.getDirectoryPath(FileUtils.getNativeBracketsDirectoryPath()) + FOLDER_AUTOINSTALL + "/", bundles = [], installZips = [], updateZips = [], deferred = new $.Deferred(); FileSystem.getDirectoryForPath(dirPath).getContents(function (err, contents) { var autoExtensions = PreferencesManager.getViewState(FOLDER_AUTOINSTALL) || {}, autoExtDirty = false; if (!err) { bundles = contents.filter(function (dirItem) { return (dirItem.isFile && FileUtils.getFileExtension(dirItem.fullPath) === "zip"); }); } // Parse zip files and separate new installs vs. updates validatePromise = Async.doInParallel(bundles, function (file) { var result = new $.Deferred(); // Call validate() so that we open the local zip file and parse the // package.json. We need the name to detect if this zip will be a // new install or an update. Package.validate(file.fullPath, { requirePackageJSON: true }).done(function (info) { if (info.errors.length) { result.reject(Package.formatError(info.errors)); return; } var extensionInfo, installedVersion, extensionName = info.metadata.name, autoExtVersion = autoExtensions && autoExtensions[extensionName]; // Verify extension has not already been auto-installed/updated if (autoExtVersion && semver.lte(info.metadata.version, autoExtVersion)) { // Have already installed/updated version >= version of this extension result.reject(); return; } // Verify extension has not already been installed/updated by some other means extensionInfo = extensions[extensionName]; installedVersion = extensionInfo && extensionInfo.installInfo && extensionInfo.installInfo.metadata.version; if (installedVersion && semver.lte(info.metadata.version, installedVersion)) { // Have already installed/updated version >= version of this extension result.reject(); return; } // Keep track of auto-installed extensions so we only install an extension once autoExtensions[extensionName] = info.metadata.version; autoExtDirty = true; if (installedVersion) { updateZips.push(file); } else { installZips.push(file); } result.resolve(); }).fail(function (err) { result.reject(Package.formatError(err)); }); return result.promise(); }); validatePromise.done(function () { var installPromise = Async.doSequentially(installZips, function (file) { return Package.installFromPath(file.fullPath); }); var updatePromise = installPromise.always(function () { return Async.doSequentially(updateZips, function (file) { return Package.installUpdate(file.fullPath); }); }); // Always resolve the outer promise updatePromise.always(deferred.resolve); }).fail(function (errorArray) { deferred.reject(errorArray); }).always(function () { if (autoExtDirty) { // Store info in prefs PreferencesManager.setViewState(FOLDER_AUTOINSTALL, autoExtensions); } }); }); return deferred.promise(); }
/** * Copy a directory source to a destination * @param {!Directory} source Directory for the source directory to copy * @param {!string} destination Destination path to copy the source directory * @param {?{parseOffsets:boolean, infos:Object, removePrefix:boolean}}} options * parseOffsets - allows optional offset markup parsing. File is written to the * destination path without offsets. Offset data is passed to the * doneCallbacks of the promise. * infos - an optional Object used when parseOffsets is true. Offset * information is attached here, indexed by the file destination path. * removePrefix - When parseOffsets is true, set removePrefix true * to add a new key to the infos array that drops the destination * path root. * @return {$.Promise} A promise resolved when the directory and all it's * contents are copied to the destination or rejected immediately * upon the first error. */ function copyDirectoryEntry(source, destination, options) { options = options || {}; options.infos = options.infos || {}; var parseOffsets = options.parseOffsets || false, removePrefix = options.removePrefix || true, deferred = new $.Deferred(), destDir = FileSystem.getDirectoryForPath(destination); // create the destination folder destDir.create(function (err) { if (err && err !== FileSystemError.ALREADY_EXISTS) { deferred.reject(); return; } source.getContents(function (err, contents) { if (!err) { // copy all children of this directory var copyChildrenPromise = Async.doInParallel( contents, function copyChild(child) { var childDestination = destination + "/" + child.name, promise; if (child.isDirectory) { promise = copyDirectoryEntry(child, childDestination, options); } else { promise = copyFileEntry(child, childDestination, options); if (parseOffsets) { // save offset data for each file path promise.done(function (destinationEntry, offsets, text) { options.infos[childDestination] = { offsets : offsets, fileEntry : destinationEntry, text : text }; }); } } return promise; } ); copyChildrenPromise.then(deferred.resolve, deferred.reject); } else { deferred.reject(err); } }); }); deferred.always(function () { // remove destination path prefix if (removePrefix && options.infos) { var shortKey; Object.keys(options.infos).forEach(function (key) { shortKey = key.substr(destination.length + 1); options.infos[shortKey] = options.infos[key]; }); } }); return deferred.promise(); }