Bug 561472 - Add a new DownloadPaths JavaScript module. r=sdwilsh

This commit is contained in:
Paolo Amadini 2010-05-12 12:11:53 +02:00
parent 2e5b6f9223
commit b471c5525e
4 changed files with 260 additions and 55 deletions

View File

@ -0,0 +1,121 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (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.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Download Manager Utility Code.
*
* The Initial Developer of the Original Code is
* Paolo Amadini <http://www.amadzone.org/>.
* Portions created by the Initial Developer are Copyright (C) 2010
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
var EXPORTED_SYMBOLS = [
"DownloadPaths",
];
/**
* This module provides the DownloadPaths object which contains methods for
* giving names and paths to files being downloaded.
*
* List of methods:
*
* nsILocalFile
* createNiceUniqueFile(nsILocalFile aLocalFile)
*
* [string base, string ext]
* splitBaseNameAndExtension(string aLeafName)
*/
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const Cr = Components.results;
const DownloadPaths = {
/**
* Creates a uniquely-named file starting from the name of the provided file.
* If a file with the provided name already exists, the function attempts to
* create nice alternatives, like "base(1).ext" (instead of "base-1.ext").
*
* If a unique name cannot be found, the function throws the XPCOM exception
* NS_ERROR_FILE_TOO_BIG. Other exceptions, like NS_ERROR_FILE_ACCESS_DENIED,
* can also be expected.
*
* @param aTemplateFile
* nsILocalFile whose leaf name is going to be used as a template. The
* provided object is not modified.
* @returns A new instance of an nsILocalFile object pointing to the newly
* created empty file. On platforms that support permission bits, the
* file is created with permissions 600.
*/
createNiceUniqueFile: function DP_createNiceUniqueFile(aTemplateFile) {
// Work on a clone of the provided template file object.
var curFile = aTemplateFile.clone().QueryInterface(Ci.nsILocalFile);
var [base, ext] = DownloadPaths.splitBaseNameAndExtension(curFile.leafName);
// Try other file names, for example "base(1).txt" or "base(1).tar.gz",
// only if the file name initially set already exists.
for (let i = 1; i < 10000 && curFile.exists(); i++) {
curFile.leafName = base + "(" + i + ")" + ext;
}
// At this point we hand off control to createUnique, which will create the
// file with the name we chose, if it is valid. If not, createUnique will
// attempt to modify it again, for example it will shorten very long names
// that can't be created on some platforms, and for which a normal call to
// nsIFile.create would result in NS_ERROR_FILE_NOT_FOUND. This can result
// very rarely in strange names like "base(9999).tar-1.gz" or "ba-1.gz".
curFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0600);
return curFile;
},
/**
* Separates the base name from the extension in a file name, recognizing some
* double extensions like ".tar.gz".
*
* @param aLeafName
* The full leaf name to be parsed. Be careful when processing names
* containing leading or trailing dots or spaces.
* @returns [base, ext]
* The base name of the file, which can be empty, and its extension,
* which always includes the leading dot unless it's an empty string.
* Concatenating the two items always results in the original name.
*/
splitBaseNameAndExtension: function DP_splitBaseNameAndExtension(aLeafName) {
// The following regular expression is built from these key parts:
// .*? Matches the base name non-greedily.
// \.[A-Z0-9]{1,3} Up to three letters or numbers preceding a
// double extension.
// \.(?:gz|bz2|Z) The second part of common double extensions.
// \.[^.]* Matches any extension or a single trailing dot.
var [, base, ext] = /(.*?)(\.[A-Z0-9]{1,3}\.(?:gz|bz2|Z)|\.[^.]*)?$/i
.exec(aLeafName);
// Return an empty string instead of undefined if no extension is found.
return [base, ext || ""];
}
};

View File

@ -48,6 +48,7 @@ EXTRA_PP_COMPONENTS = nsHelperAppDlg.js
EXTRA_JS_MODULES = \
DownloadLastDir.jsm \
DownloadPaths.jsm \
DownloadUtils.jsm \
$(NULL)

View File

