NodeFileSystemWritableFileStream.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.NodeFileSystemWritableFileStream = exports.createSwapFile = void 0;
  4. const buffer_1 = require("@jsonjoy.com/fs-node-builtins/lib/internal/buffer");
  5. const util_1 = require("./util");
  6. /**
  7. * When Chrome writes to the file, it creates a copy of the file with extension
  8. * `.crswap` and then replaces the original file with the copy only when the
  9. * `close()` method is called. If the `abort()` method is called, the `.crswap`
  10. * file is deleted.
  11. *
  12. * If a file name with with extension `.crswap` is already taken, it
  13. * creates a new swap file with extension `.1.crswap` and so on.
  14. */
  15. const createSwapFile = async (fs, path, keepExistingData) => {
  16. let handle;
  17. let swapPath = path + '.crswap';
  18. try {
  19. handle = await fs.promises.open(swapPath, 'ax');
  20. }
  21. catch (error) {
  22. if (!error || typeof error !== 'object' || error.code !== 'EEXIST')
  23. throw error;
  24. }
  25. if (!handle) {
  26. for (let i = 1; i < 1000; i++) {
  27. try {
  28. swapPath = `${path}.${i}.crswap`;
  29. handle = await fs.promises.open(swapPath, 'ax');
  30. break;
  31. }
  32. catch (error) {
  33. if (!error || typeof error !== 'object' || error.code !== 'EEXIST')
  34. throw error;
  35. }
  36. }
  37. }
  38. if (!handle)
  39. throw new Error(`Could not create a swap file for "${path}".`);
  40. if (keepExistingData)
  41. await fs.promises.copyFile(path, swapPath, fs.constants.COPYFILE_FICLONE);
  42. return [handle, swapPath];
  43. };
  44. exports.createSwapFile = createSwapFile;
  45. const WS = (typeof WritableStream === 'undefined' ? require('stream/web').WritableStream : WritableStream);
  46. /**
  47. * Is a WritableStream object with additional convenience methods, which
  48. * operates on a single file on disk. The interface is accessed through the
  49. * `FileSystemFileHandle.createWritable()` method.
  50. *
  51. * @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemWritableFileStream
  52. */
  53. class NodeFileSystemWritableFileStream extends WS {
  54. constructor(fs, path, keepExistingData, ctx) {
  55. const swap = { handle: undefined, path: '', offset: 0 };
  56. super({
  57. async start() {
  58. if (ctx.locks.isLocked(path)) {
  59. throw (0, util_1.newNoModificationAllowedError)();
  60. }
  61. ctx.locks.acquireLock(path);
  62. try {
  63. const promise = (0, exports.createSwapFile)(fs, path, keepExistingData);
  64. swap.ready = promise.then(() => undefined);
  65. const [handle, swapPath] = await promise;
  66. swap.handle = handle;
  67. swap.path = swapPath;
  68. }
  69. catch (error) {
  70. ctx.locks.releaseLock(path);
  71. throw error;
  72. }
  73. },
  74. async write(chunk) {
  75. await swap.ready;
  76. const handle = swap.handle;
  77. if (!handle)
  78. throw new Error('Invalid state');
  79. const buffer = buffer_1.Buffer.from(typeof chunk === 'string'
  80. ? chunk
  81. : chunk instanceof Blob
  82. ? new Uint8Array(await chunk.arrayBuffer())
  83. : chunk);
  84. const { bytesWritten } = await handle.write(buffer, 0, buffer.length, swap.offset);
  85. swap.offset += bytesWritten;
  86. },
  87. async close() {
  88. await swap.ready;
  89. const handle = swap.handle;
  90. if (!handle)
  91. return;
  92. await handle.close();
  93. await fs.promises.rename(swap.path, path);
  94. ctx.locks.releaseLock(path);
  95. },
  96. async abort() {
  97. await swap.ready;
  98. const handle = swap.handle;
  99. if (!handle)
  100. return;
  101. await handle.close();
  102. await fs.promises.unlink(swap.path);
  103. ctx.locks.releaseLock(path);
  104. },
  105. });
  106. this.fs = fs;
  107. this.path = path;
  108. this.ctx = ctx;
  109. this.swap = swap;
  110. }
  111. /**
  112. * @sse https://developer.mozilla.org/en-US/docs/Web/API/FileSystemWritableFileStream/seek
  113. * @param position An `unsigned long` describing the byte position from the top
  114. * (beginning) of the file.
  115. */
  116. async seek(position) {
  117. this.swap.offset = position;
  118. }
  119. /**
  120. * @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemWritableFileStream/truncate
  121. * @param size An `unsigned long` of the amount of bytes to resize the stream to.
  122. */
  123. async truncate(size) {
  124. await this.swap.ready;
  125. const handle = this.swap.handle;
  126. if (!handle)
  127. throw new Error('Invalid state');
  128. await handle.truncate(size);
  129. if (this.swap.offset > size)
  130. this.swap.offset = size;
  131. }
  132. async writeBase(chunk) {
  133. const writer = this.getWriter();
  134. try {
  135. await writer.write(chunk);
  136. }
  137. finally {
  138. writer.releaseLock();
  139. }
  140. }
  141. async write(params) {
  142. if (!params)
  143. throw new TypeError('Missing required argument: params');
  144. switch (typeof params) {
  145. case 'string': {
  146. return this.writeBase(params);
  147. }
  148. case 'object': {
  149. const constructor = params.constructor;
  150. switch (constructor) {
  151. case ArrayBuffer:
  152. case Blob:
  153. case DataView:
  154. return this.writeBase(params);
  155. default: {
  156. if (ArrayBuffer.isView(params)) {
  157. return this.writeBase(params);
  158. }
  159. else {
  160. const options = params;
  161. switch (options.type) {
  162. case 'write': {
  163. if (typeof options.position === 'number')
  164. await this.seek(options.position);
  165. return this.writeBase(params.data);
  166. }
  167. case 'truncate': {
  168. if (typeof params.size !== 'number')
  169. throw new TypeError('Missing required argument: size');
  170. if (this.swap.offset > params.size)
  171. this.swap.offset = params.size;
  172. return this.truncate(params.size);
  173. }
  174. case 'seek':
  175. if (typeof params.position !== 'number')
  176. throw new TypeError('Missing required argument: position');
  177. return this.seek(params.position);
  178. default:
  179. throw new TypeError('Invalid argument: params');
  180. }
  181. }
  182. }
  183. }
  184. }
  185. default:
  186. throw new TypeError('Invalid argument: params');
  187. }
  188. }
  189. }
  190. exports.NodeFileSystemWritableFileStream = NodeFileSystemWritableFileStream;
  191. //# sourceMappingURL=NodeFileSystemWritableFileStream.js.map