src/file/FileUtils.js
/*
* Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
/*jslint regexp: true */
/*global unescape */
/**
* Set of utilities for working with files and text content.
*/
define(function (require, exports, module) {
"use strict";
require("utils/Global");
var FileSystemError = require("filesystem/FileSystemError"),
DeprecationWarning = require("utils/DeprecationWarning"),
LanguageManager = require("language/LanguageManager"),
PerfUtils = require("utils/PerfUtils"),
Strings = require("strings"),
StringUtils = require("utils/StringUtils");
// These will be loaded asynchronously
var DocumentCommandHandlers, LiveDevelopmentUtils;
/**
* @const {Number} Maximium file size (in megabytes)
* (for display strings)
* This must be a hard-coded value since this value
* tells how low-level APIs should behave which cannot
* have a load order dependency on preferences manager
*/
var MAX_FILE_SIZE_MB = 16;
/**
* @const {Number} Maximium file size (in bytes)
* This must be a hard-coded value since this value
* tells how low-level APIs should behave which cannot
* have a load order dependency on preferences manager
*/
var MAX_FILE_SIZE = MAX_FILE_SIZE_MB * 1024 * 1024;
/**
* @const {List} list of File Extensions which will be opened in external Application
*/
var extListToBeOpenedInExtApp = [];
/**
* Asynchronously reads a file as UTF-8 encoded text.
* @param {!File} file File to read
* @return {$.Promise} a jQuery promise that will be resolved with the
* file's text content plus its timestamp, or rejected with a FileSystemError string
* constant if the file can not be read.
*/
function readAsText(file) {
var result = new $.Deferred();
// Measure performance
var perfTimerName = PerfUtils.markStart("readAsText:\t" + file.fullPath);
result.always(function () {
PerfUtils.addMeasurement(perfTimerName);
});
// Read file
file.read(function (err, data, encoding, stat) {
if (!err) {
result.resolve(data, stat.mtime);
} else {
result.reject(err);
}
});
return result.promise();
}
/**
* Asynchronously writes a file as UTF-8 encoded text.
* @param {!File} file File to write
* @param {!string} text
* @param {boolean=} allowBlindWrite Indicates whether or not CONTENTS_MODIFIED
* errors---which can be triggered if the actual file contents differ from
* the FileSystem's last-known contents---should be ignored.
* @return {$.Promise} a jQuery promise that will be resolved when
* file writing completes, or rejected with a FileSystemError string constant.
*/
function writeText(file, text, allowBlindWrite) {
var result = new $.Deferred(),
options = {};
if (allowBlindWrite) {
options.blind = true;
}
file.write(text, options, function (err) {
if (!err) {
result.resolve();
} else {
result.reject(err);
}
});
return result.promise();
}
/**
* Line endings
* @enum {string}
*/
var LINE_ENDINGS_CRLF = "CRLF",
LINE_ENDINGS_LF = "LF";
/**
* Returns the standard line endings for the current platform
* @return {LINE_ENDINGS_CRLF|LINE_ENDINGS_LF}
*/
function getPlatformLineEndings() {
return brackets.platform === "win" ? LINE_ENDINGS_CRLF : LINE_ENDINGS_LF;
}
/**
* Scans the first 1000 chars of the text to determine how it encodes line endings. Returns
* null if usage is mixed or if no line endings found.
* @param {!string} text
* @return {null|LINE_ENDINGS_CRLF|LINE_ENDINGS_LF}
*/
function sniffLineEndings(text) {
var subset = text.substr(0, 1000); // (length is clipped to text.length)
var hasCRLF = /\r\n/.test(subset);
var hasLF = /[^\r]\n/.test(subset);
if ((hasCRLF && hasLF) || (!hasCRLF && !hasLF)) {
return null;
} else {
return hasCRLF ? LINE_ENDINGS_CRLF : LINE_ENDINGS_LF;
}
}
/**
* Translates any line ending types in the given text to the be the single form specified
* @param {!string} text
* @param {null|LINE_ENDINGS_CRLF|LINE_ENDINGS_LF} lineEndings
* @return {string}
*/
function translateLineEndings(text, lineEndings) {
if (lineEndings !== LINE_ENDINGS_CRLF && lineEndings !== LINE_ENDINGS_LF) {
lineEndings = getPlatformLineEndings();
}
var eolStr = (lineEndings === LINE_ENDINGS_CRLF ? "\r\n" : "\n");
var findAnyEol = /\r\n|\r|\n/g;
return text.replace(findAnyEol, eolStr);
}
/**
* @param {!FileSystemError} name
* @return {!string} User-friendly, localized error message
*/
function getFileErrorString(name) {
// There are a few error codes that we have specific error messages for. The rest are
// displayed with a generic "(error N)" message.
var result;
if (name === FileSystemError.NOT_FOUND) {
result = Strings.NOT_FOUND_ERR;
} else if (name === FileSystemError.NOT_READABLE) {
result = Strings.NOT_READABLE_ERR;
} else if (name === FileSystemError.NOT_WRITABLE) {
result = Strings.NO_MODIFICATION_ALLOWED_ERR_FILE;
} else if (name === FileSystemError.CONTENTS_MODIFIED) {
result = Strings.CONTENTS_MODIFIED_ERR;
} else if (name === FileSystemError.UNSUPPORTED_ENCODING) {
result = Strings.UNSUPPORTED_ENCODING_ERR;
} else if (name === FileSystemError.EXCEEDS_MAX_FILE_SIZE) {
result = StringUtils.format(Strings.EXCEEDS_MAX_FILE_SIZE, MAX_FILE_SIZE_MB);
} else if (name === FileSystemError.ENCODE_FILE_FAILED) {
result = Strings.ENCODE_FILE_FAILED_ERR;
} else if (name === FileSystemError.DECODE_FILE_FAILED) {
result = Strings.DECODE_FILE_FAILED_ERR;
} else if (name === FileSystemError.UNSUPPORTED_UTF16_ENCODING) {
result = Strings.UNSUPPORTED_UTF16_ENCODING_ERR;
} else {
result = StringUtils.format(Strings.GENERIC_ERROR, name);
}
return result;
}
/**
* Shows an error dialog indicating that the given file could not be opened due to the given error
* @deprecated Use DocumentCommandHandlers.showFileOpenError() instead
*
* @param {!FileSystemError} name
* @return {!Dialog}
*/
function showFileOpenError(name, path) {
DeprecationWarning.deprecationWarning("FileUtils.showFileOpenError() has been deprecated. " +
"Please use DocumentCommandHandlers.showFileOpenError() instead.");
return DocumentCommandHandlers.showFileOpenError(name, path);
}
/**
* Creates an HTML string for a list of files to be reported on, suitable for use in a dialog.
* @param {Array.<string>} Array of filenames or paths to display.
*/
function makeDialogFileList(paths) {
var result = "<ul class='dialog-list'>";
paths.forEach(function (path) {
result += "<li><span class='dialog-filename'>";
result += StringUtils.breakableUrl(path);
result += "</span></li>";
});
result += "</ul>";
return result;
}
/**
* Convert a URI path to a native path.
* On both platforms, this unescapes the URI
* On windows, URI paths start with a "/", but have a drive letter ("C:"). In this
* case, remove the initial "/".
* @param {!string} path
* @return {string}
*/
function convertToNativePath(path) {
path = unescape(path);
if (path.indexOf(":") !== -1 && path[0] === "/") {
return path.substr(1);
}
return path;
}
/**
* Convert a Windows-native path to use Unix style slashes.
* On Windows, this converts "C:\foo\bar\baz.txt" to "C:/foo/bar/baz.txt".
* On Mac, this does nothing, since Mac paths are already in Unix syntax.
* (Note that this does not add an initial forward-slash. Internally, our
* APIs generally use the "C:/foo/bar/baz.txt" style for "native" paths.)
* @param {string} path A native-style path.
* @return {string} A Unix-style path.
*/
function convertWindowsPathToUnixPath(path) {
if (brackets.platform === "win") {
path = path.replace(/\\/g, "/");
}
return path;
}
/**
* Removes the trailing slash from a path, if it has one.
* Warning: this differs from the format of most paths used in Brackets! Use paths ending in "/"
* normally, as this is the format used by Directory.fullPath.
*
* @param {string} path
* @return {string}
*/
function stripTrailingSlash(path) {
if (path && path[path.length - 1] === "/") {
return path.slice(0, -1);
} else {
return path;
}
}
/**
* Get the name of a file or a directory, removing any preceding path.
* @param {string} fullPath full path to a file or directory
* @return {string} Returns the base name of a file or the name of a
* directory
*/
function getBaseName(fullPath) {
var lastSlash = fullPath.lastIndexOf("/");
if (lastSlash === fullPath.length - 1) { // directory: exclude trailing "/" too
return fullPath.slice(fullPath.lastIndexOf("/", fullPath.length - 2) + 1, -1);
} else {
return fullPath.slice(lastSlash + 1);
}
}
/**
* Returns a native absolute path to the 'brackets' source directory.
* Note that this only works when run in brackets/src/index.html, so it does
* not work for unit tests (which is run from brackets/test/SpecRunner.html)
*
* WARNING: unlike most paths in Brackets, this path EXCLUDES the trailing "/".
* @return {string}
*/
function getNativeBracketsDirectoryPath() {
var pathname = decodeURI(window.location.pathname);
var directory = pathname.substr(0, pathname.lastIndexOf("/"));
return convertToNativePath(directory);
}
/**
* Given the module object passed to JS module define function,
* convert the path to a native absolute path.
* Returns a native absolute path to the module folder.
*
* WARNING: unlike most paths in Brackets, this path EXCLUDES the trailing "/".
* @return {string}
*/
function getNativeModuleDirectoryPath(module) {
var path;
if (module && module.uri) {
path = decodeURI(module.uri);
// Remove module name and trailing slash from path.
path = path.substr(0, path.lastIndexOf("/"));
}
return path;
}
/**
* Get the file extension (excluding ".") given a path OR a bare filename.
* Returns "" for names with no extension. If the name starts with ".", the
* full remaining text is considered the extension.
*
* @param {string} fullPath full path to a file or directory
* @return {string} Returns the extension of a filename or empty string if
* the argument is a directory or a filename with no extension
*/
function getFileExtension(fullPath) {
var baseName = getBaseName(fullPath),
idx = baseName.lastIndexOf(".");
if (idx === -1) {
return "";
}
return baseName.substr(idx + 1);
}
/**
* Get the file extension (excluding ".") given a path OR a bare filename.
* Returns "" for names with no extension.
* If the only `.` in the file is the first character,
* returns "" as this is not considered an extension.
* This method considers known extensions which include `.` in them.
* @deprecated Use LanguageManager.getCompoundFileExtension() instead
*
* @param {string} fullPath full path to a file or directory
* @return {string} Returns the extension of a filename or empty string if
* the argument is a directory or a filename with no extension
*/
function getSmartFileExtension(fullPath) {
DeprecationWarning.deprecationWarning("FileUtils.getSmartFileExtension() has been deprecated. " +
"Please use LanguageManager.getCompoundFileExtension() instead.");
return LanguageManager.getCompoundFileExtension(fullPath);
}
/**
* Computes filename as relative to the basePath. For example:
* basePath: /foo/bar/, filename: /foo/bar/baz.txt
* returns: baz.txt
*
* The net effect is that the common prefix is stripped away. If basePath is not
* a prefix of filename, then undefined is returned.
*
* @param {string} basePath Path against which we're computing the relative path
* @param {string} filename Full path to the file for which we are computing a relative path
* @return {string} relative path
*/
function getRelativeFilename(basePath, filename) {
if (!filename || filename.substr(0, basePath.length) !== basePath) {
return;
}
return filename.substr(basePath.length);
}
/**
* Determine if file extension is a static html file extension.
* @param {string} filePath could be a path, a file name or just a file extension
* @return {boolean} Returns true if fileExt is in the list
*/
function isStaticHtmlFileExt(filePath) {
DeprecationWarning.deprecationWarning("FileUtils.isStaticHtmlFileExt() has been deprecated. " +
"Please use LiveDevelopmentUtils.isStaticHtmlFileExt() instead.");
return LiveDevelopmentUtils.isStaticHtmlFileExt(filePath);
}
/**
* Get the parent directory of a file. If a directory is passed, the SAME directory is returned.
* @param {string} fullPath full path to a file or directory
* @return {string} Returns the path to the parent directory of a file or the path of a directory,
* including trailing "/"
*/
function getDirectoryPath(fullPath) {
return fullPath.substr(0, fullPath.lastIndexOf("/") + 1);
}
/**
* Get the parent folder of the given file/folder path. Differs from getDirectoryPath() when 'fullPath'
* is a directory itself: returns its parent instead of the original path. (Note: if you already have a
* FileSystemEntry, it's faster to use entry.parentPath instead).
* @param {string} fullPath full path to a file or directory
* @return {string} Path of containing folder (including trailing "/"); or "" if path was the root
*/
function getParentPath(fullPath) {
if (fullPath === "/") {
return "";
}
return fullPath.substring(0, fullPath.lastIndexOf("/", fullPath.length - 2) + 1);
}
/**
* Get the file name without the extension. Returns "" if name starts with "."
* @param {string} filename File name of a file or directory, without preceding path
* @return {string} Returns the file name without the extension
*/
function getFilenameWithoutExtension(filename) {
var index = filename.lastIndexOf(".");
return index === -1 ? filename : filename.slice(0, index);
}
/**
* @private
* OS-specific helper for `compareFilenames()`
* @return {Function} The OS-specific compare function
*/
var _cmpNames = (function () {
if (brackets.platform === "win") {
// Use this function on Windows
return function (filename1, filename2, lang) {
var f1 = getFilenameWithoutExtension(filename1),
f2 = getFilenameWithoutExtension(filename2);
return f1.localeCompare(f2, lang, {numeric: true});
};
}
// Use this function other OSes
return function (filename1, filename2, lang) {
return filename1.localeCompare(filename2, lang, {numeric: true});
};
}());
/**
* Compares 2 filenames in lowercases. In Windows it compares the names without the
* extension first and then the extensions to fix issue #4409
* @param {string} filename1
* @param {string} filename2
* @param {boolean} extFirst If true it compares the extensions first and then the file names.
* @return {number} The result of the compare function
*/
function compareFilenames(filename1, filename2, extFirst) {
var lang = brackets.getLocale();
filename1 = filename1.toLocaleLowerCase();
filename2 = filename2.toLocaleLowerCase();
function cmpExt() {
var ext1 = getFileExtension(filename1),
ext2 = getFileExtension(filename2);
return ext1.localeCompare(ext2, lang, {numeric: true});
}
function cmpNames() {
return _cmpNames(filename1, filename2, lang);
}
return extFirst ? (cmpExt() || cmpNames()) : (cmpNames() || cmpExt());
}
/**
* Compares two paths segment-by-segment, used for sorting. When two files share a path prefix,
* the less deeply nested one is sorted earlier in the list. Sorts files within the same parent
* folder based on `compareFilenames()`.
* @param {string} path1
* @param {string} path2
* @return {number} -1, 0, or 1 depending on whether path1 is less than, equal to, or greater than
* path2 according to this ordering.
*/
function comparePaths(path1, path2) {
var entryName1, entryName2,
pathParts1 = path1.split("/"),
pathParts2 = path2.split("/"),
length = Math.min(pathParts1.length, pathParts2.length),
folders1 = pathParts1.length - 1,
folders2 = pathParts2.length - 1,
index = 0;
while (index < length) {
entryName1 = pathParts1[index];
entryName2 = pathParts2[index];
if (entryName1 !== entryName2) {
if (index < folders1 && index < folders2) {
return entryName1.toLocaleLowerCase().localeCompare(entryName2.toLocaleLowerCase());
} else if (index >= folders1 && index >= folders2) {
return compareFilenames(entryName1, entryName2);
}
return (index >= folders1 && index < folders2) ? -1 : 1;
}
index++;
}
return 0;
}
/**
* @param {string} path Native path in the format used by FileSystemEntry.fullPath
* @return {string} URI-encoded version suitable for appending to 'file:///`. It's not safe to use encodeURI()
* directly since it doesn't escape chars like "#".
*/
function encodeFilePath(path) {
var pathArray = path.split("/");
pathArray = pathArray.map(function (subPath) {
return encodeURIComponent(subPath);
});
return pathArray.join("/");
}
/**
* @param {string} ext extension string a file
* @return {string} returns true If file to be opened in External Application.
*
*/
function shouldOpenInExternalApplication(ext) {
return extListToBeOpenedInExtApp.includes(ext);
}
/**
* @param {string} ext File Extensions to be added in External App List
*
*/
function addExtensionToExternalAppList(ext) {
if(Array.isArray(ext)) {
extListToBeOpenedInExtApp = ext;
} else if (typeof ext === 'string'){
extListToBeOpenedInExtApp.push(ext);
}
}
// Asynchronously load DocumentCommandHandlers
// This avoids a temporary circular dependency created
// by relocating showFileOpenError() until deprecation is over
require(["document/DocumentCommandHandlers"], function (dchModule) {
DocumentCommandHandlers = dchModule;
});
// Asynchronously load LiveDevelopmentUtils
// This avoids a temporary circular dependency created
// by relocating isStaticHtmlFileExt() until deprecation is over
require(["LiveDevelopment/LiveDevelopmentUtils"], function (lduModule) {
LiveDevelopmentUtils = lduModule;
});
// Define public API
exports.LINE_ENDINGS_CRLF = LINE_ENDINGS_CRLF;
exports.LINE_ENDINGS_LF = LINE_ENDINGS_LF;
exports.getPlatformLineEndings = getPlatformLineEndings;
exports.sniffLineEndings = sniffLineEndings;
exports.translateLineEndings = translateLineEndings;
exports.showFileOpenError = showFileOpenError;
exports.getFileErrorString = getFileErrorString;
exports.makeDialogFileList = makeDialogFileList;
exports.readAsText = readAsText;
exports.writeText = writeText;
exports.convertToNativePath = convertToNativePath;
exports.convertWindowsPathToUnixPath = convertWindowsPathToUnixPath;
exports.getNativeBracketsDirectoryPath = getNativeBracketsDirectoryPath;
exports.getNativeModuleDirectoryPath = getNativeModuleDirectoryPath;
exports.stripTrailingSlash = stripTrailingSlash;
exports.isStaticHtmlFileExt = isStaticHtmlFileExt;
exports.getDirectoryPath = getDirectoryPath;
exports.getParentPath = getParentPath;
exports.getBaseName = getBaseName;
exports.getRelativeFilename = getRelativeFilename;
exports.getFilenameWithoutExtension = getFilenameWithoutExtension;
exports.getFileExtension = getFileExtension;
exports.getSmartFileExtension = getSmartFileExtension;
exports.compareFilenames = compareFilenames;
exports.comparePaths = comparePaths;
exports.MAX_FILE_SIZE = MAX_FILE_SIZE;
exports.encodeFilePath = encodeFilePath;
exports.shouldOpenInExternalApplication = shouldOpenInExternalApplication;
exports.addExtensionToExternalAppList = addExtensionToExternalAppList;
});