公司實(shí)現(xiàn)文件上傳技術(shù)選型采用后端SpringBoot/Cloud,前端vue Bootstrap ,阿里云OSS作為文件存儲(chǔ),大文件上傳功能單獨(dú)抽取封裝大文件上傳組件,可供所有的大文件的操作。
后端框架 | 版本 |
SpringBoot | 2.5.6 |
Spring-Cloud | 2020.0.4 |
mysql | 8.0.26 |
pagehelper | 1.3.1 |
Mybatis | 2.2.0 |
Redis | 5.0 |
Fastjson | 1.2.78 |
前端框架 | 版本 |
Vue | 2.6.11 |
axios | 0.24.0 |
vue-router | 3.5.3 |
Bootstrap | 4.6.2 |
文章目錄
- ??一、前端部分??
- ??2.1. 注冊(cè)阿里云??
- ??2.2. 開(kāi)通OSS??
- ??2.3. 進(jìn)入管控臺(tái)??
- ??2.4. 創(chuàng)建 Bucket??
- ??2.5. 創(chuàng)建OSS用戶(hù)??
- ??2.6. OSS權(quán)限??
- ??四、后端部分??
一、前端部分
1. 小節(jié)頁(yè)面
小節(jié)頁(yè)面作為文件上傳父頁(yè)面
<div class="form-group">
<label class="col-sm-2 control-label">視頻</label>
<div class="col-sm-10">
<vod :text="'上傳視頻'"
:input-id="'video-upload'"
:suffixs="['mp4']"
:use="FILE_USE.COURSE.key"
:after-upload="afterUpload">
</vod>
<div v-show="section.video" class="row">
<div class="col-md-9">
<player v-bind:player-id="'form-player-div'"
ref="player"></player>
<video v-bind:src="section.video" id="video" controls="controls" class="hidden"></video>
</div>
</div>
</div>
</div>
2. js部分
<script>
import BigFile from "@/components/big-file";
export default {
components: { BigFile },
name: 'business-section',
data: function () {
return {
section: {},
sections: [],
FILE_USE: FILE_USE,
}
},
methods: {
/**
* 點(diǎn)擊【新增】
*/
add() {
let _this = this
_this.section = {}
$("#form-modal").modal("show")
},
/**
* 點(diǎn)擊【編輯】
*/
edit(section) {
let _this = this
_this.section = $.extend({}, section)
$("#form-modal").modal("show")
},
/**
* 點(diǎn)擊【保存】
*/
save() {
let _this = this
_this.section.video = "";
// 保存校驗(yàn)
if (1 != 1
|| !Validator.require(_this.section.title, "標(biāo)題")
|| !Validator.length(_this.section.title, "標(biāo)題", 1, 50)
|| !Validator.length(_this.section.video, "視頻", 1, 200)
) {
return;
}
_this.section.courseId = _this.course.id
_this.section.chapterId = _this.chapter.id
Loading.show()
_this.$api.post(process.env.VUE_APP_SERVER + '/business/admin/section/save', _this.section).then((res) => {
Loading.hide()
let resp = res.data
if (resp.success) {
$("#form-modal").modal("hide")
_this.list(1)
Toast.success("保存成功!")
} else {
Toast.warning(resp.message)
}
})
},
afterUpload(resp) {
let _this = this
let video = resp.content.path;
},
},
}
</script>
3. 大文件上傳組件
<template>
<div>
<button type="button" v-on:click="selectFile()" class="btn btn-white btn-default btn-round">
<i class="ace-icon fa fa-upload"></i>
{{ text }}
</button>
<input class="hidden" type="file" ref="file" v-on:change="uploadFile()" v-bind:id="inputId+'-input'">
</div>
</template>
<script>
export default {
name: 'big-file',
props: {
text: {
default: "上傳大文件"
},
inputId: {
default: "file-upload"
},
suffixs: {
default: []
},
use: {
default: ""
},
shardSize: {
default: 50 * 1024
},
url: {
default: "oss-append"
},
saveType: {
default: "oss/"
},
afterUpload: {
type: Function,
default: null
},
},
data: function () {
return {}
},
methods: {
uploadFile() {
let _this = this;
let formData = new window.FormData();
let file = _this.$refs.file.files[0];
console.log(JSON.stringify(file));
/*
name: "test.mp4"
lastModified: 1901173357457
lastModifiedDate: Tue May 27 2099 14:49:17 GMT+0800 (中國(guó)標(biāo)準(zhǔn)時(shí)間) {}
webkitRelativePath: ""
size: 37415970
type: "video/mp4"
*/
// 生成文件標(biāo)識(shí),標(biāo)識(shí)多次上傳的是不是同一個(gè)文件
let key = hex_md5(file.name + file.size + file.type);
let key10 = parseInt(key, 16);
let key62 = Tool._10to62(key10);
console.log(key, key10, key62);
console.log(hex_md5(Array()));
/*
d41d8cd98f00b204e9800998ecf8427e
2.8194976848941264e+38
6sfSqfOwzmik4A4icMYuUe
*/
// 判斷文件格式
let suffixs = _this.suffixs;
let fileName = file.name;
let suffix = fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length).toLowerCase();
let validateSuffix = false;
for (let i = 0; i < suffixs.length; i++) {
if (suffixs[i].toLowerCase() === suffix) {
validateSuffix = true;
break;
}
}
if (!validateSuffix) {
Toast.warning("文件格式不正確!只支持上傳:" + suffixs.join(","));
$("#" + _this.inputId + "-input").val("");
return;
}
// 文件分片
// let shardSize = 10 * 1024 * 1024; //以10MB為一個(gè)分片
// let shardSize = 50 * 1024; //以50KB為一個(gè)分片
let shardSize = _this.shardSize;
let shardIndex = 1; //分片索引,1表示第1個(gè)分片
let size = file.size;
let shardTotal = Math.ceil(size / shardSize); //總片數(shù)
let param = {
'shardIndex': shardIndex,
'shardSize': shardSize,
'shardTotal': shardTotal,
'use': _this.use,
'name': file.name,
'suffix': suffix,
'size': file.size,
'key': key62
};
_this.check(param);
},
/**
* 檢查文件狀態(tài),是否已上傳過(guò)?傳到第幾個(gè)分片?
*/
check(param) {
let _this = this;
_this.$api.get(process.env.VUE_APP_SERVER + '/file/admin/check/' + _this.saveType + param.key).then((response) => {
let resp = response.data;
if (resp.success) {
let obj = resp.content;
if (!obj) {
param.shardIndex = 1;
console.log("沒(méi)有找到文件記錄,從分片1開(kāi)始上傳");
_this.upload(param);
} else if (obj.shardIndex === obj.shardTotal) {
// 已上傳分片 = 分片總數(shù),說(shuō)明已全部上傳完,不需要再上傳
Toast.success("文件極速秒傳成功!");
_this.afterUpload(resp);
$("#" + _this.inputId + "-input").val("");
} else {
param.shardIndex = obj.shardIndex + 1;
console.log("找到文件記錄,從分片" + param.shardIndex + "開(kāi)始上傳");
_this.upload(param);
}
} else {
Toast.warning("文件上傳失敗");
$("#" + _this.inputId + "-input").val("");
}
})
},
/**
* 將分片數(shù)據(jù)轉(zhuǎn)成base64進(jìn)行上傳
*/
upload(param) {
let _this = this;
let shardIndex = param.shardIndex;
let shardTotal = param.shardTotal;
let shardSize = param.shardSize;
let fileShard = _this.getFileShard(shardIndex, shardSize);
// 將圖片轉(zhuǎn)為base64進(jìn)行傳輸
let fileReader = new FileReader();
Progress.show(parseInt((shardIndex - 1) * 100 / shardTotal));
fileReader.onload = function (e) {
let base64 = e.target.result;
// console.log("base64:", base64);
param.shard = base64;
_this.$api.post(process.env.VUE_APP_SERVER + '/file/admin/' + _this.url, param).then((response) => {
let resp = response.data;
console.log("上傳文件成功:", resp);
Progress.show(parseInt(shardIndex * 100 / shardTotal));
if (shardIndex < shardTotal) {
// 上傳下一個(gè)分片
param.shardIndex = param.shardIndex + 1;
_this.upload(param);
} else {
Progress.hide();
_this.afterUpload(resp);
$("#" + _this.inputId + "-input").val("");
}
});
};
fileReader.readAsDataURL(fileShard);
},
getFileShard(shardIndex, shardSize) {
let _this = this;
let file = _this.$refs.file.files[0];
let start = (shardIndex - 1) * shardSize; //當(dāng)前分片起始位置
let end = Math.min(file.size, start + shardSize); //當(dāng)前分片結(jié)束位置
let fileShard = file.slice(start, end); //從文件中截取當(dāng)前的分片數(shù)據(jù)
return fileShard;
},
selectFile() {
let _this = this;
$("#" + _this.inputId + "-input").trigger("click");
}
}
}
</script>
二、阿里云OSS
官網(wǎng):??https://www.aliyun.com??
2.1. 注冊(cè)阿里云
??https://account.aliyun.com/register/register.htm??
2.2. 開(kāi)通OSS
2.3. 進(jìn)入管控臺(tái)
2.4. 創(chuàng)建 Bucket
讀寫(xiě)權(quán)限選擇【公共讀】,意思是都可以或者有權(quán)限看,沒(méi)其他特殊請(qǐng)求,其他的保持默認(rèn),點(diǎn)擊確定即可
2.5. 創(chuàng)建OSS用戶(hù)
或者
2.6. OSS權(quán)限
三、OSS Client 開(kāi)發(fā)文檔
??https://www.aliyun.com/product/oss??
3.1. OSS Client SDK
開(kāi)發(fā)語(yǔ)言java 追加上傳(斷點(diǎn)續(xù)傳已實(shí)現(xiàn))
3.2. 限制
3.3. SDK Client
這里就是官網(wǎng)提供的java語(yǔ)言的SDK Client
四、后端部分
??https://help.aliyun.com/document_detail/32009.html??
4.1.依賴(lài)引入
<!-- OSS Java SDK -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.10.2</version>
</dependency>
4.2. 配置
# 應(yīng)用名稱(chēng)
spring.application.name=file
# 應(yīng)用端口
server.port=9003
# 注冊(cè)到eureka
eureka.client.service-url.defaultZone=http://localhost:8761/eureka
# 請(qǐng)求訪(fǎng)問(wèn)前綴
server.servlet.context-path=/file
# 本地存儲(chǔ)靜態(tài)文件路徑
file.path=D:/file/imooc/course/
# 訪(fǎng)問(wèn)靜態(tài)文件路徑(用于文件回顯或者文件下載)
file.domain=http://127.0.0.1:9000/file/f/
# 文件大?。ㄈ绻罱ù笮〕^(guò)此配置的大小或拋出異常)
spring.servlet.multipart.max-file-size=50MB
# 請(qǐng)求大小
spring.servlet.multipart.max-request-size=50MB
# OSS 配置
oss.accessKeyId=xxx
oss.accessKeySecret=xxx
oss.endpoint=http://oss-cn-beijing.aliyuncs.com
oss.ossDomain=http://bucket名稱(chēng).oss-cn-beijing.aliyuncs.com/
oss.bucket=xxx
- oss.endpoint 和oss.ossDomain獲取方式
- bucket 獲取方式
- oss.accessKeyId和oss.accessKeySecret獲取方式
4.3. api接口
package com.course.file.controller.admin;
import com.alibaba.fastjson.JSON;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.AppendObjectRequest;
import com.aliyun.oss.model.AppendObjectResult;
import com.aliyun.oss.model.ObjectMetadata;
import com.aliyun.oss.model.PutObjectRequest;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.vod.model.v20170321.GetMezzanineInfoResponse;
import com.course.server.dto.FileDto;
import com.course.server.dto.ResponseDto;
import com.course.server.enums.FileUseEnum;
import com.course.server.service.FileService;
import com.course.server.util.Base64ToMultipartFile;
import com.course.server.util.UuidUtil;
import com.course.server.util.VodUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.ByteArrayInputStream;
@RequestMapping("/admin")
@RestController
public class OssController {
public static final Logger LOG = LoggerFactory.getLogger(OssController.class);
public static final String BUSINESS_NAME = "文件上傳";
@Value("${oss.accessKeyId}")
private String accessKeyId;
@Value("${oss.accessKeySecret}")
private String accessKeySecret;
@Value("${oss.endpoint}")
private String endpoint;
@Value("${oss.bucket}")
private String bucket;
@Value("${oss.ossDomain}")
private String ossDomain;
@Resource
private FileService fileService;
/**
* oss追加上傳
*
* @param fileDto
* @return
* @throws Exception
*/
@PostMapping("/oss-append")
public ResponseDto fileUpload(@RequestBody FileDto fileDto) throws Exception {
LOG.info("上傳文件開(kāi)始");
//接收前端的歸屬文件類(lèi)型 COURSE("C", "課程"), TEACHER("T", "講師");
String use = fileDto.getUse();
// 為了支持一個(gè)文件上傳多次,展示歷史的不同版本,因此上傳文件前,統(tǒng)一添加文件前綴,下載時(shí),統(tǒng)一截取文件沒(méi)那個(gè)前8位處理
String key = fileDto.getKey();
//分片索引,1表示第1個(gè)分片
Integer shardIndex = fileDto.getShardIndex();
// 文件分片大小 shardSize = 10 * 1024 * 1024;
// 以10MB為一個(gè)分片
Integer shardSize = fileDto.getShardSize();
// 具體的文件 由于為了統(tǒng)一使用FileDto對(duì)象接收,默認(rèn)接收類(lèi)型是MultipartFile,這里現(xiàn)在接收類(lèi)型是String ,前端將文件提前轉(zhuǎn)成了Base64
String shardBase64 = fileDto.getShard();
// 將具體的文件在由Base64轉(zhuǎn)成MultipartFile類(lèi)型
MultipartFile shard = Base64ToMultipartFile.base64ToMultipart(shardBase64);
//接收前端的歸屬文件類(lèi)型 COURSE("C", "課程"), TEACHER("T", "講師");
FileUseEnum useEnum = FileUseEnum.getByCode(use);
//文件全名
String filename = shard.getOriginalFilename();
//如果文件夾不存在,則創(chuàng)建
String dir = useEnum.name().toLowerCase();
String path = new StringBuffer(dir)
.append("/")
.append(key)
.append(".")
.append(filename)
.toString();// course6sfSqfOwzmik4A4icMYuUe.mp4
// 創(chuàng)建OSSClient實(shí)例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
ObjectMetadata meta = new ObjectMetadata();
// 指定上傳的內(nèi)容類(lèi)型。
meta.setContentType("text/plain");
// 通過(guò)AppendObjectRequest設(shè)置多個(gè)參數(shù)。
AppendObjectRequest appendObjectRequest = new AppendObjectRequest(bucket, path,
new ByteArrayInputStream(shard.getBytes()), meta);
// 通過(guò)AppendObjectRequest設(shè)置單個(gè)參數(shù)。
// 設(shè)置Bucket名稱(chēng)。
//appendObjectRequest.setBucketName(bucketName);
// 設(shè)置Object名稱(chēng)。即不包含Bucket名稱(chēng)在內(nèi)的Object的完整路徑,例如example/test.txt。
//appendObjectRequest.setKey(objectName);
// 設(shè)置待追加的內(nèi)容。可選類(lèi)型包括InputStream類(lèi)型和File類(lèi)型。此處為InputStream類(lèi)型。
//appendObjectRequest.setInputStream(new ByteArrayInputStream(content1.getBytes()));
// 設(shè)置待追加的內(nèi)容。可選類(lèi)型包括InputStream類(lèi)型和File類(lèi)型。此處為File類(lèi)型。
//appendObjectRequest.setFile(new File("D:\localpath\examplefile.txt"));
// 指定文件的元信息,第一次追加時(shí)有效。
//appendObjectRequest.setMetadata(meta);
// 第一次追加。
// 設(shè)置文件的追加位置。
// appendObjectRequest.setPosition(0L);
appendObjectRequest.setPosition((long) (shardIndex - 1) * shardSize);
AppendObjectResult appendObjectResult = ossClient.appendObject(appendObjectRequest);
// 文件的64位CRC值。此值根據(jù)ECMA-182標(biāo)準(zhǔn)計(jì)算得出
System.out.println(appendObjectResult.getObjectCRC());
// 關(guān)閉OSSClient。
ossClient.shutdown();
LOG.info("保存文件記錄開(kāi)始");
fileDto.setPath(path);
fileService.save(fileDto);
ResponseDto responseDto = new ResponseDto();
// 文件OSS地址存儲(chǔ)到fileDto,統(tǒng)一返回前端
fileDto.setPath(ossDomain + path);
responseDto.setContent(fileDto);
return responseDto;
}
/**
* 斷點(diǎn)續(xù)傳檢查
*
* @param key
* @return
* @throws Exception
*/
@GetMapping("/check/oss/{key}")
public ResponseDto check(@PathVariable String key) throws Exception {
LOG.info("檢查上傳分片開(kāi)始:{}", key);
ResponseDto responseDto = new ResponseDto();
FileDto fileDto = fileService.findByKey(key);
if (fileDto != null) {
fileDto.setPath(ossDomain + fileDto.getPath());
}
responseDto.setContent(fileDto);
return responseDto;
}
}
本文摘自 :https://blog.51cto.com/g