Skip to content

Commit 8a48c79

Browse files
committed
Initial changes
1 parent d7591ed commit 8a48c79

6 files changed

Lines changed: 368 additions & 3 deletions

File tree

js/common/repl-file-transfer.js

Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
import {FileOps} from '@adafruit/circuitpython-repl-js';
2+
3+
class FileTransferClient {
4+
constructor(connectionStatusCB, repl) {
5+
this.connectionStatus = connectionStatusCB;
6+
this._dirHandle = null;
7+
this._fileops = new FileOps(repl);
8+
}
9+
10+
async readOnly() {
11+
return await this._readOnly();
12+
}
13+
14+
async _readOnly(path = null) {
15+
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));
23+
}
24+
25+
async _checkConnection() {
26+
//
27+
if (!this.connectionStatus(true)) {
28+
throw new Error("Unable to perform file operation. Not Connected.");
29+
}
30+
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;
99+
}
100+
return false;
101+
}
102+
103+
getWorkingDirectoryName() {
104+
if (this._dirHandle) {
105+
return this._dirHandle.name;
106+
}
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;
122+
}
123+
124+
async readFile(path, raw = false) {
125+
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) {
136+
return raw ? null : "";
137+
}
138+
}
139+
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) {
147+
await this._checkConnection();
148+
await this._checkWritable();
149+
150+
/*if (modificationTime) {
151+
console.warn("Setting modification time not currently supported in USB Workflow.");
152+
}*/
153+
154+
if (!raw) {
155+
let encoder = new TextEncoder();
156+
let same = contents.slice(0, offset);
157+
let different = contents.slice(offset);
158+
offset = encoder.encode(same).byteLength;
159+
contents = encoder.encode(different);
160+
} else if (offset > 0) {
161+
contents = contents.slice(offset);
162+
}
163+
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();
175+
}
176+
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) {
187+
await this._checkConnection();
188+
await this._checkWritable();
189+
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;
206+
}
207+
208+
// Returns an array of objects, one object for each file or directory in the given path
209+
async listDir(path, subfolderHandle=null) {
210+
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;
283+
}
284+
285+
// Deletes the file or directory at the given path. Directories must be empty.
286+
async delete(path) {
287+
await this._checkConnection();
288+
await this._checkWritable();
289+
290+
const [parentFolder, itemName] = this._splitPath(path);
291+
const parentFolderHandle = await this._getSubfolderHandle(parentFolder);
292+
293+
await parentFolderHandle.removeEntry(itemName);
294+
295+
return true;
296+
}
297+
298+
// Moves the file or directory from oldPath to newPath.
299+
async move(oldPath, newPath) {
300+
await this._checkConnection();
301+
await this._checkWritable();
302+
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;
320+
}
321+
322+
async versionInfo() {
323+
// Possibly open /boot_out.txt and read the version info
324+
let versionInfo = {};
325+
console.log("Reading version info");
326+
let bootout = await this.readFile('/boot_out.txt', false);
327+
console.log(bootout);
328+
if (!bootout) {
329+
return null;
330+
}
331+
332+
// Add these items as they are found
333+
const searchItems = {
334+
version: /Adafruit CircuitPython (.*?) on/,
335+
build_date: /on ([0-9]{4}-[0-9]{2}-[0-9]{2});/,
336+
board_name: /; (.*?) with/,
337+
mcu_name: /with (.*?)\r?\n/,
338+
board_id: /Board ID:(.*?)\r?\n/,
339+
uid: /UID:([0-9A-F]{12})\r?\n/,
340+
}
341+
342+
for (const [key, regex] of Object.entries(searchItems)) {
343+
const match = bootout.match(regex);
344+
345+
if (match) {
346+
versionInfo[key] = match[1];
347+
}
348+
}
349+
350+
return versionInfo;
351+
}
352+
}
353+
354+
export {FileTransferClient};

js/workflows/usb.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {CONNTYPE, CONNSTATE} from '../constants.js';
22
import {Workflow} from './workflow.js';
33
import {GenericModal} from '../common/dialogs.js';
4-
import {FileTransferClient} from '../common/usb-file-transfer.js';
4+
import {FileTransferClient} from '../common/repl-file-transfer.js';
55

66
let btnRequestSerialDevice, btnSelectHostFolder, btnUseHostFolder, lblWorkingfolder;
77

@@ -246,7 +246,7 @@ class USBWorkflow extends Workflow {
246246
this.updateConnected(CONNSTATE.partial);
247247

248248
// At this point we should see if we should init the file client and check if have a saved dir handle
249-
this.initFileClient(new FileTransferClient(this.connectionStatus.bind(this), this._uid));
249+
this.initFileClient(new FileTransferClient(this.connectionStatus.bind(this), this.repl));
250250
const fileClient = this.fileHelper.getFileClient();
251251
const result = await fileClient.loadSavedDirHandle();
252252
if (result) {

js/workflows/workflow.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {REPL} from 'circuitpython-repl-js';
1+
import {REPL} from '@adafruit/circuitpython-repl-js';
22

33
import {FileHelper} from '../common/file.js';
44
import {UnsavedDialog} from '../common/dialogs.js';

0 commit comments

Comments
 (0)