Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like this method deserves a test.

}

if (needleLen > haystackLen - offset) {
Expand Down
29 changes: 27 additions & 2 deletions src/vs/platform/files/browser/indexedDBFileSystemProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { ExtUri } from '../../../base/common/resources.js';
import { isString } from '../../../base/common/types.js';
import { URI, UriDto } from '../../../base/common/uri.js';
import { localize } from '../../../nls.js';
import { createFileSystemProviderError, FileChangeType, IFileDeleteOptions, IFileOverwriteOptions, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileChange, IFileSystemProviderWithFileReadWriteCapability, IStat, IWatchOptions } from '../common/files.js';
import { createFileSystemProviderError, FileChangeType, IFileDeleteOptions, IFileOverwriteOptions, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileChange, IFileSystemProviderWithFileAppendCapability, IFileSystemProviderWithFileReadWriteCapability, IStat, IWatchOptions } from '../common/files.js';
import { IndexedDB } from '../../../base/browser/indexedDB.js';
import { BroadcastDataChannel } from '../../../base/browser/broadcast.js';

Expand Down Expand Up @@ -152,10 +152,11 @@ class IndexedDBFileSystemNode {
}
}

export class IndexedDBFileSystemProvider extends Disposable implements IFileSystemProviderWithFileReadWriteCapability {
export class IndexedDBFileSystemProvider extends Disposable implements IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileAppendCapability {

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

Expand Down Expand Up @@ -263,6 +264,30 @@ export class IndexedDBFileSystemProvider extends Disposable implements IFileSyst
await this.bulkWrite([[resource, content]]);
}

async appendFile(resource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise<void> {
const existing = await this.stat(resource).catch(() => undefined);
if (existing?.type === FileType.Directory) {
throw ERR_FILE_IS_DIR;
}

// Read existing content and append
let existingContent: Uint8Array;
try {
existingContent = await this.readFile(resource);
} catch (error) {
if (!opts.create) {
throw error;
}
existingContent = new Uint8Array(0);
}

const newContent = new Uint8Array(existingContent.byteLength + content.byteLength);
newContent.set(existingContent, 0);
newContent.set(content, existingContent.byteLength);

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

async rename(from: URI, to: URI, opts: IFileOverwriteOptions): Promise<void> {
const fileTree = await this.getFiletree();
const fromEntry = fileTree.read(from.path);
Expand Down
8 changes: 7 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, IFileSystemProviderWithFileAppendCapability, 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 All @@ -29,6 +29,7 @@ export class DiskFileSystemProviderClient extends Disposable implements
IFileSystemProviderWithFileReadStreamCapability,
IFileSystemProviderWithFileFolderCopyCapability,
IFileSystemProviderWithFileAtomicReadCapability,
IFileSystemProviderWithFileAppendCapability,
IFileSystemProviderWithFileCloneCapability {

constructor(
Expand Down Expand Up @@ -56,6 +57,7 @@ export class DiskFileSystemProviderClient extends Disposable implements
FileSystemProviderCapabilities.FileAtomicRead |
FileSystemProviderCapabilities.FileAtomicWrite |
FileSystemProviderCapabilities.FileAtomicDelete |
FileSystemProviderCapabilities.FileAppend |
FileSystemProviderCapabilities.FileClone |
FileSystemProviderCapabilities.FileRealpath;

Expand Down Expand Up @@ -160,6 +162,10 @@ export class DiskFileSystemProviderClient extends Disposable implements
return this.channel.call('writeFile', [resource, VSBuffer.wrap(content), opts]);
}

appendFile(resource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise<void> {
return this.channel.call('appendFile', [resource, VSBuffer.wrap(content), opts]);
}

open(resource: URI, opts: IFileOpenOptions): Promise<number> {
return this.channel.call('open', [resource, opts]);
}
Expand Down
56 changes: 55 additions & 1 deletion 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, IFileWriteOptions, 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 @@ -431,6 +431,60 @@ export class FileService extends Disposable implements IFileService {
return this.resolve(resource, { resolveMetadata: true });
}

async appendFile(resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream, options?: IWriteFileOptions): Promise<IFileStatWithMetadata> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it wouldn't be just easier to have another option in IWriteFileOptions to append? Then you could pretty much reuse everything all the way down to the implementation and just use the a mode when writing?

const provider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(resource), resource);

try {
// if provider supports append, use it directly
if (hasFileAppendCapability(provider)) {
const buffer = bufferOrReadableOrStream instanceof VSBuffer
? bufferOrReadableOrStream
: isReadableStream(bufferOrReadableOrStream)
? await streamToBuffer(bufferOrReadableOrStream)
: readableToBuffer(bufferOrReadableOrStream);

const writeOptions: IFileWriteOptions = {
create: true,
overwrite: false,
unlock: options?.unlock ?? false,
atomic: options?.atomic ?? false
};

await provider.appendFile(resource, buffer.buffer, writeOptions);

// events
this._onDidRunOperation.fire(new FileOperationEvent(resource, FileOperation.WRITE));
} else {
// fallback: read existing content and write combined content
let existingContent: VSBuffer;
try {
const fileContent = await this.readFile(resource);
existingContent = fileContent.value;
} catch (error) {
// if file doesn't exist, start with empty content
if (toFileOperationResult(error) === FileOperationResult.FILE_NOT_FOUND) {
existingContent = VSBuffer.fromString('');
} else {
throw error;
}
}

const newContent = bufferOrReadableOrStream instanceof VSBuffer
? bufferOrReadableOrStream
: isReadableStream(bufferOrReadableOrStream)
? await streamToBuffer(bufferOrReadableOrStream)
: readableToBuffer(bufferOrReadableOrStream);

const combinedContent = VSBuffer.concat([existingContent, newContent]);
await this.writeFile(resource, combinedContent, options);
}
} catch (error) {
throw new FileOperationError(localize('err.append', "Unable to append to file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), options);
}

return this.resolve(resource, { resolveMetadata: true });
}


private async peekBufferForWriting(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream): Promise<VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream> {
let peekResult: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream;
Expand Down
23 changes: 22 additions & 1 deletion src/vs/platform/files/common/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,13 @@ export interface IFileService {
*/
writeFile(resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream, options?: IWriteFileOptions): Promise<IFileStatWithMetadata>;

/**
* Appends content to the end of the file.
*
* Emits a `FileOperation.WRITE` file operation event when successful.
*/
appendFile(resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream, options?: IWriteFileOptions): Promise<IFileStatWithMetadata>;

/**
* Moves the file/folder to a new path identified by the resource.
*
Expand Down Expand Up @@ -654,7 +661,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 All @@ -676,6 +688,7 @@ export interface IFileSystemProvider {

readFile?(resource: URI): Promise<Uint8Array>;
writeFile?(resource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise<void>;
appendFile?(resource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise<void>;

readFileStream?(resource: URI, opts: IFileReadStreamOptions, token: CancellationToken): ReadableStreamEvents<Uint8Array>;

Expand All @@ -696,6 +709,14 @@ export function hasReadWriteCapability(provider: IFileSystemProvider): provider
return !!(provider.capabilities & FileSystemProviderCapabilities.FileReadWrite);
}

export interface IFileSystemProviderWithFileAppendCapability extends IFileSystemProvider {
appendFile(resource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise<void>;
}

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

export interface IFileSystemProviderWithFileFolderCopyCapability extends IFileSystemProvider {
copy(from: URI, to: URI, opts: IFileOverwriteOptions): Promise<void>;
}
Expand Down
38 changes: 34 additions & 4 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, IFileSystemProviderWithFileAppendCapability, IFileSystemProviderWithFileReadWriteCapability, IStat, IWatchOptions, createFileSystemProviderError, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileOpenOptions, IFileSystemProviderWithFileAtomicDeleteCapability, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileAtomicWriteCapability, IFileSystemProviderWithFileReadStreamCapability } from './files.js';

class File implements IStat {

Expand Down Expand Up @@ -56,6 +56,7 @@ export class InMemoryFileSystemProvider extends Disposable implements
IFileSystemProviderWithFileReadWriteCapability,
IFileSystemProviderWithOpenReadWriteCloseCapability,
IFileSystemProviderWithFileReadStreamCapability,
IFileSystemProviderWithFileAppendCapability,
IFileSystemProviderWithFileAtomicReadCapability,
IFileSystemProviderWithFileAtomicWriteCapability,
IFileSystemProviderWithFileAtomicDeleteCapability {
Expand All @@ -65,14 +66,14 @@ export class InMemoryFileSystemProvider extends Disposable implements
private _onDidChangeCapabilities = this._register(new Emitter<void>());
readonly onDidChangeCapabilities = this._onDidChangeCapabilities.event;

private _capabilities = FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.PathCaseSensitive;
private _capabilities = FileSystemProviderCapabilities.FileReadWrite | 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 = readonly ? FileSystemProviderCapabilities.Readonly | FileSystemProviderCapabilities.PathCaseSensitive | FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.FileAppend
: FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.FileAppend | FileSystemProviderCapabilities.PathCaseSensitive;
this._onDidChangeCapabilities.fire();
}
}
Expand Down Expand Up @@ -136,6 +137,35 @@ export class InMemoryFileSystemProvider extends Disposable implements
this._fireSoon({ type: FileChangeType.UPDATED, resource });
}

async appendFile(resource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise<void> {
const basename = resources.basename(resource);
const parent = this._lookupParentDirectory(resource);
let entry = parent.entries.get(basename);
if (entry instanceof Directory) {
throw createFileSystemProviderError('file is directory', FileSystemProviderErrorCode.FileIsADirectory);
}
if (!entry && !opts.create) {
throw createFileSystemProviderError('file not found', FileSystemProviderErrorCode.FileNotFound);
}
if (!entry) {
entry = new File(basename);
parent.entries.set(basename, entry);
this._fireSoon({ type: FileChangeType.ADDED, resource });
}

// Append to existing data
const existingData = entry.data || new Uint8Array(0);
const newData = new Uint8Array(existingData.byteLength + content.byteLength);
newData.set(existingData, 0);
newData.set(content, existingData.byteLength);

entry.mtime = Date.now();
entry.size = newData.byteLength;
entry.data = newData;

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;
Expand Down
Loading
Loading