1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/components/osfile/modules/osfile_shared_front.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,576 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +/** 1.9 + * Code shared by OS.File front-ends. 1.10 + * 1.11 + * This code is meant to be included by another library. It is also meant to 1.12 + * be executed only on a worker thread. 1.13 + */ 1.14 + 1.15 +if (typeof Components != "undefined") { 1.16 + throw new Error("osfile_shared_front.jsm cannot be used from the main thread"); 1.17 +} 1.18 +(function(exports) { 1.19 + 1.20 +let SharedAll = 1.21 + require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm"); 1.22 +let Path = require("resource://gre/modules/osfile/ospath.jsm"); 1.23 +let Lz4 = 1.24 + require("resource://gre/modules/workers/lz4.js"); 1.25 +let LOG = SharedAll.LOG.bind(SharedAll, "Shared front-end"); 1.26 +let clone = SharedAll.clone; 1.27 + 1.28 +/** 1.29 + * Code shared by implementations of File. 1.30 + * 1.31 + * @param {*} fd An OS-specific file handle. 1.32 + * @param {string} path File path of the file handle, used for error-reporting. 1.33 + * @constructor 1.34 + */ 1.35 +let AbstractFile = function AbstractFile(fd, path) { 1.36 + this._fd = fd; 1.37 + if (!path) { 1.38 + throw new TypeError("path is expected"); 1.39 + } 1.40 + this._path = path; 1.41 +}; 1.42 + 1.43 +AbstractFile.prototype = { 1.44 + /** 1.45 + * Return the file handle. 1.46 + * 1.47 + * @throw OS.File.Error if the file has been closed. 1.48 + */ 1.49 + get fd() { 1.50 + if (this._fd) { 1.51 + return this._fd; 1.52 + } 1.53 + throw OS.File.Error.closed("accessing file", this._path); 1.54 + }, 1.55 + /** 1.56 + * Read bytes from this file to a new buffer. 1.57 + * 1.58 + * @param {number=} bytes If unspecified, read all the remaining bytes from 1.59 + * this file. If specified, read |bytes| bytes, or less if the file does notclone 1.60 + * contain that many bytes. 1.61 + * @param {JSON} options 1.62 + * @return {Uint8Array} An array containing the bytes read. 1.63 + */ 1.64 + read: function read(bytes, options = {}) { 1.65 + options = clone(options); 1.66 + options.bytes = bytes == null ? this.stat().size : bytes; 1.67 + let buffer = new Uint8Array(options.bytes); 1.68 + let size = this.readTo(buffer, options); 1.69 + if (size == options.bytes) { 1.70 + return buffer; 1.71 + } else { 1.72 + return buffer.subarray(0, size); 1.73 + } 1.74 + }, 1.75 + 1.76 + /** 1.77 + * Read bytes from this file to an existing buffer. 1.78 + * 1.79 + * Note that, by default, this function may perform several I/O 1.80 + * operations to ensure that the buffer is as full as possible. 1.81 + * 1.82 + * @param {Typed Array | C pointer} buffer The buffer in which to 1.83 + * store the bytes. The buffer must be large enough to 1.84 + * accomodate |bytes| bytes. 1.85 + * @param {*=} options Optionally, an object that may contain the 1.86 + * following fields: 1.87 + * - {number} bytes The number of |bytes| to write from the buffer. If 1.88 + * unspecified, this is |buffer.byteLength|. Note that |bytes| is required 1.89 + * if |buffer| is a C pointer. 1.90 + * 1.91 + * @return {number} The number of bytes actually read, which may be 1.92 + * less than |bytes| if the file did not contain that many bytes left. 1.93 + */ 1.94 + readTo: function readTo(buffer, options = {}) { 1.95 + let {ptr, bytes} = SharedAll.normalizeToPointer(buffer, options.bytes); 1.96 + let pos = 0; 1.97 + while (pos < bytes) { 1.98 + let chunkSize = this._read(ptr, bytes - pos, options); 1.99 + if (chunkSize == 0) { 1.100 + break; 1.101 + } 1.102 + pos += chunkSize; 1.103 + ptr = SharedAll.offsetBy(ptr, chunkSize); 1.104 + } 1.105 + 1.106 + return pos; 1.107 + }, 1.108 + 1.109 + /** 1.110 + * Write bytes from a buffer to this file. 1.111 + * 1.112 + * Note that, by default, this function may perform several I/O 1.113 + * operations to ensure that the buffer is fully written. 1.114 + * 1.115 + * @param {Typed array | C pointer} buffer The buffer in which the 1.116 + * the bytes are stored. The buffer must be large enough to 1.117 + * accomodate |bytes| bytes. 1.118 + * @param {*=} options Optionally, an object that may contain the 1.119 + * following fields: 1.120 + * - {number} bytes The number of |bytes| to write from the buffer. If 1.121 + * unspecified, this is |buffer.byteLength|. Note that |bytes| is required 1.122 + * if |buffer| is a C pointer. 1.123 + * 1.124 + * @return {number} The number of bytes actually written. 1.125 + */ 1.126 + write: function write(buffer, options = {}) { 1.127 + 1.128 + let {ptr, bytes} = 1.129 + SharedAll.normalizeToPointer(buffer, options.bytes || undefined); 1.130 + 1.131 + let pos = 0; 1.132 + while (pos < bytes) { 1.133 + let chunkSize = this._write(ptr, bytes - pos, options); 1.134 + pos += chunkSize; 1.135 + ptr = SharedAll.offsetBy(ptr, chunkSize); 1.136 + } 1.137 + return pos; 1.138 + } 1.139 +}; 1.140 + 1.141 +/** 1.142 + * 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. 1.143 + * 1.144 + * @param {string} path The path to the file. 1.145 + * @param {*=} options Additional options for file opening. This 1.146 + * implementation interprets the following fields: 1.147 + * 1.148 + * - {number} humanReadable If |true|, create a new filename appending a decimal number. ie: filename-1.ext, filename-2.ext. 1.149 + * If |false| use HEX numbers ie: filename-A65BC0.ext 1.150 + * - {number} maxReadableNumber Used to limit the amount of tries after a failed 1.151 + * file creation. Default is 20. 1.152 + * 1.153 + * @return {Object} contains A file object{file} and the path{path}. 1.154 + * @throws {OS.File.Error} If the file could not be opened. 1.155 + */ 1.156 +AbstractFile.openUnique = function openUnique(path, options = {}) { 1.157 + let mode = { 1.158 + create : true 1.159 + }; 1.160 + 1.161 + let dirName = Path.dirname(path); 1.162 + let leafName = Path.basename(path); 1.163 + let lastDotCharacter = leafName.lastIndexOf('.'); 1.164 + let fileName = leafName.substring(0, lastDotCharacter != -1 ? lastDotCharacter : leafName.length); 1.165 + let suffix = (lastDotCharacter != -1 ? leafName.substring(lastDotCharacter) : ""); 1.166 + let uniquePath = ""; 1.167 + let maxAttempts = options.maxAttempts || 99; 1.168 + let humanReadable = !!options.humanReadable; 1.169 + const HEX_RADIX = 16; 1.170 + // We produce HEX numbers between 0 and 2^24 - 1. 1.171 + const MAX_HEX_NUMBER = 16777215; 1.172 + 1.173 + try { 1.174 + return { 1.175 + path: path, 1.176 + file: OS.File.open(path, mode) 1.177 + }; 1.178 + } catch (ex if ex instanceof OS.File.Error && ex.becauseExists) { 1.179 + for (let i = 0; i < maxAttempts; ++i) { 1.180 + try { 1.181 + if (humanReadable) { 1.182 + uniquePath = Path.join(dirName, fileName + "-" + (i + 1) + suffix); 1.183 + } else { 1.184 + let hexNumber = Math.floor(Math.random() * MAX_HEX_NUMBER).toString(HEX_RADIX); 1.185 + uniquePath = Path.join(dirName, fileName + "-" + hexNumber + suffix); 1.186 + } 1.187 + return { 1.188 + path: uniquePath, 1.189 + file: OS.File.open(uniquePath, mode) 1.190 + }; 1.191 + } catch (ex if ex instanceof OS.File.Error && ex.becauseExists) { 1.192 + // keep trying ... 1.193 + } 1.194 + } 1.195 + throw OS.File.Error.exists("could not find an unused file name.", path); 1.196 + } 1.197 +}; 1.198 + 1.199 +/** 1.200 + * Code shared by iterators. 1.201 + */ 1.202 +AbstractFile.AbstractIterator = function AbstractIterator() { 1.203 +}; 1.204 +AbstractFile.AbstractIterator.prototype = { 1.205 + /** 1.206 + * Allow iterating with |for| 1.207 + */ 1.208 + __iterator__: function __iterator__() { 1.209 + return this; 1.210 + }, 1.211 + /** 1.212 + * Apply a function to all elements of the directory sequentially. 1.213 + * 1.214 + * @param {Function} cb This function will be applied to all entries 1.215 + * of the directory. It receives as arguments 1.216 + * - the OS.File.Entry corresponding to the entry; 1.217 + * - the index of the entry in the enumeration; 1.218 + * - the iterator itself - calling |close| on the iterator stops 1.219 + * the loop. 1.220 + */ 1.221 + forEach: function forEach(cb) { 1.222 + let index = 0; 1.223 + for (let entry in this) { 1.224 + cb(entry, index++, this); 1.225 + } 1.226 + }, 1.227 + /** 1.228 + * Return several entries at once. 1.229 + * 1.230 + * Entries are returned in the same order as a walk with |forEach| or 1.231 + * |for(...)|. 1.232 + * 1.233 + * @param {number=} length If specified, the number of entries 1.234 + * to return. If unspecified, return all remaining entries. 1.235 + * @return {Array} An array containing the next |length| entries, or 1.236 + * less if the iteration contains less than |length| entries left. 1.237 + */ 1.238 + nextBatch: function nextBatch(length) { 1.239 + let array = []; 1.240 + let i = 0; 1.241 + for (let entry in this) { 1.242 + array.push(entry); 1.243 + if (++i >= length) { 1.244 + return array; 1.245 + } 1.246 + } 1.247 + return array; 1.248 + } 1.249 +}; 1.250 + 1.251 +/** 1.252 + * Utility function shared by implementations of |OS.File.open|: 1.253 + * extract read/write/trunc/create/existing flags from a |mode| 1.254 + * object. 1.255 + * 1.256 + * @param {*=} mode An object that may contain fields |read|, 1.257 + * |write|, |truncate|, |create|, |existing|. These fields 1.258 + * are interpreted only if true-ish. 1.259 + * @return {{read:bool, write:bool, trunc:bool, create:bool, 1.260 + * existing:bool}} an object recapitulating the options set 1.261 + * by |mode|. 1.262 + * @throws {TypeError} If |mode| contains other fields, or 1.263 + * if it contains both |create| and |truncate|, or |create| 1.264 + * and |existing|. 1.265 + */ 1.266 +AbstractFile.normalizeOpenMode = function normalizeOpenMode(mode) { 1.267 + let result = { 1.268 + read: false, 1.269 + write: false, 1.270 + trunc: false, 1.271 + create: false, 1.272 + existing: false, 1.273 + append: true 1.274 + }; 1.275 + for (let key in mode) { 1.276 + let val = !!mode[key]; // bool cast. 1.277 + switch (key) { 1.278 + case "read": 1.279 + result.read = val; 1.280 + break; 1.281 + case "write": 1.282 + result.write = val; 1.283 + break; 1.284 + case "truncate": // fallthrough 1.285 + case "trunc": 1.286 + result.trunc = val; 1.287 + result.write |= val; 1.288 + break; 1.289 + case "create": 1.290 + result.create = val; 1.291 + result.write |= val; 1.292 + break; 1.293 + case "existing": // fallthrough 1.294 + case "exist": 1.295 + result.existing = val; 1.296 + break; 1.297 + case "append": 1.298 + result.append = val; 1.299 + break; 1.300 + default: 1.301 + throw new TypeError("Mode " + key + " not understood"); 1.302 + } 1.303 + } 1.304 + // Reject opposite modes 1.305 + if (result.existing && result.create) { 1.306 + throw new TypeError("Cannot specify both existing:true and create:true"); 1.307 + } 1.308 + if (result.trunc && result.create) { 1.309 + throw new TypeError("Cannot specify both trunc:true and create:true"); 1.310 + } 1.311 + // Handle read/write 1.312 + if (!result.write) { 1.313 + result.read = true; 1.314 + } 1.315 + return result; 1.316 +}; 1.317 + 1.318 +/** 1.319 + * Return the contents of a file. 1.320 + * 1.321 + * @param {string} path The path to the file. 1.322 + * @param {number=} bytes Optionally, an upper bound to the number of bytes 1.323 + * to read. DEPRECATED - please use options.bytes instead. 1.324 + * @param {object=} options Optionally, an object with some of the following 1.325 + * fields: 1.326 + * - {number} bytes An upper bound to the number of bytes to read. 1.327 + * - {string} compression If "lz4" and if the file is compressed using the lz4 1.328 + * compression algorithm, decompress the file contents on the fly. 1.329 + * 1.330 + * @return {Uint8Array} A buffer holding the bytes 1.331 + * and the number of bytes read from the file. 1.332 + */ 1.333 +AbstractFile.read = function read(path, bytes, options = {}) { 1.334 + if (bytes && typeof bytes == "object") { 1.335 + options = bytes; 1.336 + bytes = options.bytes || null; 1.337 + } 1.338 + if ("encoding" in options && typeof options.encoding != "string") { 1.339 + throw new TypeError("Invalid type for option encoding"); 1.340 + } 1.341 + if ("compression" in options && typeof options.compression != "string") { 1.342 + throw new TypeError("Invalid type for option compression: " + options.compression); 1.343 + } 1.344 + if ("bytes" in options && typeof options.bytes != "number") { 1.345 + throw new TypeError("Invalid type for option bytes"); 1.346 + } 1.347 + let file = exports.OS.File.open(path); 1.348 + try { 1.349 + let buffer = file.read(bytes, options); 1.350 + if ("compression" in options) { 1.351 + if (options.compression == "lz4") { 1.352 + buffer = Lz4.decompressFileContent(buffer, options); 1.353 + } else { 1.354 + throw OS.File.Error.invalidArgument("Compression"); 1.355 + } 1.356 + } 1.357 + if (!("encoding" in options)) { 1.358 + return buffer; 1.359 + } 1.360 + let decoder; 1.361 + try { 1.362 + decoder = new TextDecoder(options.encoding); 1.363 + } catch (ex if ex instanceof TypeError) { 1.364 + throw OS.File.Error.invalidArgument("Decode"); 1.365 + } 1.366 + return decoder.decode(buffer); 1.367 + } finally { 1.368 + file.close(); 1.369 + } 1.370 +}; 1.371 + 1.372 +/** 1.373 + * Write a file, atomically. 1.374 + * 1.375 + * By opposition to a regular |write|, this operation ensures that, 1.376 + * until the contents are fully written, the destination file is 1.377 + * not modified. 1.378 + * 1.379 + * Limitation: In a few extreme cases (hardware failure during the 1.380 + * write, user unplugging disk during the write, etc.), data may be 1.381 + * corrupted. If your data is user-critical (e.g. preferences, 1.382 + * application data, etc.), you may wish to consider adding options 1.383 + * |tmpPath| and/or |flush| to reduce the likelihood of corruption, as 1.384 + * detailed below. Note that no combination of options can be 1.385 + * guaranteed to totally eliminate the risk of corruption. 1.386 + * 1.387 + * @param {string} path The path of the file to modify. 1.388 + * @param {Typed Array | C pointer} buffer A buffer containing the bytes to write. 1.389 + * @param {*=} options Optionally, an object determining the behavior 1.390 + * of this function. This object may contain the following fields: 1.391 + * - {number} bytes The number of bytes to write. If unspecified, 1.392 + * |buffer.byteLength|. Required if |buffer| is a C pointer. 1.393 + * - {string} tmpPath If |null| or unspecified, write all data directly 1.394 + * to |path|. If specified, write all data to a temporary file called 1.395 + * |tmpPath| and, once this write is complete, rename the file to 1.396 + * replace |path|. Performing this additional operation is a little 1.397 + * slower but also a little safer. 1.398 + * - {bool} noOverwrite - If set, this function will fail if a file already 1.399 + * exists at |path|. 1.400 + * - {bool} flush - If |false| or unspecified, return immediately once the 1.401 + * write is complete. If |true|, before writing, force the operating system 1.402 + * to write its internal disk buffers to the disk. This is considerably slower 1.403 + * (not just for the application but for the whole system) but also safer: 1.404 + * if the system shuts down improperly (typically due to a kernel freeze 1.405 + * or a power failure) or if the device is disconnected before the buffer 1.406 + * is flushed, the file has more chances of not being corrupted. 1.407 + * - {string} compression - If empty or unspecified, do not compress the file. 1.408 + * If "lz4", compress the contents of the file atomically using lz4. For the 1.409 + * time being, the container format is specific to Mozilla and cannot be read 1.410 + * by means other than OS.File.read(..., { compression: "lz4"}) 1.411 + * - {string} backupTo - If specified, backup the destination file as |backupTo|. 1.412 + * Note that this function renames the destination file before overwriting it. 1.413 + * If the process or the operating system freezes or crashes 1.414 + * during the short window between these operations, 1.415 + * the destination file will have been moved to its backup. 1.416 + * 1.417 + * @return {number} The number of bytes actually written. 1.418 + */ 1.419 +AbstractFile.writeAtomic = 1.420 + function writeAtomic(path, buffer, options = {}) { 1.421 + 1.422 + // Verify that path is defined and of the correct type 1.423 + if (typeof path != "string" || path == "") { 1.424 + throw new TypeError("File path should be a (non-empty) string"); 1.425 + } 1.426 + let noOverwrite = options.noOverwrite; 1.427 + if (noOverwrite && OS.File.exists(path)) { 1.428 + throw OS.File.Error.exists("writeAtomic", path); 1.429 + } 1.430 + 1.431 + if (typeof buffer == "string") { 1.432 + // Normalize buffer to a C buffer by encoding it 1.433 + let encoding = options.encoding || "utf-8"; 1.434 + buffer = new TextEncoder(encoding).encode(buffer); 1.435 + } 1.436 + 1.437 + if ("compression" in options && options.compression == "lz4") { 1.438 + buffer = Lz4.compressFileContent(buffer, options); 1.439 + options = Object.create(options); 1.440 + options.bytes = buffer.byteLength; 1.441 + } 1.442 + 1.443 + let bytesWritten = 0; 1.444 + 1.445 + if (!options.tmpPath) { 1.446 + if (options.backupTo) { 1.447 + try { 1.448 + OS.File.move(path, options.backupTo, {noCopy: true}); 1.449 + } catch (ex if ex.becauseNoSuchFile) { 1.450 + // The file doesn't exist, nothing to backup. 1.451 + } 1.452 + } 1.453 + // Just write, without any renaming trick 1.454 + let dest = OS.File.open(path, {write: true, truncate: true}); 1.455 + try { 1.456 + bytesWritten = dest.write(buffer, options); 1.457 + if (options.flush) { 1.458 + dest.flush(); 1.459 + } 1.460 + } finally { 1.461 + dest.close(); 1.462 + } 1.463 + return bytesWritten; 1.464 + } 1.465 + 1.466 + let tmpFile = OS.File.open(options.tmpPath, {write: true, truncate: true}); 1.467 + try { 1.468 + bytesWritten = tmpFile.write(buffer, options); 1.469 + if (options.flush) { 1.470 + tmpFile.flush(); 1.471 + } 1.472 + } catch (x) { 1.473 + OS.File.remove(options.tmpPath); 1.474 + throw x; 1.475 + } finally { 1.476 + tmpFile.close(); 1.477 + } 1.478 + 1.479 + if (options.backupTo) { 1.480 + try { 1.481 + OS.File.move(path, options.backupTo, {noCopy: true}); 1.482 + } catch (ex if ex.becauseNoSuchFile) { 1.483 + // The file doesn't exist, nothing to backup. 1.484 + } 1.485 + } 1.486 + 1.487 + OS.File.move(options.tmpPath, path, {noCopy: true}); 1.488 + return bytesWritten; 1.489 +}; 1.490 + 1.491 +/** 1.492 + * This function is used by removeDir to avoid calling lstat for each 1.493 + * files under the specified directory. External callers should not call 1.494 + * this function directly. 1.495 + */ 1.496 +AbstractFile.removeRecursive = function(path, options = {}) { 1.497 + let iterator = new OS.File.DirectoryIterator(path); 1.498 + if (!iterator.exists()) { 1.499 + if (!("ignoreAbsent" in options) || options.ignoreAbsent) { 1.500 + return; 1.501 + } 1.502 + } 1.503 + 1.504 + try { 1.505 + for (let entry in iterator) { 1.506 + if (entry.isDir) { 1.507 + if (entry.isLink) { 1.508 + // Unlike Unix symlinks, NTFS junctions or NTFS symlinks to 1.509 + // directories are directories themselves. OS.File.remove() 1.510 + // will not work for them. 1.511 + OS.File.removeEmptyDir(entry.path, options); 1.512 + } else { 1.513 + // Normal directories. 1.514 + AbstractFile.removeRecursive(entry.path, options); 1.515 + } 1.516 + } else { 1.517 + // NTFS symlinks to files, Unix symlinks, or regular files. 1.518 + OS.File.remove(entry.path, options); 1.519 + } 1.520 + } 1.521 + } finally { 1.522 + iterator.close(); 1.523 + } 1.524 + 1.525 + OS.File.removeEmptyDir(path); 1.526 +}; 1.527 + 1.528 +/** 1.529 + * Create a directory and, optionally, its parent directories. 1.530 + * 1.531 + * @param {string} path The name of the directory. 1.532 + * @param {*=} options Additional options. 1.533 + * 1.534 + * - {string} from If specified, the call to |makeDir| creates all the 1.535 + * ancestors of |path| that are descendants of |from|. Note that |path| 1.536 + * must be a descendant of |from|, and that |from| and its existing 1.537 + * subdirectories present in |path| must be user-writeable. 1.538 + * Example: 1.539 + * makeDir(Path.join(profileDir, "foo", "bar"), { from: profileDir }); 1.540 + * creates directories profileDir/foo, profileDir/foo/bar 1.541 + * - {bool} ignoreExisting If |false|, throw an error if the directory 1.542 + * already exists. |true| by default. Ignored if |from| is specified. 1.543 + * - {number} unixMode Under Unix, if specified, a file creation mode, 1.544 + * as per libc function |mkdir|. If unspecified, dirs are 1.545 + * created with a default mode of 0700 (dir is private to 1.546 + * the user, the user can read, write and execute). Ignored under Windows 1.547 + * or if the file system does not support file creation modes. 1.548 + * - {C pointer} winSecurity Under Windows, if specified, security 1.549 + * attributes as per winapi function |CreateDirectory|. If 1.550 + * unspecified, use the default security descriptor, inherited from 1.551 + * the parent directory. Ignored under Unix or if the file system 1.552 + * does not support security descriptors. 1.553 + */ 1.554 +AbstractFile.makeDir = function(path, options = {}) { 1.555 + if (!options.from) { 1.556 + return OS.File._makeDir(path, options); 1.557 + } 1.558 + if (!path.startsWith(options.from)) { 1.559 + throw new Error("Incorrect use of option |from|: " + path + " is not a descendant of " + options.from); 1.560 + } 1.561 + let innerOptions = Object.create(options, { 1.562 + ignoreExisting: { 1.563 + value: true 1.564 + } 1.565 + }); 1.566 + // Compute the elements that appear in |path| but not in |options.from|. 1.567 + let items = Path.split(path).components.slice(Path.split(options.from).components.length); 1.568 + let current = options.from; 1.569 + for (let item of items) { 1.570 + current = Path.join(current, item); 1.571 + OS.File._makeDir(current, innerOptions); 1.572 + } 1.573 +}; 1.574 + 1.575 +if (!exports.OS.Shared) { 1.576 + exports.OS.Shared = {}; 1.577 +} 1.578 +exports.OS.Shared.AbstractFile = AbstractFile; 1.579 +})(this);