一 、前端代码
主要组件
<el-upload
ref="uploadRef"
:action="UPLOAD.action"
:data="UPLOAD.data"
name="file"
:auto-upload="false"
:before-upload="beforeUpload"
:on-change="onFileChange"
:on-progress="handleProgress"
:on-success="handleSuccess"
multiple
>
<template #trigger>
<button ref="triggerBtn">select file</button>
</template>
</el-upload>
原本想在beforeUpload中对file.name进行编码,可这个属性没有setter,改不了。
换个思路,前端改不了,那看看后端
我用的node.js的express框架
二、乱码原因:
这个乱码的原因是因为multer模块不支持非Unicode字符,所以中文乱码。
file.originalname 是乱码的(如果 文件名原来是中文)
三、解决方案:
1. 方案一:使用 iconv-lite 模块
const iconv = require('iconv-lite');
转成中文: iconv.decode(file.originalname, 'utf-8')
nodejs+express + multer+iconv 服务端文件上传主要代码
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, req.body.absdir); // 这里使用请求参数中的 absDir 作为文件存储目录
},
filename: function (req, file, cb) {
cb(null, iconv.decode(file.originalname, 'utf-8')); // 使用原始文件名作为文件名
}
});
// 创建 Multer 实例
const upload = multer({ storage: storage });
// 处理文件上传的路由
app.post('/upload', upload.array('file'), (req, res) => {
if (req.files.length === 0) {
res.status(400).send('没有选择要上传的文件');
return;
}
res.status(200).send('文件上传成功');
});
2.方案二:编码处理
我问了gpt,根据它的提示:
multer的文件名编码格式并不支持直接指定为utf-8。multer内部使用的是默认的操作系统编码来保存文件名。如果你的操作系统编码不支持特定字符,那么multer保存的文件名可能会出现乱码。
但你可以尝试通过以下方式解决这个问题:
- 将multer保存的文件名进行编码处理:
Copy
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads'); // 指定上传文件的保存目录
},
filename: function (req, file, cb) {
const originalname = Buffer.from(file.originalname, 'utf-8'); // 将原始文件名进行utf-8编码处理
const extension = path.extname(originalname.toString()); // 将Buffer对象转换为字符串并获取扩展名
const basename = path.basename(originalname.toString(), extension); // 获取文件名
cb(null, basename + '-' + Date.now() + extension); // 生成保存的文件名
}
});
- 在路由中获取文件名时进行解码处理:
app.post('/upload', upload.single('file'), function(req, res) {
const originalname = Buffer.from(req.file.originalname, 'base64').toString('utf-8'); // 将文件名进行解码处理
});
通过以上方式,你可以尝试处理文件名的编码问题。如果这种方式还不能解决乱码问题,可能需要在操作系统级别设置正确的编码环境。
四、后端主要代码
upload 多文件上传 后端主要代码
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, req.body.absdir); // 这里使用请求参数中的 absDir 作为文件存储目录
},
filename: function (req, file, cb) {
const originalname = Buffer.from(file.originalname, 'binary').toString("utf-8"); // 将原始文件名进行utf-8编码处理
cb(null, originalname); // 生成保存的文件名
}
});
const upload = multer({
storage: storage,
fileFilter: function (req, file, cb) {
// 检查文件类型
// if (!file.mimetype.startsWith('image/')) {
// cb(new Error('只允许上传图片文件'));
// } else {
// cb(null, true);
// }
cb(null, true);
},
limits: {
fileSize: 1 * 1024 * 1024 ,// 限制文件大小为 1MB
fileSize: 1 * 1024 * 1024 * 1024 * 2 //限制文件大小为 2GB
}
});
app.post('/upload', upload.single('file'), function(req, res) {
const originalname = Buffer.from(req.file.originalname, 'binary').toString('utf-8'); // 将文件名进行解码处理
console.log("upload==",originalname," "+Buffer.from("你好啊1234", 'utf-8').toString('binary'))
res.send(originalname);
});
全部代码:
一、前端
<template>
<el-button @click="showdialog">
<el-icon :size="20">
<FolderAdd />
</el-icon>
</el-button>
<el-dialog
v-model="dialogVisible"
title="操作"
width="80%"
custom-class="el-dialog2"
destroy-on-close
@close="handleCloseDialog"
>
<el-input v-if="!optionMode" v-model="creatFolderName" placeholder="输入文件夹名"></el-input>
<!-- <span>This is a message</span> -->
<div v-if="optionMode">
<el-row :gutter="4" justify="space-around">
<el-col :span="10">
<div class="option_div add" @click="shouInput">
<div>
<el-icon :size="20">
<FolderAdd />
</el-icon>
</div>
<span>新建文件夹</span>
</div>
</el-col>
<el-col :span="10">
<!-- <div class="option_div upload" @click="doUploadFile" >
<div>
<el-icon :size="20">
<Upload />
</el-icon>
</div>
<span>上传文件</span>
</div> -->
<div class="option_div upload" @click="doUploadFile" >
<div>
<el-icon :size="20">
<Upload />
</el-icon>
</div>
<span>上传文件</span>
<div style="width:0;height:0;margin: 0;padding:0;border: none;overflow: hidden;">
<el-upload
ref="uploadRef"
:action="UPLOAD.action"
:data="UPLOAD.data"
name="file"
:auto-upload="false"
:before-upload="beforeUpload"
:on-change="onFileChange"
:on-progress="handleProgress"
:on-success="handleSuccess"
:on-error="handleError"
multiple
>
<template #trigger>
<button ref="triggerBtn">select file</button>
</template>
</el-upload>
</div>
</div>
</el-col>
</el-row>
</div>
<template #footer>
<span v-if="optionMode" class="dialog-footer" style="color:#1e82ee;">
Author: QX
</span>
<el-button type="primary" v-if="!optionMode" @click="doCreateFolder" :loading="loading">创建</el-button>
<el-row v-for="(item,index) in fileList" :key="item.name+index">
<el-col>
<div>
<span style="color:gray;">{{ item.name +" "+item.size }}</span>
</div>
</el-col>
</el-row>
<el-button @click="startSubmit">发送</el-button>
</template>
</el-dialog>
</template>
<script>
import { FolderAdd, Upload } from "@element-plus/icons-vue";
import { reactive, ref } from 'vue';
import { ElMessage } from 'element-plus';
import axios from 'axios';
import api from '@/api';
export default {
name: "FolderComponent",
components: {
FolderAdd,
Upload
},
props:["updateDir","getjoinPath"],
setup(props){
let { updateDir, getjoinPath } = props;
let dialogVisible = ref(false);
let optionMode = ref(true);
let creatFolderName = ref("");
const uploadRef = ref(null);
const triggerBtn = ref(null);
let fileList = reactive([]);
let UPLOAD = reactive({
action:api.upload,
data:{
absdir:''
}
})
const startSubmit = ()=>{
UPLOAD.data.absdir = getjoinPath();
console.log("上传的参数:");
console.log(UPLOAD.data);
uploadRef.value.submit();
}
const beforeUpload = (file)=> {
console.log("beforeUpload:"+file);
return true;
}
const onFileChange = (file,files) =>{
file;
fileList.splice(0,fileList.length,...files);
let t=[];
files.forEach(file =>{
t.push({
name:file.name,
uid:file.uid
})
})
console.log("onfile changed",file);
}
const handleProgress = (event, file, fileList)=>{
file,fileList;
console.log("event.percent",event.percent)
}
const handleSuccess = (response, file, fileList) => {
console.log("handleSuccess",file.name);
ElMessage.warning("清除数据")
uploadRef.value.clearFiles();
}
const handleError = () => {
ElMessage.warning("清除数据")
console.error("handleError")
uploadRef.value.clearFiles();
}
let loading = ref(false);
const showdialog = () => {
dialogVisible.value = true;
}
const doCreateFolder = () => {
if(creatFolderName.value=="" || creatFolderName.value.length==0){
return ElMessage({
type: 'warning',
message: "文件夹名称不能为空"
})
}
loading.value=true;
// console.log(creatFolderName.value);
//判断文件夹名称是否合法,检测是否有非法字符
let reg = /[`~!@#$%^&*()+<>?:"{},.\/;'[\]]/im;
if(reg.test(creatFolderName.value)){
loading.value=false;
return ElMessage({
type: 'warning',
message: "文件夹名称不能包含非法字符",
});
}else{
axios.post(api.createFolder,{
path:getjoinPath(),
name: creatFolderName.value
}).then(res => {
loading.value=false;
creatFolderName.value = "";
if(res.data.code == 200){
ElMessage.success(res.data.msg);
updateDir();
optionMode.value = true;
dialogVisible.value = false;
}else{
ElMessage.error(res.data.msg);
updateDir();
optionMode.value = false;
}
})
.catch(err => {
loading.value=false;
creatFolderName.value = "";
console.log(err);
})
}
}
const handleCloseDialog = ()=>{
dialogVisible.value=false;
optionMode.value=true;
}
const doUploadFile = () => {
// 模拟点击上传组件
triggerBtn.value.click();
uploadRef.value.handleStart=(rawFile)=>{
console.log("uploadRef");
console.log(rawFile);
}
}
const shouInput = () => {
setTimeout(() => {
optionMode.value = false;
}, 200);
}
return {
dialogVisible,
creatFolderName,
showdialog,
shouInput,
optionMode,
doCreateFolder,
loading,
props,
doUploadFile,
handleCloseDialog,
uploadRef,
triggerBtn,
fileList,
beforeUpload,
onFileChange,
handleProgress,
handleSuccess,
UPLOAD,
startSubmit,
handleError
}
}
};
</script>
<style>
.el-dialog.el-dialog2 {
background-color: #2f2f35 !important;
}
.option_div {
border: 1px solid rgb(144, 144, 144);
border-radius: 9px;
padding: 10px;
}
.option_div.add {
border: 1px solid rgb(11, 89, 173);
}
.option_div.upload {
border: 1px solid rgb(171, 119, 8);
}
.option_div:hover {
background-color: rgb(0, 0, 0);
}
</style>
二、后端全部代码
const express = require('express');
const os = require('os');
const fs = require('fs');
const path = require('path');
const multer = require('multer');
// const iconv = require('iconv-lite');
// const multiparty = require('multiparty');
const app = express();
const bodyParser = require('body-parser'); // 引入 body-parser 模块
const qrcode = require('./qrcode');
app.use(bodyParser.json()); // 解析 JSON 格式的请求体
app.use(bodyParser.urlencoded({ extended: true })); // 解析 URL-encoded 格式的请求体
// 部署
// 静态资源目录,指向刚刚打包生成的 dist 目录
// app.use(express.static(path.join(__dirname, 'dist')));
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
next();
});
app.get('/ostype', (req, res) => {
return res.end(os.type());
});
app.get('/getipaddress',(req, res) => {
return res.end("http://"+getWLAN_IP()+":3000");
})
app.get('/dir',(req, res) => {
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
const dir = req.query.dir;
fs.readdir(dir, (err, files) => {
if (err) {
console.error("获取路径",dir,"出错",err);
res.statusCode = 400;
res.end('路径不存在:'+dir);
return;
}
// 创建一个对象用于存储文件夹和文件
const fileObj = {
folders: [],
files: [],
permission:[]//需要权限才能访问
};
files.forEach(file => {
try{
// 使用 fs.statSync() 同步获取文件或文件夹的状态信息
const stats = fs.statSync(dir + '/' + file);
const e={
name:file,
size:stats.size,
mtime:stats.mtime
}
if (stats.isDirectory()) {
fileObj.folders.push(e);
} else {
fileObj.files.push(e);
}
}catch(e){
fileObj.permission.push(file);
}
});
res.end(JSON.stringify({
code: 200,
data: fileObj
}));
});
});
app.get('/getdownload',(req, res) => {
const absPath = req.query.fullpath;
if (fs.existsSync(absPath)) {
// 设置 Content-Disposition header,指定文件名
// 对文件名进行 URL 编码
const filename = encodeURIComponent(path.basename(absPath));
const stat = fs.statSync(absPath);
const fileSize = stat.size;
console.log("download ",absPath,fileSize+"byte");
res.setHeader('Content-Length', fileSize);
// 设置 Content-Disposition header,指定文件名
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${filename}`);
// 以流的方式将文件发送给客户端
const fileStream = fs.createReadStream(absPath);
fileStream.pipe(res);
} else {
res.status(404).send('文件不存在');
}
});
app.post('/download', (req, res) => {
// console.log(req.body);
const fullpath = req.body.fullpath; // 获取请求中的 fullpath 参数
// 根据 fullpath 参数进行文件读取操作
const absPath = fullpath;
// 读取文件内容
fs.readFile(absPath, (err, data) => {
if (err) {
console.error('文件读取失败:', err);
res.status(500).send('文件读取失败');
} else {
// 将文件内容作为响应返回给前端
res.send(data);
}
});
});
app.post('/downloadStream', (req, res) => {
console.log(req.body);
const fullpath = req.body.fullpath; // 获取请求中的 fullpath 参数
// 根据 fullpath 参数进行文件读取操作
const absPath = fullpath;
/**
* 在这段代码中,我们使用 createReadStream
* 方法创建了一个文件读取的可读流 fileStream,
* 然后使用 pipe 方法将其直接导向响应的可写流 res。
* 这样就实现了边读取文件边传输给前端进行下载的效果。
*/
// 读取文件内容
// 检查文件是否存在
// 检查文件是否存在
if (fs.existsSync(absPath)) {
// 设置 Content-Disposition header,指定文件名
// 对文件名进行 URL 编码
const filename = encodeURIComponent(path.basename(absPath));
const stat = fs.statSync(absPath);
const fileSize = stat.size;
res.setHeader('Content-Length', fileSize);
console.log("fileSize=",fileSize);
// 设置 Content-Disposition header,指定文件名
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${filename}`);
// 以流的方式将文件发送给客户端
const fileStream = fs.createReadStream(absPath);
fileStream.pipe(res);
} else {
res.status(404).send('文件不存在');
}
});
app.post('/music/stream', (req, res) => {
const fullpath = req.body.fullpath; // 从请求中获取音频文件的完整路径
// 使用 fs 模块读取音频文件,并将数据以流的形式传输给客户端
const stream = fs.createReadStream(fullpath); // 创建可读流
// 设置响应头,指定音频格式和分块传输方式
res.setHeader('Content-Type', 'audio/flac');
res.setHeader('Transfer-Encoding', 'chunked');
// 将音频数据流通过响应发送给客户端
stream.pipe(res);
});
app.get('/getplay',(req, res) => {
const absPath = req.query.fullpath;
if (fs.existsSync(absPath)) {
// 设置 Content-Disposition header,指定文件名
// 对文件名进行 URL 编码
const filename = encodeURIComponent(path.basename(absPath));
res.setHeader('Content-Type', 'audio/flac');
res.setHeader('Transfer-Encoding', 'chunked');
// 设置 Content-Disposition header,指定文件名
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${filename}`);
// 以流的方式将文件发送给客户端
const fileStream = fs.createReadStream(absPath);
fileStream.pipe(res);
} else {
console.log(absPath);
res.status(404).send('文件不存在');
}
});
app.post("/createfolder", (req, res) => {
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
const absDir = req.body.path;
const folderName = req.body.name;
// 判断路径是否存在
if (!fs.existsSync(absDir)) {
// 路径不存在的处理逻辑
res.end(JSON.stringify({
code: 404,
msg: "路径"+absDir+"不存在"
}));
return;
}
// 判断文件夹是否存在
const folderPath = path.join(absDir, folderName);
if (fs.existsSync(folderPath)) {
// 文件夹已存在的处理逻辑
// res.status(409).send("");
res.end(JSON.stringify({
code: 405,
msg: "文件夹"+folderName+"已存在"
}));
return;
}
// 创建文件夹
fs.mkdirSync(folderPath);
// 返回成功响应
// res.status(200).send("文件夹创建成功");
res.end(JSON.stringify({
code: 200,
msg: "文件夹创建成功,"+folderPath
}));
});
/*
测试一:
需要引入模块iconv
// console.log("seq",seq,"utf-8",iconv.decode(file.originalname, 'utf-8'),"fileName",JSON.parse(req.body.fileInfoList)[seq-1].name);
// multer模块不支持中文这种非Unicode字符,需要别的模块转换iconv
var seq=0;
// 设置文件存储目录和文件名
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, req.body.absdir); // 这里使用请求参数中的 absDir 作为文件存储目录
},
filename: function (req, file, cb) {
cb(null, iconv.decode(file.originalname, 'utf-8')); // 使用原始文件名作为文件名
}
});
// 创建 Multer 实例
const upload = multer({ storage: storage });
// 处理文件上传的路由
app.post('/upload', upload.array('file'), (req, res) => {
if (req.files.length === 0) {
res.status(400).send('没有选择要上传的文件');
return;
}
res.status(200).send('文件上传成功');
});
*/
/*
测试二
上传太直接了,没有限制,不灵活,不能判断
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, req.body.absdir); // 这里使用请求参数中的 absDir 作为文件存储目录
},
filename: function (req, file, cb) {
console.log("save file ",file.originalname);
cb(null, file.originalname + ""); // 使用原始文件名作为文件名
}
});
const upload = multer({
storage: storage,
fileFilter: function (req, file, cb) {
// 检查文件类型
// if (!file.mimetype.startsWith('image/')) {
// cb(new Error('只允许上传图片文件'));
// } else {
// cb(null, true);
// }
cb(null, true);
},
limits: {
fileSize: 1 * 1024 * 1024 ,// 限制文件大小为 1MB
fileSize: 1 * 1024 * 1024 * 1024 * 2 //限制文件大小为 2GB
}
});
app.post('/upload', upload.array('file'), (req, res) => {
if (req.files.length === 0) {
res.status(400).send('没有选择要上传的文件');
return;
}
// 处理每个上传的文件
req.files.forEach(file => {
// 检查文件名称
if (file.originalname) {
// res.status(400).send('文件名必须以 prefix_ 开头');
res.status(200).send(req.body.absdir+"收到"+file.originalname);
return;
}
// 保存文件到指定目录
// ...
});
// res.status(200).send('文件上传成功');
});
*/
/**
* 测试三
* 不用额外引入模块,又能过滤文件,设置个数、大小、类型
*/
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, req.body.absdir); // 这里使用请求参数中的 absDir 作为文件存储目录
},
filename: function (req, file, cb) {
const originalname = Buffer.from(file.originalname, 'binary').toString("utf-8"); // 将原始文件名进行utf-8编码处理
cb(null, originalname); // 生成保存的文件名
}
});
const upload = multer({
storage: storage,
fileFilter: function (req, file, cb) {
// 检查文件类型
// if (!file.mimetype.startsWith('image/')) {
// cb(new Error('只允许上传图片文件'));
// } else {
// cb(null, true);
// }
cb(null, true);
},
limits: {
fileSize: 1 * 1024 * 1024 ,// 限制文件大小为 1MB
fileSize: 1 * 1024 * 1024 * 1024 * 2 //限制文件大小为 2GB
}
});
app.post('/upload', upload.single('file'), function(req, res) {
const originalname = Buffer.from(req.file.originalname, 'binary').toString('utf-8'); // 将文件名进行解码处理
console.log("upload==",originalname," "+Buffer.from("你好啊1234", 'utf-8').toString('binary'))
res.send(originalname);
});
app.listen(3000, () => {
console.log('Server is running on port 3000...');
let homeUrl = "http://"+getWLAN_IP()+":3000";
console.log(homeUrl);
printQRCODE(homeUrl);
});
function printQRCODE(text){
// 创建二维码生成器实例
var qr = new qrcode(0, 'M');
// 设置要生成二维码的文本
// 使用二维码生成器生成二维码
qr.addData(text);
qr.make();
// 获取生成的二维码的字符表示形式
var qrData = qr.createASCII();
// 打印二维码
console.log(qrData);
}
function getWLAN_IP() {
const interfaces = os.networkInterfaces();
const addresses = [];
for (let k in interfaces) {
for (let k2 in interfaces[k]) {
let address = interfaces[k][k2];
if (address.family.toLowerCase().match('ipv4')!=null) {
addresses.push({
interface: k,
ip: address.address
});
if (k.toLowerCase().match("wlan")!=null) {
return address.address;
}
}
}
}
}