@theia/filesystem 1.65.0-next.6 → 1.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/lib/browser/download/file-download-command-contribution.d.ts +1 -1
  2. package/lib/browser/download/file-download-command-contribution.d.ts.map +1 -1
  3. package/lib/browser/download/file-download-command-contribution.js +3 -3
  4. package/lib/browser/download/file-download-command-contribution.js.map +1 -1
  5. package/lib/browser/download/file-download-frontend-module.d.ts.map +1 -1
  6. package/lib/browser/download/file-download-frontend-module.js +2 -1
  7. package/lib/browser/download/file-download-frontend-module.js.map +1 -1
  8. package/lib/browser/download/file-download-service.d.ts +2 -10
  9. package/lib/browser/download/file-download-service.d.ts.map +1 -1
  10. package/lib/browser/download/file-download-service.js +8 -7
  11. package/lib/browser/download/file-download-service.js.map +1 -1
  12. package/lib/browser/file-resource.js +1 -1
  13. package/lib/browser/file-resource.js.map +1 -1
  14. package/lib/browser/file-service.d.ts +1 -1
  15. package/lib/browser/file-service.d.ts.map +1 -1
  16. package/lib/browser/file-service.js +1 -1
  17. package/lib/browser/file-service.js.map +1 -1
  18. package/lib/browser/file-tree/file-tree-widget.d.ts +1 -1
  19. package/lib/browser/file-tree/file-tree-widget.d.ts.map +1 -1
  20. package/lib/browser/file-tree/file-tree-widget.js +3 -3
  21. package/lib/browser/file-tree/file-tree-widget.js.map +1 -1
  22. package/lib/browser/filesystem-frontend-contribution.d.ts +5 -4
  23. package/lib/browser/filesystem-frontend-contribution.d.ts.map +1 -1
  24. package/lib/browser/filesystem-frontend-contribution.js +5 -5
  25. package/lib/browser/filesystem-frontend-contribution.js.map +1 -1
  26. package/lib/browser/filesystem-frontend-module.d.ts.map +1 -1
  27. package/lib/browser/filesystem-frontend-module.js +4 -3
  28. package/lib/browser/filesystem-frontend-module.js.map +1 -1
  29. package/lib/browser/filesystem-saveable-service.d.ts.map +1 -1
  30. package/lib/browser/filesystem-saveable-service.js +5 -1
  31. package/lib/browser/filesystem-saveable-service.js.map +1 -1
  32. package/lib/browser/index.d.ts +0 -1
  33. package/lib/browser/index.d.ts.map +1 -1
  34. package/lib/browser/index.js +0 -1
  35. package/lib/browser/index.js.map +1 -1
  36. package/lib/browser/{file-upload-service.d.ts → upload/file-upload-service-impl.d.ts} +16 -52
  37. package/lib/browser/upload/file-upload-service-impl.d.ts.map +1 -0
  38. package/lib/browser/{file-upload-service.js → upload/file-upload-service-impl.js} +27 -27
  39. package/lib/browser/upload/file-upload-service-impl.js.map +1 -0
  40. package/lib/browser-only/browser-only-filesystem-frontend-module.d.ts.map +1 -1
  41. package/lib/browser-only/browser-only-filesystem-frontend-module.js +8 -0
  42. package/lib/browser-only/browser-only-filesystem-frontend-module.js.map +1 -1
  43. package/lib/browser-only/download/file-download-command-contribution.d.ts +15 -0
  44. package/lib/browser-only/download/file-download-command-contribution.d.ts.map +1 -0
  45. package/lib/browser-only/download/file-download-command-contribution.js +55 -0
  46. package/lib/browser-only/download/file-download-command-contribution.js.map +1 -0
  47. package/lib/browser-only/download/file-download-frontend-module.d.ts +4 -0
  48. package/lib/browser-only/download/file-download-frontend-module.d.ts.map +1 -0
  49. package/lib/browser-only/download/file-download-frontend-module.js +27 -0
  50. package/lib/browser-only/download/file-download-frontend-module.js.map +1 -0
  51. package/lib/browser-only/download/file-download-service.d.ts +86 -0
  52. package/lib/browser-only/download/file-download-service.d.ts.map +1 -0
  53. package/lib/browser-only/download/file-download-service.js +551 -0
  54. package/lib/browser-only/download/file-download-service.js.map +1 -0
  55. package/lib/browser-only/file-search.d.ts +38 -0
  56. package/lib/browser-only/file-search.d.ts.map +1 -0
  57. package/lib/browser-only/file-search.js +153 -0
  58. package/lib/browser-only/file-search.js.map +1 -0
  59. package/lib/browser-only/opfs-filesystem-initialization.d.ts +4 -2
  60. package/lib/browser-only/opfs-filesystem-initialization.d.ts.map +1 -1
  61. package/lib/browser-only/opfs-filesystem-initialization.js +4 -1
  62. package/lib/browser-only/opfs-filesystem-initialization.js.map +1 -1
  63. package/lib/browser-only/opfs-filesystem-provider.d.ts +89 -12
  64. package/lib/browser-only/opfs-filesystem-provider.d.ts.map +1 -1
  65. package/lib/browser-only/opfs-filesystem-provider.js +345 -181
  66. package/lib/browser-only/opfs-filesystem-provider.js.map +1 -1
  67. package/lib/browser-only/upload/file-upload-service-impl.d.ts +67 -0
  68. package/lib/browser-only/upload/file-upload-service-impl.d.ts.map +1 -0
  69. package/lib/browser-only/upload/file-upload-service-impl.js +328 -0
  70. package/lib/browser-only/upload/file-upload-service-impl.js.map +1 -0
  71. package/lib/common/download/file-download.d.ts +17 -0
  72. package/lib/common/download/file-download.d.ts.map +1 -0
  73. package/lib/common/download/{file-download-data.js → file-download.js} +3 -2
  74. package/lib/common/download/file-download.js.map +1 -0
  75. package/lib/common/files.d.ts +8 -1
  76. package/lib/common/files.d.ts.map +1 -1
  77. package/lib/common/files.js +35 -1
  78. package/lib/common/files.js.map +1 -1
  79. package/lib/{browser → common}/filesystem-preferences.d.ts +3 -1
  80. package/lib/common/filesystem-preferences.d.ts.map +1 -0
  81. package/lib/{browser → common}/filesystem-preferences.js +17 -11
  82. package/lib/common/filesystem-preferences.js.map +1 -0
  83. package/lib/common/index.d.ts +1 -0
  84. package/lib/common/index.d.ts.map +1 -1
  85. package/lib/common/index.js +1 -0
  86. package/lib/common/index.js.map +1 -1
  87. package/lib/common/io.js +7 -1
  88. package/lib/common/io.js.map +1 -1
  89. package/lib/common/upload/file-upload.d.ts +45 -0
  90. package/lib/common/upload/file-upload.d.ts.map +1 -0
  91. package/{src/common/download/file-download-data.ts → lib/common/upload/file-upload.js} +6 -13
  92. package/lib/common/upload/file-upload.js.map +1 -0
  93. package/lib/node/disk-file-system-provider.d.ts.map +1 -1
  94. package/lib/node/disk-file-system-provider.js +2 -4
  95. package/lib/node/disk-file-system-provider.js.map +1 -1
  96. package/lib/node/download/file-download-handler.js +2 -2
  97. package/lib/node/download/file-download-handler.js.map +1 -1
  98. package/lib/node/filesystem-backend-module.d.ts.map +1 -1
  99. package/lib/node/filesystem-backend-module.js +3 -1
  100. package/lib/node/filesystem-backend-module.js.map +1 -1
  101. package/lib/node/parcel-watcher/parcel-filesystem-service.d.ts +2 -2
  102. package/lib/node/parcel-watcher/parcel-filesystem-service.d.ts.map +1 -1
  103. package/lib/node/parcel-watcher/parcel-filesystem-service.js.map +1 -1
  104. package/lib/node/upload/node-file-upload-service.d.ts.map +1 -0
  105. package/lib/node/{node-file-upload-service.js → upload/node-file-upload-service.js} +1 -1
  106. package/lib/node/upload/node-file-upload-service.js.map +1 -0
  107. package/package.json +11 -5
  108. package/src/browser/download/file-download-command-contribution.ts +1 -1
  109. package/src/browser/download/file-download-frontend-module.ts +3 -2
  110. package/src/browser/download/file-download-service.ts +7 -12
  111. package/src/browser/file-resource.ts +1 -1
  112. package/src/browser/file-service.ts +1 -1
  113. package/src/browser/file-tree/file-tree-widget.tsx +1 -1
  114. package/src/browser/filesystem-frontend-contribution.ts +4 -5
  115. package/src/browser/filesystem-frontend-module.ts +4 -3
  116. package/src/browser/filesystem-saveable-service.ts +5 -1
  117. package/src/browser/index.ts +0 -1
  118. package/src/browser/{file-upload-service.ts → upload/file-upload-service-impl.ts} +31 -72
  119. package/src/browser-only/browser-only-filesystem-frontend-module.ts +10 -0
  120. package/src/browser-only/download/file-download-command-contribution.ts +56 -0
  121. package/src/browser-only/download/file-download-frontend-module.ts +26 -0
  122. package/src/browser-only/download/file-download-service.ts +726 -0
  123. package/src/browser-only/file-search.ts +170 -0
  124. package/src/browser-only/opfs-filesystem-initialization.ts +7 -4
  125. package/src/browser-only/opfs-filesystem-provider.ts +402 -189
  126. package/src/browser-only/upload/file-upload-service-impl.ts +408 -0
  127. package/src/common/download/file-download.ts +40 -0
  128. package/src/common/files.ts +42 -1
  129. package/src/{browser → common}/filesystem-preferences.ts +14 -14
  130. package/src/common/index.ts +1 -0
  131. package/src/common/io.ts +6 -1
  132. package/src/common/upload/file-upload.ts +65 -0
  133. package/src/node/disk-file-system-provider.ts +3 -4
  134. package/src/node/download/file-download-handler.ts +1 -1
  135. package/src/node/filesystem-backend-module.ts +3 -1
  136. package/src/node/parcel-watcher/parcel-filesystem-service.ts +2 -2
  137. package/src/node/{node-file-upload-service.ts → upload/node-file-upload-service.ts} +1 -1
  138. package/lib/browser/file-upload-service.d.ts.map +0 -1
  139. package/lib/browser/file-upload-service.js.map +0 -1
  140. package/lib/browser/filesystem-preferences.d.ts.map +0 -1
  141. package/lib/browser/filesystem-preferences.js.map +0 -1
  142. package/lib/common/download/file-download-data.d.ts +0 -7
  143. package/lib/common/download/file-download-data.d.ts.map +0 -1
  144. package/lib/common/download/file-download-data.js.map +0 -1
  145. package/lib/node/node-file-upload-service.d.ts.map +0 -1
  146. package/lib/node/node-file-upload-service.js.map +0 -1
  147. /package/lib/node/{node-file-upload-service.d.ts → upload/node-file-upload-service.d.ts} +0 -0