@ -78,6 +78,7 @@ const PREF_BD_USEDOWNLOADDIR = "browser.download.useDownloadDir";
const nsITimer = Components.interfaces.nsITimer;
Components.utils.import("resource://gre/modules/DownloadLastDir.jsm");
Components.utils.import("resource://gre/modules/DownloadPaths.jsm");
/* ctor
*/
@ -313,7 +314,7 @@ nsUnknownContentTypeDialog.prototype = {
aLeafName = "unnamed" + (aFileExt ? "." + aFileExt : "");
aLocalFile.append(aLeafName);
this.makeFileUnique(aLocalFile);
var createdFile = DownloadPaths.createNiceUniqueFile(aLocalFile);
#ifdef XP_WIN
let ext;
@ -324,66 +325,17 @@ nsUnknownContentTypeDialog.prototype = {
// Append a file extension if it's an executable that doesn't have one
// but make sure we actually have an extension to add
let leaf = aLocalFile.leafName;
if (aLocalFile.isExecutable() && ext &&
leaf.substring(leaf.length - ext.length) != ext) {
let f = aLocalFile.clone();
let leaf = createdFile.leafName;
if (ext && leaf.slice(-ext.length) != ext && createdFile.isExecutable()) {
createdFile.remove(false);
aLocalFile.leafName = leaf + ext;
f.remove(false);
this.makeFileUnique(aLocalFile);
createdFile = DownloadPaths.createNiceUniqueFile(aLocalFile);
}
#endif
return aLocalFile;
return createdFile;
},
/**
* Generates and returns a uniquely-named file from aLocalFile. If
* aLocalFile does not exist, it will be the file returned; otherwise, a
* file whose name is similar to that of aLocalFile will be returned.
*/
makeFileUnique: function (aLocalFile)
{
try {
// Note - this code is identical to that in
// toolkit/content/contentAreaUtils.js.
// If you are updating this code, update that code too! We can't share code
// here since this is called in a js component.
var collisionCount = 0;
while (aLocalFile.exists()) {
collisionCount++;
if (collisionCount == 1) {
// Append "(2)" before the last dot in (or at the end of) the filename
// special case .ext.gz etc files so we don't wind up with .tar(2).gz
if (aLocalFile.leafName.match(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i)) {
aLocalFile.leafName = aLocalFile.leafName.replace(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i, "(2)$&");
}
else {
aLocalFile.leafName = aLocalFile.leafName.replace(/(\.[^\.]*)?$/, "(2)$&");
}
}
else {
// replace the last (n) in the filename with (n+1)
aLocalFile.leafName = aLocalFile.leafName.replace(/^(.*\()\d+\)/, "$1" + (collisionCount+1) + ")");
}
}
aLocalFile.create(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0600);
}
catch (e) {
dump("*** exception in validateLeafName: " + e + "\n");
if (e.result == Components.results.NS_ERROR_FILE_ACCESS_DENIED)
throw e;
if (aLocalFile.leafName == "" || aLocalFile.isDirectory()) {
aLocalFile.append("unnamed");
if (aLocalFile.exists())
aLocalFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0600);
}
}
},
// ---------- implementation methods ----------
// Web progress listener so we can detect errors while mLauncher is

View File

@ -0,0 +1,131 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
/* ***** BEGIN LICENSE BLOCK *****
*
* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*
* ***** END LICENSE BLOCK ***** */
/**
* Tests for the "DownloadPaths.jsm" JavaScript module.
*/
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const Cr = Components.results;
Cu.import("resource://gre/modules/DownloadPaths.jsm");
/**
* Provides a temporary save directory.
*
* @returns nsIFile pointing to the new or existing directory.
*/
function createTemporarySaveDirectory()
{
var saveDir = Cc["@mozilla.org/file/directory_service;1"].
getService(Ci.nsIProperties).get("TmpD", Ci.nsIFile);
saveDir.append("testsavedir");
if (!saveDir.exists()) {
saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0755);
}
return saveDir;
}
function testSplitBaseNameAndExtension(aLeafName, [aBase, aExt])
{
var [base, ext] = DownloadPaths.splitBaseNameAndExtension(aLeafName);
do_check_eq(base, aBase);
do_check_eq(ext, aExt);
// If we modify the base name and concatenate it with the extension again,
// another roundtrip through the function should give a consistent result.
// The only exception is when we introduce an extension in a file name that
// didn't have one or that ended with one of the special cases like ".gz". If
// we avoid using a dot and we introduce at least another special character,
// the results are always consistent.
[base, ext] = DownloadPaths.splitBaseNameAndExtension("(" + base + ")" + ext);
do_check_eq(base, "(" + aBase + ")");
do_check_eq(ext, aExt);
}
function testCreateNiceUniqueFile(aTempFile, aExpectedLeafName)
{
var createdFile = DownloadPaths.createNiceUniqueFile(aTempFile);
do_check_eq(createdFile.leafName, aExpectedLeafName);
}
function run_test()
{
// Usual file names.
testSplitBaseNameAndExtension("base", ["base", ""]);
testSplitBaseNameAndExtension("base.ext", ["base", ".ext"]);
testSplitBaseNameAndExtension("base.application", ["base", ".application"]);
testSplitBaseNameAndExtension("base.x.Z", ["base", ".x.Z"]);
testSplitBaseNameAndExtension("base.ext.Z", ["base", ".ext.Z"]);
testSplitBaseNameAndExtension("base.ext.gz", ["base", ".ext.gz"]);
testSplitBaseNameAndExtension("base.ext.Bz2", ["base", ".ext.Bz2"]);
testSplitBaseNameAndExtension("base..ext", ["base.", ".ext"]);
testSplitBaseNameAndExtension("base..Z", ["base.", ".Z"]);
testSplitBaseNameAndExtension("base. .Z", ["base. ", ".Z"]);
testSplitBaseNameAndExtension("base.base.Bz2", ["base.base", ".Bz2"]);
testSplitBaseNameAndExtension("base .ext", ["base ", ".ext"]);
// Corner cases. A name ending with a dot technically has no extension, but
// we consider the ending dot separately from the base name so that modifying
// the latter never results in an extension being introduced accidentally.
// Names beginning with a dot are hidden files on Unix-like platforms and if
// their name doesn't contain another dot they should have no extension, but
// on Windows the whole name is considered as an extension.
testSplitBaseNameAndExtension("base.", ["base", "."]);
testSplitBaseNameAndExtension(".ext", ["", ".ext"]);
// Unusual file names (not recommended as input to the function).
testSplitBaseNameAndExtension("base. ", ["base", ". "]);
testSplitBaseNameAndExtension("base ", ["base ", ""]);
testSplitBaseNameAndExtension("", ["", ""]);
testSplitBaseNameAndExtension(" ", [" ", ""]);
testSplitBaseNameAndExtension(" . ", [" ", ". "]);
testSplitBaseNameAndExtension(" .. ", [" .", ". "]);
testSplitBaseNameAndExtension(" .ext", [" ", ".ext"]);
testSplitBaseNameAndExtension(" .ext. ", [" .ext", ". "]);
testSplitBaseNameAndExtension(" .ext.gz ", [" .ext", ".gz "]);
var destDir = createTemporarySaveDirectory();
try {
// Single extension.
var tempFile = destDir.clone();
tempFile.append("test.txt");
testCreateNiceUniqueFile(tempFile, "test.txt");
testCreateNiceUniqueFile(tempFile, "test(1).txt");
testCreateNiceUniqueFile(tempFile, "test(2).txt");
// Double extension.
tempFile.leafName = "test.tar.gz";
testCreateNiceUniqueFile(tempFile, "test.tar.gz");
testCreateNiceUniqueFile(tempFile, "test(1).tar.gz");
testCreateNiceUniqueFile(tempFile, "test(2).tar.gz");
// Test automatic shortening of long file names. We don't know exactly how
// many characters are removed, because it depends on the name of the folder
// where the file is located.
tempFile.leafName = new Array(256).join("T") + ".txt";
var newFile = DownloadPaths.createNiceUniqueFile(tempFile);
do_check_true(newFile.leafName.length < tempFile.leafName.length);
do_check_eq(newFile.leafName.slice(-4), ".txt");
// Creating a valid file name from an invalid one is not always possible.
tempFile.append("file-under-long-directory.txt");
try {
DownloadPaths.createNiceUniqueFile(tempFile);
do_throw("Exception expected with a long parent directory name.")
} catch (e) {
// An exception is expected, but we don't know which one exactly.
}
} finally {
// Clean up the temporary directory.
destDir.remove(true);
}
}