文件的上传与下载

示例源码: https://gitee.com/achieve502938049/apprentice-file.git

1|0单文件上传

1|1客户端

前端通过 <input type="file"> 获取 File 对象,通过 FormData 二进制传输。

<template> <el-button type='success' :icon='Search' @click='selectFile'>选择文件</el-button> <el-button type='primary' :disabled='disabled' @click='upload'> 上传 <el-icon class='el-icon--right'> <Upload /> </el-icon> </el-button> <br /> <el-tag type='info' v-if='file'>{{ file.name }}</el-tag> <input type='file' class='hidden' ref='fileEl' @change='fileChange' /> </template>
const file = ref(null); const disabled = computed(() => !file.value); const fileEl = ref(null); const selectFile = () => { fileEl.value.click(); }; const fileChange = () => { file.value = fileEl.value.files[0]; }; const upload = () => { if (!file.value) { return ElMessage.error('请选择文件'); } singleUpload({ file: file.value }).then(res => { const data = res.data; ElMessage({ message: `${data.filename} 上传成功!`, type: 'success' }); }); };

1|2服务端

借助 multer 中间件,会将文件暂缓存到指定目录中,再通过文件流写入指定路径。

const multer = require('multer'); const upload = multer({ dest: 'uploads/' }); // router router.post('/single', upload.single('file'), async function(req, res, next) { const file = req.file; const filename = file.originalname; const path = `${uploadDir}/${filename}`; try { await fileUtils.writeStream(file.path, path); res.send(ResponseVo.success({ path, filename })); } catch (err) { res.send(ResponseVo.error(err)); } }); // fileUtils function writeStream(sourcePath, targetPath) { return new Promise((resolve, reject) => { try { const readStream = fs.createReadStream(sourcePath), writeStream = fs.createWriteStream(targetPath); readStream.pipe(writeStream); readStream.on('end', () => { resolve(); fs.unlinkSync(sourcePath); }); } catch (err) { reject(err); } }); }

2|0多文件上传

客户端中 <input type="file" multiple> 增加 multiple 属性能让浏览器一次性选取多个文件。

服务端中再对接收的 File 数组对象依次进行单文件输入。

3|0Base64上传

3|1客户端

File 对象转换为 Base64,以字符的方式传输。

const fileChange = async () => { const file = fileEl.value.files[0]; base64Str.value = await covertFile2Base64(file); filename = file.name; }; const upload = () => { if (!base64Str.value) { return ElMessage.error('请选择文件'); } base64Upload({ file: base64Str.value, filename }).then(res => { const data = res.data; ElMessage({ message: `${data.filename} 上传成功!`, type: 'success' }); }); }; function covertFile2Base64(file): Promise<String> { return new Promise(resolve => { const fileReader = new FileReader(); fileReader.readAsDataURL(file); fileReader.onload = (ev) => { resolve(ev.target.result); }; }); }

3|2服务端

router.post('/base64', upload.none(), async function(req, res, next) { const fileBuffer = bufferUtils.covertBase64ToFileBuffer(req.body.file), filename = req.body.filename, spark = new SparkMD5.ArrayBuffer(), suffix = shared.getFileExtendingName(filename); spark.append(fileBuffer); const path = `${uploadDir}/${spark.end()}.${suffix}`; const exists = fileUtils.fileExists(path); if (exists) { return res.send(ResponseVo.success({ path, filename }, 'exists')); } try { await fileUtils.writeFile(fileBuffer, path); res.send(ResponseVo.success({ path, filename })); } catch (err) { res.send(ResponseVo.error(err)); } }); // fileUtils function writeFile(buffer, targetPath) { return new Promise((resolve, reject) => { fs.writeFile(targetPath, buffer, (err) => { if (err) { reject(err); return; } resolve(); }); }); }

4|0大文件分片上传

4|1客户端

将文件分割成若干个小文件,并予以特定的命名,待小文件全部上传后在服务端中进行文件合并。

