@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.
- package/lib/browser/download/file-download-command-contribution.d.ts +1 -1
- package/lib/browser/download/file-download-command-contribution.d.ts.map +1 -1
- package/lib/browser/download/file-download-command-contribution.js +3 -3
- package/lib/browser/download/file-download-command-contribution.js.map +1 -1
- package/lib/browser/download/file-download-frontend-module.d.ts.map +1 -1
- package/lib/browser/download/file-download-frontend-module.js +2 -1
- package/lib/browser/download/file-download-frontend-module.js.map +1 -1
- package/lib/browser/download/file-download-service.d.ts +2 -10
- package/lib/browser/download/file-download-service.d.ts.map +1 -1
- package/lib/browser/download/file-download-service.js +8 -7
- package/lib/browser/download/file-download-service.js.map +1 -1
- package/lib/browser/file-resource.js +1 -1
- package/lib/browser/file-resource.js.map +1 -1
- package/lib/browser/file-service.d.ts +1 -1
- package/lib/browser/file-service.d.ts.map +1 -1
- package/lib/browser/file-service.js +1 -1
- package/lib/browser/file-service.js.map +1 -1
- package/lib/browser/file-tree/file-tree-widget.d.ts +1 -1
- package/lib/browser/file-tree/file-tree-widget.d.ts.map +1 -1
- package/lib/browser/file-tree/file-tree-widget.js +3 -3
- package/lib/browser/file-tree/file-tree-widget.js.map +1 -1
- package/lib/browser/filesystem-frontend-contribution.d.ts +5 -4
- package/lib/browser/filesystem-frontend-contribution.d.ts.map +1 -1
- package/lib/browser/filesystem-frontend-contribution.js +5 -5
- package/lib/browser/filesystem-frontend-contribution.js.map +1 -1
- package/lib/browser/filesystem-frontend-module.d.ts.map +1 -1
- package/lib/browser/filesystem-frontend-module.js +4 -3
- package/lib/browser/filesystem-frontend-module.js.map +1 -1
- package/lib/browser/filesystem-saveable-service.d.ts.map +1 -1
- package/lib/browser/filesystem-saveable-service.js +5 -1
- package/lib/browser/filesystem-saveable-service.js.map +1 -1
- package/lib/browser/index.d.ts +0 -1
- package/lib/browser/index.d.ts.map +1 -1
- package/lib/browser/index.js +0 -1
- package/lib/browser/index.js.map +1 -1
- package/lib/browser/{file-upload-service.d.ts → upload/file-upload-service-impl.d.ts} +16 -52
- package/lib/browser/upload/file-upload-service-impl.d.ts.map +1 -0
- package/lib/browser/{file-upload-service.js → upload/file-upload-service-impl.js} +27 -27
- package/lib/browser/upload/file-upload-service-impl.js.map +1 -0
- package/lib/browser-only/browser-only-filesystem-frontend-module.d.ts.map +1 -1
- package/lib/browser-only/browser-only-filesystem-frontend-module.js +8 -0
- package/lib/browser-only/browser-only-filesystem-frontend-module.js.map +1 -1
- package/lib/browser-only/download/file-download-command-contribution.d.ts +15 -0
- package/lib/browser-only/download/file-download-command-contribution.d.ts.map +1 -0
- package/lib/browser-only/download/file-download-command-contribution.js +55 -0
- package/lib/browser-only/download/file-download-command-contribution.js.map +1 -0
- package/lib/browser-only/download/file-download-frontend-module.d.ts +4 -0
- package/lib/browser-only/download/file-download-frontend-module.d.ts.map +1 -0
- package/lib/browser-only/download/file-download-frontend-module.js +27 -0
- package/lib/browser-only/download/file-download-frontend-module.js.map +1 -0
- package/lib/browser-only/download/file-download-service.d.ts +86 -0
- package/lib/browser-only/download/file-download-service.d.ts.map +1 -0
- package/lib/browser-only/download/file-download-service.js +551 -0
- package/lib/browser-only/download/file-download-service.js.map +1 -0
- package/lib/browser-only/file-search.d.ts +38 -0
- package/lib/browser-only/file-search.d.ts.map +1 -0
- package/lib/browser-only/file-search.js +153 -0
- package/lib/browser-only/file-search.js.map +1 -0
- package/lib/browser-only/opfs-filesystem-initialization.d.ts +4 -2
- package/lib/browser-only/opfs-filesystem-initialization.d.ts.map +1 -1
- package/lib/browser-only/opfs-filesystem-initialization.js +4 -1
- package/lib/browser-only/opfs-filesystem-initialization.js.map +1 -1
- package/lib/browser-only/opfs-filesystem-provider.d.ts +89 -12
- package/lib/browser-only/opfs-filesystem-provider.d.ts.map +1 -1
- package/lib/browser-only/opfs-filesystem-provider.js +345 -181
- package/lib/browser-only/opfs-filesystem-provider.js.map +1 -1
- package/lib/browser-only/upload/file-upload-service-impl.d.ts +67 -0
- package/lib/browser-only/upload/file-upload-service-impl.d.ts.map +1 -0
- package/lib/browser-only/upload/file-upload-service-impl.js +328 -0
- package/lib/browser-only/upload/file-upload-service-impl.js.map +1 -0
- package/lib/common/download/file-download.d.ts +17 -0
- package/lib/common/download/file-download.d.ts.map +1 -0
- package/lib/common/download/{file-download-data.js → file-download.js} +3 -2
- package/lib/common/download/file-download.js.map +1 -0
- package/lib/common/files.d.ts +8 -1
- package/lib/common/files.d.ts.map +1 -1
- package/lib/common/files.js +35 -1
- package/lib/common/files.js.map +1 -1
- package/lib/{browser → common}/filesystem-preferences.d.ts +3 -1
- package/lib/common/filesystem-preferences.d.ts.map +1 -0
- package/lib/{browser → common}/filesystem-preferences.js +17 -11
- package/lib/common/filesystem-preferences.js.map +1 -0
- package/lib/common/index.d.ts +1 -0
- package/lib/common/index.d.ts.map +1 -1
- package/lib/common/index.js +1 -0
- package/lib/common/index.js.map +1 -1
- package/lib/common/io.js +7 -1
- package/lib/common/io.js.map +1 -1
- package/lib/common/upload/file-upload.d.ts +45 -0
- package/lib/common/upload/file-upload.d.ts.map +1 -0
- package/{src/common/download/file-download-data.ts → lib/common/upload/file-upload.js} +6 -13
- package/lib/common/upload/file-upload.js.map +1 -0
- package/lib/node/disk-file-system-provider.d.ts.map +1 -1
- package/lib/node/disk-file-system-provider.js +2 -4
- package/lib/node/disk-file-system-provider.js.map +1 -1
- package/lib/node/download/file-download-handler.js +2 -2
- package/lib/node/download/file-download-handler.js.map +1 -1
- package/lib/node/filesystem-backend-module.d.ts.map +1 -1
- package/lib/node/filesystem-backend-module.js +3 -1
- package/lib/node/filesystem-backend-module.js.map +1 -1
- package/lib/node/parcel-watcher/parcel-filesystem-service.d.ts +2 -2
- package/lib/node/parcel-watcher/parcel-filesystem-service.d.ts.map +1 -1
- package/lib/node/parcel-watcher/parcel-filesystem-service.js.map +1 -1
- package/lib/node/upload/node-file-upload-service.d.ts.map +1 -0
- package/lib/node/{node-file-upload-service.js → upload/node-file-upload-service.js} +1 -1
- package/lib/node/upload/node-file-upload-service.js.map +1 -0
- package/package.json +11 -5
- package/src/browser/download/file-download-command-contribution.ts +1 -1
- package/src/browser/download/file-download-frontend-module.ts +3 -2
- package/src/browser/download/file-download-service.ts +7 -12
- package/src/browser/file-resource.ts +1 -1
- package/src/browser/file-service.ts +1 -1
- package/src/browser/file-tree/file-tree-widget.tsx +1 -1
- package/src/browser/filesystem-frontend-contribution.ts +4 -5
- package/src/browser/filesystem-frontend-module.ts +4 -3
- package/src/browser/filesystem-saveable-service.ts +5 -1
- package/src/browser/index.ts +0 -1
- package/src/browser/{file-upload-service.ts → upload/file-upload-service-impl.ts} +31 -72
- package/src/browser-only/browser-only-filesystem-frontend-module.ts +10 -0
- package/src/browser-only/download/file-download-command-contribution.ts +56 -0
- package/src/browser-only/download/file-download-frontend-module.ts +26 -0
- package/src/browser-only/download/file-download-service.ts +726 -0
- package/src/browser-only/file-search.ts +170 -0
- package/src/browser-only/opfs-filesystem-initialization.ts +7 -4
- package/src/browser-only/opfs-filesystem-provider.ts +402 -189
- package/src/browser-only/upload/file-upload-service-impl.ts +408 -0
- package/src/common/download/file-download.ts +40 -0
- package/src/common/files.ts +42 -1
- package/src/{browser → common}/filesystem-preferences.ts +14 -14
- package/src/common/index.ts +1 -0
- package/src/common/io.ts +6 -1
- package/src/common/upload/file-upload.ts +65 -0
- package/src/node/disk-file-system-provider.ts +3 -4
- package/src/node/download/file-download-handler.ts +1 -1
- package/src/node/filesystem-backend-module.ts +3 -1
- package/src/node/parcel-watcher/parcel-filesystem-service.ts +2 -2
- package/src/node/{node-file-upload-service.ts → upload/node-file-upload-service.ts} +1 -1
- package/lib/browser/file-upload-service.d.ts.map +0 -1
- package/lib/browser/file-upload-service.js.map +0 -1
- package/lib/browser/filesystem-preferences.d.ts.map +0 -1
- package/lib/browser/filesystem-preferences.js.map +0 -1
- package/lib/common/download/file-download-data.d.ts +0 -7
- package/lib/common/download/file-download-data.d.ts.map +0 -1
- package/lib/common/download/file-download-data.js.map +0 -1
- package/lib/node/node-file-upload-service.d.ts.map +0 -1
- package/lib/node/node-file-upload-service.js.map +0 -1
- /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
|
+
}
|
package/src/common/files.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
101
|
+
scope: PreferenceScope.Folder,
|
|
102
|
+
overridable: true
|
|
103
103
|
},
|
|
104
104
|
'files.maxConcurrentUploads': {
|
|
105
105
|
type: 'integer',
|
package/src/common/index.ts
CHANGED
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
|
-
|
|
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');
|