Skip to content

Commit

Permalink
Merge pull request #43815 from Microsoft/isidorn/symbolicLinks
Browse files Browse the repository at this point in the history
show symbolic links as decorations in explorer
  • Loading branch information
isidorn committed Feb 21, 2018
2 parents efa3333 + 5e23401 commit 302fd56
Show file tree
Hide file tree
Showing 11 changed files with 110 additions and 30 deletions.
22 changes: 21 additions & 1 deletion src/vs/base/node/extfs.ts
Expand Up @@ -43,6 +43,26 @@ export function readdir(path: string, callback: (error: Error, files: string[])
return fs.readdir(path, callback);
}

export function statLink(path: string, callback: (error: Error, statAndIsLink: { stat: fs.Stats, isSymbolicLink: boolean }) => void): void {
fs.lstat(path, (error, stat) => {
if (error) {
return callback(error, null);
}

if (stat.isSymbolicLink()) {
fs.stat(path, (error, stat) => {
if (error) {
return callback(error, null);
}

callback(null, { stat, isSymbolicLink: true });
});
} else {
callback(null, { stat, isSymbolicLink: false });
}
});
}

export function copy(source: string, target: string, callback: (error: Error) => void, copiedSources?: { [path: string]: boolean }): void {
if (!copiedSources) {
copiedSources = Object.create(null);
Expand Down Expand Up @@ -628,4 +648,4 @@ export function watch(path: string, onChange: (type: string, path: string) => vo
}

return void 0;
}
}
4 changes: 4 additions & 0 deletions src/vs/base/node/pfs.ts
Expand Up @@ -54,6 +54,10 @@ export function stat(path: string): TPromise<fs.Stats> {
return nfcall(fs.stat, path);
}

export function statLink(path: string): TPromise<{ stat: fs.Stats, isSymbolicLink: boolean }> {
return nfcall(extfs.statLink, path);
}

export function lstat(path: string): TPromise<fs.Stats> {
return nfcall(fs.lstat, path);
}
Expand Down
37 changes: 36 additions & 1 deletion src/vs/base/test/node/extfs/extfs.test.ts
Expand Up @@ -74,6 +74,41 @@ suite('Extfs', () => {
}); // 493 = 0755
});

test('stat link', function (done: () => void) {
const id1 = uuid.generateUuid();
const parentDir = path.join(os.tmpdir(), 'vsctests', id1);
const directory = path.join(parentDir, 'extfs', id1);

const id2 = uuid.generateUuid();
const symbolicLink = path.join(parentDir, 'extfs', id2);

mkdirp(directory, 493, error => {
if (error) {
return onError(error, done);
}

fs.symlinkSync(directory, symbolicLink);

extfs.statLink(directory, (error, statAndIsLink) => {
if (error) {
return onError(error, done);
}

assert.ok(!statAndIsLink.isSymbolicLink);

extfs.statLink(symbolicLink, (error, statAndIsLink) => {
if (error) {
return onError(error, done);
}

assert.ok(statAndIsLink.isSymbolicLink);
extfs.delSync(directory);
done();
});
});
});
});