const fileChange = async () => { file.value = fileEl.value.files[0]; hash = `${file.value.name}_${Date.now()}`; }; const upload = () => { if (!file.value) { return ElMessage.error('请选择文件'); } const chunks = splitChunks(file.value); Promise.all(chunks.map(chunk => chunkUpload(chunk))).then(() => { mergeChunk({ hash, filename: file.value.name }).then(res => { const data = res.data; ElMessage({ message: `${data?.filename} 上传成功!`, type: 'success' }); }); }); }; interface IChunk { chunk: Blob, filename: String; } const splitChunks = (file): Array<IChunk> => { const chunkSize = 1024 * 1024 * 2, count = Math.ceil(file.size / chunkSize), chunks: Array<IChunk> = []; let index = 0; while (index < count) { chunks.push({ chunk: file.slice(index * chunkSize, (index + 1) * chunkSize), filename: `${index}_${hash}` }); index++; } return chunks; };

4|2服务端

// 上传文件切片 router.post('/chunk', upload.single('chunk'), async function(req, res, next) { const file = req.file, buffer = bufferUtils.covertBase64ToFileBuffer(file), filenameStr = req.body.filename, spark = new SparkMD5.ArrayBuffer(); const index = filenameStr.indexOf('_'); const idx = filenameStr.slice(0, index); const filename = filenameStr.slice(index + 1, filenameStr.length); spark.append(buffer); const hash = spark.end(); const dirPath = `${uploadDir}/${filename}`; fileUtils.mkDir(dirPath); const path = `${dirPath}/${idx}_${hash}`; const exists = fileUtils.fileExists(path); if (exists) { return res.send(ResponseVo.success({ path, filename: hash }, 'exists')); } try { await fileUtils.writeStream(file.path, path); res.send(ResponseVo.success()); } catch (err) { res.send(ResponseVo.error(err)); } }); // 合并切片 router.post('/merge', upload.none(), async function(req, res, next) { const hash = req.body.hash; const filename = req.body.filename; const dirPath = `${uploadDir}/${hash}`; try { const fileList = fileUtils.readDir(dirPath).sort((a, b) => { function getSort(str) { return parseInt(str.split('_')[0]); } return getSort(a) - getSort(b); }).map(file => `${dirPath}/${file}`); const path = `${uploadDir}/${filename}`; fileUtils.mergeFile(path, fileList); fileUtils.removeDir(dirPath); res.send(ResponseVo.success({ path, filename })); } catch (err) { res.send(ResponseVo.error(err)); } }); // fileUtils function readDir(path) { if (!fs.existsSync(path)) return []; return fs.readdirSync(path); } function mergeFile(targetPath, fileList) { fileList.forEach(filePath => { fs.appendFileSync(targetPath, fs.readFileSync(filePath)); }); } function removeDir(path) { const list = readDir(path); list.forEach(file => { const filePath = `${path}/${file}`; fs.unlinkSync(filePath); }); fs.rmdirSync(path); }

5|0文件下载

使用 Express 提供的 res.download() 可以很方便的自动识别文件类型,返回对应的格式。

router.get('/', function(req, res, next) { const filename = req.query.file; const path = `${downloadDir}/${filename}`; const exists = fileUtils.fileExists(path); if (!exists) return res.send(ResponseVo.error('file is not exists')); try { res.download(path); } catch (err) { res.send(ResponseVo.error(err)); } });

6|0二进制流下载

6|1服务端

在响应头中设置 application/octet-stream 等特定的属性,即可让请求返回二进制流。

router.get('/stream', async function(req, res, next) { const filename = req.query.file; const path = `${downloadDir}/${filename}`; const exists = fileUtils.fileExists(path); if (!exists) return res.send(ResponseVo.error('file is not exists')); try { const data = await fileUtils.readFile(path); res.set('Content-Type', 'application/octet-stream'); res.set('Content-Disposition', `attachment;filename=${filename}`); res.end(data); } catch (err) { res.send(ResponseVo.error(err)); } });

6|2客户端

客户端接收数据时需预先约定数据接方式,再通过 a 标签的 download 属性触发下载。

// axios export function streamDownload(filename) { return axios({ url: `/download/stream?file=${filename}`, method: 'get', responseType: 'blob' }); } // 数据拼装 const handleStreamDownload = () => { const filename = 'demo.xlsx'; streamDownload(filename).then(res => { const objectUrl = URL.createObjectURL(res); const a = document.createElement('a'); a.href = objectUrl; a.setAttribute('download', filename); document.body.appendChild(a); a.click(); document.body.removeChild(a); }); };

__EOF__

本文作者Odyssey
本文链接https://www.cnblogs.com/qingzhao/p/17608706.html
关于博主:I am a good person
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   --Odyssey--  阅读(27)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示