Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Add endpoints to VirtualFS #5065

Merged
merged 1 commit into from
Dec 12, 2023
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
228 changes: 204 additions & 24 deletions packages/cspell-io/src/VirtualFS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import type { DirEntry, Disposable, FileReference, FileResource, Stats } from '.

type UrlOrReference = URL | FileReference;

type NextProvider = (url: URL) => FileSystem | undefined;
type NextProvider = (url: URL) => ProviderFileSystem | undefined;

export interface VirtualFS extends Disposable {
registerFileSystemProvider(provider: FileSystemProvider): Disposable;

/**
* Get the fs for a given url.
*/
getFS(url: URL): FileSystem | undefined;
getFS(url: URL): FileSystem;

/**
* The file system. All requests will first use getFileSystem to get the file system before making the request.
Expand All @@ -25,29 +25,73 @@ export interface VirtualFS extends Disposable {
reset(): void;
}

export interface FileSystem extends Disposable {
export enum FSCapabilityFlags {
None = 0,
Stat = 1 << 0,
Read = 1 << 1,
Write = 1 << 2,
ReadWrite = Read | Write,
ReadDir = 1 << 3,
WriteDir = 1 << 4,
ReadWriteDir = ReadDir | WriteDir,
}

interface FileSystemProviderInfo {
name: string;
}

interface FileSystemBase {
stat(url: UrlOrReference): Stats | Promise<Stats>;
readFile(url: UrlOrReference): Promise<FileResource>;
readDirectory?(url: URL): Promise<DirEntry[]>;
readDirectory(url: URL): Promise<DirEntry[]>;
writeFile(file: FileResource): Promise<FileReference>;
/**
* Information about the provider.
* It is up to the provider to define what information is available.
*/
providerInfo: FileSystemProviderInfo;
}

export interface FileSystem extends FileSystemBase {
getCapabilities(url: URL): FSCapabilities;
hasProvider: boolean;
}

export interface ProviderFileSystem extends FileSystemBase, Disposable {
/**
* These are the general capabilities for the provider's file system.
* It is possible for a provider to support more capabilities for a given url by providing a getCapabilities function.
*/
capabilities: FSCapabilityFlags;

/**
* Get the capabilities for a URL. Make it possible for a provider to support more capabilities for a given url.
* These capabilities should be more restrictive than the general capabilities.
* @param url - the url to try
* @returns the capabilities for the url.
*/
getCapabilities?: (url: URL) => FSCapabilities;
}

export interface FileSystemProvider extends Partial<Disposable> {
/** Name of the Provider */
name: string;
/**
* Get the file system for a given url. The provider is cached based upon the protocol and hostname.
* @param url - the url to get the file system for.
* @param next - call this function to get the next provider to try. This is useful for chaining providers that operate on the same protocol.
*/
getFileSystem(url: URL, next: NextProvider): FileSystem | undefined;
getFileSystem(url: URL, next: NextProvider): ProviderFileSystem | undefined;
}

