在做的项目中,有需求是在HDFS文件系统中支持管理视频数据,并在前端支持在线预览。其中后端使用的是Spring boot框架,前端是React框架。
大概需要实现以下的核心功能
后端提供一个上传接口,将二进制流存到HDFS中。这里使用的是 MultipartFile,直接接受一个二进制流。
前端在用户点击上传按钮的时候,把要上传的文件转化为二进制流并调用后端接口
后端简单实现
@PostMapping("/file")
public ResponseEntity<String> upload(@RequestParam("file") MultipartFile file,
@RequestParam("dirPath") String dirPath) {
return uploadSingleFile(file, dirPath);
}
public ResponseEntity<String> uploadSingleFile(MultipartFile file, String dirPath) {
String fileName = file.getOriginalFilename();
String filePath = dirPath + "/" + fileName;
try {
uploadFileToHdfs(file, dirPath);
return new ResponseEntity<>(HttpStatus.OK);
} catch (PathNotFoundException | PathIsDirectoryException e) {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
} catch (IOException e) {
logger.error("Upload " + filePath + " failed.", e);
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
public void uploadFileToHdfs(MultipartFile file, String dirPath) throws IOException {
// 连接hdfs,实际项目中需要放到构造函数中,防止重复连接
conf = new Configuration();
conf.set("dfs.client.use.datanode.hostname", "true");
conf.set("fs.defaultFS", hdfs://mirage-cluster-01:8020);
fs = FileSystem.get(conf);
String fileName = file.getOriginalFilename();
// 这里只是简单实现,需要调用hdfs接口判断 文件是否已经存在/文件夹路径是否存在等等
Path fullDirPath = new Path("hdfs://mirage-cluster-01:8020/dev" + dirPath));
Path fullFilePath = new Path(fullDirPath.toString() + "/" + fileName);
FSDataOutputStream outputStream = fs.create(fullFilePath);
outputStream.write(file.getBytes());
outputStream.close();
}
前端简单实现(假设已经用表单选择好了要上传的文件,点击确定触发 handleOnOk() 函数)
handleOnOk = async e => {
// 假设已经验证过没有同名文件/文件夹
// 选择的参数存在了组件的state中
const { fileName, path, uploadFile } = this.state;
// 验证上传文件是否为空
if (uploadFile === null) {
message.error(‘请先选择上传的文件‘);
this.setState({
confirmLoading: false,
})
return;
}
// 构造上传文件表单
let file = null;
if (fileName !== ‘‘) {
file = new File([uploadFile],
fileName,
{
‘type‘: uploadFile.type,
});
}
const formData = new FormData();
formData.append(‘file‘, file);
formData.append(‘dirPath‘, path);
// 发送请求
const init = {
method: ‘POST‘,
mode: ‘cors‘,
body: formData,
}
const url = `http://ip:port/file`; //后端接口地址
const response = await fetch(url, init);
... // 下面根据response的状态码判断是否上传成功
}
后端提供一个下载接口,直接返回一个二进制流。
前端点击下载按钮,调用接口,下载文件到本地。
后端简单实现
@GetMapping("/file")
public ResponseEntity<InputStreamResource> download(@RequestParam("filePath") String filePath) {
return downloadSingleFile(filePath);
}
public ResponseEntity<InputStreamResource> downloadSingleFile(String filePath) {
// 假设已经做完了路径异常判断
String fileName = pathSplit[pathSplit.length - 1];
try {
InputStream in = getHdfsFileInputStream(filePath);
InputStreamResource resource = new InputStreamResource(in);
// 设置一些协议参数
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + URLEncoder.encode(fileName, "UTF-8"))
.body(resource);
} catch {
...
}
}
/**
* 获取 HDFS 文件的 IO 流
*/
public InputStream getHdfsFileInputStream(@NotNull String filePath) throws IOException {
// 连接hdfs,实际项目中需要放到构造函数中,防止重复连接
conf = new Configuration();
conf.set("dfs.client.use.datanode.hostname", "true");
conf.set("fs.defaultFS", hdfs://mirage-cluster-01:8020);
fs = FileSystem.get(conf);
// 读取文件流
Path fullFilePath = new Path("hdfs://mirage-cluster-01:8020/dev" + filePath));
InputStream in = fs.open(fullFilePath);
return in;
}
前端简单实现 (假设已经选中了要下载的文件,点击按钮触发了 handleOnOk() 函数)
handleOnOK = async (e) => {
e.stopPropagation();
const { filePathUpload, file } = this.props;
const { name, path } = file;
// 直接请求后端接口
const url = `http://ip:port/file?filePath=${path}`;
const response = await fetchTool(url, init);
if (response && response.status === 200) {
// 读取文件流
const data = await response.blob();
// 创建下载链接
let blobUrl = window.URL.createObjectURL(data);
// 创建一个a标签用于下载
const aElement = document.createElement(‘a‘);
document.body.appendChild(aElement);
aElement.style.display = ‘none‘;
aElement.href = blobUrl;
// 设置下载后文件名
aElement.download = name;
// 触发点击链接,开始下载
aElement.click();
// 下载完成,移除对象
document.body.removeChild(aElement);
}
}
后端提供一个预览接口,返回文件流。
前端通过video标签播放。
后端简单实现
@GetMapping("/video-preview")
public ResponseEntity<InputStreamResource> preview(@RequestParam String filePath,
@RequestHeader String range) {
// 前端使用video标签发起的请求会在header里自带的range参数,对应视频进度条请求视频内容
return videoPreview(filePath, range);
}
public ResponseEntity<InputStreamResource> videoPreview(String filePath, String range) {
try {
// 获取文件流
InputStream in = getHdfsFileInputStream(filePath);
InputStreamResource resource = new InputStreamResource(in);
// 计算一些需要在response里设置的参数
long fileLen = getHdfsFileStatus(filePath).getLen();
long videoRange = Long.parseLong(range.substring(range.indexOf("=") + 1, range.indexOf("-")));
String[] pathSplit = filePath.split("/");
String fileName = pathSplit[pathSplit.length - 1];
// 这里设置的参数都很关键,不然前端播放视频不能拖动进度条
return ResponseEntity.ok()
.header("Content-type","video/mp4")
.header("Content-Disposition", "attachment; filename="+fileName)
.header("Content-Range", String.valueOf(videoRange + (fileLen-1)))
.header("Accept-Ranges", "bytes")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.contentLength(fileLen)
.body(resource);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
前端简单实现(假设点击播放按钮,在一个Modal内部,生成播放视频的代码)
buildVideoShowData = () => {
const { file } = this.props;
const { path } = file;
const url = `http://ip:port/video-preview?filePath=${path}`;
return (
<video width="840" height="630"
controls=‘controls‘
preload=‘auto‘
autoPlay={true}
loop={true}
>
<source src={url} type="video/mp4"/>
</video>
)
}
如果前端播放的video的src是一个指向视频文件的路径,比如将一些视频存放在部署了前端的同一台服务器的本地硬盘上,src=‘./video/xxx.mp4‘。这样的话不需要后端接口,可以在前端直接播放,并且可以拖动视频进度条控制进度。
但这样相当于把视频在前端写死,如果要支持播放用户上传的视频,就不好搞。
所以提供了后端接口从HDFS中读文件流,并将src设置为接口地址获取文件流。在我一开始写这个后端接口时,并没有设置好这些header参数,导致前端播放视频时无法拖动进度条,只能从头往后看。在设置了这些参数之后,后端就可以根据前端传来的视频的range,返回视频进度条对应的内容,做到真正的在线预览。
原文:https://www.cnblogs.com/yanch01/p/14585297.html