|
|
|
@ -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<string, string[] | undefined> = {};
|
|
|
|
|
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<string, string[] | undefined> = {};
|
|
|
|
|
|
|
|
|
|
// 将 '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<void> 类型的事件发射器,用于在关闭时发出无参数的事件。
|
|
|
|
|
protected onCloseEmitter = new vscode.EventEmitter<void>();
|
|
|
|
|
// 定义一个受保护的类属性 onDidChangeFileEmitter,它是一个 vscode.EventEmitter<vscode.FileChangeEvent[]> 类型的事件发射器,用于在文件发生变化时发出文件变化事件数组。
|
|
|
|
|
protected onDidChangeFileEmitter = new vscode.EventEmitter<vscode.FileChangeEvent[]>();
|
|
|
|
|
// 定义一个受保护的类属性 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<vscode.FileStat>} 一个 Promise,解析为文件状态对象。
|
|
|
|
|
*/
|
|
|
|
|
public async stat(uri: vscode.Uri): Promise<vscode.FileStat> {
|
|
|
|
|
// 使用 await 等待一个 Promise,该 Promise 通过调用 this.sftp.stat 并传入 uri 的路径和回调函数来创建。
|
|
|
|
|
// 如果出现错误,则捕获错误并调用 handleError 方法处理错误,然后将结果作为 never 类型返回。
|
|
|
|
|
const stat = await toPromise<ssh2.sftp.Stats>(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<ssh2.sftp.DirectoryEntry[]>(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>} 如果操作同步完成则返回 void,否则返回一个 Promise,解析为 void。
|
|
|
|
|
*/
|
|
|
|
|
public createDirectory(uri: vscode.Uri): void | Promise<void> {
|
|
|
|
|
// 使用 toPromise 方法将 this.sftp.mkdir 方法包装成一个 Promise,该方法用于在 SFTP 连接上创建目录。
|
|
|
|
|
// 如果出现错误,则捕获错误并调用 handleError 方法处理错误。
|
|
|
|
|
return toPromise<void>(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>} 如果文件内容可以立即获取则返回 Uint8Array,否则返回一个 Promise,解析为 Uint8Array。
|
|
|
|
|
*/
|
|
|
|
|
public readFile(uri: vscode.Uri): Uint8Array | Promise<Uint8Array> {
|
|
|
|
|
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>} 如果操作同步完成则返回 void,否则返回一个 Promise,解析为 void。
|
|
|
|
|
*/
|
|
|
|
|
public writeFile(uri: vscode.Uri, content: Uint8Array, options: { create: boolean; overwrite: boolean; }): void | Promise<void> {
|
|
|
|
|
return new Promise(async (resolve, reject) => {
|
|
|
|
|
let mode: number | undefined;
|
|
|
|
|
let fileExists = false;
|
|
|
|
|
try {
|
|
|
|
|
const stat = await toPromise<ssh2.sftp.Stats>(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<ssh2.sftp.Stats>(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<any>} 一个 Promise,表示删除操作的结果。
|
|
|
|
|
*/
|
|
|
|
|
public async delete(uri: vscode.Uri, options: { recursive: boolean; }): Promise<any> {
|
|
|
|
|
// 获取指定 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>} 如果操作同步完成则返回 void,否则返回一个 Promise,解析为 void。
|
|
|
|
|
*/
|
|
|
|
|
public rename(oldUri: vscode.Uri, newUri: vscode.Uri, options: { overwrite: boolean; }): void | Promise<void> {
|
|
|
|
|
// 使用 toPromise 方法将 this.sftp.rename 方法包装成一个 Promise,该方法用于在 SFTP 连接上重命名文件或目录。
|
|
|
|
|
return toPromise<void>(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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|