@theia/filesystem 1.55.1 → 1.57.0-next.112

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 (56) hide show
  1. package/README.md +2 -1
  2. package/lib/browser/breadcrumbs/filepath-breadcrumbs-container.d.ts +1 -2
  3. package/lib/browser/breadcrumbs/filepath-breadcrumbs-container.d.ts.map +1 -1
  4. package/lib/browser/breadcrumbs/filepath-breadcrumbs-container.js +0 -4
  5. package/lib/browser/breadcrumbs/filepath-breadcrumbs-container.js.map +1 -1
  6. package/lib/browser/filesystem-frontend-module.d.ts.map +1 -1
  7. package/lib/browser/filesystem-frontend-module.js +4 -0
  8. package/lib/browser/filesystem-frontend-module.js.map +1 -1
  9. package/lib/browser/filesystem-preferences.d.ts +1 -0
  10. package/lib/browser/filesystem-preferences.d.ts.map +1 -1
  11. package/lib/browser/filesystem-preferences.js +6 -0
  12. package/lib/browser/filesystem-preferences.js.map +1 -1
  13. package/lib/browser/vscode-file-service-contribution.d.ts +25 -0
  14. package/lib/browser/vscode-file-service-contribution.d.ts.map +1 -0
  15. package/lib/browser/vscode-file-service-contribution.js +99 -0
  16. package/lib/browser/vscode-file-service-contribution.js.map +1 -0
  17. package/lib/browser-only/browser-only-filesystem-frontend-module.js +7 -7
  18. package/lib/browser-only/browser-only-filesystem-frontend-module.js.map +1 -1
  19. package/lib/browser-only/opfs-filesystem-initialization.d.ts +11 -0
  20. package/lib/browser-only/opfs-filesystem-initialization.d.ts.map +1 -0
  21. package/lib/browser-only/opfs-filesystem-initialization.js +33 -0
  22. package/lib/browser-only/opfs-filesystem-initialization.js.map +1 -0
  23. package/lib/browser-only/opfs-filesystem-provider.d.ts +31 -0
  24. package/lib/browser-only/opfs-filesystem-provider.d.ts.map +1 -0
  25. package/lib/browser-only/opfs-filesystem-provider.js +323 -0
  26. package/lib/browser-only/opfs-filesystem-provider.js.map +1 -0
  27. package/lib/common/remote-file-system-provider.d.ts +3 -3
  28. package/lib/common/remote-file-system-provider.d.ts.map +1 -1
  29. package/lib/common/remote-file-system-provider.js +28 -29
  30. package/lib/common/remote-file-system-provider.js.map +1 -1
  31. package/lib/node/download/file-download-endpoint.d.ts +0 -1
  32. package/lib/node/download/file-download-endpoint.d.ts.map +1 -1
  33. package/lib/node/download/file-download-handler.d.ts +0 -1
  34. package/lib/node/download/file-download-handler.d.ts.map +1 -1
  35. package/lib/node/node-file-upload-service.d.ts +0 -1
  36. package/lib/node/node-file-upload-service.d.ts.map +1 -1
  37. package/package.json +4 -5
  38. package/src/browser/breadcrumbs/filepath-breadcrumbs-container.ts +1 -4
  39. package/src/browser/filesystem-frontend-module.ts +4 -0
  40. package/src/browser/filesystem-preferences.ts +7 -0
  41. package/src/browser/vscode-file-service-contribution.ts +93 -0
  42. package/src/browser-only/browser-only-filesystem-frontend-module.ts +7 -7
  43. package/src/browser-only/opfs-filesystem-initialization.ts +36 -0
  44. package/src/browser-only/opfs-filesystem-provider.ts +346 -0
  45. package/src/common/download/README.md +8 -4
  46. package/src/common/remote-file-system-provider.ts +32 -34
  47. package/lib/browser-only/browserfs-filesystem-initialization.d.ts +0 -13
  48. package/lib/browser-only/browserfs-filesystem-initialization.d.ts.map +0 -1
  49. package/lib/browser-only/browserfs-filesystem-initialization.js +0 -55
  50. package/lib/browser-only/browserfs-filesystem-initialization.js.map +0 -1
  51. package/lib/browser-only/browserfs-filesystem-provider.d.ts +0 -46
  52. package/lib/browser-only/browserfs-filesystem-provider.d.ts.map +0 -1
  53. package/lib/browser-only/browserfs-filesystem-provider.js +0 -440
  54. package/lib/browser-only/browserfs-filesystem-provider.js.map +0 -1
  55. package/src/browser-only/browserfs-filesystem-initialization.ts +0 -61
  56. package/src/browser-only/browserfs-filesystem-provider.ts +0 -462
