就HTTP而言,客户端下载的只是一堆字节。 但是,客户真的很想知道如何解释这些字节。 它是图像吗? 或者也许是ZIP文件? 本系列的最后一部分描述了如何向客户端提示她下载的内容。

设置

内容类型描述了要返回的资源的MIME类型 。 此标头指示Web浏览器如何处理从下载服务器流出的字节流。 如果没有此标头,浏览器将无法得知其实际接收到的内容,只会像显示文本文件一样显示内容。 不用说二进制PDF(请参见上面的屏幕截图),像文本文件一样显示的图像或视频看起来并不好。 最难的部分是以某种方式实际获得媒体类型。 幸运的是,Java本身有一个用于根据资源的扩展名和/或内容来猜测媒体类型的工具:

import com.google.common.net.MediaType;
import java.io.*;
import java.time.Instant;public class FileSystemPointer implements FilePointer {private final MediaType mediaTypeOrNull;public FileSystemPointer(File target) {final String contentType = java.nio.file.Files.probeContentType(target.toPath());this.mediaTypeOrNull = contentType != null ?MediaType.parse(contentType) :null;}

请注意,使用Optional<T>作为类字段不是惯用的,因为它不是可Serializable并且我们避免了潜在的问题。 知道媒体类型后,我们必须在响应中返回它。 注意,这一小段代码使用了JDK 8和Guava中的Optional ,以及Spring框架和Guava中的MediaType类。 多么糟糕的类型系统!

private ResponseEntity<Resource> response(FilePointer filePointer, HttpStatus status, Resource body) {final ResponseEntity.BodyBuilder responseBuilder = ResponseEntity.status(status).eTag(filePointer.getEtag()).contentLength(filePointer.getSize()).lastModified(filePointer.getLastModified().toEpochMilli());filePointer.getMediaType().map(this::toMediaType).ifPresent(responseBuilder::contentType);return responseBuilder.body(body);
}private MediaType toMediaType(com.google.common.net.MediaType input) {return input.charset().transform(c -> new MediaType(input.type(), input.subtype(), c)).or(new MediaType(input.type(), input.subtype()));
}@Override
public Optional<MediaType> getMediaType() {return Optional.ofNullable(mediaTypeOrNull);
}

保留原始文件名和扩展名

当您直接在Web浏览器中打开文档时,虽然Content-type效果很好,但是可以想象您的用户将该文档存储在磁盘上。 浏览器是决定显示还是存储下载的文件不在本文的讨论范围之内,但是我们应该为两者做好准备。 如果浏览器只是将文件存储在磁盘上,则必须使用某种名称进行保存。 默认情况下,Firefox将使用URL的最后一部分,在本例中,该部分恰好是资源的UUID。 不太用户友好。 铬是好一点-知道根据MIME类型Content-type报头,将试探性地加入适当的扩展名,例如.zip中的情况下, application/zip 。 但是文件名仍然是随机的UUID,而用户上传的文件可能是cats.zip 。 因此,如果您的目标是浏览器而不是自动化客户端,则最好使用真实名称作为URL的最后一部分。 我们仍然希望使用UUID在内部区分资源,避免冲突并且不公开我们的内部存储结构。 但是在外部,我们可以重定向到用户友好的URL,但为了安全起见保留UUID。 首先,我们需要一个额外的端点:

@RequestMapping(method = {GET, HEAD}, value = "/{uuid}")
public ResponseEntity<Resource> redirect(HttpMethod method,@PathVariable UUID uuid,@RequestHeader(IF_NONE_MATCH) Optional<String> requestEtagOpt,@RequestHeader(IF_MODIFIED_SINCE) Optional<Date> ifModifiedSinceOpt) {return findExistingFile(method, uuid).map(file -> file.redirect(requestEtagOpt, ifModifiedSinceOpt)).orElseGet(() -> new ResponseEntity<>(NOT_FOUND));
}@RequestMapping(method = {GET, HEAD}, value = "/{uuid}/{filename}")
public ResponseEntity<Resource> download(HttpMethod method,@PathVariable UUID uuid,@RequestHeader(IF_NONE_MATCH) Optional<String> requestEtagOpt,@RequestHeader(IF_MODIFIED_SINCE) Optional<Date> ifModifiedSinceOpt) {return findExistingFile(method, uuid).map(file -> file.handle(requestEtagOpt, ifModifiedSinceOpt)).orElseGet(() -> new ResponseEntity<>(NOT_FOUND));
}private Optional<ExistingFile> findExistingFile(HttpMethod method, @PathVariable UUID uuid) {return storage.findFile(uuid).map(pointer -> new ExistingFile(method, pointer, uuid));
}

如果仔细观察,甚至没有使用{filename} ,它只是浏览器的提示。 如果需要额外的安全性,可以将提供的文件名与映射到给定UUID文件名进行比较。 这里真正重要的是,仅要求提供UUID重定向我们:

$ curl -v localhost:8080/download/4a8883b6-ead6-4b9e-8979-85f9846cab4b
> GET /download/4a8883b6-ead6-4b9e-8979-85f9846cab4b HTTP/1.1
...
< HTTP/1.1 301 Moved Permanently
< Location: /download/4a8883b6-ead6-4b9e-8979-85f9846cab4b/cats.zip

而且您需要进行一次额外的网络行程来获取实际文件:

> GET /download/4a8883b6-ead6-4b9e-8979-85f9846cab4b/cats.zip HTTP/1.1
...
>
HTTP/1.1 200 OK
< ETag: "be20c3b1...fb1a4"
< Last-Modified: Thu, 21 Aug 2014 22:44:37 GMT
< Content-Type: application/zip;charset=UTF-8
< Content-Length: 489455

该实现很简单,但是为了避免重复,对其进行了一些重构:

public ResponseEntity<Resource> redirect(Optional<String> requestEtagOpt, Optional<Date> ifModifiedSinceOpt) {if (cached(requestEtagOpt, ifModifiedSinceOpt))return notModified(filePointer);return redirectDownload(filePointer);
}public ResponseEntity<Resource> handle(Optional<String> requestEtagOpt, Optional<Date> ifModifiedSinceOpt) {if (cached(requestEtagOpt, ifModifiedSinceOpt))return notModified(filePointer);return serveDownload(filePointer);
}private boolean cached(Optional<String> requestEtagOpt, Optional<Date> ifModifiedSinceOpt) {final boolean matchingEtag = requestEtagOpt.map(filePointer::matchesEtag).orElse(false);final boolean notModifiedSince = ifModifiedSinceOpt.map(Date::toInstant).map(filePointer::modifiedAfter).orElse(false);return matchingEtag || notModifiedSince;
}private ResponseEntity<Resource> redirectDownload(FilePointer filePointer) {try {log.trace("Redirecting {} '{}'", method, filePointer);return ResponseEntity.status(MOVED_PERMANENTLY).location(new URI("/download/" + uuid + "/" + filePointer.getOriginalName())).body(null);} catch (URISyntaxException e) {throw new IllegalArgumentException(e);}
}private ResponseEntity<Resource> serveDownload(FilePointer filePointer) {log.debug("Serving {} '{}'", method, filePointer);final InputStreamResource resource = resourceToReturn(filePointer);return response(filePointer, OK, resource);
}

您甚至可以进一步使用高阶函数来避免重复:

public ResponseEntity<Resource> redirect(Optional<String> requestEtagOpt, Optional<Date> ifModifiedSinceOpt) {return serveWithCaching(requestEtagOpt, ifModifiedSinceOpt, this::redirectDownload);
}public ResponseEntity<Resource> handle(Optional<String> requestEtagOpt, Optional<Date> ifModifiedSinceOpt) {return serveWithCaching(requestEtagOpt, ifModifiedSinceOpt, this::serveDownload);
}private ResponseEntity<Resource> serveWithCaching(Optional<String> requestEtagOpt, Optional<Date> ifModifiedSinceOpt, Function<FilePointer, ResponseEntity<Resource>> notCachedResponse) {if (cached(requestEtagOpt, ifModifiedSinceOpt))return notModified(filePointer);return notCachedResponse.apply(filePointer);
}

显然,额外的重定向是每次下载都必须支付的额外费用,因此这是一个折衷方案。 您可以考虑基于User-agent启发式(如果是浏览器,则为重定向;如果是自动客户端,则为服务器),以避免非人工客户端的重定向。 这样就结束了我们有关文件下载的系列文章。 HTTP / 2的出现必将带来更多的改进和技术,例如确定优先级。

编写下载服务器

  • 第一部分:始终流式传输,永远不要完全保留在内存中
  • 第二部分:标头:Last-Modified,ETag和If-None-Match
  • 第三部分:标头:内容长度和范围
  • 第四部分:有效地实现HEAD操作
  • 第五部分:油门下载速度
  • 第六部分:描述您发送的内容(内容类型等)
  • 这些文章中开发的示例应用程序可在GitHub上找到。

翻译自: https://www.javacodegeeks.com/2015/07/writing-a-download-server-part-vi-describe-what-you-send-content-type-et-al.html

编写下载服务器。 第六部分:描述您发送的内容(内容类型等)相关推荐

  1. 服务器编写_编写下载服务器。 第六部分:描述您发送的内容(内容类型等)...

    服务器编写 就HTTP而言,客户端下载的只是一堆字节. 但是,客户真的很想知道如何解释这些字节. 它是图像吗? 还是ZIP文件? 本系列的最后一部分描述了如何向客户端提示她下载的内容. 设置 内容类型 ...

  2. mwc校准油门_编写下载服务器。 第五部分:油门下载速度

    mwc校准油门 在僵尸网络时代,您可以租用几百美元来运行自己的分布式拒绝服务攻击,拥有紧急开关来有选择地关闭昂贵的功能或严重降低性能是一个巨大的胜利. 在缓解问题的同时,您的应用程序仍可运行. 当然, ...

  3. 服务器禁止head 请求_编写下载服务器。 第四部分:有效地执行HEAD操作

    服务器禁止head 请求 HEAD是一个经常被遗忘的HTTP方法(动词),其行为类似于GET,但不返回正文. 您使用HEAD来检查资源的存在(如果不存在,它应该返回404),并确保您的缓存中没有陈旧的 ...

  4. 502无法解析服务器标头_编写下载服务器。 第三部分:标头:内容长度和范围...

    502无法解析服务器标头 这次,我们将探索更多HTTP请求和响应标头,以改善下载服务器的实现: Content-length和Range . 前者表示下载量很大,后者允许部分下载文件,或者从我们开始的 ...

  5. grpc 流式传输_编写下载服务器。 第一部分:始终流式传输,永远不要完全保留在内存中...

    grpc 流式传输 下载各种文件(文本或二进制文件)是每个企业应用程序的生死攸关的事情. PDF文档,附件,媒体,可执行文件,CSV,超大文件等.几乎每个应用程序迟早都必须提供某种形式的下载. 下载是 ...

  6. 502无法解析服务器标头_编写下载服务器。 第二部分:标头:Last-Modified,ETag和If-None-Match...

    502无法解析服务器标头 客户端缓存是万维网的基础之一. 服务器应通知客户端资源的有效性,客户端应尽可能快地对其进行缓存. 如我们所见,如果不缓存Web,它将非常缓慢. 只需在任何网站上Ctrl + ...

  7. 编写下载服务器。 第五部分:油门下载速度

    在僵尸网络时代,您可以租用几百美元来运行自己的分布式拒绝服务攻击,拥有紧急开关有选择地关闭昂贵的功能或极大地降低性能是一个巨大的胜利. 在缓解问题的同时,您的应用程序仍可运行. 当然,这种安全措施在高 ...

  8. 编写下载服务器。 第四部分:有效地实现HEAD操作

    HEAD是一个经常被遗忘的HTTP方法(动词),其行为类似于GET,但不会返回正文. 您使用HEAD来检查资源的存在(如果不存在,它应该返回404),并确保您的缓存中没有陈旧的版本. 在这种情况下,您 ...

  9. 编写下载服务器。 第一部分:始终流式传输,永远不要完全保留在内存中

    下载各种文件(文本或二进制文件)是每个企业应用程序的生死攸关的事情. PDF文档,附件,媒体,可执行文件,CSV,超大文件等.几乎每个应用程序迟早都必须提供某种形式的下载. 下载是通过HTTP来实现的 ...

最新文章

  1. Android之GSON解析JSON
  2. day26 re正则表达式
  3. 推荐一个在线图片处理神奇,图片处理绝大多数需求,都能在浏览器里搞定
  4. soapui返回值类型都有哪些_法兰的类型都有哪些以及法兰的设计
  5. 2017.7.31 征途 失败总结
  6. 简历职称 计算机,简历中专业职称是什么
  7. Python新手需要掌握的知识点
  8. 001信息化和信息系统
  9. 【原创】/Restarting/ Splay树 (普通平衡树 文艺平衡树 bzoj1895 poj 2580 SuperMemo 题解)
  10. 谈谈我了解的那些在线it学习网站
  11. win10便签常驻桌面_win10技巧分享第六篇——win10自带的备忘录便签功能
  12. 发送候选文字到光标所在位置
  13. 企业邮箱域名怎么选?公司邮箱格式怎么写?
  14. learn.log - 进程管理器fastcgi原理及fastcgi_param详解
  15. 如何为一个kafka集群选择topics/partitions的数量
  16. ad stm8l 热电偶_STM8L之ADC
  17. html求三角形的面积,JavaScript计算三角形面积
  18. HTML中的图片如何自适应屏幕?这篇文章有图片的自适应用法介绍
  19. SQL千万级大数据量查询优化
  20. iperf安装与使用

热门文章

  1. 班级日常分享,一天一瞬间
  2. EF框架中,在实体中手动更新字段,数据库数据未同步到程序中应该怎么解决呢?
  3. Photoshop图像修饰工具
  4. 集合中重写equals方法删除new的对象
  5. PostgreSQL 数据类型
  6. arm芯片厂家排名_国产芯片目前至少有10种,其中有3种,达到了世界顶尖水平
  7. think-in-java(9)接口
  8. 查看oracle会话和进程_带有Oracle Digital Assistant和Fn Project的会话式UI
  9. gluon_带有Gluon Ignite和Dagger的JavaFX中的依赖注入
  10. selenide_使用Selenide进行有效的UI测试