Skip to content
This repository was archived by the owner on Jan 12, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
169 changes: 103 additions & 66 deletions drd-fs/src/web/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<MemFS> {
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}`
);
}
}
});
}
Expand Down
63 changes: 54 additions & 9 deletions drd-fs/src/web/memfs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import {
Disposable,
EventEmitter,
FileChangeEvent,
FileChangeType,
FileStat,
FileSystemError,
FileSystemProvider,
FileType,
Uri,
workspace,
} from "vscode";

import { XMLParser } from "fast-xml-parser";
Expand Down Expand Up @@ -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<FileChangeEvent[]>();
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<void> {
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() {
Expand Down Expand Up @@ -184,6 +208,7 @@ export class MemFS implements FileSystemProvider, Disposable {
root = new Directory(Uri.parse("memfs:/"), "");

async stat(uri: Uri): Promise<FileStat> {
await this.waitForInitialization();
const data = await this.readDavDirectory(uri.path);

if (data[0]) {
Expand All @@ -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 || {};
Expand All @@ -224,6 +250,7 @@ export class MemFS implements FileSystemProvider, Disposable {
// --- manage file contents

async readFile(uri: Uri): Promise<Uint8Array> {
await this.waitForInitialization();
const res = await this.davRequest(uri.path, {
method: "GET",
body: undefined,
Expand All @@ -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,
Expand All @@ -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, {
Expand All @@ -258,23 +287,39 @@ 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",
});
}

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<FileChangeEvent[]>();
get onDidChangeFile() {
return this._emitter.event;
}

watch(
Expand Down
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down