源代码在GitHub - 629y/course: Spring Cloud + Vue前后端分离-在线课程
Spring Cloud + Vue前后端分离-第10章 基于阿里云OSS的文件上传
前面介绍的文件上传是基于本地文件服务器的文件上传,但是自己搭文件服务器会有很多运维的问题,比如磁盘满了要扩容,高峰期要增加带宽,低谷期要减少带宽,为了安全,我们还要对文件做备份等等。
所以一般会选择云平台来存储文件,云平台有很多,比如阿里云、腾讯云、百度云等,云平台做的最好的是亚马逊,我们这里选择国内最大的阿里云。
10-1 阿里云OSS简介
面向海量数据规模的分布式存储服务
从浏览器通过ECS( 服务器)访问OSS,算内网访问,不算流量费,但是要考虑ECS的带宽问题。从浏览器直接访问OSS资源,算外网访问OSS,需付流量费,但是可以不用考虑带宽。

10-2 基于OSS接口的文件上传
阿里云OSS控台的使用




bucket:存储空间,名称必须是全阿里云唯一





这个文件地址可以直接打开观看
基于OSS接口实现文件上传
1.大文件断点续传与极速秒传:基于OSS接口实现文件上传,增加文件追加上传oss-append 和简单上传oss-simple




pom.xml

