基于tus协议OSS实现断点续传

本文涉及的产品
对象存储 OSS,20GB 3个月
对象存储 OSS,恶意文件检测 1000次 1年
对象存储 OSS,内容安全 1000次 1年
简介: 当我们说“可恢复文件上传”时,我们指的是上传可以随时中断,然后从失败开始的状态恢复的能力。这种中断可能是意外的(例如,连接中断或服务器崩溃),也可能是用户决定暂停上传时自愿的。在传统的上传实现中,在这种情况下您的进度会丢失,但是 tus 使您能够从这些中断中恢复并在上传停止的地方继续

背景

   当我们说“可恢复文件上传”时,我们指的是上传可以随时中断,然后从失败开始的状态恢复的能力。这种中断可能是意外的(例如,连接中断或服务器崩溃),也可能是用户决定暂停上传时自愿的。在传统的上传实现中,在这种情况下您的进度会丢失,但是 tus 使您能够从这些中断中恢复并在上传停止的地方继续。

适用场景

  • 在部分不可靠的网络上运行,在这些网络中,连接很容易断开或连接可能在一段时间内根本不可用,例如在使用移动数据时。
  • 处理大文件并希望避免因为上传中断而不得不重新上传部分文件(注意:“大”是一个相对词。如果您的上行链路速度为 100KB/s,则 10MB 的文件可能很大)。
  • 希望为您的用户提供暂停上传并稍后(甚至可能在几天后)恢复上传的功能。
  • 不想依赖专有的上传解决方案,而是更喜欢在免费和开源项目的基础上进行构建

工作原理

 一个 tus 上传被分解为不同的 HTTP 请求,每个请求都有不同的目的:

  • 首先,客户端向POST服务器发送请求以发起上传。这个上传创建请求告诉服务器关于上传的基本信息,例如它的大小或附加元数据。如果服务器接受此上传创建请求,它将返回一个成功的响应,并将Location标头设置为上传 URL上传 URL 用于唯一标识和引用新创建的上传资源。
  • 创建上传后,客户端可以通过向PATCH上传 URL发送请求来开始传输实际的上传内容,如前一个POST请求中返回的那样理想情况下,此PATCH请求应包含尽可能多的上传内容,以尽量减少上传持续时间。所述PATCH请求还必须包含Upload-Offset报头,它告诉在该字节偏移量的服务器应写入上传的数据服务器。如果PATCH请求成功传输了整个上传内容,那么您的上传就完成了!
  • 如果PATCH请求因其他原因中断或失败,客户端可以尝试恢复上传。要恢复,客户端必须知道服务器收到了多少数据。此信息是通过向HEAD上传 URL发送请求并检查返回的Upload-Offset标头来获得的。一旦客户端知道上传偏移量,它可以发送另一个PATCH请求,直到上传完成。
  • 或者,如果客户端因为不再需要上传而想要删除上传,DELETE则可以向上传 URL 发送请求。在此之后,上传可以被服务器清理,并且不再可能恢复上传

流程图

请求类型与名字说明

  • OPTIONS请求主要是获取协议描述,支持的各种参数,协议细节,其实tus使用Header来进行服务器和客户端信息交互,OPTIONS需要实现两个Action,一个用于总的协议描述,另一个可以获取到当前文件的上传进度Offset。
  • POST请求当有新文件需要上传时候,注册文件信息,文件名,文件大小。
  • HEAD请求请求当前文件的服务器信息,返回文件大小和当前进度。
  • PATCH请求上传文件,写入磁盘系统。


   名词解释:

  • Upload-Offset:上传偏移,请求和响应header中指定资源的偏移。该值必须是一个非负整数。
  • Upload-Length:上传长度,请求和响应header中指定整个上载的大小。该值必须是一个非负整数。
  • Tus-Version:协议版本,响应报头必须是逗号分隔的由服务器支持的协议版本的列表。该列表必须按服务器的偏好排序
  • Tus-Resumable:报头必须被包含在每个请求中除了响应OPTIONS请求。该值必须是客户端或服务器使用的协议版本。如果客户端指定的版本不被服务器支持,它必须以412 Precondition Failed状态响应并且必须Tus-Version在响应中包含 头。此外,服务器不得处理请求。
  • Tus-Max-Size:响应报头必须是一个非负整数,其表示以字节为单位的整个上载所允许的最大值

