Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/agentHost/common/agentHostFileSystemProvider.ts
13394 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { decodeBase64, VSBuffer } from '../../../base/common/buffer.js';
7
import { Emitter } from '../../../base/common/event.js';
8
import { Disposable, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';
9
import { basename, dirname } from '../../../base/common/resources.js';
10
import { URI } from '../../../base/common/uri.js';
11
import { createFileSystemProviderError, FilePermission, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileChange, IFileDeleteOptions, IFileOverwriteOptions, IFileSystemProvider, IFileWriteOptions, IStat } from '../../files/common/files.js';
12
import { fromAgentHostUri, toAgentHostUri } from './agentHostUri.js';
13
import { type IAgentConnection } from './agentService.js';
14
import { ContentEncoding, type DirectoryEntry, type ResourceDeleteParams, type ResourceDeleteResult, type ResourceListResult, type ResourceMoveParams, type ResourceMoveResult, type ResourceReadResult, type ResourceWriteParams, type ResourceWriteResult } from './state/protocol/commands.js';
15
16
/**
17
* Interface for performing resource operations on a remote endpoint.
18
*
19
* Both {@link IAgentConnection} (client→server) and client-exposed
20
* filesystems (server→client) satisfy this contract.
21
*/
22
export interface IRemoteFilesystemConnection {
23
resourceList(uri: URI): Promise<ResourceListResult>;
24
resourceRead(uri: URI): Promise<ResourceReadResult>;
25
resourceWrite(params: ResourceWriteParams): Promise<ResourceWriteResult>;
26
resourceDelete(params: ResourceDeleteParams): Promise<ResourceDeleteResult>;
27
resourceMove(params: ResourceMoveParams): Promise<ResourceMoveResult>;
28
}
29
30
/**
31
* Build a {@link AGENT_HOST_SCHEME} URI for a given connection authority
32
* and remote path. Assumes the remote path is a `file://` resource.
33
*/
34
export function agentHostUri(authority: string, path: string): URI {
35
return toAgentHostUri(URI.file(path), authority);
36
}
37
38
/**
39
* Extract the remote filesystem path from a {@link AGENT_HOST_SCHEME} URI.
40
*/
41
export function agentHostRemotePath(uri: URI): string {
42
return fromAgentHostUri(uri).path;
43
}
44
45
// ---- Abstract base ----------------------------------------------------------
46
47
/**
48
* {@link IFileSystemProvider} that proxies filesystem operations
49
* through a {@link IRemoteFilesystemConnection}.
50
*
51
* URIs encode the original scheme and authority in the path so any remote
52
* resource can be represented. Subclasses provide the URI decode function
53
* and scheme-specific helpers.
54
*
55
* Individual connections are identified by the URI's authority component.
56
*/
57
export abstract class AHPFileSystemProvider extends Disposable implements IFileSystemProvider {
58
59
readonly capabilities =
60
FileSystemProviderCapabilities.PathCaseSensitive |
61
FileSystemProviderCapabilities.FileReadWrite;
62
63
private readonly _onDidChangeCapabilities = this._register(new Emitter<void>());
64
readonly onDidChangeCapabilities = this._onDidChangeCapabilities.event;
65
66
private readonly _onDidChangeFile = this._register(new Emitter<readonly IFileChange[]>());
67
readonly onDidChangeFile = this._onDidChangeFile.event;
68
69
private readonly _authorityToConnection = new Map<string, IRemoteFilesystemConnection>();
70
71
/**
72
* Register a mapping from a URI authority to a connection.
73
* Returns a disposable that unregisters the mapping.
74
*/
75
registerAuthority(authority: string, connection: IRemoteFilesystemConnection): IDisposable {
76
this._authorityToConnection.set(authority, connection);
77
return toDisposable(() => this._authorityToConnection.delete(authority));
78
}
79
80
/** Decode a provider URI back to the original URI for the remote endpoint. */
81
protected abstract _decodeUri(resource: URI): URI;
82
83
watch(): IDisposable {
84
return Disposable.None;
85
}
86
87
async stat(resource: URI): Promise<IStat> {
88
const path = resource.path;
89
90
if (path === '/' || path === '') {
91
return { type: FileType.Directory, mtime: 0, ctime: 0, size: 0, permissions: FilePermission.Readonly };
92
}
93
const decoded = this._decodeUri(resource);
94
if (decoded.scheme === 'session-db' || decoded.scheme === 'git-blob') {
95
return { type: FileType.File, mtime: 0, ctime: 0, size: 0, permissions: FilePermission.Readonly };
96
}
97
98
if (decoded.path === '/' || decoded.path === '') {
99
return { type: FileType.Directory, mtime: 0, ctime: 0, size: 0, permissions: FilePermission.Readonly };
100
}
101
102
const parentUri = dirname(resource);
103
const name = basename(resource);
104
105
const entries = await this._listDirectory(resource.authority, parentUri);
106
const entry = entries.find(e => e.name === name);
107
if (!entry) {
108
throw createFileSystemProviderError(`File not found: ${path}`, FileSystemProviderErrorCode.FileNotFound);
109
}
110
111
return {
112
type: entry.type === 'directory' ? FileType.Directory : FileType.File,
113
mtime: 0,
114
ctime: 0,
115
size: 0,
116
permissions: FilePermission.Readonly,
117
};
118
}
119
120
async readdir(resource: URI): Promise<[string, FileType][]> {
121
const entries = await this._listDirectory(resource.authority, resource);
122
return entries.map(e => [e.name, e.type === 'directory' ? FileType.Directory : FileType.File]);
123
}
124
125
async readFile(resource: URI): Promise<Uint8Array> {
126
const connection = this._getConnection(resource.authority);
127
try {
128
const originalUri = this._decodeUri(resource);
129
const result = await connection.resourceRead(originalUri);
130
if (result.encoding === ContentEncoding.Base64) {
131
return decodeBase64(result.data).buffer;
132
}
133
return VSBuffer.fromString(result.data).buffer;
134
} catch (err) {
135
throw createFileSystemProviderError(
136
err instanceof Error ? err.message : String(err),
137
FileSystemProviderErrorCode.FileNotFound,
138
);
139
}
140
}
141
142
async writeFile(resource: URI, content: Uint8Array, _opts: IFileWriteOptions): Promise<void> {
143
const connection = this._getConnection(resource.authority);
144
try {
145
const originalUri = this._decodeUri(resource);
146
await connection.resourceWrite({
147
uri: originalUri.toString(),
148
data: VSBuffer.wrap(content).toString(),
149
encoding: ContentEncoding.Utf8,
150
});
151
} catch (err) {
152
throw createFileSystemProviderError(
153
err instanceof Error ? err.message : String(err),
154
FileSystemProviderErrorCode.NoPermissions,
155
);
156
}
157
}
158
159
async mkdir(): Promise<void> {
160
throw createFileSystemProviderError('mkdir not supported on remote filesystem', FileSystemProviderErrorCode.NoPermissions);
161
}
162
163
async delete(resource: URI, opts: IFileDeleteOptions): Promise<void> {
164
const connection = this._getConnection(resource.authority);
165
try {
166
const originalUri = this._decodeUri(resource);
167
await connection.resourceDelete({ uri: originalUri.toString(), recursive: opts.recursive });
168
} catch (err) {
169
throw createFileSystemProviderError(
170
err instanceof Error ? err.message : String(err),
171
FileSystemProviderErrorCode.NoPermissions,
172
);
173
}
174
}
175
176
async rename(from: URI, to: URI, opts: IFileOverwriteOptions): Promise<void> {
177
const connection = this._getConnection(from.authority);
178
try {
179
const originalFrom = this._decodeUri(from);
180
const originalTo = this._decodeUri(to);
181
await connection.resourceMove({ source: originalFrom.toString(), destination: originalTo.toString(), failIfExists: !opts.overwrite });
182
} catch (err) {
183
throw createFileSystemProviderError(
184
err instanceof Error ? err.message : String(err),
185
FileSystemProviderErrorCode.NoPermissions,
186
);
187
}
188
}
189
190
// ---- Internals ----------------------------------------------------------
191
192
private _getConnection(authority: string): IRemoteFilesystemConnection {
193
const connection = this._authorityToConnection.get(authority);
194
if (!connection) {
195
throw createFileSystemProviderError(`No connection for authority: ${authority}`, FileSystemProviderErrorCode.Unavailable);
196
}
197
return connection;
198
}
199
200
private async _listDirectory(authority: string, resource: URI): Promise<readonly DirectoryEntry[]> {
201
const connection = this._getConnection(authority);
202
try {
203
const originalUri = this._decodeUri(resource);
204
const result = await connection.resourceList(originalUri);
205
return result.entries;
206
} catch (err) {
207
throw createFileSystemProviderError(
208
err instanceof Error ? err.message : String(err),
209
FileSystemProviderErrorCode.Unavailable,
210
);
211
}
212
}
213
}
214
215
// ---- Agent Host filesystem (client reads agent host files) ------------------
216
217
/**
218
* Filesystem provider for accessing agent host files from the
219
* client side. Registered under the `vscode-agent-host` scheme.
220
*
221
* ```
222
* vscode-agent-host://[connectionAuthority]/[originalScheme]/[originalAuthority]/[originalPath]
223
* ```
224
*/
225
export class AgentHostFileSystemProvider extends AHPFileSystemProvider {
226
protected _decodeUri(resource: URI): URI {
227
return fromAgentHostUri(resource);
228
}
229
}
230
231