From 9eec8ec6e013aa820fc8ddca7cb476ebc9158637 Mon Sep 17 00:00:00 2001 From: Looly Date: Thu, 20 Aug 2020 17:04:37 +0800 Subject: [PATCH] add downloadFileFromUrl --- CHANGELOG.md | 1 + .../java/cn/hutool/http/HttpResponse.java | 143 ++++++++++-------- .../main/java/cn/hutool/http/HttpUtil.java | 44 +++--- 3 files changed, 108 insertions(+), 80 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index afea31399..fda811d8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### 新特性 * 【core 】 StrUtil增加firstNonXXX方法(issue#1020@Github) * 【core 】 BeanCopier修改规则,可选bean拷贝空字段报错问题(pr#160@Gitee) +* 【http 】 HttpUtil增加downloadFileFromUrl(pr#1023@Github) ### Bug修复# * 【poi 】 修复ExcelBase.isXlsx方法判断问题(issue#I1S502@Gitee) diff --git a/hutool-http/src/main/java/cn/hutool/http/HttpResponse.java b/hutool-http/src/main/java/cn/hutool/http/HttpResponse.java index 371c30719..f22a41e27 100644 --- a/hutool-http/src/main/java/cn/hutool/http/HttpResponse.java +++ b/hutool-http/src/main/java/cn/hutool/http/HttpResponse.java @@ -1,14 +1,26 @@ package cn.hutool.http; import cn.hutool.core.convert.Convert; -import cn.hutool.core.io.*; +import cn.hutool.core.io.FastByteArrayOutputStream; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.io.StreamProgress; +import cn.hutool.core.lang.Assert; import cn.hutool.core.util.CharsetUtil; import cn.hutool.core.util.ReUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.URLUtil; import cn.hutool.http.cookie.GlobalCookieManager; -import java.io.*; +import java.io.ByteArrayInputStream; +import java.io.Closeable; +import java.io.EOFException; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.net.HttpCookie; import java.nio.charset.Charset; import java.util.List; @@ -17,32 +29,43 @@ import java.util.Map.Entry; /** * Http响应类
* 非线程安全对象 - * - * @author Looly * + * @author Looly */ public class HttpResponse extends HttpBase implements Closeable { - /** 持有连接对象 */ + /** + * 持有连接对象 + */ protected HttpConnection httpConnection; - /** Http请求原始流 */ + /** + * Http请求原始流 + */ protected InputStream in; - /** 是否异步,异步下只持有流,否则将在初始化时直接读取body内容 */ + /** + * 是否异步,异步下只持有流,否则将在初始化时直接读取body内容 + */ private volatile boolean isAsync; - /** 响应状态码 */ + /** + * 响应状态码 + */ protected int status; - /** 是否忽略读取Http响应体 */ + /** + * 是否忽略读取Http响应体 + */ private final boolean ignoreBody; - /** 从响应中获取的编码 */ + /** + * 从响应中获取的编码 + */ private Charset charsetFromResponse; /** * 构造 - * + * * @param httpConnection {@link HttpConnection} - * @param charset 编码,从请求编码中获取默认编码 - * @param isAsync 是否异步 - * @param isIgnoreBody 是否忽略读取响应体 + * @param charset 编码,从请求编码中获取默认编码 + * @param isAsync 是否异步 + * @param isIgnoreBody 是否忽略读取响应体 * @since 3.1.2 */ protected HttpResponse(HttpConnection httpConnection, Charset charset, boolean isAsync, boolean isIgnoreBody) { @@ -55,7 +78,7 @@ public class HttpResponse extends HttpBase implements Closeable { /** * 获取状态码 - * + * * @return 状态码 */ public int getStatus() { @@ -64,7 +87,7 @@ public class HttpResponse extends HttpBase implements Closeable { /** * 请求是否成功,判断依据为:状态码范围在200~299内。 - * + * * @return 是否成功请求 * @since 4.1.9 */ @@ -76,7 +99,7 @@ public class HttpResponse extends HttpBase implements Closeable { * 同步
* 如果为异步状态,则暂时不读取服务器中响应的内容,而是持有Http链接的{@link InputStream}。
* 当调用此方法时,异步状态转为同步状态,此时从Http链接流中读取body内容并暂存在内容中。如果已经是同步状态,则不进行任何操作。 - * + * * @return this */ public HttpResponse sync() { @@ -84,9 +107,10 @@ public class HttpResponse extends HttpBase implements Closeable { } // ---------------------------------------------------------------- Http Response Header start + /** * 获取内容编码 - * + * * @return String */ public String contentEncoding() { @@ -95,7 +119,7 @@ public class HttpResponse extends HttpBase implements Closeable { /** * 是否为gzip压缩过的内容 - * + * * @return 是否为gzip压缩过的内容 */ public boolean isGzip() { @@ -105,7 +129,7 @@ public class HttpResponse extends HttpBase implements Closeable { /** * 是否为zlib(Defalte)压缩过的内容 - * + * * @return 是否为zlib(Defalte)压缩过的内容 * @since 4.5.7 */ @@ -116,7 +140,7 @@ public class HttpResponse extends HttpBase implements Closeable { /** * 是否为Transfer-Encoding:Chunked的内容 - * + * * @return 是否为Transfer-Encoding:Chunked的内容 * @since 4.6.2 */ @@ -127,7 +151,7 @@ public class HttpResponse extends HttpBase implements Closeable { /** * 获取本次请求服务器返回的Cookie信息 - * + * * @return Cookie字符串 * @since 3.1.1 */ @@ -137,7 +161,7 @@ public class HttpResponse extends HttpBase implements Closeable { /** * 获取Cookie - * + * * @return Cookie列表 * @since 3.1.1 */ @@ -147,7 +171,7 @@ public class HttpResponse extends HttpBase implements Closeable { /** * 获取Cookie - * + * * @param name Cookie名 * @return {@link HttpCookie} * @since 4.1.4 @@ -166,7 +190,7 @@ public class HttpResponse extends HttpBase implements Closeable { /** * 获取Cookie值 - * + * * @param name Cookie名 * @return Cookie值 * @since 4.1.4 @@ -178,12 +202,13 @@ public class HttpResponse extends HttpBase implements Closeable { // ---------------------------------------------------------------- Http Response Header end // ---------------------------------------------------------------- Body start + /** * 获得服务区响应流
* 异步模式下获取Http原生流,同步模式下获取获取到的在内存中的副本
* 如果想在同步模式下获取流,请先调用{@link #sync()}方法强制同步
* 流获取后处理完毕需关闭此类 - * + * * @return 响应流 */ public InputStream bodyStream() { @@ -196,7 +221,7 @@ public class HttpResponse extends HttpBase implements Closeable { /** * 获取响应流字节码
* 此方法会转为同步模式 - * + * * @return byte[] */ public byte[] bodyBytes() { @@ -206,7 +231,7 @@ public class HttpResponse extends HttpBase implements Closeable { /** * 获取响应主体 - * + * * @return String * @throws HttpException 包装IO异常 */ @@ -218,9 +243,9 @@ public class HttpResponse extends HttpBase implements Closeable { * 将响应内容写出到{@link OutputStream}
* 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出
* 写出后会关闭Http流(异步模式) - * - * @param out 写出的流 - * @param isCloseOut 是否关闭输出流 + * + * @param out 写出的流 + * @param isCloseOut 是否关闭输出流 * @param streamProgress 进度显示接口,通过实现此接口显示下载进度 * @return 写出bytes数 * @since 3.3.2 @@ -238,7 +263,7 @@ public class HttpResponse extends HttpBase implements Closeable { } } } - + /** * 将响应内容写出到文件
* 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出
@@ -246,31 +271,25 @@ public class HttpResponse extends HttpBase implements Closeable { * * @param destFile 写出到的文件 * @param streamProgress 进度显示接口,通过实现此接口显示下载进度 - * * @return 写出bytes数 - * * @since 3.3.2 */ public long writeBody(File destFile, StreamProgress streamProgress) { - if (null == destFile) { - throw new NullPointerException("[destFile] is null!"); - } - - File outFile = completeFileNameFromHeader(destFile); - OutputStream outputStream = FileUtil.getOutputStream(outFile); + Assert.notNull(destFile, "[destFile] is null!"); + + final File outFile = completeFileNameFromHeader(destFile); + final OutputStream outputStream = FileUtil.getOutputStream(outFile); return writeBody(outputStream, true, streamProgress); } - - + + /** * 将响应内容写出到文件
* 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出
* 写出后会关闭Http流(异步模式) * * @param destFile 写出到的文件 - * * @return 写出bytes数 - * * @since 3.3.2 */ public long writeBody(File destFile) { @@ -281,7 +300,7 @@ public class HttpResponse extends HttpBase implements Closeable { * 将响应内容写出到文件
* 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出
* 写出后会关闭Http流(异步模式) - * + * * @param destFilePath 写出到的文件的路径 * @return 写出bytes数 * @since 3.3.2 @@ -298,7 +317,7 @@ public class HttpResponse extends HttpBase implements Closeable { // 关闭连接 this.httpConnection.disconnectQuietly(); } - + @Override public String toString() { StringBuilder sb = StrUtil.builder(); @@ -306,26 +325,26 @@ public class HttpResponse extends HttpBase implements Closeable { for (Entry> entry : this.headers.entrySet()) { sb.append(" ").append(entry).append(StrUtil.CRLF); } - + sb.append("Response Body: ").append(StrUtil.CRLF); sb.append(" ").append(this.body()).append(StrUtil.CRLF); - + return sb.toString(); } - + /** * 从响应头补全下载文件名 * * @param destFile 目标文件夹或者目标文件 - * * @return File 保存的文件 + * @since 5.4.1 */ public File completeFileNameFromHeader(File destFile) { - if (!destFile.isDirectory()) { + if (false == destFile.isDirectory()) { // 非目录直接返回 return destFile; } - + // 从头信息中获取文件名 String fileName = getFileNameFromDisposition(); if (StrUtil.isBlank(fileName)) { @@ -339,9 +358,9 @@ public class HttpResponse extends HttpBase implements Closeable { } return FileUtil.file(destFile, fileName); } - + // ---------------------------------------------------------------- Private method start - + /** * 初始化Http响应,并在报错时关闭连接。
* 初始化包括: @@ -351,7 +370,7 @@ public class HttpResponse extends HttpBase implements Closeable { * 2、读取头信息 * 3、持有Http流,并不关闭流 * - * + * * @return this * @throws HttpException IO异常 */ @@ -368,13 +387,13 @@ public class HttpResponse extends HttpBase implements Closeable { /** * 初始化Http响应
* 初始化包括: - * + * *
 	 * 1、读取Http状态
 	 * 2、读取头信息
 	 * 3、持有Http流,并不关闭流
 	 * 
- * + * * @return this * @throws HttpException IO异常 */ @@ -417,7 +436,7 @@ public class HttpResponse extends HttpBase implements Closeable { /** * 读取主体,忽略EOFException异常 - * + * * @param in 输入流 * @throws IORuntimeException IO异常 */ @@ -444,14 +463,14 @@ public class HttpResponse extends HttpBase implements Closeable { /** * 强制同步,用于初始化
* 强制同步后变化如下: - * + * *
 	 * 1、读取body内容到内存
 	 * 2、异步状态设为false(变为同步状态)
 	 * 3、关闭Http流
 	 * 4、断开与服务器连接
 	 * 
- * + * * @return this */ private HttpResponse forceSync() { @@ -473,7 +492,7 @@ public class HttpResponse extends HttpBase implements Closeable { } return this; } - + /** * 从Content-Disposition头中获取文件名 * @@ -490,6 +509,6 @@ public class HttpResponse extends HttpBase implements Closeable { } return fileName; } - + // ---------------------------------------------------------------- Private method end } diff --git a/hutool-http/src/main/java/cn/hutool/http/HttpUtil.java b/hutool-http/src/main/java/cn/hutool/http/HttpUtil.java index f64ab753d..db816cd27 100644 --- a/hutool-http/src/main/java/cn/hutool/http/HttpUtil.java +++ b/hutool-http/src/main/java/cn/hutool/http/HttpUtil.java @@ -5,10 +5,15 @@ import cn.hutool.core.io.FastByteArrayOutputStream; import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.IoUtil; import cn.hutool.core.io.StreamProgress; +import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; import cn.hutool.core.net.url.UrlQuery; import cn.hutool.core.text.StrBuilder; -import cn.hutool.core.util.*; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; import cn.hutool.http.server.SimpleServer; import java.io.File; @@ -310,8 +315,7 @@ public class HttpUtil { * @since 4.0.4 */ public static long downloadFile(String url, File destFile, int timeout, StreamProgress streamProgress) { - HttpResponse response = requestDownloadFile(url, destFile, timeout); - return response.writeBody(destFile, streamProgress); + return requestDownloadFile(url, destFile, timeout).writeBody(destFile, streamProgress); } /** @@ -320,7 +324,8 @@ public class HttpUtil { * @param url 请求的url * @param dest 目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名 * - * @return 文件 + * @return 下载的文件对象 + * @since 5.4.1 */ public static File downloadFileFromUrl(String url, String dest) { return downloadFileFromUrl(url, FileUtil.file(dest)); @@ -332,7 +337,8 @@ public class HttpUtil { * @param url 请求的url * @param destFile 目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名 * - * @return 文件 + * @return 下载的文件对象 + * @since 5.4.1 */ public static File downloadFileFromUrl(String url, File destFile) { return downloadFileFromUrl(url, destFile, null); @@ -345,7 +351,8 @@ public class HttpUtil { * @param destFile 目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名 * @param timeout 超时,单位毫秒,-1表示默认超时 * - * @return 文件 + * @return 下载的文件对象 + * @since 5.4.1 */ public static File downloadFileFromUrl(String url, File destFile, int timeout) { return downloadFileFromUrl(url, destFile, timeout, null); @@ -358,7 +365,8 @@ public class HttpUtil { * @param destFile 目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名 * @param streamProgress 进度条 * - * @return 文件 + * @return 下载的文件对象 + * @since 5.4.1 */ public static File downloadFileFromUrl(String url, File destFile, StreamProgress streamProgress) { return downloadFileFromUrl(url, destFile, -1, streamProgress); @@ -372,12 +380,13 @@ public class HttpUtil { * @param timeout 超时,单位毫秒,-1表示默认超时 * @param streamProgress 进度条 * - * @return 文件 + * @return 下载的文件对象 + * @since 5.4.1 */ public static File downloadFileFromUrl(String url, File destFile, int timeout, StreamProgress streamProgress) { HttpResponse response = requestDownloadFile(url, destFile, timeout); - File outFile = response.completeFileNameFromHeader(destFile); + final File outFile = response.completeFileNameFromHeader(destFile); long writeBytes = response.writeBody(outFile, streamProgress); return outFile; } @@ -390,19 +399,18 @@ public class HttpUtil { * @param timeout 超时时间 * * @return HttpResponse + * @since 5.4.1 */ private static HttpResponse requestDownloadFile(String url, File destFile, int timeout) { - if (StrUtil.isBlank(url)) { - throw new NullPointerException("[url] is null!"); - } - if (null == destFile) { - throw new NullPointerException("[destFile] is null!"); - } + Assert.notBlank(url, "[url] is blank !"); + Assert.notNull(url, "[destFile] is null !"); + final HttpResponse response = HttpRequest.get(url).timeout(timeout).executeAsync(); - if (!response.isOk()) { - throw new HttpException("Server response error with status code: [{}]", response.getStatus()); + if (response.isOk()) { + return response; } - return response; + + throw new HttpException("Server response error with status code: [{}]", response.getStatus()); } /**