# aeip_upload

> upload component

Latest version **1.0.5** (published 2021-01-12) · ISC license · 0 weekly downloads

## Install

```sh
npm install aeip_upload
pnpm add aeip_upload
yarn add aeip_upload
bun add aeip_upload
```

## Health

**Score 15/100 (F)** — status: abandoned.

Positive: no vulnerabilities.

Warnings: low downloads; no types; no esm support.

Negative: abandoned; low maintenance score.

## Facts

| | |
|---|---|
| Version | 1.0.5 |
| Published | 2021-01-12 |
| First published | 2020-10-25 |
| Weekly downloads | 0 |
| License | ISC |
| TypeScript types | none |
| Module format | CommonJS |
| Dependencies | 0 |
| Unpacked size | 31.2 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Author | aeipyuan |
| Maintainers | aeipyuan |

## Links

- npm: https://www.npmjs.com/package/aeip_upload
- npm.io page: https://npm.io/package/aeip_upload

## Recent versions

- 1.0.5 (latest) — 2021-01-12
- 1.0.4 — 2020-11-22
- 1.0.3 — 2020-11-21
- 1.0.2 — 2020-11-21
- 1.0.1 — 2020-10-26
- 1.0.0 — 2020-10-25

## README

# 前端

> 简介：一个基于Vue的实现并发数量控制，切片上传的Upload组件 

## 1.组件注册
```javascript
/* 拉取npm包 */
npm i aeip_uplpoad  

/* 全局注册 */
import AeipUpload from 'aeip_upload'
Vue.use(AeipUpload)

/* 局部注册 */
import { AeipUpload } from 'aeip_upload'
export default {
	components: {
		AeipUpload
    },
    //...
}
```
## 2.配置项
```html
<aeip-upload
    url="http://127.0.0.1:4000"
    :limit="limit"
    :maxSize="maxSize"
    :tip="'自定义tip'"
    multiple
    :fileTypes="['image/jpeg']"
    @file-success="success"
    @file-error="error"
    @file-remove="remove"
    @file-click="clickFile">
</aeip-upload>
```
- 参数说明

|变量名|含义|
|--|--|
|url|上传的目标地址，后台需要有`/multi`和`/merge`两个路径负责接受切片和合并切片|
|limit|文件数量限制，Number类型|
|maxSize|每个文件大小限制，Number类型|
|tip|关于上传的用户提示,String类型|
|multiple|多选开关，Boolean类型|
|fileTypes|支持的文件类型，Array类型|

- 事件

|事件名|含义|
|--|--|
|file-success|文件上传成功的回调，类型function(file,fileList)
|file-error|文件上传失败的回调，类型function(file,fileList)
|file-remove|文件移除的回调，类型function(file,fileList)
|file-click|点击单个文件的回调，类型function(file,fileList)

## 3.并发控制原理
为了实现同时发送多个请求的功能，利用最大并发数和请求队列控制请求发送的时机，具体操作为：将file的每个切片生成一个req，然后将req放置到请求队列reqQueue中，request函数负责验证是否还有并发数和未发送的请求，每次发起一个请求将会减少一个并发数，在两种情况下或调用request,一是生成请求的时候，二是完成请求释放并发数后会调用request处理后面的请求
```json
fileObj数据结构:
{
		"name": "图片1.jpg",// 文件全名
		"filename": "图片1",// 文件前缀
		"suffix": ".jpg",//文件后缀
		"hash": "6003290-1547625882000", // 文件标记，size+lastmodified(md5虽然准确但是费时间不划算)
		"upTime": "1610457308869",// 上传时间
		"prevSrc": "blob:http://localhost:8080/e191fe8f-923e-4f62-a6ab-a8f05cf92733",// 临时预览路径
		"src": "",// 文件最终src
		"chunks": [], // 切片
		"finishCnt": 0,// 完成的切片数量
		"errReqs": [],// 失败的请求回调
        "isFinished": false, // 完成标志
}

```
```javascript
// 上传切片
uploadChunks(fileObj) {
	// console.log(fileObj)
	fileObj.chunks.map((chunk, index) => {
		// 生成formdata
		let fd = $utils.getFd(fileObj, chunk, index);
		// 添加请求
		const req = () => {
			// 已经成功则不发送请求
			if (fileObj.isFinished) {
				this.reqCnt++;
				this.request();
				return;
			}
			let params = { url: this.url, data: fd, path: '/multi' };
			$utils.ajax(params).then(data => {
				if (data.isFinished == true) {
					fileObj.finishCnt = fileObj.chunks.length;
					// 第一次完成触发回调
					fileObj.isFinished || this.$emit('file-success', fileObj, this.fileList);
					fileObj.isFinished = true;
				} else {
					// 所有切片上传完毕
					if (fileObj.finishCnt + 1 == fileObj.chunks.length) {
						fileObj.finishCnt += 0.2;
						// console.log(fileObj.name + '----所有切片上传完毕');
						// 合并切片
						const merge = () => {
							$utils.ajax({
								url: this.url,
								path: '/merge',
								data: `filename=${fileObj.filename}&suffix=${fileObj.suffix}&hash=${fileObj.hash}&upTime=${fileObj.upTime}`
							}).then(data => {
								fileObj.finishCnt += 0.8;
								// console.log("合并完成 ---- ", data);
								// 清除临时缓存
								URL.revokeObjectURL(fileObj.prevSrc);
								fileObj.prevSrc = fileObj.src = data.fileSrc;
								this.$emit('file-success', fileObj, this.fileList);
							})
						}
						merge();
					} else {
						fileObj.finishCnt++;
					}
				}
			})
            .catch(err => {
                this.$emit('file-error', fileObj, this.fileList);
                // 回收错误请求
                fileObj.errReqs.push(req);
            })
            .finally(() => {
                // 剩余并发   
                this.reqCnt++;
                this.request();
            })
		}
		this.reqQueue.push(req);
	});
	this.request();
},
```
**优化：**
- `filename+size+lastmodefied`作为文件标识，验证文件是否上传过，上传过直接返回路径
- 前端得知文件已存在则不再发起该文件切片上传请求
- 传入上传时间戳，防止同一个文件切片文件夹冲突
# 后端

