mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
516 lines
17 KiB
JavaScript
516 lines
17 KiB
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
|
|
* Edward Lee <edward.lee@engineering.uiuc.edu>.
|
|
* Portions created by the Initial Developer are Copyright (C) 2008
|
|
* 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 = [ "DownloadUtils" ];
|
|
|
|
/**
|
|
* This module provides the DownloadUtils object which contains useful methods
|
|
* for downloads such as displaying file sizes, transfer times, and download
|
|
* locations.
|
|
*
|
|
* List of methods:
|
|
*
|
|
* [string status, double newLast]
|
|
* getDownloadStatus(int aCurrBytes, [optional] int aMaxBytes,
|
|
* [optional] double aSpeed, [optional] double aLastSec)
|
|
*
|
|
* string progress
|
|
* getTransferTotal(int aCurrBytes, [optional] int aMaxBytes)
|
|
*
|
|
* [string timeLeft, double newLast]
|
|
* getTimeLeft(double aSeconds, [optional] double aLastSec)
|
|
*
|
|
* [string displayHost, string fullHost]
|
|
* getURIHost(string aURIString)
|
|
*
|
|
* [string convertedBytes, string units]
|
|
* convertByteUnits(int aBytes)
|
|
*
|
|
* [int time, string units, int subTime, string subUnits]
|
|
* convertTimeUnits(double aSecs)
|
|
*/
|
|
|
|
const Cc = Components.classes;
|
|
const Ci = Components.interfaces;
|
|
const Cu = Components.utils;
|
|
|
|
__defineGetter__("PluralForm", function() {
|
|
delete this.PluralForm;
|
|
Cu.import("resource://gre/modules/PluralForm.jsm");
|
|
return PluralForm;
|
|
});
|
|
|
|
__defineGetter__("gDecimalSymbol", function() {
|
|
delete this.gDecimalSymbol;
|
|
return this.gDecimalSymbol = Number(5.4).toLocaleString().match(/\D/);
|
|
});
|
|
|
|
const kDownloadProperties =
|
|
"chrome://mozapps/locale/downloads/downloads.properties";
|
|
|
|
// These strings will be converted to the corresponding ones from the string
|
|
// bundle on use
|
|
let kStrings = {
|
|
statusFormat: "statusFormat2",
|
|
transferSameUnits: "transferSameUnits",
|
|
transferDiffUnits: "transferDiffUnits",
|
|
transferNoTotal: "transferNoTotal",
|
|
timePair: "timePair",
|
|
timeLeftSingle: "timeLeftSingle",
|
|
timeLeftDouble: "timeLeftDouble",
|
|
timeFewSeconds: "timeFewSeconds",
|
|
timeUnknown: "timeUnknown",
|
|
doneScheme: "doneScheme",
|
|
doneFileScheme: "doneFileScheme",
|
|
units: ["bytes", "kilobyte", "megabyte", "gigabyte"],
|
|
// Update timeSize in convertTimeUnits if changing the length of this array
|
|
timeUnits: ["seconds", "minutes", "hours", "days"],
|
|
};
|
|
|
|
// This object will lazily load the strings defined in kStrings
|
|
let gStr = {
|
|
/**
|
|
* Initialize lazy string getters
|
|
*/
|
|
_init: function()
|
|
{
|
|
// Make each "name" a lazy-loading string that knows how to load itself. We
|
|
// need to locally scope name and value to keep them around for the getter.
|
|
for (let [name, value] in Iterator(kStrings))
|
|
let ([n, v] = [name, value])
|
|
gStr.__defineGetter__(n, function() gStr._getStr(n, v));
|
|
},
|
|
|
|
/**
|
|
* Convert strings to those in the string bundle. This lazily loads the
|
|
* string bundle *once* only when used the first time.
|
|
*/
|
|
get _getStr()
|
|
{
|
|
// Delete the getter to be overwritten
|
|
delete gStr._getStr;
|
|
|
|
// Lazily load the bundle into the closure on first call to _getStr
|
|
let getStr = Cc["@mozilla.org/intl/stringbundle;1"].
|
|
getService(Ci.nsIStringBundleService).
|
|
createBundle(kDownloadProperties).
|
|
GetStringFromName;
|
|
|
|
// _getStr is a function that sets string "name" to stringbundle's "value"
|
|
return gStr._getStr = function(name, value) {
|
|
// Delete the getter to be overwritten
|
|
delete gStr[name];
|
|
|
|
try {
|
|
// "name" is a string or array of the stringbundle-loaded "value"
|
|
return gStr[name] = typeof value == "string" ?
|
|
getStr(value) :
|
|
value.map(getStr);
|
|
} catch (e) {
|
|
log(["Couldn't get string '", name, "' from property '", value, "'"]);
|
|
// Don't return anything (undefined), and because we deleted ourselves,
|
|
// future accesses will also be undefined
|
|
}
|
|
};
|
|
},
|
|
};
|
|
// Initialize the lazy string getters!
|
|
gStr._init();
|
|
|
|
// Keep track of at most this many second/lastSec pairs so that multiple calls
|
|
// to getTimeLeft produce the same time left
|
|
const kCachedLastMaxSize = 10;
|
|
let gCachedLast = [];
|
|
|
|
let DownloadUtils = {
|
|
/**
|
|
* Generate a full status string for a download given its current progress,
|
|
* total size, speed, last time remaining
|
|
*
|
|
* @param aCurrBytes
|
|
* Number of bytes transferred so far
|
|
* @param [optional] aMaxBytes
|
|
* Total number of bytes or -1 for unknown
|
|
* @param [optional] aSpeed
|
|
* Current transfer rate in bytes/sec or -1 for unknown
|
|
* @param [optional] aLastSec
|
|
* Last time remaining in seconds or Infinity for unknown
|
|
* @return A pair: [download status text, new value of "last seconds"]
|
|
*/
|
|
getDownloadStatus: function DU_getDownloadStatus(aCurrBytes, aMaxBytes,
|
|
aSpeed, aLastSec)
|
|
{
|
|
if (aMaxBytes == null)
|
|
aMaxBytes = -1;
|
|
if (aSpeed == null)
|
|
aSpeed = -1;
|
|
if (aLastSec == null)
|
|
aLastSec = Infinity;
|
|
|
|
// Calculate the time remaining if we have valid values
|
|
let seconds = (aSpeed > 0) && (aMaxBytes > 0) ?
|
|
(aMaxBytes - aCurrBytes) / aSpeed : -1;
|
|
|
|
// Update the bytes transferred and bytes total
|
|
let status;
|
|
let (transfer = DownloadUtils.getTransferTotal(aCurrBytes, aMaxBytes)) {
|
|
// Insert 1 is the download progress
|
|
status = replaceInsert(gStr.statusFormat, 1, transfer);
|
|
}
|
|
|
|
// Update the download rate
|
|
let ([rate, unit] = DownloadUtils.convertByteUnits(aSpeed)) {
|
|
// Insert 2 is the download rate
|
|
status = replaceInsert(status, 2, rate);
|
|
// Insert 3 is the |unit|/sec
|
|
status = replaceInsert(status, 3, unit);
|
|
}
|
|
|
|
// Update time remaining
|
|
let ([timeLeft, newLast] = DownloadUtils.getTimeLeft(seconds, aLastSec)) {
|
|
// Insert 4 is the time remaining
|
|
status = replaceInsert(status, 4, timeLeft);
|
|
|
|
return [status, newLast];
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Generate the transfer progress string to show the current and total byte
|
|
* size. Byte units will be as large as possible and the same units for
|
|
* current and max will be suppressed for the former.
|
|
*
|
|
* @param aCurrBytes
|
|
* Number of bytes transferred so far
|
|
* @param [optional] aMaxBytes
|
|
* Total number of bytes or -1 for unknown
|
|
* @return The transfer progress text
|
|
*/
|
|
getTransferTotal: function DU_getTransferTotal(aCurrBytes, aMaxBytes)
|
|
{
|
|
if (aMaxBytes == null)
|
|
aMaxBytes = -1;
|
|
|
|
let [progress, progressUnits] = DownloadUtils.convertByteUnits(aCurrBytes);
|
|
let [total, totalUnits] = DownloadUtils.convertByteUnits(aMaxBytes);
|
|
|
|
// Figure out which byte progress string to display
|
|
let transfer;
|
|
if (aMaxBytes < 0)
|
|
transfer = gStr.transferNoTotal;
|
|
else if (progressUnits == totalUnits)
|
|
transfer = gStr.transferSameUnits;
|
|
else
|
|
transfer = gStr.transferDiffUnits;
|
|
|
|
transfer = replaceInsert(transfer, 1, progress);
|
|
transfer = replaceInsert(transfer, 2, progressUnits);
|
|
transfer = replaceInsert(transfer, 3, total);
|
|
transfer = replaceInsert(transfer, 4, totalUnits);
|
|
|
|
return transfer;
|
|
},
|
|
|
|
/**
|
|
* Generate a "time left" string given an estimate on the time left and the
|
|
* last time. The extra time is used to give a better estimate on the time to
|
|
* show. Both the time values are doubles instead of integers to help get
|
|
* sub-second accuracy for current and future estimates.
|
|
*
|
|
* @param aSeconds
|
|
* Current estimate on number of seconds left for the download
|
|
* @param [optional] aLastSec
|
|
* Last time remaining in seconds or Infinity for unknown
|
|
* @return A pair: [time left text, new value of "last seconds"]
|
|
*/
|
|
getTimeLeft: function DU_getTimeLeft(aSeconds, aLastSec)
|
|
{
|
|
if (aLastSec == null)
|
|
aLastSec = Infinity;
|
|
|
|
if (aSeconds < 0)
|
|
return [gStr.timeUnknown, aLastSec];
|
|
|
|
// Try to find a cached lastSec for the given second
|
|
aLastSec = gCachedLast.reduce(function(aResult, aItem)
|
|
aItem[0] == aSeconds ? aItem[1] : aResult, aLastSec);
|
|
|
|
// Add the current second/lastSec pair unless we have too many
|
|
gCachedLast.push([aSeconds, aLastSec]);
|
|
if (gCachedLast.length > kCachedLastMaxSize)
|
|
gCachedLast.shift();
|
|
|
|
// Apply smoothing only if the new time isn't a huge change -- e.g., if the
|
|
// new time is more than half the previous time; this is useful for
|
|
// downloads that start/resume slowly
|
|
if (aSeconds > aLastSec / 2) {
|
|
// Apply hysteresis to favor downward over upward swings
|
|
// 30% of down and 10% of up (exponential smoothing)
|
|
let (diff = aSeconds - aLastSec) {
|
|
aSeconds = aLastSec + (diff < 0 ? .3 : .1) * diff;
|
|
}
|
|
|
|
// If the new time is similar, reuse something close to the last seconds,
|
|
// but subtract a little to provide forward progress
|
|
let diff = aSeconds - aLastSec;
|
|
let diffPct = diff / aLastSec * 100;
|
|
if (Math.abs(diff) < 5 || Math.abs(diffPct) < 5)
|
|
aSeconds = aLastSec - (diff < 0 ? .4 : .2);
|
|
}
|
|
|
|
// Decide what text to show for the time
|
|
let timeLeft;
|
|
if (aSeconds < 4) {
|
|
// Be friendly in the last few seconds
|
|
timeLeft = gStr.timeFewSeconds;
|
|
} else {
|
|
// Convert the seconds into its two largest units to display
|
|
let [time1, unit1, time2, unit2] =
|
|
DownloadUtils.convertTimeUnits(aSeconds);
|
|
|
|
let pair1 = replaceInsert(gStr.timePair, 1, time1);
|
|
pair1 = replaceInsert(pair1, 2, unit1);
|
|
let pair2 = replaceInsert(gStr.timePair, 1, time2);
|
|
pair2 = replaceInsert(pair2, 2, unit2);
|
|
|
|
// Only show minutes for under 1 hour unless there's a few minutes left;
|
|
// or the second pair is 0.
|
|
if ((aSeconds < 3600 && time1 >= 4) || time2 == 0) {
|
|
timeLeft = replaceInsert(gStr.timeLeftSingle, 1, pair1);
|
|
} else {
|
|
// We've got 2 pairs of times to display
|
|
timeLeft = replaceInsert(gStr.timeLeftDouble, 1, pair1);
|
|
timeLeft = replaceInsert(timeLeft, 2, pair2);
|
|
}
|
|
}
|
|
|
|
return [timeLeft, aSeconds];
|
|
},
|
|
|
|
/**
|
|
* Get the appropriate display host string for a URI string depending on if
|
|
* the URI has an eTLD + 1, is an IP address, a local file, or other protocol
|
|
*
|
|
* @param aURIString
|
|
* The URI string to try getting an eTLD + 1, etc.
|
|
* @return A pair: [display host for the URI string, full host name]
|
|
*/
|
|
getURIHost: function DU_getURIHost(aURIString)
|
|
{
|
|
let ioService = Cc["@mozilla.org/network/io-service;1"].
|
|
getService(Ci.nsIIOService);
|
|
let eTLDService = Cc["@mozilla.org/network/effective-tld-service;1"].
|
|
getService(Ci.nsIEffectiveTLDService);
|
|
let idnService = Cc["@mozilla.org/network/idn-service;1"].
|
|
getService(Ci.nsIIDNService);
|
|
|
|
// Get a URI that knows about its components
|
|
let uri = ioService.newURI(aURIString, null, null);
|
|
|
|
// Get the inner-most uri for schemes like jar:
|
|
if (uri instanceof Ci.nsINestedURI)
|
|
uri = uri.innermostURI;
|
|
|
|
let fullHost;
|
|
try {
|
|
// Get the full host name; some special URIs fail (data: jar:)
|
|
fullHost = uri.host;
|
|
} catch (e) {
|
|
fullHost = "";
|
|
}
|
|
|
|
let displayHost;
|
|
try {
|
|
// This might fail if it's an IP address or doesn't have more than 1 part
|
|
let baseDomain = eTLDService.getBaseDomain(uri);
|
|
|
|
// Convert base domain for display; ignore the isAscii out param
|
|
displayHost = idnService.convertToDisplayIDN(baseDomain, {});
|
|
} catch (e) {
|
|
// Default to the host name
|
|
displayHost = fullHost;
|
|
}
|
|
|
|
// Check if we need to show something else for the host
|
|
if (uri.scheme == "file") {
|
|
// Display special text for file protocol
|
|
displayHost = gStr.doneFileScheme;
|
|
fullHost = displayHost;
|
|
} else if (displayHost.length == 0) {
|
|
// Got nothing; show the scheme (data: about: moz-icon:)
|
|
displayHost = replaceInsert(gStr.doneScheme, 1, uri.scheme);
|
|
fullHost = displayHost;
|
|
} else if (uri.port != -1) {
|
|
// Tack on the port if it's not the default port
|
|
let port = ":" + uri.port;
|
|
displayHost += port;
|
|
fullHost += port;
|
|
}
|
|
|
|
return [displayHost, fullHost];
|
|
},
|
|
|
|
/**
|
|
* Converts a number of bytes to the appropriate unit that results in an
|
|
* internationalized number that needs fewer than 4 digits.
|
|
*
|
|
* @param aBytes
|
|
* Number of bytes to convert
|
|
* @return A pair: [new value with 3 sig. figs., its unit]
|
|
*/
|
|
convertByteUnits: function DU_convertByteUnits(aBytes)
|
|
{
|
|
let unitIndex = 0;
|
|
|
|
// Convert to next unit if it needs 4 digits (after rounding), but only if
|
|
// we know the name of the next unit
|
|
while ((aBytes >= 999.5) && (unitIndex < gStr.units.length - 1)) {
|
|
aBytes /= 1024;
|
|
unitIndex++;
|
|
}
|
|
|
|
// Get rid of insignificant bits by truncating to 1 or 0 decimal points
|
|
// 0 -> 0; 1.2 -> 1.2; 12.3 -> 12.3; 123.4 -> 123; 234.5 -> 235
|
|
aBytes = aBytes.toFixed((aBytes > 0) && (aBytes < 100) ? 1 : 0);
|
|
|
|
if (gDecimalSymbol != ".")
|
|
aBytes = aBytes.replace(".", gDecimalSymbol);
|
|
return [aBytes, gStr.units[unitIndex]];
|
|
},
|
|
|
|
/**
|
|
* Converts a number of seconds to the two largest units. Time values are
|
|
* whole numbers, and units have the correct plural/singular form.
|
|
*
|
|
* @param aSecs
|
|
* Seconds to convert into the appropriate 2 units
|
|
* @return 4-item array [first value, its unit, second value, its unit]
|
|
*/
|
|
convertTimeUnits: function DU_convertTimeUnits(aSecs)
|
|
{
|
|
// These are the maximum values for seconds, minutes, hours corresponding
|
|
// with gStr.timeUnits without the last item
|
|
let timeSize = [60, 60, 24];
|
|
|
|
let time = aSecs;
|
|
let scale = 1;
|
|
let unitIndex = 0;
|
|
|
|
// Keep converting to the next unit while we have units left and the
|
|
// current one isn't the largest unit possible
|
|
while ((unitIndex < timeSize.length) && (time >= timeSize[unitIndex])) {
|
|
time /= timeSize[unitIndex];
|
|
scale *= timeSize[unitIndex];
|
|
unitIndex++;
|
|
}
|
|
|
|
let value = convertTimeUnitsValue(time);
|
|
let units = convertTimeUnitsUnits(value, unitIndex);
|
|
|
|
let extra = aSecs - value * scale;
|
|
let nextIndex = unitIndex - 1;
|
|
|
|
// Convert the extra time to the next largest unit
|
|
for (let index = 0; index < nextIndex; index++)
|
|
extra /= timeSize[index];
|
|
|
|
let value2 = convertTimeUnitsValue(extra);
|
|
let units2 = convertTimeUnitsUnits(value2, nextIndex);
|
|
|
|
return [value, units, value2, units2];
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Private helper for convertTimeUnits that gets the display value of a time
|
|
*
|
|
* @param aTime
|
|
* Time value for display
|
|
* @return An integer value for the time rounded down
|
|
*/
|
|
function convertTimeUnitsValue(aTime)
|
|
{
|
|
return Math.floor(aTime);
|
|
}
|
|
|
|
/**
|
|
* Private helper for convertTimeUnits that gets the display units of a time
|
|
*
|
|
* @param aTime
|
|
* Time value for display
|
|
* @param aIndex
|
|
* Index into gStr.timeUnits for the appropriate unit
|
|
* @return The appropriate plural form of the unit for the time
|
|
*/
|
|
function convertTimeUnitsUnits(aTime, aIndex)
|
|
{
|
|
// Negative index would be an invalid unit, so just give empty
|
|
if (aIndex < 0)
|
|
return "";
|
|
|
|
return PluralForm.get(aTime, gStr.timeUnits[aIndex]);
|
|
}
|
|
|
|
/**
|
|
* Private helper function to replace a placeholder string with a real string
|
|
*
|
|
* @param aText
|
|
* Source text containing placeholder (e.g., #1)
|
|
* @param aIndex
|
|
* Index number of placeholder to replace
|
|
* @param aValue
|
|
* New string to put in place of placeholder
|
|
* @return The string with placeholder replaced with the new string
|
|
*/
|
|
function replaceInsert(aText, aIndex, aValue)
|
|
{
|
|
return aText.replace("#" + aIndex, aValue);
|
|
}
|
|
|
|
/**
|
|
* Private helper function to log errors to the error console and command line
|
|
*
|
|
* @param aMsg
|
|
* Error message to log or an array of strings to concat
|
|
*/
|
|
function log(aMsg)
|
|
{
|
|
let msg = "DownloadUtils.jsm: " + (aMsg.join ? aMsg.join("") : aMsg);
|
|
Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService).
|
|
logStringMessage(msg);
|
|
dump(msg + "\n");
|
|
}
|