test('delSync - swallows file not found error', function () {
const id = uuid.generateUuid();
const parentDir = path.join(os.tmpdir(), 'vsctests', id);
Expand Down Expand Up @@ -524,4 +559,4 @@ suite('Extfs', () => {
extfs.del(parentDir, os.tmpdir(), done, ignore);
});
});
});
});
5 changes: 5 additions & 0 deletions src/vs/platform/files/common/files.ts
Expand Up @@ -410,6 +410,11 @@ export interface IFileStat extends IBaseStat {
*/
isDirectory: boolean;

/**
* The resource is a symbolic link.
*/
isSymbolicLink?: boolean;

/**
* The children of the file stat or undefined if none.
*/
Expand Down
12 changes: 9 additions & 3 deletions src/vs/workbench/parts/files/common/explorerModel.ts
Expand Up @@ -75,15 +75,17 @@ export class FileStat implements IFileStat {
public mtime: number;
public etag: string;
private _isDirectory: boolean;
private _isSymbolicLink: boolean;
public children: FileStat[];
public parent: FileStat;

public isDirectoryResolved: boolean;

constructor(resource: URI, public root: FileStat, isDirectory?: boolean, name: string = getPathLabel(resource), mtime?: number, etag?: string) {
constructor(resource: URI, public root: FileStat, isSymbolicLink?: boolean, isDirectory?: boolean, name: string = getPathLabel(resource), mtime?: number, etag?: string) {
this.resource = resource;
this.name = name;
this.isDirectory = !!isDirectory;
this._isSymbolicLink = !!isSymbolicLink;
this.etag = etag;
this.mtime = mtime;

Expand All @@ -94,6 +96,10 @@ export class FileStat implements IFileStat {
this.isDirectoryResolved = false;
}

public get isSymbolicLink(): boolean {
return this._isSymbolicLink;
}

public get isDirectory(): boolean {
return this._isDirectory;
}
Expand Down Expand Up @@ -123,7 +129,7 @@ export class FileStat implements IFileStat {
}

public static create(raw: IFileStat, root: FileStat, resolveTo?: URI[]): FileStat {
const stat = new FileStat(raw.resource, root, raw.isDirectory, raw.name, raw.mtime, raw.etag);
const stat = new FileStat(raw.resource, root, raw.isSymbolicLink, raw.isDirectory, raw.name, raw.mtime, raw.etag);

// Recursively add children if present
if (stat.isDirectory) {
Expand Down Expand Up @@ -316,7 +322,7 @@ export class NewStatPlaceholder extends FileStat {
private directoryPlaceholder: boolean;

constructor(isDirectory: boolean, root: FileStat) {
super(URI.file(''), root, false, '');
super(URI.file(''), root, false, false, '');

this.id = NewStatPlaceholder.ID++;
this.isDirectoryResolved = isDirectory;
Expand Down
Expand Up @@ -31,14 +31,20 @@ export class ExplorerDecorationsProvider implements IDecorationsProvider {
}

provideDecorations(resource: URI): IDecorationData {
const fileStat = this.model.roots.filter(r => r.resource.toString() === resource.toString()).pop();
const fileStat = this.model.findClosest(resource);
if (fileStat && fileStat.nonexistentRoot) {
return {
tooltip: localize('canNotResolve', "Can not resolve workspace folder"),
letter: '!',
color: listInvalidItemForeground,
};
}
if (fileStat && fileStat.isSymbolicLink) {
return {
tooltip: localize('symbolicLlink', "Symbolic Link"),
letter: '\u2937'
};
}

return undefined;
}
Expand Down
Expand Up @@ -14,7 +14,7 @@ import { validateFileName } from 'vs/workbench/parts/files/electron-browser/file
import { FileStat } from 'vs/workbench/parts/files/common/explorerModel';

function createStat(path: string, name: string, isFolder: boolean, hasChildren: boolean, size: number, mtime: number): FileStat {
return new FileStat(toResource(path), undefined, isFolder, name, mtime);
return new FileStat(toResource(path), undefined, false, isFolder, name, mtime);
}

function toResource(path) {
Expand Down Expand Up @@ -239,20 +239,20 @@ suite('Files - View Model', () => {
test('Merge Local with Disk', function () {
const d = new Date().toUTCString();

const merge1 = new FileStat(URI.file(join('C:\\', '/path/to')), undefined, true, 'to', Date.now(), d);
const merge2 = new FileStat(URI.file(join('C:\\', '/path/to')), undefined, true, 'to', Date.now(), new Date(0).toUTCString());
const merge1 = new FileStat(URI.file(join('C:\\', '/path/to')), undefined, false, true, 'to', Date.now(), d);
const merge2 = new FileStat(URI.file(join('C:\\', '/path/to')), undefined, false, true, 'to', Date.now(), new Date(0).toUTCString());

// Merge Properties
FileStat.mergeLocalWithDisk(merge2, merge1);
assert.strictEqual(merge1.mtime, merge2.mtime);

// Merge Child when isDirectoryResolved=false is a no-op
merge2.addChild(new FileStat(URI.file(join('C:\\', '/path/to/foo.html')), undefined, true, 'foo.html', Date.now(), d));
merge2.addChild(new FileStat(URI.file(join('C:\\', '/path/to/foo.html')), undefined, false, true, 'foo.html', Date.now(), d));
FileStat.mergeLocalWithDisk(merge2, merge1);
assert.strictEqual(merge1.children.length, 0);

// Merge Child with isDirectoryResolved=true
const child = new FileStat(URI.file(join('C:\\', '/path/to/foo.html')), undefined, true, 'foo.html', Date.now(), d);
const child = new FileStat(URI.file(join('C:\\', '/path/to/foo.html')), undefined, false, true, 'foo.html', Date.now(), d);
merge2.removeChild(child);
merge2.addChild(child);
merge2.isDirectoryResolved = true;
Expand All @@ -266,4 +266,4 @@ suite('Files - View Model', () => {
FileStat.mergeLocalWithDisk(merge2, merge1);
assert.ok(existingChild === merge1.children[0]);
});
});
});
Expand Up @@ -31,6 +31,7 @@ function toIFileStat(provider: IFileSystemProvider, tuple: [URI, IStat], recurse
const [resource, stat] = tuple;
const fileStat: IFileStat = {
isDirectory: false,
isSymbolicLink: stat.type === FileType.Symlink,
resource: resource,
name: basename(resource.path),
mtime: stat.mtime,
Expand Down
34 changes: 18 additions & 16 deletions src/vs/workbench/services/files/node/fileService.ts
Expand Up @@ -912,8 +912,8 @@ export class FileService implements IFileService {
private toStatResolver(resource: uri): TPromise<StatResolver> {
const absolutePath = this.toAbsolutePath(resource);

return pfs.stat(absolutePath).then(stat => {
return new StatResolver(resource, stat.isDirectory(), stat.mtime.getTime(), stat.size, this.options.verboseLogging ? this.options.errorLogger : void 0);
return pfs.statLink(absolutePath).then(({ isSymbolicLink, stat }) => {
return new StatResolver(resource, isSymbolicLink, stat.isDirectory(), stat.mtime.getTime(), stat.size, this.options.verboseLogging ? this.options.errorLogger : void 0);
});
}

Expand Down Expand Up @@ -1146,25 +1146,21 @@ export class FileService implements IFileService {
}

export class StatResolver {
private resource: uri;
private isDirectory: boolean;
private mtime: number;
private name: string;
private etag: string;
private size: number;
private errorLogger: (error: Error | string) => void;

constructor(resource: uri, isDirectory: boolean, mtime: number, size: number, errorLogger?: (error: Error | string) => void) {
constructor(
private resource: uri,
private isSymbolicLink: boolean,
private isDirectory: boolean,
private mtime: number,
private size: number,
private errorLogger?: (error: Error | string) => void
) {
assert.ok(resource && resource.scheme === Schemas.file, `Invalid resource: ${resource}`);

this.resource = resource;
this.isDirectory = isDirectory;
this.mtime = mtime;
this.name = getBaseLabel(resource);
this.etag = etag(size, mtime);
this.size = size;

this.errorLogger = errorLogger;
}

public resolve(options: IResolveFileOptions): TPromise<IFileStat> {
Expand All @@ -1173,6 +1169,7 @@ export class StatResolver {
const fileStat: IFileStat = {
resource: this.resource,
isDirectory: this.isDirectory,
isSymbolicLink: this.isSymbolicLink,
name: this.name,
etag: this.etag,
size: this.size,
Expand Down Expand Up @@ -1223,6 +1220,7 @@ export class StatResolver {
flow.parallel(files, (file: string, clb: (error: Error, children: IFileStat) => void) => {
const fileResource = uri.file(paths.resolve(absolutePath, file));
let fileStat: fs.Stats;
let isSymbolicLink = false;
const $this = this;

flow.sequence(
Expand All @@ -1235,7 +1233,10 @@ export class StatResolver {
},

function stat(this: any): void {
fs.stat(fileResource.fsPath, this);
extfs.statLink(fileResource.fsPath, (error: Error, statAndIsLink) => {
isSymbolicLink = statAndIsLink.isSymbolicLink;
this(null, statAndIsLink.stat);
});
},

function countChildren(this: any, fsstat: fs.Stats): void {
Expand All @@ -1254,6 +1255,7 @@ export class StatResolver {
const childStat: IFileStat = {
resource: fileResource,
isDirectory: fileStat.isDirectory(),
isSymbolicLink,
name: file,
mtime: fileStat.mtime.getTime(),
etag: etag(fileStat),
Expand Down Expand Up @@ -1293,4 +1295,4 @@ export class StatResolver {
});
});
}
}
}
4 changes: 2 additions & 2 deletions src/vs/workbench/services/files/test/node/resolver.test.ts
Expand Up @@ -19,7 +19,7 @@ function create(relativePath: string): StatResolver {
let absolutePath = relativePath ? path.join(basePath, relativePath) : basePath;
let fsStat = fs.statSync(absolutePath);

return new StatResolver(uri.file(absolutePath), fsStat.isDirectory(), fsStat.mtime.getTime(), fsStat.size, void 0);
return new StatResolver(uri.file(absolutePath), fsStat.isSymbolicLink(), fsStat.isDirectory(), fsStat.mtime.getTime(), fsStat.size, void 0);
}

function toResource(relativePath: string): uri {
Expand Down Expand Up @@ -183,4 +183,4 @@ suite('Stat Resolver', () => {
})
.done(() => done(), done);
});
});
});
Expand Up @@ -390,6 +390,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
mtime: content.mtime,
etag: content.etag,
isDirectory: false,
isSymbolicLink: false,
children: void 0
};
this.updateLastResolvedDiskStat(resolvedStat);
Expand Down

0 comments on commit 302fd56

Please sign in to comment.