OssController.java
java
package com.course.file.controller.admin;
import com.alibaba.fastjson.JSONObject;
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.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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.ByteArrayInputStream;
@RestController
@RequestMapping("/admin")
public class OssController {
private static final Logger LOG = LoggerFactory.getLogger(OssController.class);
@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.domain}")
private String ossDomain;
public static final String BUSINESS_NAME = "文件上传";
@Resource
private FileService fileService;
@PostMapping("/oss-append")
public ResponseDto fileUpload(@RequestBody FileDto fileDto) throws Exception {
LOG.info("上传文件开始");
String use = fileDto.getUse();
String key = fileDto.getKey();
String suffix = fileDto.getSuffix();
Integer shardIndex = fileDto.getShardIndex();
Integer shardSize = fileDto.getShardSize();
String shardBase64 = fileDto.getShard();
MultipartFile shard = Base64ToMultipartFile.base64ToMultipart(shardBase64);
// 保存文件到本地
FileUseEnum useEnum = FileUseEnum.getByCode(use);
// //如果文件夹不存在则创建
String dir = useEnum.name().toLowerCase();
// File fullDir = new File(FILE_PATH + dir);
// if (!fullDir.exists()) {
// fullDir.mkdir();
// }
// String path = dir + File.separator + key + "." + suffix + "." + fileDto.getShardIndex();
String path = new StringBuffer(dir)
.append("/")
.append(key)
.append(".")
.append(suffix)
.toString(); // course\6sfSqfOwzmik4A4icMYuUe.mp4
// String localPath = new StringBuffer(path)
// .append(".")
// .append(fileDto.getShardIndex())
// .toString(); // course\6sfSqfOwzmik4A4icMYuUe.mp4.1
// String fullPath = FILE_PATH + localPath;
// File dest = new File(fullPath);
// shard.transferTo(dest);
// LOG.info(dest.getAbsolutePath());
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
ObjectMetadata meta = new ObjectMetadata();
// 指定上传的内容类型。
meta.setContentType("text/plain");
// 通过AppendObjectRequest设置多个参数。
AppendObjectRequest appendObjectRequest = new AppendObjectRequest(bucket, path, new ByteArrayInputStream(shard.getBytes()), meta);
// 通过AppendObjectRequest设置单个参数。
// 设置存储空间名称。
//appendObjectRequest.setBucketName("<yourBucketName>");
// 设置文件名称。
//appendObjectRequest.setKey("<yourObjectName>");
// 设置待追加的内容。有两种可选类型:InputStream类型和File类型。这里为InputStream类型。
//appendObjectRequest.setInputStream(new ByteArrayInputStream(content1.getBytes()));
// 设置待追加的内容。有两种可选类型:InputStream类型和File类型。这里为File类型。
//appendObjectRequest.setFile(new File("<yourLocalFile>"));
// 指定文件的元信息,第一次追加时有效。
//appendObjectRequest.setMetadata(meta);
// 第一次追加。
// 设置文件的追加位置。
// appendObjectRequest.setPosition(0L);
appendObjectRequest.setPosition((long) ((shardIndex - 1) * shardSize));
AppendObjectResult appendObjectResult = ossClient.appendObject(appendObjectRequest);
// 文件的64位CRC值。此值根据ECMA-182标准计算得出。
System.out.println(appendObjectResult.getObjectCRC());
System.out.println(JSONObject.toJSONString(appendObjectResult));
// // 第二次追加。
// // nextPosition指明下一次请求中应当提供的Position,即文件当前的长度。
// appendObjectRequest.setPosition(appendObjectResult.getNextPosition());
// appendObjectRequest.setInputStream(new ByteArrayInputStream(content2.getBytes()));
// appendObjectResult = ossClient.appendObject(appendObjectRequest);
//
// // 第三次追加。
// appendObjectRequest.setPosition(appendObjectResult.getNextPosition());
// appendObjectRequest.setInputStream(new ByteArrayInputStream(content3.getBytes()));
// appendObjectResult = ossClient.appendObject(appendObjectRequest);
// 关闭OSSClient。
ossClient.shutdown();
LOG.info("保存文件记录开始");
fileDto.setPath(path);
fileService.save(fileDto);
ResponseDto responseDto = new ResponseDto();
fileDto.setPath(ossDomain + path);
responseDto.setContent(fileDto);
// if (fileDto.getShardIndex().equals(fileDto.getShardTotal())) {
// this.merge(fileDto);
// }
return responseDto;
}
@PostMapping("/oss-simple")
public ResponseDto fileUpload(@RequestParam MultipartFile file, String use) throws Exception {
LOG.info("上传文件开始");
FileUseEnum useEnum = FileUseEnum.getByCode(use);
String key = UuidUtil.getShortUuid();
String fileName = file.getOriginalFilename();
String suffix = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
String dir = useEnum.name().toLowerCase();
String path = dir + "/" + key + "." + suffix;
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 创建PutObjectRequest对象。
// String content = "Hello OSS";
// <yourObjectName>表示上传文件到OSS时需要指定包含文件后缀在内的完整路径,例如abc/efg/123.jpg。
PutObjectRequest putObjectRequest = new PutObjectRequest(bucket, path, new ByteArrayInputStream(file.getBytes()));
// 如果需要上传时设置存储类型与访问权限,请参考以下示例代码。
// ObjectMetadata metadata = new ObjectMetadata();
// metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
// metadata.setObjectAcl(CannedAccessControlList.Private);
// putObjectRequest.setMetadata(metadata);
// 上传字符串。
ossClient.putObject(putObjectRequest);
// LOG.info("保存文件记录开始");
// fileDto.setPath(path);
// fileService.save(fileDto);
ResponseDto responseDto = new ResponseDto();
FileDto fileDto = new FileDto();
fileDto.setPath(ossDomain + path);
responseDto.setContent(fileDto);
return responseDto;
}
}
application.properties

big-file.vue

file.vue

演示:头像上传是用的单文件传输到OSS;视频上传是用的分片传输到OSS
teacher.vue


测试
上传视频和头像



10-3{white}阿里云视频点播服务介绍{white}{white}
阿里云视频点播服务介绍

视频点播控台的使用



















视频点播帮助文档概览







10-4 基于OSS原生SDK上传视频到点播服务
VOD上传并加密转码
VOD上传并加密转码
1.视频加密与授权播放:基于 OSS原生SDK上传视频到点播服务,官方示例代码
pom.xml(course)

pom.xml(server)

