Source code

Revision control

Other Tools

1
/* vim: sw=2 ts=2 sts=2 expandtab filetype=javascript
2
* This Source Code Form is subject to the terms of the Mozilla Public
3
* License, v. 2.0. If a copy of the MPL was not distributed with this
4
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5
6
"use strict";
7
8
var EXPORTED_SYMBOLS = [ "DownloadUtils" ];
9
10
/**
11
* This module provides the DownloadUtils object which contains useful methods
12
* for downloads such as displaying file sizes, transfer times, and download
13
* locations.
14
*
15
* List of methods:
16
*
17
* [string status, double newLast]
18
* getDownloadStatus(int aCurrBytes, [optional] int aMaxBytes,
19
* [optional] double aSpeed, [optional] double aLastSec)
20
*
21
* string progress
22
* getTransferTotal(int aCurrBytes, [optional] int aMaxBytes)
23
*
24
* [string timeLeft, double newLast]
25
* getTimeLeft(double aSeconds, [optional] double aLastSec)
26
*
27
* [string dateCompact, string dateComplete]
28
* getReadableDates(Date aDate, [optional] Date aNow)
29
*
30
* [string displayHost, string fullHost]
31
* getURIHost(string aURIString)
32
*
33
* [string convertedBytes, string units]
34
* convertByteUnits(int aBytes)
35
*
36
* [int time, string units, int subTime, string subUnits]
37
* convertTimeUnits(double aSecs)
38
*/
39
40
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
41
42
ChromeUtils.defineModuleGetter(this, "PluralForm",
44
45
const MS_PER_DAY = 24 * 60 * 60 * 1000;
46
47
var localeNumberFormatCache = new Map();
48
function getLocaleNumberFormat(fractionDigits) {
49
if (!localeNumberFormatCache.has(fractionDigits)) {
50
localeNumberFormatCache.set(fractionDigits,
51
new Services.intl.NumberFormat(undefined,
52
{ maximumFractionDigits: fractionDigits,
53
minimumFractionDigits: fractionDigits }));
54
}
55
return localeNumberFormatCache.get(fractionDigits);
56
}
57
58
const kDownloadProperties =
60
61
var gStr = {
62
statusFormat: "statusFormat3",
63
statusFormatInfiniteRate: "statusFormatInfiniteRate",
64
statusFormatNoRate: "statusFormatNoRate",
65
transferSameUnits: "transferSameUnits2",
66
transferDiffUnits: "transferDiffUnits2",
67
transferNoTotal: "transferNoTotal2",
68
timePair: "timePair3",
69
timeLeftSingle: "timeLeftSingle3",
70
timeLeftDouble: "timeLeftDouble3",
71
timeFewSeconds: "timeFewSeconds2",
72
timeUnknown: "timeUnknown2",
73
yesterday: "yesterday",
74
doneScheme: "doneScheme2",
75
doneFileScheme: "doneFileScheme",
76
units: ["bytes", "kilobyte", "megabyte", "gigabyte"],
77
// Update timeSize in convertTimeUnits if changing the length of this array
78
timeUnits: ["shortSeconds", "shortMinutes", "shortHours", "shortDays"],
79
infiniteRate: "infiniteRate",
80
};
81
82
// This lazily initializes the string bundle upon first use.
83
Object.defineProperty(this, "gBundle", {
84
configurable: true,
85
enumerable: true,
86
get() {
87
delete this.gBundle;
88
return this.gBundle = Services.strings.createBundle(kDownloadProperties);
89
},
90
});
91
92
// Keep track of at most this many second/lastSec pairs so that multiple calls
93
// to getTimeLeft produce the same time left
94
const kCachedLastMaxSize = 10;
95
var gCachedLast = [];
96
97
var DownloadUtils = {
98
/**
99
* Generate a full status string for a download given its current progress,
100
* total size, speed, last time remaining
101
*
102
* @param aCurrBytes
103
* Number of bytes transferred so far
104
* @param [optional] aMaxBytes
105
* Total number of bytes or -1 for unknown
106
* @param [optional] aSpeed
107
* Current transfer rate in bytes/sec or -1 for unknown
108
* @param [optional] aLastSec
109
* Last time remaining in seconds or Infinity for unknown
110
* @return A pair: [download status text, new value of "last seconds"]
111
*/
112
getDownloadStatus: function DU_getDownloadStatus(aCurrBytes, aMaxBytes,
113
aSpeed, aLastSec) {
114
let [transfer, timeLeft, newLast, normalizedSpeed]
115
= this._deriveTransferRate(aCurrBytes, aMaxBytes, aSpeed, aLastSec);
116
117
let [rate, unit] = DownloadUtils.convertByteUnits(normalizedSpeed);
118
119
let status;
120
if (rate === "Infinity") {
121
// Infinity download speed doesn't make sense. Show a localized phrase instead.
122
let params = [transfer, gBundle.GetStringFromName(gStr.infiniteRate), timeLeft];
123
status = gBundle.formatStringFromName(gStr.statusFormatInfiniteRate, params);
124
} else {
125
let params = [transfer, rate, unit, timeLeft];
126
status = gBundle.formatStringFromName(gStr.statusFormat, params);
127
}
128
return [status, newLast];
129
},
130
131
/**
132
* Generate a status string for a download given its current progress,
133
* total size, speed, last time remaining. The status string contains the
134
* time remaining, as well as the total bytes downloaded. Unlike
135
* getDownloadStatus, it does not include the rate of download.
136
*
137
* @param aCurrBytes
138
* Number of bytes transferred so far
139
* @param [optional] aMaxBytes
140
* Total number of bytes or -1 for unknown
141
* @param [optional] aSpeed
142
* Current transfer rate in bytes/sec or -1 for unknown
143
* @param [optional] aLastSec
144
* Last time remaining in seconds or Infinity for unknown
145
* @return A pair: [download status text, new value of "last seconds"]
146
*/
147
getDownloadStatusNoRate:
148
function DU_getDownloadStatusNoRate(aCurrBytes, aMaxBytes, aSpeed,
149
aLastSec) {
150
let [transfer, timeLeft, newLast]
151
= this._deriveTransferRate(aCurrBytes, aMaxBytes, aSpeed, aLastSec);
152
153
let params = [transfer, timeLeft];
154
let status = gBundle.formatStringFromName(gStr.statusFormatNoRate, params);
155
return [status, newLast];
156
},
157
158
/**
159
* Helper function that returns a transfer string, a time remaining string,
160
* and a new value of "last seconds".
161
* @param aCurrBytes
162
* Number of bytes transferred so far
163
* @param [optional] aMaxBytes
164
* Total number of bytes or -1 for unknown
165
* @param [optional] aSpeed
166
* Current transfer rate in bytes/sec or -1 for unknown
167
* @param [optional] aLastSec
168
* Last time remaining in seconds or Infinity for unknown
169
* @return A triple: [amount transferred string, time remaining string,
170
* new value of "last seconds"]
171
*/
172
_deriveTransferRate: function DU__deriveTransferRate(aCurrBytes,
173
aMaxBytes, aSpeed,
174
aLastSec) {
175
if (aMaxBytes == null)
176
aMaxBytes = -1;
177
if (aSpeed == null)
178
aSpeed = -1;
179
if (aLastSec == null)
180
aLastSec = Infinity;
181
182
// Calculate the time remaining if we have valid values
183
let seconds = (aSpeed > 0) && (aMaxBytes > 0) ?
184
(aMaxBytes - aCurrBytes) / aSpeed : -1;
185
186
let transfer = DownloadUtils.getTransferTotal(aCurrBytes, aMaxBytes);
187
let [timeLeft, newLast] = DownloadUtils.getTimeLeft(seconds, aLastSec);
188
return [transfer, timeLeft, newLast, aSpeed];
189
},
190
191
/**
192
* Generate the transfer progress string to show the current and total byte
193
* size. Byte units will be as large as possible and the same units for
194
* current and max will be suppressed for the former.
195
*
196
* @param aCurrBytes
197
* Number of bytes transferred so far
198
* @param [optional] aMaxBytes
199
* Total number of bytes or -1 for unknown
200
* @return The transfer progress text
201
*/
202
getTransferTotal: function DU_getTransferTotal(aCurrBytes, aMaxBytes) {
203
if (aMaxBytes == null)
204
aMaxBytes = -1;
205
206
let [progress, progressUnits] = DownloadUtils.convertByteUnits(aCurrBytes);
207
let [total, totalUnits] = DownloadUtils.convertByteUnits(aMaxBytes);
208
209
// Figure out which byte progress string to display
210
let name, values;
211
if (aMaxBytes < 0) {
212
name = gStr.transferNoTotal;
213
values = [
214
progress,
215
progressUnits,
216
];
217
} else if (progressUnits == totalUnits) {
218
name = gStr.transferSameUnits;
219
values = [
220
progress,
221
total,
222
totalUnits,
223
];
224
} else {
225
name = gStr.transferDiffUnits;
226
values = [
227
progress,
228
progressUnits,
229
total,
230
totalUnits,
231
];
232
}
233
234
return gBundle.formatStringFromName(name, values);
235
},
236
237
/**
238
* Generate a "time left" string given an estimate on the time left and the
239
* last time. The extra time is used to give a better estimate on the time to
240
* show. Both the time values are doubles instead of integers to help get
241
* sub-second accuracy for current and future estimates.
242
*
243
* @param aSeconds
244
* Current estimate on number of seconds left for the download
245
* @param [optional] aLastSec
246
* Last time remaining in seconds or Infinity for unknown
247
* @return A pair: [time left text, new value of "last seconds"]
248
*/
249
getTimeLeft: function DU_getTimeLeft(aSeconds, aLastSec) {
250
let nf = new Services.intl.NumberFormat();
251
if (aLastSec == null)
252
aLastSec = Infinity;
253
254
if (aSeconds < 0)
255
return [gBundle.GetStringFromName(gStr.timeUnknown), aLastSec];
256
257
// Try to find a cached lastSec for the given second
258
aLastSec = gCachedLast.reduce((aResult, aItem) =>
259
aItem[0] == aSeconds ? aItem[1] : aResult, aLastSec);
260
261
// Add the current second/lastSec pair unless we have too many
262
gCachedLast.push([aSeconds, aLastSec]);
263
if (gCachedLast.length > kCachedLastMaxSize)
264
gCachedLast.shift();
265
266
// Apply smoothing only if the new time isn't a huge change -- e.g., if the
267
// new time is more than half the previous time; this is useful for
268
// downloads that start/resume slowly
269
if (aSeconds > aLastSec / 2) {
270
// Apply hysteresis to favor downward over upward swings
271
// 30% of down and 10% of up (exponential smoothing)
272
let diff = aSeconds - aLastSec;
273
aSeconds = aLastSec + (diff < 0 ? .3 : .1) * diff;
274
275
// If the new time is similar, reuse something close to the last seconds,
276
// but subtract a little to provide forward progress
277
let diffPct = diff / aLastSec * 100;
278
if (Math.abs(diff) < 5 || Math.abs(diffPct) < 5)
279
aSeconds = aLastSec - (diff < 0 ? .4 : .2);
280
}
281
282
// Decide what text to show for the time
283
let timeLeft;
284
if (aSeconds < 4) {
285
// Be friendly in the last few seconds
286
timeLeft = gBundle.GetStringFromName(gStr.timeFewSeconds);
287
} else {
288
// Convert the seconds into its two largest units to display
289
let [time1, unit1, time2, unit2] =
290
DownloadUtils.convertTimeUnits(aSeconds);
291
292
let pair1 =
293
gBundle.formatStringFromName(gStr.timePair, [nf.format(time1), unit1]);
294
let pair2 =
295
gBundle.formatStringFromName(gStr.timePair, [nf.format(time2), unit2]);
296
297
// Only show minutes for under 1 hour unless there's a few minutes left;
298
// or the second pair is 0.
299
if ((aSeconds < 3600 && time1 >= 4) || time2 == 0) {
300
timeLeft = gBundle.formatStringFromName(gStr.timeLeftSingle,
301
[pair1]);
302
} else {
303
// We've got 2 pairs of times to display
304
timeLeft = gBundle.formatStringFromName(gStr.timeLeftDouble,
305
[pair1, pair2]);
306
}
307
}
308
309
return [timeLeft, aSeconds];
310
},
311
312
/**
313
* Converts a Date object to two readable formats, one compact, one complete.
314
* The compact format is relative to the current date, and is not an accurate
315
* representation. For example, only the time is displayed for today. The
316
* complete format always includes both the date and the time, excluding the
317
* seconds, and is often shown when hovering the cursor over the compact
318
* representation.
319
*
320
* @param aDate
321
* Date object representing the date and time to format. It is assumed
322
* that this value represents a past date.
323
* @param [optional] aNow
324
* Date object representing the current date and time. The real date
325
* and time of invocation is used if this parameter is omitted.
326
* @return A pair: [compact text, complete text]
327
*/
328
getReadableDates: function DU_getReadableDates(aDate, aNow) {
329
if (!aNow) {
330
aNow = new Date();
331
}
332
333
// Figure out when today begins
334
let today = new Date(aNow.getFullYear(), aNow.getMonth(), aNow.getDate());
335
336
let dateTimeCompact;
337
let dateTimeFull;
338
339
// Figure out if the time is from today, yesterday, this week, etc.
340
if (aDate >= today) {
341
let dts = new Services.intl.DateTimeFormat(undefined, {
342
timeStyle: "short",
343
});
344
dateTimeCompact = dts.format(aDate);
345
} else if (today - aDate < (MS_PER_DAY)) {
346
// After yesterday started, show yesterday
347
dateTimeCompact = gBundle.GetStringFromName(gStr.yesterday);
348
} else if (today - aDate < (6 * MS_PER_DAY)) {
349
// After last week started, show day of week
350
dateTimeCompact = aDate.toLocaleDateString(undefined, { weekday: "long" });
351
} else {
352
// Show month/day
353
dateTimeCompact = aDate.toLocaleString(undefined, {
354
month: "long",
355
day: "numeric",
356
});
357
}
358
359
const dtOptions = { dateStyle: "long", timeStyle: "short" };
360
dateTimeFull =
361
new Services.intl.DateTimeFormat(undefined, dtOptions).format(aDate);
362
363
return [dateTimeCompact, dateTimeFull];
364
},
365
366
/**
367
* Get the appropriate display host string for a URI string depending on if
368
* the URI has an eTLD + 1, is an IP address, a local file, or other protocol
369
*
370
* @param aURIString
371
* The URI string to try getting an eTLD + 1, etc.
372
* @return A pair: [display host for the URI string, full host name]
373
*/
374
getURIHost: function DU_getURIHost(aURIString) {
375
let idnService = Cc["@mozilla.org/network/idn-service;1"].
376
getService(Ci.nsIIDNService);
377
378
// Get a URI that knows about its components
379
let uri;
380
try {
381
uri = Services.io.newURI(aURIString);
382
} catch (ex) {
383
return ["", ""];
384
}
385
386
// Get the inner-most uri for schemes like jar:
387
if (uri instanceof Ci.nsINestedURI)
388
uri = uri.innermostURI;
389
390
let fullHost;
391
try {
392
// Get the full host name; some special URIs fail (data: jar:)
393
fullHost = uri.host;
394
} catch (e) {
395
fullHost = "";
396
}
397
398
let displayHost;
399
try {
400
// This might fail if it's an IP address or doesn't have more than 1 part
401
let baseDomain = Services.eTLD.getBaseDomain(uri);
402
403
// Convert base domain for display; ignore the isAscii out param
404
displayHost = idnService.convertToDisplayIDN(baseDomain, {});
405
} catch (e) {
406
// Default to the host name
407
displayHost = fullHost;
408
}
409
410
// Check if we need to show something else for the host
411
if (uri.scheme == "file") {
412
// Display special text for file protocol
413
displayHost = gBundle.GetStringFromName(gStr.doneFileScheme);
414
fullHost = displayHost;
415
} else if (displayHost.length == 0) {
416
// Got nothing; show the scheme (data: about: moz-icon:)
417
displayHost =
418
gBundle.formatStringFromName(gStr.doneScheme, [uri.scheme]);
419
fullHost = displayHost;
420
} else if (uri.port != -1) {
421
// Tack on the port if it's not the default port
422
let port = ":" + uri.port;
423
displayHost += port;
424
fullHost += port;
425
}
426
427
return [displayHost, fullHost];
428
},
429
430
/**
431
* Converts a number of bytes to the appropriate unit that results in an
432
* internationalized number that needs fewer than 4 digits.
433
*
434
* @param aBytes
435
* Number of bytes to convert
436
* @return A pair: [new value with 3 sig. figs., its unit]
437
*/
438
convertByteUnits: function DU_convertByteUnits(aBytes) {
439
let unitIndex = 0;
440
441
// Convert to next unit if it needs 4 digits (after rounding), but only if
442
// we know the name of the next unit
443
while ((aBytes >= 999.5) && (unitIndex < gStr.units.length - 1)) {
444
aBytes /= 1024;
445
unitIndex++;
446
}
447
448
// Get rid of insignificant bits by truncating to 1 or 0 decimal points
449
// 0 -> 0; 1.2 -> 1.2; 12.3 -> 12.3; 123.4 -> 123; 234.5 -> 235
450
// added in bug 462064: (unitIndex != 0) makes sure that no decimal digit for bytes appears when aBytes < 100
451
let fractionDigits = (aBytes > 0) && (aBytes < 100) && (unitIndex != 0) ? 1 : 0;
452
453
// Don't try to format Infinity values using NumberFormat.
454
if (aBytes === Infinity) {
455
aBytes = "Infinity";
456
} else {
457
aBytes = getLocaleNumberFormat(fractionDigits).format(aBytes);
458
}
459
460
return [aBytes, gBundle.GetStringFromName(gStr.units[unitIndex])];
461
},
462
463
/**
464
* Converts a number of seconds to the two largest units. Time values are
465
* whole numbers, and units have the correct plural/singular form.
466
*
467
* @param aSecs
468
* Seconds to convert into the appropriate 2 units
469
* @return 4-item array [first value, its unit, second value, its unit]
470
*/
471
convertTimeUnits: function DU_convertTimeUnits(aSecs) {
472
// These are the maximum values for seconds, minutes, hours corresponding
473
// with gStr.timeUnits without the last item
474
let timeSize = [60, 60, 24];
475
476
let time = aSecs;
477
let scale = 1;
478
let unitIndex = 0;
479
480
// Keep converting to the next unit while we have units left and the
481
// current one isn't the largest unit possible
482
while ((unitIndex < timeSize.length) && (time >= timeSize[unitIndex])) {
483
time /= timeSize[unitIndex];
484
scale *= timeSize[unitIndex];
485
unitIndex++;
486
}
487
488
let value = convertTimeUnitsValue(time);
489
let units = convertTimeUnitsUnits(value, unitIndex);
490
491
let extra = aSecs - value * scale;
492
let nextIndex = unitIndex - 1;
493
494
// Convert the extra time to the next largest unit
495
for (let index = 0; index < nextIndex; index++)
496
extra /= timeSize[index];
497
498
let value2 = convertTimeUnitsValue(extra);
499
let units2 = convertTimeUnitsUnits(value2, nextIndex);
500
501
return [value, units, value2, units2];
502
},
503
};
504
505
/**
506
* Private helper for convertTimeUnits that gets the display value of a time
507
*
508
* @param aTime
509
* Time value for display
510
* @return An integer value for the time rounded down
511
*/
512
function convertTimeUnitsValue(aTime) {
513
return Math.floor(aTime);
514
}
515
516
/**
517
* Private helper for convertTimeUnits that gets the display units of a time
518
*
519
* @param aTime
520
* Time value for display
521
* @param aIndex
522
* Index into gStr.timeUnits for the appropriate unit
523
* @return The appropriate plural form of the unit for the time
524
*/
525
function convertTimeUnitsUnits(aTime, aIndex) {
526
// Negative index would be an invalid unit, so just give empty
527
if (aIndex < 0)
528
return "";
529
530
return PluralForm.get(aTime, gBundle.GetStringFromName(gStr.timeUnits[aIndex]));
531
}
532
533
/**
534
* Private helper function to log errors to the error console and command line
535
*
536
* @param aMsg
537
* Error message to log or an array of strings to concat
538
*/
539
// function log(aMsg) {
540
// let msg = "DownloadUtils.jsm: " + (aMsg.join ? aMsg.join("") : aMsg);
541
// Services.console.logStringMessage(msg);
542
// dump(msg + "\n");
543
// }