@@ -10,54 +10,131 @@ import { inject, injectable } from 'inversify';
10
10
import * as path from 'path' ;
11
11
import * as tmp from 'tmp' ;
12
12
import { promisify } from 'util' ;
13
- import { FileStat } from 'vscode' ;
14
13
import { createDeferred } from '../utils/async' ;
15
14
import { noop } from '../utils/misc' ;
16
- import { IFileSystem , IPlatformService , TemporaryFile } from './types' ;
15
+ import { FileStat , FileType , IFileSystem , IPlatformService , TemporaryFile } from './types' ;
17
16
18
17
const globAsync = promisify ( glob ) ;
19
18
19
+ // This helper function determines the file type of the given stats
20
+ // object. The type follows the convention of node's fs module, where
21
+ // a file has exactly one type. Symlinks are not resolved.
22
+ function convertFileType ( stat : fs . Stats ) : FileType {
23
+ if ( stat . isFile ( ) ) {
24
+ return FileType . File ;
25
+ } else if ( stat . isDirectory ( ) ) {
26
+ return FileType . Directory ;
27
+ } else if ( stat . isSymbolicLink ( ) ) {
28
+ // The caller is responsible for combining this ("logical or")
29
+ // with File or Directory as necessary.
30
+ return FileType . SymbolicLink ;
31
+ } else {
32
+ return FileType . Unknown ;
33
+ }
34
+ }
35
+
36
+ async function getFileType ( filename : string ) : Promise < FileType > {
37
+ let stat : fs . Stats ;
38
+ try {
39
+ // Note that we used to use stat() here instead of lstat().
40
+ // This shouldn't matter because the only consumers were
41
+ // internal methods that have been updated appropriately.
42
+ stat = await fs . lstat ( filename ) ;
43
+ } catch {
44
+ return FileType . Unknown ;
45
+ }
46
+ if ( ! stat . isSymbolicLink ( ) ) {
47
+ return convertFileType ( stat ) ;
48
+ }
49
+
50
+ // For symlinks we emulate the behavior of the vscode.workspace.fs API.
51
+ // See: https://code.visualstudio.com/api/references/vscode-api#FileType
52
+ try {
53
+ stat = await fs . stat ( filename ) ;
54
+ } catch {
55
+ return FileType . SymbolicLink ;
56
+ }
57
+ if ( stat . isFile ( ) ) {
58
+ return FileType . SymbolicLink | FileType . File ;
59
+ } else if ( stat . isDirectory ( ) ) {
60
+ return FileType . SymbolicLink | FileType . Directory ;
61
+ } else {
62
+ return FileType . SymbolicLink ;
63
+ }
64
+ }
65
+
66
+ export function convertStat ( old : fs . Stats , filetype : FileType ) : FileStat {
67
+ return {
68
+ type : filetype ,
69
+ size : old . size ,
70
+ // FileStat.ctime and FileStat.mtime only have 1-millisecond
71
+ // resolution, while node provides nanosecond resolution. So
72
+ // for now we round to the nearest integer.
73
+ // See: https://github.com/microsoft/vscode/issues/84526
74
+ ctime : Math . round ( old . ctimeMs ) ,
75
+ mtime : Math . round ( old . mtimeMs )
76
+ } ;
77
+ }
78
+
20
79
@injectable ( )
21
80
export class FileSystem implements IFileSystem {
22
- constructor ( @inject ( IPlatformService ) private platformService : IPlatformService ) { }
81
+ constructor (
82
+ @inject ( IPlatformService ) private platformService : IPlatformService
83
+ ) { }
84
+
85
+ //=================================
86
+ // path-related
23
87
24
88
public get directorySeparatorChar ( ) : string {
25
89
return path . sep ;
26
90
}
91
+
92
+ public arePathsSame ( path1 : string , path2 : string ) : boolean {
93
+ path1 = path . normalize ( path1 ) ;
94
+ path2 = path . normalize ( path2 ) ;
95
+ if ( this . platformService . isWindows ) {
96
+ return path1 . toUpperCase ( ) === path2 . toUpperCase ( ) ;
97
+ } else {
98
+ return path1 === path2 ;
99
+ }
100
+ }
101
+
102
+ public getRealPath ( filePath : string ) : Promise < string > {
103
+ return new Promise < string > ( resolve => {
104
+ fs . realpath ( filePath , ( err , realPath ) => {
105
+ resolve ( err ? filePath : realPath ) ;
106
+ } ) ;
107
+ } ) ;
108
+ }
109
+
110
+ //=================================
111
+ // "raw" operations
112
+
27
113
public async stat ( filePath : string ) : Promise < FileStat > {
28
114
// Do not import vscode directly, as this isn't available in the Debugger Context.
29
115
// If stat is used in debugger context, it will fail, however theres a separate PR that will resolve this.
30
116
// tslint:disable-next-line: no-require-imports
31
117
const vscode = require ( 'vscode' ) ;
118
+ // Note that, prior to the November release of VS Code,
119
+ // stat.ctime was always 0.
120
+ // See: https://github.com/microsoft/vscode/issues/84525
32
121
return vscode . workspace . fs . stat ( vscode . Uri . file ( filePath ) ) ;
33
122
}
34
-
35
- public objectExists ( filePath : string , statCheck : ( s : fs . Stats ) => boolean ) : Promise < boolean > {
36
- return new Promise < boolean > ( resolve => {
37
- fs . stat ( filePath , ( error , stats ) => {
38
- if ( error ) {
39
- return resolve ( false ) ;
40
- }
41
- return resolve ( statCheck ( stats ) ) ;
42
- } ) ;
43
- } ) ;
123
+ public async lstat ( filename : string ) : Promise < FileStat > {
124
+ const stat = await fs . lstat ( filename ) ;
125
+ // Note that, unlike stat(), lstat() does not include the type
126
+ // of the symlink's target.
127
+ const fileType = convertFileType ( stat ) ;
128
+ return convertStat ( stat , fileType ) ;
44
129
}
45
130
46
- public fileExists ( filePath : string ) : Promise < boolean > {
47
- return this . objectExists ( filePath , stats => stats . isFile ( ) ) ;
48
- }
49
- public fileExistsSync ( filePath : string ) : boolean {
50
- return fs . existsSync ( filePath ) ;
51
- }
52
- /**
53
- * Reads the contents of the file using utf8 and returns the string contents.
54
- * @param {string } filePath
55
- * @returns {Promise<string> }
56
- * @memberof FileSystem
57
- */
131
+ // Return the UTF8-decoded text of the file.
58
132
public readFile ( filePath : string ) : Promise < string > {
59
133
return fs . readFile ( filePath , 'utf8' ) ;
60
134
}
135
+ public readFileSync ( filePath : string ) : string {
136
+ return fs . readFileSync ( filePath , 'utf8' ) ;
137
+ }
61
138
public readData ( filePath : string ) : Promise < Buffer > {
62
139
return fs . readFile ( filePath ) ;
63
140
}
@@ -66,10 +143,6 @@ export class FileSystem implements IFileSystem {
66
143
await fs . writeFile ( filePath , data , options ) ;
67
144
}
68
145
69
- public directoryExists ( filePath : string ) : Promise < boolean > {
70
- return this . objectExists ( filePath , stats => stats . isDirectory ( ) ) ;
71
- }
72
-
73
146
public createDirectory ( directoryPath : string ) : Promise < void > {
74
147
return fs . mkdirp ( directoryPath ) ;
75
148
}
@@ -80,79 +153,20 @@ export class FileSystem implements IFileSystem {
80
153
return deferred . promise ;
81
154
}
82
155
83
- public async listdir ( root : string ) : Promise < string [ ] > {
84
- return new Promise < string [ ] > ( resolve => {
85
- // Now look for Interpreters in this directory
86
- fs . readdir ( root , ( err , names ) => {
87
- if ( err ) {
88
- return resolve ( [ ] ) ;
89
- }
90
- resolve ( names . map ( name => path . join ( root , name ) ) ) ;
91
- } ) ;
92
- } ) ;
93
- }
94
-
95
- public getSubDirectories ( rootDir : string ) : Promise < string [ ] > {
96
- return new Promise < string [ ] > ( resolve => {
97
- fs . readdir ( rootDir , async ( error , files ) => {
98
- if ( error ) {
99
- return resolve ( [ ] ) ;
100
- }
101
- const subDirs = ( await Promise . all (
102
- files . map ( async name => {
103
- const fullPath = path . join ( rootDir , name ) ;
104
- try {
105
- if ( ( await fs . stat ( fullPath ) ) . isDirectory ( ) ) {
106
- return fullPath ;
107
- }
108
- // tslint:disable-next-line:no-empty
109
- } catch ( ex ) { }
110
- } )
111
- ) ) . filter ( dir => dir !== undefined ) as string [ ] ;
112
- resolve ( subDirs ) ;
156
+ public async listdir ( dirname : string ) : Promise < [ string , FileType ] [ ] > {
157
+ const files = await fs . readdir ( dirname ) ;
158
+ const promises = files
159
+ . map ( async basename => {
160
+ const filename = path . join ( dirname , basename ) ;
161
+ const fileType = await getFileType ( filename ) ;
162
+ return [ filename , fileType ] as [ string , FileType ] ;
113
163
} ) ;
114
- } ) ;
115
- }
116
-
117
- public async getFiles ( rootDir : string ) : Promise < string [ ] > {
118
- const files = await fs . readdir ( rootDir ) ;
119
- return files . filter ( async f => {
120
- const fullPath = path . join ( rootDir , f ) ;
121
- if ( ( await fs . stat ( fullPath ) ) . isFile ( ) ) {
122
- return true ;
123
- }
124
- return false ;
125
- } ) ;
126
- }
127
-
128
- public arePathsSame ( path1 : string , path2 : string ) : boolean {
129
- path1 = path . normalize ( path1 ) ;
130
- path2 = path . normalize ( path2 ) ;
131
- if ( this . platformService . isWindows ) {
132
- return path1 . toUpperCase ( ) === path2 . toUpperCase ( ) ;
133
- } else {
134
- return path1 === path2 ;
135
- }
164
+ return Promise . all ( promises ) ;
136
165
}
137
166
138
167
public appendFile ( filename : string , data : { } ) : Promise < void > {
139
168
return fs . appendFile ( filename , data ) ;
140
169
}
141
- public appendFileSync ( filename : string , data : { } , encoding : string ) : void ;
142
- public appendFileSync ( filename : string , data : { } , options ?: { encoding ?: string ; mode ?: number ; flag ?: string } ) : void ;
143
- // tslint:disable-next-line:unified-signatures
144
- public appendFileSync ( filename : string , data : { } , options ?: { encoding ?: string ; mode ?: string ; flag ?: string } ) : void ;
145
- public appendFileSync ( filename : string , data : { } , optionsOrEncoding : { } ) : void {
146
- return fs . appendFileSync ( filename , data , optionsOrEncoding ) ;
147
- }
148
-
149
- public getRealPath ( filePath : string ) : Promise < string > {
150
- return new Promise < string > ( resolve => {
151
- fs . realpath ( filePath , ( err , realPath ) => {
152
- resolve ( err ? filePath : realPath ) ;
153
- } ) ;
154
- } ) ;
155
- }
156
170
157
171
public copyFile ( src : string , dest : string ) : Promise < void > {
158
172
const deferred = createDeferred < void > ( ) ;
@@ -177,6 +191,90 @@ export class FileSystem implements IFileSystem {
177
191
return deferred . promise ;
178
192
}
179
193
194
+ public chmod ( filePath : string , mode : string | number ) : Promise < void > {
195
+ return new Promise < void > ( ( resolve , reject ) => {
196
+ fileSystem . chmod ( filePath , mode , ( err : NodeJS . ErrnoException | null ) => {
197
+ if ( err ) {
198
+ return reject ( err ) ;
199
+ }
200
+ resolve ( ) ;
201
+ } ) ;
202
+ } ) ;
203
+ }
204
+
205
+ public async move ( src : string , tgt : string ) {
206
+ await fs . rename ( src , tgt ) ;
207
+ }
208
+
209
+ public createReadStream ( filePath : string ) : fileSystem . ReadStream {
210
+ return fileSystem . createReadStream ( filePath ) ;
211
+ }
212
+
213
+ public createWriteStream ( filePath : string ) : fileSystem . WriteStream {
214
+ return fileSystem . createWriteStream ( filePath ) ;
215
+ }
216
+
217
+ //=================================
218
+ // utils
219
+
220
+ public objectExists ( filePath : string , statCheck : ( s : fs . Stats ) => boolean ) : Promise < boolean > {
221
+ return new Promise < boolean > ( resolve => {
222
+ // Note that we are using stat() rather than lstat(). This
223
+ // means that any symlinks are getting resolved.
224
+ fs . stat ( filePath , ( error , stats ) => {
225
+ if ( error ) {
226
+ return resolve ( false ) ;
227
+ }
228
+ return resolve ( statCheck ( stats ) ) ;
229
+ } ) ;
230
+ } ) ;
231
+ }
232
+ public fileExists ( filePath : string ) : Promise < boolean > {
233
+ return this . objectExists ( filePath , stats => stats . isFile ( ) ) ;
234
+ }
235
+ public fileExistsSync ( filePath : string ) : boolean {
236
+ return fs . existsSync ( filePath ) ;
237
+ }
238
+ public directoryExists ( filePath : string ) : Promise < boolean > {
239
+ return this . objectExists ( filePath , stats => stats . isDirectory ( ) ) ;
240
+ }
241
+
242
+ public async getSubDirectories ( dirname : string ) : Promise < string [ ] > {
243
+ let files : [ string , FileType ] [ ] ;
244
+ try {
245
+ files = await this . listdir ( dirname ) ;
246
+ } catch {
247
+ // We're only preserving pre-existng behavior here...
248
+ return [ ] ;
249
+ }
250
+ return files
251
+ . filter ( ( [ _file , fileType ] ) => {
252
+ // We preserve the pre-existing behavior of following
253
+ // symlinks.
254
+ return ( fileType & FileType . Directory ) > 0 ;
255
+ } )
256
+ . map ( ( [ filename , _ft ] ) => filename ) ;
257
+ }
258
+ public async getFiles ( dirname : string ) : Promise < string [ ] > {
259
+ let files : [ string , FileType ] [ ] ;
260
+ try {
261
+ files = await this . listdir ( dirname ) ;
262
+ } catch ( err ) {
263
+ // This matches what getSubDirectories() does.
264
+ if ( ! await fs . pathExists ( dirname ) ) {
265
+ return [ ] ;
266
+ }
267
+ throw err ; // re-throw
268
+ }
269
+ return files
270
+ . filter ( ( [ _file , fileType ] ) => {
271
+ // We preserve the pre-existing behavior of following
272
+ // symlinks.
273
+ return ( fileType & FileType . File ) > 0 ;
274
+ } )
275
+ . map ( ( [ filename , _ft ] ) => filename ) ;
276
+ }
277
+
180
278
public getFileHash ( filePath : string ) : Promise < string > {
181
279
return new Promise < string > ( ( resolve , reject ) => {
182
280
fs . lstat ( filePath , ( err , stats ) => {
@@ -191,6 +289,7 @@ export class FileSystem implements IFileSystem {
191
289
} ) ;
192
290
} ) ;
193
291
}
292
+
194
293
public async search ( globPattern : string , cwd ?: string ) : Promise < string [ ] > {
195
294
let found : string [ ] ;
196
295
if ( cwd ) {
@@ -203,6 +302,7 @@ export class FileSystem implements IFileSystem {
203
302
}
204
303
return Array . isArray ( found ) ? found : [ ] ;
205
304
}
305
+
206
306
public createTemporaryFile ( extension : string ) : Promise < TemporaryFile > {
207
307
return new Promise < TemporaryFile > ( ( resolve , reject ) => {
208
308
tmp . file ( { postfix : extension } , ( err , tmpFile , _ , cleanupCallback ) => {
@@ -214,33 +314,6 @@ export class FileSystem implements IFileSystem {
214
314
} ) ;
215
315
}
216
316
217
- public createReadStream ( filePath : string ) : fileSystem . ReadStream {
218
- return fileSystem . createReadStream ( filePath ) ;
219
- }
220
-
221
- public createWriteStream ( filePath : string ) : fileSystem . WriteStream {
222
- return fileSystem . createWriteStream ( filePath ) ;
223
- }
224
-
225
- public chmod ( filePath : string , mode : string ) : Promise < void > {
226
- return new Promise < void > ( ( resolve , reject ) => {
227
- fileSystem . chmod ( filePath , mode , ( err : NodeJS . ErrnoException | null ) => {
228
- if ( err ) {
229
- return reject ( err ) ;
230
- }
231
- resolve ( ) ;
232
- } ) ;
233
- } ) ;
234
- }
235
-
236
- public readFileSync ( filePath : string ) : string {
237
- return fs . readFileSync ( filePath , 'utf8' ) ;
238
- }
239
-
240
- public async move ( src : string , tgt : string ) {
241
- await fs . rename ( src , tgt ) ;
242
- }
243
-
244
317
public async isDirReadonly ( dirname : string ) : Promise < boolean > {
245
318
const filePath = `${ dirname } ${ path . sep } ___vscpTest___` ;
246
319
return new Promise < boolean > ( resolve => {
0 commit comments