自定义上传本地文件夹到七牛云
*** 应用于我公司开发的桌面应用安装包自动更新***
实现上传本地文件夹到七牛云的一个完整流程
前言
首先保证本地有qiniu插件
yarn add qiniu
其他的借助的是node环境,通过命令行实现上传
在package.json中,有两行这个命令
"scripts":{***"upload": "cross-env NODE_ENV=production node upload_to_qiniu/index.cjs","upload:dev": "cross-env NODE_ENV=development node upload_to_qiniu/index.cjs",***
}
上面两个命令能看出来,分别对应生产环境和开发环境,通过node执行当前json文件同级目录下的upload_to_qiniu/index.cjs文件,这个文件就是执行的上传文件夹到七牛云。
下面是index.cjs内容。
基本配置
const qiniu = require("qiniu");
const path = require("path");
const fs = require("fs-extra");
const { version } = require("../package.json");//要上传的文件的版本号
let env = process.env.NODE_ENV;//对应执行命令的NODE_ENV
console.log(`output->当前环境`, env);
const configPath = {// 生产环境配置production: {releaseDir: "本地要上传目标文件夹名称",qiniu: {bucket: "***",domain: "***",uploadFolder: "要上传到七牛云的文件夹位置"},},// 测试环境配置development: {releaseDir: "本地要上传目标文件夹名称",qiniu: {bucket: "***", // 使用测试专用桶domain: "***",uploadFolder: "要上传到七牛云的文件夹位置"},}
};// 七牛云配置
const accessKey = "***";
const secretKey = "***";// 获取当前环境配置
const envConfig = configPath[env];
const { bucket, domain, uploadFolder } = envConfig.qiniu;
const releaseDir_config = envConfig.releaseDir;// 初始化
const mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
const config = new qiniu.conf.Config();
const formUploader = new qiniu.form_up.FormUploader(config);
const putExtra = new qiniu.form_up.PutExtra();// 文件扩展名到 MIME 类型的映射
const MIME_TYPES = {// ".exe": "application/octet-stream",//最开始用的这个,发现上传之后,exe文件会默认被转为图片类型,应该是跟七牛云的配置有关".exe": "application/x-msdownload",".json": "application/json",".yml": "application/yaml",".yaml": "application/yaml",".zip": "application/zip",".gz": "application/gzip",".dmg": "application/x-apple-diskimage",".appimage": "application/x-appimage"
};// 需要排除的文件夹
const EXCLUDED_FOLDERS = ["win-unpacked"];
上传文件
上传方法
//上传方法
function main(){
try{
console.log(`开始上传版本 ${version} 到七牛云...`);// 获取 要上传文件 目录路径
const releaseDir = path.join(__dirname, "../" + releaseDir_config);//这个路径是相对于本index.cjs的路径,所以在这里加了一个../// 检查目录是否存在if (!(await fs.pathExists(releaseDir))) {throw new Error(releaseDir_config + "目录不存在,请检查路径");}// 上传整个 release 目录(排除指定文件夹)console.log(`开始上传 ${releaseDir_config} 目录...`);const uploadResults = await uploadDirectory(releaseDir, uploadFolder);console.log(`目录上传完成,共 ${uploadResults.length} 个文件`);}catch(error)console.error("上传过程中发生错误:", error);process.exit(1);{}
}
递归上传目录
// 检查路径是否应该被排除
function shouldExclude(filePath) {return EXCLUDED_FOLDERS.some(folder =>filePath.includes(path.sep + folder + path.sep) ||filePath.endsWith(path.sep + folder));
}
async function uploadDirectory(localDir,remoteDir){const files = await fs.readdir(localDir);//读取本地文件夹const uploadResults = []; for (const file of files) {const localPath = path.join(localDir, file);const stat = await fs.stat(localPath);// 检查是否需要排除此路径if (shouldExclude(localPath)) {console.log(`跳过排除的路径: ${localPath}`);continue;}if (stat.isDirectory()) {// 递归上传子目录const subRemoteDir = path.join(remoteDir, file).replace(/\\/g, "/");const subResults = await uploadDirectory(localPath, subRemoteDir);uploadResults.push(...subResults);} else {// 上传文件const remoteKey = path.join(remoteDir, file).replace(/\\/g, "/");try {const url = await uploadFile(localPath, remoteKey);uploadResults.push(url);console.log(`上传成功: ${url}`);} catch (error) {console.error(`上传失败: ${localPath}`, error);}}
}return uploadResults;
}
根据上传的exe文件生成版本信息json,并上传
// 生成并上传版本信息 JSON
async function uploadVersionJson(exeUrl) {const versionInfo = {version,releaseDate: new Date().toISOString(),updateLog: "修复了一些问题,提升了稳定性",forceUpdate: true, // 强制更新标记downloadUrl: exeUrl};const jsonPath = path.join(__dirname, "version.json");fs.writeFileSync(jsonPath, JSON.stringify(versionInfo, null, 2));const remoteKey = `${uploadFolder}/version.json`;return uploadFile(jsonPath, remoteKey);
}// 上传版本信息 JSON// 获取 exe 文件列表const exeFiles = (await fs.readdir(releaseDir)).filter(file => file.endsWith(".exe")).filter(file => !shouldExclude(path.join(releaseDir, file)));const exeFile = exeFiles[0];const exeUrl = `${domain}/${uploadFolder}/${exeFile}`;const jsonUrl = await uploadVersionJson(exeUrl);console.log(`版本信息上传成功: ${jsonUrl}`);console.log(`上传完成,共 ${exeFiles.length} 个 exe 文件`);
完整代码如下:
const qiniu = require("qiniu");
const path = require("path");
const fs = require("fs-extra");
const { version } = require("../package.json");
let env = process.env.NODE_ENV;
console.log(`output->当前环境`, env);
const configPath = {// 生产环境配置production: {releaseDir: "release",qiniu: {bucket: "",domain: "",uploadFolder: ""},},// 测试环境配置development: {releaseDir: "",qiniu: {bucket: "", // 使用测试专用桶domain: "",uploadFolder: ""},}
};// 七牛云配置
const accessKey = "";
const secretKey = "";// 获取当前环境配置
const envConfig = configPath[env];
const { bucket, domain, uploadFolder } = envConfig.qiniu;
const releaseDir_config = envConfig.releaseDir;// 初始化
const mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
const config = new qiniu.conf.Config();
const formUploader = new qiniu.form_up.FormUploader(config);
const putExtra = new qiniu.form_up.PutExtra();// 文件扩展名到 MIME 类型的映射
const MIME_TYPES = {// ".exe": "application/octet-stream",".exe": "application/x-msdownload",".json": "application/json",".yml": "application/yaml",".yaml": "application/yaml",".zip": "application/zip",".gz": "application/gzip",".dmg": "application/x-apple-diskimage",".appimage": "application/x-appimage"
};// 需要排除的文件夹
const EXCLUDED_FOLDERS = ["win-unpacked"];// 检查路径是否应该被排除
function shouldExclude(filePath) {return EXCLUDED_FOLDERS.some(folder =>filePath.includes(path.sep + folder + path.sep) ||filePath.endsWith(path.sep + folder));
}
// 获取文件的 MIME 类型
function getMimeType(filePath) {const ext = path.extname(filePath).toLowerCase();return MIME_TYPES[ext] || "application/octet-stream";
}// 上传单个文件
async function uploadFile(localFile, key) {return new Promise((resolve, reject) => {// const options = { scope: bucket };const options = {scope: `${bucket}:${key}`, // 指定具体文件路径允许覆盖insertOnly: 0 // 允许覆盖已存在文件};const putPolicy = new qiniu.rs.PutPolicy(options);const uploadToken = putPolicy.uploadToken(mac);const mimeType = getMimeType(localFile);putExtra.mimeType = mimeType;formUploader.putFile(uploadToken,key,localFile,putExtra,(respErr, respBody, respInfo) => {if (respErr) reject(respErr);else if (respInfo.statusCode === 200) resolve(`${domain}/${key}`);elsereject(new Error(`上传失败: ${respInfo.statusCode} - ${JSON.stringify(respBody)}`));});});
}// 生成并上传版本信息 JSON
async function uploadVersionJson(exeUrl) {const versionInfo = {version,releaseDate: new Date().toISOString(),updateLog: "修复了一些问题,提升了稳定性",forceUpdate: true, // 强制更新标记downloadUrl: exeUrl};const jsonPath = path.join(__dirname, "version.json");fs.writeFileSync(jsonPath, JSON.stringify(versionInfo, null, 2));const remoteKey = `${uploadFolder}/version.json`;return uploadFile(jsonPath, remoteKey);
}// 递归上传目录
async function uploadDirectory(localDir, remoteDir) {const files = await fs.readdir(localDir);const uploadResults = [];for (const file of files) {const localPath = path.join(localDir, file);const stat = await fs.stat(localPath);// 检查是否需要排除此路径if (shouldExclude(localPath)) {console.log(`跳过排除的路径: ${localPath}`);continue;}if (stat.isDirectory()) {// 递归上传子目录const subRemoteDir = path.join(remoteDir, file).replace(/\\/g, "/");const subResults = await uploadDirectory(localPath, subRemoteDir);uploadResults.push(...subResults);} else {// 上传文件const remoteKey = path.join(remoteDir, file).replace(/\\/g, "/");try {const url = await uploadFile(localPath, remoteKey);uploadResults.push(url);console.log(`上传成功: ${url}`);} catch (error) {console.error(`上传失败: ${localPath}`, error);}}}return uploadResults;
}// 主函数
async function main() {try {console.log(`开始上传版本 ${version} 到七牛云...`);const releaseDir = path.join(__dirname, "../" + releaseDir_config);// 检查目录是否存在if (!(await fs.pathExists(releaseDir))) {throw new Error(releaseDir_config + "目录不存在,请检查路径");}// 上传整个 release 目录(排除指定文件夹)console.log(`开始上传 ${releaseDir_config} 目录...`);const uploadResults = await uploadDirectory(releaseDir, uploadFolder);console.log(`目录上传完成,共 ${uploadResults.length} 个文件`);// 上传版本信息 JSON// 获取 exe 文件列表const exeFiles = (await fs.readdir(releaseDir)).filter(file => file.endsWith(".exe")).filter(file => !shouldExclude(path.join(releaseDir, file)));const exeFile = exeFiles[0];const exeUrl = `${domain}/${uploadFolder}/${exeFile}`;const jsonUrl = await uploadVersionJson(exeUrl);console.log(`版本信息上传成功: ${jsonUrl}`);console.log(`上传完成,共 ${exeFiles.length} 个 exe 文件`);} catch (error) {console.error("上传过程中发生错误:", error);process.exit(1);}
}main().catch(console.error);