java demo实现

废话不多说我们开始上手写代码,当我们上传一个大文件时候

1.首先执行OPTIONS请求可以是用于收集有关服务器的当前配置信息

  • 前端Request
OPTIONS/filesHTTP/1.1Host: http://localhost:8080/tus


  • 服务端需要对该请求做出响应和对应的java代码实现
HTTP/1.1204NoContentTus-Resumable: 1.0.0Tus-Version: 1.0.0,0.2.2,0.2.1Tus-Max-Size: 1073741824Tus-Extension: creation,expiration
@RequestMapping(method=RequestMethod.OPTIONS)
publicvoidgetTusOptions(HttpServletResponseresponse) {
response.setHeader(ACCESS_CONTROL_EXPOSE_HEADER, ACCESS_CONTROL_EXPOSE_OPTIONS_VALUE);
response.setHeader(TUS_RESUMABLE_HEADER, TUS_RESUMABLE_VALUE);
response.setHeader(TUS_VERSION_HEADER, TUS_VERSION_VALUE);
response.setHeader(TUS_MAX_SIZE_HEADER, String.valueOf(tusConfigProperties.getMaxSize()));
response.setHeader(TUS_EXTENTION_HEADER, TUS_EXTENTION_VALUE);
response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, ACCESS_CONTROL_ALLOW_ORIGIN_VALUE);
response.setHeader(ACCESS_CONTROL_ALLOW_METHODS_HEADER, ACCESS_CONTROL_ALLOW_METHODS_VALUE);
response.setStatus(HttpStatus.NO_CONTENT.value());
    }


2.执行POST请求获取Location,需要必须指定Upload-FileIdUpload-FileName

  • 前端Request
POST/filesHTTP/1.1Host: http://localhost:8080/tusContent-Length: 0Upload-Length: 100Tus-Resumable: 1.0.0Upload-FileId: 8CDFF8AE590F41EF94D322BCE31F5B51Upload-FileName: test


  • 服务端需要对该请求做出响应和对应的java代码实现
HTTP/1.1201CreatedLocation: http://localhost:8080/tus/8CDFF8AE590F41EF94D322BCE31F5B51Tus-Resumable: 1.0.0@PostMappingpublicvoidprocessPost(@RequestHeader(value=UPLOAD_LENGTH_HEADER, required=false) IntegeruploadLength,
@RequestHeader(UPLOAD_FILE_ID_HEADER) StringfileId,
@RequestHeader(UPLOAD_FILE_NAME_HEADER) StringfileName,
HttpServletRequestrequest,
HttpServletResponseresponse) throwsUnsupportedEncodingException {
fileName=URLDecoder.decode(fileName, "utf-8");
tusFileUploadService.initUpload(fileId, uploadLength, fileName);
response.setHeader(ACCESS_CONTROL_EXPOSE_HEADER, ACCESS_CONTROL_EXPOSE_POST_VALUE);
Stringlocation=UriComponentsBuilder.fromUriString(request.getRequestURI() +"/"+fileId).build().toString();
response.setHeader(LOCATION_HEADER, location);
response.setHeader(TUS_RESUMABLE_HEADER, TUS_RESUMABLE_VALUE);
response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, ACCESS_CONTROL_ALLOW_ORIGIN_VALUE);
response.setHeader(ACCESS_CONTROL_ALLOW_METHODS_HEADER, ACCESS_CONTROL_ALLOW_METHODS_VALUE);
response.setStatus(HttpStatus.CREATED.value());
    }
