Skip to content

Commit 04c313a

Browse files
committed
USB File Transfer working for non-CIRCUITPY boards
1 parent 16292dd commit 04c313a

12 files changed

Lines changed: 818 additions & 565 deletions

index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@
179179
<button class="purple-button ok-button">Ok</button>
180180
</div>
181181
</div>
182-
<div class="popup-modal shadow prompt" data-popup-modal="progress">
182+
<div class="popup-modal shadow prompt" data-popup-modal="progress" data-tabbable="false">
183183
<div class="label centered" id="status"></div>
184184
<div class="label centered" id="percentage"></div>
185185
<progress value="0"></progress>

js/common/dialogs.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ class GenericModal {
101101
this._modalLayerId = modalLayers.length;
102102
modal.style.zIndex = BLACKOUT_ZINDEX + 1 + (this._modalLayerId * 2);
103103

104-
if (!this._trap){
104+
if (!this._trap && modal.dataset.tabbable !== "false"){
105105
this._trap = focusTrap.createFocusTrap(modal, {
106106
initialFocus: () => modal,
107107
allowOutsideClick: true,

js/common/file_dialog.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ class FileDialog extends GenericModal {
183183
this._addFile({path: "..", isDir: true}, "fa-folder-open");
184184
}
185185
if (!this._fileHelper) {
186-
console.log("no client");
186+
console.error("no client");
187187
return;
188188
}
189189

@@ -196,7 +196,7 @@ class FileDialog extends GenericModal {
196196
this._addFile(fileObj);
197197
}
198198
} catch (e) {
199-
console.log(e);
199+
console.error(e);
200200
}
201201
this._setElementValue('fileNameField', "");
202202
this._setElementEnabled('okButton', this._validSelectableFolder());

js/common/fsapi-file-transfer.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ class FileTransferClient {
8484

8585
async loadDirHandle(preferSaved = true) {
8686
if (preferSaved) {
87-
const result = await loadSavedDirHandle();
87+
const result = await this.loadSavedDirHandle();
8888
if (!result) {
8989
return true;
9090
}

js/common/repl-file-transfer.js

Lines changed: 20 additions & 255 deletions
Original file line numberDiff line numberDiff line change
@@ -1,156 +1,47 @@
1-
import {FileOps} from '@adafruit/circuitpython-repl-js';
1+
import {FileOps} from 'circuitpython-repl-js';
22

33
class FileTransferClient {
44
constructor(connectionStatusCB, repl) {
55
this.connectionStatus = connectionStatusCB;
66
this._dirHandle = null;
7-
this._fileops = new FileOps(repl);
7+
this._fileops = new FileOps(repl, false);
8+
this._isReadOnly = null;
89
}
910

1011
async readOnly() {
11-
return await this._readOnly();
12-
}
13-
14-
async _readOnly(path = null) {
1512
await this._checkConnection();
16-
17-
let folderHandle = this._dirHandle;
18-
if (path) {
19-
folderHandle = await this._getSubfolderHandle(path);
20-
}
21-
22-
return !(await this._verifyPermission(folderHandle));
13+
return this._isReadOnly;
2314
}
2415

2516
async _checkConnection() {
26-
//
2717
if (!this.connectionStatus(true)) {
2818
throw new Error("Unable to perform file operation. Not Connected.");
2919
}
3020

31-
if (!this._dirHandle) {
32-
await this.loadDirHandle();
33-
34-
if (this._dirHandle) {
35-
const info = await this.versionInfo();
36-
console.log(info);
37-
console.log("Found via REPL: " + this._uid);
38-
if (info) {
39-
console.log("Found via boot_out.txt: " + info.uid);
40-
} else {
41-
console.log("Unable to read boot_out.txt");
42-
}
43-
44-
// TODO: This needs to be more reliable before we stop the user from continuing
45-
if (info && info.uid && this._uid) {
46-
if (this._uid == info.uid) {
47-
console.log("UIDs found in REPL and boot_out.txt match!");
48-
}
49-
}
50-
51-
if (!info === null) {
52-
// We're likely not in the root directory of the device because
53-
// boot_out.txt probably wasn't found
54-
}
55-
56-
// TODO: Verify this is a circuitpython drive
57-
// Perhaps check boot_out.txt, Certain structural elements, etc.
58-
// Not sure how to verify it's the same device that we are using webserial for
59-
// Perhaps we can match something in boot_out.txt to the device name
60-
61-
// For now we're just going to trust the user
62-
}
63-
}
64-
65-
if (!this._dirHandle) {
66-
throw new Error("Unable to perform file operation. No Working Folder Selected.");
67-
}
68-
}
69-
70-
async loadSavedDirHandle() {
71-
try {
72-
const savedDirHandle = await get('usb-working-directory');
73-
// Request permission to make it writable
74-
if (savedDirHandle && (await this._verifyPermission(savedDirHandle))) {
75-
// Check if the stored directory is available. It will fail if not.
76-
await savedDirHandle.getFileHandle("boot_out.txt");
77-
this._dirHandle = savedDirHandle;
78-
return true;
79-
}
80-
} catch (e) {
81-
console.error("Unable to access boot_out.txt in saved directory handle:", e);
82-
}
83-
return false;
84-
}
85-
86-
async loadDirHandle(preferSaved = true) {
87-
if (preferSaved) {
88-
const result = await loadSavedDirHandle();
89-
if (!result) {
90-
return true;
91-
}
92-
}
93-
94-
const dirHandle = await window.showDirectoryPicker({mode: 'readwrite'});
95-
if (dirHandle) {
96-
await set('usb-working-directory', dirHandle);
97-
this._dirHandle = dirHandle;
98-
return true;
21+
if (this._isReadOnly === null) {
22+
this._isReadOnly = await this._fileops.isReadOnly();
9923
}
100-
return false;
10124
}
10225

103-
getWorkingDirectoryName() {
104-
if (this._dirHandle) {
105-
return this._dirHandle.name;
26+
async _checkWritable() {
27+
if (await this.readOnly()) {
28+
throw new Error("File System is Read Only.");
10629
}
107-
return null;
108-
}
109-
110-
async _verifyPermission(folderHandle) {
111-
const options = {mode: 'readwrite'};
112-
113-
if (await folderHandle.queryPermission(options) === 'granted') {
114-
return true;
115-
}
116-
117-
if (await folderHandle.requestPermission(options) === 'granted') {
118-
return true;
119-
}
120-
121-
return false;
12230
}
12331

12432
async readFile(path, raw = false) {
12533
await this._checkConnection();
126-
127-
const [folder, filename] = this._splitPath(path);
128-
129-
try {
130-
const folderHandle = await this._getSubfolderHandle(folder);
131-
const fileHandle = await folderHandle.getFileHandle(filename);
132-
const fileData = await fileHandle.getFile();
133-
134-
return raw ? fileData : await fileData.text();
135-
} catch (e) {
34+
let contents = await this._fileops.readFile(path, raw);
35+
if (contents === null) {
13636
return raw ? null : "";
13737
}
38+
return contents;
13839
}
13940

140-
async _checkWritable() {
141-
if (await this.readOnly()) {
142-
throw new Error("File System is Read Only.");
143-
}
144-
}
145-
146-
async writeFile(path, offset, contents, modificationTime = null, raw = false) {
41+
async writeFile(path, offset, contents, modificationTime, raw = false) {
14742
await this._checkConnection();
14843
await this._checkWritable();
14944

150-
/*if (modificationTime) {
151-
console.warn("Setting modification time not currently supported in USB Workflow.");
152-
}*/
153-
15445
if (!raw) {
15546
let encoder = new TextEncoder();
15647
let same = contents.slice(0, offset);
@@ -161,162 +52,36 @@ class FileTransferClient {
16152
contents = contents.slice(offset);
16253
}
16354

164-
const [folder, filename] = this._splitPath(path);
165-
166-
const folderHandle = await this._getSubfolderHandle(folder);
167-
const fileHandle = await folderHandle.getFileHandle(filename, {create: true});
168-
169-
const writable = await fileHandle.createWritable();
170-
if (offset > 0) {
171-
await writable.seek(offset);
172-
}
173-
await writable.write(contents);
174-
await writable.close();
55+
return await this._fileops.writeFile(path, contents, offset, modificationTime, raw);
17556
}
17657

177-
_splitPath(path) {
178-
let pathParts = path.split("/");
179-
const filename = pathParts.pop();
180-
const folder = pathParts.join("/");
181-
182-
return [folder, filename];
183-
}
184-
185-
// Makes the directory and any missing parents
186-
async makeDir(path, modificationTime = null) {
58+
async makeDir(path, modificationTime = Date.now()) {
18759
await this._checkConnection();
18860
await this._checkWritable();
18961

190-
if (modificationTime) {
191-
console.warn("Setting modification time not currently supported in USB Workflow.");
192-
}
193-
194-
const [parentFolder, folderName] = this._splitPath(path);
195-
const parentFolderHandle = await this._getSubfolderHandle(parentFolder, true);
196-
197-
for await (const [entryName, entryHandle] of parentFolderHandle.entries()) {
198-
if (entryName === folderName) {
199-
throw new Error("Folder already exists.");
200-
}
201-
}
202-
203-
await parentFolderHandle.getDirectoryHandle(folderName, { create: true });
204-
205-
return true;
62+
return await this._fileops.makeDir(path, modificationTime);
20663
}
20764

20865
// Returns an array of objects, one object for each file or directory in the given path
209-
async listDir(path, subfolderHandle=null) {
66+
async listDir(path) {
21067
await this._checkConnection();
211-
212-
let contents = [];
213-
if (!subfolderHandle) {
214-
subfolderHandle = await this._getSubfolderHandle(path);
215-
}
216-
217-
// Get all files and folders in the folder
218-
for await (const [filename, entryHandle] of subfolderHandle.entries()) {
219-
let result = null;
220-
if (entryHandle.kind === 'file') {
221-
result = await entryHandle.getFile();
222-
contents.push({
223-
path: result.name,
224-
isDir: false,
225-
fileSize: result.size,
226-
fileDate: Number(result.lastModified),
227-
});
228-
} else if (entryHandle.kind === 'directory') {
229-
result = await entryHandle;
230-
contents.push({
231-
path: result.name,
232-
isDir: true,
233-
fileSize: 0,
234-
fileDate: null,
235-
});
236-
}
237-
}
238-
239-
return contents;
240-
}
241-
242-
async _getSubfolderHandle(path, createIfMissing = false) {
243-
if (!path.length || path.substr(-1) != "/") {
244-
path += "/";
245-
}
246-
247-
// Navigate to folder
248-
let currentDirHandle = this._dirHandle;
249-
const subfolders = path.split("/").slice(1, -1);
250-
let currentPath = "/";
251-
252-
if (subfolders.length) {
253-
for (const subfolder of subfolders) {
254-
try {
255-
if ((await this._getItemKind(currentDirHandle, subfolder)) === 'directory') {
256-
currentDirHandle = await currentDirHandle.getDirectoryHandle(subfolder, {create: !this.readOnly() && createIfMissing});
257-
currentPath += subfolder + "/";
258-
} else {
259-
return currentDirHandle;
260-
}
261-
} catch (e) {
262-
if (e.name === 'NotFoundError') {
263-
throw new Error(`Folder ${subfolder} not found in ${currentPath}`);
264-
} else {
265-
console.log(e.name);
266-
throw e;
267-
}
268-
}
269-
}
270-
}
271-
272-
return currentDirHandle;
273-
}
274-
275-
async _getItemKind(directoryHandle, itemName) {
276-
for await (const [filename, entryHandle] of directoryHandle.entries()) {
277-
if (filename === itemName) {
278-
return entryHandle.kind;
279-
}
280-
}
281-
282-
return null;
68+
return await this._fileops.listDir(path);
28369
}
28470

28571
// Deletes the file or directory at the given path. Directories must be empty.
28672
async delete(path) {
28773
await this._checkConnection();
28874
await this._checkWritable();
28975

290-
const [parentFolder, itemName] = this._splitPath(path);
291-
const parentFolderHandle = await this._getSubfolderHandle(parentFolder);
292-
293-
await parentFolderHandle.removeEntry(itemName);
294-
295-
return true;
76+
return await this._fileops.delete(path);
29677
}
29778

29879
// Moves the file or directory from oldPath to newPath.
29980
async move(oldPath, newPath) {
30081
await this._checkConnection();
30182
await this._checkWritable();
30283

303-
// Check that this is a file and not a folder
304-
const [oldPathFolder, oldItemName] = this._splitPath(oldPath);
305-
const oldPathHandle = await this._getSubfolderHandle(oldPathFolder);
306-
if (await this._getItemKind(oldPathHandle, oldItemName) == "directory") {
307-
throw new Error("Folder moving is not supported.");
308-
}
309-
310-
// Copy the fileby reading from the old path and writing to the new one
311-
const fileData = await this.readFile(oldPath, true);
312-
await this.writeFile(newPath, 0, fileData, null, true);
313-
314-
// Delete the old file
315-
await this.delete(oldPath);
316-
317-
console.warn(`Attempting to Move from ${oldPath} to ${newPath}`);
318-
319-
return true;
84+
return await this._fileops.move(oldPath, newPath);
32085
}
32186

32287
async versionInfo() {

0 commit comments

Comments
 (0)