|
1 | 1 | import FileSystemHandle from './FileSystemHandle.js'
|
2 | 2 | import FileSystemWritableFileStream from './FileSystemWritableFileStream.js'
|
3 |
| -import { errors } from './util.js' |
4 |
| - |
5 |
| -const { INVALID, SYNTAX, GONE } = errors |
| 3 | +import './createWritable.js' |
6 | 4 |
|
7 | 5 | const kAdapter = Symbol('adapter')
|
8 | 6 |
|
@@ -46,184 +44,5 @@ Object.defineProperties(FileSystemFileHandle.prototype, {
|
46 | 44 | getFile: { enumerable: true }
|
47 | 45 | })
|
48 | 46 |
|
49 |
| -// Safari doesn't support async createWritable streams yet. |
50 |
| -if ( |
51 |
| - globalThis.FileSystemFileHandle && |
52 |
| - !globalThis.FileSystemFileHandle.prototype.createWritable |
53 |
| -) { |
54 |
| - const wm = new WeakMap() |
55 |
| - |
56 |
| - let workerUrl |
57 |
| - |
58 |
| - // Worker code that should be inlined (can't use any external functions) |
59 |
| - const code = () => { |
60 |
| - let fileHandle, handle |
61 |
| - |
62 |
| - onmessage = async evt => { |
63 |
| - const port = evt.ports[0] |
64 |
| - const cmd = evt.data |
65 |
| - switch (cmd.type) { |
66 |
| - case 'open': |
67 |
| - const file = cmd.name |
68 |
| - |
69 |
| - let dir = await navigator.storage.getDirectory() |
70 |
| - |
71 |
| - for (const folder of cmd.path) { |
72 |
| - dir = await dir.getDirectoryHandle(folder) |
73 |
| - } |
74 |
| - |
75 |
| - fileHandle = await dir.getFileHandle(file) |
76 |
| - handle = await fileHandle.createSyncAccessHandle() |
77 |
| - break |
78 |
| - case 'write': |
79 |
| - handle.write(cmd.data, { at: cmd.position }) |
80 |
| - handle.flush() |
81 |
| - break |
82 |
| - case 'truncate': |
83 |
| - handle.truncate(cmd.size) |
84 |
| - break |
85 |
| - case 'abort': |
86 |
| - case 'close': |
87 |
| - handle.close() |
88 |
| - break |
89 |
| - } |
90 |
| - |
91 |
| - port.postMessage(0) |
92 |
| - } |
93 |
| - } |
94 |
| - |
95 |
| - |
96 |
| - globalThis.FileSystemFileHandle.prototype.createWritable = async function (options) { |
97 |
| - // Safari only support writing data in a worker with sync access handle. |
98 |
| - if (!workerUrl) { |
99 |
| - const stringCode = `(${code.toString()})()` |
100 |
| - const blob = new Blob([stringCode], { |
101 |
| - type: 'text/javascript' |
102 |
| - }) |
103 |
| - workerUrl = URL.createObjectURL(blob) |
104 |
| - } |
105 |
| - const worker = new Worker(workerUrl, { type: 'module' }) |
106 |
| - |
107 |
| - let position = 0 |
108 |
| - const textEncoder = new TextEncoder() |
109 |
| - let size = await this.getFile().then(file => file.size) |
110 |
| - |
111 |
| - const send = message => new Promise((resolve, reject) => { |
112 |
| - const mc = new MessageChannel() |
113 |
| - mc.port1.onmessage = evt => { |
114 |
| - if (evt.data instanceof Error) reject(evt.data) |
115 |
| - else resolve(evt.data) |
116 |
| - mc.port1.close() |
117 |
| - mc.port2.close() |
118 |
| - mc.port1.onmessage = null |
119 |
| - } |
120 |
| - worker.postMessage(message, [mc.port2]) |
121 |
| - }) |
122 |
| - |
123 |
| - // Safari also don't support transferable file system handles. |
124 |
| - // So we need to pass the path to the worker. This is a bit hacky and ugly. |
125 |
| - const root = await navigator.storage.getDirectory() |
126 |
| - const parent = await wm.get(this) |
127 |
| - const path = await root.resolve(parent) |
128 |
| - |
129 |
| - // Should likely never happen, but just in case... |
130 |
| - if (path === null) throw new DOMException(...GONE) |
131 |
| - |
132 |
| - let controller |
133 |
| - await send({ type: 'open', path, name: this.name }) |
134 |
| - |
135 |
| - if (options?.keepExistingData === false) { |
136 |
| - await send({ type: 'truncate', size: 0 }) |
137 |
| - size = 0 |
138 |
| - } |
139 |
| - |
140 |
| - const ws = new FileSystemWritableFileStream({ |
141 |
| - start: ctrl => { |
142 |
| - controller = ctrl |
143 |
| - }, |
144 |
| - async write(chunk) { |
145 |
| - const isPlainObject = chunk?.constructor === Object |
146 |
| - |
147 |
| - if (isPlainObject) { |
148 |
| - chunk = { ...chunk } |
149 |
| - } else { |
150 |
| - chunk = { type: 'write', data: chunk, position } |
151 |
| - } |
152 |
| - |
153 |
| - if (chunk.type === 'write') { |
154 |
| - if (!('data' in chunk)) { |
155 |
| - await send({ type: 'close' }) |
156 |
| - throw new DOMException(...SYNTAX('write requires a data argument')) |
157 |
| - } |
158 |
| - |
159 |
| - chunk.position ??= position |
160 |
| - |
161 |
| - if (typeof chunk.data === 'string') { |
162 |
| - chunk.data = textEncoder.encode(chunk.data) |
163 |
| - } |
164 |
| - |
165 |
| - else if (chunk.data instanceof ArrayBuffer) { |
166 |
| - chunk.data = new Uint8Array(chunk.data) |
167 |
| - } |
168 |
| - |
169 |
| - else if (!(chunk.data instanceof Uint8Array) && ArrayBuffer.isView(chunk.data)) { |
170 |
| - chunk.data = new Uint8Array(chunk.data.buffer, chunk.data.byteOffset, chunk.data.byteLength) |
171 |
| - } |
172 |
| - |
173 |
| - else if (!(chunk.data instanceof Uint8Array)) { |
174 |
| - const ab = await new Response(chunk.data).arrayBuffer() |
175 |
| - chunk.data = new Uint8Array(ab) |
176 |
| - } |
177 |
| - |
178 |
| - if (Number.isInteger(chunk.position) && chunk.position >= 0) { |
179 |
| - position = chunk.position |
180 |
| - } |
181 |
| - position += chunk.data.byteLength |
182 |
| - size += chunk.data.byteLength |
183 |
| - } else if (chunk.type === 'seek') { |
184 |
| - if (Number.isInteger(chunk.position) && chunk.position >= 0) { |
185 |
| - if (size < chunk.position) { |
186 |
| - throw new DOMException(...INVALID) |
187 |
| - } |
188 |
| - console.log('seeking', chunk) |
189 |
| - position = chunk.position |
190 |
| - return // Don't need to enqueue seek... |
191 |
| - } else { |
192 |
| - await send({ type: 'close' }) |
193 |
| - throw new DOMException(...SYNTAX('seek requires a position argument')) |
194 |
| - } |
195 |
| - } else if (chunk.type === 'truncate') { |
196 |
| - if (Number.isInteger(chunk.size) && chunk.size >= 0) { |
197 |
| - size = chunk.size |
198 |
| - if (position > size) { position = size } |
199 |
| - } else { |
200 |
| - await send({ type: 'close' }) |
201 |
| - throw new DOMException(...SYNTAX('truncate requires a size argument')) |
202 |
| - } |
203 |
| - } |
204 |
| - |
205 |
| - await send(chunk) |
206 |
| - }, |
207 |
| - async close () { |
208 |
| - await send({ type: 'close' }) |
209 |
| - worker.terminate() |
210 |
| - }, |
211 |
| - async abort (reason) { |
212 |
| - await send({ type: 'abort', reason }) |
213 |
| - worker.terminate() |
214 |
| - }, |
215 |
| - }) |
216 |
| - |
217 |
| - return ws |
218 |
| - } |
219 |
| - |
220 |
| - const orig = FileSystemDirectoryHandle.prototype.getFileHandle |
221 |
| - FileSystemDirectoryHandle.prototype.getFileHandle = async function (...args) { |
222 |
| - const handle = await orig.call(this, ...args) |
223 |
| - wm.set(handle, this) |
224 |
| - return handle |
225 |
| - } |
226 |
| -} |
227 |
| - |
228 | 47 | export default FileSystemFileHandle
|
229 | 48 | export { FileSystemFileHandle }
|
0 commit comments