package/package.json CHANGED
@@ -1,15 +1,14 @@
1
1
  {
2
2
  "name": "@theia/filesystem",
3
- "version": "1.55.1",
3
+ "version": "1.57.0-next.112+f4778c273",
4
4
  "description": "Theia - FileSystem Extension",
5
5
  "dependencies": {
6
- "@theia/core": "1.55.1",
6
+ "@theia/core": "1.57.0-next.112+f4778c273",
7
7
  "@types/body-parser": "^1.17.0",
8
8
  "@types/multer": "^1.4.7",
9
9
  "@types/tar-fs": "^1.16.1",
10
10
  "async-mutex": "^0.3.1",
11
11
  "body-parser": "^1.18.3",
12
- "browserfs": "^1.4.3",
13
12
  "http-status-codes": "^1.3.0",
14
13
  "minimatch": "^5.1.0",
15
14
  "multer": "1.4.4-lts.1",
@@ -72,10 +71,10 @@
72
71
  "watch": "theiaext watch"
73
72
  },
74
73
  "devDependencies": {
75
- "@theia/ext-scripts": "1.55.1"
74
+ "@theia/ext-scripts": "1.58.0"
76
75
  },
77
76
  "nyc": {
78
77
  "extends": "../../configs/nyc.json"
79
78
  },
80
- "gitHead": "be4713a778be16ba831af63a6fbd43b2301e882d"
79
+ "gitHead": "f4778c2737bb75613f0e1f99da8996bad91f6e17"
81
80
  }
@@ -15,7 +15,7 @@
15
15
  // *****************************************************************************
16
16
 
17
17
  import { Container, interfaces, injectable, inject } from '@theia/core/shared/inversify';
18
- import { TreeProps, ContextMenuRenderer, TreeNode, OpenerService, open, NodeProps, defaultTreeProps } from '@theia/core/lib/browser';
18
+ import { TreeProps, ContextMenuRenderer, TreeNode, open, NodeProps, defaultTreeProps } from '@theia/core/lib/browser';
19
19
  import { FileTreeModel, FileStatNode, createFileTreeContainer, FileTreeWidget } from '../file-tree';
20
20
 
21
21
  const BREADCRUMBS_FILETREE_CLASS = 'theia-FilepathBreadcrumbFileTree';
@@ -35,9 +35,6 @@ export function createFileTreeBreadcrumbsWidget(parent: interfaces.Container): B
35
35
  @injectable()