publicTusFileUploadinitUpload(StringfileId, IntegeruploadLength, StringfileName) {
uploadLength=null==uploadLength?0 : uploadLength;
if (uploadLength>tusConfigProperties.getMaxSize()) {
thrownewBizException("上传的文件大小不超过"+FileUtil.prettySize(tusConfigProperties.getMaxSize()));
        }
OSSossClient=buildOssClient();
InitiateMultipartUploadRequestinitiateMultipartUploadRequest=newInitiateMultipartUploadRequest(
ossConfigProperties.getBucket(), "tus/"+fileId+"/"+fileName);
InitiateMultipartUploadResultresult=ossClient.initiateMultipartUpload(initiateMultipartUploadRequest);
StringuploadId=result.getUploadId();
TusFileUploadtusFileUpload=newTusFileUpload();
tusFileUpload.setFileId(fileId);
tusFileUpload.setUploadLength(uploadLength);
tusFileUpload.setOffset(0);
tusFileUpload.setFileName(fileName);
tusFileUpload.setUploadId(uploadId);
tusFileUpload.setWrappedPartETags(newArrayList<>());
saveToRedis(tusFileUpload);
ossClient.shutdown();
returntusFileUpload;
    }


3.拿到返回的Location之后就会带上fileId继续PATCH请求

  • 前端Request
PATCH/files/8CDFF8AE590F41EF94D322BCE31F5B51HTTP/1.1Host: http://localhost:8080/tusContent-Type: application/offset+octet-streamContent-Length: 30Upload-Offset: 0Tus-Resumable: 1.0.0[remaining30bytes]


  • 服务端需要对该请求做出响应和对应的java代码实现
HTTP/1.1204NoContentTus-Resumable: 1.0.0Upload-Offset: 70
@RequestMapping(method= {RequestMethod.PATCH, RequestMethod.POST}, value="/{fileId}")
publicvoidprocessPatch(@RequestHeader(UPLOAD_OFFSET_HEADER) LonguploadOffset,
@RequestHeader(value=CONTENT_LENGTH_HEADER, required=false) LongcontentLength,
@RequestHeader(CONTENT_TYPE_HEADER) StringcontentType,
@PathVariableStringfileId,
InputStreaminputStream,
HttpServletResponseresponse) {
if (null==uploadOffset||uploadOffset<0) {
thrownewBizException("文件上传大小异常");
        }
if (!TUS_CONTENT_TYPE_VALUE.equals(contentType)) {
thrownewBizException("文件类型异常");
        }
TusFileUploadtusFileUpload=tusFileUploadService.findOne(fileId);
log.debug("Tus file offset: [{}]", tusFileUpload.getOffset());
log.debug("Tus file final length: [{}]", tusFileUpload.getUploadLength());
if (tusFileUpload.getUploadLength() <tusFileUpload.getOffset()) {
thrownewBizException("文件上传大小异常");
        }
//successfulif (tusFileUpload.getUploadLength() ==tusFileUpload.getOffset()) {
tusFileUploadService.completeUpload(tusFileUpload);
response.setHeader("Upload-Offset", Long.toString(tusFileUpload.getOffset()));
response.setStatus(HttpStatus.OK.value());
return;
        }
PartListinguploadedParts=tusFileUploadService.listUploadedParts(tusFileUpload);
// 真正上传longnewOffset=tusFileUploadService.uploadPart(tusFileUpload, inputStream, contentLength, uploadedParts);
// 不需要这次上传完成就通知200状态,到下次 patch 获取状态,否则 python 客户端会验证失败。if (tusFileUpload.getUploadLength() ==newOffset) {
tusFileUploadService.completeUpload(tusFileUpload);
        }
response.setHeader(ACCESS_CONTROL_EXPOSE_HEADER, ACCESS_CONTROL_EXPOSE_PATCH_VALUE);
response.setHeader(TUS_RESUMABLE_HEADER, TUS_RESUMABLE_VALUE);
response.setHeader(UPLOAD_OFFSET_HEADER, Long.toString(newOffset));
response.setStatus(HttpStatus.NO_CONTENT.value());
    }


4.如果我们在此期间发生了断网和故意暂停。他会终端上传并且把当前的偏移量缓存起来。我们再次点击继续上传的时候会先调用head请求获取缓存中对应的偏移量之后则重复第3步继续上传直到完成为止.

  • 前端Request
