Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
5f4ea1c
Group the IFileSystem methods.
ericsnowcurrently Dec 10, 2019
adc6b11
Group FileSystem methods.
ericsnowcurrently Dec 10, 2019
7911537
Make the FS tests "functional".
ericsnowcurrently Dec 10, 2019
2069cef
Drop IFileSystem.objectExists.
ericsnowcurrently Dec 10, 2019
64171ff
Re-organize tests.
ericsnowcurrently Dec 10, 2019
3917ce5
Fix up the unit tests.
ericsnowcurrently Dec 11, 2019
5ebb737
Support numbers in chmod().
ericsnowcurrently Dec 11, 2019
8db3cdb
Add a CleanupFixture and use it in FSFixture.
ericsnowcurrently Dec 11, 2019
f0f565a
Unrevert a bunch of functional tests.
ericsnowcurrently Dec 11, 2019
bf21a43
Add some missing blank lines.
ericsnowcurrently Dec 11, 2019
e28dea1
Alias vscode's FileType and FileStat locally.
ericsnowcurrently Dec 11, 2019
a5f0fef
Do not depend on IFileSystem in the old debug adapter.
ericsnowcurrently Oct 22, 2019
b441de6
Excise IPlatformService from the old debug adapter.
ericsnowcurrently Oct 23, 2019
fb4a2d2
Fix the failing tests.
ericsnowcurrently Dec 11, 2019
f32d5f3
Drop appendFileSync().
ericsnowcurrently Dec 11, 2019
5a41b81
Add tests for appendFile().
ericsnowcurrently Dec 12, 2019
2b5e1b6
Drop unneeded TODO comments.
ericsnowcurrently Dec 12, 2019
4f9060e
Add tests for *Exists().
ericsnowcurrently Dec 12, 2019
7a11004
Add tests for createReadStream().
ericsnowcurrently Dec 12, 2019
1043045
Add tests for listdir().
ericsnowcurrently Dec 12, 2019
b3ea9ac
Add tests for lstat().
ericsnowcurrently Dec 12, 2019
522d297
Add tests for getRealPath().
ericsnowcurrently Dec 12, 2019
2b9aed3
Add tests for move().
ericsnowcurrently Dec 12, 2019
8805265
Drop an accidental change.
ericsnowcurrently Dec 17, 2019
a69513b
lint
ericsnowcurrently Dec 17, 2019
1c2cbcf
Be more careful about what getFiles() returns.
ericsnowcurrently Dec 19, 2019
8971d2e
Drop TODO items for things VS Code has fixed.
ericsnowcurrently Dec 19, 2019
5c1a7e9
Make sure stat() handles sockets properly.
ericsnowcurrently Dec 19, 2019
b83a92a
Add a doc comment for a helper function.
ericsnowcurrently Dec 30, 2019
b73384f
Fix a typo.
ericsnowcurrently Dec 31, 2019
679623c
Fix a recently added debugger test.
ericsnowcurrently Dec 31, 2019
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
319 changes: 196 additions & 123 deletions src/client/common/platform/fileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,54 +10,131 @@ import { inject, injectable } from 'inversify';
import * as path from 'path';
import * as tmp from 'tmp';
import { promisify } from 'util';
import { FileStat } from 'vscode';
import { createDeferred } from '../utils/async';
import { noop } from '../utils/misc';
import { IFileSystem, IPlatformService, TemporaryFile } from './types';
import { FileStat, FileType, IFileSystem, IPlatformService, TemporaryFile } from './types';

const globAsync = promisify(glob);

// This helper function determines the file type of the given stats
// object. The type follows the convention of node's fs module, where
// a file has exactly one type. Symlinks are not resolved.
function convertFileType(stat: fs.Stats): FileType {
if (stat.isFile()) {
return FileType.File;
} else if (stat.isDirectory()) {
return FileType.Directory;
} else if (stat.isSymbolicLink()) {
// The caller is responsible for combining this ("logical or")
// with File or Directory as necessary.
return FileType.SymbolicLink;
} else {
return FileType.Unknown;
}
Comment thread
DonJayamanne marked this conversation as resolved.
}

async function getFileType(filename: string): Promise<FileType> {
let stat: fs.Stats;
try {
// Note that we used to use stat() here instead of lstat().
// This shouldn't matter because the only consumers were
// internal methods that have been updated appropriately.
stat = await fs.lstat(filename);
} catch {
return FileType.Unknown;
}
if (!stat.isSymbolicLink()) {
return convertFileType(stat);
}

// For symlinks we emulate the behavior of the vscode.workspace.fs API.
// See: https://code.visualstudio.com/api/references/vscode-api#FileType
try {
stat = await fs.stat(filename);
} catch {
return FileType.SymbolicLink;
}
if (stat.isFile()) {
return FileType.SymbolicLink | FileType.File;
} else if (stat.isDirectory()) {
return FileType.SymbolicLink | FileType.Directory;
} else {
return FileType.SymbolicLink;
}
}

