|
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/. */ |
|
4 |
|
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 */ |
|
11 |
|
12 if (typeof Components != "undefined") { |
|
13 throw new Error("osfile_shared_front.jsm cannot be used from the main thread"); |
|
14 } |
|
15 (function(exports) { |
|
16 |
|
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; |
|
24 |
|
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 }; |
|
39 |
|
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 }, |
|
72 |
|
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 } |
|
102 |
|
103 return pos; |
|
104 }, |
|
105 |
|
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 = {}) { |
|
124 |
|
125 let {ptr, bytes} = |
|
126 SharedAll.normalizeToPointer(buffer, options.bytes || undefined); |
|
127 |
|
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 }; |
|
137 |
|
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 }; |
|
157 |
|
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; |
|
169 |
|
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 }; |
|
195 |
|
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 }; |
|
247 |
|
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 }; |
|
314 |
|
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 }; |
|
368 |
|
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 = {}) { |
|
418 |
|
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 } |
|
427 |
|
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 } |
|
433 |
|
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 } |
|
439 |
|
440 let bytesWritten = 0; |
|
441 |
|
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 } |
|
462 |
|
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 } |
|
475 |
|
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 } |
|
483 |
|
484 OS.File.move(options.tmpPath, path, {noCopy: true}); |
|
485 return bytesWritten; |
|
486 }; |
|
487 |
|
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 } |
|
500 |
|
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 } |
|
521 |
|
522 OS.File.removeEmptyDir(path); |
|
523 }; |
|
524 |
|
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 }; |
|
571 |
|
572 if (!exports.OS.Shared) { |
|
573 exports.OS.Shared = {}; |
|
574 } |
|
575 exports.OS.Shared.AbstractFile = AbstractFile; |
|
576 })(this); |