HEAD/files/24e533e02ec3bc40c387f1a0e460e216HTTP/1.1Host: http://localhost:8080/tusTus-Resumable: 1.0.0


  • 服务端需要对该请求做出响应和对应的java代码实现
HTTP/1.1200OKUpload-Offset: 70Tus-Resumable: 1.0.0
@RequestMapping(method=RequestMethod.HEAD, value="/{fileId}")
publicvoidprocessHead(@PathVariableStringfileId, HttpServletResponseresponse) {
TusFileUploadfile=tusFileUploadService.findOne(fileId);
log.debug("File: [{}] upload offset: [{}]", fileId, file.getOffset());
response.setHeader(ACCESS_CONTROL_EXPOSE_HEADER, ACCESS_CONTROL_EXPOSE_HEAD_VALUE);
response.setHeader(UPLOAD_OFFSET_HEADER, Long.toString(file.getOffset()));
response.setHeader(UPLOAD_LENGTH_HEADER, Long.toString(file.getUploadLength()));
response.setHeader(TUS_RESUMABLE_HEADER, TUS_RESUMABLE_VALUE);
response.setStatus(HttpStatus.OK.value());
    }


相关实践学习
借助OSS搭建在线教育视频课程分享网站
本教程介绍如何基于云服务器ECS和对象存储OSS,搭建一个在线教育视频课程分享网站。
相关文章
|
Java 对象存储
|
9月前
|
前端开发 API 开发工具
阿里云oss开发实践:大文件分片、断点续传、实时进度 React+Node+Socket.IO
阿里云oss开发实践:大文件分片、断点续传、实时进度 React+Node+Socket.IO
933 1
|
10月前
|
存储 弹性计算 安全
HTTP、CDN 和 OSS 为什么过时了?深入聊聊 Web3 世界中的协议和硬盘:IPFS
HTTP、CDN 和 OSS 为什么过时了?深入聊聊 Web3 世界中的协议和硬盘:IPFS
246 0
|
机器学习/深度学习 SQL 分布式计算
如何将模型更新到 OSS 实现离线更新|学习笔记
快速学习如何将模型更新到 OSS 实现离线更新。
183 0
如何将模型更新到 OSS 实现离线更新|学习笔记
|
SQL 存储 Cloud Native
CDN转存离线日志到OSS,实现更长时间的日志存储
CDN转存离线日志到OSS,实现更长时间的日志存储
532 1
CDN转存离线日志到OSS,实现更长时间的日志存储
|
存储 安全 Java
SpringBoot2.x系列教程24--SpringBoot实现文件上传到OSS阿里云存储
前言 在上一章节中,壹哥 给大家讲解了如何在SpringBoot实现单个文件和多个文件上传,但是那种方式现在挺多项目中都不再采用了,而是会把文件上传到云存储服务器中,比如阿里云、腾讯云、华为云等。所以今天 壹哥 利用阿里云的OSS存储,讲解如何把本地文件存储到云存储服务器中,本文会教给你如何购买配置阿里云OSS,并且教你如何实现文件上传到云空间,干货满满哦。 一. 阿里云OSS简介 1. 存储服务简介 我们进行项目开发,很多时候都需要进行文件、图片等的上传,对于很多项目来说,虽然有FastDFS等文件存储服务器技术,但其实我们完全没有必要搭建自己的图片等文件服务器。对一个小型非专业的应用来说,
1437 0
|
消息中间件 JavaScript 数据挖掘
|
监控 关系型数据库 Serverless
基于Serverless+OSS分分钟实现图片秒变素描
在阿里云Serverless函数计算服务中部署普通图片转素描图函数服务,实现将批量上传到指定OSS桶内的图片自动转换为素描图并保存到另一个OSS桶内
基于Serverless+OSS分分钟实现图片秒变素描
|
弹性计算 对象存储 网络虚拟化
CEN+私网NAT实现跨地域访问OSS需求-CEN基础版
CEN+私网NAT实现跨地域访问OSS需求-CEN基础版
J3
|
存储 安全 API
快捷餐饮之店家后台OSS文件管理实现
快捷餐饮之店家后台OSS文件管理实现
J3
305 0
快捷餐饮之店家后台OSS文件管理实现