export function convertStat(old: fs.Stats, filetype: FileType): FileStat {
return {
type: filetype,
size: old.size,
// FileStat.ctime and FileStat.mtime only have 1-millisecond
// resolution, while node provides nanosecond resolution. So
// for now we round to the nearest integer.
// See: https://github.com/microsoft/vscode/issues/84526
ctime: Math.round(old.ctimeMs),
mtime: Math.round(old.mtimeMs)
};
}

@injectable()
export class FileSystem implements IFileSystem {
constructor(@inject(IPlatformService) private platformService: IPlatformService) {}
constructor(
@inject(IPlatformService) private platformService: IPlatformService
) { }

//=================================
// path-related

public get directorySeparatorChar(): string {
return path.sep;
}

public arePathsSame(path1: string, path2: string): boolean {
path1 = path.normalize(path1);
path2 = path.normalize(path2);
if (this.platformService.isWindows) {
return path1.toUpperCase() === path2.toUpperCase();
} else {
return path1 === path2;
}
}

public getRealPath(filePath: string): Promise<string> {
return new Promise<string>(resolve => {
fs.realpath(filePath, (err, realPath) => {
resolve(err ? filePath : realPath);
});
});
}

//=================================
// "raw" operations

public async stat(filePath: string): Promise<FileStat> {
// Do not import vscode directly, as this isn't available in the Debugger Context.
// If stat is used in debugger context, it will fail, however theres a separate PR that will resolve this.
// tslint:disable-next-line: no-require-imports
const vscode = require('vscode');
// Note that, prior to the November release of VS Code,
// stat.ctime was always 0.
// See: https://github.com/microsoft/vscode/issues/84525
return vscode.workspace.fs.stat(vscode.Uri.file(filePath));
}

public objectExists(filePath: string, statCheck: (s: fs.Stats) => boolean): Promise<boolean> {
return new Promise<boolean>(resolve => {
fs.stat(filePath, (error, stats) => {
if (error) {
return resolve(false);
}
return resolve(statCheck(stats));
});
});
public async lstat(filename: string): Promise<FileStat> {
const stat = await fs.lstat(filename);
// Note that, unlike stat(), lstat() does not include the type
// of the symlink's target.
const fileType = convertFileType(stat);
return convertStat(stat, fileType);
Comment thread
DonJayamanne marked this conversation as resolved.
}

public fileExists(filePath: string): Promise<boolean> {
return this.objectExists(filePath, stats => stats.isFile());
}
public fileExistsSync(filePath: string): boolean {
return fs.existsSync(filePath);
}
/**
* Reads the contents of the file using utf8 and returns the string contents.
* @param {string} filePath
* @returns {Promise<string>}
* @memberof FileSystem
*/
// Return the UTF8-decoded text of the file.
public readFile(filePath: string): Promise<string> {
return fs.readFile(filePath, 'utf8');
}
public readFileSync(filePath: string): string {
return fs.readFileSync(filePath, 'utf8');
}
public readData(filePath: string): Promise<Buffer> {
return fs.readFile(filePath);
}
Expand All @@ -66,10 +143,6 @@ export class FileSystem implements IFileSystem {
await fs.writeFile(filePath, data, options);
}

public directoryExists(filePath: string): Promise<boolean> {
return this.objectExists(filePath, stats => stats.isDirectory());
}

public createDirectory(directoryPath: string): Promise<void> {
return fs.mkdirp(directoryPath);
}
Expand All @@ -80,79 +153,20 @@ export class FileSystem implements IFileSystem {
return deferred.promise;
}

public async listdir(root: string): Promise<string[]> {
return new Promise<string[]>(resolve => {
// Now look for Interpreters in this directory
fs.readdir(root, (err, names) => {
if (err) {
return resolve([]);
}
resolve(names.map(name => path.join(root, name)));
});
});
}

public getSubDirectories(rootDir: string): Promise<string[]> {
return new Promise<string[]>(resolve => {
fs.readdir(rootDir, async (error, files) => {
if (error) {
return resolve([]);
}
const subDirs = (await Promise.all(
files.map(async name => {
const fullPath = path.join(rootDir, name);
try {
if ((await fs.stat(fullPath)).isDirectory()) {
return fullPath;
}
// tslint:disable-next-line:no-empty
} catch (ex) {}
})
)).filter(dir => dir !== undefined) as string[];
resolve(subDirs);
public async listdir(dirname: string): Promise<[string, FileType][]> {
const files = await fs.readdir(dirname);
const promises = files
.map(async basename => {
const filename = path.join(dirname, basename);
const fileType = await getFileType(filename);
return [filename, fileType] as [string, FileType];
});
});
}

public async getFiles(rootDir: string): Promise<string[]> {
const files = await fs.readdir(rootDir);
return files.filter(async f => {
const fullPath = path.join(rootDir, f);
if ((await fs.stat(fullPath)).isFile()) {
return true;
}
return false;
});
}

public arePathsSame(path1: string, path2: string): boolean {
path1 = path.normalize(path1);
path2 = path.normalize(path2);
if (this.platformService.isWindows) {
return path1.toUpperCase() === path2.toUpperCase();
} else {
return path1 === path2;
}
return Promise.all(promises);
}

public appendFile(filename: string, data: {}): Promise<void> {
return fs.appendFile(filename, data);
}
public appendFileSync(filename: string, data: {}, encoding: string): void;
public appendFileSync(filename: string, data: {}, options?: { encoding?: string; mode?: number; flag?: string }): void;
// tslint:disable-next-line:unified-signatures
public appendFileSync(filename: string, data: {}, options?: { encoding?: string; mode?: string; flag?: string }): void;
public appendFileSync(filename: string, data: {}, optionsOrEncoding: {}): void {
return fs.appendFileSync(filename, data, optionsOrEncoding);
}

public getRealPath(filePath: string): Promise<string> {
return new Promise<string>(resolve => {
fs.realpath(filePath, (err, realPath) => {
resolve(err ? filePath : realPath);
});
});
}

public copyFile(src: string, dest: string): Promise<void> {
const deferred = createDeferred<void>();
Expand All @@ -177,6 +191,90 @@ export class FileSystem implements IFileSystem {
return deferred.promise;
}

public chmod(filePath: string, mode: string | number): Promise<void> {
return new Promise<void>((resolve, reject) => {
fileSystem.chmod(filePath, mode, (err: NodeJS.ErrnoException | null) => {
if (err) {
return reject(err);
}
resolve();
});
});
}

public async move(src: string, tgt: string) {
await fs.rename(src, tgt);
}

public createReadStream(filePath: string): fileSystem.ReadStream {
return fileSystem.createReadStream(filePath);
}

public createWriteStream(filePath: string): fileSystem.WriteStream {
return fileSystem.createWriteStream(filePath);
}

//=================================
// utils

public objectExists(filePath: string, statCheck: (s: fs.Stats) => boolean): Promise<boolean> {
return new Promise<boolean>(resolve => {
// Note that we are using stat() rather than lstat(). This
// means that any symlinks are getting resolved.
fs.stat(filePath, (error, stats) => {
if (error) {
return resolve(false);
}
return resolve(statCheck(stats));
});
});
}
public fileExists(filePath: string): Promise<boolean> {
return this.objectExists(filePath, stats => stats.isFile());
}
public fileExistsSync(filePath: string): boolean {
return fs.existsSync(filePath);
}
public directoryExists(filePath: string): Promise<boolean> {
return this.objectExists(filePath, stats => stats.isDirectory());
}

public async getSubDirectories(dirname: string): Promise<string[]> {
let files: [string, FileType][];
try {
files = await this.listdir(dirname);
} catch {
// We're only preserving pre-existng behavior here...
return [];
}
return files
.filter(([_file, fileType]) => {
// We preserve the pre-existing behavior of following
// symlinks.
return (fileType & FileType.Directory) > 0;
})
.map(([filename, _ft]) => filename);
}
public async getFiles(dirname: string): Promise<string[]> {
let files: [string, FileType][];
try {
files = await this.listdir(dirname);
} catch (err) {
// This matches what getSubDirectories() does.
if (!await fs.pathExists(dirname)) {
return [];
}
throw err; // re-throw
}
return files
.filter(([_file, fileType]) => {
// We preserve the pre-existing behavior of following
// symlinks.
return (fileType & FileType.File) > 0;
})
.map(([filename, _ft]) => filename);
}

public getFileHash(filePath: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
fs.lstat(filePath, (err, stats) => {
Expand All @@ -191,6 +289,7 @@ export class FileSystem implements IFileSystem {
});
});
}

public async search(globPattern: string, cwd?: string): Promise<string[]> {
let found: string[];
if (cwd) {
Expand All @@ -203,6 +302,7 @@ export class FileSystem implements IFileSystem {
}
return Array.isArray(found) ? found : [];
}

public createTemporaryFile(extension: string): Promise<TemporaryFile> {
return new Promise<TemporaryFile>((resolve, reject) => {
tmp.file({ postfix: extension }, (err, tmpFile, _, cleanupCallback) => {
Expand All @@ -214,33 +314,6 @@ export class FileSystem implements IFileSystem {
});
}

public createReadStream(filePath: string): fileSystem.ReadStream {
return fileSystem.createReadStream(filePath);
}

public createWriteStream(filePath: string): fileSystem.WriteStream {
return fileSystem.createWriteStream(filePath);
}

public chmod(filePath: string, mode: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
fileSystem.chmod(filePath, mode, (err: NodeJS.ErrnoException | null) => {
if (err) {
return reject(err);
}
resolve();
});
});
}

public readFileSync(filePath: string): string {
return fs.readFileSync(filePath, 'utf8');
}

public async move(src: string, tgt: string) {
await fs.rename(src, tgt);
}

public async isDirReadonly(dirname: string): Promise<boolean> {
const filePath = `${dirname}${path.sep}___vscpTest___`;
return new Promise<boolean>(resolve => {
Expand Down
Loading