ssh2-scp 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README-CN.md +126 -0
- package/README.md +126 -0
- package/dist/esm/ssh-fs.d.ts +81 -0
- package/dist/esm/ssh-fs.js +324 -0
- package/dist/esm/transfer.d.ts +116 -0
- package/dist/esm/transfer.js +133 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 ZHAO Xudong
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README-CN.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# ssh2-scp
|
|
2
|
+
|
|
3
|
+
通过 SSH2 会话进行远程文件系统操作(无需 SFTP)。
|
|
4
|
+
|
|
5
|
+
[English](./README.md)
|
|
6
|
+
|
|
7
|
+
## 特性
|
|
8
|
+
|
|
9
|
+
- 通过 SSH2 shell/command 会话进行文件操作(无需 SFTP)
|
|
10
|
+
- 支持所有常用文件系统操作
|
|
11
|
+
- 同时支持 ESM 和 CJS 导出
|
|
12
|
+
- 支持 TypeScript
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install ssh2-scp
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## 使用方法
|
|
21
|
+
|
|
22
|
+
```javascript
|
|
23
|
+
import { Client } from 'ssh2'
|
|
24
|
+
import { createSshFs } from 'ssh2-scp'
|
|
25
|
+
|
|
26
|
+
const client = new Client()
|
|
27
|
+
client.on('ready', () => {
|
|
28
|
+
const fs = createSshFs(client)
|
|
29
|
+
|
|
30
|
+
// 列出目录
|
|
31
|
+
const files = await fs.list('/path/to/dir')
|
|
32
|
+
|
|
33
|
+
// 读取文件
|
|
34
|
+
const content = await fs.readFile('/path/to/file.txt')
|
|
35
|
+
|
|
36
|
+
// 写入文件
|
|
37
|
+
await fs.writeFile('/path/to/file.txt', 'Hello World')
|
|
38
|
+
|
|
39
|
+
// 获取文件信息
|
|
40
|
+
const stat = await fs.stat('/path/to/file')
|
|
41
|
+
console.log(stat.isFile(), stat.isDirectory(), stat.size)
|
|
42
|
+
|
|
43
|
+
// ... 更多操作
|
|
44
|
+
})
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## API
|
|
48
|
+
|
|
49
|
+
### 构造函数
|
|
50
|
+
|
|
51
|
+
```javascript
|
|
52
|
+
createSshFs(client)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- `client` - 已认证的 ssh2 Client 实例
|
|
56
|
+
|
|
57
|
+
### 方法
|
|
58
|
+
|
|
59
|
+
#### 文件操作
|
|
60
|
+
|
|
61
|
+
- `readFile(remotePath)` - 读取文件内容为字符串
|
|
62
|
+
- `writeFile(remotePath, content, mode?)` - 写入字符串内容到文件
|
|
63
|
+
- `readFileBase64(remotePath)` - 读取文件为 base64 编码字符串
|
|
64
|
+
- `writeFileBase64(remotePath, base64Content)` - 写入 base64 内容到文件
|
|
65
|
+
|
|
66
|
+
#### 目录操作
|
|
67
|
+
|
|
68
|
+
- `list(remotePath)` - 列出目录内容
|
|
69
|
+
- `mkdir(remotePath, options?)` - 创建目录
|
|
70
|
+
- `rmdir(remotePath)` - 删除目录
|
|
71
|
+
|
|
72
|
+
#### 文件/目录操作
|
|
73
|
+
|
|
74
|
+
- `cp(from, to)` - 复制文件或目录
|
|
75
|
+
- `mv(from, to)` - 移动/重命名文件或目录
|
|
76
|
+
- `rename(oldPath, newPath)` - 重命名文件或目录
|
|
77
|
+
- `rm(remotePath)` - 删除文件
|
|
78
|
+
- `touch(remotePath)` - 创建空文件或更新时间戳
|
|
79
|
+
|
|
80
|
+
#### 文件信息
|
|
81
|
+
|
|
82
|
+
- `stat(remotePath)` - 获取文件信息(跟随符号链接)
|
|
83
|
+
- `lstat(remotePath)` - 获取文件信息(不跟随符号链接)
|
|
84
|
+
- `realpath(remotePath)` - 获取规范路径
|
|
85
|
+
- `readlink(remotePath)` - 读取符号链接目标
|
|
86
|
+
- `getFolderSize(folderPath)` - 获取文件夹大小和文件数量
|
|
87
|
+
|
|
88
|
+
#### 权限
|
|
89
|
+
|
|
90
|
+
- `chmod(remotePath, mode)` - 更改文件权限
|
|
91
|
+
|
|
92
|
+
#### 工具
|
|
93
|
+
|
|
94
|
+
- `getHomeDir()` - 获取主目录
|
|
95
|
+
- `runExec(command)` - 执行原始 shell 命令
|
|
96
|
+
|
|
97
|
+
## 文件传输
|
|
98
|
+
|
|
99
|
+
```javascript
|
|
100
|
+
import { Transfer } from 'ssh2-scp/transfer'
|
|
101
|
+
|
|
102
|
+
const transfer = new Transfer(fs, {
|
|
103
|
+
type: 'download', // 或 'upload'
|
|
104
|
+
remotePath: '/远程/路径',
|
|
105
|
+
localPath: '/本地/路径',
|
|
106
|
+
chunkSize: 32768,
|
|
107
|
+
onProgress: (transferred, total) => {
|
|
108
|
+
console.log(`进度: ${transferred}/${total}`)
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
await transfer.startTransfer()
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 传输选项
|
|
116
|
+
|
|
117
|
+
- `type` - 传输类型:`'download'`(下载)或 `'upload'`(上传)
|
|
118
|
+
- `remotePath` - 远程文件/文件夹路径
|
|
119
|
+
- `localPath` - 本地文件/文件夹路径
|
|
120
|
+
- `chunkSize` - 传输块大小(默认:32768)
|
|
121
|
+
- `onProgress` - 进度回调函数 `(transferred, total) => void`
|
|
122
|
+
- `onData` - 数据回调函数 `(count) => void`
|
|
123
|
+
|
|
124
|
+
## 许可证
|
|
125
|
+
|
|
126
|
+
MIT
|
package/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# ssh2-scp
|
|
2
|
+
|
|
3
|
+
Remote file system operations over SSH2 session without SFTP
|
|
4
|
+
|
|
5
|
+
[中文](./README-CN.md)
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- File operations via SSH2 shell/command session (no SFTP required)
|
|
10
|
+
- Supports all common file system operations
|
|
11
|
+
- Both ESM and CJS exports
|
|
12
|
+
- TypeScript support
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install ssh2-scp
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```javascript
|
|
23
|
+
import { Client } from 'ssh2'
|
|
24
|
+
import { createSshFs } from 'ssh2-scp'
|
|
25
|
+
|
|
26
|
+
const client = new Client()
|
|
27
|
+
client.on('ready', () => {
|
|
28
|
+
const fs = createSshFs(client)
|
|
29
|
+
|
|
30
|
+
// List directory
|
|
31
|
+
const files = await fs.list('/path/to/dir')
|
|
32
|
+
|
|
33
|
+
// Read file
|
|
34
|
+
const content = await fs.readFile('/path/to/file.txt')
|
|
35
|
+
|
|
36
|
+
// Write file
|
|
37
|
+
await fs.writeFile('/path/to/file.txt', 'Hello World')
|
|
38
|
+
|
|
39
|
+
// Get file stats
|
|
40
|
+
const stat = await fs.stat('/path/to/file')
|
|
41
|
+
console.log(stat.isFile(), stat.isDirectory(), stat.size)
|
|
42
|
+
|
|
43
|
+
// ... and more
|
|
44
|
+
})
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## API
|
|
48
|
+
|
|
49
|
+
### Constructor
|
|
50
|
+
|
|
51
|
+
```javascript
|
|
52
|
+
createSshFs(client)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- `client` - An authenticated ssh2 Client instance
|
|
56
|
+
|
|
57
|
+
### Methods
|
|
58
|
+
|
|
59
|
+
#### File Operations
|
|
60
|
+
|
|
61
|
+
- `readFile(remotePath)` - Read file content as string
|
|
62
|
+
- `writeFile(remotePath, content, mode?)` - Write string content to file
|
|
63
|
+
- `readFileBase64(remotePath)` - Read file as base64 encoded string
|
|
64
|
+
- `writeFileBase64(remotePath, base64Content)` - Write base64 content to file
|
|
65
|
+
|
|
66
|
+
#### Directory Operations
|
|
67
|
+
|
|
68
|
+
- `list(remotePath)` - List directory contents
|
|
69
|
+
- `mkdir(remotePath, options?)` - Create directory
|
|
70
|
+
- `rmdir(remotePath)` - Remove directory
|
|
71
|
+
|
|
72
|
+
#### File/Directory Manipulation
|
|
73
|
+
|
|
74
|
+
- `cp(from, to)` - Copy file or directory
|
|
75
|
+
- `mv(from, to)` - Move/rename file or directory
|
|
76
|
+
- `rename(oldPath, newPath)` - Rename file or directory
|
|
77
|
+
- `rm(remotePath)` - Remove file
|
|
78
|
+
- `touch(remotePath)` - Create empty file or update timestamp
|
|
79
|
+
|
|
80
|
+
#### File Info
|
|
81
|
+
|
|
82
|
+
- `stat(remotePath)` - Get file stats (follows symlinks)
|
|
83
|
+
- `lstat(remotePath)` - Get file stats (does not follow symlinks)
|
|
84
|
+
- `realpath(remotePath)` - Get canonical path
|
|
85
|
+
- `readlink(remotePath)` - Read symlink target
|
|
86
|
+
- `getFolderSize(folderPath)` - Get folder size and file count
|
|
87
|
+
|
|
88
|
+
#### Permissions
|
|
89
|
+
|
|
90
|
+
- `chmod(remotePath, mode)` - Change file permissions
|
|
91
|
+
|
|
92
|
+
#### Utilities
|
|
93
|
+
|
|
94
|
+
- `getHomeDir()` - Get home directory
|
|
95
|
+
- `runExec(command)` - Execute raw shell command
|
|
96
|
+
|
|
97
|
+
## Transfer
|
|
98
|
+
|
|
99
|
+
```javascript
|
|
100
|
+
import { Transfer } from 'ssh2-scp/transfer'
|
|
101
|
+
|
|
102
|
+
const transfer = new Transfer(fs, {
|
|
103
|
+
type: 'download', // or 'upload'
|
|
104
|
+
remotePath: '/remote/path',
|
|
105
|
+
localPath: '/local/path',
|
|
106
|
+
chunkSize: 32768,
|
|
107
|
+
onProgress: (transferred, total) => {
|
|
108
|
+
console.log(`Progress: ${transferred}/${total}`)
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
await transfer.startTransfer()
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Transfer Options
|
|
116
|
+
|
|
117
|
+
- `type` - Transfer type: `'download'` or `'upload'`
|
|
118
|
+
- `remotePath` - Remote file/folder path
|
|
119
|
+
- `localPath` - Local file/folder path
|
|
120
|
+
- `chunkSize` - Chunk size for transfer (default: 32768)
|
|
121
|
+
- `onProgress` - Progress callback `(transferred, total) => void`
|
|
122
|
+
- `onData` - Data callback `(count) => void`
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
MIT
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Client } from 'ssh2';
|
|
2
|
+
|
|
3
|
+
export declare function createSshFs(session: Client, options?: SshFsOptions): SshFs;
|
|
4
|
+
|
|
5
|
+
export declare interface FileInfo {
|
|
6
|
+
type: string;
|
|
7
|
+
name: string;
|
|
8
|
+
size: number;
|
|
9
|
+
modifyTime: number;
|
|
10
|
+
accessTime: number;
|
|
11
|
+
mode: number;
|
|
12
|
+
rights: {
|
|
13
|
+
user: string;
|
|
14
|
+
group: string;
|
|
15
|
+
other: string;
|
|
16
|
+
};
|
|
17
|
+
owner: number;
|
|
18
|
+
group: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export declare class SshFs {
|
|
22
|
+
private session;
|
|
23
|
+
constructor(session: Client, _options?: SshFsOptions);
|
|
24
|
+
private getExecOpts;
|
|
25
|
+
private getMonthIndex;
|
|
26
|
+
private runCmd;
|
|
27
|
+
private rmFolderCmd;
|
|
28
|
+
private rmCmd;
|
|
29
|
+
getHomeDir(): Promise<string>;
|
|
30
|
+
rmdir(remotePath: string): Promise<unknown>;
|
|
31
|
+
private removeDirectoryRecursively;
|
|
32
|
+
rmrf(remotePath: string): Promise<string>;
|
|
33
|
+
touch(remotePath: string): Promise<string>;
|
|
34
|
+
cp(from: string, to: string): Promise<unknown>;
|
|
35
|
+
mv(from: string, to: string): Promise<unknown>;
|
|
36
|
+
runExec(cmd: string): Promise<string>;
|
|
37
|
+
getFolderSize(folderPath: string): Promise<{
|
|
38
|
+
size: string;
|
|
39
|
+
count: number;
|
|
40
|
+
}>;
|
|
41
|
+
private listFiles;
|
|
42
|
+
list(remotePath: string): Promise<FileInfo[]>;
|
|
43
|
+
mkdir(remotePath: string, options?: {
|
|
44
|
+
mode?: number;
|
|
45
|
+
}): Promise<string>;
|
|
46
|
+
stat(remotePath: string): Promise<Stats>;
|
|
47
|
+
readlink(remotePath: string): Promise<string>;
|
|
48
|
+
realpath(remotePath: string): Promise<string>;
|
|
49
|
+
lstat(remotePath: string): Promise<Stats>;
|
|
50
|
+
chmod(remotePath: string, mode: number): Promise<string>;
|
|
51
|
+
rename(remotePath: string, remotePathNew: string): Promise<string>;
|
|
52
|
+
rmFolder(remotePath: string): Promise<string>;
|
|
53
|
+
rm(remotePath: string): Promise<string>;
|
|
54
|
+
readFile(remotePath: string, options?: {
|
|
55
|
+
chunkSize?: number;
|
|
56
|
+
}): Promise<Buffer>;
|
|
57
|
+
writeFile(remotePath: string, str: Buffer | string, mode?: number, _options?: {
|
|
58
|
+
chunkSize?: number;
|
|
59
|
+
}): Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export declare interface SshFsOptions {
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export declare interface Stats {
|
|
66
|
+
isDirectory: () => boolean;
|
|
67
|
+
isFile: () => boolean;
|
|
68
|
+
isBlockDevice: () => boolean;
|
|
69
|
+
isCharacterDevice: () => boolean;
|
|
70
|
+
isSymbolicLink: () => boolean;
|
|
71
|
+
isFIFO: () => boolean;
|
|
72
|
+
isSocket: () => boolean;
|
|
73
|
+
size: number;
|
|
74
|
+
mode: number;
|
|
75
|
+
uid: number;
|
|
76
|
+
gid: number;
|
|
77
|
+
atime: number;
|
|
78
|
+
mtime: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export { }
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
class SshFs {
|
|
2
|
+
session;
|
|
3
|
+
constructor(session, _options) {
|
|
4
|
+
this.session = session;
|
|
5
|
+
}
|
|
6
|
+
getExecOpts() {
|
|
7
|
+
return {};
|
|
8
|
+
}
|
|
9
|
+
getMonthIndex(month) {
|
|
10
|
+
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
11
|
+
return months.indexOf(month);
|
|
12
|
+
}
|
|
13
|
+
runCmd(cmd, timeout = 3e4) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
let timeoutId;
|
|
16
|
+
const cleanup = () => {
|
|
17
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
18
|
+
};
|
|
19
|
+
timeoutId = setTimeout(() => {
|
|
20
|
+
cleanup();
|
|
21
|
+
reject(new Error(`Command timed out: ${cmd}`));
|
|
22
|
+
}, timeout);
|
|
23
|
+
this.session.exec(cmd, (err, stream) => {
|
|
24
|
+
cleanup();
|
|
25
|
+
if (err) {
|
|
26
|
+
reject(err);
|
|
27
|
+
} else {
|
|
28
|
+
let out = Buffer.from("");
|
|
29
|
+
stream.on("end", () => {
|
|
30
|
+
resolve(out.toString());
|
|
31
|
+
}).on("data", (data) => {
|
|
32
|
+
out = Buffer.concat([out, data]);
|
|
33
|
+
}).stderr.on("data", (data) => {
|
|
34
|
+
reject(data.toString());
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
rmFolderCmd(remotePath) {
|
|
41
|
+
return this.runCmd(`rmdir "${remotePath}"`);
|
|
42
|
+
}
|
|
43
|
+
rmCmd(remotePath) {
|
|
44
|
+
return this.runCmd(`rm "${remotePath}"`);
|
|
45
|
+
}
|
|
46
|
+
async getHomeDir() {
|
|
47
|
+
return this.realpath(".");
|
|
48
|
+
}
|
|
49
|
+
async rmdir(remotePath) {
|
|
50
|
+
try {
|
|
51
|
+
return await this.rmrf(remotePath);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.error("rm -rf dir error", err);
|
|
54
|
+
return this.removeDirectoryRecursively(remotePath);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async removeDirectoryRecursively(remotePath) {
|
|
58
|
+
try {
|
|
59
|
+
const contents = await this.listFiles(remotePath);
|
|
60
|
+
for (const item of contents) {
|
|
61
|
+
if (item.name === "." || item.name === "..") continue;
|
|
62
|
+
const itemPath = `${remotePath}/${item.name}`;
|
|
63
|
+
if (item.type === "d") {
|
|
64
|
+
await this.removeDirectoryRecursively(itemPath);
|
|
65
|
+
} else {
|
|
66
|
+
await this.rmCmd(itemPath);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
await this.rmFolderCmd(remotePath);
|
|
70
|
+
} catch (e) {
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
rmrf(remotePath) {
|
|
74
|
+
return this.runCmd(`rm -rf "${remotePath}"`);
|
|
75
|
+
}
|
|
76
|
+
touch(remotePath) {
|
|
77
|
+
return this.runCmd(`touch "${remotePath}"`);
|
|
78
|
+
}
|
|
79
|
+
cp(from, to) {
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
const cmd = `cp -r "${from}" "${to}"`;
|
|
82
|
+
this.session.exec(cmd, this.getExecOpts(), (err) => {
|
|
83
|
+
if (err) reject(err);
|
|
84
|
+
else resolve(1);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
mv(from, to) {
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
const cmd = `mv "${from}" "${to}"`;
|
|
91
|
+
this.session.exec(cmd, this.getExecOpts(), (err) => {
|
|
92
|
+
if (err) reject(err);
|
|
93
|
+
else resolve(1);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
runExec(cmd) {
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
this.session.exec(cmd, this.getExecOpts(), (err, stream) => {
|
|
100
|
+
if (err) {
|
|
101
|
+
reject(err);
|
|
102
|
+
} else {
|
|
103
|
+
let out = Buffer.from("");
|
|
104
|
+
stream.on("end", () => {
|
|
105
|
+
resolve(out.toString());
|
|
106
|
+
}).on("data", (data) => {
|
|
107
|
+
out = Buffer.concat([out, data]);
|
|
108
|
+
}).stderr.on("data", (data) => {
|
|
109
|
+
reject(data.toString());
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
async getFolderSize(folderPath) {
|
|
116
|
+
const output = await this.runExec(`du -sh "${folderPath}" && find "${folderPath}" -type f | wc -l`);
|
|
117
|
+
const lines = output.trim().split("\n");
|
|
118
|
+
const size = lines[0]?.split(" ")[0] || "0";
|
|
119
|
+
const count = parseInt(lines[1] || "0", 10);
|
|
120
|
+
return { size, count };
|
|
121
|
+
}
|
|
122
|
+
async listFiles(remotePath) {
|
|
123
|
+
const output = await this.runCmd(`ls -la "${remotePath}"`);
|
|
124
|
+
const lines = output.trim().split("\n");
|
|
125
|
+
const result = [];
|
|
126
|
+
for (let i = 0; i < lines.length; i++) {
|
|
127
|
+
const line = lines[i].trim();
|
|
128
|
+
if (!line || i === 0) continue;
|
|
129
|
+
const parts = line.split(/\s+/);
|
|
130
|
+
if (parts.length < 9) continue;
|
|
131
|
+
const name = parts.slice(8).join(" ");
|
|
132
|
+
if (name === "." || name === "..") continue;
|
|
133
|
+
const type = parts[0].charAt(0);
|
|
134
|
+
const mode = parseInt(parts[0].slice(1), 8);
|
|
135
|
+
const owner = parseInt(parts[2], 10);
|
|
136
|
+
const group = parseInt(parts[3], 10);
|
|
137
|
+
const size = parseInt(parts[4], 10);
|
|
138
|
+
const month = parts[5];
|
|
139
|
+
const day = parts[6];
|
|
140
|
+
const timeOrYear = parts[7];
|
|
141
|
+
let mtime = 0;
|
|
142
|
+
const now = /* @__PURE__ */ new Date();
|
|
143
|
+
const year = now.getFullYear();
|
|
144
|
+
if (timeOrYear.includes(":")) {
|
|
145
|
+
const [hour, minute] = timeOrYear.split(":").map(Number);
|
|
146
|
+
mtime = new Date(year, this.getMonthIndex(month), parseInt(day, 10), hour, minute).getTime();
|
|
147
|
+
} else {
|
|
148
|
+
mtime = new Date(parseInt(timeOrYear, 10), this.getMonthIndex(month), parseInt(day, 10)).getTime();
|
|
149
|
+
}
|
|
150
|
+
result.push({
|
|
151
|
+
type,
|
|
152
|
+
name,
|
|
153
|
+
size,
|
|
154
|
+
modifyTime: mtime,
|
|
155
|
+
accessTime: mtime,
|
|
156
|
+
mode,
|
|
157
|
+
rights: {
|
|
158
|
+
user: parts[0].substring(1, 4),
|
|
159
|
+
group: parts[0].substring(4, 7),
|
|
160
|
+
other: parts[0].substring(7, 10)
|
|
161
|
+
},
|
|
162
|
+
owner,
|
|
163
|
+
group
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
list(remotePath) {
|
|
169
|
+
return this.listFiles(remotePath);
|
|
170
|
+
}
|
|
171
|
+
async mkdir(remotePath, options = {}) {
|
|
172
|
+
const cmd = options.mode ? `mkdir -m ${options.mode.toString(8)} -p "${remotePath}"` : `mkdir -p "${remotePath}"`;
|
|
173
|
+
return this.runCmd(cmd);
|
|
174
|
+
}
|
|
175
|
+
async stat(remotePath) {
|
|
176
|
+
const isSymlink = await this.runCmd(`test -L "${remotePath}" && echo 1 || echo 0`).then((r) => r.trim() === "1");
|
|
177
|
+
const output = await this.runCmd(`stat -c '%s %h %u %g %Y %Y %a' "${remotePath}"`);
|
|
178
|
+
const parts = output.trim().split(/\s+/);
|
|
179
|
+
if (parts.length < 7) {
|
|
180
|
+
return {
|
|
181
|
+
size: 0,
|
|
182
|
+
mode: 0,
|
|
183
|
+
uid: 0,
|
|
184
|
+
gid: 0,
|
|
185
|
+
atime: 0,
|
|
186
|
+
mtime: 0,
|
|
187
|
+
isFile: () => false,
|
|
188
|
+
isDirectory: () => false,
|
|
189
|
+
isSymbolicLink: () => false,
|
|
190
|
+
isBlockDevice: () => false,
|
|
191
|
+
isCharacterDevice: () => false,
|
|
192
|
+
isFIFO: () => false,
|
|
193
|
+
isSocket: () => false
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const [size, _nlink, uid, gid, atime, mtime, modeOct] = parts;
|
|
197
|
+
const mode = parseInt(modeOct, 8);
|
|
198
|
+
return {
|
|
199
|
+
size: parseInt(size, 10),
|
|
200
|
+
mode,
|
|
201
|
+
uid: parseInt(uid, 10),
|
|
202
|
+
gid: parseInt(gid, 10),
|
|
203
|
+
atime: parseInt(atime, 10) * 1e3,
|
|
204
|
+
mtime: parseInt(mtime, 10) * 1e3,
|
|
205
|
+
isFile: () => (mode & 61440) === 32768,
|
|
206
|
+
isDirectory: () => (mode & 61440) === 16384,
|
|
207
|
+
isSymbolicLink: () => isSymlink,
|
|
208
|
+
isBlockDevice: () => (mode & 61440) === 24576,
|
|
209
|
+
isCharacterDevice: () => (mode & 61440) === 8192,
|
|
210
|
+
isFIFO: () => (mode & 61440) === 4096,
|
|
211
|
+
isSocket: () => (mode & 61440) === 49152
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
readlink(remotePath) {
|
|
215
|
+
return this.runCmd(`readlink "${remotePath}"`).then((output) => output.trim());
|
|
216
|
+
}
|
|
217
|
+
realpath(remotePath) {
|
|
218
|
+
return this.runCmd(`realpath "${remotePath}"`).then((output) => output.trim());
|
|
219
|
+
}
|
|
220
|
+
async lstat(remotePath) {
|
|
221
|
+
const output = await this.runCmd(`ls -ld "${remotePath}"`);
|
|
222
|
+
const isSymlink = output.trim().startsWith("l");
|
|
223
|
+
const statOutput = await this.runCmd(`stat -c '%s %h %u %g %Y %Y %a' "${remotePath}"`);
|
|
224
|
+
const parts = statOutput.trim().split(/\s+/);
|
|
225
|
+
if (parts.length < 7) {
|
|
226
|
+
return {
|
|
227
|
+
size: 0,
|
|
228
|
+
mode: 0,
|
|
229
|
+
uid: 0,
|
|
230
|
+
gid: 0,
|
|
231
|
+
atime: 0,
|
|
232
|
+
mtime: 0,
|
|
233
|
+
isFile: () => false,
|
|
234
|
+
isDirectory: () => false,
|
|
235
|
+
isSymbolicLink: () => false,
|
|
236
|
+
isBlockDevice: () => false,
|
|
237
|
+
isCharacterDevice: () => false,
|
|
238
|
+
isFIFO: () => false,
|
|
239
|
+
isSocket: () => false
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
const [size, _nlink, uid, gid, atime, mtime, modeOct] = parts;
|
|
243
|
+
const mode = parseInt(modeOct, 8);
|
|
244
|
+
return {
|
|
245
|
+
size: parseInt(size, 10),
|
|
246
|
+
mode,
|
|
247
|
+
uid: parseInt(uid, 10),
|
|
248
|
+
gid: parseInt(gid, 10),
|
|
249
|
+
atime: parseInt(atime, 10) * 1e3,
|
|
250
|
+
mtime: parseInt(mtime, 10) * 1e3,
|
|
251
|
+
isFile: () => !isSymlink && (mode & 61440) === 32768,
|
|
252
|
+
isDirectory: () => !isSymlink && (mode & 61440) === 16384,
|
|
253
|
+
isSymbolicLink: () => isSymlink,
|
|
254
|
+
isBlockDevice: () => (mode & 61440) === 24576,
|
|
255
|
+
isCharacterDevice: () => (mode & 61440) === 8192,
|
|
256
|
+
isFIFO: () => (mode & 61440) === 4096,
|
|
257
|
+
isSocket: () => (mode & 61440) === 49152
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
chmod(remotePath, mode) {
|
|
261
|
+
return this.runCmd(`chmod ${mode.toString(8)} "${remotePath}"`);
|
|
262
|
+
}
|
|
263
|
+
rename(remotePath, remotePathNew) {
|
|
264
|
+
return this.runCmd(`mv "${remotePath}" "${remotePathNew}"`);
|
|
265
|
+
}
|
|
266
|
+
rmFolder(remotePath) {
|
|
267
|
+
return this.rmFolderCmd(remotePath);
|
|
268
|
+
}
|
|
269
|
+
rm(remotePath) {
|
|
270
|
+
return this.rmCmd(remotePath);
|
|
271
|
+
}
|
|
272
|
+
async readFile(remotePath, options) {
|
|
273
|
+
const chunkSize = options?.chunkSize ?? 64 * 1024;
|
|
274
|
+
const fileSizeOutput = await this.runCmd(`stat -c %s "${remotePath}"`);
|
|
275
|
+
const fileSize = parseInt(fileSizeOutput.trim(), 10);
|
|
276
|
+
if (fileSize <= chunkSize) {
|
|
277
|
+
const output = await this.runCmd(`cat "${remotePath}"`);
|
|
278
|
+
return Buffer.from(output, "binary");
|
|
279
|
+
}
|
|
280
|
+
const chunks = [];
|
|
281
|
+
for (let offset = 0; offset < fileSize; offset += chunkSize) {
|
|
282
|
+
const cmd = `dd if="${remotePath}" bs=1K skip=${Math.floor(offset / 1024)} count=${Math.ceil(chunkSize / 1024)} 2>/dev/null`;
|
|
283
|
+
const chunkOutput = await this.runCmd(cmd);
|
|
284
|
+
if (chunkOutput) {
|
|
285
|
+
chunks.push(Buffer.from(chunkOutput, "binary"));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return Buffer.concat(chunks);
|
|
289
|
+
}
|
|
290
|
+
async writeFile(remotePath, str, mode, _options) {
|
|
291
|
+
const data = typeof str === "string" ? Buffer.from(str) : str;
|
|
292
|
+
const sizeThreshold = 64 * 1024;
|
|
293
|
+
if (data.length <= sizeThreshold) {
|
|
294
|
+
const escapedContent = data.toString("binary").replace(/'/g, "'\\''");
|
|
295
|
+
const cmd = `printf '%s' '${escapedContent}' > "${remotePath}"`;
|
|
296
|
+
await this.runCmd(cmd);
|
|
297
|
+
} else {
|
|
298
|
+
const tempBase = `/tmp/ssh-fs-${Date.now()}`;
|
|
299
|
+
await this.runCmd(`mkdir -p "${tempBase}"`);
|
|
300
|
+
try {
|
|
301
|
+
const chunkSize = 64 * 1024;
|
|
302
|
+
for (let i = 0; i < data.length; i += chunkSize) {
|
|
303
|
+
const chunk = data.slice(i, i + chunkSize);
|
|
304
|
+
const chunkFile = `${tempBase}/c${Math.floor(i / chunkSize)}`;
|
|
305
|
+
const escapedChunk = chunk.toString("binary").replace(/'/g, "'\\''");
|
|
306
|
+
await this.runCmd(`printf '%s' '${escapedChunk}' > "${chunkFile}"`);
|
|
307
|
+
}
|
|
308
|
+
await this.runCmd(`cat ${tempBase}/c* > "${remotePath}"`);
|
|
309
|
+
} finally {
|
|
310
|
+
await this.runCmd(`rm -rf "${tempBase}"`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (mode) {
|
|
314
|
+
await this.runCmd(`chmod ${mode.toString(8)} "${remotePath}"`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function createSshFs(session, options) {
|
|
319
|
+
return new SshFs(session, options);
|
|
320
|
+
}
|
|
321
|
+
export {
|
|
322
|
+
SshFs,
|
|
323
|
+
createSshFs
|
|
324
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Client } from 'ssh2';
|
|
2
|
+
|
|
3
|
+
declare interface FileInfo {
|
|
4
|
+
type: string;
|
|
5
|
+
name: string;
|
|
6
|
+
size: number;
|
|
7
|
+
modifyTime: number;
|
|
8
|
+
accessTime: number;
|
|
9
|
+
mode: number;
|
|
10
|
+
rights: {
|
|
11
|
+
user: string;
|
|
12
|
+
group: string;
|
|
13
|
+
other: string;
|
|
14
|
+
};
|
|
15
|
+
owner: number;
|
|
16
|
+
group: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
declare class SshFs {
|
|
20
|
+
private session;
|
|
21
|
+
constructor(session: Client, _options?: SshFsOptions);
|
|
22
|
+
private getExecOpts;
|
|
23
|
+
private getMonthIndex;
|
|
24
|
+
private runCmd;
|
|
25
|
+
private rmFolderCmd;
|
|
26
|
+
private rmCmd;
|
|
27
|
+
getHomeDir(): Promise<string>;
|
|
28
|
+
rmdir(remotePath: string): Promise<unknown>;
|
|
29
|
+
private removeDirectoryRecursively;
|
|
30
|
+
rmrf(remotePath: string): Promise<string>;
|
|
31
|
+
touch(remotePath: string): Promise<string>;
|
|
32
|
+
cp(from: string, to: string): Promise<unknown>;
|
|
33
|
+
mv(from: string, to: string): Promise<unknown>;
|
|
34
|
+
runExec(cmd: string): Promise<string>;
|
|
35
|
+
getFolderSize(folderPath: string): Promise<{
|
|
36
|
+
size: string;
|
|
37
|
+
count: number;
|
|
38
|
+
}>;
|
|
39
|
+
private listFiles;
|
|
40
|
+
list(remotePath: string): Promise<FileInfo[]>;
|
|
41
|
+
mkdir(remotePath: string, options?: {
|
|
42
|
+
mode?: number;
|
|
43
|
+
}): Promise<string>;
|
|
44
|
+
stat(remotePath: string): Promise<Stats>;
|
|
45
|
+
readlink(remotePath: string): Promise<string>;
|
|
46
|
+
realpath(remotePath: string): Promise<string>;
|
|
47
|
+
lstat(remotePath: string): Promise<Stats>;
|
|
48
|
+
chmod(remotePath: string, mode: number): Promise<string>;
|
|
49
|
+
rename(remotePath: string, remotePathNew: string): Promise<string>;
|
|
50
|
+
rmFolder(remotePath: string): Promise<string>;
|
|
51
|
+
rm(remotePath: string): Promise<string>;
|
|
52
|
+
readFile(remotePath: string, options?: {
|
|
53
|
+
chunkSize?: number;
|
|
54
|
+
}): Promise<Buffer>;
|
|
55
|
+
writeFile(remotePath: string, str: Buffer | string, mode?: number, _options?: {
|
|
56
|
+
chunkSize?: number;
|
|
57
|
+
}): Promise<void>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
declare interface SshFsOptions {
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
declare interface Stats {
|
|
64
|
+
isDirectory: () => boolean;
|
|
65
|
+
isFile: () => boolean;
|
|
66
|
+
isBlockDevice: () => boolean;
|
|
67
|
+
isCharacterDevice: () => boolean;
|
|
68
|
+
isSymbolicLink: () => boolean;
|
|
69
|
+
isFIFO: () => boolean;
|
|
70
|
+
isSocket: () => boolean;
|
|
71
|
+
size: number;
|
|
72
|
+
mode: number;
|
|
73
|
+
uid: number;
|
|
74
|
+
gid: number;
|
|
75
|
+
atime: number;
|
|
76
|
+
mtime: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export declare class Transfer {
|
|
80
|
+
private session;
|
|
81
|
+
private options;
|
|
82
|
+
private chunkSize;
|
|
83
|
+
private state;
|
|
84
|
+
private aborted;
|
|
85
|
+
constructor(sshFs: SshFs, options: TransferOptions);
|
|
86
|
+
startTransfer(): Promise<void>;
|
|
87
|
+
private getRemoteFileSize;
|
|
88
|
+
private runExec;
|
|
89
|
+
private download;
|
|
90
|
+
private upload;
|
|
91
|
+
pause(): void;
|
|
92
|
+
resume(): void;
|
|
93
|
+
getState(): TransferState;
|
|
94
|
+
destroy(): void;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export declare interface TransferOptions {
|
|
98
|
+
type: TransferType;
|
|
99
|
+
remotePath: string;
|
|
100
|
+
localPath: string;
|
|
101
|
+
chunkSize?: number;
|
|
102
|
+
onProgress?: (transferred: number, total: number) => void;
|
|
103
|
+
onData?: (count: number) => void;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export declare interface TransferState {
|
|
107
|
+
transferred: number;
|
|
108
|
+
total: number;
|
|
109
|
+
paused: boolean;
|
|
110
|
+
completed: boolean;
|
|
111
|
+
error?: Error;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export declare type TransferType = 'download' | 'upload';
|
|
115
|
+
|
|
116
|
+
export { }
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
class Transfer {
|
|
4
|
+
session;
|
|
5
|
+
options;
|
|
6
|
+
chunkSize;
|
|
7
|
+
state;
|
|
8
|
+
aborted = false;
|
|
9
|
+
constructor(sshFs, options) {
|
|
10
|
+
this.session = sshFs.session;
|
|
11
|
+
this.options = options;
|
|
12
|
+
this.chunkSize = options.chunkSize || 32768;
|
|
13
|
+
this.state = {
|
|
14
|
+
transferred: 0,
|
|
15
|
+
total: 0,
|
|
16
|
+
paused: false,
|
|
17
|
+
completed: false
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
async startTransfer() {
|
|
21
|
+
if (this.state.completed || this.state.error || this.aborted) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const isDownload = this.options.type === "download";
|
|
25
|
+
try {
|
|
26
|
+
if (isDownload) {
|
|
27
|
+
await this.download();
|
|
28
|
+
} else {
|
|
29
|
+
await this.upload();
|
|
30
|
+
}
|
|
31
|
+
this.state.completed = true;
|
|
32
|
+
} catch (err) {
|
|
33
|
+
this.state.error = err;
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async getRemoteFileSize(remotePath) {
|
|
38
|
+
const output = await this.runExec(`stat -c %s "${remotePath}" 2>/dev/null || stat -f %z "${remotePath}" 2>/dev/null`);
|
|
39
|
+
return parseInt(output.trim(), 10) || 0;
|
|
40
|
+
}
|
|
41
|
+
runExec(cmd, stdinData) {
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
this.session.exec(cmd, (err, stream) => {
|
|
44
|
+
if (err) {
|
|
45
|
+
return reject(err);
|
|
46
|
+
}
|
|
47
|
+
if (stdinData) {
|
|
48
|
+
stream.stdin.write(stdinData);
|
|
49
|
+
stream.stdin.end();
|
|
50
|
+
}
|
|
51
|
+
let out = Buffer.from("");
|
|
52
|
+
stream.on("close", () => {
|
|
53
|
+
resolve(out.toString());
|
|
54
|
+
}).on("data", (data) => {
|
|
55
|
+
out = Buffer.concat([out, data]);
|
|
56
|
+
}).stderr.on("data", (data) => {
|
|
57
|
+
console.error("stderr:", data.toString());
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async download() {
|
|
63
|
+
const { remotePath, localPath, onProgress, onData } = this.options;
|
|
64
|
+
const dir = path.dirname(localPath);
|
|
65
|
+
if (!fs.existsSync(dir)) {
|
|
66
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
67
|
+
}
|
|
68
|
+
this.state.total = await this.getRemoteFileSize(remotePath);
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
const cmd = `dd if="${remotePath}" bs=${this.chunkSize} 2>/dev/null`;
|
|
71
|
+
this.session.exec(cmd, (err, stream) => {
|
|
72
|
+
if (err) {
|
|
73
|
+
return reject(err);
|
|
74
|
+
}
|
|
75
|
+
const writeStream = fs.createWriteStream(localPath);
|
|
76
|
+
let localTransferred = 0;
|
|
77
|
+
stream.on("data", (data) => {
|
|
78
|
+
if (this.state.paused || this.aborted) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
writeStream.write(data);
|
|
82
|
+
localTransferred += data.length;
|
|
83
|
+
this.state.transferred = localTransferred;
|
|
84
|
+
if (onProgress) {
|
|
85
|
+
onProgress(localTransferred, this.state.total);
|
|
86
|
+
}
|
|
87
|
+
if (onData) {
|
|
88
|
+
onData(data.length);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
stream.on("close", () => {
|
|
92
|
+
writeStream.end(() => {
|
|
93
|
+
resolve();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
stream.on("error", (err2) => {
|
|
97
|
+
this.state.error = err2;
|
|
98
|
+
writeStream.end();
|
|
99
|
+
reject(err2);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
async upload() {
|
|
105
|
+
const { remotePath, localPath, onProgress, onData } = this.options;
|
|
106
|
+
const fileContent = fs.readFileSync(localPath);
|
|
107
|
+
this.state.total = fileContent.length;
|
|
108
|
+
await this.runExec(`dd of="${remotePath}" bs=${this.chunkSize} 2>/dev/null`, fileContent);
|
|
109
|
+
this.state.transferred = this.state.total;
|
|
110
|
+
if (onProgress) {
|
|
111
|
+
onProgress(this.state.total, this.state.total);
|
|
112
|
+
}
|
|
113
|
+
if (onData) {
|
|
114
|
+
onData(fileContent.length);
|
|
115
|
+
}
|
|
116
|
+
this.state.completed = true;
|
|
117
|
+
}
|
|
118
|
+
pause() {
|
|
119
|
+
this.state.paused = true;
|
|
120
|
+
}
|
|
121
|
+
resume() {
|
|
122
|
+
this.state.paused = false;
|
|
123
|
+
}
|
|
124
|
+
getState() {
|
|
125
|
+
return { ...this.state };
|
|
126
|
+
}
|
|
127
|
+
destroy() {
|
|
128
|
+
this.aborted = true;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export {
|
|
132
|
+
Transfer
|
|
133
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ssh2-scp",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Remote file system operations over SSH2 session without SFTP",
|
|
5
|
+
"homepage": "https://github.com/electerm/ssh2-scp#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/electerm/ssh2-scp/issues"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/electerm/ssh2-scp.git"
|
|
12
|
+
},
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "ZHAO Xudong <zxdong@gmail.com>",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./dist/cjs/ssh-fs.js",
|
|
17
|
+
"module": "./dist/esm/ssh-fs.js",
|
|
18
|
+
"types": "./dist/esm/ssh-fs.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"import": "./dist/esm/ssh-fs.js",
|
|
22
|
+
"require": "./dist/cjs/ssh-fs.js",
|
|
23
|
+
"types": "./dist/esm/ssh-fs.d.ts"
|
|
24
|
+
},
|
|
25
|
+
"./transfer": {
|
|
26
|
+
"import": "./dist/esm/transfer.js",
|
|
27
|
+
"require": "./dist/cjs/transfer.js",
|
|
28
|
+
"types": "./dist/esm/transfer.d.ts"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"README.md",
|
|
34
|
+
"README-CN.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "vite build --config build/vite.config.ts",
|
|
39
|
+
"build:esm": "vite build --config build/vite.config.ts --outDir dist/esm",
|
|
40
|
+
"build:cjs": "vite build --config build/vite.config.cjs.ts --outDir dist/cjs",
|
|
41
|
+
"test": "node test/ssh2-fs.test.mjs && node test/transfer.test.mjs",
|
|
42
|
+
"lint": "echo \"Lint not configured\" && exit 0",
|
|
43
|
+
"typecheck": "tsc --noEmit"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^20.10.0",
|
|
47
|
+
"@types/ssh2": "^1.11.0",
|
|
48
|
+
"@types/tar": "^6.1.0",
|
|
49
|
+
"ssh2": "^1.15.0",
|
|
50
|
+
"tar": "^6.2.0",
|
|
51
|
+
"typescript": "^5.3.0",
|
|
52
|
+
"vite": "^5.0.0",
|
|
53
|
+
"vite-plugin-dts": "^3.6.0"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=22.0.0"
|
|
57
|
+
}
|
|
58
|
+
}
|