From c84cc723836e5707b7ae69f1a702db58542166e3 Mon Sep 17 00:00:00 2001 From: yetao Date: Wed, 30 Oct 2024 16:15:30 +0800 Subject: [PATCH] =?UTF-8?q?refactor=F0=9F=8E=A8:=20=20(=E9=98=85=E8=AF=BB?= =?UTF-8?q?=E4=BB=A3=E7=A0=81)=EF=BC=9AsshFileSystem.ts=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/sshFileSystem.ts | 381 ++++++++++++++++++++++++++++++++----------- 1 file changed, 289 insertions(+), 92 deletions(-) diff --git a/src/sshFileSystem.ts b/src/sshFileSystem.ts index 39b283f..d2a98a2 100644 --- a/src/sshFileSystem.ts +++ b/src/sshFileSystem.ts @@ -1,221 +1,418 @@ +// 导入 FileSystemConfig 类型,该类型定义了文件系统的配置 import type { FileSystemConfig } from 'common/fileSystemConfig'; +// 导入 path 模块,用于处理和转换文件路径 import * as path from 'path'; +// 导入 ssh2 模块,用于实现 SSH 连接和文件传输 import type * as ssh2 from 'ssh2'; +// 导入 vscode 模块,用于与 VS Code 编辑器进行交互 import * as vscode from 'vscode'; +// 导入 FlagValue 类型和相关函数,用于处理全局标志 import { FlagValue, getFlag, subscribeToGlobalFlags } from './flags'; +// 导入 Logger、Logging 类型和相关常量,用于日志记录 import { Logger, Logging, LOGGING_NO_STACKTRACE, LOGGING_SINGLE_LINE_STACKTRACE, withStacktraceOffset } from './logging'; +// 导入 toPromise 函数,用于将回调函数转换为 Promise import { toPromise } from './utils'; + // This makes it report a single line of the stacktrace of where the e.g. logger.info() call happened // while also making it that if we're logging an error, only the first 4 lines of the stack (including the error message) are shown // (usually the errors we report on happen deep inside ssh2 or ssh2-streams, we don't really care that much about it) +// 配置错误报告的堆栈跟踪为单行,并限制行数为 4 const LOGGING_HANDLE_ERROR = withStacktraceOffset(1, { ...LOGGING_SINGLE_LINE_STACKTRACE, maxErrorStack: 4 }); // All absolute paths (relative to the FS root or a workspace root) // If it ends with /, .startsWith is used, otherwise a raw equal +// 定义一个数组,包含在文件系统操作中应该忽略的文件或目录路径 const IGNORE_NOT_FOUND: string[] = [ + // Visual Studio Code 配置文件夹 '/.vscode', + // Visual Studio Code 配置文件夹(带斜杠) '/.vscode/', + // Git 版本控制文件夹 '/.git/', + // Node.js 模块文件夹 '/node_modules', + // Maven 项目配置文件 '/pom.xml', + // Android 项目主目录下的 AndroidManifest.xml 文件 '/app/src/main/AndroidManifest.xml', + // Gradle 构建文件 '/build.gradle', + // DevContainer 配置文件 '/.devcontainer/devcontainer.json', + // Python 项目配置文件 '/pyproject.toml', ]; -function shouldIgnoreNotFound(target: string) { +/** + * 判断是否应该忽略未找到的情况。 + * @param {string} target - 目标字符串。 + * @returns {boolean} 如果应该忽略未找到的情况则返回 true,否则返回 false。 + */ + function shouldIgnoreNotFound(target: string) { + // 如果 IGNORE_NOT_FOUND 数组中存在与 target 完全相等的元素,或者存在以 '/' 结尾且 target 以该元素开头的元素,则返回 true if (IGNORE_NOT_FOUND.some(entry => entry === target || entry.endsWith('/') && target.startsWith(entry))) return true; + + // 遍历当前打开的工作区文件夹 for (const { uri: { path: wsPath } } of vscode.workspace.workspaceFolders || []) { - if (!target.startsWith(wsPath)) continue; - let local = path.posix.relative(wsPath, target); - if (!local.startsWith('/')) local = `/${local}`; - if (IGNORE_NOT_FOUND.some(entry => entry === local || entry.endsWith('/') && local.startsWith(entry))) return true; + // 如果 target 不是以当前工作区文件夹路径开头,则继续下一个循环 + if (!target.startsWith(wsPath)) continue; + // 计算 target 相对于工作区文件夹路径的相对路径 + let local = path.posix.relative(wsPath, target); + // 如果相对路径不是以 '/' 开头,则在前面添加 '/' + if (!local.startsWith('/')) local = `/${local}`; + // 如果 IGNORE_NOT_FOUND 数组中存在与 local 完全相等的元素,或者存在以 '/' 结尾且 local 以该元素开头的元素,则返回 true + if (IGNORE_NOT_FOUND.some(entry => entry === local || entry.endsWith('/') && local.startsWith(entry))) return true; } + // 如果都不满足条件,则返回 false return false; } -const DEBUG_NOTIFY_FLAGS: Record = {}; -DEBUG_NOTIFY_FLAGS.write = ['createdirectory', 'writefile', 'delete', 'rename']; -DEBUG_NOTIFY_FLAGS.all = [...DEBUG_NOTIFY_FLAGS.write, 'readdirectory', 'readfile', 'stat']; +/** + * 定义一个名为 DEBUG_NOTIFY_FLAGS 的常量对象,用于存储调试通知标志。 + * 该对象中的键是标志名称,值是对应的操作列表,如果未定义则为 undefined。 + */ + const DEBUG_NOTIFY_FLAGS: Record = {}; + + // 将 'createdirectory'、'writefile'、'delete'、'rename' 操作添加到 DEBUG_NOTIFY_FLAGS 对象中,键为 'write' + DEBUG_NOTIFY_FLAGS.write = ['createdirectory', 'writefile', 'delete', 'rename']; + + // 将 DEBUG_NOTIFY_FLAGS.write 中的操作列表和 'readdirectory'、'readfile'、'stat' 操作合并,添加到 DEBUG_NOTIFY_FLAGS 对象中,键为 'all' + DEBUG_NOTIFY_FLAGS.all = [...DEBUG_NOTIFY_FLAGS.write, 'readdirectory', 'readfile', 'stat']; export class SSHFileSystem implements vscode.FileSystemProvider { + + // 定义一个受保护的类属性 onCloseEmitter,它是一个 vscode.EventEmitter 类型的事件发射器,用于在关闭时发出无参数的事件。 protected onCloseEmitter = new vscode.EventEmitter(); + // 定义一个受保护的类属性 onDidChangeFileEmitter,它是一个 vscode.EventEmitter 类型的事件发射器,用于在文件发生变化时发出文件变化事件数组。 protected onDidChangeFileEmitter = new vscode.EventEmitter(); + // 定义一个受保护的类属性 debugFlags,它是一个字符串数组,用于存储调试标志。 protected debugFlags: string[]; + // 定义一个受保护的类属性 notifyErrorFlags,它是一个字符串数组,用于存储通知错误标志。 protected notifyErrorFlags: string[]; + // 定义一个公共的类属性 closed,它是一个布尔值,表示是否已关闭。 public closed = false; + // 定义一个公共的类属性 closing,它是一个布尔值,表示是否正在关闭。 public closing = false; + // 定义一个公共的类属性 copy,它的类型为 undefined,可能用于存储副本或其他数据。 public copy = undefined; + // 定义一个公共的类属性 onClose,它是 onCloseEmitter 的事件,用于订阅关闭事件。 public onClose = this.onCloseEmitter.event; + // 定义一个公共的类属性 onDidChangeFile,它是 onDidChangeFileEmitter 的事件,用于订阅文件变化事件。 public onDidChangeFile = this.onDidChangeFileEmitter.event; + // 定义一个受保护的类属性 logging,它的类型为 Logger,可能用于日志记录。 protected logging: Logger; + /** + * 构造函数,用于创建一个与 SSH 文件系统相关的对象。 + * @param {string} authority - 一个字符串,表示权限信息。 + * @param {ssh2.SFTP} sftp - 一个来自 ssh2 库的 SFTP 对象,用于与远程服务器进行文件传输操作。 + * @param {FileSystemConfig} config - 文件系统配置对象。 + */ constructor(public readonly authority: string, protected sftp: ssh2.SFTP, public readonly config: FileSystemConfig) { + // 设置 logging 属性为一个特定作用域的日志记录器,用于记录与当前权限相关的日志信息。 this.logging = Logging.scope(`SSHFileSystem(${authority})`, false); + // 当 SFTP 连接结束时,设置 closed 属性为 true,并触发 onCloseEmitter 事件发射器。 this.sftp.on('end', () => (this.closed = true, this.onCloseEmitter.fire())); + // 记录一条信息日志,表示 SSH 文件系统已创建。 this.logging.info('SSHFileSystem created'); + // 订阅全局标志,并在标志发生变化时执行相应的逻辑。 const subscription = subscribeToGlobalFlags(() => { // DEBUG_FS flag, with support for an 'all' alias - this.debugFlags = `${getFlag('DEBUG_FS', this.config.flags)?.[0] || ''}`.toLowerCase().split(/,\s*|\s+/g); - if (this.debugFlags.includes('all')) this.debugFlags.push('showignored', 'full', 'converted'); - // FS_NOTIFY_ERRORS flag, with support for a 'write' and 'all' alias, defined in DEBUG_NOTIFY_FLAGS - let notifyErrorFlag: FlagValue = (getFlag('FS_NOTIFY_ERRORS', this.config.flags) || ['write'])[0]; - if (notifyErrorFlag === true) notifyErrorFlag = 'all'; // Flag used to be a boolean flag in v1.25.0 and earlier - this.notifyErrorFlags = (typeof notifyErrorFlag === 'string' ? notifyErrorFlag.toLowerCase().split(/,\s*|\s+/g) : []); - for (const flag of this.notifyErrorFlags) { - const alias = DEBUG_NOTIFY_FLAGS[flag]; - if (alias) this.notifyErrorFlags.push(...alias); - } + // 获取 DEBUG_FS 标志的值,并处理得到调试标志数组。如果标志包含 'all',则添加一些额外的标志。 + this.debugFlags = `${getFlag('DEBUG_FS', this.config.flags)?.[0] || ''}`.toLowerCase().split(/,\s*|\s+/g); + if (this.debugFlags.includes('all')) this.debugFlags.push('showignored', 'full', 'converted'); + // FS_NOTIFY_ERRORS flag, with support for a 'write' and 'all' alias, defined in DEBUG_NOTIFY_FLAGS + // 获取 FS_NOTIFY_ERRORS 标志的值,并处理得到通知错误标志数组。支持 'write' 和 'all' 别名,并处理 DEBUG_NOTIFY_FLAGS 中的别名。 + let notifyErrorFlag: FlagValue = (getFlag('FS_NOTIFY_ERRORS', this.config.flags) || ['write'])[0]; + if (notifyErrorFlag === true) notifyErrorFlag = 'all'; // 在 v1.25.0 及更早版本中,该标志曾是一个布尔值标志。 + this.notifyErrorFlags = (typeof notifyErrorFlag === 'string'? notifyErrorFlag.toLowerCase().split(/,\s*|\s+/g) : []); + for (const flag of this.notifyErrorFlags) { + const alias = DEBUG_NOTIFY_FLAGS[flag]; + if (alias) this.notifyErrorFlags.push(...alias); + } }); + // 当 onClose 事件触发时,取消订阅全局标志。 this.onClose(() => subscription.dispose()); } + /** + * 断开连接的公共方法。 + * @public + * @method disconnect + * @returns {void} + */ public disconnect() { + // 设置 closing 属性为 true,表示正在关闭连接。 this.closing = true; + // 调用 SFTP 对象的 end 方法来结束 SFTP 连接。 this.sftp.end(); } /* FileSystemProvider */ + /** + * 用于监视给定 URI 的公共方法。 + * @public + * @method watch + * @param {vscode.Uri} uri - 要监视的资源的 URI。 + * @param {Object} options - 监视选项,包含 recursive(是否递归监视)和 excludes(要排除的路径列表)。 + * @returns {vscode.Disposable} 一个可处置对象,用于取消监视。 + */ public watch(uri: vscode.Uri, options: { recursive: boolean; excludes: string[]; }): vscode.Disposable { + // 当前方法未实现,抛出错误提示。 // throw new Error('Method not implemented.'); + // 返回一个空的可处置对象,在实际应用中,这里应该返回一个真正用于取消监视的可处置对象。 return new vscode.Disposable(() => { }); } + /** + * 获取指定 URI 的文件状态的公共方法。 + * @public + * @method stat + * @async + * @param {vscode.Uri} uri - 要获取状态的资源的 URI。 + * @returns {Promise} 一个 Promise,解析为文件状态对象。 + */ public async stat(uri: vscode.Uri): Promise { + // 使用 await 等待一个 Promise,该 Promise 通过调用 this.sftp.stat 并传入 uri 的路径和回调函数来创建。 + // 如果出现错误,则捕获错误并调用 handleError 方法处理错误,然后将结果作为 never 类型返回。 const stat = await toPromise(cb => this.sftp.stat(uri.path, cb)) .catch(e => this.handleError('stat', uri, e, true) as never); + // 从获取到的状态对象中提取 mtime(修改时间)和 size(大小),如果不存在则默认为 0。 const { mtime = 0, size = 0 } = stat; + // 初始化文件类型为未知类型。 let type = vscode.FileType.Unknown; + // 以下代码使用位运算来判断文件类型,并设置相应的文件类型标志。 + // 如果是文件,则设置文件类型为文件类型标志;如果是目录,则设置为目录类型标志;如果是符号链接,则设置为符号链接类型标志。 // tslint:disable no-bitwise */ if (stat.isFile()) type = type | vscode.FileType.File; if (stat.isDirectory()) type = type | vscode.FileType.Directory; if (stat.isSymbolicLink()) type = type | vscode.FileType.SymbolicLink; // tslint:enable no-bitwise */ + // 返回一个包含文件类型、修改时间、大小和创建时间(默认为 0)的文件状态对象。 return { - type, mtime, size, - ctime: 0, + type, mtime, size, + ctime: 0, }; } + /** + * 读取指定 URI 对应的目录内容的公共方法。 + * @public + * @method readDirectory + * @async + * @param {vscode.Uri} uri - 要读取的目录的 URI。 + * @returns {Promise<[string, vscode.FileType][]>} 一个 Promise,解析为包含文件名和文件类型的数组。 + */ public async readDirectory(uri: vscode.Uri): Promise<[string, vscode.FileType][]> { + // 使用 await 等待一个 Promise,该 Promise 通过调用 this.sftp.readdir 并传入 uri 的路径和回调函数来创建。 + // 如果出现错误,则捕获错误并调用 handleError 方法处理错误,然后将结果作为 never 类型返回。 const entries = await toPromise(cb => this.sftp.readdir(uri.path, cb)) .catch((e) => this.handleError('readDirectory', uri, e, true) as never); + // 使用 Promise.all 并行处理每个目录项,将每个目录项转换为文件名和文件类型的数组。 return Promise.all(entries.map(async (file) => { - const furi = uri.with({ path: `${uri.path}${uri.path.endsWith('/') ? '' : '/'}${file.filename}` }); - // Mode in octal representation is 120XXX for links, e.g. 120777 - // Any link's mode & 170000 should equal 120000 (using the octal system, at least) - // tslint:disable-next-line:no-bitwise - const link = (file.attrs.mode! & 61440) === 40960 ? vscode.FileType.SymbolicLink : 0; - try { - const type = (await this.stat(furi)).type; - // tslint:disable-next-line:no-bitwise - return [file.filename, type | link] as [string, vscode.FileType]; - } catch (e) { - this.logging.warning.withOptions(LOGGING_SINGLE_LINE_STACKTRACE)`Error in readDirectory for ${furi}: ${e}`; + // 根据当前目录的 URI 和目录项的文件名构建文件的 URI。 + const furi = uri.with({ path: `${uri.path}${uri.path.endsWith('/') ? '' : '/'}${file.filename}` }); + // 判断文件是否为符号链接。通过位运算检查文件的属性模式,确定是否为符号链接,并设置相应的文件类型标志。 + // Mode in octal representation is 120XXX for links, e.g. 120777 + // Any link's mode & 170000 should equal 120000 (using the octal system, at least) // tslint:disable-next-line:no-bitwise - return [file.filename, vscode.FileType.Unknown | link] as [string, vscode.FileType]; - } + const link = (file.attrs.mode! & 61440) === 40960 ? vscode.FileType.SymbolicLink : 0; + try { + // 调用 stat 方法获取文件的状态,包括文件类型。 + const type = (await this.stat(furi)).type; + // tslint:disable-next-line:no-bitwise + // 返回文件名和文件类型(包括可能的符号链接类型)的数组。 + return [file.filename, type | link] as [string, vscode.FileType]; + } catch (e) { + // 如果获取文件状态时出现错误,记录错误日志,并返回文件名和未知文件类型(包括可能的符号链接类型)的数组。 + this.logging.warning.withOptions(LOGGING_SINGLE_LINE_STACKTRACE)`Error in readDirectory for ${furi}: ${e}`; + // tslint:disable-next-line:no-bitwise + return [file.filename, vscode.FileType.Unknown | link] as [string, vscode.FileType]; + } })); } + /** + * 创建目录的公共方法。 + * @public + * @method createDirectory + * @param {vscode.Uri} uri - 要创建的目录的 URI。 + * @returns {void | Promise} 如果操作同步完成则返回 void,否则返回一个 Promise,解析为 void。 + */ public createDirectory(uri: vscode.Uri): void | Promise { + // 使用 toPromise 方法将 this.sftp.mkdir 方法包装成一个 Promise,该方法用于在 SFTP 连接上创建目录。 + // 如果出现错误,则捕获错误并调用 handleError 方法处理错误。 return toPromise(cb => this.sftp.mkdir(uri.path, cb)).catch(e => this.handleError('createDirectory', uri, e, true)); } + /** + * 读取指定 URI 对应的文件内容的公共方法。 + * @public + * @method readFile + * @param {vscode.Uri} uri - 要读取的文件的 URI。 + * @returns {Uint8Array | Promise} 如果文件内容可以立即获取则返回 Uint8Array,否则返回一个 Promise,解析为 Uint8Array。 + */ public readFile(uri: vscode.Uri): Uint8Array | Promise { return new Promise((resolve, reject) => { - const stream = this.sftp.createReadStream(uri.path, { autoClose: true }); - const bufs = []; - stream.on('data', bufs.push.bind(bufs)); - stream.on('error', e => this.handleError('readFile', uri, e, reject)); - stream.on('close', () => { - resolve(new Uint8Array(Buffer.concat(bufs))); - }); + // 创建一个读取流,用于从 SFTP 连接上读取指定路径的文件内容。 + const stream = this.sftp.createReadStream(uri.path, { autoClose: true }); + // 存储读取到的文件内容的缓冲区数组。 + const bufs = []; + // 当有数据可读时,将数据添加到缓冲区数组中。 + stream.on('data', bufs.push.bind(bufs)); + // 当读取流发生错误时,调用 handleError 方法处理错误,并将错误传递给 Promise 的 reject 函数。 + stream.on('error', e => this.handleError('readFile', uri, e, reject)); + // 当读取流关闭时,将缓冲区数组中的内容合并为一个 Uint8Array,并将其传递给 Promise 的 resolve 函数。 + stream.on('close', () => { + resolve(new Uint8Array(Buffer.concat(bufs))); + }); }); } + /** + * 写入文件内容到指定 URI 的公共方法。 + * @public + * @method writeFile + * @param {vscode.Uri} uri - 要写入文件的 URI。 + * @param {Uint8Array} content - 要写入的文件内容。 + * @param {Object} options - 写入选项,包含 create(是否创建文件)和 overwrite(是否覆盖文件)。 + * @returns {void | Promise} 如果操作同步完成则返回 void,否则返回一个 Promise,解析为 void。 + */ public writeFile(uri: vscode.Uri, content: Uint8Array, options: { create: boolean; overwrite: boolean; }): void | Promise { return new Promise(async (resolve, reject) => { - let mode: number | undefined; - let fileExists = false; - try { - const stat = await toPromise(cb => this.sftp.stat(uri.path, cb)); - mode = stat.mode; - fileExists = true; - } catch (e) { - if (e.message === 'No such file') { - mode = this.config.newFileMode as number; - if (typeof mode === 'string') mode = Number(mode); - if (typeof mode !== 'number') mode = 0o664; - if (Number.isNaN(mode)) throw new Error(`Invalid umask '${this.config.newFileMode}'`); - } else { - this.handleError('writeFile', uri, e); - vscode.window.showWarningMessage(`Couldn't read the permissions for '${uri.path}', permissions might be overwritten`); + // 初始化文件模式变量,用于设置写入文件时的权限。 + let mode: number | undefined; + // 标记文件是否已经存在。 + let fileExists = false; + try { + // 尝试获取文件的状态信息,包括文件模式等。 + const stat = await toPromise(cb => this.sftp.stat(uri.path, cb)); + mode = stat.mode; + fileExists = true; + } catch (e) { + // 如果文件不存在,根据配置设置新文件的模式,并进行一些类型转换和错误处理。 + if (e.message === 'No such file') { + mode = this.config.newFileMode as number; + if (typeof mode === 'string') mode = Number(mode); + if (typeof mode !== 'number') mode = 0o664; + if (Number.isNaN(mode)) throw new Error(`Invalid umask '${this.config.newFileMode}'`); + } else { + // 如果出现其他错误,调用 handleError 方法处理错误。 + this.handleError('writeFile', uri, e); + // 显示警告消息,表示无法读取文件权限,权限可能会被覆盖。 + vscode.window.showWarningMessage(`Couldn't read the permissions for '${uri.path}', permissions might be overwritten`); + } } - } - const stream = this.sftp.createWriteStream(uri.path, { mode, flags: 'w' }); - stream.on('error', e => this.handleError('writeFile', uri, e, reject)); - stream.end(content, () => { - this.onDidChangeFileEmitter.fire([{ uri, type: fileExists ? vscode.FileChangeType.Changed : vscode.FileChangeType.Created }]); - resolve(); - }); + // 创建一个写入流,用于将内容写入指定路径的文件,并设置文件模式和写入标志。 + const stream = this.sftp.createWriteStream(uri.path, { mode, flags: 'w' }); + // 当写入流发生错误时,调用 handleError 方法处理错误,并将错误传递给 Promise 的 reject 函数。 + stream.on('error', e => this.handleError('writeFile', uri, e, reject)); + // 当写入流结束时,触发文件变化事件,并将 Promise 解析为完成状态。 + stream.end(content, () => { + this.onDidChangeFileEmitter.fire([{ uri, type: fileExists ? vscode.FileChangeType.Changed : vscode.FileChangeType.Created }]); + resolve(); + }); }); } + /** + * 删除指定 URI 对应的文件或目录的公共方法。 + * @public + * @method delete + * @async + * @param {vscode.Uri} uri - 要删除的资源的 URI。 + * @param {Object} options - 删除选项,包含 recursive(是否递归删除目录)。 + * @returns {Promise} 一个 Promise,表示删除操作的结果。 + */ public async delete(uri: vscode.Uri, options: { recursive: boolean; }): Promise { + // 获取指定 URI 的文件状态。 const stats = await this.stat(uri); + // 定义一个函数,用于触发文件删除事件。 const fireEvent = () => this.onDidChangeFileEmitter.fire([{ uri, type: vscode.FileChangeType.Deleted }]); + // 如果是文件或符号链接,则执行 unlink 操作进行删除。 // tslint:disable no-bitwise */ if (stats.type & (vscode.FileType.SymbolicLink | vscode.FileType.File)) { - return toPromise(cb => this.sftp.unlink(uri.path, cb)) - .then(fireEvent).catch(e => this.handleError('delete', uri, e, true)); + return toPromise(cb => this.sftp.unlink(uri.path, cb)) + .then(fireEvent).catch(e => this.handleError('delete', uri, e, true)); } else if ((stats.type & vscode.FileType.Directory) && options.recursive) { - return toPromise(cb => this.sftp.rmdir(uri.path, cb)) - .then(fireEvent).catch(e => this.handleError('delete', uri, e, true)); + // 如果是目录且 recursive 为 true,则执行 rmdir 操作进行删除。 + return toPromise(cb => this.sftp.rmdir(uri.path, cb)) + .then(fireEvent).catch(e => this.handleError('delete', uri, e, true)); } + // 如果不是上述情况,也尝试执行 unlink 操作进行删除。 return toPromise(cb => this.sftp.unlink(uri.path, cb)) .then(fireEvent).catch(e => this.handleError('delete', uri, e, true)); // tslint:enable no-bitwise */ } + /** + * 重命名文件或目录的公共方法。 + * @public + * @method rename + * @param {vscode.Uri} oldUri - 要重命名的资源的旧 URI。 + * @param {vscode.Uri} newUri - 重命名后的资源的新 URI。 + * @param {Object} options - 重命名选项,包含 overwrite(是否覆盖目标资源)。 + * @returns {void | Promise} 如果操作同步完成则返回 void,否则返回一个 Promise,解析为 void。 + */ public rename(oldUri: vscode.Uri, newUri: vscode.Uri, options: { overwrite: boolean; }): void | Promise { + // 使用 toPromise 方法将 this.sftp.rename 方法包装成一个 Promise,该方法用于在 SFTP 连接上重命名文件或目录。 return toPromise(cb => this.sftp.rename(oldUri.path, newUri.path, cb)) - .then(() => this.onDidChangeFileEmitter.fire([ - { uri: oldUri, type: vscode.FileChangeType.Deleted }, - { uri: newUri, type: vscode.FileChangeType.Created } - ])) + .then(() => { + // 当重命名成功后,触发文件变化事件,表示旧资源被删除,新资源被创建。 + this.onDidChangeFileEmitter.fire([ + { uri: oldUri, type: vscode.FileChangeType.Deleted }, + { uri: newUri, type: vscode.FileChangeType.Created } + ]); + }) .catch(e => this.handleError('rename', newUri, e, true)); } // Helper function to handle/report errors with proper (and minimal) stacktraces and such - protected handleError(method: string, uri: vscode.Uri, e: Error & { code?: any }, doThrow: (boolean | ((error: any) => void)) = false): any { - const ignore = e.code === 2 && [method === 'stat', shouldIgnoreNotFound(uri.path)]; - if (ignore && ignore.includes(true) && !this.debugFlags.includes('disableignored')) { +/** + * 处理错误的受保护方法。 + * @protected + * @method handleError + * @param {string} method - 发生错误的方法名称。 + * @param {vscode.Uri} uri - 与错误相关的资源的 URI。 + * @param {Error & { code?: any }} e - 错误对象。 + * @param {boolean | ((error: any) => void)} doThrow - 控制是否抛出错误,默认为 false。如果是函数,则将错误传递给该函数。 + * @returns {any} 根据不同情况返回不同的值。如果 doThrow 为 false 且没有其他处理,则返回 undefined。 + */ + protected handleError(method: string, uri: vscode.Uri, e: Error & { code?: any }, doThrow: (boolean | ((error: any) => void)) = false): any { + // 判断是否应该忽略错误。如果错误代码为 2(通常表示文件不存在)且满足特定条件,则进行忽略处理。 + const ignore = e.code === 2 && [method === 'stat', shouldIgnoreNotFound(uri.path)]; + if (ignore && ignore.includes(true) && !this.debugFlags.includes('disableignored')) { e = vscode.FileSystemError.FileNotFound(uri); // Whenever a workspace opens, VSCode (and extensions) (indirectly) stat a bunch of files // (.vscode/tasks.json etc, .git/, node_modules for NodeJS, pom.xml for Maven, ...) + // 如果开启了显示被忽略错误的调试标志,则记录被忽略的错误信息。 if (this.debugFlags.includes('showignored')) { - const flags = `${ignore[0] ? 'F' : ''}${ignore[1] ? 'A' : ''}`; - this.logging.debug(`Ignored (${flags}) FileNotFound error for ${method}: ${uri}`, LOGGING_NO_STACKTRACE); + const flags = `${ignore[0] ? 'F' : ''}${ignore[1] ? 'A' : ''}`; + this.logging.debug(`Ignored (${flags}) FileNotFound error for ${method}: ${uri}`, LOGGING_NO_STACKTRACE); } + // 如果 doThrow 为 true,则抛出错误;如果 doThrow 是一个函数,则将错误传递给该函数;否则返回 undefined。 if (doThrow === true) throw e; else if (doThrow) return doThrow(e); else return; - } - else if (this.debugFlags.includes('full')) { + } else if (this.debugFlags.includes('full')) { + // 如果开启了完整错误日志的调试标志,则记录详细的错误信息,包括方法名、URI 和错误对象。 this.logging.debug.withOptions(LOGGING_HANDLE_ERROR)`Error during ${method} ${uri}: ${e}`; - } else if (this.debugFlags.includes('minimal')) { + } else if (this.debugFlags.includes('minimal')) { + // 如果开启了最小错误日志的调试标志,则记录简化的错误信息,包括方法名、URI、错误名称和错误消息。 this.logging.debug.withOptions({ ...LOGGING_NO_STACKTRACE, maxErrorStack: 0 })`Error during ${method} ${uri}: ${e.name}: ${e.message}`; - } - // Convert SSH2Stream error codes into VS Code errors - if (doThrow && typeof e.code === 'number') { + } + // Convert SSH2Stream error codes into VS Code errors + // 如果 doThrow 为 true 且错误对象有错误代码,则将 SSH2Stream 的错误代码转换为 VS Code 的错误类型。 + if (doThrow && typeof e.code === 'number') { const oldE = e; if (e.code === 2) { // No such file or directory - e = vscode.FileSystemError.FileNotFound(uri); + e = vscode.FileSystemError.FileNotFound(uri); } else if (e.code === 3) { // Permission denied - e = vscode.FileSystemError.NoPermissions(uri); + e = vscode.FileSystemError.NoPermissions(uri); } else if (e.code === 6) { // No connection - e = vscode.FileSystemError.Unavailable(uri); + e = vscode.FileSystemError.Unavailable(uri); } else if (e.code === 7) { // Connection lost - e = vscode.FileSystemError.Unavailable(uri); + e = vscode.FileSystemError.Unavailable(uri); } + // 如果错误被转换且开启了转换错误的调试标志,则记录转换后的错误信息。 if (e !== oldE && this.debugFlags.includes('converted')) - Logging.debug(`Error converted to: ${e}`); - } - // Display an error notification if the FS_ERROR_NOTIFICATION flag is enabled - if (this.notifyErrorFlags.includes(method.toLowerCase())) { + Logging.debug(`Error converted to: ${e}`); + } + // Display an error notification if the FS_ERROR_NOTIFICATION flag is enabled + // 如果开启了通知错误的标志且当前错误方法在通知错误列表中,则显示错误消息通知。 + if (this.notifyErrorFlags.includes(method.toLowerCase())) { vscode.window.showErrorMessage(`Error handling ${method} for: ${uri}\n${e.message || e}`); - } - if (doThrow === true) throw e; - if (doThrow) return doThrow(e); } + // 如果 doThrow 为 true,则抛出错误;如果 doThrow 是一个函数,则将错误传递给该函数。 + if (doThrow === true) throw e; + if (doThrow) return doThrow(e); +} }