add downloadFileFromUrl

This commit is contained in:
Looly 2020-08-20 17:04:37 +08:00
parent 9a94f2e750
commit 9eec8ec6e0
3 changed files with 108 additions and 80 deletions

View File

@ -8,6 +8,7 @@
### 新特性 ### 新特性
* 【core 】 StrUtil增加firstNonXXX方法issue#1020@Github * 【core 】 StrUtil增加firstNonXXX方法issue#1020@Github
* 【core 】 BeanCopier修改规则可选bean拷贝空字段报错问题pr#160@Gitee * 【core 】 BeanCopier修改规则可选bean拷贝空字段报错问题pr#160@Gitee
* 【http 】 HttpUtil增加downloadFileFromUrlpr#1023@Github
### Bug修复# ### Bug修复#
* 【poi 】 修复ExcelBase.isXlsx方法判断问题issue#I1S502@Gitee * 【poi 】 修复ExcelBase.isXlsx方法判断问题issue#I1S502@Gitee

View File

@ -1,14 +1,26 @@
package cn.hutool.http; package cn.hutool.http;
import cn.hutool.core.convert.Convert; 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.CharsetUtil;
import cn.hutool.core.util.ReUtil; import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil; import cn.hutool.core.util.URLUtil;
import cn.hutool.http.cookie.GlobalCookieManager; 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.net.HttpCookie;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.List; import java.util.List;
@ -17,32 +29,43 @@ import java.util.Map.Entry;
/** /**
* Http响应类<br> * Http响应类<br>
* 非线程安全对象 * 非线程安全对象
*
* @author Looly
* *
* @author Looly
*/ */
public class HttpResponse extends HttpBase<HttpResponse> implements Closeable { public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
/** 持有连接对象 */ /**
* 持有连接对象
*/
protected HttpConnection httpConnection; protected HttpConnection httpConnection;
/** Http请求原始流 */ /**
* Http请求原始流
*/
protected InputStream in; protected InputStream in;
/** 是否异步异步下只持有流否则将在初始化时直接读取body内容 */ /**
* 是否异步异步下只持有流否则将在初始化时直接读取body内容
*/
private volatile boolean isAsync; private volatile boolean isAsync;
/** 响应状态码 */ /**
* 响应状态码
*/
protected int status; protected int status;
/** 是否忽略读取Http响应体 */ /**
* 是否忽略读取Http响应体
*/
private final boolean ignoreBody; private final boolean ignoreBody;
/** 从响应中获取的编码 */ /**
* 从响应中获取的编码
*/
private Charset charsetFromResponse; private Charset charsetFromResponse;
/** /**
* 构造 * 构造
* *
* @param httpConnection {@link HttpConnection} * @param httpConnection {@link HttpConnection}
* @param charset 编码从请求编码中获取默认编码 * @param charset 编码从请求编码中获取默认编码
* @param isAsync 是否异步 * @param isAsync 是否异步
* @param isIgnoreBody 是否忽略读取响应体 * @param isIgnoreBody 是否忽略读取响应体
* @since 3.1.2 * @since 3.1.2
*/ */
protected HttpResponse(HttpConnection httpConnection, Charset charset, boolean isAsync, boolean isIgnoreBody) { protected HttpResponse(HttpConnection httpConnection, Charset charset, boolean isAsync, boolean isIgnoreBody) {
@ -55,7 +78,7 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
/** /**
* 获取状态码 * 获取状态码
* *
* @return 状态码 * @return 状态码
*/ */
public int getStatus() { public int getStatus() {
@ -64,7 +87,7 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
/** /**
* 请求是否成功判断依据为状态码范围在200~299内 * 请求是否成功判断依据为状态码范围在200~299内
* *
* @return 是否成功请求 * @return 是否成功请求
* @since 4.1.9 * @since 4.1.9
*/ */
@ -76,7 +99,7 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
* 同步<br> * 同步<br>
* 如果为异步状态则暂时不读取服务器中响应的内容而是持有Http链接的{@link InputStream}<br> * 如果为异步状态则暂时不读取服务器中响应的内容而是持有Http链接的{@link InputStream}<br>
* 当调用此方法时异步状态转为同步状态此时从Http链接流中读取body内容并暂存在内容中如果已经是同步状态则不进行任何操作 * 当调用此方法时异步状态转为同步状态此时从Http链接流中读取body内容并暂存在内容中如果已经是同步状态则不进行任何操作
* *
* @return this * @return this
*/ */
public HttpResponse sync() { public HttpResponse sync() {
@ -84,9 +107,10 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
} }
// ---------------------------------------------------------------- Http Response Header start // ---------------------------------------------------------------- Http Response Header start
/** /**
* 获取内容编码 * 获取内容编码
* *
* @return String * @return String
*/ */
public String contentEncoding() { public String contentEncoding() {
@ -95,7 +119,7 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
/** /**
* 是否为gzip压缩过的内容 * 是否为gzip压缩过的内容
* *
* @return 是否为gzip压缩过的内容 * @return 是否为gzip压缩过的内容
*/ */
public boolean isGzip() { public boolean isGzip() {
@ -105,7 +129,7 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
/** /**
* 是否为zlib(Defalte)压缩过的内容 * 是否为zlib(Defalte)压缩过的内容
* *
* @return 是否为zlib(Defalte)压缩过的内容 * @return 是否为zlib(Defalte)压缩过的内容
* @since 4.5.7 * @since 4.5.7
*/ */
@ -116,7 +140,7 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
/** /**
* 是否为Transfer-Encoding:Chunked的内容 * 是否为Transfer-Encoding:Chunked的内容
* *
* @return 是否为Transfer-Encoding:Chunked的内容 * @return 是否为Transfer-Encoding:Chunked的内容
* @since 4.6.2 * @since 4.6.2
*/ */
@ -127,7 +151,7 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
/** /**
* 获取本次请求服务器返回的Cookie信息 * 获取本次请求服务器返回的Cookie信息
* *
* @return Cookie字符串 * @return Cookie字符串
* @since 3.1.1 * @since 3.1.1
*/ */
@ -137,7 +161,7 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
/** /**
* 获取Cookie * 获取Cookie
* *
* @return Cookie列表 * @return Cookie列表
* @since 3.1.1 * @since 3.1.1
*/ */
@ -147,7 +171,7 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
/** /**
* 获取Cookie * 获取Cookie
* *
* @param name Cookie名 * @param name Cookie名
* @return {@link HttpCookie} * @return {@link HttpCookie}
* @since 4.1.4 * @since 4.1.4
@ -166,7 +190,7 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
/** /**
* 获取Cookie值 * 获取Cookie值
* *
* @param name Cookie名 * @param name Cookie名
* @return Cookie值 * @return Cookie值
* @since 4.1.4 * @since 4.1.4
@ -178,12 +202,13 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
// ---------------------------------------------------------------- Http Response Header end // ---------------------------------------------------------------- Http Response Header end
// ---------------------------------------------------------------- Body start // ---------------------------------------------------------------- Body start
/** /**
* 获得服务区响应流<br> * 获得服务区响应流<br>
* 异步模式下获取Http原生流同步模式下获取获取到的在内存中的副本<br> * 异步模式下获取Http原生流同步模式下获取获取到的在内存中的副本<br>
* 如果想在同步模式下获取流请先调用{@link #sync()}方法强制同步<br> * 如果想在同步模式下获取流请先调用{@link #sync()}方法强制同步<br>
* 流获取后处理完毕需关闭此类 * 流获取后处理完毕需关闭此类
* *
* @return 响应流 * @return 响应流
*/ */
public InputStream bodyStream() { public InputStream bodyStream() {
@ -196,7 +221,7 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
/** /**
* 获取响应流字节码<br> * 获取响应流字节码<br>
* 此方法会转为同步模式 * 此方法会转为同步模式
* *
* @return byte[] * @return byte[]
*/ */
public byte[] bodyBytes() { public byte[] bodyBytes() {
@ -206,7 +231,7 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
/** /**
* 获取响应主体 * 获取响应主体
* *
* @return String * @return String
* @throws HttpException 包装IO异常 * @throws HttpException 包装IO异常
*/ */
@ -218,9 +243,9 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
* 将响应内容写出到{@link OutputStream}<br> * 将响应内容写出到{@link OutputStream}<br>
* 异步模式下直接读取Http流写出同步模式下将存储在内存中的响应内容写出<br> * 异步模式下直接读取Http流写出同步模式下将存储在内存中的响应内容写出<br>
* 写出后会关闭Http流异步模式 * 写出后会关闭Http流异步模式
* *
* @param out 写出的流 * @param out 写出的流
* @param isCloseOut 是否关闭输出流 * @param isCloseOut 是否关闭输出流
* @param streamProgress 进度显示接口通过实现此接口显示下载进度 * @param streamProgress 进度显示接口通过实现此接口显示下载进度
* @return 写出bytes数 * @return 写出bytes数
* @since 3.3.2 * @since 3.3.2
@ -238,7 +263,7 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
} }
} }
} }
/** /**
* 将响应内容写出到文件<br> * 将响应内容写出到文件<br>
* 异步模式下直接读取Http流写出同步模式下将存储在内存中的响应内容写出<br> * 异步模式下直接读取Http流写出同步模式下将存储在内存中的响应内容写出<br>
@ -246,31 +271,25 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
* *
* @param destFile 写出到的文件 * @param destFile 写出到的文件
* @param streamProgress 进度显示接口通过实现此接口显示下载进度 * @param streamProgress 进度显示接口通过实现此接口显示下载进度
*
* @return 写出bytes数 * @return 写出bytes数
*
* @since 3.3.2 * @since 3.3.2
*/ */
public long writeBody(File destFile, StreamProgress streamProgress) { public long writeBody(File destFile, StreamProgress streamProgress) {
if (null == destFile) { Assert.notNull(destFile, "[destFile] is null!");
throw new NullPointerException("[destFile] is null!");
} final File outFile = completeFileNameFromHeader(destFile);
final OutputStream outputStream = FileUtil.getOutputStream(outFile);
File outFile = completeFileNameFromHeader(destFile);
OutputStream outputStream = FileUtil.getOutputStream(outFile);
return writeBody(outputStream, true, streamProgress); return writeBody(outputStream, true, streamProgress);
} }
/** /**
* 将响应内容写出到文件<br> * 将响应内容写出到文件<br>
* 异步模式下直接读取Http流写出同步模式下将存储在内存中的响应内容写出<br> * 异步模式下直接读取Http流写出同步模式下将存储在内存中的响应内容写出<br>
* 写出后会关闭Http流异步模式 * 写出后会关闭Http流异步模式
* *
* @param destFile 写出到的文件 * @param destFile 写出到的文件
*
* @return 写出bytes数 * @return 写出bytes数
*
* @since 3.3.2 * @since 3.3.2
*/ */
public long writeBody(File destFile) { public long writeBody(File destFile) {
@ -281,7 +300,7 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
* 将响应内容写出到文件<br> * 将响应内容写出到文件<br>
* 异步模式下直接读取Http流写出同步模式下将存储在内存中的响应内容写出<br> * 异步模式下直接读取Http流写出同步模式下将存储在内存中的响应内容写出<br>
* 写出后会关闭Http流异步模式 * 写出后会关闭Http流异步模式
* *
* @param destFilePath 写出到的文件的路径 * @param destFilePath 写出到的文件的路径
* @return 写出bytes数 * @return 写出bytes数
* @since 3.3.2 * @since 3.3.2
@ -298,7 +317,7 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
// 关闭连接 // 关闭连接
this.httpConnection.disconnectQuietly(); this.httpConnection.disconnectQuietly();
} }
@Override @Override
public String toString() { public String toString() {
StringBuilder sb = StrUtil.builder(); StringBuilder sb = StrUtil.builder();
@ -306,26 +325,26 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
for (Entry<String, List<String>> entry : this.headers.entrySet()) { for (Entry<String, List<String>> entry : this.headers.entrySet()) {
sb.append(" ").append(entry).append(StrUtil.CRLF); sb.append(" ").append(entry).append(StrUtil.CRLF);
} }
sb.append("Response Body: ").append(StrUtil.CRLF); sb.append("Response Body: ").append(StrUtil.CRLF);
sb.append(" ").append(this.body()).append(StrUtil.CRLF); sb.append(" ").append(this.body()).append(StrUtil.CRLF);
return sb.toString(); return sb.toString();
} }
/** /**
* 从响应头补全下载文件名 * 从响应头补全下载文件名
* *
* @param destFile 目标文件夹或者目标文件 * @param destFile 目标文件夹或者目标文件
*
* @return File 保存的文件 * @return File 保存的文件
* @since 5.4.1
*/ */
public File completeFileNameFromHeader(File destFile) { public File completeFileNameFromHeader(File destFile) {
if (!destFile.isDirectory()) { if (false == destFile.isDirectory()) {
// 非目录直接返回 // 非目录直接返回
return destFile; return destFile;
} }
// 从头信息中获取文件名 // 从头信息中获取文件名
String fileName = getFileNameFromDisposition(); String fileName = getFileNameFromDisposition();
if (StrUtil.isBlank(fileName)) { if (StrUtil.isBlank(fileName)) {
@ -339,9 +358,9 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
} }
return FileUtil.file(destFile, fileName); return FileUtil.file(destFile, fileName);
} }
// ---------------------------------------------------------------- Private method start // ---------------------------------------------------------------- Private method start
/** /**
* 初始化Http响应并在报错时关闭连接<br> * 初始化Http响应并在报错时关闭连接<br>
* 初始化包括 * 初始化包括
@ -351,7 +370,7 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
* 2读取头信息 * 2读取头信息
* 3持有Http流并不关闭流 * 3持有Http流并不关闭流
* </pre> * </pre>
* *
* @return this * @return this
* @throws HttpException IO异常 * @throws HttpException IO异常
*/ */
@ -368,13 +387,13 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
/** /**
* 初始化Http响应<br> * 初始化Http响应<br>
* 初始化包括 * 初始化包括
* *
* <pre> * <pre>
* 1读取Http状态 * 1读取Http状态
* 2读取头信息 * 2读取头信息
* 3持有Http流并不关闭流 * 3持有Http流并不关闭流
* </pre> * </pre>
* *
* @return this * @return this
* @throws HttpException IO异常 * @throws HttpException IO异常
*/ */
@ -417,7 +436,7 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
/** /**
* 读取主体忽略EOFException异常 * 读取主体忽略EOFException异常
* *
* @param in 输入流 * @param in 输入流
* @throws IORuntimeException IO异常 * @throws IORuntimeException IO异常
*/ */
@ -444,14 +463,14 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
/** /**
* 强制同步用于初始化<br> * 强制同步用于初始化<br>
* 强制同步后变化如下 * 强制同步后变化如下
* *
* <pre> * <pre>
* 1读取body内容到内存 * 1读取body内容到内存
* 2异步状态设为false变为同步状态 * 2异步状态设为false变为同步状态
* 3关闭Http流 * 3关闭Http流
* 4断开与服务器连接 * 4断开与服务器连接
* </pre> * </pre>
* *
* @return this * @return this
*/ */
private HttpResponse forceSync() { private HttpResponse forceSync() {
@ -473,7 +492,7 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
} }
return this; return this;
} }
/** /**
* 从Content-Disposition头中获取文件名 * 从Content-Disposition头中获取文件名
* *
@ -490,6 +509,6 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
} }
return fileName; return fileName;
} }
// ---------------------------------------------------------------- Private method end // ---------------------------------------------------------------- Private method end
} }

View File

@ -5,10 +5,15 @@ import cn.hutool.core.io.FastByteArrayOutputStream;
import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil; import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.StreamProgress; import cn.hutool.core.io.StreamProgress;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil; import cn.hutool.core.map.MapUtil;
import cn.hutool.core.net.url.UrlQuery; import cn.hutool.core.net.url.UrlQuery;
import cn.hutool.core.text.StrBuilder; 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 cn.hutool.http.server.SimpleServer;
import java.io.File; import java.io.File;
@ -310,8 +315,7 @@ public class HttpUtil {
* @since 4.0.4 * @since 4.0.4
*/ */
public static long downloadFile(String url, File destFile, int timeout, StreamProgress streamProgress) { public static long downloadFile(String url, File destFile, int timeout, StreamProgress streamProgress) {
HttpResponse response = requestDownloadFile(url, destFile, timeout); return requestDownloadFile(url, destFile, timeout).writeBody(destFile, streamProgress);
return response.writeBody(destFile, streamProgress);
} }
/** /**
@ -320,7 +324,8 @@ public class HttpUtil {
* @param url 请求的url * @param url 请求的url
* @param dest 目标文件或目录当为目录时取URL中的文件名取不到使用编码后的URL做为文件名 * @param dest 目标文件或目录当为目录时取URL中的文件名取不到使用编码后的URL做为文件名
* *
* @return 文件 * @return 下载的文件对象
* @since 5.4.1
*/ */
public static File downloadFileFromUrl(String url, String dest) { public static File downloadFileFromUrl(String url, String dest) {
return downloadFileFromUrl(url, FileUtil.file(dest)); return downloadFileFromUrl(url, FileUtil.file(dest));
@ -332,7 +337,8 @@ public class HttpUtil {
* @param url 请求的url * @param url 请求的url
* @param destFile 目标文件或目录当为目录时取URL中的文件名取不到使用编码后的URL做为文件名 * @param destFile 目标文件或目录当为目录时取URL中的文件名取不到使用编码后的URL做为文件名
* *
* @return 文件 * @return 下载的文件对象
* @since 5.4.1
*/ */
public static File downloadFileFromUrl(String url, File destFile) { public static File downloadFileFromUrl(String url, File destFile) {
return downloadFileFromUrl(url, destFile, null); return downloadFileFromUrl(url, destFile, null);
@ -345,7 +351,8 @@ public class HttpUtil {
* @param destFile 目标文件或目录当为目录时取URL中的文件名取不到使用编码后的URL做为文件名 * @param destFile 目标文件或目录当为目录时取URL中的文件名取不到使用编码后的URL做为文件名
* @param timeout 超时单位毫秒-1表示默认超时 * @param timeout 超时单位毫秒-1表示默认超时
* *
* @return 文件 * @return 下载的文件对象
* @since 5.4.1
*/ */
public static File downloadFileFromUrl(String url, File destFile, int timeout) { public static File downloadFileFromUrl(String url, File destFile, int timeout) {
return downloadFileFromUrl(url, destFile, timeout, null); return downloadFileFromUrl(url, destFile, timeout, null);
@ -358,7 +365,8 @@ public class HttpUtil {
* @param destFile 目标文件或目录当为目录时取URL中的文件名取不到使用编码后的URL做为文件名 * @param destFile 目标文件或目录当为目录时取URL中的文件名取不到使用编码后的URL做为文件名
* @param streamProgress 进度条 * @param streamProgress 进度条
* *
* @return 文件 * @return 下载的文件对象
* @since 5.4.1
*/ */
public static File downloadFileFromUrl(String url, File destFile, StreamProgress streamProgress) { public static File downloadFileFromUrl(String url, File destFile, StreamProgress streamProgress) {
return downloadFileFromUrl(url, destFile, -1, streamProgress); return downloadFileFromUrl(url, destFile, -1, streamProgress);
@ -372,12 +380,13 @@ public class HttpUtil {
* @param timeout 超时单位毫秒-1表示默认超时 * @param timeout 超时单位毫秒-1表示默认超时
* @param streamProgress 进度条 * @param streamProgress 进度条
* *
* @return 文件 * @return 下载的文件对象
* @since 5.4.1
*/ */
public static File downloadFileFromUrl(String url, File destFile, int timeout, StreamProgress streamProgress) { public static File downloadFileFromUrl(String url, File destFile, int timeout, StreamProgress streamProgress) {
HttpResponse response = requestDownloadFile(url, destFile, timeout); HttpResponse response = requestDownloadFile(url, destFile, timeout);
File outFile = response.completeFileNameFromHeader(destFile); final File outFile = response.completeFileNameFromHeader(destFile);
long writeBytes = response.writeBody(outFile, streamProgress); long writeBytes = response.writeBody(outFile, streamProgress);
return outFile; return outFile;
} }
@ -390,19 +399,18 @@ public class HttpUtil {
* @param timeout 超时时间 * @param timeout 超时时间
* *
* @return HttpResponse * @return HttpResponse
* @since 5.4.1
*/ */
private static HttpResponse requestDownloadFile(String url, File destFile, int timeout) { private static HttpResponse requestDownloadFile(String url, File destFile, int timeout) {
if (StrUtil.isBlank(url)) { Assert.notBlank(url, "[url] is blank !");
throw new NullPointerException("[url] is null!"); Assert.notNull(url, "[destFile] is null !");
}
if (null == destFile) {
throw new NullPointerException("[destFile] is null!");
}
final HttpResponse response = HttpRequest.get(url).timeout(timeout).executeAsync(); final HttpResponse response = HttpRequest.get(url).timeout(timeout).executeAsync();
if (!response.isOk()) { if (response.isOk()) {
throw new HttpException("Server response error with status code: [{}]", response.getStatus()); return response;
} }
return response;
throw new HttpException("Server response error with status code: [{}]", response.getStatus());
} }
/** /**