forked from circuitpython/web-editor
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathfsapi-file-transfer.js
355 lines (289 loc) · 11.4 KB
/
fsapi-file-transfer.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
import { get, set } from 'idb-keyval';
class FileTransferClient {
constructor(connectionStatusCB, uid) {
this.connectionStatus = connectionStatusCB;
this._dirHandle = null;
this._uid = uid;
}
async readOnly() {
return await this._readOnly();
}
async _readOnly(path = null) {
await this._checkConnection();
let folderHandle = this._dirHandle;
if (path) {
folderHandle = await this._getSubfolderHandle(path);
}
return !(await this._verifyPermission(folderHandle));
}
async _checkConnection() {
if (!this.connectionStatus(true)) {
throw new Error("Unable to perform file operation. Not Connected.");
}
if (!this._dirHandle) {
await this.loadDirHandle();
if (this._dirHandle) {
const info = await this.versionInfo();
console.log(info);
console.log("Found via REPL: " + this._uid);
if (info) {
console.log("Found via boot_out.txt: " + info.uid);
} else {
console.log("Unable to read boot_out.txt");
}
// TODO: This needs to be more reliable before we stop the user from continuing
if (info && info.uid && this._uid) {
if (this._uid == info.uid) {
console.log("UIDs found in REPL and boot_out.txt match!");
}
}
if (!info === null) {
// We're likely not in the root directory of the device because
// boot_out.txt probably wasn't found
}
// TODO: Verify this is a circuitpython drive
// Perhaps check boot_out.txt, Certain structural elements, etc.
// Not sure how to verify it's the same device that we are using webserial for
// Perhaps we can match something in boot_out.txt to the device name
// For now we're just going to trust the user
}
}
if (!this._dirHandle) {
throw new Error("Unable to perform file operation. No Working Folder Selected.");
}
}
async loadSavedDirHandle() {
try {
const savedDirHandle = await get('usb-working-directory');
// Request permission to make it writable
if (savedDirHandle && (await this._verifyPermission(savedDirHandle))) {
// Check if the stored directory is available. It will fail if not.
await savedDirHandle.getFileHandle("boot_out.txt");
this._dirHandle = savedDirHandle;
return true;
}
} catch (e) {
console.error("Unable to access boot_out.txt in saved directory handle:", e);
}
return false;
}
async loadDirHandle(preferSaved = true) {
if (preferSaved) {
const result = await this.loadSavedDirHandle();
if (!result) {
return true;
}
}
const dirHandle = await window.showDirectoryPicker({mode: 'readwrite'});
if (dirHandle) {
await set('usb-working-directory', dirHandle);
this._dirHandle = dirHandle;
return true;
}
return false;
}
getWorkingDirectoryName() {
if (this._dirHandle) {
return this._dirHandle.name;
}
return null;
}
async _verifyPermission(folderHandle) {
const options = {mode: 'readwrite'};
if (await folderHandle.queryPermission(options) === 'granted') {
return true;
}
if (await folderHandle.requestPermission(options) === 'granted') {
return true;
}
return false;
}
async readFile(path, raw = false) {
await this._checkConnection();
const [folder, filename] = this._splitPath(path);
try {
const folderHandle = await this._getSubfolderHandle(folder);
const fileHandle = await folderHandle.getFileHandle(filename);
const fileData = await fileHandle.getFile();
return raw ? fileData : await fileData.text();
} catch (e) {
return raw ? null : "";
}
}
async _checkWritable() {
if (await this.readOnly()) {
throw new Error("File System is Read Only.");
}
}
async writeFile(path, offset, contents, modificationTime = null, raw = false) {
await this._checkConnection();
await this._checkWritable();
/*if (modificationTime) {
console.warn("Setting modification time not currently supported in USB Workflow.");
}*/
if (!raw) {
let encoder = new TextEncoder();
let same = contents.slice(0, offset);
let different = contents.slice(offset);
offset = encoder.encode(same).byteLength;
contents = encoder.encode(different);
} else if (offset > 0) {
contents = contents.slice(offset);
}
const [folder, filename] = this._splitPath(path);
const folderHandle = await this._getSubfolderHandle(folder);
const fileHandle = await folderHandle.getFileHandle(filename, {create: true});
const writable = await fileHandle.createWritable();
if (offset > 0) {
await writable.seek(offset);
}
await writable.write(contents);
await writable.close();
}
_splitPath(path) {
let pathParts = path.split("/");
const filename = pathParts.pop();
const folder = pathParts.join("/");
return [folder, filename];
}
// Makes the directory and any missing parents
async makeDir(path, modificationTime = null) {
await this._checkConnection();
await this._checkWritable();
if (modificationTime) {
console.warn("Setting modification time not currently supported in USB Workflow.");
}
const [parentFolder, folderName] = this._splitPath(path);
const parentFolderHandle = await this._getSubfolderHandle(parentFolder, true);
for await (const [entryName, entryHandle] of parentFolderHandle.entries()) {
if (entryName === folderName) {
throw new Error("Folder already exists.");
}
}
await parentFolderHandle.getDirectoryHandle(folderName, { create: true });
return true;
}
// Returns an array of objects, one object for each file or directory in the given path
async listDir(path, subfolderHandle=null) {
await this._checkConnection();
let contents = [];
if (!subfolderHandle) {
subfolderHandle = await this._getSubfolderHandle(path);
}
// Get all files and folders in the folder
for await (const [filename, entryHandle] of subfolderHandle.entries()) {
let result = null;
if (entryHandle.kind === 'file') {
result = await entryHandle.getFile();
contents.push({
path: result.name,
isDir: false,
fileSize: result.size,
fileDate: Number(result.lastModified),
});
} else if (entryHandle.kind === 'directory') {
result = await entryHandle;
contents.push({
path: result.name,
isDir: true,
fileSize: 0,
fileDate: null,
});
}
}
return contents;
}
async _getSubfolderHandle(path, createIfMissing = false) {
if (!path.length || path.substr(-1) != "/") {
path += "/";
}
// Navigate to folder
let currentDirHandle = this._dirHandle;
const subfolders = path.split("/").slice(1, -1);
let currentPath = "/";
if (subfolders.length) {
for (const subfolder of subfolders) {
try {
if ((await this._getItemKind(currentDirHandle, subfolder)) === 'directory') {
currentDirHandle = await currentDirHandle.getDirectoryHandle(subfolder, {create: !this.readOnly() && createIfMissing});
currentPath += subfolder + "/";
} else {
return currentDirHandle;
}
} catch (e) {
if (e.name === 'NotFoundError') {
throw new Error(`Folder ${subfolder} not found in ${currentPath}`);
} else {
console.log(e.name);
throw e;
}
}
}
}
return currentDirHandle;
}
async _getItemKind(directoryHandle, itemName) {
for await (const [filename, entryHandle] of directoryHandle.entries()) {
if (filename === itemName) {
return entryHandle.kind;
}
}
return null;
}
// Deletes the file or directory at the given path. Directories must be empty.
async delete(path) {
await this._checkConnection();
await this._checkWritable();
const [parentFolder, itemName] = this._splitPath(path);
const parentFolderHandle = await this._getSubfolderHandle(parentFolder);
await parentFolderHandle.removeEntry(itemName);
return true;
}
// Moves the file or directory from oldPath to newPath.
async move(oldPath, newPath) {
await this._checkConnection();
await this._checkWritable();
// Check that this is a file and not a folder
const [oldPathFolder, oldItemName] = this._splitPath(oldPath);
const oldPathHandle = await this._getSubfolderHandle(oldPathFolder);
if (await this._getItemKind(oldPathHandle, oldItemName) == "directory") {
throw new Error("Folder moving is not supported.");
}
// Copy the fileby reading from the old path and writing to the new one
const fileData = await this.readFile(oldPath, true);
await this.writeFile(newPath, 0, fileData, null, true);
// Delete the old file
await this.delete(oldPath);
console.warn(`Attempting to Move from ${oldPath} to ${newPath}`);
return true;
}
async versionInfo() {
// Possibly open /boot_out.txt and read the version info
let versionInfo = {};
console.log("Reading version info");
let bootout = await this.readFile('/boot_out.txt', false);
console.log(bootout);
if (!bootout) {
console.error("Unable to read boot_out.txt");
return null;
}
bootout += "\n";
// Add these items as they are found
const searchItems = {
version: /Adafruit CircuitPython (.*?) on/,
build_date: /on ([0-9]{4}-[0-9]{2}-[0-9]{2});/,
board_name: /; (.*?) with/,
mcu_name: /with (.*?)\r?\n/,
board_id: /Board ID:(.*?)\r?\n/,
uid: /UID:([0-9A-F]{12,16})\r?\n/,
}
for (const [key, regex] of Object.entries(searchItems)) {
const match = bootout.match(regex);
if (match) {
versionInfo[key] = match[1];
}
}
return versionInfo;
}
}
export {FileTransferClient};