VodUtil.java
java
package com.course.server.util;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.aliyun.oss.OSSClient;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.http.FormatType;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.vod.model.v20170321.*;
import org.apache.commons.codec.binary.Base64;
import java.io.File;
import java.io.InputStream;
public class VodUtil {
/**
* 使用AK初始化VOD客户端
* @param accessKeyId
* @param accessKeySecret
* @return
* @throws ClientException
*/
public static DefaultAcsClient initVodClient(String accessKeyId, String accessKeySecret) throws ClientException {
// 点播服务接入区域,国内请填cn-shanghai,其他区域请参考文档[点播中心](~~98194~~)
String regionId = "cn-shanghai";
DefaultProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, accessKeySecret);
DefaultAcsClient client = new DefaultAcsClient(profile);
return client;
}
/**
* 获取视频上传地址和凭证
* @param vodClient
* @return
* @throws ClientException
*/
public static CreateUploadVideoResponse createUploadVideo(DefaultAcsClient vodClient, String fileName) throws ClientException {
CreateUploadVideoRequest request = new CreateUploadVideoRequest();
request.setFileName(fileName);
request.setTitle(fileName);
//request.setDescription("this is desc");
//request.setTags("tag1,tag2");
// request.setCoverURL("http://vod.aliyun.com/test_cover_url.jpg");
// request.setCateId(1000115308L);
// request.setTemplateGroupId("78fffb8c0c2426efd5baaaafed76fe36");
//request.setWorkflowId("");
//request.setStorageLocation("");
//request.setAppId("app-1000000");
//设置请求超时时间
request.setSysReadTimeout(1000);
request.setSysConnectTimeout(1000);
return vodClient.getAcsResponse(request);
}
/**
* 使用上传凭证和地址初始化OSS客户端(注意需要先Base64解码并Json Decode再传入)
* @param uploadAuth
* @param uploadAddress
* @return
*/
public static OSSClient initOssClient(JSONObject uploadAuth, JSONObject uploadAddress) {
String endpoint = uploadAddress.getString("Endpoint");
String accessKeyId = uploadAuth.getString("AccessKeyId");
String accessKeySecret = uploadAuth.getString("AccessKeySecret");
String securityToken = uploadAuth.getString("SecurityToken");
return new OSSClient(endpoint, accessKeyId, accessKeySecret, securityToken);
}
/**
* 简单上传
* @param ossClient
* @param uploadAddress
* @param inputStream
*/
public static void uploadLocalFile(OSSClient ossClient, JSONObject uploadAddress, InputStream inputStream){
String bucketName = uploadAddress.getString("Bucket");
String objectName = uploadAddress.getString("FileName");
// 单文件上传
ossClient.putObject(bucketName, objectName, inputStream);
/* 视频点播不支持追加上传
// 追加上传
ObjectMetadata meta = new ObjectMetadata();
meta.setContentType("text/plain");
AppendObjectRequest request = new AppendObjectRequest(bucketName, objectName, file, meta);
request.setPosition(0L);
ossClient.appendObject(request);*/
}
/**
* 上传本地文件
* @param ossClient
* @param uploadAddress
* @param localFile
*/
public static void uploadLocalFile(OSSClient ossClient, JSONObject uploadAddress, String localFile){
String bucketName = uploadAddress.getString("Bucket");
String objectName = uploadAddress.getString("FileName");
File file = new File(localFile);
// 单文件上传
ossClient.putObject(bucketName, objectName, file);
/* 视频点播不支持追加上传
// 追加上传
ObjectMetadata meta = new ObjectMetadata();
meta.setContentType("text/plain");
AppendObjectRequest request = new AppendObjectRequest(bucketName, objectName, file, meta);
request.setPosition(0L);
ossClient.appendObject(request);*/
}
/**
* 刷新上传凭证
* @param vodClient
* @return
* @throws ClientException
*/
public static RefreshUploadVideoResponse refreshUploadVideo(DefaultAcsClient vodClient) throws ClientException {
RefreshUploadVideoRequest request = new RefreshUploadVideoRequest();
request.setAcceptFormat(FormatType.JSON);
request.setVideoId("VideoId");
//设置请求超时时间
request.setSysReadTimeout(1000);
request.setSysConnectTimeout(1000);
return vodClient.getAcsResponse(request);
}
/**
* 获取源文件信息
* @param client 发送请求客户端
* @return GetMezzanineInfoResponse 获取源文件信息响应数据
* @throws Exception
*/
public static GetMezzanineInfoResponse getMezzanineInfo(DefaultAcsClient client, String videoId) throws Exception {
GetMezzanineInfoRequest request = new GetMezzanineInfoRequest();
request.setVideoId(videoId);
//源片下载地址过期时间
request.setAuthTimeout(3600L);
return client.getAcsResponse(request);
}
/**
* 获取播放凭证函数
* @param client
* @return
* @throws Exception
*/
public static GetVideoPlayAuthResponse getVideoPlayAuth(DefaultAcsClient client, String videoId) throws Exception {
GetVideoPlayAuthRequest request = new GetVideoPlayAuthRequest();
request.setVideoId(videoId);
return client.getAcsResponse(request);
}
public static void main(String[] argv) {
//您的AccessKeyId
String accessKeyId = "xxxxxxxxxxxxx";
//您的AccessKeySecret
String accessKeySecret = "xxxxxxxxxxxxx";
//需要上传到VOD的本地视频文件的完整路径,需要包含文件扩展名
String localFile = "/Users/zhaoxiaoyun/Downloads/fourcats.mp4";
try {
// 初始化VOD客户端并获取上传地址和凭证
DefaultAcsClient vodClient = initVodClient(accessKeyId, accessKeySecret);
String fileName = "test.mp4";
CreateUploadVideoResponse createUploadVideoResponse = createUploadVideo(vodClient, fileName);
// 执行成功会返回VideoId、UploadAddress和UploadAuth
String videoId = createUploadVideoResponse.getVideoId();
JSONObject uploadAuth = JSONObject.parseObject(
Base64.decodeBase64(createUploadVideoResponse.getUploadAuth()), JSONObject.class);
JSONObject uploadAddress = JSONObject.parseObject(
Base64.decodeBase64(createUploadVideoResponse.getUploadAddress()), JSONObject.class);
// 使用UploadAuth和UploadAddress初始化OSS客户端
OSSClient ossClient = initOssClient(uploadAuth, uploadAddress);
// 上传文件,注意是同步上传会阻塞等待,耗时与文件大小和网络上行带宽有关
uploadLocalFile(ossClient, uploadAddress, localFile);
System.out.println("上传视频成功, VideoId : " + videoId); // 7d6b8c07ab48456e932187080f42e88f
GetMezzanineInfoResponse response = new GetMezzanineInfoResponse();
response = getMezzanineInfo(vodClient, videoId);
System.out.println("获取视频信息, response : " + JSON.toJSONString(response));
} catch (Exception e) {
System.out.println("上传视频失败, ErrorMessage : " + e.getLocalizedMessage());
}
}
}
记得修改自己的


