зеркало из https://github.com/mozilla/gecko-dev.git
626 строки
20 KiB
JavaScript
626 строки
20 KiB
JavaScript
/* vim: sw=2 ts=2 sts=2 expandtab filetype=javascript
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
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 dateCompact, string dateComplete]
|
|
* getReadableDates(Date aDate, [optional] Date aNow)
|
|
*
|
|
* [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 MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
|
|
const BYTE_UNITS = [
|
|
"download-utils-bytes",
|
|
"download-utils-kilobyte",
|
|
"download-utils-megabyte",
|
|
"download-utils-gigabyte",
|
|
];
|
|
|
|
const TIME_UNITS = [
|
|
"download-utils-short-seconds",
|
|
"download-utils-short-minutes",
|
|
"download-utils-short-hours",
|
|
"download-utils-short-days",
|
|
];
|
|
|
|
// These are the maximum values for seconds, minutes, hours corresponding
|
|
// with TIME_UNITS without the last item
|
|
const TIME_SIZES = [60, 60, 24];
|
|
|
|
var localeNumberFormatCache = new Map();
|
|
function getLocaleNumberFormat(fractionDigits) {
|
|
if (!localeNumberFormatCache.has(fractionDigits)) {
|
|
localeNumberFormatCache.set(
|
|
fractionDigits,
|
|
new Services.intl.NumberFormat(undefined, {
|
|
maximumFractionDigits: fractionDigits,
|
|
minimumFractionDigits: fractionDigits,
|
|
})
|
|
);
|
|
}
|
|
return localeNumberFormatCache.get(fractionDigits);
|
|
}
|
|
|
|
const l10n = new Localization(["toolkit/downloads/downloadUtils.ftl"], true);
|
|
|
|
// Keep track of at most this many second/lastSec pairs so that multiple calls
|
|
// to getTimeLeft produce the same time left
|
|
const kCachedLastMaxSize = 10;
|
|
var gCachedLast = [];
|
|
|
|
var 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
|
|
) {
|
|
let [
|
|
transfer,
|
|
timeLeft,
|
|
newLast,
|
|
normalizedSpeed,
|
|
] = this._deriveTransferRate(aCurrBytes, aMaxBytes, aSpeed, aLastSec);
|
|
|
|
let [rate, unit] = DownloadUtils.convertByteUnits(normalizedSpeed);
|
|
|
|
let status;
|
|
if (rate === "Infinity") {
|
|
// Infinity download speed doesn't make sense. Show a localized phrase instead.
|
|
status = l10n.formatValueSync("download-utils-status-infinite-rate", {
|
|
transfer,
|
|
timeLeft,
|
|
});
|
|
} else {
|
|
status = l10n.formatValueSync("download-utils-status", {
|
|
transfer,
|
|
rate,
|
|
unit,
|
|
timeLeft,
|
|
});
|
|
}
|
|
return [status, newLast];
|
|
},
|
|
|
|
/**
|
|
* Generate a status string for a download given its current progress,
|
|
* total size, speed, last time remaining. The status string contains the
|
|
* time remaining, as well as the total bytes downloaded. Unlike
|
|
* getDownloadStatus, it does not include the rate of download.
|
|
*
|
|
* @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"]
|
|
*/
|
|
getDownloadStatusNoRate: function DU_getDownloadStatusNoRate(
|
|
aCurrBytes,
|
|
aMaxBytes,
|
|
aSpeed,
|
|
aLastSec
|
|
) {
|
|
let [transfer, timeLeft, newLast] = this._deriveTransferRate(
|
|
aCurrBytes,
|
|
aMaxBytes,
|
|
aSpeed,
|
|
aLastSec
|
|
);
|
|
|
|
let status = l10n.formatValueSync("download-utils-status-no-rate", {
|
|
transfer,
|
|
timeLeft,
|
|
});
|
|
return [status, newLast];
|
|
},
|
|
|
|
/**
|
|
* Helper function that returns a transfer string, a time remaining string,
|
|
* and a new value of "last seconds".
|
|
* @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 triple: [amount transferred string, time remaining string,
|
|
* new value of "last seconds"]
|
|
*/
|
|
_deriveTransferRate: function DU__deriveTransferRate(
|
|
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;
|
|
|
|
let transfer = DownloadUtils.getTransferTotal(aCurrBytes, aMaxBytes);
|
|
let [timeLeft, newLast] = DownloadUtils.getTimeLeft(seconds, aLastSec);
|
|
return [transfer, timeLeft, newLast, aSpeed];
|
|
},
|
|
|
|
/**
|
|
* 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 name;
|
|
if (aMaxBytes < 0) {
|
|
name = "download-utils-transfer-no-total";
|
|
} else if (progressUnits == totalUnits) {
|
|
name = "download-utils-transfer-same-units";
|
|
} else {
|
|
name = "download-utils-transfer-diff-units";
|
|
}
|
|
|
|
return l10n.formatValueSync(name, {
|
|
progress,
|
|
progressUnits,
|
|
total,
|
|
totalUnits,
|
|
});
|
|
},
|
|
|
|
/**
|
|
* 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) {
|
|
let nf = new Services.intl.NumberFormat();
|
|
if (aLastSec == null) {
|
|
aLastSec = Infinity;
|
|
}
|
|
|
|
if (aSeconds < 0) {
|
|
return [l10n.formatValueSync("download-utils-time-unknown"), aLastSec];
|
|
}
|
|
|
|
// Try to find a cached lastSec for the given second
|
|
aLastSec = gCachedLast.reduce(
|
|
(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 ? 0.3 : 0.1) * diff;
|
|
|
|
// If the new time is similar, reuse something close to the last seconds,
|
|
// but subtract a little to provide forward progress
|
|
let diffPct = (diff / aLastSec) * 100;
|
|
if (Math.abs(diff) < 5 || Math.abs(diffPct) < 5) {
|
|
aSeconds = aLastSec - (diff < 0 ? 0.4 : 0.2);
|
|
}
|
|
}
|
|
|
|
// Decide what text to show for the time
|
|
let timeLeft;
|
|
if (aSeconds < 4) {
|
|
// Be friendly in the last few seconds
|
|
timeLeft = l10n.formatValueSync("download-utils-time-few-seconds");
|
|
} else {
|
|
// Convert the seconds into its two largest units to display
|
|
let [time1, unit1, time2, unit2] = DownloadUtils.convertTimeUnits(
|
|
aSeconds
|
|
);
|
|
|
|
const pair1 = l10n.formatValueSync("download-utils-time-pair", {
|
|
time: nf.format(time1),
|
|
unit: unit1,
|
|
});
|
|
|
|
// 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 = l10n.formatValueSync("download-utils-time-left-single", {
|
|
time: pair1,
|
|
});
|
|
} else {
|
|
// We've got 2 pairs of times to display
|
|
const pair2 = l10n.formatValueSync("download-utils-time-pair", {
|
|
time: nf.format(time2),
|
|
unit: unit2,
|
|
});
|
|
timeLeft = l10n.formatValueSync("download-utils-time-left-double", {
|
|
time1: pair1,
|
|
time2: pair2,
|
|
});
|
|
}
|
|
}
|
|
|
|
return [timeLeft, aSeconds];
|
|
},
|
|
|
|
/**
|
|
* Converts a Date object to two readable formats, one compact, one complete.
|
|
* The compact format is relative to the current date, and is not an accurate
|
|
* representation. For example, only the time is displayed for today. The
|
|
* complete format always includes both the date and the time, excluding the
|
|
* seconds, and is often shown when hovering the cursor over the compact
|
|
* representation.
|
|
*
|
|
* @param aDate
|
|
* Date object representing the date and time to format. It is assumed
|
|
* that this value represents a past date.
|
|
* @param [optional] aNow
|
|
* Date object representing the current date and time. The real date
|
|
* and time of invocation is used if this parameter is omitted.
|
|
* @return A pair: [compact text, complete text]
|
|
*/
|
|
getReadableDates: function DU_getReadableDates(aDate, aNow) {
|
|
if (!aNow) {
|
|
aNow = new Date();
|
|
}
|
|
|
|
// Figure out when today begins
|
|
let today = new Date(aNow.getFullYear(), aNow.getMonth(), aNow.getDate());
|
|
|
|
let dateTimeCompact;
|
|
let dateTimeFull;
|
|
|
|
// Figure out if the time is from today, yesterday, this week, etc.
|
|
if (aDate >= today) {
|
|
let dts = new Services.intl.DateTimeFormat(undefined, {
|
|
timeStyle: "short",
|
|
});
|
|
dateTimeCompact = dts.format(aDate);
|
|
} else if (today - aDate < MS_PER_DAY) {
|
|
// After yesterday started, show yesterday
|
|
dateTimeCompact = l10n.formatValueSync("download-utils-yesterday");
|
|
} else if (today - aDate < 6 * MS_PER_DAY) {
|
|
// After last week started, show day of week
|
|
dateTimeCompact = aDate.toLocaleDateString(undefined, {
|
|
weekday: "long",
|
|
});
|
|
} else {
|
|
// Show month/day
|
|
dateTimeCompact = aDate.toLocaleString(undefined, {
|
|
month: "long",
|
|
day: "numeric",
|
|
});
|
|
}
|
|
|
|
const dtOptions = { dateStyle: "long", timeStyle: "short" };
|
|
dateTimeFull = new Services.intl.DateTimeFormat(
|
|
undefined,
|
|
dtOptions
|
|
).format(aDate);
|
|
|
|
return [dateTimeCompact, dateTimeFull];
|
|
},
|
|
|
|
/**
|
|
* 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 idnService = Cc["@mozilla.org/network/idn-service;1"].getService(
|
|
Ci.nsIIDNService
|
|
);
|
|
|
|
// Get a URI that knows about its components
|
|
let uri;
|
|
try {
|
|
uri = Services.io.newURI(aURIString);
|
|
} catch (ex) {
|
|
return ["", ""];
|
|
}
|
|
|
|
// Get the inner-most uri for schemes like jar:
|
|
if (uri instanceof Ci.nsINestedURI) {
|
|
uri = uri.innermostURI;
|
|
}
|
|
|
|
if (uri.scheme == "blob") {
|
|
let origin = new URL(uri.spec).origin;
|
|
// Origin can be "null" for blob URIs from a sandbox.
|
|
if (origin != "null") {
|
|
// `newURI` can throw (like for null) and throwing here breaks...
|
|
// a lot of stuff. So let's avoid doing that in case there are other
|
|
// edgecases we're missing here.
|
|
try {
|
|
uri = Services.io.newURI(origin);
|
|
} catch (ex) {
|
|
console.error(ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
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 = Services.eTLD.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 = l10n.formatValueSync("download-utils-done-file-scheme");
|
|
fullHost = displayHost;
|
|
} else if (!displayHost.length) {
|
|
// Got nothing; show the scheme (data: about: moz-icon:)
|
|
displayHost = l10n.formatValueSync("download-utils-done-scheme", {
|
|
scheme: 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 < BYTE_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
|
|
// added in bug 462064: (unitIndex != 0) makes sure that no decimal digit for bytes appears when aBytes < 100
|
|
let fractionDigits = aBytes > 0 && aBytes < 100 && unitIndex != 0 ? 1 : 0;
|
|
|
|
// Don't try to format Infinity values using NumberFormat.
|
|
if (aBytes === Infinity) {
|
|
aBytes = "Infinity";
|
|
} else {
|
|
aBytes = getLocaleNumberFormat(fractionDigits).format(aBytes);
|
|
}
|
|
|
|
return [aBytes, l10n.formatValueSync(BYTE_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) {
|
|
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 < TIME_SIZES.length && time >= TIME_SIZES[unitIndex]) {
|
|
time /= TIME_SIZES[unitIndex];
|
|
scale *= TIME_SIZES[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 /= TIME_SIZES[index];
|
|
}
|
|
|
|
let value2 = convertTimeUnitsValue(extra);
|
|
let units2 = convertTimeUnitsUnits(value2, nextIndex);
|
|
|
|
return [value, units, value2, units2];
|
|
},
|
|
|
|
/**
|
|
* Converts a number of seconds to "downloading file opens in X" status.
|
|
* @param aSeconds
|
|
* Seconds to convert into the time format.
|
|
* @return status object, example:
|
|
* status = {
|
|
* l10n: {
|
|
* id: "downloading-file-opens-in-minutes-and-seconds",
|
|
* args: { minutes: 2, seconds: 30 },
|
|
* },
|
|
* };
|
|
*/
|
|
getFormattedTimeStatus: function DU_getFormattedTimeStatus(aSeconds) {
|
|
aSeconds = Math.floor(aSeconds);
|
|
let l10n;
|
|
if (!isFinite(aSeconds) || aSeconds < 0) {
|
|
l10n = {
|
|
id: "downloading-file-opens-in-some-time-2",
|
|
};
|
|
} else if (aSeconds < 60) {
|
|
l10n = {
|
|
id: "downloading-file-opens-in-seconds-2",
|
|
args: { seconds: aSeconds },
|
|
};
|
|
} else if (aSeconds < 3600) {
|
|
let minutes = Math.floor(aSeconds / 60);
|
|
let seconds = aSeconds % 60;
|
|
l10n = seconds
|
|
? {
|
|
args: { seconds, minutes },
|
|
id: "downloading-file-opens-in-minutes-and-seconds-2",
|
|
}
|
|
: { args: { minutes }, id: "downloading-file-opens-in-minutes-2" };
|
|
} else {
|
|
let hours = Math.floor(aSeconds / 3600);
|
|
let minutes = Math.floor((aSeconds % 3600) / 60);
|
|
l10n = {
|
|
args: { hours, minutes },
|
|
id: "downloading-file-opens-in-hours-and-minutes-2",
|
|
};
|
|
}
|
|
return { l10n };
|
|
},
|
|
};
|
|
|
|
/**
|
|
* 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 timeValue
|
|
* 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(timeValue, aIndex) {
|
|
// Negative index would be an invalid unit, so just give empty
|
|
if (aIndex < 0) {
|
|
return "";
|
|
}
|
|
|
|
return l10n.formatValueSync(TIME_UNITS[aIndex], { timeValue });
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
// Services.console.logStringMessage(msg);
|
|
// dump(msg + "\n");
|
|
// }
|