Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/vs/base/common/buffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ export function binaryIndexOf(haystack: Uint8Array, needle: Uint8Array, offset =
}

if (needleLen === 1) {
return haystack.indexOf(needle[0]);
return haystack.indexOf(needle[0], offset);
Comment thread
connor4312 marked this conversation as resolved.
}

if (needleLen > haystackLen - offset) {
Expand Down
2 changes: 2 additions & 0 deletions src/vs/base/test/common/buffer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,10 +437,12 @@ suite('Buffer', () => {

assert.strictEqual(haystack.indexOf(VSBuffer.fromString('a')), 0);
assert.strictEqual(haystack.indexOf(VSBuffer.fromString('c')), 2);
assert.strictEqual(haystack.indexOf(VSBuffer.fromString('c'), 4), 7);

assert.strictEqual(haystack.indexOf(VSBuffer.fromString('abcaa')), 0);
assert.strictEqual(haystack.indexOf(VSBuffer.fromString('caaab')), 8);
assert.strictEqual(haystack.indexOf(VSBuffer.fromString('ccc')), 15);
assert.strictEqual(haystack.indexOf(VSBuffer.fromString('cc'), 9), 15);

assert.strictEqual(haystack.indexOf(VSBuffer.fromString('cccb')), -1);
});
Expand Down
14 changes: 13 additions & 1 deletion src/vs/platform/files/browser/indexedDBFileSystemProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ export class IndexedDBFileSystemProvider extends Disposable implements IFileSyst

readonly capabilities: FileSystemProviderCapabilities =
FileSystemProviderCapabilities.FileReadWrite
| FileSystemProviderCapabilities.FileAppend
| FileSystemProviderCapabilities.PathCaseSensitive;
readonly onDidChangeCapabilities: Event<void> = Event.None;

Expand Down Expand Up @@ -260,7 +261,18 @@ export class IndexedDBFileSystemProvider extends Disposable implements IFileSyst
if (existing?.type === FileType.Directory) {
throw ERR_FILE_IS_DIR;
}
await this.bulkWrite([[resource, content]]);

let finalContent = content;
if (opts.append && existing) {
// Read existing content and append new content to it
const existingContent = await this.readFile(resource);
const combined = new Uint8Array(existingContent.byteLength + content.byteLength);
combined.set(existingContent, 0);
combined.set(content, existingContent.byteLength);
finalContent = combined;
}

await this.bulkWrite([[resource, finalContent]]);
}

