-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathSessionFsProvider.cs
More file actions
348 lines (301 loc) · 14.7 KB
/
SessionFsProvider.cs
File metadata and controls
348 lines (301 loc) · 14.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/
using GitHub.Copilot.Rpc;
using System.Text.Json;
namespace GitHub.Copilot;
/// <summary>
/// Result of a SQLite query execution via <see cref="ISessionFsSqliteProvider"/>.
/// Same shape as <see cref="SessionFsSqliteQueryResult"/> but without the <c>Error</c> field,
/// since providers signal errors by throwing.
/// </summary>
public sealed class SessionFsSqliteResult
{
/// <summary>Column names from the result set.</summary>
public IList<string> Columns { get; set; } = [];
/// <summary>For SELECT: rows as column-keyed dictionaries. For others: empty.</summary>
public IList<IDictionary<string, object>> Rows { get; set; } = [];
/// <summary>Number of rows affected (for INSERT/UPDATE/DELETE).</summary>
public long RowsAffected { get; set; }
/// <summary>Last inserted row ID (for INSERT).</summary>
public long? LastInsertRowid { get; set; }
}
/// <summary>
/// Optional interface for <see cref="SessionFsProvider"/> subclasses that support
/// per-session SQLite databases. Implement this interface on your provider to enable
/// the runtime's SQL tool to route queries through your SessionFs implementation.
/// </summary>
public interface ISessionFsSqliteProvider
{
/// <summary>
/// Executes a SQLite query against the per-session database.
/// </summary>
/// <param name="queryType">How to execute: <c>"exec"</c> for DDL/multi-statement, <c>"query"</c> for SELECT, <c>"run"</c> for INSERT/UPDATE/DELETE.</param>
/// <param name="query">SQL query to execute.</param>
/// <param name="bindParams">Optional named bind parameters.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The query result, or <c>null</c> for exec-type queries.</returns>
Task<SessionFsSqliteResult?> QueryAsync(
SessionFsSqliteQueryType queryType,
string query,
IDictionary<string, object?>? bindParams,
CancellationToken cancellationToken);
/// <summary>
/// Checks whether the per-session SQLite database already exists, without creating it.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
Task<bool> ExistsAsync(CancellationToken cancellationToken);
}
/// <summary>
/// Base class for session filesystem providers. Subclasses override the
/// virtual methods and use normal C# patterns (return values, throw exceptions).
/// The base class catches exceptions and converts them to <see cref="SessionFsError"/>
/// results expected by the runtime.
/// To add SQLite support, also implement <see cref="ISessionFsSqliteProvider"/>.
/// </summary>
public abstract class SessionFsProvider : ISessionFsHandler
{
/// <summary>Reads the full content of a file. Throw if the file does not exist.</summary>
/// <param name="path">SessionFs-relative path.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The file content as a UTF-8 string.</returns>
protected abstract Task<string> ReadFileAsync(string path, CancellationToken cancellationToken);
/// <summary>Writes content to a file, creating it (and parent directories) if needed.</summary>
/// <param name="path">SessionFs-relative path.</param>
/// <param name="content">Content to write.</param>
/// <param name="mode">Optional POSIX-style permission mode. Null means use OS default.</param>
/// <param name="cancellationToken">Cancellation token.</param>
protected abstract Task WriteFileAsync(string path, string content, int? mode, CancellationToken cancellationToken);
/// <summary>Appends content to a file, creating it (and parent directories) if needed.</summary>
/// <param name="path">SessionFs-relative path.</param>
/// <param name="content">Content to append.</param>
/// <param name="mode">Optional POSIX-style permission mode. Null means use OS default.</param>
/// <param name="cancellationToken">Cancellation token.</param>
protected abstract Task AppendFileAsync(string path, string content, int? mode, CancellationToken cancellationToken);
/// <summary>Checks whether a path exists.</summary>
/// <param name="path">SessionFs-relative path.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns><c>true</c> if the path exists, <c>false</c> otherwise.</returns>
protected abstract Task<bool> ExistsAsync(string path, CancellationToken cancellationToken);
/// <summary>Gets metadata about a file or directory. Throw if the path does not exist.</summary>
/// <param name="path">SessionFs-relative path.</param>
/// <param name="cancellationToken">Cancellation token.</param>
protected abstract Task<SessionFsStatResult> StatAsync(string path, CancellationToken cancellationToken);
/// <summary>Creates a directory (and optionally parents). Does not fail if it already exists.</summary>
/// <param name="path">SessionFs-relative path.</param>
/// <param name="recursive">Whether to create parent directories.</param>
/// <param name="mode">Optional POSIX-style permission mode (e.g., 0x1FF for 0777). Null means use OS default.</param>
/// <param name="cancellationToken">Cancellation token.</param>
protected abstract Task MakeDirectoryAsync(string path, bool recursive, int? mode, CancellationToken cancellationToken);
/// <summary>Lists entry names in a directory. Throw if the directory does not exist.</summary>
/// <param name="path">SessionFs-relative path.</param>
/// <param name="cancellationToken">Cancellation token.</param>
protected abstract Task<IList<string>> ReadDirectoryAsync(string path, CancellationToken cancellationToken);
/// <summary>Lists entries with type info in a directory. Throw if the directory does not exist.</summary>
/// <param name="path">SessionFs-relative path.</param>
/// <param name="cancellationToken">Cancellation token.</param>
protected abstract Task<IList<SessionFsReaddirWithTypesEntry>> ReadDirectoryWithTypesAsync(string path, CancellationToken cancellationToken);
/// <summary>Removes a file or directory. Throw if the path does not exist (unless <paramref name="force"/> is true).</summary>
/// <param name="path">SessionFs-relative path.</param>
/// <param name="recursive">Whether to remove directory contents recursively.</param>
/// <param name="force">If true, do not throw when the path does not exist.</param>
/// <param name="cancellationToken">Cancellation token.</param>
protected abstract Task RemoveAsync(string path, bool recursive, bool force, CancellationToken cancellationToken);
/// <summary>Renames/moves a file or directory.</summary>
/// <param name="src">Source path.</param>
/// <param name="dest">Destination path.</param>
/// <param name="cancellationToken">Cancellation token.</param>
protected abstract Task RenameAsync(string src, string dest, CancellationToken cancellationToken);
// ---- ISessionFsHandler implementation (private, handles error mapping) ----
async Task<SessionFsReadFileResult> ISessionFsHandler.ReadFileAsync(SessionFsReadFileRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
try
{
var content = await ReadFileAsync(request.Path, cancellationToken).ConfigureAwait(false);
return new SessionFsReadFileResult { Content = content };
}
catch (Exception ex)
{
return new SessionFsReadFileResult { Error = ToSessionFsError(ex) };
}
}
async Task<SessionFsError?> ISessionFsHandler.WriteFileAsync(SessionFsWriteFileRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
try
{
await WriteFileAsync(request.Path, request.Content, (int?)request.Mode, cancellationToken).ConfigureAwait(false);
return null;
}
catch (Exception ex)
{
return ToSessionFsError(ex);
}
}
async Task<SessionFsError?> ISessionFsHandler.AppendFileAsync(SessionFsAppendFileRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
try
{
await AppendFileAsync(request.Path, request.Content, (int?)request.Mode, cancellationToken).ConfigureAwait(false);
return null;
}
catch (Exception ex)
{
return ToSessionFsError(ex);
}
}
async Task<SessionFsExistsResult> ISessionFsHandler.ExistsAsync(SessionFsExistsRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
try
{
var exists = await ExistsAsync(request.Path, cancellationToken).ConfigureAwait(false);
return new SessionFsExistsResult { Exists = exists };
}
catch
{
return new SessionFsExistsResult { Exists = false };
}
}
async Task<SessionFsStatResult> ISessionFsHandler.StatAsync(SessionFsStatRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
try
{
return await StatAsync(request.Path, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
return new SessionFsStatResult { Error = ToSessionFsError(ex) };
}
}
async Task<SessionFsError?> ISessionFsHandler.MkdirAsync(SessionFsMkdirRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
try
{
await MakeDirectoryAsync(request.Path, request.Recursive ?? false, (int?)request.Mode, cancellationToken).ConfigureAwait(false);
return null;
}
catch (Exception ex)
{
return ToSessionFsError(ex);
}
}
async Task<SessionFsReaddirResult> ISessionFsHandler.ReaddirAsync(SessionFsReaddirRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
try
{
var entries = await ReadDirectoryAsync(request.Path, cancellationToken).ConfigureAwait(false);
return new SessionFsReaddirResult { Entries = entries };
}
catch (Exception ex)
{
return new SessionFsReaddirResult { Error = ToSessionFsError(ex) };
}
}
async Task<SessionFsReaddirWithTypesResult> ISessionFsHandler.ReaddirWithTypesAsync(SessionFsReaddirWithTypesRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
try
{
var entries = await ReadDirectoryWithTypesAsync(request.Path, cancellationToken).ConfigureAwait(false);
return new SessionFsReaddirWithTypesResult { Entries = entries };
}
catch (Exception ex)
{
return new SessionFsReaddirWithTypesResult { Error = ToSessionFsError(ex) };
}
}
async Task<SessionFsError?> ISessionFsHandler.RmAsync(SessionFsRmRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
try
{
await RemoveAsync(request.Path, request.Recursive ?? false, request.Force ?? false, cancellationToken).ConfigureAwait(false);
return null;
}
catch (Exception ex)
{
return ToSessionFsError(ex);
}
}
async Task<SessionFsError?> ISessionFsHandler.RenameAsync(SessionFsRenameRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
try
{
await RenameAsync(request.Src, request.Dest, cancellationToken).ConfigureAwait(false);
return null;
}
catch (Exception ex)
{
return ToSessionFsError(ex);
}
}
async Task<SessionFsSqliteQueryResult> ISessionFsHandler.SqliteQueryAsync(SessionFsSqliteQueryRequest request, CancellationToken cancellationToken)
{
if (this is not ISessionFsSqliteProvider sqliteProvider)
{
return new SessionFsSqliteQueryResult
{
Error = new SessionFsError { Code = SessionFsErrorCode.UNKNOWN, Message = "SQLite is not supported by this provider." },
};
}
try
{
var bindParams = request.Params?.ToDictionary(
kvp => kvp.Key,
kvp => JsonElementToValue(kvp.Value));
var result = await sqliteProvider.QueryAsync(request.QueryType, request.Query, bindParams, cancellationToken).ConfigureAwait(false);
return new SessionFsSqliteQueryResult
{
Rows = result?.Rows?.Select(row => (IDictionary<string, JsonElement>)row.ToDictionary(
kvp => kvp.Key,
kvp => CopilotClient.ToJsonElementForWire(kvp.Value)!.Value)).ToList() ?? [],
Columns = result?.Columns ?? [],
RowsAffected = result?.RowsAffected ?? 0,
LastInsertRowid = result?.LastInsertRowid,
};
}
catch (Exception ex)
{
return new SessionFsSqliteQueryResult { Error = ToSessionFsError(ex) };
}
}
async Task<SessionFsSqliteExistsResult> ISessionFsHandler.SqliteExistsAsync(SessionFsSqliteExistsRequest request, CancellationToken cancellationToken)
{
if (this is not ISessionFsSqliteProvider sqliteProvider)
{
return new SessionFsSqliteExistsResult { Exists = false };
}
try
{
var exists = await sqliteProvider.ExistsAsync(cancellationToken).ConfigureAwait(false);
return new SessionFsSqliteExistsResult { Exists = exists };
}
catch
{
return new SessionFsSqliteExistsResult { Exists = false };
}
}
private static SessionFsError ToSessionFsError(Exception ex)
{
var code = ex is FileNotFoundException or DirectoryNotFoundException
? SessionFsErrorCode.ENOENT
: SessionFsErrorCode.UNKNOWN;
return new SessionFsError { Code = code, Message = ex.Message };
}
private static object? JsonElementToValue(JsonElement element) => element.ValueKind switch
{
JsonValueKind.Null => null,
JsonValueKind.True => true,
JsonValueKind.False => false,
JsonValueKind.String => element.GetString(),
JsonValueKind.Number => element.TryGetInt64(out var l) ? l : element.GetDouble(),
_ => element.GetRawText(),
};
}