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. diff --git a/drd-fs/src/web/extension.ts b/drd-fs/src/web/extension.ts index b558618..0db0ac5 100644 --- a/drd-fs/src/web/extension.ts +++ b/drd-fs/src/web/extension.ts @@ -3,88 +3,125 @@ 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 + console.log("Druid FS extension is now active!"); + // 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); + context.subscriptions.push(fsRegistration); + context.subscriptions.push(memFs); + + // Initialize credentials asynchronously and allow file operations to wait + initializeCredentials(); + + async function initializeCredentials() { + try { + // 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"); - //const webdavUrl = "http://localhost:8011"; - const webdavUrl = "http://localhost:9190/webdav"; - let apikey = "admin"; - let accessToken = undefined; - let pathPrefix = undefined;*/ + // 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("/"); - 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"); + // 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 initialize credentials:", 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..3913a55 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,46 @@ 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 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." + ); + } } private getAuthHeader() { @@ -184,6 +208,7 @@ export class MemFS implements FileSystemProvider, Disposable { root = new Directory(Uri.parse("memfs:/"), ""); async stat(uri: Uri): Promise { + await this.waitForInitialization(); const data = await this.readDavDirectory(uri.path); if (data[0]) { @@ -198,6 +223,7 @@ export class MemFS implements FileSystemProvider, Disposable { } async readDirectory(uri: Uri): Promise<[string, FileType][]> { + await this.waitForInitialization(); const list = await this.readDavDirectory(uri.path); const { prefix = "" } = this.webdavOptions || {}; @@ -224,6 +250,7 @@ export class MemFS implements FileSystemProvider, Disposable { // --- manage file contents async readFile(uri: Uri): Promise { + await this.waitForInitialization(); const res = await this.davRequest(uri.path, { method: "GET", body: undefined, @@ -238,6 +265,7 @@ export class MemFS implements FileSystemProvider, Disposable { content: Uint8Array, options: { create: boolean; overwrite: boolean } ) { + await this.waitForInitialization(); await this.davRequest(uri.path, { method: "PUT", body: content as any, @@ -247,6 +275,7 @@ export class MemFS implements FileSystemProvider, Disposable { // --- manage files/folders async rename(oldUri: Uri, newUri: Uri, options: { overwrite: boolean }) { + await this.waitForInitialization(); const { prefix = "" } = this.webdavOptions || {}; await this.davRequest(oldUri.path, { @@ -258,12 +287,14 @@ export class MemFS implements FileSystemProvider, Disposable { } async delete(uri: Uri) { + await this.waitForInitialization(); await this.davRequest(uri.path, { method: "DELETE", }); } async createDirectory(uri: Uri) { + await this.waitForInitialization(); await this.davRequest(uri.path, { method: "MKCOL", }); @@ -271,10 +302,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: {