@@ -0,0 +1,408 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2025 Maksim Kachurin
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
18
+ import URI from '@theia/core/lib/common/uri';
19
+ import { CancellationTokenSource, CancellationToken, checkCancelled, isCancelled } from '@theia/core/lib/common/cancellation';
20
+ import { Deferred } from '@theia/core/lib/common/promise-util';
21
+ import { MessageService } from '@theia/core/lib/common/message-service';
22
+ import { Progress } from '@theia/core/lib/common/message-service-protocol';
23
+ import throttle = require('@theia/core/shared/lodash.throttle');
24
+ import { Semaphore } from 'async-mutex';
25
+ import { FileService } from '../../browser/file-service';
26
+ import { ConfirmDialog, Dialog } from '@theia/core/lib/browser';
27
+ import { nls } from '@theia/core/lib/common/nls';
28
+ import { Emitter, Event } from '@theia/core/lib/common/event';
29
+ import { FileSystemPreferences } from '../../common/filesystem-preferences';
30
+ import { fileToStream } from '@theia/core/lib/common/stream';
31
+ import { minimatch } from 'minimatch';
32
+
33
+ import type { FileUploadService } from '../../common/upload/file-upload';
34
+
35
+ interface UploadState {
36
+ uploaded?: boolean;
37
+ failed?: boolean;
38
+ }
39
+
40
+ interface UploadFilesParams {
41
+ source: FileUploadService.Source,
42
+ progress: Progress,
43
+ token: CancellationToken,
44
+ onDidUpload?: (uri: string) => void,
45
+ }
46
+
47
+ @injectable()
48
+ export class FileUploadServiceImpl implements FileUploadService {
49
+
50
+ static TARGET = 'target';
51
+ static UPLOAD = 'upload';
52
+
53
+ protected readonly onDidUploadEmitter = new Emitter<string[]>();
54
+ protected uploadForm: FileUploadService.Form;
55
+ protected deferredUpload?: Deferred<FileUploadService.UploadResult>;
56
+
57
+ @inject(FileSystemPreferences)
58
+ protected fileSystemPreferences: FileSystemPreferences;
59
+
60
+ @inject(FileService)
61
+ protected fileService: FileService;
62
+
63
+ @inject(MessageService)
64
+ protected readonly messageService: MessageService;
65
+
66
+ private readonly ignorePatterns: string[] = [];
67
+
68
+ get onDidUpload(): Event<string[]> {
69
+ return this.onDidUploadEmitter.event;
70
+ }
71
+
72
+ get maxConcurrentUploads(): number {
73
+ const maxConcurrentUploads = this.fileSystemPreferences['files.maxConcurrentUploads'];
74
+ return maxConcurrentUploads > 0 ? maxConcurrentUploads : Infinity;
75
+ }
76
+
77
+ @postConstruct()
78
+ protected init(): void {
79
+ this.uploadForm = this.createUploadForm();
80
+ }
81
+
82
+ protected createUploadForm(): FileUploadService.Form {
83
+ const targetInput = document.createElement('input');
84
+ targetInput.type = 'text';
85
+ targetInput.spellcheck = false;
86
+ targetInput.name = FileUploadServiceImpl.TARGET;
87
+ targetInput.classList.add('theia-input');
88
+
89
+ const fileInput = document.createElement('input');
90
+ fileInput.type = 'file';
91
+ fileInput.classList.add('theia-input');
92
+ fileInput.name = FileUploadServiceImpl.UPLOAD;
93
+ fileInput.multiple = true;
94
+
95
+ const form = document.createElement('form');
96
+ form.style.display = 'none';
97
+ form.enctype = 'multipart/form-data';
98
+ form.append(targetInput);
99
+ form.append(fileInput);
100
+
101
+ document.body.appendChild(form);
102
+
103
+ fileInput.addEventListener('change', () => {
104
+ if (this.deferredUpload && fileInput.value) {
105
+ const source: FileUploadService.Source = new FormData(form);
106
+ // clean up for reuse
107
+ fileInput.value = '';
108
+ const targetUri = new URI(<string>source.get(FileUploadServiceImpl.TARGET));
109
+ const { resolve, reject } = this.deferredUpload;
110
+ this.deferredUpload = undefined;
111
+ const { onDidUpload } = this.uploadForm;
112
+ this.withProgress(
113
+ (progress, token) => this.uploadFiles(targetUri, { source, progress, token, onDidUpload })
114
+ ).then(resolve, reject);
115
+ }
116
+ });
117
+
118
+ return { targetInput, fileInput };
119
+ }
120
+
121
+ async upload(targetUri: string | URI, params: FileUploadService.UploadParams): Promise<FileUploadService.UploadResult> {
122
+ const { source, onDidUpload } = params || {};
123
+
124
+ if (source) {
125
+ return this.withProgress(
126
+ (progress, token) => this.uploadFiles(
127
+ typeof targetUri === 'string' ? new URI(targetUri) : targetUri,
128
+ { source, progress, token, onDidUpload }
129
+ )
130
+ );
131
+ }
132
+ this.deferredUpload = new Deferred<FileUploadService.UploadResult>();
133
+ this.uploadForm.targetInput.value = String(targetUri);
134
+ this.uploadForm.fileInput.click();
135
+ this.uploadForm.onDidUpload = onDidUpload;
136
+ return this.deferredUpload.promise;
137
+ }
138
+
139
+ protected async withProgress<T>(
140
+ cb: (progress: Progress, token: CancellationToken) => Promise<T>
141
+ ): Promise<T> {
142
+ const cancellationSource = new CancellationTokenSource();
143
+ const { token } = cancellationSource;
144
+ const text = nls.localize('theia/filesystem/uploadFiles', 'Saving Files');
145
+ const progress = await this.messageService.showProgress(
146
+ { text, options: { cancelable: true } },
147
+ () => cancellationSource.cancel()
148
+ );
149
+ try {
150
+ return await cb(progress, token);
151
+ } finally {
152
+ progress.cancel();
153
+ }
154
+ }
155
+
156
+ protected async confirmOverwrite(fileUri: URI): Promise<boolean> {
157
+ const dialog = new ConfirmDialog({
158
+ title: nls.localizeByDefault('Replace'),
159
+ msg: nls.localizeByDefault("A file or folder with the name '{0}' already exists in the destination folder. Do you want to replace it?", fileUri.path.base),
160
+ ok: nls.localizeByDefault('Replace'),
161
+ cancel: Dialog.CANCEL
162
+ });
163
+ return !!await dialog.open();
164
+ }
165
+
166
+ /**
167
+ * Upload all files to the filesystem
168
+ */
169
+ protected async uploadFiles(targetUri: URI, params: UploadFilesParams): Promise<FileUploadService.UploadResult> {
170
+ const status = new Map<URI, UploadState>();
171
+
172
+ const report = throttle(() => {
173
+ const list = Array.from(status.values());
174
+ const total = list.length;
175
+ const done = list.filter(item => item.uploaded).length;
176
+
177
+ params.progress.report({
178
+ message: nls.localize('theia/filesystem/processedOutOf', 'Processed {0} out of {1}', done, total),
179
+ work: { total, done }
180
+ });
181
+ }, 100);
182
+
183
+ const uploads: Promise<void>[] = [];
184
+ const uploadSemaphore = new Semaphore(this.maxConcurrentUploads);
185
+
186
+ try {
187
+ const files = await this.enumerateFiles(targetUri, params.source, params.token);
188
+
189
+ for (const { file, uri } of files) {
190
+ checkCancelled(params.token);
191
+
192
+ // Check exists and confirm overwrite before adding to queue
193
+ if (await this.fileService.exists(uri) && !await this.confirmOverwrite(uri)) {
194
+ continue;
195
+ }
196
+
197
+ status.set(uri, {
198
+ uploaded: false
199
+ });
200
+ report();
201
+
202
+ uploads.push(uploadSemaphore.runExclusive(async () => {
203
+ const entry = status.get(uri);
204
+
205
+ try {
206
+ checkCancelled(params.token);
207
+
208
+ await this.uploadFile(file, uri);
209
+
210
+ if (entry) {
211
+ entry.uploaded = true;
212
+ report();
213
+ }
214
+
215
+ if (params.onDidUpload) {
216
+ params.onDidUpload(uri.toString(true));
217
+ }
218
+ } catch (error) {
219
+ if (entry) {
220
+ entry.failed = true;
221
+ report();
222
+ }
223
+ throw error;
224
+ }
225
+ }));
226
+ }
227
+
228
+ checkCancelled(params.token);
229
+ await Promise.all(uploads);
230
+ } catch (error) {
231
+ uploadSemaphore.cancel();
232
+
233
+ if (!isCancelled(error)) {
234
+ this.messageService.error(nls.localize('theia/filesystem/uploadFailed', 'An error occurred while saving a file. {0}', error.message));
235
+ throw error;
236
+ }
237
+ }
238
+
239
+ const uploaded = Array.from(status.keys()).map(uri => uri.toString(true));
240
+
241
+ this.onDidUploadEmitter.fire(uploaded);
242
+
243
+ return { uploaded };
244
+ }
245
+
246
+ /**
247
+ * Upload (write) a file directly to the filesystem
248
+ */
249
+ protected async uploadFile(
250
+ file: File,
251
+ targetUri: URI
252
+ ): Promise<void> {
253
+ await this.fileService.writeFile(targetUri, fileToStream(file));
254
+ }
255
+
256
+ /**
257
+ * Normalize sources into an array of { file, uri } objects
258
+ */
259
+ protected async enumerateFiles(targetUri: URI, source: FileUploadService.Source, token: CancellationToken): Promise<{ file: File; uri: URI }[]> {
260
+ checkCancelled(token);
261
+
262
+ if (source instanceof FormData) {
263
+ // Handle FormData declaratively
264
+ const files = source.getAll(FileUploadServiceImpl.UPLOAD)
265
+ .filter((entry): entry is File => entry instanceof File)
266
+ .filter(entry => this.shouldIncludeFile(entry.name))
267
+ .map(entry => ({
268
+ file: entry,
269
+ uri: targetUri.resolve(entry.name)
270
+ }));
271
+
272
+ return files;
273
+ } else if (source instanceof DataTransfer) {
274
+ // Use WebKit Entries for folder traversal
275
+ if (source.items && this.supportsWebKitEntries()) {
276
+ // Collect all files first
277
+ const allFiles: { file: File; uri: URI }[] = [];
278
+ const items = Array.from(source.items);
279
+ const entries = items.map(item => item.webkitGetAsEntry()).filter((entry): entry is WebKitEntry => !!entry);
280
+
281
+ for (let i = 0; i < entries.length; i++) {
282
+ const entry = entries[i];
283
+ const filesFromEntry = await this.traverseEntry(targetUri, entry!, token);
284
+
285
+ allFiles.push(...filesFromEntry);
286
+ }
287
+
288
+ return allFiles;
289
+ } else {
290
+ // Fall back to flat file list
291
+ return Array.from(source.files)
292
+ .filter((file): file is File => !!file)
293
+ .filter(file => this.shouldIncludeFile(file.name))
294
+ .map(file => ({
295
+ file,
296
+ uri: targetUri.resolve(file.name)
297
+ }));
298
+ }
299
+ } else {
300
+ // Handle CustomDataTransfer declaratively
301
+ const files = await Promise.all(
302
+ Array.from(source)
303
+ .map(async ([, item]) => {
304
+ const fileData = item.asFile();
305
+ if (fileData && this.shouldIncludeFile(fileData.name)) {
306
+ const data = await fileData.data();
307
+ return {
308
+ file: new File([data as BlobPart], fileData.name, { type: 'application/octet-stream' }),
309
+ uri: targetUri.resolve(fileData.name)
310
+ };
311
+ }
312
+ return undefined;
313
+ })
314
+ );
315
+
316
+ return files.filter(Boolean) as { file: File; uri: URI }[];
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Traverse WebKit Entries (files and folders)
322
+ */
323
+ protected async traverseEntry(
324
+ base: URI,
325
+ entry: WebKitEntry,
326
+ token: CancellationToken
327
+ ): Promise<{ file: File; uri: URI }[]> {
328
+ if (!entry) {
329
+ return [];
330
+ }
331
+
332
+ // Skip system entries
333
+ if (!this.shouldIncludeFile(entry.name)) {
334
+ return [];
335
+ }
336
+
337
+ // directory
338
+ if (entry.isDirectory) {
339
+ const dir = entry as WebKitDirectoryEntry;
340
+ const newBase = base.resolve(dir.name);
341
+
342
+ const entries = await this.readAllEntries(dir, token);
343
+ checkCancelled(token);
344
+
345
+ const chunks = await Promise.all(
346
+ entries.map(sub => this.traverseEntry(newBase, sub, token))
347
+ );
348
+
349
+ return chunks.flat();
350
+ }
351
+
352
+ // file
353
+ const fileEntry = entry as WebKitFileEntry;
354
+ const file = await this.readFileEntry(fileEntry, token);
355
+ checkCancelled(token);
356
+
357
+ return [{ file, uri: base.resolve(entry.name) }];
358
+ }
359
+
360
+ /**
361
+ * Read all entries from a WebKit directory entry
362
+ */
363
+ protected async readAllEntries(
364
+ dir: WebKitDirectoryEntry,
365
+ token: CancellationToken
366
+ ): Promise<WebKitEntry[]> {
367
+ const reader = dir.createReader();
368
+ const out: WebKitEntry[] = [];
369
+
370
+ while (true) {
371
+ checkCancelled(token);
372
+
373
+ const batch = await new Promise<WebKitEntry[]>((resolve, reject) =>
374
+ reader.readEntries(resolve, reject)
375
+ );
376
+
377
+ if (!batch.length) {break; }
378
+ out.push(...batch);
379
+
380
+ // yield to the event loop to keep UI responsive
381
+ await Promise.resolve();
382
+ }
383
+ return out;
384
+ }
385
+
386
+ /**
387
+ * Read a file from a WebKit file entry
388
+ */
389
+ protected async readFileEntry(
390
+ fileEntry: WebKitFileEntry,
391
+ token: CancellationToken
392
+ ): Promise<File> {
393
+ checkCancelled(token);
394
+ try {
395
+ return await new Promise<File>((resolve, reject) => fileEntry.file(resolve, reject));
396
+ } catch (err) {
397
+ throw err;
398
+ }
399
+ }
400
+
401
+ protected supportsWebKitEntries(): boolean {
402
+ return typeof DataTransferItem.prototype.webkitGetAsEntry === 'function';
403
+ }
404
+
405
+ protected shouldIncludeFile(path: string): boolean {
406
+ return !this.ignorePatterns.some((pattern: string) => minimatch(path, pattern));
407
+ }
408
+ }
@@ -0,0 +1,40 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2018 TypeFox and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { isObject, type URI } from '@theia/core/lib/common';
18
+
19
+ export interface FileDownloadData {
20
+ readonly uris: string[];
21
+ }
22
+
23
+ export namespace FileDownloadData {
24
+ export function is(arg: unknown): arg is FileDownloadData {
25
+ return isObject(arg) && 'uris' in arg;
26
+ }
27
+ }
28
+
29
+ export namespace FileDownloadService {
30
+ export interface DownloadOptions {
31
+ // `true` if the download link has to be copied to the clipboard. This will not trigger the actual download. Defaults to `false`.
32
+ readonly copyLink?: boolean;
33
+ }
34
+ }
35
+
36
+ export const FileDownloadService = Symbol('FileDownloadService');
37
+
38
+ export interface FileDownloadService {
39
+ download(uris: URI[], options?: FileDownloadService.DownloadOptions): Promise<void>;
40
+ }
@@ -970,8 +970,9 @@ export function etag(stat: { mtime: number | undefined, size: number | undefined
970
970
 
971
971
  return stat.mtime.toString(29) + stat.size.toString(31);
972
972
  }
973
+
973
974
  /**
974
- * Helper to format a raw byte size into a human readable label.
975
+ * Helper class for formatting and parsing byte sizes.
975
976
  */
976
977
  export class BinarySize {
977
978
  static readonly KB = 1024;
@@ -979,6 +980,9 @@ export class BinarySize {
979
980
  static readonly GB = BinarySize.MB * BinarySize.KB;
980
981
  static readonly TB = BinarySize.GB * BinarySize.KB;
981
982
 
983
+ /**
984
+ * Formats a byte size into a human readable string (e.g., "1.5MB", "2.3GB").
985
+ */
982
986
  static formatSize(size: number): string {
983
987
  if (size < BinarySize.KB) {
984
988
  return size + 'B';
@@ -994,4 +998,41 @@ export class BinarySize {
994
998
  }
995
999
  return (size / BinarySize.TB).toFixed(2) + 'TB';
996
1000
  }
1001
+
1002
+ /**
1003
+ * Parses a human readable string (e.g., "1.5MB", "2.3GB") and returns the size in bytes
1004
+ */
1005
+ static parseSize(sizeInput: string | number | undefined): number {
1006
+ if (typeof sizeInput === 'number') {
1007
+ return Math.round(sizeInput);
1008
+ }
1009
+
1010
+ if (!sizeInput) {
1011
+ return 0;
1012
+ }
1013
+
1014
+ const trimmed = sizeInput.trim().toUpperCase();
1015
+ const match = /^(\d+(?:\.\d+)?)([BKMG])?$/.exec(trimmed);
1016
+
1017
+ // If the format is invalid, return 0
1018
+ if (!match) {
1019
+ return 0;
1020
+ }
1021
+
1022
+ const value = parseFloat(match[1]);
1023
+ const unit = match[2];
1024
+
1025
+ switch (unit) {
1026
+ case 'K':
1027
+ return Math.round(value * BinarySize.KB);
1028
+ case 'M':
1029
+ return Math.round(value * BinarySize.MB);
1030
+ case 'G':
1031
+ return Math.round(value * BinarySize.GB);
1032
+ case 'T':
1033
+ return Math.round(value * BinarySize.TB);
1034
+ default:
1035
+ return Math.round(value);
1036
+ }
1037
+ }
997
1038
  }
@@ -15,15 +15,12 @@
15
15
  // *****************************************************************************
16
16
 
17
17
  import { interfaces } from '@theia/core/shared/inversify';
18
- import {
19
- createPreferenceProxy,
20
- PreferenceProxy,
21
- PreferenceService,
22
- PreferenceSchema,
23
- PreferenceContribution
24
- } from '@theia/core/lib/browser/preferences';
25
- import { SUPPORTED_ENCODINGS } from '@theia/core/lib/browser/supported-encodings';
18
+ import { createPreferenceProxy, PreferenceProxy } from '@theia/core/lib/common/preferences/preference-proxy';
19
+ import { PreferenceScope } from '@theia/core/lib/common/preferences/preference-scope';
20
+ import { PreferenceService } from '@theia/core/lib/common/preferences/preference-service';
21
+ import { SUPPORTED_ENCODINGS } from '@theia/core/lib/common/supported-encodings';
26
22
  import { nls } from '@theia/core/lib/common/nls';
23
+ import { PreferenceContribution, PreferenceSchema } from '@theia/core/lib/common/preferences/preference-schema';
27
24
 
28
25
  // See https://github.com/Microsoft/vscode/issues/30180
29
26
  export const WIN32_MAX_FILE_SIZE_MB = 300; // 300 MB
@@ -36,7 +33,6 @@ export const MAX_FILE_SIZE_MB = typeof process === 'object'
36
33
  : 32;
37
34
 
38
35
  export const filesystemPreferenceSchema: PreferenceSchema = {
39
- type: 'object',
40
36
  properties: {
41
37
  'files.watcherExclude': {
42
38
  // eslint-disable-next-line max-len
@@ -48,14 +44,14 @@ export const filesystemPreferenceSchema: PreferenceSchema = {
48
44
  '**/.git/objects/**': true,
49
45
  '**/.git/subtree-cache/**': true
50
46
  },
51
- scope: 'resource'
47
+ scope: PreferenceScope.Folder
52
48
  },
53
49
  'files.exclude': {
54
50
  type: 'object',
55
51
  default: { '**/.git': true, '**/.svn': true, '**/.hg': true, '**/CVS': true, '**/.DS_Store': true },
56
52
  // eslint-disable-next-line max-len
57
53
  markdownDescription: nls.localize('theia/filesystem/filesExclude', 'Configure glob patterns for excluding files and folders. For example, the file Explorer decides which files and folders to show or hide based on this setting.'),
58
- scope: 'resource'
54
+ scope: PreferenceScope.Folder
59
55
  },
60
56
  'files.enableTrash': {
61
57
  type: 'boolean',
@@ -64,6 +60,7 @@ export const filesystemPreferenceSchema: PreferenceSchema = {
64
60
  },
65
61
  'files.associations': {
66
62
  type: 'object',
63
+ default: {},
67
64
  markdownDescription: nls.localizeByDefault(
68
65
  // eslint-disable-next-line max-len
69
66
  'Configure [glob patterns](https://aka.ms/vscode-glob-patterns) of file associations to languages (for example `\"*.extension\": \"html\"`). Patterns will match on the absolute path of a file if they contain a path separator and will match on the name of the file otherwise. These have precedence over the default associations of the languages installed.'
@@ -74,7 +71,8 @@ export const filesystemPreferenceSchema: PreferenceSchema = {
74
71
  default: false,
75
72
  // eslint-disable-next-line max-len
76
73
  description: nls.localizeByDefault('When enabled, the editor will attempt to guess the character set encoding when opening files. This setting can also be configured per language. Note, this setting is not respected by text search. Only {0} is respected.', '`#files.encoding#`'),
77
- scope: 'language-overridable',
74
+ scope: PreferenceScope.Folder,
75
+ overridable: true,
78
76
  included: Object.keys(SUPPORTED_ENCODINGS).length > 1
79
77
  },
80
78
  'files.participants.timeout': {
@@ -93,13 +91,15 @@ export const filesystemPreferenceSchema: PreferenceSchema = {
93
91
  type: 'boolean',
94
92
  default: false,
95
93
  description: nls.localizeByDefault('When enabled, will trim trailing whitespace when saving a file.'),
96
- scope: 'language-overridable'
94
+ scope: PreferenceScope.Folder,
95
+ overridable: true
97
96
  },
98
97
  'files.insertFinalNewline': {
99
98
  type: 'boolean',
100
99
  default: false,
101
100
  description: nls.localizeByDefault('When enabled, insert a final new line at the end of the file when saving it.'),
102
- scope: 'language-overridable'
101
+ scope: PreferenceScope.Folder,
102
+ overridable: true
103
103
  },
104
104
  'files.maxConcurrentUploads': {
105
105
  type: 'integer',
@@ -16,3 +16,4 @@
16
16
 
17
17
  export * from './filesystem';
18
18
  export * from './filesystem-utils';
19
+ export * from './filesystem-preferences';
package/src/common/io.ts CHANGED
@@ -75,7 +75,12 @@ async function doReadFileIntoStream<T>(provider: FileSystemProviderWithOpenReadW
75
75
  const handle = await provider.open(resource, { create: false });
76
76
 
77
77
  // Check for cancellation
78
- throwIfCancelled(token);
78
+ try {
79
+ throwIfCancelled(token);
80
+ } catch (error) {
81
+ await provider.close(handle);
82
+ throw error;
83
+ }
79
84
 
80
85
  try {
81
86
  let totalBytesRead = 0;
@@ -0,0 +1,65 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2019 TypeFox and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import URI from '@theia/core/lib/common/uri';
18
+ import { CancellationToken } from '@theia/core/lib/common/cancellation';
19
+ import { Progress } from '@theia/core/lib/common/message-service-protocol';
20
+ import { Event } from '@theia/core/lib/common/event';
21
+
22
+ export type CustomDataTransfer = Iterable<readonly [string, CustomDataTransferItem]>;
23
+
24
+ export interface CustomDataTransferItem {
25
+ asFile(): {
26
+ readonly id: string;
27
+ readonly name: string;
28
+ data(): Promise<Uint8Array>;
29
+ } | undefined
30
+ }
31
+
32
+ export interface FileUploadService {
33
+ upload(targetUri: string | URI, params?: FileUploadService.UploadParams): Promise<FileUploadService.UploadResult>;
34
+ readonly onDidUpload: Event<string[]>;
35
+ }
36
+
37
+ export namespace FileUploadService {
38
+ export type Source = FormData | DataTransfer | CustomDataTransfer;
39
+ export interface UploadEntry {
40
+ file: File
41
+ uri: URI
42
+ }
43
+ export interface Context {
44
+ progress: Progress
45
+ token: CancellationToken
46
+ accept: (entry: UploadEntry) => Promise<void>
47
+ }
48
+ export interface Form {
49
+ targetInput: HTMLInputElement
50
+ fileInput: HTMLInputElement
51
+ onDidUpload?: (uri: string) => void
52
+ }
53
+ export interface UploadParams {
54
+ source?: FileUploadService.Source,
55
+ progress?: Progress,
56
+ token?: CancellationToken,
57
+ onDidUpload?: (uri: string) => void,
58
+ leaveInTemp?: boolean
59
+ }
60
+ export interface UploadResult {
61
+ uploaded: string[]
62
+ }
63
+ }
64
+
65
+ export const FileUploadService = Symbol('FileUploadService');