## 1. 切片接收接口
multiparty插件对post请求进行解析，获取切片相关参数，将切片放到以`文件名+hash(size+lastmodified)`为名字的文件夹下，切片名就是切片对应的索引，方便合并

![切片未合并时的状况](https://gitee.com/aeipyuan/picture_bed/raw/master/picture_bed/images/20210112212841.png)

```javascript
// 多文件切片上传
function multipleUpload(req, res) {
    new multiparty.Form().parse(req, (err, fields, file) => {
        if (err) {
            res.statusCode = 400;
            res.end(JSON.stringify({
                msg: err
            }))
        }
        try {
            // 获取参数
            let { upTime, hash, filename, suffix, index, total } = JSON.parse(fields.chunkInfo[0]);
            let chunk = file.data[0];// 切片数据
            console.log(hash, upTime, filename, suffix, index, total)
            // 文件夹
            let fullname = `${filename}-${hash}${suffix}`;// 文件名-hash.后缀
            let filePath = path.resolve('./static/uploads', `${fullname}`);
            // 文件名-hash-上传时间戳（时间戳防止同一文件切片覆盖）
            let dirPath = path.resolve('./static/uploads', `${filename}-${hash}-${upTime}`);
            // 不存在该文件的情况下才会处理切片
            if (!fs.existsSync(filePath)) {// 文件名-hash(size+lastmodified)相同视为同一文件
                // 创建文件夹
                fs.existsSync(dirPath) || fs.mkdirSync(dirPath);
                // 创建读写流
                let rs = fs.createReadStream(chunk.path);
                //'C:\\Users\\14329\\AppData\\Local\\Temp\\-mpvktFV0iyjEfWvJz6Q1h_x'
                let ws = fs.createWriteStream(path.resolve(dirPath, index + ''));
                // 0.jpg
                rs.pipe(ws);
                rs.on('end', function () {
                    // 上传成功
                    res.end(JSON.stringify({
                        msg: '切片' + index + '上传成功'
                    }));
                });
            } else {
                rmDir(dirPath);// 检查是否有多余切片并删除
                res.end(JSON.stringify({
                    msg: '文件已存在',
                    isFinished: true,
                    imgSrc: `http://${CONFIG.uploadIP}:${CONFIG.port}/${CONFIG.uploadDir}/${fullname}`
                }));
            }
        } catch (err) {
            console.log(err)
            res.statusCode = 500;
            res.end(JSON.stringify({
                msg: err
            }))
        }
    })
}
```
## 2. 切片合并接口
前端组件验证切片完成后会自动发起merge请求，后端接到请求后根据文件名读取文件夹，按照切片索引顺序进行合并，合并一个切片同时删除一个，合并完成后删除文件夹
```javascript
// 切片合并
function mergeUpload(req, res) {
    let data = '';
    req.on('data', chunk => data += chunk);
    req.on('end', () => {
        try {
            let { filename, hash, suffix, upTime } = qs.parse(data);
            // console.log(filename, hash, suffix, upTime);
            let dirname = `${filename}-${hash}-${upTime}`;
            let fullname = `${filename}-${hash}${suffix}`;
            // 文件夹路径
            let dirPath = path.resolve(CONFIG.uploadDir, dirname);
            let filePath = path.resolve(CONFIG.uploadDir, fullname);
            if (fs.existsSync(filePath)) {
                rmDir(dirPath);// 检查是否有多余切片并删除
                res.end(JSON.stringify({
                    msg: '文件已存在',
                    isFinished: true,
                    filename,
                    fileSrc: `http://${CONFIG.uploadIP}:${CONFIG.port}/${CONFIG.uploadDir}/${fullname}`
                }))
            }
            else if (fs.existsSync(dirPath)) {
                let chunks = fs.readdirSync(dirPath),
                    total = chunks.length;
                // 排序
                chunks.sort((a, b) => a - b);
                // 合并
                let to = fs.createWriteStream(filePath, { flags: 'w+' });
                const mergeChunk = function (chunkIdx) {
                    let chunkPath = path.resolve(dirPath, chunkIdx + '');
                    // console.log(filePath, chunkIdx, chunkPath)
                    console.log(dirname + "--合并切片--" + chunkIdx);
                    let from = fs.createReadStream(chunkPath);
                    from.pipe(to, { end: false });
                    from.on('end', () => {
                        fs.unlinkSync(chunkPath);
                        if (chunkIdx + 1 < total) {
                            mergeChunk(chunkIdx + 1);
                        } else {
                            console.log(dirname + " -- 合并完成");
                            fs.rmdirSync(dirPath);
                            res.end(JSON.stringify({
                                msg: '合并成功',
                                filename,
                                fileSrc: `http://${CONFIG.uploadIP}:${CONFIG.port}/${CONFIG.uploadDir}/${fullname}`
                            }))
                        }
                    });
                }
                mergeChunk(0);
            }
        } catch (err) {
            console.log(err)
            res.statusCode = 400;
            res.end(JSON.stringify({
                msg: err
            }))
        }
    })
}
```

---
_Source: https://npm.io/package/aeip_upload · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