VOD上传到指定分类,并自动转码
1.视频加密与授权播放:基于 OSS原生SDK上传视频到点播服务,上传到指定分类,并自动转码
VodUtil.java





视频点播不支持追加上传
1.视频加密与授权播放:基于 OSS原生SDK上传视频到点播服务,视频点播不支持追加上传
官方示例中,用的是简单上传putObject ,我们要改成我们需要的追加上传
但是有严格封装编码要求的文件类型,都是不支持追加方式上传的;比如音视频、图片等,上传到视频点播不支持追加上传
VodUtil.java

效果就不演示了,感兴趣的自己尝试一下,把这段代码打开,把上面的单文件上传注释,然后看效果,是一直在上传,但是无法上传成功!
项目集成VOD上传
小节增加vod字段
1.视频加密与授权播放:小节增加vod字段
由于视频点播不支持追加上传,我们只能改成单文件传
all.sql

generatorConfig.xml

SectionDto.java

section.vue

简单快速的方案:在阿里云控台做文件上传和转码,然后将vod复制到我们自己的控台

增加vod组件,用于上传视频到视频点播服务
1.视频加密与授权播放:增加vod组件,用于上传视频到视频点播服务
big-file.vue



vod组件就是对big-file组件做了一层封装。程序员天然喜欢对代码做封装,要注意度,封装得越多,灵活度就越低
vod.vue
TypeScript
<template>
<big-file v-bind:input-id="inputId"
v-bind:text="text"
v-bind:suffixs="suffixs"
v-bind:use="use"
v-bind:after-upload="afterUpload"
v-bind:shard-size="shardSize"
v-bind:url="'oss-append'">
</big-file>
</template>
<script>
import BigFile from "./big-file";
export default {
name: "vod",
components: {BigFile},
props: {
text: {
default: "上传VOD"
},
inputId: {
default: "vod-upload"
},
suffixs: {
default: []
},
use: {
default: ""
},
shardSize: {
default: 50 * 1024
},
afterUpload: {
type: Function,
default: null
},
},
}
</script>
目前为止,vod和big-file功能一样,需要先测试下vod组件是否可用
section.vue