class CVirtualFS implements VirtualFS {
private readonly providers = new Set<FileSystemProvider>();
private cachedFs = new Map<string, FileSystem | undefined>();
private cachedFs = new Map<string, WrappedProviderFs>();
private revCacheFs = new Map<FileSystemProvider, Set<string>>();
readonly fs: Required<FileSystem>;

constructor() {
this.fs = fsPassThrough((url) => this.getFS(url));
this.fs = fsPassThrough((url) => this._getFS(url));
}

registerFileSystemProvider(provider: FileSystemProvider): Disposable {
Expand All @@ -63,11 +107,16 @@ class CVirtualFS implements VirtualFS {
};
}

getFS(url: URL): FileSystem | undefined {
getFS(url: URL): FileSystem {
return this._getFS(url);
}

private _getFS(url: URL): WrappedProviderFs {
const key = `${url.protocol}${url.hostname}`;

if (this.cachedFs.has(key)) {
return this.cachedFs.get(key);
const cached = this.cachedFs.get(key);
if (cached) {
return cached;
}

const fnNext = (provider: FileSystemProvider, next: NextProvider) => {
Expand Down Expand Up @@ -96,7 +145,7 @@ class CVirtualFS implements VirtualFS {
next = fnNext(provider, next);
}

const fs = next(url);
const fs = new WrappedProviderFs(next(url));
this.cachedFs.set(key, fs);
return fs;
}
Expand All @@ -110,7 +159,7 @@ class CVirtualFS implements VirtualFS {
private disposeOfCachedFs(): void {
for (const [key, fs] of [...this.cachedFs].reverse()) {
try {
fs?.dispose?.();
WrappedProviderFs.disposeOf(fs);
} catch (e) {
// continue - we are cleaning up.
}
Expand All @@ -132,26 +181,26 @@ class CVirtualFS implements VirtualFS {
}
}

function fsPassThrough(fs: (url: URL) => FileSystem | undefined): Required<FileSystem> {
function fsPassThrough(fs: (url: URL) => WrappedProviderFs): Required<FileSystem> {
function gfs(ur: UrlOrReference, name: string): FileSystem {
const url = urlOrReferenceToUrl(ur);
const f = fs(url);
if (!f)
throw new VFSErrorUnhandledRequest(
if (!f.hasProvider)
throw new VFSErrorUnsupportedRequest(
name,
url,
ur instanceof URL ? undefined : { url: ur.url.toString(), encoding: ur.encoding },
);
return f;
}
return {
providerInfo: { name: 'default' },
hasProvider: true,
stat: async (url) => gfs(url, 'stat').stat(url),
readFile: async (url) => gfs(url, 'readFile').readFile(url),
readDirectory: async (url) => {
const fs = gfs(url, 'readDirectory');
return fs.readDirectory ? fs.readDirectory(url) : Promise.resolve([]);
},
dispose: () => undefined,
writeFile: async (file) => gfs(file, 'writeFile').writeFile(file),
readDirectory: async (url) => gfs(url, 'readDirectory').readDirectory(url),
getCapabilities: (url) => gfs(url, 'getCapabilities').getCapabilities(url),
};
}

Expand All @@ -167,15 +216,20 @@ export function createVirtualFS(cspellIO?: CSpellIO): VirtualFS {
}

function cspellIOToFsProvider(cspellIO: CSpellIO): FileSystemProvider {
const name = 'CSpellIO';
const supportedProtocols = new Set(['file:', 'http:', 'https:']);
const fs: FileSystem = {
const fs: ProviderFileSystem = {
providerInfo: { name },
stat: (url) => cspellIO.getStat(url),
readFile: (url) => cspellIO.readFile(url),
readDirectory: (url) => cspellIO.readDirectory(url),
writeFile: (file) => cspellIO.writeFile(file.url, file.content),
dispose: () => undefined,
capabilities: FSCapabilityFlags.Stat | FSCapabilityFlags.ReadWrite | FSCapabilityFlags.ReadDir,
};

return {
name,
getFileSystem: (url, _next) => {
return supportedProtocols.has(url.protocol) ? fs : undefined;
},
Expand All @@ -191,21 +245,147 @@ export function getDefaultVirtualFs(): VirtualFS {
return defaultVirtualFs;
}

function wrapError(e: unknown): unknown {
if (e instanceof VFSError) return e;
// return new VFSError(e instanceof Error ? e.message : String(e), { cause: e });
return e;
}

export class VFSError extends Error {
constructor(message: string, options?: { cause?: Error }) {
constructor(message: string, options?: { cause?: unknown }) {
super(message, options);
}
}

export class VFSErrorUnhandledRequest extends VFSError {
export class VFSErrorUnsupportedRequest extends VFSError {
public readonly url?: string | undefined;

constructor(
public readonly request: string,
url?: URL | string,
public readonly parameters?: unknown,
) {
super(`Unhandled request: ${request}`);
super(`Unsupported request: ${request}`);
this.url = url?.toString();
}
}

export interface FSCapabilities {
readonly flags: FSCapabilityFlags;
readonly readFile: boolean;
readonly writeFile: boolean;
readonly readDirectory: boolean;
readonly writeDirectory: boolean;
readonly stat: boolean;
}

class CFsCapabilities {
constructor(readonly flags: FSCapabilityFlags) {}

get readFile(): boolean {
return !!(this.flags & FSCapabilityFlags.Read);
}

get writeFile(): boolean {
return !!(this.flags & FSCapabilityFlags.Write);
}

get readDirectory(): boolean {
return !!(this.flags & FSCapabilityFlags.ReadDir);
}

get writeDirectory(): boolean {
return !!(this.flags & FSCapabilityFlags.WriteDir);
}

get stat(): boolean {
return !!(this.flags & FSCapabilityFlags.Stat);
}
}

export function fsCapabilities(flags: FSCapabilityFlags): FSCapabilities {
return new CFsCapabilities(flags);
}

class WrappedProviderFs implements FileSystem {
readonly hasProvider: boolean;
readonly capabilities: FSCapabilityFlags;
readonly providerInfo: FileSystemProviderInfo;
private _capabilities: FSCapabilities;
constructor(private readonly fs: ProviderFileSystem | undefined) {
this.hasProvider = !!fs;
this.capabilities = fs?.capabilities || FSCapabilityFlags.None;
this._capabilities = fsCapabilities(this.capabilities);
this.providerInfo = fs?.providerInfo || { name: 'unknown' };
}

getCapabilities(url: URL): FSCapabilities {
if (this.fs?.getCapabilities) return this.fs.getCapabilities(url);

return this._capabilities;
}

async stat(url: UrlOrReference): Promise<Stats> {
try {
checkCapabilityOrThrow(
this.fs,
this.capabilities,
FSCapabilityFlags.Stat,
'stat',
urlOrReferenceToUrl(url),
);
return await this.fs.stat(url);
} catch (e) {
throw wrapError(e);
}
}

async readFile(url: UrlOrReference): Promise<FileResource> {
try {
checkCapabilityOrThrow(
this.fs,
this.capabilities,
FSCapabilityFlags.Read,
'readFile',
urlOrReferenceToUrl(url),
);
return await this.fs.readFile(url);
} catch (e) {
throw wrapError(e);
}
}

async readDirectory(url: URL): Promise<DirEntry[]> {
try {
checkCapabilityOrThrow(this.fs, this.capabilities, FSCapabilityFlags.ReadDir, 'readDirectory', url);
return await this.fs.readDirectory(url);
} catch (e) {
throw wrapError(e);
}
}

async writeFile(file: FileResource): Promise<FileReference> {
try {
checkCapabilityOrThrow(this.fs, this.capabilities, FSCapabilityFlags.Write, 'writeFile', file.url);
return await this.fs.writeFile(file);
} catch (e) {
throw wrapError(e);
}
}

static disposeOf(fs: FileSystem): void {
fs instanceof WrappedProviderFs && fs.fs?.dispose();
}
}

function checkCapabilityOrThrow(
fs: ProviderFileSystem | undefined,
capabilities: FSCapabilityFlags,
flag: FSCapabilityFlags,
name: string,
url: URL,
): asserts fs is ProviderFileSystem {
if (!(capabilities & flag)) {
throw new VFSErrorUnsupportedRequest(name, url);
}
}
Loading