async rename(from: URI, to: URI, opts: IFileOverwriteOptions): Promise<void> {
Expand Down
3 changes: 2 additions & 1 deletion src/vs/platform/files/common/diskFileSystemProviderClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { newWriteableStream, ReadableStreamEventPayload, ReadableStreamEvents }
import { URI } from '../../../base/common/uri.js';
import { generateUuid } from '../../../base/common/uuid.js';
import { IChannel } from '../../../base/parts/ipc/common/ipc.js';
import { createFileSystemProviderError, IFileAtomicReadOptions, IFileDeleteOptions, IFileOpenOptions, IFileOverwriteOptions, IFileReadStreamOptions, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileChange, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileCloneCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IStat, IWatchOptions, IFileSystemProviderError } from './files.js';
import { createFileSystemProviderError, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileAtomicReadOptions, IFileChange, IFileDeleteOptions, IFileOpenOptions, IFileOverwriteOptions, IFileReadStreamOptions, IFileSystemProviderError, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileCloneCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileWriteOptions, IStat, IWatchOptions } from './files.js';
import { reviveFileChanges } from './watcher.js';

export const LOCAL_FILE_SYSTEM_CHANNEL_NAME = 'localFilesystem';
Expand Down Expand Up @@ -56,6 +56,7 @@ export class DiskFileSystemProviderClient extends Disposable implements
FileSystemProviderCapabilities.FileAtomicRead |
FileSystemProviderCapabilities.FileAtomicWrite |
FileSystemProviderCapabilities.FileAtomicDelete |
FileSystemProviderCapabilities.FileAppend |
FileSystemProviderCapabilities.FileClone |
FileSystemProviderCapabilities.FileRealpath;

Expand Down
11 changes: 8 additions & 3 deletions src/vs/platform/files/common/fileService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { extUri, extUriIgnorePathCase, IExtUri, isAbsolutePath } from '../../../
import { consumeStream, isReadableBufferedStream, isReadableStream, listenStream, newWriteableStream, peekReadable, peekStream, transform } from '../../../base/common/stream.js';
import { URI } from '../../../base/common/uri.js';
import { localize } from '../../../nls.js';
import { ensureFileSystemProviderError, etag, ETAG_DISABLED, FileChangesEvent, IFileDeleteOptions, FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FilePermission, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, hasFileAtomicReadCapability, hasFileFolderCopyCapability, hasFileReadStreamCapability, hasOpenReadWriteCloseCapability, hasReadWriteCapability, ICreateFileOptions, IFileContent, IFileService, IFileStat, IFileStatWithMetadata, IFileStreamContent, IFileSystemProvider, IFileSystemProviderActivationEvent, IFileSystemProviderCapabilitiesChangeEvent, IFileSystemProviderRegistrationEvent, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IReadFileOptions, IReadFileStreamOptions, IResolveFileOptions, IFileStatResult, IFileStatResultWithMetadata, IResolveMetadataFileOptions, IStat, IFileStatWithPartialMetadata, IWatchOptions, IWriteFileOptions, NotModifiedSinceFileOperationError, toFileOperationResult, toFileSystemProviderErrorCode, hasFileCloneCapability, TooLargeFileOperationError, hasFileAtomicDeleteCapability, hasFileAtomicWriteCapability, IWatchOptionsWithCorrelation, IFileSystemWatcher, IWatchOptionsWithoutCorrelation, hasFileRealpathCapability } from './files.js';
import { ensureFileSystemProviderError, etag, ETAG_DISABLED, FileChangesEvent, IFileDeleteOptions, FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FilePermission, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, hasFileAppendCapability, hasFileAtomicReadCapability, hasFileFolderCopyCapability, hasFileReadStreamCapability, hasOpenReadWriteCloseCapability, hasReadWriteCapability, ICreateFileOptions, IFileContent, IFileService, IFileStat, IFileStatWithMetadata, IFileStreamContent, IFileSystemProvider, IFileSystemProviderActivationEvent, IFileSystemProviderCapabilitiesChangeEvent, IFileSystemProviderRegistrationEvent, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IReadFileOptions, IReadFileStreamOptions, IResolveFileOptions, IFileStatResult, IFileStatResultWithMetadata, IResolveMetadataFileOptions, IStat, IFileStatWithPartialMetadata, IWatchOptions, IWriteFileOptions, NotModifiedSinceFileOperationError, toFileOperationResult, toFileSystemProviderErrorCode, hasFileCloneCapability, TooLargeFileOperationError, hasFileAtomicDeleteCapability, hasFileAtomicWriteCapability, IWatchOptionsWithCorrelation, IFileSystemWatcher, IWatchOptionsWithoutCorrelation, hasFileRealpathCapability } from './files.js';
import { readFileIntoStream } from './io.js';
import { ILogService } from '../../log/common/log.js';
import { ErrorNoTelemetry } from '../../../base/common/errors.js';
Expand Down Expand Up @@ -460,6 +460,11 @@ export class FileService extends Disposable implements IFileService {
throw new Error(localize('writeFailedUnlockUnsupported', "Unable to unlock file '{0}' because provider does not support it.", this.resourceForError(resource)));
}

// Validate append support
if (options?.append && !hasFileAppendCapability(provider)) {
throw new FileOperationError(localize('err.noAppend', "Filesystem provider for scheme '{0}' does not does not support append", this.resourceForError(resource)), FileOperationResult.FILE_PERMISSION_DENIED);
}

// Validate atomic support
const atomic = !!options?.atomic;
if (atomic) {
Expand Down Expand Up @@ -1263,7 +1268,7 @@ export class FileService extends Disposable implements IFileService {
return this.writeQueue.queueFor(resource, async () => {

// open handle
const handle = await provider.open(resource, { create: true, unlock: options?.unlock ?? false });
const handle = await provider.open(resource, { create: true, unlock: options?.unlock ?? false, append: options?.append ?? false });

// write into handle until all bytes from buffer have been written
try {
Expand Down Expand Up @@ -1374,7 +1379,7 @@ export class FileService extends Disposable implements IFileService {
}

// Write through the provider
await provider.writeFile(resource, buffer.buffer, { create: true, overwrite: true, unlock: options?.unlock ?? false, atomic: options?.atomic ?? false });
await provider.writeFile(resource, buffer.buffer, { create: true, overwrite: true, unlock: options?.unlock ?? false, atomic: options?.atomic ?? false, append: options?.append ?? false });
}

private async doPipeBuffered(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
Expand Down
30 changes: 29 additions & 1 deletion src/vs/platform/files/common/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ export interface IFileService {

/**
* Updates the content replacing its previous value.
* If `options.append` is true, appends content to the end of the file instead.
*
* Emits a `FileOperation.WRITE` file operation event when successful.
*/
Expand Down Expand Up @@ -381,6 +382,12 @@ export interface IFileWriteOptions extends IFileOverwriteOptions, IFileUnlockOpt
* throw an error otherwise if the file does not exist.
*/
readonly create: boolean;

/**
* Set to `true` to append content to the end of the file. Implies `create: true`,
* and set only when the corresponding `FileAppend` capability is defined.
*/
readonly append?: boolean;
}

export type IFileOpenOptions = IFileOpenForReadOptions | IFileOpenForWriteOptions;
Expand All @@ -403,6 +410,12 @@ export interface IFileOpenForWriteOptions extends IFileUnlockOptions {
* A hint that the file should be opened for reading and writing.
*/
readonly create: true;

/**
* Open the file in append mode. This will write data to the
* end of the file.
*/
readonly append?: boolean;
}

export interface IFileDeleteOptions {
Expand Down Expand Up @@ -654,7 +667,12 @@ export const enum FileSystemProviderCapabilities {
/**
* Provider support to resolve real paths.
*/
FileRealpath = 1 << 18
FileRealpath = 1 << 18,

/**
* Provider support to append to files.
*/
FileAppend = 1 << 19
}

export interface IFileSystemProvider {
Expand Down Expand Up @@ -696,6 +714,10 @@ export function hasReadWriteCapability(provider: IFileSystemProvider): provider
return !!(provider.capabilities & FileSystemProviderCapabilities.FileReadWrite);
}

export function hasFileAppendCapability(provider: IFileSystemProvider): boolean {
return !!(provider.capabilities & FileSystemProviderCapabilities.FileAppend);
}

export interface IFileSystemProviderWithFileFolderCopyCapability extends IFileSystemProvider {
copy(from: URI, to: URI, opts: IFileOverwriteOptions): Promise<void>;
}
Expand Down Expand Up @@ -1387,6 +1409,12 @@ export interface IWriteFileOptions {
* and then renaming it over the target.
*/
readonly atomic?: IFileAtomicOptions | false;

/**
* If set to true, will append to the end of the file instead of
* replacing its contents. Will create the file if it doesn't exist.
*/
readonly append?: boolean;
}

export interface IResolveFileOptions {
Expand Down
90 changes: 71 additions & 19 deletions src/vs/platform/files/common/inMemoryFilesystemProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Disposable, IDisposable } from '../../../base/common/lifecycle.js';
import * as resources from '../../../base/common/resources.js';
import { ReadableStreamEvents, newWriteableStream } from '../../../base/common/stream.js';
import { URI } from '../../../base/common/uri.js';
import { FileChangeType, IFileDeleteOptions, IFileOverwriteOptions, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileChange, IFileSystemProviderWithFileReadWriteCapability, IStat, IWatchOptions, createFileSystemProviderError, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileOpenOptions, IFileSystemProviderWithFileAtomicDeleteCapability, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileAtomicWriteCapability, IFileSystemProviderWithFileReadStreamCapability } from './files.js';
import { FileChangeType, IFileDeleteOptions, IFileOverwriteOptions, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileChange, IFileSystemProviderWithFileReadWriteCapability, IStat, IWatchOptions, createFileSystemProviderError, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileOpenOptions, IFileSystemProviderWithFileAtomicDeleteCapability, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileAtomicWriteCapability, IFileSystemProviderWithFileReadStreamCapability, isFileOpenForWriteOptions } from './files.js';

class File implements IStat {

Expand Down Expand Up @@ -61,18 +61,17 @@ export class InMemoryFileSystemProvider extends Disposable implements
IFileSystemProviderWithFileAtomicDeleteCapability {

private memoryFdCounter = 0;
private readonly fdMemory = new Map<number, Uint8Array>();
private readonly fdMemory = new Map<number, { file: File; resource: URI; append: boolean; write: boolean }>();
private _onDidChangeCapabilities = this._register(new Emitter<void>());
readonly onDidChangeCapabilities = this._onDidChangeCapabilities.event;

private _capabilities = FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.PathCaseSensitive;
private _capabilities = FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.FileOpenReadWriteClose | FileSystemProviderCapabilities.FileAppend | FileSystemProviderCapabilities.PathCaseSensitive;
get capabilities(): FileSystemProviderCapabilities { return this._capabilities; }

setReadOnly(readonly: boolean) {
const isReadonly = !!(this._capabilities & FileSystemProviderCapabilities.Readonly);
if (readonly !== isReadonly) {
this._capabilities = readonly ? FileSystemProviderCapabilities.Readonly | FileSystemProviderCapabilities.PathCaseSensitive | FileSystemProviderCapabilities.FileReadWrite
: FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.PathCaseSensitive;
this._capabilities = FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.FileAppend | FileSystemProviderCapabilities.PathCaseSensitive | (readonly ? FileSystemProviderCapabilities.Readonly : 0);
this._onDidChangeCapabilities.fire();
}
}
Expand Down Expand Up @@ -130,47 +129,100 @@ export class InMemoryFileSystemProvider extends Disposable implements
this._fireSoon({ type: FileChangeType.ADDED, resource });
}
entry.mtime = Date.now();
entry.size = content.byteLength;
entry.data = content;

if (opts.append) {
entry.size += content.byteLength;
const oldData = entry.data ?? new Uint8Array(0);
const newData = new Uint8Array(oldData.byteLength + content.byteLength);
newData.set(oldData, 0);
newData.set(content, oldData.byteLength);
entry.data = newData;
} else {
entry.size = content.byteLength;
entry.data = content;
}

this._fireSoon({ type: FileChangeType.UPDATED, resource });
}

// file open/read/write/close
open(resource: URI, opts: IFileOpenOptions): Promise<number> {
const data = this._lookupAsFile(resource, false).data;
if (data) {
const fd = this.memoryFdCounter++;
this.fdMemory.set(fd, data);
return Promise.resolve(fd);
let file = this._lookup(resource, true);
const write = isFileOpenForWriteOptions(opts);
const append = write && !!opts.append;

if (!file) {
if (!write) {
throw createFileSystemProviderError('file not found', FileSystemProviderErrorCode.FileNotFound);
}
// Create the file if opening for write
const basename = resources.basename(resource);
const parent = this._lookupParentDirectory(resource);
file = new File(basename);
file.data = new Uint8Array(0);
parent.entries.set(basename, file);
this._fireSoon({ type: FileChangeType.ADDED, resource });
} else if (file instanceof Directory) {
throw createFileSystemProviderError('file is directory', FileSystemProviderErrorCode.FileIsADirectory);
}
throw createFileSystemProviderError('file not found', FileSystemProviderErrorCode.FileNotFound);

if (!file.data) {
file.data = new Uint8Array(0);
}

const fd = this.memoryFdCounter++;
this.fdMemory.set(fd, { file, resource, write, append });
return Promise.resolve(fd);
}

close(fd: number): Promise<void> {
const fdData = this.fdMemory.get(fd);
if (fdData?.write) {
// Update file metadata on close
fdData.file.mtime = Date.now();
fdData.file.size = fdData.file.data?.byteLength ?? 0;
this._fireSoon({ type: FileChangeType.UPDATED, resource: fdData.resource });
}
this.fdMemory.delete(fd);
return Promise.resolve();
}

read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
const memory = this.fdMemory.get(fd);
if (!memory) {
const fdData = this.fdMemory.get(fd);
if (!fdData) {
throw createFileSystemProviderError(`No file with that descriptor open`, FileSystemProviderErrorCode.Unavailable);
}

const toWrite = VSBuffer.wrap(memory).slice(pos, pos + length);
if (!fdData.file.data) {
return Promise.resolve(0);
}

const toWrite = VSBuffer.wrap(fdData.file.data).slice(pos, pos + length);
data.set(toWrite.buffer, offset);
return Promise.resolve(toWrite.byteLength);
}

write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
const memory = this.fdMemory.get(fd);
if (!memory) {
const fdData = this.fdMemory.get(fd);
if (!fdData) {
throw createFileSystemProviderError(`No file with that descriptor open`, FileSystemProviderErrorCode.Unavailable);
}

const toWrite = VSBuffer.wrap(data).slice(offset, offset + length);
memory.set(toWrite.buffer, pos);
fdData.file.data ??= new Uint8Array(0);

// In append mode, always write at the end
const writePos = fdData.append ? fdData.file.data.byteLength : pos;

// Grow the buffer if needed
const endPos = writePos + toWrite.byteLength;
if (endPos > fdData.file.data.byteLength) {
const newData = new Uint8Array(endPos);
newData.set(fdData.file.data, 0);
fdData.file.data = newData;
}

fdData.file.data.set(toWrite.buffer, writePos);
return Promise.resolve(toWrite.byteLength);
}

Expand Down
Loading
Loading