ftp-cos-deploy 1.0.0

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/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # git-push-dir
2
+ - 基于Git的部署工具
3
+ - 可将指定目录推送至远程Git仓库
4
+
5
+ ## 使用方法
6
+
7
+ 1. 安装依赖
8
+ ```bash
9
+ npm install git-push-dir
10
+ ```
11
+
12
+ 2. 引入模块
13
+ ```javascript
14
+ // deploy.js
15
+ import { deploy } from 'git-push-dir'
16
+
17
+ deploy({
18
+ /** 远程仓库地址 (例如: https://gitee.com/user/repo.git) */
19
+ remoteUrl: string;
20
+ /** 推送的分支名,默认为 'dist' */
21
+ branch?: string;
22
+ /** 项目根目录,默认为 process.cwd() */
23
+ rootDir?: string;
24
+ /** 构建输出目录名 (相对于 rootDir),默认为 'dist' */
25
+ distDir?: string;
26
+ /** 需要从根目录复制到发布目录的文件列表 */
27
+ filesToCopy?: string[];
28
+ /** 自定义日志打印函数 */
29
+ logger?: (msg: string) => void;
30
+ });
31
+ ```
32
+
33
+ 3. 控制台执行
34
+ ```bash
35
+ node deploy.js
36
+ ```
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "ftp-cos-deploy",
3
+ "version": "1.0.0",
4
+ "description": "一款基于FTP和COS的部署工具,可将指定目录推送至远程FTP服务器并上传至COS存储桶。",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "module": "./src/index.js",
8
+ "types": "./src/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./src/index.d.ts",
12
+ "require": "./src/index.js",
13
+ "import": "./src/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "src"
18
+ ],
19
+ "keywords": [
20
+ "deploy",
21
+ "ftp",
22
+ "cos"
23
+ ],
24
+ "author": "hsg",
25
+ "license": "MIT",
26
+ "dependencies": {
27
+ "basic-ftp": "^5.1.0",
28
+ "chalk": "^5.6.2",
29
+ "cli-progress": "^3.12.0",
30
+ "cos-nodejs-sdk-v5": "^2.15.4"
31
+ }
32
+ }
package/src/cos.js ADDED
@@ -0,0 +1,231 @@
1
+ /**
2
+ * 腾讯云COS文件/文件夹上传模块(Node.js版)
3
+ * ES6语法实现带进度条功能
4
+ * @param {Object} config - COS配置参数
5
+ * @param {string} config.SecretId - 腾讯云SecretId
6
+ * @param {string} config.SecretKey - 腾讯云SecretKey
7
+ * @param {string} config.Bucket - 存储桶名称
8
+ * @param {string} config.Region - 存储桶地域
9
+ * @param {Function} onProgress - 上传进度回调函数
10
+ * @returns {Object} 包含upload方法的对象
11
+ */
12
+ import COS from 'cos-nodejs-sdk-v5';
13
+ import fs from 'fs/promises';
14
+ import path from 'path';
15
+ import { createReadStream } from 'fs';
16
+ // 导入进度条模块
17
+ import { createFileProgressBar } from './progress.js';
18
+
19
+ /**
20
+ * 创建COS上传工具实例
21
+ * @param {Object} config - COS配置参数
22
+ * @returns {Object} 上传工具对象
23
+ */
24
+ export function createCos(config) {
25
+ // 配置参数验证
26
+ if (!config || !config.SecretId || !config.SecretKey || !config.Bucket || !config.Region) {
27
+ throw new Error('缺少必要的配置参数');
28
+ }
29
+
30
+ // 默认配置
31
+ const cosConfig = {
32
+ ...config,
33
+ timeout: config.timeout || 300000, // 5分钟超时时间
34
+ chunkSize: config.chunkSize || 1024 * 1024 * 5, // 5MB分片大小
35
+ showProgress: config.showProgress !== false, // 默认显示进度条
36
+ };
37
+
38
+ // 初始化COS实例
39
+ const cos = new COS({
40
+ SecretId: cosConfig.SecretId,
41
+ SecretKey: cosConfig.SecretKey,
42
+ });
43
+
44
+ /**
45
+ * 递归遍历目录并上传文件
46
+ * @param {string} dirPath - 目录路径
47
+ * @param {string} baseDir - 基础目录路径
48
+ * @param {string} remotePrefix - 远程存储路径前缀
49
+ * @param {Function} fileHandler - 文件处理函数
50
+ */
51
+ const traverseDirectory = async (dirPath, baseDir, remotePrefix, fileHandler) => {
52
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
53
+
54
+ for (const entry of entries) {
55
+ const fullPath = path.join(dirPath, entry.name);
56
+
57
+ if (entry.isDirectory()) {
58
+ await traverseDirectory(fullPath, baseDir, remotePrefix, fileHandler);
59
+ } else if (entry.isFile()) {
60
+ await fileHandler(fullPath);
61
+ }
62
+ }
63
+ };
64
+
65
+ /**
66
+ * 上传单个文件到COS
67
+ * @param {string} filePath - 本地文件路径
68
+ * @param {string} baseDir - 基础目录路径
69
+ * @param {string} remotePrefix - 远程存储路径前缀
70
+ */
71
+ const uploadFileToCOS = async (filePath, baseDir, remotePrefix) => {
72
+ try {
73
+ // 获取相对路径以保持目录结构
74
+ const relativePath = path.relative(baseDir, filePath);
75
+ const key = remotePrefix
76
+ ? path.posix.join(remotePrefix, relativePath.replace(/\\/g, '/'))
77
+ : relativePath.replace(/\\/g, '/');
78
+
79
+ // 获取文件信息
80
+ const stats = await fs.stat(filePath);
81
+ const fileSize = stats.size;
82
+
83
+ // 创建文件进度条
84
+ let fileBar = null;
85
+ if (cosConfig.showProgress) {
86
+ fileBar = createFileProgressBar(filePath);
87
+ }
88
+
89
+ // 小文件直接上传
90
+ if (fileSize <= cosConfig.chunkSize) {
91
+ return new Promise((resolve) => {
92
+ cos.putObject({
93
+ Bucket: cosConfig.Bucket,
94
+ Region: cosConfig.Region,
95
+ Key: key,
96
+ Body: createReadStream(filePath),
97
+ ContentLength: fileSize,
98
+ onProgress: (progressData) => {
99
+ // 更新文件进度条
100
+ if (fileBar) {
101
+ fileBar.update(progressData.loaded);
102
+ }
103
+
104
+ // 调用用户提供的进度回调
105
+ cosConfig.onProgress?.({
106
+ key,
107
+ percent: progressData.percent * 100,
108
+ loaded: progressData.loaded,
109
+ total: progressData.total,
110
+ file: filePath
111
+ });
112
+ }
113
+ }, (err, data) => {
114
+ // 结束文件进度条
115
+ if (fileBar) {
116
+ fileBar.end();
117
+ }
118
+
119
+ if (err) {
120
+ resolve({
121
+ success: false,
122
+ key,
123
+ path: filePath,
124
+ error: err.message
125
+ });
126
+ } else {
127
+ resolve({
128
+ success: true,
129
+ key,
130
+ path: filePath,
131
+ etag: data.ETag,
132
+ requestId: data.RequestId
133
+ });
134
+ }
135
+ });
136
+ });
137
+ }
138
+
139
+ // 大文件分片上传
140
+ return new Promise((resolve) => {
141
+ cos.sliceUploadFile({
142
+ Bucket: cosConfig.Bucket,
143
+ Region: cosConfig.Region,
144
+ Key: key,
145
+ FilePath: filePath,
146
+ SliceSize: cosConfig.chunkSize,
147
+ onProgress: (progressData) => {
148
+ // 更新文件进度条
149
+ if (fileBar) {
150
+ fileBar.update(progressData.loaded);
151
+ }
152
+
153
+ // 调用用户提供的进度回调
154
+ cosConfig.onProgress?.({
155
+ key,
156
+ percent: progressData.percent * 100,
157
+ loaded: progressData.loaded,
158
+ total: progressData.total,
159
+ file: filePath
160
+ });
161
+ }
162
+ }, (err, data) => {
163
+ // 结束文件进度条
164
+ if (fileBar) {
165
+ fileBar.end();
166
+ }
167
+
168
+ if (err) {
169
+ resolve({
170
+ success: false,
171
+ key,
172
+ path: filePath,
173
+ error: err.message
174
+ });
175
+ } else {
176
+ resolve({
177
+ success: true,
178
+ key,
179
+ path: filePath,
180
+ etag: data.ETag,
181
+ requestId: data.RequestId
182
+ });
183
+ }
184
+ });
185
+ });
186
+ } catch (error) {
187
+
188
+ return {
189
+ success: false,
190
+ path: filePath,
191
+ error: error.message
192
+ };
193
+ }
194
+ };
195
+
196
+ /**
197
+ * 上传文件或文件夹
198
+ * @param {string} localPath - 本地文件或文件夹路径
199
+ * @param {string} [remotePrefix=''] - 远程存储路径前缀
200
+ */
201
+ const upload = async (localPath, remotePrefix = '') => {
202
+ // 验证路径是否存在
203
+ try {
204
+ await fs.access(localPath);
205
+ } catch (error) {
206
+ throw new Error(`路径不存在: ${localPath}`);
207
+ }
208
+
209
+ // 获取路径信息
210
+ const stats = await fs.stat(localPath);
211
+ const results = [];
212
+
213
+ if (stats.isDirectory()) {
214
+ // 处理文件夹
215
+ const baseDir = path.dirname(localPath);
216
+ await traverseDirectory(localPath, baseDir, remotePrefix, (filePath) => {
217
+ results.push(uploadFileToCOS(filePath, baseDir, remotePrefix));
218
+ });
219
+ } else {
220
+ // 处理单个文件
221
+ results.push(uploadFileToCOS(localPath, path.dirname(localPath), remotePrefix));
222
+ }
223
+
224
+ return Promise.all(results);
225
+ };
226
+
227
+ // 返回公共方法
228
+ return {
229
+ upload
230
+ };
231
+ }
package/src/ftp.js ADDED
@@ -0,0 +1,148 @@
1
+ import ftp from 'basic-ftp';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { createFileProgressBar } from './progress.js';
5
+
6
+ /**
7
+ * FTP文件/文件夹上传模块(Node.js版)
8
+ * ES6语法实现带进度条功能
9
+ * @param {Object} config - FTP配置参数
10
+ * @param {string} config.host - 服务器主机名,默认值:localhost
11
+ * @param {number} [config.port=21] - 服务器端口号,默认值:21
12
+ * @param {string} config.user - 用户名,默认值:anonymous
13
+ * @param {string} config.password - 密码,默认值:guest
14
+ * @param {boolean|string} [config.secure=false] - 是否使用显式 FTPS 加密,默认值:false。如果需要支持旧版隐式 FTPS,请设置为 "implicit"。
15
+ * @param {Object} [config.secureOptions=null] - TLS 选项,与 Node.js 中的 tls.connect() 相同。默认值:null
16
+ * @param {string} [config.remoteDir='/'] - 远程目录,默认值:/
17
+ * @returns {Object} 包含upload方法的对象
18
+ */
19
+ export function createFtp(config) {
20
+ const ftpConfig = {
21
+ ...config,
22
+ remoteDir: config.remoteDir.replace(/\\/g, '/').endsWith('/')
23
+ ? config.remoteDir.replace(/\\/g, '/')
24
+ : config.remoteDir.replace(/\\/g, '/') + '/'
25
+ }
26
+
27
+ /**
28
+ * 上传文件或文件夹到FTP(支持进度条)
29
+ * @param {string} localPath - 本地文件/文件夹路径
30
+ * @param {string} [remoteName] - 远程保存名称(文件夹上传时可选,默认使用本地名称)
31
+ */
32
+ async function upload(localPath, remoteName) {
33
+ const client = new ftp.Client();
34
+ try {
35
+ await client.access(ftpConfig);
36
+ const stats = fs.statSync(localPath);
37
+
38
+ const safeRemoteName = remoteName ? remoteName.replace(/\\/g, '/') : path.basename(localPath).replace(/\\/g, '/');
39
+ const remoteFolderPath = ftpConfig.remoteDir + safeRemoteName;
40
+
41
+ // 1. 如果是文件,直接上传
42
+ if (stats.isFile()) {
43
+ await uploadSingleFile(client, localPath, remoteFolderPath);
44
+ } else if (stats.isDirectory()) {
45
+ // 2. 如果是文件夹,递归上传所有内容
46
+
47
+ // 创建远程文件夹(如果不存在)
48
+ await client.ensureDir(remoteFolderPath);
49
+ console.log(`📂 开始上传文件夹:${localPath} → ${remoteFolderPath}`);
50
+
51
+ // 获取文件夹内所有文件(含子目录)
52
+ const allFiles = getAllFilesInDir(localPath);
53
+ const totalFiles = allFiles.length;
54
+
55
+ // 初始化总进度条
56
+ const mainBar = createFolderProgressBar(totalFiles);
57
+
58
+ // 逐个上传文件
59
+ for (let i = 0; i < allFiles.length; i++) {
60
+ const file = allFiles[i];
61
+ // 计算远程文件路径(保持目录结构)
62
+ const relativePath = path.relative(localPath, file);
63
+ const remoteFilePath = path.join(remoteFolderPath, relativePath);
64
+
65
+ // 上传单个文件,并更新总进度条
66
+ await uploadSingleFile(client, file, remoteFilePath, false);
67
+ mainBar.update(i + 1);
68
+ }
69
+ mainBar.end();
70
+ console.log(`\n✅ 文件夹上传完成:共 ${totalFiles} 个文件`);
71
+ }
72
+
73
+ } catch (err) {
74
+ console.error('\n❌ 上传失败:', err.message);
75
+ } finally {
76
+ client.close();
77
+ }
78
+ }
79
+
80
+ /**
81
+ * 上传单个文件(内部使用,支持进度条)
82
+ * @param {ftp.Client} client - FTP客户端实例
83
+ * @param {string} localFilePath - 本地文件路径
84
+ * @param {string} remoteFilePath - 远程文件路径
85
+ * @param {boolean} [showProgress=true] - 是否显示单个文件进度条
86
+ */
87
+ async function uploadSingleFile(client, localFilePath, remoteFilePath, showProgress = true) {
88
+ let fileBar = null;
89
+
90
+ // 初始化单个文件进度条(如果需要)
91
+ if (showProgress) {
92
+ fileBar = createFileProgressBar(localFilePath);
93
+ }
94
+
95
+ // 读取文件流并监听进度
96
+ const fileStream = fs.createReadStream(localFilePath);
97
+ let uploadedBytes = 0;
98
+
99
+ fileStream.on('data', (chunk) => {
100
+ uploadedBytes += chunk.length;
101
+ if (showProgress) {
102
+ fileBar.update(uploadedBytes);
103
+ }
104
+ });
105
+
106
+ try {
107
+ // 执行上传
108
+ await client.uploadFrom(localFilePath, remoteFilePath);
109
+
110
+ // 清理进度条
111
+ if (showProgress) {
112
+ fileBar.end();
113
+ // console.log(`✅ ${path.basename(localFilePath)} 上传完成\n`);
114
+ }
115
+ } catch (error) {
116
+ fileBar.end();
117
+ throw error
118
+ }
119
+ }
120
+
121
+ /**
122
+ * 递归获取文件夹内所有文件路径
123
+ * @param {string} dir - 文件夹路径
124
+ * @returns {string[]} 文件路径列表
125
+ */
126
+ function getAllFilesInDir(dir) {
127
+ let results = [];
128
+ const list = fs.readdirSync(dir);
129
+
130
+ list.forEach((file) => {
131
+ const fullPath = path.join(dir, file);
132
+ const stats = fs.statSync(fullPath);
133
+
134
+ if (stats.isDirectory()) {
135
+ // 递归处理子目录
136
+ results = results.concat(getAllFilesInDir(fullPath));
137
+ } else {
138
+ // 添加文件路径
139
+ results.push(fullPath);
140
+ }
141
+ });
142
+ return results;
143
+ }
144
+
145
+ return {
146
+ upload
147
+ }
148
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,66 @@
1
+ declare module 'ftp-cos-deploy' {
2
+ interface CosOptions {
3
+ Bucket: string
4
+ Region: string
5
+ SecretId: string
6
+ SecretKey: string
7
+ timeout?: number // 超时时间,单位毫秒
8
+ chunkSize?: number // 分片大小,单位字节
9
+ showProgress?: boolean // 是否显示进度条
10
+ }
11
+
12
+ interface CosClient {
13
+ /**
14
+ * 上传文件或文件夹
15
+ * @param {string} localPath - 本地文件或文件夹路径
16
+ * @param {string} [remotePrefix=''] - 远程存储路径前缀
17
+ */
18
+ uploadFile: (localPath: string, remotePrefix?: string) => Promise<void>
19
+ }
20
+
21
+ interface FtpOptions {
22
+ host: string // 服务器主机名,默认值:localhost
23
+ port?: number // 服务器端口号,默认值:21
24
+ user: string // 用户名,默认值:anonymous
25
+ password: string // 密码,默认值:guest
26
+ secure: boolean | "implicit" // 是否使用显式 FTPS 加密,默认值:false。如果需要支持旧版隐式 FTPS,请设置为 "implicit"。
27
+ secureOptions?: any // TLS 选项,与 Node.js 中的 tls.connect() 相同。默认值:null
28
+ remoteDir?: string // 远程目录,默认值:/
29
+ }
30
+
31
+ interface FtpClient {
32
+ /**
33
+ * 上传文件或文件夹到FTP(支持进度条)
34
+ * @param {string} localPath - 本地文件/文件夹路径
35
+ * @param {string} [remoteName] - 远程保存名称(文件夹上传时可选,默认使用本地名称)
36
+ */
37
+ upload: (localPath: string, remoteName?: string) => Promise<void>
38
+ }
39
+ /**
40
+ * 腾讯云COS文件/文件夹上传模块(Node.js版)
41
+ * ES6语法实现带进度条功能
42
+ * @param {Object} config - COS配置参数
43
+ * @param {string} config.SecretId - 腾讯云SecretId
44
+ * @param {string} config.SecretKey - 腾讯云SecretKey
45
+ * @param {string} config.Bucket - 存储桶名称
46
+ * @param {string} config.Region - 存储桶地域
47
+ * @param {Function} onProgress - 上传进度回调函数
48
+ * @returns {Object} 包含upload方法的对象
49
+ */
50
+ export function createCos(options: CosOptions): CosClient
51
+
52
+ /**
53
+ * FTP文件/文件夹上传模块(Node.js版)
54
+ * ES6语法实现带进度条功能
55
+ * @param {Object} config - FTP配置参数
56
+ * @param {string} config.host - 服务器主机名,默认值:localhost
57
+ * @param {number} [config.port=21] - 服务器端口号,默认值:21
58
+ * @param {string} config.user - 用户名,默认值:anonymous
59
+ * @param {string} config.password - 密码,默认值:guest
60
+ * @param {boolean|string} [config.secure=false] - 是否使用显式 FTPS 加密,默认值:false。如果需要支持旧版隐式 FTPS,请设置为 "implicit"。
61
+ * @param {Object} [config.secureOptions=null] - TLS 选项,与 Node.js 中的 tls.connect() 相同。默认值:null
62
+ * @param {string} [config.remoteDir='/'] - 远程目录,默认值:/
63
+ * @returns {Object} 包含upload方法的对象
64
+ */
65
+ export function createFtp(options: FtpOptions): FtpClient
66
+ }
package/src/index.js ADDED
@@ -0,0 +1,3 @@
1
+
2
+ export { createFtp } from './ftp.js'
3
+ export { createCos } from './cos.js'
@@ -0,0 +1,87 @@
1
+ import cliProgress from 'cli-progress';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import chalk from 'chalk'; // 导入chalk用于颜色处理
5
+
6
+ /**
7
+ * 创建文件夹进度条
8
+ * @param {number} total - 总文件数
9
+ * @returns {object} - 包含更新和结束方法的对象
10
+ */
11
+ export function createFolderProgressBar(total) {
12
+ const bar = new cliProgress.SingleBar(
13
+ {
14
+ format: '总进度 | {bar} | {percentage}% | {value}/{total} 文件',
15
+ barCompleteChar: '\u2588',
16
+ barIncompleteChar: '\u2591',
17
+ hideCursor: true
18
+ },
19
+ cliProgress.Presets.shades_classic
20
+ );
21
+ bar.start(total, 0);
22
+
23
+ return {
24
+ update: (current) => {
25
+ bar.update(current);
26
+ },
27
+ end: () => {
28
+ bar.stop();
29
+ }
30
+ }
31
+ }
32
+
33
+ export function createFileProgressBar(localFilePath, color='green') {
34
+ // 根据颜色参数获取对应的chalk颜色方法
35
+ const colorMethod = chalk[color] || chalk.green;
36
+
37
+ const fileStats = fs.statSync(localFilePath);
38
+ const totalBytes = fileStats.size;
39
+ const { unit, scale } = getUnitAndScale(totalBytes);
40
+
41
+ const bar = new cliProgress.SingleBar(
42
+ {
43
+ format: `正在上传 ${colorMethod('[{bar}]')} {percentage}% | {value}/{total} | ${path.basename(localFilePath)}`,
44
+ barCompleteChar: '\u2588',
45
+ barIncompleteChar: '\u2591',
46
+ hideCursor: true,
47
+ formatValue: (value, options, type) => {
48
+ if (type === 'value' || type === 'total') {
49
+ // 对value和total应用相同的单位和scale
50
+ return `${(value / scale).toFixed(2)}${unit}`;
51
+ }
52
+
53
+ // 其他类型保持默认
54
+ return value.toString();
55
+ }
56
+ },
57
+ cliProgress.Presets.shades_classic
58
+ );
59
+ bar.start(totalBytes, 0);
60
+
61
+ return {
62
+ update: (current) => {
63
+ bar.update(current);
64
+ },
65
+ end: () => {
66
+ bar.stop();
67
+ }
68
+ }
69
+ }
70
+
71
+ // 根据总值动态确定单位
72
+ function getUnitAndScale(totalBytes) {
73
+ const units = ['B', 'KB', 'MB']; // 最大支持MB
74
+ let scale = 1;
75
+ let unit = units[0];
76
+
77
+ // 根据总值确定单位(如1024KB以上用MB)
78
+ if (totalBytes >= 1024 * 1024) {
79
+ scale = 1024 * 1024;
80
+ unit = units[2]; // MB
81
+ } else if (totalBytes >= 1024) {
82
+ scale = 1024;
83
+ unit = units[1]; // KB
84
+ }
85
+
86
+ return { unit, scale };
87
+ }