in packages/core/src/shared/fs/fs.ts [254:568]
reasonDesc: getTelemetryReasonDesc(e),
})
getLogger().warn(`writeFile atomic Node failed for, ${uri.fsPath}, with %O`, e)
// The atomic rename techniques were not successful, so we will
// just resort to regular a non-atomic write
}
} finally {
// clean up temp file since it possibly remains
if (await fs.exists(tempFile)) {
await fs.delete(tempFile)
}
}
}
await write(uri)
}
/**
* Rename (move) a file or folder.
* @param oldPath
* @param newPath
* @returns
*/
async rename(oldPath: vscode.Uri | string, newPath: vscode.Uri | string) {
const oldUri = toUri(oldPath)
const newUri = toUri(newPath)
const errHandler = createPermissionsErrorHandler(this.isWeb, oldUri, 'rw*')
/**
* We were seeing 'FileNotFound' errors during renames, even though we did a `writeFile()` right before the rename.
* The error looks to be from here: https://github.com/microsoft/vscode/blob/09d5f4efc5089ce2fc5c8f6aeb51d728d7f4e758/src/vs/platform/files/node/diskFileSystemProvider.ts#L747
* So a guess is that the exists()(stat() under the hood) call needs to be retried since there may be a race condition.
*/
let attempts = 0
const isExists = await waitUntil(async () => {
const result = await fs.exists(oldUri)
attempts += 1
return result
}, FileSystem.renameTimeoutOpts)
// TODO: Move the `ide_fileSystem` or some variation of this metric in to common telemetry
// TODO: Deduplicate the `ide_fileSystem` call. Maybe have a method that all operations pass through which wraps the call in telemetry.
// Then look to report telemetry failure events.
const scrubbedPath = scrubNames(oldUri.fsPath)
if (!isExists) {
// source path never existed after multiple attempts.
// Either the file never existed, or had we waited longer it would have. We won't know.
telemetry.ide_fileSystem.emit({
result: 'Failed',
action: 'rename',
reason: 'SourceNotExists',
reasonDesc: `After ${FileSystem.renameTimeoutOpts.timeout}ms the source path did not exist: ${scrubbedPath}`,
})
} else if (attempts > 1) {
// Indicates that rename() would have failed if we had not waited for it to exist.
telemetry.ide_fileSystem.emit({
result: 'Succeeded',
action: 'rename',
reason: 'RenameRaceCondition',
reasonDesc: `After multiple attempts the source path existed: ${scrubbedPath}`,
attempts: attempts,
})
}
return vfs.rename(oldUri, newUri, { overwrite: true }).then(undefined, errHandler)
}
/**
* It looks like scenario of a failed rename is rare,
* so we can afford to have a longer timeout and interval.
*
* These values are an arbitrary guess to how long it takes
* for a newly created file to be visible on the filesystem.
*/
static readonly renameTimeoutOpts = {
timeout: 10_000,
interval: 300,
} as const
/**
* The stat of the file, throws if the file does not exist or on any other error.
*/
async stat(uri: vscode.Uri | string): Promise<vscode.FileStat> {
const path = toUri(uri)
return await vfs.stat(path)
}
/**
* Change permissions on file. Note that this will do nothing on browser.
* @param uri file whose permissions should be set.
* @param mode new permissions in octal notation.
* More info: https://nodejs.org/api/fs.html#fspromiseschmodpath-mode
* Examples: https://www.geeksforgeeks.org/node-js-fspromises-chmod-method/
*/
async chmod(uri: vscode.Uri | string, mode: number): Promise<void> {
if (!this.isWeb) {
const path = toUri(uri)
await chmod(path.fsPath, mode)
}
}
/**
* Deletes a file or directory. It is not an error if the file/directory does not exist, unless
* its parent directory is not listable (executable).
*
* @param fileOrDir Path to file or directory
* @param opt Options.
* - `recursive`: forcefully delete a directory. Use `recursive:false` (the default) to prevent
* accidentally deleting a directory when a file is expected.
* - `force`: ignore "not found" errors. Defaults to true if `recursive:true`, else defaults to
* false.
*/
async delete(fileOrDir: string | vscode.Uri, opt_?: { recursive?: boolean; force?: boolean }): Promise<void> {
const opt = { ...opt_, recursive: !!opt_?.recursive }
opt.force = opt.force === false ? opt.force : !!(opt.force || opt.recursive)
const uri = toUri(fileOrDir)
const parent = vscode.Uri.joinPath(uri, '..')
const errHandler = createPermissionsErrorHandler(this.isWeb, parent, '*wx')
if (opt.recursive) {
// Error messages may be misleading if using the `recursive` option.
// Need to implement our own recursive delete if we want detailed info.
return vfs.delete(uri, opt).then(undefined, (err) => {
if (!opt.force || !isFileNotFoundError(err)) {
throw err
}
// Else: ignore "not found" error.
})
}
return vfs.delete(uri, opt).then(undefined, async (err) => {
const notFound = isFileNotFoundError(err)
if (notFound && opt.force) {
return // Ignore "not found" error.
} else if (this.isWeb || isPermissionsError(err)) {
throw await errHandler(err)
} else if (uri.scheme !== 'file' || (!isWin() && !notFound)) {
throw err
} else {
// Try to build a more detailed "not found" error.
// if (isMinVscode('1.80.0') && notFound) {
// return // Old Nodejs does not have constants.S_IXUSR.
// }
// Attempting to delete a file in a non-executable directory results in ENOENT.
// But this might not be true. The file could exist, we just don't know about it.
// Note: Windows has no "x" (executable) flag.
const parentStat = await nodefs.stat(parent.fsPath).catch(() => {
throw err
})
const isParentExecutable = isWin() || !!(parentStat.mode & nodeConstants.S_IXUSR)
if (!isParentExecutable) {
const userInfo = this.getUserInfo()
throw new PermissionsError(parent, parentStat, userInfo, '*wx', err)
} else if (notFound) {
return
}
}
throw err
})
}
async readdir(uri: vscode.Uri | string): Promise<[string, vscode.FileType][]> {
return await vfs.readDirectory(toUri(uri))
}
/**
* Copy target file or directory
* @param source
* @param target
* @returns
*/
async copy(source: vscode.Uri | string, target: vscode.Uri | string): Promise<void> {
const sourcePath = toUri(source)
const targetPath = toUri(target)
return await vfs.copy(sourcePath, targetPath, { overwrite: true })
}
/**
* Checks if the current user has _at least_ the specified permissions.
*
* This throws {@link PermissionsError} when permissions are insufficient.
*/
async checkPerms(file: string | vscode.Uri, perms: PermissionsTriplet): Promise<void> {
// TODO: implement checkExactPerms() by checking the file mode.
// public static async checkExactPerms(file: string | vscode.Uri, perms: `${PermissionsTriplet}${PermissionsTriplet}${PermissionsTriplet}`)
const uri = toUri(file)
const errHandler = createPermissionsErrorHandler(this.isWeb, uri, perms)
const flags = Array.from(perms) as (keyof typeof this.modeMap)[]
const mode = flags.reduce((m, f) => m | this.modeMap[f], nodeConstants.F_OK)
return nodefs.access(uri.fsPath, mode).catch(errHandler)
}
/**
* Returns the file or directory location given by `envVar` if it is non-empty and the location is valid (exists) on the filesystem.
*
* Special case: if 'HOMEPATH' is given then $HOMEDRIVE is prepended to it.
*
* Throws an exception if the env var path is non-empty but invalid.
*
* @param envVar Environment variable name
* @param kind Expect a valid file, directory, or either.
*/
async tryGetFilepathEnvVar(envVar: string, kind: vscode.FileType | undefined): Promise<string | undefined> {
let envVal = process.env[envVar]
if (envVal) {
// Special case: Windows $HOMEPATH depends on $HOMEDRIVE.
if (envVar === 'HOMEPATH') {
const homeDrive = process.env.HOMEDRIVE || 'C:'
envVal = _path.join(homeDrive, envVal)
}
// Expand "~/" to home dir.
const f = resolvePath(envVal, this.#homeDir ?? 'UNKNOWN-HOME')
if (await fs.exists(f, kind)) {
return f
}
throw new ToolkitError(`\$${envVar} filepath is invalid: "${f}"`)
}
}
/**
* Initializes the FileSystem object. Resolves the user's home directory and validates related
* environment variables. Should be called:
* 1. at startup after all env vars are set
* 2. whenver env vars change
*
* @param onFail Invoked if a valid home directory could not be resolved
* @returns List of error messages if any invalid env vars were found.
*/
async init(
extContext: vscode.ExtensionContext,
onFail: (homeDir: string) => void,
osUserInfo?: typeof os.userInfo
): Promise<string[]> {
this.#username = undefined
this.#osUserInfo = osUserInfo ?? os.userInfo
if (this.isWeb) {
// When in browser we cannot access the users desktop file system.
// Instead, VS Code provided uris will use the browsers storage.
// IMPORTANT: we must preserve the scheme of this URI or else VS Code
// will incorrectly interpret the path.
this.#homeDir = extContext.globalStorageUri.toString()
return []
}
/** Logger may not be available during startup, so messages are stored here. */
const logMsgs: string[] = []
function logErr(e: unknown): undefined {
logMsgs.push((e as Error).message)
return undefined
}
const tryGet = (envName: string) => {
return this.tryGetFilepathEnvVar(envName, vscode.FileType.Directory).catch(logErr)
}
let p: string | undefined
if ((p = await tryGet('HOME'))) {
this.#homeDir = p
} else if ((p = await tryGet('USERPROFILE'))) {
this.#homeDir = p
} else if ((p = await tryGet('HOMEPATH'))) {
this.#homeDir = p
} else {
this.#homeDir = os.homedir()
}
// If $HOME is bogus, os.homedir() will still return it! All we can do is show an error.
if (!(await fs.exists(this.#homeDir, vscode.FileType.Directory))) {
onFail(this.#homeDir)
}
return logMsgs
}
/**
* Gets the (cached) user home directory path.
*
* To update the cached value (e.g. after environment variables changed), call {@link init()}.
*/
getUserHomeDir(): string {
if (!this.#homeDir) {
throw new Error('call fs.init() before using fs.getHomeDirectory()')
}
return this.#homeDir
}
/**
* Gets the application cache folder for the current platform
*
* Follows the cache_dir convention outlined in https://crates.io/crates/dirs
*/
getCacheDir(): string {
if (isWeb()) {
const homeDir = this.#homeDir
if (!homeDir) {
throw new ToolkitError('Web home directory not found', {
code: 'WebHomeDirectoryNotFound',
})
}
return homeDir
}
switch (process.platform) {
case 'darwin': {
return _path.join(this.getUserHomeDir(), 'Library/Caches')
}
case 'win32': {
const localAppData = process.env.LOCALAPPDATA
if (!localAppData) {
throw new ToolkitError('LOCALAPPDATA environment variable not set', {