mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-06 01:25:38 -05:00
392 lines
13 KiB
TypeScript
392 lines
13 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import { Disposable, IDisposable, toDisposable, combinedDisposable } from 'vs/base/common/lifecycle';
|
|
import { IFileService, IResolveFileOptions, IResourceEncodings, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, IResolveContentOptions, IContent, IStreamContent, ITextSnapshot, IUpdateContentOptions, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat } from 'vs/platform/files/common/files';
|
|
import { URI } from 'vs/base/common/uri';
|
|
import { Event, Emitter } from 'vs/base/common/event';
|
|
import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
|
|
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
|
import { isAbsolutePath, dirname, basename, joinPath, isEqual } from 'vs/base/common/resources';
|
|
import { localize } from 'vs/nls';
|
|
import { TernarySearchTree } from 'vs/base/common/map';
|
|
import { isNonEmptyArray } from 'vs/base/common/arrays';
|
|
import { getBaseLabel } from 'vs/base/common/labels';
|
|
import { ILogService } from 'vs/platform/log/common/log';
|
|
|
|
export class FileService2 extends Disposable implements IFileService {
|
|
|
|
//#region TODO@Ben HACKS
|
|
|
|
private _impl: IFileService;
|
|
|
|
setImpl(service: IFileService): void {
|
|
this._impl = this._register(service);
|
|
|
|
this._register(service.onFileChanges(e => this._onFileChanges.fire(e)));
|
|
this._register(service.onAfterOperation(e => this._onAfterOperation.fire(e)));
|
|
}
|
|
|
|
//#endregion
|
|
|
|
_serviceBrand: ServiceIdentifier<any>;
|
|
|
|
constructor(@ILogService private logService: ILogService) {
|
|
super();
|
|
}
|
|
|
|
//#region File System Provider
|
|
|
|
private _onDidChangeFileSystemProviderRegistrations: Emitter<IFileSystemProviderRegistrationEvent> = this._register(new Emitter<IFileSystemProviderRegistrationEvent>());
|
|
get onDidChangeFileSystemProviderRegistrations(): Event<IFileSystemProviderRegistrationEvent> { return this._onDidChangeFileSystemProviderRegistrations.event; }
|
|
|
|
private _onWillActivateFileSystemProvider: Emitter<IFileSystemProviderActivationEvent> = this._register(new Emitter<IFileSystemProviderActivationEvent>());
|
|
get onWillActivateFileSystemProvider(): Event<IFileSystemProviderActivationEvent> { return this._onWillActivateFileSystemProvider.event; }
|
|
|
|
private readonly provider = new Map<string, IFileSystemProvider>();
|
|
|
|
registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable {
|
|
if (this.provider.has(scheme)) {
|
|
throw new Error(`A provider for the scheme ${scheme} is already registered.`);
|
|
}
|
|
|
|
let legacyDisposal: IDisposable;
|
|
if (this._impl) {
|
|
legacyDisposal = this._impl.registerProvider(scheme, provider);
|
|
} else {
|
|
legacyDisposal = Disposable.None;
|
|
}
|
|
|
|
// Add provider with event
|
|
this.provider.set(scheme, provider);
|
|
this._onDidChangeFileSystemProviderRegistrations.fire({ added: true, scheme, provider });
|
|
|
|
// Forward change events from provider
|
|
const providerFileListener = provider.onDidChangeFile(changes => this._onFileChanges.fire(new FileChangesEvent(changes)));
|
|
|
|
return combinedDisposable([
|
|
toDisposable(() => {
|
|
this._onDidChangeFileSystemProviderRegistrations.fire({ added: false, scheme, provider });
|
|
this.provider.delete(scheme);
|
|
|
|
providerFileListener.dispose();
|
|
}),
|
|
legacyDisposal
|
|
]);
|
|
}
|
|
|
|
async activateProvider(scheme: string): Promise<void> {
|
|
|
|
// Emit an event that we are about to activate a provider with the given scheme.
|
|
// Listeners can participate in the activation by registering a provider for it.
|
|
const joiners: Promise<void>[] = [];
|
|
this._onWillActivateFileSystemProvider.fire({
|
|
scheme,
|
|
join(promise) {
|
|
if (promise) {
|
|
joiners.push(promise);
|
|
}
|
|
},
|
|
});
|
|
|
|
if (this.provider.has(scheme)) {
|
|
return Promise.resolve(); // provider is already here so we can return directly
|
|
}
|
|
|
|
// If the provider is not yet there, make sure to join on the listeners assuming
|
|
// that it takes a bit longer to register the file system provider.
|
|
await Promise.all(joiners);
|
|
}
|
|
|
|
canHandleResource(resource: URI): boolean {
|
|
return this.provider.has(resource.scheme);
|
|
}
|
|
|
|
private async withProvider(resource: URI): Promise<IFileSystemProvider> {
|
|
|
|
// Assert path is absolute
|
|
if (!isAbsolutePath(resource)) {
|
|
throw new FileOperationError(
|
|
localize('invalidPath', "The path of resource '{0}' must be absolute", resource.toString(true)),
|
|
FileOperationResult.FILE_INVALID_PATH
|
|
);
|
|
}
|
|
|
|
// Activate provider
|
|
await this.activateProvider(resource.scheme);
|
|
|
|
// Assert provider
|
|
const provider = this.provider.get(resource.scheme);
|
|
if (!provider) {
|
|
const err = new Error();
|
|
err.name = 'ENOPRO';
|
|
err.message = `no provider for ${resource.toString()}`;
|
|
|
|
return Promise.reject(err);
|
|
}
|
|
|
|
return provider;
|
|
}
|
|
|
|
//#endregion
|
|
|
|
private _onAfterOperation: Emitter<FileOperationEvent> = this._register(new Emitter<FileOperationEvent>());
|
|
get onAfterOperation(): Event<FileOperationEvent> { return this._onAfterOperation.event; }
|
|
|
|
//#region File Metadata Resolving
|
|
|
|
async resolveFile(resource: URI, options?: IResolveFileOptions): Promise<IFileStat> {
|
|
try {
|
|
return await this.doResolveFile(resource, options);
|
|
} catch (error) {
|
|
|
|
// Specially handle file not found case as file operation result
|
|
if (toFileSystemProviderErrorCode(error) === FileSystemProviderErrorCode.FileNotFound) {
|
|
throw new FileOperationError(
|
|
localize('fileNotFoundError', "File not found ({0})", resource.toString(true)),
|
|
FileOperationResult.FILE_NOT_FOUND
|
|
);
|
|
}
|
|
|
|
// Bubble up any other error as is
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private async doResolveFile(resource: URI, options?: IResolveFileOptions): Promise<IFileStat> {
|
|
const provider = await this.withProvider(resource);
|
|
|
|
// leverage a trie to check for recursive resolving
|
|
const to = options && options.resolveTo;
|
|
const trie = TernarySearchTree.forPaths<true>();
|
|
trie.set(resource.toString(), true);
|
|
if (isNonEmptyArray(to)) {
|
|
to.forEach(uri => trie.set(uri.toString(), true));
|
|
}
|
|
|
|
const stat = await provider.stat(resource);
|
|
|
|
return await this.toFileStat(provider, resource, stat, undefined, (stat, siblings) => {
|
|
|
|
// check for recursive resolving
|
|
if (Boolean(trie.findSuperstr(stat.resource.toString()) || trie.get(stat.resource.toString()))) {
|
|
return true;
|
|
}
|
|
|
|
// check for resolving single child folders
|
|
if (stat.isDirectory && options && options.resolveSingleChildDescendants) {
|
|
return siblings === 1;
|
|
}
|
|
|
|
return false;
|
|
});
|
|
}
|
|
|
|
private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat, siblings: number | undefined, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise<IFileStat> {
|
|
|
|
// convert to file stat
|
|
const fileStat: IFileStat = {
|
|
resource,
|
|
name: getBaseLabel(resource),
|
|
isDirectory: (stat.type & FileType.Directory) !== 0,
|
|
isSymbolicLink: (stat.type & FileType.SymbolicLink) !== 0,
|
|
isReadonly: !!(provider.capabilities & FileSystemProviderCapabilities.Readonly),
|
|
mtime: stat.mtime,
|
|
size: stat.size,
|
|
etag: stat.mtime.toString(29) + stat.size.toString(31),
|
|
};
|
|
|
|
// check to recurse for directories
|
|
if (fileStat.isDirectory && recurse(fileStat, siblings)) {
|
|
try {
|
|
const entries = await provider.readdir(resource);
|
|
|
|
fileStat.children = await Promise.all(entries.map(async entry => {
|
|
const childResource = joinPath(resource, entry[0]);
|
|
const childStat = await provider.stat(childResource);
|
|
|
|
return this.toFileStat(provider, childResource, childStat, entries.length, recurse);
|
|
}));
|
|
} catch (error) {
|
|
this.logService.trace(error);
|
|
|
|
fileStat.children = []; // gracefully handle errors, we may not have permissions to read
|
|
}
|
|
|
|
return fileStat;
|
|
}
|
|
|
|
return Promise.resolve(fileStat);
|
|
}
|
|
|
|
async resolveFiles(toResolve: { resource: URI; options?: IResolveFileOptions; }[]): Promise<IResolveFileResult[]> {
|
|
|
|
// soft-groupBy, keep order, don't rearrange/merge groups
|
|
const groups: Array<typeof toResolve> = [];
|
|
let group: typeof toResolve | undefined;
|
|
for (const request of toResolve) {
|
|
if (!group || group[0].resource.scheme !== request.resource.scheme) {
|
|
group = [];
|
|
groups.push(group);
|
|
}
|
|
|
|
group.push(request);
|
|
}
|
|
|
|
// resolve files
|
|
const result: IResolveFileResult[] = [];
|
|
for (const group of groups) {
|
|
for (const groupEntry of group) {
|
|
try {
|
|
const stat = await this.doResolveFile(groupEntry.resource, groupEntry.options);
|
|
result.push({ stat, success: true });
|
|
} catch (error) {
|
|
this.logService.trace(error);
|
|
|
|
result.push({ stat: undefined, success: false });
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
async existsFile(resource: URI): Promise<boolean> {
|
|
try {
|
|
await this.resolveFile(resource);
|
|
|
|
return true;
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
//#endregion
|
|
|
|
//#region File Reading/Writing
|
|
|
|
get encoding(): IResourceEncodings { return this._impl.encoding; }
|
|
|
|
createFile(resource: URI, content?: string, options?: ICreateFileOptions): Promise<IFileStat> {
|
|
return this._impl.createFile(resource, content, options);
|
|
}
|
|
|
|
resolveContent(resource: URI, options?: IResolveContentOptions): Promise<IContent> {
|
|
return this._impl.resolveContent(resource, options);
|
|
}
|
|
|
|
resolveStreamContent(resource: URI, options?: IResolveContentOptions): Promise<IStreamContent> {
|
|
return this._impl.resolveStreamContent(resource, options);
|
|
}
|
|
|
|
updateContent(resource: URI, value: string | ITextSnapshot, options?: IUpdateContentOptions): Promise<IFileStat> {
|
|
return this._impl.updateContent(resource, value, options);
|
|
}
|
|
|
|
//#endregion
|
|
|
|
//#region Move/Copy/Delete/Create Folder
|
|
|
|
moveFile(source: URI, target: URI, overwrite?: boolean): Promise<IFileStat> {
|
|
return this._impl.moveFile(source, target, overwrite);
|
|
}
|
|
|
|
copyFile(source: URI, target: URI, overwrite?: boolean): Promise<IFileStat> {
|
|
return this._impl.copyFile(source, target, overwrite);
|
|
}
|
|
|
|
async createFolder(resource: URI): Promise<IFileStat> {
|
|
const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource));
|
|
|
|
// mkdir recursively
|
|
await this.mkdirp(provider, resource);
|
|
|
|
// events
|
|
const fileStat = await this.resolveFile(resource);
|
|
this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, fileStat));
|
|
|
|
return fileStat;
|
|
}
|
|
|
|
private async mkdirp(provider: IFileSystemProvider, directory: URI): Promise<void> {
|
|
const directoriesToCreate: string[] = [];
|
|
|
|
// mkdir until we reach root
|
|
while (!isEqual(directory, dirname(directory))) {
|
|
try {
|
|
const stat = await provider.stat(directory);
|
|
if ((stat.type & FileType.Directory) === 0) {
|
|
throw new Error(`${directory.toString()} exists, but is not a directory`);
|
|
}
|
|
|
|
break; // we have hit a directory that exists -> good
|
|
} catch (error) {
|
|
|
|
// Bubble up any other error that is not file not found
|
|
if (toFileSystemProviderErrorCode(error) !== FileSystemProviderErrorCode.FileNotFound) {
|
|
throw error;
|
|
}
|
|
|
|
// Upon error, remember directories that need to be created
|
|
directoriesToCreate.push(basename(directory));
|
|
|
|
// Continue up
|
|
directory = dirname(directory);
|
|
}
|
|
}
|
|
|
|
// Create directories as needed
|
|
for (let i = directoriesToCreate.length - 1; i >= 0; i--) {
|
|
directory = joinPath(directory, directoriesToCreate[i]);
|
|
await provider.mkdir(directory);
|
|
}
|
|
}
|
|
|
|
async del(resource: URI, options?: { useTrash?: boolean; recursive?: boolean; }): Promise<void> {
|
|
if (options && options.useTrash) {
|
|
return this._impl.del(resource, options); //TODO@ben this is https://github.com/Microsoft/vscode/issues/48259
|
|
}
|
|
|
|
const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource));
|
|
|
|
// Delete through provider
|
|
await provider.delete(resource, { recursive: !!(options && options.recursive) });
|
|
|
|
// Events
|
|
this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE));
|
|
}
|
|
|
|
//#endregion
|
|
|
|
//#region File Watching
|
|
|
|
private _onFileChanges: Emitter<FileChangesEvent> = this._register(new Emitter<FileChangesEvent>());
|
|
get onFileChanges(): Event<FileChangesEvent> { return this._onFileChanges.event; }
|
|
|
|
watchFileChanges(resource: URI): void {
|
|
this._impl.watchFileChanges(resource);
|
|
}
|
|
|
|
unwatchFileChanges(resource: URI): void {
|
|
this._impl.unwatchFileChanges(resource);
|
|
}
|
|
|
|
//#endregion
|
|
|
|
//#region Helpers
|
|
|
|
private throwIfFileSystemIsReadonly(provider: IFileSystemProvider): IFileSystemProvider {
|
|
if (provider.capabilities & FileSystemProviderCapabilities.Readonly) {
|
|
throw new FileOperationError(localize('err.readonly', "Resource can not be modified."), FileOperationResult.FILE_PERMISSION_DENIED);
|
|
}
|
|
|
|
return provider;
|
|
}
|
|
|
|
//#endregion
|
|
}
|
|
|
|
registerSingleton(IFileService, FileService2);
|