36
36
  export class BreadcrumbsFileTreeWidget extends FileTreeWidget {
37
37
 
38
- @inject(OpenerService)
39
- protected readonly openerService: OpenerService;
40
-
41
38
  constructor(
42
39
  @inject(TreeProps) props: TreeProps,
43
40
  @inject(FileTreeModel) override readonly model: FileTreeModel,
@@ -33,6 +33,7 @@ import { FilepathBreadcrumbsContribution } from './breadcrumbs/filepath-breadcru
33
33
  import { BreadcrumbsFileTreeWidget, createFileTreeBreadcrumbsWidget } from './breadcrumbs/filepath-breadcrumbs-container';
34
34
  import { FilesystemSaveableService } from './filesystem-saveable-service';
35
35
  import { SaveableService } from '@theia/core/lib/browser/saveable-service';
36
+ import { VSCodeFileServiceContribution, VSCodeFileSystemProvider } from './vscode-file-service-contribution';
36
37
 
37
38
  export default new ContainerModule((bind, unbind, isBound, rebind) => {
38
39
  bindFileSystemPreferences(bind);
@@ -46,6 +47,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
46
47
  bind(RemoteFileSystemProvider).toSelf().inSingletonScope();
47
48
  bind(RemoteFileServiceContribution).toSelf().inSingletonScope();
48
49
  bind(FileServiceContribution).toService(RemoteFileServiceContribution);
50
+ bind(VSCodeFileSystemProvider).toSelf().inSingletonScope();
51
+ bind(VSCodeFileServiceContribution).toSelf().inSingletonScope();
52
+ bind(FileServiceContribution).toService(VSCodeFileServiceContribution);
49
53
 
50
54
  bind(FileSystemWatcherErrorHandler).toSelf().inSingletonScope();
51
55
 
@@ -95,6 +95,12 @@ export const filesystemPreferenceSchema: PreferenceSchema = {
95
95
  description: nls.localizeByDefault('When enabled, will trim trailing whitespace when saving a file.'),
96
96
  scope: 'language-overridable'
97
97
  },
98
+ 'files.insertFinalNewline': {
99
+ type: 'boolean',
100
+ default: false,
101
+ description: nls.localizeByDefault('When enabled, insert a final new line at the end of the file when saving it.'),
102
+ scope: 'language-overridable'
103
+ },
98
104
  'files.maxConcurrentUploads': {
99
105
  type: 'integer',
100
106
  default: 1,
@@ -116,6 +122,7 @@ export interface FileSystemConfiguration {
116
122
  'files.participants.timeout': number
117
123
  'files.maxFileSizeMB': number
118
124
  'files.trimTrailingWhitespace': boolean
125
+ 'files.insertFinalNewline': boolean
119
126
  'files.maxConcurrentUploads': number
120
127
  }
121
128
 
@@ -0,0 +1,93 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2024 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 { inject, injectable } from '@theia/core/shared/inversify';
18
+ import { FileServiceContribution, FileService } from './file-service';
19
+ import {
20
+ FileChange, FileDeleteOptions, FileOverwriteOptions, FilePermission, FileSystemProvider, FileSystemProviderCapabilities, FileType, FileWriteOptions, Stat, WatchOptions
21
+ } from '../common/files';
22
+ import { Event, URI, Disposable, Emitter } from '@theia/core';
23
+ import { JsonSchemaDataStore } from '@theia/core/lib/browser/json-schema-store';
24
+ import { BinaryBuffer } from '@theia/core/lib/common/buffer';
25
+
26
+ @injectable()
27
+ export class VSCodeFileSystemProvider implements FileSystemProvider {
28
+ readonly capabilities = FileSystemProviderCapabilities.Readonly + FileSystemProviderCapabilities.FileReadWrite;
29
+ readonly onDidChangeCapabilities = Event.None;
30
+ protected readonly onDidChangeFileEmitter = new Emitter<readonly FileChange[]>();
31
+ readonly onDidChangeFile = this.onDidChangeFileEmitter.event;
32
+ readonly onFileWatchError = Event.None;
33
+
34
+ @inject(JsonSchemaDataStore)
35
+ protected readonly store: JsonSchemaDataStore;
36
+
37
+ watch(resource: URI, opts: WatchOptions): Disposable {
38
+ return Disposable.NULL;
39
+ }
40
+ async stat(resource: URI): Promise<Stat> {
41
+ if (this.store.hasSchema(resource)) {
42
+ const currentTime = Date.now();
43
+ return {
44
+ type: FileType.File,
45
+ permissions: FilePermission.Readonly,
46
+ mtime: currentTime,
47
+ ctime: currentTime,
48
+ size: 0
49
+ };
50
+ }
51
+ throw new Error('Not Found!');
52
+ }
53
+ mkdir(resource: URI): Promise<void> {
54
+ return Promise.resolve();
55
+ }
56
+ readdir(resource: URI): Promise<[string, FileType][]> {
57
+ return Promise.resolve([]);
58
+ }
59
+ delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
60
+ return Promise.resolve();
61
+ }
62
+ rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise<void> {
63
+ return Promise.resolve();
64
+ }
65
+ async readFile(resource: URI): Promise<Uint8Array> {
66
+ if (resource.scheme !== 'vscode') {
67
+ throw new Error('Not Supported!');
68
+ }
69
+ let content: string | undefined;
70
+ if (resource.authority === 'schemas') {
71
+ content = this.store.getSchema(resource);
72
+ }
73
+ if (typeof content === 'string') {
74
+ return BinaryBuffer.fromString(content).buffer;
75
+ }
76
+ throw new Error('Not Found!');
77
+ }
78
+ writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
79
+ throw new Error('Not Supported!');
80
+ }
81
+ }
82
+
83
+ @injectable()
84
+ export class VSCodeFileServiceContribution implements FileServiceContribution {
85
+
86
+ @inject(VSCodeFileSystemProvider)
87
+ protected readonly provider: VSCodeFileSystemProvider;
88
+
89
+ registerFileSystemProviders(service: FileService): void {
90
+ service.registerProvider('vscode', this.provider);
91
+ }
92
+
93
+ }
@@ -16,19 +16,19 @@
16
16
 
17
17
  import { ContainerModule } from '@theia/core/shared/inversify';
18
18
  import { FileSystemProvider } from '../common/files';
19
- import { BrowserFSFileSystemProvider } from './browserfs-filesystem-provider';
19
+ import { OPFSFileSystemProvider } from './opfs-filesystem-provider';
20
20
  import { RemoteFileSystemProvider, RemoteFileSystemServer } from '../common/remote-file-system-provider';
21
- import { BrowserFSInitialization, DefaultBrowserFSInitialization } from './browserfs-filesystem-initialization';
21
+ import { OPFSInitialization, DefaultOPFSInitialization } from './opfs-filesystem-initialization';
22
22
  import { BrowserOnlyFileSystemProviderServer } from './browser-only-filesystem-provider-server';
23
23
 
24
24
  export default new ContainerModule((bind, _unbind, isBound, rebind) => {
25
- bind(DefaultBrowserFSInitialization).toSelf();
26
- bind(BrowserFSFileSystemProvider).toSelf();
27
- bind(BrowserFSInitialization).toService(DefaultBrowserFSInitialization);
25
+ bind(DefaultOPFSInitialization).toSelf();
26
+ bind(OPFSFileSystemProvider).toSelf();
27
+ bind(OPFSInitialization).toService(DefaultOPFSInitialization);
28
28
  if (isBound(FileSystemProvider)) {
29
- rebind(FileSystemProvider).to(BrowserFSFileSystemProvider).inSingletonScope();
29
+ rebind(FileSystemProvider).to(OPFSFileSystemProvider).inSingletonScope();
30
30
  } else {
31
- bind(FileSystemProvider).to(BrowserFSFileSystemProvider).inSingletonScope();
31
+ bind(FileSystemProvider).to(OPFSFileSystemProvider).inSingletonScope();
32
32
  }
33
33
  if (isBound(RemoteFileSystemProvider)) {
34
34
  rebind(RemoteFileSystemServer).to(BrowserOnlyFileSystemProviderServer).inSingletonScope();
@@ -0,0 +1,36 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2023 EclipseSource 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 type { OPFSFileSystemProvider } from './opfs-filesystem-provider';
18
+ import { injectable } from '@theia/core/shared/inversify';
19
+
20
+ export const OPFSInitialization = Symbol('OPFSInitialization');
21
+ export interface OPFSInitialization {
22
+ getRootDirectory(): Promise<FileSystemDirectoryHandle>
23
+ initializeFS(provider: OPFSFileSystemProvider): Promise<void>;
24
+ }
25
+
26
+ @injectable()
27
+ export class DefaultOPFSInitialization implements OPFSInitialization {
28
+
29
+ getRootDirectory(): Promise<FileSystemDirectoryHandle> {
30
+ return navigator.storage.getDirectory();
31
+ }
32
+
33
+ async initializeFS(provider: OPFSFileSystemProvider): Promise<void> {
34
+
35
+ }
36
+ }
@@ -0,0 +1,346 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2024 EclipseSource 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 { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
18
+ import {
19
+ FileChange, FileChangeType, FileDeleteOptions,
20
+ FileOverwriteOptions, FileSystemProviderCapabilities,
21
+ FileSystemProviderError,
22
+ FileSystemProviderErrorCode,
23
+ FileSystemProviderWithFileReadWriteCapability,
24
+ FileType, FileWriteOptions, Stat, WatchOptions, createFileSystemProviderError
25
+ } from '../common/files';
26
+ import { Emitter, Event, URI, Disposable, Path } from '@theia/core';
27
+ import { OPFSInitialization } from './opfs-filesystem-initialization';
28
+
29
+ /** Options to be used when traversing the file system handles */
30
+ interface CreateFileSystemHandleOptions {
31
+ isDirectory?: boolean;
32
+ create?: boolean;
33
+ }
34
+
35
+ @injectable()
36
+ export class OPFSFileSystemProvider implements FileSystemProviderWithFileReadWriteCapability {
37
+ capabilities: FileSystemProviderCapabilities = FileSystemProviderCapabilities.FileReadWrite;
38
+ onDidChangeCapabilities: Event<void> = Event.None;
39
+
40
+ private readonly onDidChangeFileEmitter = new Emitter<readonly FileChange[]>();
41
+ readonly onDidChangeFile = this.onDidChangeFileEmitter.event;
42
+ onFileWatchError: Event<void> = Event.None;
43
+
44
+ @inject(OPFSInitialization)
45
+ protected readonly initialization: OPFSInitialization;
46
+
47
+ private directoryHandle: FileSystemDirectoryHandle;
48
+ private initialized: Promise<true>;
49
+
50
+ @postConstruct()
51
+ protected init(): void {
52
+ const setup = async (): Promise<true> => {
53
+ this.directoryHandle = await this.initialization.getRootDirectory();
54
+ await this.initialization.initializeFS(new Proxy(this, {
55
+ get(target, prop, receiver): unknown {
56
+ if (prop === 'initialized') {
57
+ return Promise.resolve(true);
58
+ }
59
+ return Reflect.get(target, prop, receiver);
60
+ }
61
+ }));
62
+ return true;
63
+ };
64
+ this.initialized = setup();
65
+ }
66
+
67
+ watch(_resource: URI, _opts: WatchOptions): Disposable {
68
+ return Disposable.NULL;
69
+ }
70
+
71
+ async exists(resource: URI): Promise<boolean> {
72
+ try {
73
+ await this.initialized;
74
+ await this.toFileSystemHandle(resource);
75
+ return true;
76
+ } catch (error) {
77
+ return false;
78
+ }
79
+ }
80
+
81
+ async stat(resource: URI): Promise<Stat> {
82
+ try {
83
+ await this.initialized;
84
+
85
+ const handle = await this.toFileSystemHandle(resource);
86
+
87
+ if (handle.kind === 'file') {
88
+ const fileHandle = handle as FileSystemFileHandle;
89
+ const file = await fileHandle.getFile();
90
+ return {
91
+ type: FileType.File,
92
+ ctime: file.lastModified,
93
+ mtime: file.lastModified,
94
+ size: file.size
95
+ };
96
+ } else if (handle.kind === 'directory') {
97
+ return {
98
+ type: FileType.Directory,
99
+ ctime: 0,
100
+ mtime: 0,
101
+ size: 0
102
+ };
103
+ }
104
+
105
+ throw createFileSystemProviderError('Unknown file handle error', FileSystemProviderErrorCode.Unknown);
106
+ } catch (error) {
107
+ throw toFileSystemProviderError(error);
108
+ }
109
+ }
110
+
111
+ async mkdir(resource: URI): Promise<void> {
112
+ await this.initialized;
113
+ try {
114
+ await this.toFileSystemHandle(resource, { create: true, isDirectory: true });
115
+ this.onDidChangeFileEmitter.fire([{ resource, type: FileChangeType.ADDED }]);
116
+ } catch (error) {
117
+ throw toFileSystemProviderError(error, true);
118
+ }
119
+ }
120
+
121
+ async readdir(resource: URI): Promise<[string, FileType][]> {
122
+ await this.initialized;
123
+
124
+ try {
125
+ // Get the directory handle from the directoryHandle
126
+ const directoryHandle = await this.toFileSystemHandle(resource, { create: false, isDirectory: true }) as FileSystemDirectoryHandle;
127
+
128
+ const result: [string, FileType][] = [];
129
+
130
+ // Iterate through the entries in the directory (files and subdirectories)
131
+ for await (const [name, handle] of directoryHandle.entries()) {
132
+ // Determine the type of the entry (file or directory)
133
+ if (handle.kind === 'file') {
134
+ result.push([name, FileType.File]);
135
+ } else if (handle.kind === 'directory') {
136
+ result.push([name, FileType.Directory]);
137
+ }
138
+ }
139
+
140
+ return result;
141
+ } catch (error) {
142
+ throw toFileSystemProviderError(error, true);
143
+ }
144
+ }
145
+
146
+ async delete(resource: URI, _opts: FileDeleteOptions): Promise<void> {
147
+ await this.initialized;
148
+ try {
149
+ const parentURI = resource.parent;
150
+ const parentHandle = await this.toFileSystemHandle(parentURI, { create: false, isDirectory: true });
151
+ if (parentHandle.kind !== 'directory') {
152
+ throw createFileSystemProviderError(new Error('Parent is not a directory'), FileSystemProviderErrorCode.FileNotADirectory);
153
+ }
154
+ const name = resource.path.base;
155
+ return (parentHandle as FileSystemDirectoryHandle).removeEntry(name, { recursive: _opts.recursive });
156
+ } catch (error) {
157
+ throw toFileSystemProviderError(error);
158
+ } finally {
159
+ this.onDidChangeFileEmitter.fire([{ resource, type: FileChangeType.DELETED }]);
160
+ }
161
+ }
162
+
163
+ async rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise<void> {
164
+ await this.initialized;
165
+
166
+ try {
167
+ const fromHandle = await this.toFileSystemHandle(from);
168
+ // Check whether the source is a file or directory
169
+ if (fromHandle.kind === 'directory') {
170
+ // Create the new directory and get the handle
171
+ await this.mkdir(to);
172
+ const toHandle = await this.toFileSystemHandle(to) as FileSystemDirectoryHandle;
173
+ await copyDirectoryContents(fromHandle as FileSystemDirectoryHandle, toHandle);
174
+
175
+ // Delete the old directory
176
+ await this.delete(from, { recursive: true, useTrash: false });
177
+ } else {
178
+ const content = await this.readFile(from);
179
+ await this.writeFile(to, content, { create: true, overwrite: opts.overwrite });
180
+ await this.delete(from, { recursive: true, useTrash: false });
181
+ }
182
+
183
+ this.onDidChangeFileEmitter.fire([{ resource: to, type: FileChangeType.ADDED }]);
184
+ } catch (error) {
185
+ throw toFileSystemProviderError(error);
186
+ }
187
+ }
188
+
189
+ async readFile(resource: URI): Promise<Uint8Array> {
190
+ await this.initialized;
191
+
192
+ try {
193
+ // Get the file handle from the directoryHandle
194
+ const fileHandle = await this.toFileSystemHandle(resource, { create: false, isDirectory: false }) as FileSystemFileHandle;
195
+
196
+ // Get the file itself (which includes the content)
197
+ const file = await fileHandle.getFile();
198
+
199
+ // Read the file as an ArrayBuffer and convert it to Uint8Array
200
+ const arrayBuffer = await file.arrayBuffer();
201
+ return new Uint8Array(arrayBuffer);
202
+ } catch (error) {
203
+ throw toFileSystemProviderError(error, false);
204
+ }
205
+ }
206
+
207
+ async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
208
+ await this.initialized;
209
+ let writeableHandle: FileSystemWritableFileStream | undefined = undefined;
210
+ try {
211
+ // Validate target unless { create: true, overwrite: true }
212
+ if (!opts.create || !opts.overwrite) {
213
+ const fileExists = await this.stat(resource).then(() => true, () => false);
214
+ if (fileExists) {
215
+ if (!opts.overwrite) {
216
+ throw createFileSystemProviderError('File already exists', FileSystemProviderErrorCode.FileExists);
217
+ }
218
+ } else {
219
+ if (!opts.create) {
220
+ throw createFileSystemProviderError('File does not exist', FileSystemProviderErrorCode.FileNotFound);
221
+ }
222
+ }
223
+ }
224
+
225
+ const handle = await this.toFileSystemHandle(resource, { create: true, isDirectory: false }) as FileSystemFileHandle;
226
+
227
+ // Open
228
+ writeableHandle = await handle?.createWritable();
229
+
230
+ // Write content at once
231
+ await writeableHandle?.write(content);
232
+
233
+ this.onDidChangeFileEmitter.fire([{ resource: resource, type: FileChangeType.UPDATED }]);
234
+ } catch (error) {
235
+ throw toFileSystemProviderError(error, false);
236
+ } finally {
237
+ if (typeof writeableHandle !== 'undefined') {
238
+ await writeableHandle.close();
239
+ }
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Returns the FileSystemHandle for the given resource given by a URI.
245
+ * @param resource URI/path of the resource
246
+ * @param options Options for the creation of the handle while traversing the path
247
+ * @returns FileSystemHandle for the given resource
248
+ */
249
+ private async toFileSystemHandle(resource: URI, options?: CreateFileSystemHandleOptions): Promise<FileSystemHandle> {
250
+ const pathParts = resource.path.toString().split(Path.separator).filter(Boolean);
251
+
252
+ return recursiveFileSystemHandle(this.directoryHandle, pathParts, options);
253
+ }
254
+ }
255
+
256
+ // #region Helper functions
257
+ async function recursiveFileSystemHandle(handle: FileSystemHandle, pathParts: string[], options?: CreateFileSystemHandleOptions): Promise<FileSystemHandle> {
258
+ // We reached the end of the path, this happens only when not creating
259
+ if (pathParts.length === 0) {
260
+ return handle;
261
+ }
262
+ // If there are parts left, the handle must be a directory
263
+ if (handle.kind !== 'directory') {
264
+ throw createFileSystemProviderError('Not a directory', FileSystemProviderErrorCode.FileNotADirectory);
265
+ }
266
+ const dirHandle = handle as FileSystemDirectoryHandle;
267
+ // We need to create it and thus we need to stop early to create the file or directory
268
+ if (pathParts.length === 1 && options?.create) {
269
+ if (options?.isDirectory) {
270
+ return dirHandle.getDirectoryHandle(pathParts[0], { create: options.create });
271
+ } else {
272
+ return dirHandle.getFileHandle(pathParts[0], { create: options.create });
273
+ }
274
+ }
275
+
276
+ // Continue to resolve the path
277
+ const part = pathParts.shift()!;
278
+ for await (const entry of dirHandle.entries()) {
279
+ // Check the entry name in the current directory
280
+ if (entry[0] === part) {
281
+ return recursiveFileSystemHandle(entry[1], pathParts, options);
282
+ }
283
+ }
284
+
285
+ // If we haven't found the part, we need to create it along the way
286
+ if (options?.create) {
287
+ const newHandle = await dirHandle.getDirectoryHandle(part, { create: true });
288
+ return recursiveFileSystemHandle(newHandle, pathParts, options);
289
+ }
290
+
291
+ throw createFileSystemProviderError('File not found', FileSystemProviderErrorCode.FileNotFound);
292
+ }
293
+
294
+ // Function to copy directory contents recursively
295
+ async function copyDirectoryContents(sourceHandle: FileSystemDirectoryHandle, destinationHandle: FileSystemDirectoryHandle): Promise<void> {
296
+ for await (const [name, handle] of sourceHandle.entries()) {
297
+ if (handle.kind === 'file') {
298
+ const file = await (handle as FileSystemFileHandle).getFile();
299
+ const newFileHandle = await destinationHandle.getFileHandle(name, { create: true });
300
+ const writable = await newFileHandle.createWritable();
301
+ try {
302
+ await writable.write(await file.arrayBuffer());
303
+ } finally {
304
+ await writable.close();
305
+ }
306
+ } else if (handle.kind === 'directory') {
307
+ const newSubDirHandle = await destinationHandle.getDirectoryHandle(name, { create: true });
308
+ await copyDirectoryContents(handle as FileSystemDirectoryHandle, newSubDirHandle);
309
+ }
310
+ }
311
+ }
312
+
313
+ function toFileSystemProviderError(error: DOMException, is_dir?: boolean): FileSystemProviderError {
314
+ if (error instanceof FileSystemProviderError) {
315
+ return error; // avoid double conversion
316
+ }
317
+
318
+ let code: FileSystemProviderErrorCode;
319
+ switch (error.name) {
320
+ case 'NotFoundError':
321
+ code = FileSystemProviderErrorCode.FileNotFound;
322
+ break;
323
+ case 'InvalidModificationError':
324
+ code = FileSystemProviderErrorCode.FileExists;
325
+ break;
326
+ case 'NotAllowedError':
327
+ code = FileSystemProviderErrorCode.NoPermissions;
328
+ break;
329
+ case 'TypeMismatchError':
330
+ if (!is_dir) {
331
+ code = FileSystemProviderErrorCode.FileIsADirectory;
332
+ } else {
333
+ code = FileSystemProviderErrorCode.FileNotADirectory;
334
+ }
335
+
336
+ break;
337
+ case 'QuotaExceededError':
338
+ code = FileSystemProviderErrorCode.FileTooLarge;
339
+ break;
340
+ default:
341
+ code = FileSystemProviderErrorCode.Unknown;
342
+ }
343
+
344
+ return createFileSystemProviderError(error, code);
345
+ }
346
+ // #endregion
@@ -3,6 +3,7 @@
3
3
  Provides the file download contribution to the `Files` navigator.
4
4
 
5
5
  Supports single and multi file downloads.
6
+
6
7
  1. A single file will be downloaded as is.
7
8
  2. Folders will be downloaded az tar archives.
8
9
  3. When downloading multiple files, the name of the closest common parent directory will be used for the archive.
@@ -10,10 +11,11 @@ Supports single and multi file downloads.
10
11
 
11
12
  ### REST API
12
13
 
13
- - To download a single file or folder use the following endpoint: `GET /files/?uri=/encoded/file/uri/to/the/resource`.
14
- - Example: `curl -X GET http://localhost:3000/files/?uri=file:///Users/akos.kitta/git/theia/package.json`.
14
+ - To download a single file or folder use the following endpoint: `GET /files/?uri=/encoded/file/uri/to/the/resource`.
15
+ - Example: `curl -X GET http://localhost:3000/files/?uri=file:///Users/akos.kitta/git/theia/package.json`.
16
+
17
+ - To download multiple files (from the same folder) use the `PUT /files/` endpoint with the `application/json` content type header and the following body format:
15
18
 
16
- - To download multiple files (from the same folder) use the `PUT /files/` endpoint with the `application/json` content type header and the following body format:
17
19
  ```json
18
20
  {
19
21
  "uri": [
@@ -22,10 +24,12 @@ Supports single and multi file downloads.
22
24
  ]
23
25
  }
24
26
  ```
27
+
25
28
  ```
26
29
  curl -X PUT -H "Content-Type: application/json" -d '{ "uris": ["file:///Users/akos.kitta/git/theia/package.json", "file:///Users/akos.kitta/git/theia/README.md"] }' http://localhost:3000/files/
27
30
  ```
28
31
 
29
32
  ## License
33
+
30
34
  - [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
31
- - [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
35
+ - [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)