Fri, 16 Jan 2015 18:13:44 +0100
Integrate suggestion from review to improve consistency with existing code.
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 /**
6 * Code shared by OS.File front-ends.
7 *
8 * This code is meant to be included by another library. It is also meant to
9 * be executed only on a worker thread.
10 */
12 if (typeof Components != "undefined") {
13 throw new Error("osfile_shared_front.jsm cannot be used from the main thread");
14 }
15 (function(exports) {
17 let SharedAll =
18 require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
19 let Path = require("resource://gre/modules/osfile/ospath.jsm");
20 let Lz4 =
21 require("resource://gre/modules/workers/lz4.js");
22 let LOG = SharedAll.LOG.bind(SharedAll, "Shared front-end");
23 let clone = SharedAll.clone;
25 /**
26 * Code shared by implementations of File.
27 *
28 * @param {*} fd An OS-specific file handle.
29 * @param {string} path File path of the file handle, used for error-reporting.
30 * @constructor
31 */
32 let AbstractFile = function AbstractFile(fd, path) {
33 this._fd = fd;
34 if (!path) {
35 throw new TypeError("path is expected");
36 }
37 this._path = path;
38 };
40 AbstractFile.prototype = {
41 /**
42 * Return the file handle.
43 *
44 * @throw OS.File.Error if the file has been closed.
45 */
46 get fd() {
47 if (this._fd) {
48 return this._fd;
49 }
50 throw OS.File.Error.closed("accessing file", this._path);
51 },
52 /**
53 * Read bytes from this file to a new buffer.
54 *
55 * @param {number=} bytes If unspecified, read all the remaining bytes from
56 * this file. If specified, read |bytes| bytes, or less if the file does notclone
57 * contain that many bytes.
58 * @param {JSON} options
59 * @return {Uint8Array} An array containing the bytes read.
60 */
61 read: function read(bytes, options = {}) {
62 options = clone(options);
63 options.bytes = bytes == null ? this.stat().size : bytes;
64 let buffer = new Uint8Array(options.bytes);
65 let size = this.readTo(buffer, options);
66 if (size == options.bytes) {
67 return buffer;
68 } else {
69 return buffer.subarray(0, size);
70 }
71 },
73 /**
74 * Read bytes from this file to an existing buffer.
75 *
76 * Note that, by default, this function may perform several I/O
77 * operations to ensure that the buffer is as full as possible.
78 *
79 * @param {Typed Array | C pointer} buffer The buffer in which to
80 * store the bytes. The buffer must be large enough to
81 * accomodate |bytes| bytes.
82 * @param {*=} options Optionally, an object that may contain the
83 * following fields:
84 * - {number} bytes The number of |bytes| to write from the buffer. If
85 * unspecified, this is |buffer.byteLength|. Note that |bytes| is required
86 * if |buffer| is a C pointer.
87 *
88 * @return {number} The number of bytes actually read, which may be
89 * less than |bytes| if the file did not contain that many bytes left.
90 */
91 readTo: function readTo(buffer, options = {}) {
92 let {ptr, bytes} = SharedAll.normalizeToPointer(buffer, options.bytes);
93 let pos = 0;
94 while (pos < bytes) {
95 let chunkSize = this._read(ptr, bytes - pos, options);
96 if (chunkSize == 0) {
97 break;
98 }
99 pos += chunkSize;
100 ptr = SharedAll.offsetBy(ptr, chunkSize);
101 }
103 return pos;
104 },
106 /**
107 * Write bytes from a buffer to this file.
108 *
109 * Note that, by default, this function may perform several I/O
110 * operations to ensure that the buffer is fully written.
111 *
112 * @param {Typed array | C pointer} buffer The buffer in which the
113 * the bytes are stored. The buffer must be large enough to
114 * accomodate |bytes| bytes.
115 * @param {*=} options Optionally, an object that may contain the
116 * following fields:
117 * - {number} bytes The number of |bytes| to write from the buffer. If
118 * unspecified, this is |buffer.byteLength|. Note that |bytes| is required
119 * if |buffer| is a C pointer.
120 *
121 * @return {number} The number of bytes actually written.
122 */
123 write: function write(buffer, options = {}) {
125 let {ptr, bytes} =
126 SharedAll.normalizeToPointer(buffer, options.bytes || undefined);
128 let pos = 0;
129 while (pos < bytes) {
130 let chunkSize = this._write(ptr, bytes - pos, options);
131 pos += chunkSize;
132 ptr = SharedAll.offsetBy(ptr, chunkSize);
133 }
134 return pos;
135 }
136 };
138 /**
139 * Creates and opens a file with a unique name. By default, generate a random HEX number and use it to create a unique new file name.
140 *
141 * @param {string} path The path to the file.
142 * @param {*=} options Additional options for file opening. This
143 * implementation interprets the following fields:
144 *
145 * - {number} humanReadable If |true|, create a new filename appending a decimal number. ie: filename-1.ext, filename-2.ext.
146 * If |false| use HEX numbers ie: filename-A65BC0.ext
147 * - {number} maxReadableNumber Used to limit the amount of tries after a failed
148 * file creation. Default is 20.
149 *
150 * @return {Object} contains A file object{file} and the path{path}.
151 * @throws {OS.File.Error} If the file could not be opened.
152 */
153 AbstractFile.openUnique = function openUnique(path, options = {}) {
154 let mode = {
155 create : true
156 };
158 let dirName = Path.dirname(path);
159 let leafName = Path.basename(path);
160 let lastDotCharacter = leafName.lastIndexOf('.');
161 let fileName = leafName.substring(0, lastDotCharacter != -1 ? lastDotCharacter : leafName.length);
162 let suffix = (lastDotCharacter != -1 ? leafName.substring(lastDotCharacter) : "");
163 let uniquePath = "";
164 let maxAttempts = options.maxAttempts || 99;
165 let humanReadable = !!options.humanReadable;
166 const HEX_RADIX = 16;
167 // We produce HEX numbers between 0 and 2^24 - 1.
168 const MAX_HEX_NUMBER = 16777215;
170 try {
171 return {
172 path: path,
173 file: OS.File.open(path, mode)
174 };
175 } catch (ex if ex instanceof OS.File.Error && ex.becauseExists) {
176 for (let i = 0; i < maxAttempts; ++i) {
177 try {
178 if (humanReadable) {
179 uniquePath = Path.join(dirName, fileName + "-" + (i + 1) + suffix);
180 } else {
181 let hexNumber = Math.floor(Math.random() * MAX_HEX_NUMBER).toString(HEX_RADIX);
182 uniquePath = Path.join(dirName, fileName + "-" + hexNumber + suffix);
183 }
184 return {
185 path: uniquePath,
186 file: OS.File.open(uniquePath, mode)
187 };
188 } catch (ex if ex instanceof OS.File.Error && ex.becauseExists) {
189 // keep trying ...
190 }
191 }
192 throw OS.File.Error.exists("could not find an unused file name.", path);
193 }
194 };
196 /**
197 * Code shared by iterators.
198 */
199 AbstractFile.AbstractIterator = function AbstractIterator() {
200 };
201 AbstractFile.AbstractIterator.prototype = {
202 /**
203 * Allow iterating with |for|
204 */
205 __iterator__: function __iterator__() {
206 return this;
207 },
208 /**
209 * Apply a function to all elements of the directory sequentially.
210 *
211 * @param {Function} cb This function will be applied to all entries
212 * of the directory. It receives as arguments
213 * - the OS.File.Entry corresponding to the entry;
214 * - the index of the entry in the enumeration;
215 * - the iterator itself - calling |close| on the iterator stops
216 * the loop.
217 */
218 forEach: function forEach(cb) {
219 let index = 0;
220 for (let entry in this) {
221 cb(entry, index++, this);
222 }
223 },
224 /**
225 * Return several entries at once.
226 *
227 * Entries are returned in the same order as a walk with |forEach| or
228 * |for(...)|.
229 *
230 * @param {number=} length If specified, the number of entries
231 * to return. If unspecified, return all remaining entries.
232 * @return {Array} An array containing the next |length| entries, or
233 * less if the iteration contains less than |length| entries left.
234 */
235 nextBatch: function nextBatch(length) {
236 let array = [];
237 let i = 0;
238 for (let entry in this) {
239 array.push(entry);
240 if (++i >= length) {
241 return array;
242 }
243 }
244 return array;
245 }
246 };
248 /**
249 * Utility function shared by implementations of |OS.File.open|:
250 * extract read/write/trunc/create/existing flags from a |mode|
251 * object.
252 *
253 * @param {*=} mode An object that may contain fields |read|,
254 * |write|, |truncate|, |create|, |existing|. These fields
255 * are interpreted only if true-ish.
256 * @return {{read:bool, write:bool, trunc:bool, create:bool,
257 * existing:bool}} an object recapitulating the options set
258 * by |mode|.
259 * @throws {TypeError} If |mode| contains other fields, or
260 * if it contains both |create| and |truncate|, or |create|
261 * and |existing|.
262 */
263 AbstractFile.normalizeOpenMode = function normalizeOpenMode(mode) {
264 let result = {
265 read: false,
266 write: false,
267 trunc: false,
268 create: false,
269 existing: false,
270 append: true
271 };
272 for (let key in mode) {
273 let val = !!mode[key]; // bool cast.
274 switch (key) {
275 case "read":
276 result.read = val;
277 break;
278 case "write":
279 result.write = val;
280 break;
281 case "truncate": // fallthrough
282 case "trunc":
283 result.trunc = val;
284 result.write |= val;
285 break;
286 case "create":
287 result.create = val;
288 result.write |= val;
289 break;
290 case "existing": // fallthrough
291 case "exist":
292 result.existing = val;
293 break;
294 case "append":
295 result.append = val;
296 break;
297 default:
298 throw new TypeError("Mode " + key + " not understood");
299 }
300 }
301 // Reject opposite modes
302 if (result.existing && result.create) {
303 throw new TypeError("Cannot specify both existing:true and create:true");
304 }
305 if (result.trunc && result.create) {
306 throw new TypeError("Cannot specify both trunc:true and create:true");
307 }
308 // Handle read/write
309 if (!result.write) {
310 result.read = true;
311 }
312 return result;
313 };
315 /**
316 * Return the contents of a file.
317 *
318 * @param {string} path The path to the file.
319 * @param {number=} bytes Optionally, an upper bound to the number of bytes
320 * to read. DEPRECATED - please use options.bytes instead.
321 * @param {object=} options Optionally, an object with some of the following
322 * fields:
323 * - {number} bytes An upper bound to the number of bytes to read.
324 * - {string} compression If "lz4" and if the file is compressed using the lz4
325 * compression algorithm, decompress the file contents on the fly.
326 *
327 * @return {Uint8Array} A buffer holding the bytes
328 * and the number of bytes read from the file.
329 */
330 AbstractFile.read = function read(path, bytes, options = {}) {
331 if (bytes && typeof bytes == "object") {
332 options = bytes;
333 bytes = options.bytes || null;
334 }
335 if ("encoding" in options && typeof options.encoding != "string") {
336 throw new TypeError("Invalid type for option encoding");
337 }
338 if ("compression" in options && typeof options.compression != "string") {
339 throw new TypeError("Invalid type for option compression: " + options.compression);
340 }
341 if ("bytes" in options && typeof options.bytes != "number") {
342 throw new TypeError("Invalid type for option bytes");
343 }
344 let file = exports.OS.File.open(path);
345 try {
346 let buffer = file.read(bytes, options);
347 if ("compression" in options) {
348 if (options.compression == "lz4") {
349 buffer = Lz4.decompressFileContent(buffer, options);
350 } else {
351 throw OS.File.Error.invalidArgument("Compression");
352 }
353 }
354 if (!("encoding" in options)) {
355 return buffer;
356 }
357 let decoder;
358 try {
359 decoder = new TextDecoder(options.encoding);
360 } catch (ex if ex instanceof TypeError) {
361 throw OS.File.Error.invalidArgument("Decode");
362 }
363 return decoder.decode(buffer);
364 } finally {
365 file.close();
366 }
367 };
369 /**
370 * Write a file, atomically.
371 *
372 * By opposition to a regular |write|, this operation ensures that,
373 * until the contents are fully written, the destination file is
374 * not modified.
375 *
376 * Limitation: In a few extreme cases (hardware failure during the
377 * write, user unplugging disk during the write, etc.), data may be
378 * corrupted. If your data is user-critical (e.g. preferences,
379 * application data, etc.), you may wish to consider adding options
380 * |tmpPath| and/or |flush| to reduce the likelihood of corruption, as
381 * detailed below. Note that no combination of options can be
382 * guaranteed to totally eliminate the risk of corruption.
383 *
384 * @param {string} path The path of the file to modify.
385 * @param {Typed Array | C pointer} buffer A buffer containing the bytes to write.
386 * @param {*=} options Optionally, an object determining the behavior
387 * of this function. This object may contain the following fields:
388 * - {number} bytes The number of bytes to write. If unspecified,
389 * |buffer.byteLength|. Required if |buffer| is a C pointer.
390 * - {string} tmpPath If |null| or unspecified, write all data directly
391 * to |path|. If specified, write all data to a temporary file called
392 * |tmpPath| and, once this write is complete, rename the file to
393 * replace |path|. Performing this additional operation is a little
394 * slower but also a little safer.
395 * - {bool} noOverwrite - If set, this function will fail if a file already
396 * exists at |path|.
397 * - {bool} flush - If |false| or unspecified, return immediately once the
398 * write is complete. If |true|, before writing, force the operating system
399 * to write its internal disk buffers to the disk. This is considerably slower
400 * (not just for the application but for the whole system) but also safer:
401 * if the system shuts down improperly (typically due to a kernel freeze
402 * or a power failure) or if the device is disconnected before the buffer
403 * is flushed, the file has more chances of not being corrupted.
404 * - {string} compression - If empty or unspecified, do not compress the file.
405 * If "lz4", compress the contents of the file atomically using lz4. For the
406 * time being, the container format is specific to Mozilla and cannot be read
407 * by means other than OS.File.read(..., { compression: "lz4"})
408 * - {string} backupTo - If specified, backup the destination file as |backupTo|.
409 * Note that this function renames the destination file before overwriting it.
410 * If the process or the operating system freezes or crashes
411 * during the short window between these operations,
412 * the destination file will have been moved to its backup.
413 *
414 * @return {number} The number of bytes actually written.
415 */
416 AbstractFile.writeAtomic =
417 function writeAtomic(path, buffer, options = {}) {
419 // Verify that path is defined and of the correct type
420 if (typeof path != "string" || path == "") {
421 throw new TypeError("File path should be a (non-empty) string");
422 }
423 let noOverwrite = options.noOverwrite;
424 if (noOverwrite && OS.File.exists(path)) {
425 throw OS.File.Error.exists("writeAtomic", path);
426 }
428 if (typeof buffer == "string") {
429 // Normalize buffer to a C buffer by encoding it
430 let encoding = options.encoding || "utf-8";
431 buffer = new TextEncoder(encoding).encode(buffer);
432 }
434 if ("compression" in options && options.compression == "lz4") {
435 buffer = Lz4.compressFileContent(buffer, options);
436 options = Object.create(options);
437 options.bytes = buffer.byteLength;
438 }
440 let bytesWritten = 0;
442 if (!options.tmpPath) {
443 if (options.backupTo) {
444 try {
445 OS.File.move(path, options.backupTo, {noCopy: true});
446 } catch (ex if ex.becauseNoSuchFile) {
447 // The file doesn't exist, nothing to backup.
448 }
449 }
450 // Just write, without any renaming trick
451 let dest = OS.File.open(path, {write: true, truncate: true});
452 try {
453 bytesWritten = dest.write(buffer, options);
454 if (options.flush) {
455 dest.flush();
456 }
457 } finally {
458 dest.close();
459 }
460 return bytesWritten;
461 }
463 let tmpFile = OS.File.open(options.tmpPath, {write: true, truncate: true});
464 try {
465 bytesWritten = tmpFile.write(buffer, options);
466 if (options.flush) {
467 tmpFile.flush();
468 }
469 } catch (x) {
470 OS.File.remove(options.tmpPath);
471 throw x;
472 } finally {
473 tmpFile.close();
474 }
476 if (options.backupTo) {
477 try {
478 OS.File.move(path, options.backupTo, {noCopy: true});
479 } catch (ex if ex.becauseNoSuchFile) {
480 // The file doesn't exist, nothing to backup.
481 }
482 }
484 OS.File.move(options.tmpPath, path, {noCopy: true});
485 return bytesWritten;
486 };
488 /**
489 * This function is used by removeDir to avoid calling lstat for each
490 * files under the specified directory. External callers should not call
491 * this function directly.
492 */
493 AbstractFile.removeRecursive = function(path, options = {}) {
494 let iterator = new OS.File.DirectoryIterator(path);
495 if (!iterator.exists()) {
496 if (!("ignoreAbsent" in options) || options.ignoreAbsent) {
497 return;
498 }
499 }
501 try {
502 for (let entry in iterator) {
503 if (entry.isDir) {
504 if (entry.isLink) {
505 // Unlike Unix symlinks, NTFS junctions or NTFS symlinks to
506 // directories are directories themselves. OS.File.remove()
507 // will not work for them.
508 OS.File.removeEmptyDir(entry.path, options);
509 } else {
510 // Normal directories.
511 AbstractFile.removeRecursive(entry.path, options);
512 }
513 } else {
514 // NTFS symlinks to files, Unix symlinks, or regular files.
515 OS.File.remove(entry.path, options);
516 }
517 }
518 } finally {
519 iterator.close();
520 }
522 OS.File.removeEmptyDir(path);
523 };
525 /**
526 * Create a directory and, optionally, its parent directories.
527 *
528 * @param {string} path The name of the directory.
529 * @param {*=} options Additional options.
530 *
531 * - {string} from If specified, the call to |makeDir| creates all the
532 * ancestors of |path| that are descendants of |from|. Note that |path|
533 * must be a descendant of |from|, and that |from| and its existing
534 * subdirectories present in |path| must be user-writeable.
535 * Example:
536 * makeDir(Path.join(profileDir, "foo", "bar"), { from: profileDir });
537 * creates directories profileDir/foo, profileDir/foo/bar
538 * - {bool} ignoreExisting If |false|, throw an error if the directory
539 * already exists. |true| by default. Ignored if |from| is specified.
540 * - {number} unixMode Under Unix, if specified, a file creation mode,
541 * as per libc function |mkdir|. If unspecified, dirs are
542 * created with a default mode of 0700 (dir is private to
543 * the user, the user can read, write and execute). Ignored under Windows
544 * or if the file system does not support file creation modes.
545 * - {C pointer} winSecurity Under Windows, if specified, security
546 * attributes as per winapi function |CreateDirectory|. If
547 * unspecified, use the default security descriptor, inherited from
548 * the parent directory. Ignored under Unix or if the file system
549 * does not support security descriptors.
550 */
551 AbstractFile.makeDir = function(path, options = {}) {
552 if (!options.from) {
553 return OS.File._makeDir(path, options);
554 }
555 if (!path.startsWith(options.from)) {
556 throw new Error("Incorrect use of option |from|: " + path + " is not a descendant of " + options.from);
557 }
558 let innerOptions = Object.create(options, {
559 ignoreExisting: {
560 value: true
561 }
562 });
563 // Compute the elements that appear in |path| but not in |options.from|.
564 let items = Path.split(path).components.slice(Path.split(options.from).components.length);
565 let current = options.from;
566 for (let item of items) {
567 current = Path.join(current, item);
568 OS.File._makeDir(current, innerOptions);
569 }
570 };
572 if (!exports.OS.Shared) {
573 exports.OS.Shared = {};
574 }
575 exports.OS.Shared.AbstractFile = AbstractFile;
576 })(this);