我们在js里对video赋值,vue会监听到值的变化,并渲染视频控件。如果还没渲染完,我们就去获取时长,这时就会得到NaN,所以需要加延时获取

测试,记得先清空数据库和oss文件



file表增加vod字段
1.视频加密与授权播放:file表增加vod字段
all.sql


generatorConfig.xml

FileDto.java

增加视频点播文件上传功能
1.视频加密与授权播放:增加视频点播文件上传功能
application.properties

vod.vue

由于视频点播不支持追加上传,所以使用vod组件进行上传的,只能有一个分片

section.vue

获取到的可播放地址是有时效的,所以就算保存到数据库也会过期,没用。以后会根据vod来播放。

文件检查时,根据是否是视频点播文件来获取视频信息
1.视频加密与授权播放:文件检查时,根据是否是视频点播文件来获取视频信息
UploadController.java


测试

10-6 视频授权播放功能开发
阿里云播放器的基本使用
1.视频加密与授权播放:集成阿里云播放器,制作player播放器组件
player.vue
TypeScript
<template>
<div v-bind:id="playerId">
<!--要做成动态的,所以不在这个地方写了-->
<!-- <div class="prism-player" id="J_prismPlayer"></div>-->
</div>
</template>
<script>
export default {
name: "player",
props: {
playerId: {
default: "player-div"
},
},
data: function () {
return {
aliPlayer: {},//播放器实例
}
},
methods: {
playUrl(url) {
let _this = this;
console.log("开始播放:", url);
//如果已经有播放器了,则将播放器div删除
if (_this.aliPlayer) {
_this.aliPlayer = null;
$("#J_prismPlayer").remove();
}
// 初始化播放器
$("#" + _this.playerId)
.append("<div class=\"prism-player\" id=\"J_prismPlayer\"></div>");
_this.aliPlayer = new Aliplayer({
id: "J_prismPlayer",
width: '100%',
autoplay: false,
source: url,
cover: 'http://imooc-coursemac.oss-cn-shanghai.aliyuncs.com/头像1.jpg',
}, function (player) {
console.log('播放器创建好了。')
});
},
}
}
</script>
index.html


section.vue

video控件隐藏掉,不要删除,要通过它获取时长。也可以通过视频点播的API去获取视频时长。


测试


功能更强,可以倍速等等
获取vod授权码并授权播放
1.视频加密与授权播放:获取vod授权码并授权播放,需要在存储管理中把权限设置成公共读
默认是私有的,更改为公共读

VodUtil.java

VodController.java

player.vue
一个页面可能会放多个player组件,所以需要把id做成动态变化的,一个页面的元素,id值要是唯一的。

modal-player.vue
带有模态框的播放器组件modal-player,里面包含了player组件
TypeScript
<template>
<div id="player-modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-body">
<player v-bind:player-id="'modal-player-div'"
ref="player"></player>
</div>
</div>
</div>
</div>
</template>
<script>
import Player from "./player";
export default {
name: 'modal-player',
components: {Player},
data: function () {
return {
aliPlayer: {}, // 播放器实例
}
},
methods: {
playUrl(url) {
let _this = this;
_this.$refs.player.playUrl(url);
},
playVod(vod) {
let _this = this;
_this.$refs.player.playVod(vod);
$("#player-modal").modal("show");
}
}
}
</script>
<style scoped>
#player-modal .modal-body {
padding: 0;
}
</style>
section.vue




测试


