From 97c0bc2641edf76257b2562dcb09898316e6cfd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Schottst=C3=A4dt?= Date: Sat, 4 Oct 2025 20:49:53 +0200 Subject: [PATCH 1/4] chore: extension-refactor --- drd-fs/src/web/extension.ts | 151 +++++++++++++++++++++--------------- drd-fs/src/web/memfs.ts | 57 +++++++++++--- index.html | 2 +- 3 files changed, 137 insertions(+), 73 deletions(-) diff --git a/drd-fs/src/web/extension.ts b/drd-fs/src/web/extension.ts index b558618..b517e4a 100644 --- a/drd-fs/src/web/extension.ts +++ b/drd-fs/src/web/extension.ts @@ -3,88 +3,113 @@ import * as vscode from "vscode"; import { MemFS, WebDavOptions } from "./memfs"; -async function enableFs( - context: vscode.ExtensionContext, - webdavUrl: string, - credentials?: WebDavOptions -): Promise { - const memFs = new MemFS(webdavUrl, credentials); - - try { - await memFs.readDavDirectory("/"); - context.subscriptions.push(memFs); - - return memFs; - } catch (e) { - memFs.dispose(); - throw e; - } -} - export async function activate(context: vscode.ExtensionContext) { - /*const disposable = vscode.commands.registerCommand( - "drd-fs.helloWorld", - () => { - // The code you place here will be executed every time your command is executed + // Create MemFS instance without auto-registration + const memFs = new MemFS("", {}); // Start with empty URL and credentials - // Display a message box to the user - vscode.window.showInformationMessage( - "Hello World from drd-fs in a web extension host!" - ); - vscode.workspace.updateWorkspaceFolders(0, 0, { - uri: vscode.Uri.parse("memfs:/"), - name: "MemFS - Sample", - }); + // Register the file system provider immediately + const fsRegistration = vscode.workspace.registerFileSystemProvider( + "memfs", + memFs, + { + isCaseSensitive: true, } ); - context.subscriptions.push(disposable); - - //const webdavUrl = "http://localhost:8011"; - const webdavUrl = "http://localhost:9190/webdav"; - let apikey = "admin"; - let accessToken = undefined; - let pathPrefix = undefined;*/ + context.subscriptions.push(fsRegistration); + context.subscriptions.push(memFs); + // Get stored credentials let apikey = await context.secrets.get("druidfsprovider.apikey"); let accessToken = await context.secrets.get("druidfsprovider.accessToken"); let webdavUrl = await context.secrets.get("druidfsprovider.webdavUrl"); let pathPrefix = await context.secrets.get("druidfsprovider.pathPrefix"); + // If we have credentials, configure the MemFS immediately + if (webdavUrl && (apikey || accessToken)) { + try { + memFs.webdavUrl = webdavUrl; + await memFs.updateCredentials({ + basicAuthApikey: apikey, + accessToken, + prefix: pathPrefix, + }); + await memFs.readDavDirectory("/"); + + // Add workspace folder if it's not already added + const existingFolder = vscode.workspace.workspaceFolders?.find( + (folder) => folder.uri.scheme === "memfs" + ); + if (!existingFolder) { + vscode.workspace.updateWorkspaceFolders(0, 0, { + uri: vscode.Uri.parse("memfs:/"), + name: "Druid - Filesystem", + }); + } + + vscode.window.showInformationMessage("Connected to remote server."); + } catch (error) { + console.error("Failed to connect to remote server:", error); + vscode.window.showErrorMessage( + `Failed to connect to remote server: ${error}` + ); + } + } + context.messagePassingProtocol?.postMessage({ type: "ready" }); - let memFs: MemFS | undefined = undefined; + context.messagePassingProtocol?.onDidReceiveMessage(async (message) => { console.log("Received message:", message); if (message.type === "setCredentials") { - apikey = message.payload.apikey; - accessToken = message.payload.accessToken; - webdavUrl = message.payload.webdavUrl as string; - pathPrefix = message.payload.pathPrefix; + try { + vscode.window.showInformationMessage("Connecting to remote server..."); - if (memFs) { + // Store credentials for future sessions + await context.secrets.store( + "druidfsprovider.apikey", + message.payload.apikey || "" + ); + await context.secrets.store( + "druidfsprovider.accessToken", + message.payload.accessToken || "" + ); + await context.secrets.store( + "druidfsprovider.webdavUrl", + message.payload.webdavUrl || "" + ); + await context.secrets.store( + "druidfsprovider.pathPrefix", + message.payload.pathPrefix || "" + ); + + // Update credentials and URL + memFs.webdavUrl = message.payload.webdavUrl as string; await memFs.updateCredentials({ - basicAuthApikey: apikey, - accessToken, - prefix: pathPrefix, + basicAuthApikey: message.payload.apikey, + accessToken: message.payload.accessToken, + prefix: message.payload.pathPrefix, }); - console.log("Updated credentials for MemFS"); - return; - } - vscode.window.showInformationMessage("Connecting to remote server..."); - memFs = await enableFs(context, webdavUrl, { - basicAuthApikey: apikey, - accessToken, - prefix: pathPrefix, - }); + // Test the connection + await memFs.readDavDirectory("/"); - vscode.workspace.updateWorkspaceFolders(0, 0, { - uri: vscode.Uri.parse("memfs:/"), - name: "Druid - Filesystem", - }); - //vscode.workspace.registerFileSystemProvider("memfs", memFs, { - // isCaseSensitive: true, - //}); - vscode.window.showInformationMessage("Connected to remote server."); + // Add workspace folder if it's not already added + const existingFolder = vscode.workspace.workspaceFolders?.find( + (folder) => folder.uri.scheme === "memfs" + ); + if (!existingFolder) { + vscode.workspace.updateWorkspaceFolders(0, 0, { + uri: vscode.Uri.parse("memfs:/"), + name: "Druid - Filesystem", + }); + } + + vscode.window.showInformationMessage("Connected to remote server."); + } catch (error) { + console.error("Failed to connect to remote server:", error); + vscode.window.showErrorMessage( + `Failed to connect to remote server: ${error}` + ); + } } }); } diff --git a/drd-fs/src/web/memfs.ts b/drd-fs/src/web/memfs.ts index e20ce02..7e1ce25 100644 --- a/drd-fs/src/web/memfs.ts +++ b/drd-fs/src/web/memfs.ts @@ -7,12 +7,12 @@ import { Disposable, EventEmitter, FileChangeEvent, + FileChangeType, FileStat, FileSystemError, FileSystemProvider, FileType, Uri, - workspace, } from "vscode"; import { XMLParser } from "fast-xml-parser"; @@ -65,22 +65,40 @@ export type Entry = File | Directory; export class MemFS implements FileSystemProvider, Disposable { static scheme = "memfs"; private wedavUrl: string; + private _isInitialized = false; + private _emitter = new EventEmitter(); + private readonly disposables: Disposable[] = []; - private readonly disposable: Disposable; + get webdavUrl(): string { + return this.wedavUrl; + } + + set webdavUrl(value: string) { + this.wedavUrl = value.replace(/\/$/, ""); + } constructor(wedavUrl: string, private webdavOptions?: WebDavOptions) { //set the webdav url but strip the trailing slash, if any this.wedavUrl = wedavUrl.replace(/\/$/, ""); + this.disposables.push(this._emitter); - this.disposable = Disposable.from( - workspace.registerFileSystemProvider(MemFS.scheme, this, { - isCaseSensitive: true, - }) + // Mark as initialized if we have both URL and credentials + this._isInitialized = !!( + wedavUrl && + (webdavOptions?.basicAuthApikey || webdavOptions?.accessToken) ); } dispose() { - this.disposable?.dispose(); + this.disposables.forEach((d) => d.dispose()); + } + + private ensureInitialized(): void { + if (!this._isInitialized) { + throw FileSystemError.Unavailable( + "File system not yet initialized. Please configure credentials first." + ); + } } private getAuthHeader() { @@ -184,6 +202,7 @@ export class MemFS implements FileSystemProvider, Disposable { root = new Directory(Uri.parse("memfs:/"), ""); async stat(uri: Uri): Promise { + this.ensureInitialized(); const data = await this.readDavDirectory(uri.path); if (data[0]) { @@ -198,6 +217,7 @@ export class MemFS implements FileSystemProvider, Disposable { } async readDirectory(uri: Uri): Promise<[string, FileType][]> { + this.ensureInitialized(); const list = await this.readDavDirectory(uri.path); const { prefix = "" } = this.webdavOptions || {}; @@ -224,6 +244,7 @@ export class MemFS implements FileSystemProvider, Disposable { // --- manage file contents async readFile(uri: Uri): Promise { + this.ensureInitialized(); const res = await this.davRequest(uri.path, { method: "GET", body: undefined, @@ -238,6 +259,7 @@ export class MemFS implements FileSystemProvider, Disposable { content: Uint8Array, options: { create: boolean; overwrite: boolean } ) { + this.ensureInitialized(); await this.davRequest(uri.path, { method: "PUT", body: content as any, @@ -247,6 +269,7 @@ export class MemFS implements FileSystemProvider, Disposable { // --- manage files/folders async rename(oldUri: Uri, newUri: Uri, options: { overwrite: boolean }) { + this.ensureInitialized(); const { prefix = "" } = this.webdavOptions || {}; await this.davRequest(oldUri.path, { @@ -258,12 +281,14 @@ export class MemFS implements FileSystemProvider, Disposable { } async delete(uri: Uri) { + this.ensureInitialized(); await this.davRequest(uri.path, { method: "DELETE", }); } async createDirectory(uri: Uri) { + this.ensureInitialized(); await this.davRequest(uri.path, { method: "MKCOL", }); @@ -271,10 +296,24 @@ export class MemFS implements FileSystemProvider, Disposable { async updateCredentials(options: WebDavOptions) { this.webdavOptions = options; + this._isInitialized = !!( + this.wedavUrl && + (options.basicAuthApikey || options.accessToken) + ); + + if (this._isInitialized) { + // Fire change events to refresh any open files + this._emitter.fire([ + { + type: FileChangeType.Changed, + uri: Uri.parse("memfs:/"), + }, + ]); + } } - onDidChangeFile() { - return new EventEmitter(); + get onDidChangeFile() { + return this._emitter.event; } watch( diff --git a/index.html b/index.html index 9a99950..92f442b 100644 --- a/index.html +++ b/index.html @@ -176,7 +176,7 @@ console.log("Auth: Starting token refresh"); try { const response = await fetch( - "https://auth.druid.gg/api/v1/refresh", + "https://auth.druid.gg/v1/refresh", { method: "POST", headers: { From 309c86575920f892ca864cb4267c325e6ff54691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Schottst=C3=A4dt?= Date: Sat, 4 Oct 2025 22:29:48 +0200 Subject: [PATCH 2/4] chore: wait for connection --- drd-fs/src/web/extension.ts | 69 +++++++++++++++++++++---------------- drd-fs/src/web/memfs.ts | 22 +++++++----- 2 files changed, 54 insertions(+), 37 deletions(-) diff --git a/drd-fs/src/web/extension.ts b/drd-fs/src/web/extension.ts index b517e4a..b226782 100644 --- a/drd-fs/src/web/extension.ts +++ b/drd-fs/src/web/extension.ts @@ -18,40 +18,51 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(fsRegistration); context.subscriptions.push(memFs); - // Get stored credentials - let apikey = await context.secrets.get("druidfsprovider.apikey"); - let accessToken = await context.secrets.get("druidfsprovider.accessToken"); - let webdavUrl = await context.secrets.get("druidfsprovider.webdavUrl"); - let pathPrefix = await context.secrets.get("druidfsprovider.pathPrefix"); + // Initialize credentials asynchronously and allow file operations to wait + initializeCredentials(); - // If we have credentials, configure the MemFS immediately - if (webdavUrl && (apikey || accessToken)) { + async function initializeCredentials() { try { - memFs.webdavUrl = webdavUrl; - await memFs.updateCredentials({ - basicAuthApikey: apikey, - accessToken, - prefix: pathPrefix, - }); - await memFs.readDavDirectory("/"); - - // Add workspace folder if it's not already added - const existingFolder = vscode.workspace.workspaceFolders?.find( - (folder) => folder.uri.scheme === "memfs" + // Get stored credentials + let apikey = await context.secrets.get("druidfsprovider.apikey"); + let accessToken = await context.secrets.get( + "druidfsprovider.accessToken" ); - if (!existingFolder) { - vscode.workspace.updateWorkspaceFolders(0, 0, { - uri: vscode.Uri.parse("memfs:/"), - name: "Druid - Filesystem", - }); - } + let webdavUrl = await context.secrets.get("druidfsprovider.webdavUrl"); + let pathPrefix = await context.secrets.get("druidfsprovider.pathPrefix"); + + // If we have credentials, configure the MemFS immediately + if (webdavUrl && (apikey || accessToken)) { + try { + memFs.webdavUrl = webdavUrl; + await memFs.updateCredentials({ + basicAuthApikey: apikey, + accessToken, + prefix: pathPrefix, + }); + await memFs.readDavDirectory("/"); - vscode.window.showInformationMessage("Connected to remote server."); + // Add workspace folder if it's not already added + const existingFolder = vscode.workspace.workspaceFolders?.find( + (folder) => folder.uri.scheme === "memfs" + ); + if (!existingFolder) { + vscode.workspace.updateWorkspaceFolders(0, 0, { + uri: vscode.Uri.parse("memfs:/"), + name: "Druid - Filesystem", + }); + } + + vscode.window.showInformationMessage("Connected to remote server."); + } catch (error) { + console.error("Failed to connect to remote server:", error); + vscode.window.showErrorMessage( + `Failed to connect to remote server: ${error}` + ); + } + } } catch (error) { - console.error("Failed to connect to remote server:", error); - vscode.window.showErrorMessage( - `Failed to connect to remote server: ${error}` - ); + console.error("Failed to initialize credentials:", error); } } diff --git a/drd-fs/src/web/memfs.ts b/drd-fs/src/web/memfs.ts index 7e1ce25..3913a55 100644 --- a/drd-fs/src/web/memfs.ts +++ b/drd-fs/src/web/memfs.ts @@ -93,7 +93,13 @@ export class MemFS implements FileSystemProvider, Disposable { this.disposables.forEach((d) => d.dispose()); } - private ensureInitialized(): void { + private async waitForInitialization( + maxWaitMs: number = 10000 + ): Promise { + const startTime = Date.now(); + while (!this._isInitialized && Date.now() - startTime < maxWaitMs) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } if (!this._isInitialized) { throw FileSystemError.Unavailable( "File system not yet initialized. Please configure credentials first." @@ -202,7 +208,7 @@ export class MemFS implements FileSystemProvider, Disposable { root = new Directory(Uri.parse("memfs:/"), ""); async stat(uri: Uri): Promise { - this.ensureInitialized(); + await this.waitForInitialization(); const data = await this.readDavDirectory(uri.path); if (data[0]) { @@ -217,7 +223,7 @@ export class MemFS implements FileSystemProvider, Disposable { } async readDirectory(uri: Uri): Promise<[string, FileType][]> { - this.ensureInitialized(); + await this.waitForInitialization(); const list = await this.readDavDirectory(uri.path); const { prefix = "" } = this.webdavOptions || {}; @@ -244,7 +250,7 @@ export class MemFS implements FileSystemProvider, Disposable { // --- manage file contents async readFile(uri: Uri): Promise { - this.ensureInitialized(); + await this.waitForInitialization(); const res = await this.davRequest(uri.path, { method: "GET", body: undefined, @@ -259,7 +265,7 @@ export class MemFS implements FileSystemProvider, Disposable { content: Uint8Array, options: { create: boolean; overwrite: boolean } ) { - this.ensureInitialized(); + await this.waitForInitialization(); await this.davRequest(uri.path, { method: "PUT", body: content as any, @@ -269,7 +275,7 @@ export class MemFS implements FileSystemProvider, Disposable { // --- manage files/folders async rename(oldUri: Uri, newUri: Uri, options: { overwrite: boolean }) { - this.ensureInitialized(); + await this.waitForInitialization(); const { prefix = "" } = this.webdavOptions || {}; await this.davRequest(oldUri.path, { @@ -281,14 +287,14 @@ export class MemFS implements FileSystemProvider, Disposable { } async delete(uri: Uri) { - this.ensureInitialized(); + await this.waitForInitialization(); await this.davRequest(uri.path, { method: "DELETE", }); } async createDirectory(uri: Uri) { - this.ensureInitialized(); + await this.waitForInitialization(); await this.davRequest(uri.path, { method: "MKCOL", }); From 0ee4ecece05780a80230accd25e26ebb8163df58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Schottst=C3=A4dt?= Date: Sat, 4 Oct 2025 22:44:49 +0200 Subject: [PATCH 3/4] chore: improve CI --- .github/workflows/pr.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 8ec4a96..99cfcbf 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -31,10 +31,14 @@ jobs: wranglerVersion: "4.35.0" apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} command: versions upload --preview-alias pr-${{ github.event.number }} - - name: 📬 Update deployment status + - name: 📬 Update PR with deployment preview uses: marocchino/sticky-pull-request-comment@v2 with: - append: true - # Only `deployment-url` is available message: | - | ${{ matrix.name }} | ${{ steps.deploy.outputs.deployment-url }} | + ## 🚀 Preview Deployment Ready! + + Your changes have been deployed to Cloudflare and are ready for preview: + + **Preview URL:** ${{ steps.deploy.outputs.deployment-url }} + + This preview will be automatically updated when you push new commits to this PR. From 6d30416742644cb256bb5efd70a00a2ce276a987 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Schottst=C3=A4dt?= Date: Sat, 4 Oct 2025 23:08:59 +0200 Subject: [PATCH 4/4] chore: extension better logging --- drd-fs/src/web/extension.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/drd-fs/src/web/extension.ts b/drd-fs/src/web/extension.ts index b226782..0db0ac5 100644 --- a/drd-fs/src/web/extension.ts +++ b/drd-fs/src/web/extension.ts @@ -4,6 +4,7 @@ import * as vscode from "vscode"; import { MemFS, WebDavOptions } from "./memfs"; export async function activate(context: vscode.ExtensionContext) { + console.log("Druid FS extension is now active!"); // Create MemFS instance without auto-registration const memFs = new MemFS("", {}); // Start with empty URL and credentials