提交 eeaf5d87 编写于 作者: B Benjamin Pasero

files2 - implement copyFile support (same providers, copy support)

上级 fcdb3e97
......@@ -1101,8 +1101,6 @@ export interface ILegacyFileService {
updateContent(resource: URI, value: string | ITextSnapshot, options?: IUpdateContentOptions): Promise<IFileStat>;
copyFile(source: URI, target: URI, overwrite?: boolean): Promise<IFileStat>;
createFile(resource: URI, content?: string, options?: ICreateFileOptions): Promise<IFileStat>;
del(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise<void>;
......
......@@ -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'));
......
......@@ -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<IFileStatWithMetadata> {
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<IFileStatWithMetadata> {
private async doMoveWithDifferentProvider(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata> {
// 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<IFileStatWithMetadata> {
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<IFileStatWithMetadata> {
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<IFileStatWithMetadata> {
return this._impl.copyFile(source, target, overwrite); // TODO@ben implement properly
}
private async doMoveCopyWithSameProvider(source: URI, target: URI, keepCopy: boolean, overwrite?: boolean): Promise<IFileStatWithMetadata> {
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<IFileStatWithMetadata> {
// 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<IFileStatWithMetadata> {
return this._impl.copyFile(source, target, overwrite);
}
async createFolder(resource: URI): Promise<IFileStatWithMetadata> {
const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource));
......
......@@ -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<void> {
// TODO use new fs.copy method?
throw new Error('Method not implemented.');
async copy(from: URI, to: URI, opts: FileOverwriteOptions): Promise<void> {
try {
const fromFilePath = this.toFilePath(from);
const toFilePath = this.toFilePath(to);
return copy(fromFilePath, toFilePath);
} catch (error) {
throw this.toFileSystemProviderError(error);
}
}
//#endregion
......
......@@ -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
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册