Path: blob/main/src/vs/platform/agentHost/common/agentHostFileSystemProvider.ts
13394 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { decodeBase64, VSBuffer } from '../../../base/common/buffer.js';6import { Emitter } from '../../../base/common/event.js';7import { Disposable, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';8import { basename, dirname } from '../../../base/common/resources.js';9import { URI } from '../../../base/common/uri.js';10import { createFileSystemProviderError, FilePermission, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileChange, IFileDeleteOptions, IFileOverwriteOptions, IFileSystemProvider, IFileWriteOptions, IStat } from '../../files/common/files.js';11import { fromAgentHostUri, toAgentHostUri } from './agentHostUri.js';12import { type IAgentConnection } from './agentService.js';13import { ContentEncoding, type DirectoryEntry, type ResourceDeleteParams, type ResourceDeleteResult, type ResourceListResult, type ResourceMoveParams, type ResourceMoveResult, type ResourceReadResult, type ResourceWriteParams, type ResourceWriteResult } from './state/protocol/commands.js';1415/**16* Interface for performing resource operations on a remote endpoint.17*18* Both {@link IAgentConnection} (client→server) and client-exposed19* filesystems (server→client) satisfy this contract.20*/21export interface IRemoteFilesystemConnection {22resourceList(uri: URI): Promise<ResourceListResult>;23resourceRead(uri: URI): Promise<ResourceReadResult>;24resourceWrite(params: ResourceWriteParams): Promise<ResourceWriteResult>;25resourceDelete(params: ResourceDeleteParams): Promise<ResourceDeleteResult>;26resourceMove(params: ResourceMoveParams): Promise<ResourceMoveResult>;27}2829/**30* Build a {@link AGENT_HOST_SCHEME} URI for a given connection authority31* and remote path. Assumes the remote path is a `file://` resource.32*/33export function agentHostUri(authority: string, path: string): URI {34return toAgentHostUri(URI.file(path), authority);35}3637/**38* Extract the remote filesystem path from a {@link AGENT_HOST_SCHEME} URI.39*/40export function agentHostRemotePath(uri: URI): string {41return fromAgentHostUri(uri).path;42}4344// ---- Abstract base ----------------------------------------------------------4546/**47* {@link IFileSystemProvider} that proxies filesystem operations48* through a {@link IRemoteFilesystemConnection}.49*50* URIs encode the original scheme and authority in the path so any remote51* resource can be represented. Subclasses provide the URI decode function52* and scheme-specific helpers.53*54* Individual connections are identified by the URI's authority component.55*/56export abstract class AHPFileSystemProvider extends Disposable implements IFileSystemProvider {5758readonly capabilities =59FileSystemProviderCapabilities.PathCaseSensitive |60FileSystemProviderCapabilities.FileReadWrite;6162private readonly _onDidChangeCapabilities = this._register(new Emitter<void>());63readonly onDidChangeCapabilities = this._onDidChangeCapabilities.event;6465private readonly _onDidChangeFile = this._register(new Emitter<readonly IFileChange[]>());66readonly onDidChangeFile = this._onDidChangeFile.event;6768private readonly _authorityToConnection = new Map<string, IRemoteFilesystemConnection>();6970/**71* Register a mapping from a URI authority to a connection.72* Returns a disposable that unregisters the mapping.73*/74registerAuthority(authority: string, connection: IRemoteFilesystemConnection): IDisposable {75this._authorityToConnection.set(authority, connection);76return toDisposable(() => this._authorityToConnection.delete(authority));77}7879/** Decode a provider URI back to the original URI for the remote endpoint. */80protected abstract _decodeUri(resource: URI): URI;8182watch(): IDisposable {83return Disposable.None;84}8586async stat(resource: URI): Promise<IStat> {87const path = resource.path;8889if (path === '/' || path === '') {90return { type: FileType.Directory, mtime: 0, ctime: 0, size: 0, permissions: FilePermission.Readonly };91}92const decoded = this._decodeUri(resource);93if (decoded.scheme === 'session-db' || decoded.scheme === 'git-blob') {94return { type: FileType.File, mtime: 0, ctime: 0, size: 0, permissions: FilePermission.Readonly };95}9697if (decoded.path === '/' || decoded.path === '') {98return { type: FileType.Directory, mtime: 0, ctime: 0, size: 0, permissions: FilePermission.Readonly };99}100101const parentUri = dirname(resource);102const name = basename(resource);103104const entries = await this._listDirectory(resource.authority, parentUri);105const entry = entries.find(e => e.name === name);106if (!entry) {107throw createFileSystemProviderError(`File not found: ${path}`, FileSystemProviderErrorCode.FileNotFound);108}109110return {111type: entry.type === 'directory' ? FileType.Directory : FileType.File,112mtime: 0,113ctime: 0,114size: 0,115permissions: FilePermission.Readonly,116};117}118119async readdir(resource: URI): Promise<[string, FileType][]> {120const entries = await this._listDirectory(resource.authority, resource);121return entries.map(e => [e.name, e.type === 'directory' ? FileType.Directory : FileType.File]);122}123124async readFile(resource: URI): Promise<Uint8Array> {125const connection = this._getConnection(resource.authority);126try {127const originalUri = this._decodeUri(resource);128const result = await connection.resourceRead(originalUri);129if (result.encoding === ContentEncoding.Base64) {130return decodeBase64(result.data).buffer;131}132return VSBuffer.fromString(result.data).buffer;133} catch (err) {134throw createFileSystemProviderError(135err instanceof Error ? err.message : String(err),136FileSystemProviderErrorCode.FileNotFound,137);138}139}140141async writeFile(resource: URI, content: Uint8Array, _opts: IFileWriteOptions): Promise<void> {142const connection = this._getConnection(resource.authority);143try {144const originalUri = this._decodeUri(resource);145await connection.resourceWrite({146uri: originalUri.toString(),147data: VSBuffer.wrap(content).toString(),148encoding: ContentEncoding.Utf8,149});150} catch (err) {151throw createFileSystemProviderError(152err instanceof Error ? err.message : String(err),153FileSystemProviderErrorCode.NoPermissions,154);155}156}157158async mkdir(): Promise<void> {159throw createFileSystemProviderError('mkdir not supported on remote filesystem', FileSystemProviderErrorCode.NoPermissions);160}161162async delete(resource: URI, opts: IFileDeleteOptions): Promise<void> {163const connection = this._getConnection(resource.authority);164try {165const originalUri = this._decodeUri(resource);166await connection.resourceDelete({ uri: originalUri.toString(), recursive: opts.recursive });167} catch (err) {168throw createFileSystemProviderError(169err instanceof Error ? err.message : String(err),170FileSystemProviderErrorCode.NoPermissions,171);172}173}174175async rename(from: URI, to: URI, opts: IFileOverwriteOptions): Promise<void> {176const connection = this._getConnection(from.authority);177try {178const originalFrom = this._decodeUri(from);179const originalTo = this._decodeUri(to);180await connection.resourceMove({ source: originalFrom.toString(), destination: originalTo.toString(), failIfExists: !opts.overwrite });181} catch (err) {182throw createFileSystemProviderError(183err instanceof Error ? err.message : String(err),184FileSystemProviderErrorCode.NoPermissions,185);186}187}188189// ---- Internals ----------------------------------------------------------190191private _getConnection(authority: string): IRemoteFilesystemConnection {192const connection = this._authorityToConnection.get(authority);193if (!connection) {194throw createFileSystemProviderError(`No connection for authority: ${authority}`, FileSystemProviderErrorCode.Unavailable);195}196return connection;197}198199private async _listDirectory(authority: string, resource: URI): Promise<readonly DirectoryEntry[]> {200const connection = this._getConnection(authority);201try {202const originalUri = this._decodeUri(resource);203const result = await connection.resourceList(originalUri);204return result.entries;205} catch (err) {206throw createFileSystemProviderError(207err instanceof Error ? err.message : String(err),208FileSystemProviderErrorCode.Unavailable,209);210}211}212}213214// ---- Agent Host filesystem (client reads agent host files) ------------------215216/**217* Filesystem provider for accessing agent host files from the218* client side. Registered under the `vscode-agent-host` scheme.219*220* ```221* vscode-agent-host://[connectionAuthority]/[originalScheme]/[originalAuthority]/[originalPath]222* ```223*/224export class AgentHostFileSystemProvider extends AHPFileSystemProvider {225protected _decodeUri(resource: URI): URI {226return fromAgentHostUri(resource);227}228}229230231