355 строки
11 KiB
TypeScript
355 строки
11 KiB
TypeScript
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License.
|
|
|
|
import { strict } from 'assert';
|
|
import { dirname, join, relative } from 'path';
|
|
import { Readable, Writable } from 'stream';
|
|
import { URL } from 'url';
|
|
import { URI } from 'vscode-uri';
|
|
import { UriComponents } from 'vscode-uri/lib/umd/uri';
|
|
import { FileStat, FileSystem, FileType, ReadHandle, WriteStreamOptions } from '../fs/filesystem';
|
|
import { HashVerifyEvents } from '../interfaces/events';
|
|
import { Algorithm, Hash, hash } from './hash';
|
|
import { decode, encode } from './text';
|
|
|
|
/**
|
|
* This class is intended to be a drop-in replacement for the vscode uri
|
|
* class, but has a filesystem associated with it.
|
|
*
|
|
* By associating the filesystem with the URI, we can allow for file URIs
|
|
* to be scoped to a given filesystem (ie, a zip could be a filesystem )
|
|
*
|
|
* Uniform Resource Identifier (URI) https://tools.ietf.org/html/rfc3986.
|
|
* This class is a simple parser which creates the basic component parts
|
|
* (https://tools.ietf.org/html/rfc3986#section-3) with minimal validation
|
|
* and encoding.
|
|
*
|
|
*
|
|
* ```txt
|
|
* foo://example.com:8042/over/there?name=ferret#nose
|
|
* \_/ \______________/\_________/ \_________/ \__/
|
|
* | | | | |
|
|
* scheme authority path query fragment
|
|
* | _____________________|__
|
|
* / \ / \
|
|
* urn:example:animal:ferret:nose
|
|
* ```
|
|
*
|
|
*/
|
|
export class Uri implements URI {
|
|
protected constructor(public readonly fileSystem: FileSystem, protected readonly uri: URI) {
|
|
|
|
}
|
|
|
|
static readonly invalid = new Uri(<any>undefined, URI.parse('invalid:'));
|
|
|
|
static isInvalid(uri?: Uri) {
|
|
return uri === undefined || Uri.invalid === uri;
|
|
}
|
|
/**
|
|
* scheme is the 'https' part of 'https://www.msft.com/some/path?query#fragment'.
|
|
* The part before the first colon.
|
|
*/
|
|
get scheme() { return this.uri.scheme; }
|
|
|
|
/**
|
|
* authority is the 'www.msft.com' part of 'https://www.msft.com/some/path?query#fragment'.
|
|
* The part between the first double slashes and the next slash.
|
|
*/
|
|
get authority() { return this.uri.authority; }
|
|
|
|
/**
|
|
* path is the '/some/path' part of 'https://www.msft.com/some/path?query#fragment'.
|
|
*/
|
|
get path() { return this.uri.path; }
|
|
|
|
/**
|
|
* query is the 'query' part of 'https://www.msft.com/some/path?query#fragment'.
|
|
*/
|
|
get query() { return this.uri.query; }
|
|
|
|
/**
|
|
* fragment is the 'fragment' part of 'https://www.msft.com/some/path?query#fragment'.
|
|
*/
|
|
get fragment() { return this.uri.fragment; }
|
|
|
|
/**
|
|
* Creates a new Uri from a string, e.g. `https://www.msft.com/some/path`,
|
|
* `file:///usr/home`, or `scheme:with/path`.
|
|
*
|
|
* @param value A string which represents an URI (see `URI#toString`).
|
|
*/
|
|
static parse(fileSystem: FileSystem, value: string, _strict?: boolean): Uri {
|
|
return new Uri(fileSystem, URI.parse(value, _strict));
|
|
}
|
|
|
|
/**
|
|
* Creates a new Uri from a string, and replaces 'vsix' schemes with file:// instead.
|
|
*
|
|
* @param value A string which represents a URI which may be a VSIX uri.
|
|
*/
|
|
static parseFilterVsix(fileSystem: FileSystem, value: string, _strict?: boolean, vsixBaseUri?: Uri): Uri {
|
|
const parsed = URI.parse(value, _strict);
|
|
if (vsixBaseUri && parsed.scheme === 'vsix') {
|
|
return vsixBaseUri.join(parsed.path);
|
|
}
|
|
|
|
return new Uri(fileSystem, parsed);
|
|
}
|
|
|
|
/**
|
|
* Creates a new URI from a file system path, e.g. `c:\my\files`,
|
|
* `/usr/home`, or `\\server\share\some\path`.
|
|
*
|
|
* The *difference* between `URI#parse` and `URI#file` is that the latter treats the argument
|
|
* as path, not as stringified-uri. E.g. `URI.file(path)` is **not the same as**
|
|
* `URI.parse('file://' + path)` because the path might contain characters that are
|
|
* interpreted (# and ?). See the following sample:
|
|
* ```ts
|
|
const good = URI.file('/coding/c#/project1');
|
|
good.scheme === 'file';
|
|
good.path === '/coding/c#/project1';
|
|
good.fragment === '';
|
|
const bad = URI.parse('file://' + '/coding/c#/project1');
|
|
bad.scheme === 'file';
|
|
bad.path === '/coding/c'; // path is now broken
|
|
bad.fragment === '/project1';
|
|
```
|
|
*
|
|
* @param path A file system path (see `URI#fsPath`)
|
|
*/
|
|
static file(fileSystem: FileSystem, path: string): Uri {
|
|
return new Uri(fileSystem, URI.file(path));
|
|
}
|
|
|
|
/** construct an Uri from the various parts */
|
|
static from(fileSystem: FileSystem, components: {
|
|
scheme: string;
|
|
authority?: string;
|
|
path?: string;
|
|
query?: string;
|
|
fragment?: string;
|
|
}): Uri {
|
|
return new Uri(fileSystem, URI.from(components));
|
|
}
|
|
|
|
/**
|
|
* Join all arguments together and normalize the resulting Uri.
|
|
*
|
|
* Also ensures that slashes are all forward.
|
|
* */
|
|
join(...paths: Array<string>) {
|
|
return new Uri(this.fileSystem, this.with({ path: join(this.path, ...paths).replace(/\\/g, '/') }));
|
|
}
|
|
|
|
relative(target: Uri): string {
|
|
strict.ok(target.authority === this.authority, `Uris '${target.toString()}' and '${this.toString()}' are not of the same base`);
|
|
return relative(this.path, target.path).replace(/\\/g, '/');
|
|
}
|
|
|
|
/** returns true if the uri represents a file:// resource. */
|
|
get isLocal(): boolean {
|
|
return this.scheme === 'file' || this.scheme === 'vsix';
|
|
}
|
|
|
|
get isHttps(): boolean {
|
|
return this.scheme === 'https';
|
|
}
|
|
/**
|
|
* Returns a string representing the corresponding file system path of this URI.
|
|
* Will handle UNC paths, normalizes windows drive letters to lower-case, and uses the
|
|
* platform specific path separator.
|
|
*
|
|
* * Will *not* validate the path for invalid characters and semantics.
|
|
* * Will *not* look at the scheme of this URI.
|
|
* * The result shall *not* be used for display purposes but for accessing a file on disk.
|
|
*
|
|
*
|
|
* The *difference* to `URI#path` is the use of the platform specific separator and the handling
|
|
* of UNC paths. See the below sample of a file-uri with an authority (UNC path).
|
|
*
|
|
* ```ts
|
|
const u = URI.parse('file://server/c$/folder/file.txt')
|
|
u.authority === 'server'
|
|
u.path === '/shares/c$/file.txt'
|
|
u.fsPath === '\\server\c$\folder\file.txt'
|
|
```
|
|
*
|
|
* Using `URI#path` to read a file (using fs-apis) would not be enough because parts of the path,
|
|
* namely the server name, would be missing. Therefore `URI#fsPath` exists - it's sugar to ease working
|
|
* with URIs that represent files on disk (`file` scheme).
|
|
*/
|
|
get fsPath(): string {
|
|
return this.uri.fsPath;
|
|
}
|
|
|
|
/** Duplicates the current Uri, changing out any parts */
|
|
with(change: { scheme?: string | undefined; authority?: string | null | undefined; path?: string | null | undefined; query?: string | null | undefined; fragment?: string | null | undefined; }): URI {
|
|
return new Uri(this.fileSystem, this.uri.with(change));
|
|
}
|
|
|
|
/**
|
|
* Creates a string representation for this URI. It's guaranteed that calling
|
|
* `URI.parse` with the result of this function creates an URI which is equal
|
|
* to this URI.
|
|
*
|
|
* * The result shall *not* be used for display purposes but for externalization or transport.
|
|
* * The result will be encoded using the percentage encoding and encoding happens mostly
|
|
* ignore the scheme-specific encoding rules.
|
|
*
|
|
* @param skipEncoding Do not encode the result, default is `false`
|
|
*/
|
|
toString(skipEncoding?: boolean): string {
|
|
return this.uri.toString(skipEncoding);
|
|
}
|
|
|
|
get formatted(): string {
|
|
return this.scheme === 'file' ? this.uri.fsPath : this.uri.toString();
|
|
}
|
|
|
|
/** returns a JSON object with the components of the Uri */
|
|
toJSON(): UriComponents {
|
|
return this.uri.toJSON();
|
|
}
|
|
|
|
toUrl(): URL {
|
|
return new URL(this.uri.toString());
|
|
}
|
|
|
|
/* Act on this uri */
|
|
protected resolve(uriOrRelativePath?: Uri | string) {
|
|
return typeof uriOrRelativePath === 'string' ? this.join(uriOrRelativePath) : uriOrRelativePath ?? this;
|
|
}
|
|
|
|
stat(uri?: Uri | string): Promise<FileStat> {
|
|
uri = this.resolve(uri);
|
|
return uri.fileSystem.stat(uri);
|
|
}
|
|
|
|
readDirectory(uri?: Uri | string, options?: { recursive?: boolean }): Promise<Array<[Uri, FileType]>> {
|
|
uri = this.resolve(uri);
|
|
return uri.fileSystem.readDirectory(uri, options);
|
|
}
|
|
|
|
async createDirectory(uri?: Uri | string): Promise<Uri> {
|
|
uri = this.resolve(uri);
|
|
await uri.fileSystem.createDirectory(uri);
|
|
return uri;
|
|
}
|
|
|
|
readFile(uri?: Uri | string): Promise<Uint8Array> {
|
|
uri = this.resolve(uri);
|
|
return uri.fileSystem.readFile(uri);
|
|
}
|
|
|
|
async readUTF8(uri?: Uri | string): Promise<string> {
|
|
return decode(await this.readFile(uri));
|
|
}
|
|
|
|
async tryReadUTF8(uri?: Uri | string): Promise<string | undefined> {
|
|
try {
|
|
return await this.readUTF8(uri);
|
|
// eslint-disable-next-line no-empty
|
|
} catch { }
|
|
|
|
return undefined;
|
|
}
|
|
|
|
openFile(uri?: Uri | string): Promise<ReadHandle> {
|
|
uri = this.resolve(uri);
|
|
return uri.fileSystem.openFile(uri);
|
|
}
|
|
|
|
readStream(start = 0, end = Infinity): Promise<Readable> {
|
|
return this.fileSystem.readStream(this, { start, end });
|
|
}
|
|
|
|
async readBlock(start = 0, end = Infinity): Promise<Buffer> {
|
|
const stream = await this.fileSystem.readStream(this, { start, end });
|
|
|
|
let block = Buffer.alloc(0);
|
|
for await (const chunk of stream) {
|
|
block = Buffer.concat([block, chunk]);
|
|
}
|
|
return block;
|
|
}
|
|
|
|
async writeFile(content: Uint8Array): Promise<Uri> {
|
|
await this.fileSystem.writeFile(this, content);
|
|
return this;
|
|
}
|
|
|
|
writeUTF8(content: string): Promise<Uri> {
|
|
return this.writeFile(encode(content));
|
|
}
|
|
|
|
writeStream(options?: WriteStreamOptions): Promise<Writable> {
|
|
return this.fileSystem.writeStream(this, options);
|
|
}
|
|
|
|
delete(options?: { recursive?: boolean, useTrash?: boolean }): Promise<void> {
|
|
return this.fileSystem.delete(this, options);
|
|
}
|
|
|
|
exists(uri?: Uri | string): Promise<boolean> {
|
|
uri = this.resolve(uri);
|
|
return uri.fileSystem.exists(uri);
|
|
}
|
|
|
|
isFile(uri?: Uri | string): Promise<boolean> {
|
|
uri = this.resolve(uri);
|
|
return uri.fileSystem.isFile(uri);
|
|
}
|
|
|
|
isSymlink(uri?: Uri | string): Promise<boolean> {
|
|
uri = this.resolve(uri);
|
|
return uri.fileSystem.isSymlink(uri);
|
|
}
|
|
|
|
isDirectory(uri?: Uri | string): Promise<boolean> {
|
|
uri = this.resolve(uri);
|
|
return uri.fileSystem.isDirectory(uri);
|
|
}
|
|
|
|
async size(uri?: Uri | string): Promise<number> {
|
|
return (await this.stat(uri)).size;
|
|
}
|
|
|
|
async hash(algorithm?: Algorithm): Promise<string | undefined> {
|
|
if (algorithm) {
|
|
|
|
return await hash(await this.fileSystem.readStream(this), this, await this.size(), algorithm, {});
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
async hashValid(events: Partial<HashVerifyEvents>, matchOptions?: Hash) {
|
|
if (matchOptions?.algorithm && await this.exists()) {
|
|
events.hashVerifyStart?.(this.fsPath);
|
|
const result = matchOptions.value?.toLowerCase() === await hash(await this.readStream(), this, await this.size(), matchOptions.algorithm, events);
|
|
events.hashVerifyComplete?.(this.fsPath);
|
|
return result;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
get parent(): Uri {
|
|
return new Uri(this.fileSystem, this.with({
|
|
path: dirname(this.path)
|
|
}));
|
|
}
|
|
}
|
|
|
|
export function isFilePath(uriOrPath?: Uri | string): boolean {
|
|
if (uriOrPath) {
|
|
if (uriOrPath instanceof Uri) {
|
|
return uriOrPath.scheme === 'file';
|
|
}
|
|
if (uriOrPath.startsWith('file:')) {
|
|
return true;
|
|
}
|
|
return !!(/^[/\\.]|^[a-zA-Z]:/g.exec((uriOrPath || '').toString()));
|
|
}
|
|
return false;
|
|
}
|