From c6d118c4fa2d8adc45104ca0293a0e3510d69471 Mon Sep 17 00:00:00 2001 From: tyeth Date: Sun, 26 Oct 2025 02:07:06 +0000 Subject: [PATCH 1/2] wip(wifi workflow): DELETE method available doesn't represent writable on CPY 10.0.3 lilygot tdisplay S3, switch to fs json like circup --- js/common/web-file-transfer.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/js/common/web-file-transfer.js b/js/common/web-file-transfer.js index d918e57..6415cba 100644 --- a/js/common/web-file-transfer.js +++ b/js/common/web-file-transfer.js @@ -7,7 +7,11 @@ class FileTransferClient { async readOnly() { await this._checkConnection(); - return !this._allowedMethods.includes('DELETE'); + console.log("Checking read only"); + const response = await this._fetch("/fs/", {method: "GET", headers: {"Accept": "application/json"}}) + const result = await response.json(); + //TODO: Tyeth: cache this value until reconnection, as listdir / connect already fetch it + return result.writable === undefined || result.writable === false || !this._allowedMethods.includes("DELETE"); } async _checkConnection() { @@ -15,6 +19,7 @@ class FileTransferClient { throw new Error("Unable to perform file operation. Not Connected."); } + //TODO: Tyeth: reset this on reconnection if (this._allowedMethods === null) { const status = await this._fetch("/fs/", {method: "OPTIONS"}); this._allowedMethods = status.headers.get("Access-Control-Allow-Methods").split(/,/).map(method => {return method.trim().toUpperCase();}); From b0e2c2598fdeedb516cb3df78f18bc14aa8f4a43 Mon Sep 17 00:00:00 2001 From: Piclaw Date: Thu, 30 Apr 2026 15:22:27 -0700 Subject: [PATCH 2/2] Use /fs/ JSON 'writable' flag, cache it, drop DELETE-method check The previous readOnly() heuristic checked whether DELETE was advertised in the OPTIONS Access-Control-Allow-Methods response. On CircuitPython 10.0.3 (observed on a LilyGo T-Display S3) DELETE was advertised even when USB had claimed the filesystem and PUT requests were rejected with a free-form 'USB ACTIVE, try resetting board' body that the editor never surfaced -- so saves silently failed instead of being skipped up front. Switch to the same source of truth circup uses: the 'writable' field in the /fs/ JSON response. Cache the value on the FileTransferClient so readOnly() doesn't fire an extra GET on every call, and let listDir() populate the cache when it runs (which already happens at connect time via web.js, so the typical hot path costs no extra round-trip). The cache resets naturally because FileTransferClient is reconstructed on every (re)connect. Co-authored-by: tyeth --- js/common/web-file-transfer.js | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/js/common/web-file-transfer.js b/js/common/web-file-transfer.js index 6415cba..ab2a010 100644 --- a/js/common/web-file-transfer.js +++ b/js/common/web-file-transfer.js @@ -3,15 +3,25 @@ class FileTransferClient { this.hostname = hostname; this.connectionStatus = connectionStatusCB; this._allowedMethods = null; + // Cached `writable` flag from the /fs/ JSON response. Populated by + // listDir() and by readOnly() when listDir hasn't been called yet. + // A new FileTransferClient is created on every (re)connect, so this + // cache resets naturally with the connection lifecycle. + this._writable = null; } async readOnly() { await this._checkConnection(); - console.log("Checking read only"); - const response = await this._fetch("/fs/", {method: "GET", headers: {"Accept": "application/json"}}) - const result = await response.json(); - //TODO: Tyeth: cache this value until reconnection, as listdir / connect already fetch it - return result.writable === undefined || result.writable === false || !this._allowedMethods.includes("DELETE"); + // Older CircuitPython releases advertised DELETE in OPTIONS even when + // the filesystem was actually read-only (USB had it), so we use the + // `writable` field from the /fs/ JSON response instead -- same source + // of truth as circup. If we already pulled it via listDir, reuse it. + if (this._writable === null) { + const response = await this._fetch("/fs/", {method: "GET", headers: {"Accept": "application/json"}}); + const result = await response.json(); + this._writable = result.writable === true; + } + return !this._writable; } async _checkConnection() { @@ -19,7 +29,6 @@ class FileTransferClient { throw new Error("Unable to perform file operation. Not Connected."); } - //TODO: Tyeth: reset this on reconnection if (this._allowedMethods === null) { const status = await this._fetch("/fs/", {method: "OPTIONS"}); this._allowedMethods = status.headers.get("Access-Control-Allow-Methods").split(/,/).map(method => {return method.trim().toUpperCase();}); @@ -136,7 +145,12 @@ class FileTransferClient { const response = await this._fetch(`/fs${path}`, {headers: {"Accept": "application/json"}}); const results = await response.json(); - let listings = results + // Cache the writable flag whenever the FS root response carries it, + // so readOnly() doesn't need a separate round-trip. + if (results.writable !== undefined) { + this._writable = results.writable === true; + } + let listings = results; if (results.files !== undefined) { listings = results.files; }