From eeaf5d876e20884b05932988be04a8089297d07e Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 21 Mar 2019 16:59:04 +0100 Subject: [PATCH] files2 - implement copyFile support (same providers, copy support) --- src/vs/platform/files/common/files.ts | 2 - .../test/electron-browser/fileService.test.ts | 87 ------------------ .../services/files2/common/fileService2.ts | 92 +++++++++++-------- .../files2/node/diskFileSystemProvider.ts | 28 ++---- .../files2/test/node/diskFileService.test.ts | 83 ++++++++++++++++- 5 files changed, 147 insertions(+), 145 deletions(-) diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index e26c5f2913b..e9389a0dbc2 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -1101,8 +1101,6 @@ export interface ILegacyFileService { updateContent(resource: URI, value: string | ITextSnapshot, options?: IUpdateContentOptions): Promise; - copyFile(source: URI, target: URI, overwrite?: boolean): Promise; - createFile(resource: URI, content?: string, options?: ICreateFileOptions): Promise; del(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise; diff --git a/src/vs/workbench/services/files/test/electron-browser/fileService.test.ts b/src/vs/workbench/services/files/test/electron-browser/fileService.test.ts index 8f8f5c2930e..924afc8361e 100644 --- a/src/vs/workbench/services/files/test/electron-browser/fileService.test.ts +++ b/src/vs/workbench/services/files/test/electron-browser/fileService.test.ts @@ -98,93 +98,6 @@ suite('FileService', () => { }); }); - test('copyFile', () => { - let event: FileOperationEvent; - const toDispose = service.onAfterOperation(e => { - event = e; - }); - - return service.resolveFile(uri.file(path.join(testDir, 'index.html'))).then(source => { - const resource = uri.file(path.join(testDir, 'other.html')); - return service.copyFile(source.resource, resource).then(copied => { - assert.equal(fs.existsSync(copied.resource.fsPath), true); - assert.equal(fs.existsSync(source.resource.fsPath), true); - - assert.ok(event); - assert.equal(event.resource.fsPath, source.resource.fsPath); - assert.equal(event.operation, FileOperation.COPY); - assert.equal(event.target!.resource.fsPath, copied.resource.fsPath); - toDispose.dispose(); - }); - }); - }); - - test('copyFile - overwrite folder with file', function () { - let createEvent: FileOperationEvent; - let copyEvent: FileOperationEvent; - let deleteEvent: FileOperationEvent; - const toDispose = service.onAfterOperation(e => { - if (e.operation === FileOperation.CREATE) { - createEvent = e; - } else if (e.operation === FileOperation.DELETE) { - deleteEvent = e; - } else if (e.operation === FileOperation.COPY) { - copyEvent = e; - } - }); - - return service.resolveFile(uri.file(testDir)).then(parent => { - const folderResource = uri.file(path.join(parent.resource.fsPath, 'conway.js')); - return service.createFolder(folderResource).then(f => { - const resource = uri.file(path.join(testDir, 'deep', 'conway.js')); - return service.copyFile(resource, f.resource, true).then(copied => { - assert.equal(fs.existsSync(copied.resource.fsPath), true); - assert.ok(fs.statSync(copied.resource.fsPath).isFile); - - assert.ok(createEvent); - assert.ok(deleteEvent); - assert.ok(copyEvent); - - assert.equal(copyEvent.resource.fsPath, resource.fsPath); - assert.equal(copyEvent.target!.resource.fsPath, copied.resource.fsPath); - - assert.equal(deleteEvent.resource.fsPath, folderResource.fsPath); - - toDispose.dispose(); - }); - }); - }); - }); - - test('copyFile - MIX CASE', function () { - return service.resolveFile(uri.file(path.join(testDir, 'index.html'))).then(source => { - return service.moveFile(source.resource, uri.file(path.join(path.dirname(source.resource.fsPath), 'CONWAY.js'))).then(renamed => { - assert.equal(fs.existsSync(renamed.resource.fsPath), true); - assert.ok(fs.readdirSync(testDir).some(f => f === 'CONWAY.js')); - - return service.resolveFile(uri.file(path.join(testDir, 'deep', 'conway.js'))).then(source => { - const targetParent = uri.file(testDir); - const target = targetParent.with({ path: path.posix.join(targetParent.path, path.posix.basename(source.resource.path)) }); - - return service.copyFile(source.resource, target, true).then(res => { // CONWAY.js => conway.js - assert.equal(fs.existsSync(res.resource.fsPath), true); - assert.ok(fs.readdirSync(testDir).some(f => f === 'conway.js')); - }); - }); - }); - }); - }); - - test('copyFile - same file', function () { - return service.resolveFile(uri.file(path.join(testDir, 'index.html'))).then(source => { - const targetParent = uri.file(path.dirname(source.resource.fsPath)); - const target = targetParent.with({ path: path.posix.join(targetParent.path, path.posix.basename(source.resource.path)) }); - return service.copyFile(source.resource, target, true).then(copied => { - assert.equal(copied.size, source.size); - }); - }); - }); - test('updateContent', () => { const resource = uri.file(path.join(testDir, 'small.txt')); diff --git a/src/vs/workbench/services/files2/common/fileService2.ts b/src/vs/workbench/services/files2/common/fileService2.ts index c5edb918250..11cb2aed62f 100644 --- a/src/vs/workbench/services/files2/common/fileService2.ts +++ b/src/vs/workbench/services/files2/common/fileService2.ts @@ -108,10 +108,7 @@ export class FileService2 extends Disposable implements IFileService { // 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 - ); + throw new FileOperationError(localize('invalidPath', "The path of resource '{0}' must be absolute", resource.toString(true)), FileOperationResult.FILE_INVALID_PATH); } // Activate provider @@ -308,30 +305,68 @@ export class FileService2 extends Disposable implements IFileService { //#region Move/Copy/Delete/Create Folder moveFile(source: URI, target: URI, overwrite?: boolean): Promise { - if (source.scheme !== target.scheme) { - return this.doMoveAcrossScheme(source, target); + if (source.scheme === target.scheme) { + return this.doMoveCopyWithSameProvider(source, target, false /* just move */, overwrite); } - return this.doMoveWithInScheme(source, target, overwrite); + return this.doMoveWithDifferentProvider(source, target); } - private async doMoveWithInScheme(source: URI, target: URI, overwrite: boolean = false): Promise { + private async doMoveWithDifferentProvider(source: URI, target: URI, overwrite?: boolean): Promise { + + // copy file source => target + await this.copyFile(source, target, overwrite); + + // delete source + await this.del(source, { recursive: true }); + + // resolve and send events + const fileStat = await this.resolveFile(target, { resolveMetadata: true }); + this._onAfterOperation.fire(new FileOperationEvent(source, FileOperation.MOVE, fileStat)); + + return fileStat; + } + + async copyFile(source: URI, target: URI, overwrite?: boolean): Promise { + if (source.scheme === target.scheme) { + return this.doCopyWithSameProvider(source, target, overwrite); + } + + return this.doCopyWithDifferentProvider(source, target); + } + + private async doCopyWithSameProvider(source: URI, target: URI, overwrite: boolean = false): Promise { + const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(source)); + + // check if provider supports fast file/folder copy + if (provider.capabilities & FileSystemProviderCapabilities.FileFolderCopy && typeof provider.copy === 'function') { + return this.doMoveCopyWithSameProvider(source, target, true /* keep copy */, overwrite); + } + + return this._impl.copyFile(source, target, overwrite); // TODO@ben implement properly + } + + private async doCopyWithDifferentProvider(source: URI, target: URI, overwrite?: boolean): Promise { + return this._impl.copyFile(source, target, overwrite); // TODO@ben implement properly + } + + private async doMoveCopyWithSameProvider(source: URI, target: URI, keepCopy: boolean, overwrite?: boolean): Promise { const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(source)); // validation const isPathCaseSensitive = !!(provider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive); - const isCaseRename = isPathCaseSensitive ? false : isEqual(source, target, true /* ignore case */); - if (!isCaseRename && isEqualOrParent(target, source, !isPathCaseSensitive)) { - return Promise.reject(new Error(localize('unableToMoveError1', "Unable to move when source path is equal or parent of target path"))); + const isCaseChange = isPathCaseSensitive ? false : isEqual(source, target, true /* ignore case */); + if (!isCaseChange && isEqualOrParent(target, source, !isPathCaseSensitive)) { + return Promise.reject(new Error(localize('unableToMoveCopyError1', "Unable to move/copy when source path is equal or parent of target path"))); } - // delete target if we are told to overwrite and this is not a case rename - if (!isCaseRename && overwrite && await this.existsFile(target)) { + // delete target if we are told to overwrite and this is not a case change + if (!isCaseChange && overwrite && await this.existsFile(target)) { // Special case: if the target is a parent of the source, we cannot delete // it as it would delete the source as well. In this case we have to throw if (isEqualOrParent(source, target, !isPathCaseSensitive)) { - return Promise.reject(new Error(localize('unableToMoveError2', "Unable to move/copy. File would replace folder it is contained in."))); + return Promise.reject(new Error(localize('unableToMoveCopyError2', "Unable to move/copy. File would replace folder it is contained in."))); } try { @@ -344,9 +379,13 @@ export class FileService2 extends Disposable implements IFileService { // create parent folders await this.mkdirp(provider, dirname(target)); - // rename source => target + // rename/copy source => target try { - await provider.rename(source, target, { overwrite }); + if (keepCopy) { + await provider.copy!(source, target, { overwrite: !!overwrite }); + } else { + await provider.rename(source, target, { overwrite: !!overwrite }); + } } catch (error) { if (toFileSystemProviderErrorCode(error) === FileSystemProviderErrorCode.FileExists) { throw new FileOperationError(localize('unableToMoveError3', "Unable to move/copy. File already exists at destination."), FileOperationResult.FILE_MOVE_CONFLICT); @@ -357,30 +396,11 @@ export class FileService2 extends Disposable implements IFileService { // resolve and send events const fileStat = await this.resolveFile(target, { resolveMetadata: true }); - this._onAfterOperation.fire(new FileOperationEvent(source, FileOperation.MOVE, fileStat)); - - return fileStat; - } - - private async doMoveAcrossScheme(source: URI, target: URI, overwrite?: boolean): Promise { - - // copy file source => target - await this.copyFile(source, target, overwrite); - - // delete source - await this.del(source, { recursive: true }); - - // resolve and send events - const fileStat = await this.resolveFile(target, { resolveMetadata: true }); - this._onAfterOperation.fire(new FileOperationEvent(source, FileOperation.MOVE, fileStat)); + this._onAfterOperation.fire(new FileOperationEvent(source, keepCopy ? FileOperation.COPY : FileOperation.MOVE, fileStat)); return fileStat; } - copyFile(source: URI, target: URI, overwrite?: boolean): Promise { - return this._impl.copyFile(source, target, overwrite); - } - async createFolder(resource: URI): Promise { const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource)); diff --git a/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts b/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts index 22c23dc2e61..bc7a5fbd2bc 100644 --- a/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts +++ b/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts @@ -11,10 +11,9 @@ import { IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, IWatc import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { isLinux } from 'vs/base/common/platform'; -import { statLink, readdir, unlink, del, fileExists, move } from 'vs/base/node/pfs'; +import { statLink, readdir, unlink, del, move, copy } from 'vs/base/node/pfs'; import { normalize } from 'vs/base/common/path'; import { joinPath } from 'vs/base/common/resources'; -import { isEqual } from 'vs/base/common/extpath'; export class DiskFileSystemProvider extends Disposable implements IFileSystemProvider { @@ -138,28 +137,21 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro const fromFilePath = this.toFilePath(from); const toFilePath = this.toFilePath(to); - const isPathCaseSensitive = !!(this.capabilities & FileSystemProviderCapabilities.PathCaseSensitive); - const isCaseRename = isPathCaseSensitive ? false : isEqual(fromFilePath, toFilePath, true /* ignore case */); - - // handle existing target (unless this is a case rename) - if (!isCaseRename && await fileExists(toFilePath)) { - if (!opts.overwrite) { - throw createFileSystemProviderError(new Error('File at rename target already exists'), FileSystemProviderErrorCode.FileExists); - } - - await this.delete(to, { recursive: true }); - } - - // move await move(fromFilePath, toFilePath); } catch (error) { throw this.toFileSystemProviderError(error); } } - copy?(from: URI, to: URI, opts: FileOverwriteOptions): Promise { - // TODO use new fs.copy method? - throw new Error('Method not implemented.'); + async copy(from: URI, to: URI, opts: FileOverwriteOptions): Promise { + try { + const fromFilePath = this.toFilePath(from); + const toFilePath = this.toFilePath(to); + + return copy(fromFilePath, toFilePath); + } catch (error) { + throw this.toFileSystemProviderError(error); + } } //#endregion diff --git a/src/vs/workbench/services/files2/test/node/diskFileService.test.ts b/src/vs/workbench/services/files2/test/node/diskFileService.test.ts index d43da3b6349..e17bc6c1c9c 100644 --- a/src/vs/workbench/services/files2/test/node/diskFileService.test.ts +++ b/src/vs/workbench/services/files2/test/node/diskFileService.test.ts @@ -10,11 +10,11 @@ import { Schemas } from 'vs/base/common/network'; import { DiskFileSystemProvider } from 'vs/workbench/services/files2/node/diskFileSystemProvider'; import { getRandomTestPath } from 'vs/base/test/node/testUtils'; import { generateUuid } from 'vs/base/common/uuid'; -import { join, basename, dirname } from 'vs/base/common/path'; +import { join, basename, dirname, posix } from 'vs/base/common/path'; import { getPathFromAmdModule } from 'vs/base/common/amd'; import { copy, del } from 'vs/base/node/pfs'; import { URI } from 'vs/base/common/uri'; -import { existsSync, statSync } from 'fs'; +import { existsSync, statSync, readdirSync } from 'fs'; import { FileOperation, FileOperationEvent, IFileStat, FileOperationResult } from 'vs/platform/files/common/files'; import { FileService } from 'vs/workbench/services/files/node/fileService'; import { TestContextService, TestEnvironmentService, TestTextResourceConfigurationService, TestLifecycleService, TestStorageService } from 'vs/workbench/test/workbenchTestServices'; @@ -552,4 +552,83 @@ suite('Disk File Service', () => { toDispose.dispose(); }); + + test('copyFile', async () => { + let event: FileOperationEvent; + const toDispose = service.onAfterOperation(e => { + event = e; + }); + + const source = await service.resolveFile(URI.file(join(testDir, 'index.html'))); + const resource = URI.file(join(testDir, 'other.html')); + + const copied = await service.copyFile(source.resource, resource); + + assert.equal(existsSync(copied.resource.fsPath), true); + assert.equal(existsSync(source.resource.fsPath), true); + assert.ok(event!); + assert.equal(event!.resource.fsPath, source.resource.fsPath); + assert.equal(event!.operation, FileOperation.COPY); + assert.equal(event!.target!.resource.fsPath, copied.resource.fsPath); + toDispose.dispose(); + }); + + test('copyFile - overwrite folder with file', async () => { + let createEvent: FileOperationEvent; + let copyEvent: FileOperationEvent; + let deleteEvent: FileOperationEvent; + const toDispose = service.onAfterOperation(e => { + if (e.operation === FileOperation.CREATE) { + createEvent = e; + } else if (e.operation === FileOperation.DELETE) { + deleteEvent = e; + } else if (e.operation === FileOperation.COPY) { + copyEvent = e; + } + }); + + const parent = await service.resolveFile(URI.file(testDir)); + const folderResource = URI.file(join(parent.resource.fsPath, 'conway.js')); + const f = await service.createFolder(folderResource); + const resource = URI.file(join(testDir, 'deep', 'conway.js')); + + const copied = await service.copyFile(resource, f.resource, true); + + assert.equal(existsSync(copied.resource.fsPath), true); + assert.ok(statSync(copied.resource.fsPath).isFile); + assert.ok(createEvent!); + assert.ok(deleteEvent!); + assert.ok(copyEvent!); + assert.equal(copyEvent!.resource.fsPath, resource.fsPath); + assert.equal(copyEvent!.target!.resource.fsPath, copied.resource.fsPath); + assert.equal(deleteEvent!.resource.fsPath, folderResource.fsPath); + + toDispose.dispose(); + }); + + test('copyFile - MIX CASE', async () => { + const source = await service.resolveFile(URI.file(join(testDir, 'index.html'))); + const renamed = await service.moveFile(source.resource, URI.file(join(dirname(source.resource.fsPath), 'CONWAY.js'))); + assert.equal(existsSync(renamed.resource.fsPath), true); + assert.ok(readdirSync(testDir).some(f => f === 'CONWAY.js')); + const source_1 = await service.resolveFile(URI.file(join(testDir, 'deep', 'conway.js'))); + const targetParent = URI.file(testDir); + const target = targetParent.with({ path: posix.join(targetParent.path, posix.basename(source_1.resource.path)) }); + + const res = await service.copyFile(source_1.resource, target, true); + assert.equal(existsSync(res.resource.fsPath), true); + assert.ok(readdirSync(testDir).some(f => f === 'conway.js')); + }); + + test('copyFile - same file should throw', async () => { + const source = await service.resolveFile(URI.file(join(testDir, 'index.html'))); + const targetParent = URI.file(dirname(source.resource.fsPath)); + const target = targetParent.with({ path: posix.join(targetParent.path, posix.basename(source.resource.path)) }); + + try { + await service.copyFile(source.resource, target, true); + } catch (error) { + assert.ok(error); + } + }); }); \ No newline at end of file -- GitLab