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 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
+ }