Browse Source

Merge pull request #1023 from hdfg159/v5-dev

add HttpUtil download file return file
Golden Looly 5 years ago
parent
commit
9a94f2e750

+ 52 - 41
hutool-http/src/main/java/cn/hutool/http/HttpResponse.java

@@ -1,25 +1,14 @@
 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.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.io.*;
 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.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.io.*;
 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;
@@ -249,46 +238,39 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
 			}
 			}
 		}
 		}
 	}
 	}
-
+	
 	/**
 	/**
 	 * 将响应内容写出到文件<br>
 	 * 将响应内容写出到文件<br>
 	 * 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出<br>
 	 * 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出<br>
 	 * 写出后会关闭Http流(异步模式)
 	 * 写出后会关闭Http流(异步模式)
-	 * 
-	 * @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) {
 		if (null == destFile) {
 			throw new NullPointerException("[destFile] is null!");
 			throw new NullPointerException("[destFile] is null!");
 		}
 		}
-		if (destFile.isDirectory()) {
-			// 从头信息中获取文件名
-			String fileName = getFileNameFromDisposition();
-			if (StrUtil.isBlank(fileName)) {
-				final String path = this.httpConnection.getUrl().getPath();
-				// 从路径中获取文件名
-				fileName = StrUtil.subSuf(path, path.lastIndexOf('/') + 1);
-				if (StrUtil.isBlank(fileName)) {
-					// 编码后的路径做为文件名
-					fileName = URLUtil.encodeQuery(path, CharsetUtil.CHARSET_UTF_8);
-				}
-			}
-			destFile = FileUtil.file(destFile, fileName);
-		}
-
-		return writeBody(FileUtil.getOutputStream(destFile), true, streamProgress);
+		
+		File outFile = completeFileNameFromHeader(destFile);
+		OutputStream outputStream = FileUtil.getOutputStream(outFile);
+		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) {
@@ -316,7 +298,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();
@@ -324,18 +306,46 @@ 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 目标文件夹或者目标文件
+	 *
+	 * @return File 保存的文件
+	 */
+	public File completeFileNameFromHeader(File destFile) {
+		if (!destFile.isDirectory()) {
+			// 非目录直接返回
+			return destFile;
+		}
+		
+		// 从头信息中获取文件名
+		String fileName = getFileNameFromDisposition();
+		if (StrUtil.isBlank(fileName)) {
+			final String path = httpConnection.getUrl().getPath();
+			// 从路径中获取文件名
+			fileName = StrUtil.subSuf(path, path.lastIndexOf('/') + 1);
+			if (StrUtil.isBlank(fileName)) {
+				// 编码后的路径做为文件名
+				fileName = URLUtil.encodeQuery(path, CharsetUtil.CHARSET_UTF_8);
+			}
+		}
+		return FileUtil.file(destFile, fileName);
+	}
+	
 	// ---------------------------------------------------------------- Private method start
 	// ---------------------------------------------------------------- Private method start
+	
 	/**
 	/**
 	 * 初始化Http响应,并在报错时关闭连接。<br>
 	 * 初始化Http响应,并在报错时关闭连接。<br>
 	 * 初始化包括:
 	 * 初始化包括:
-	 * 
+	 *
 	 * <pre>
 	 * <pre>
 	 * 1、读取Http状态
 	 * 1、读取Http状态
 	 * 2、读取头信息
 	 * 2、读取头信息
@@ -463,10 +473,10 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
 		}
 		}
 		return this;
 		return this;
 	}
 	}
-
+	
 	/**
 	/**
 	 * 从Content-Disposition头中获取文件名
 	 * 从Content-Disposition头中获取文件名
-	 * 
+	 *
 	 * @return 文件名,empty表示无
 	 * @return 文件名,empty表示无
 	 */
 	 */
 	private String getFileNameFromDisposition() {
 	private String getFileNameFromDisposition() {
@@ -480,5 +490,6 @@ public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
 		}
 		}
 		return fileName;
 		return fileName;
 	}
 	}
+	
 	// ---------------------------------------------------------------- Private method end
 	// ---------------------------------------------------------------- Private method end
 }
 }

+ 94 - 13
hutool-http/src/main/java/cn/hutool/http/HttpUtil.java

@@ -8,11 +8,7 @@ import cn.hutool.core.io.StreamProgress;
 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.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.core.util.*;
 import cn.hutool.http.server.SimpleServer;
 import cn.hutool.http.server.SimpleServer;
 
 
 import java.io.File;
 import java.io.File;
@@ -314,6 +310,88 @@ 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 response.writeBody(destFile, streamProgress);
+	}
+	
+	/**
+	 * 下载远程文件
+	 *
+	 * @param url  请求的url
+	 * @param dest 目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名
+	 *
+	 * @return 文件
+	 */
+	public static File downloadFileFromUrl(String url, String dest) {
+		return downloadFileFromUrl(url, FileUtil.file(dest));
+	}
+	
+	/**
+	 * 下载远程文件
+	 *
+	 * @param url      请求的url
+	 * @param destFile 目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名
+	 *
+	 * @return 文件
+	 */
+	public static File downloadFileFromUrl(String url, File destFile) {
+		return downloadFileFromUrl(url, destFile, null);
+	}
+	
+	/**
+	 * 下载远程文件
+	 *
+	 * @param url      请求的url
+	 * @param destFile 目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名
+	 * @param timeout  超时,单位毫秒,-1表示默认超时
+	 *
+	 * @return 文件
+	 */
+	public static File downloadFileFromUrl(String url, File destFile, int timeout) {
+		return downloadFileFromUrl(url, destFile, timeout, null);
+	}
+	
+	/**
+	 * 下载远程文件
+	 *
+	 * @param url            请求的url
+	 * @param destFile       目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名
+	 * @param streamProgress 进度条
+	 *
+	 * @return 文件
+	 */
+	public static File downloadFileFromUrl(String url, File destFile, StreamProgress streamProgress) {
+		return downloadFileFromUrl(url, destFile, -1, streamProgress);
+	}
+	
+	/**
+	 * 下载远程文件
+	 *
+	 * @param url            请求的url
+	 * @param destFile       目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名
+	 * @param timeout        超时,单位毫秒,-1表示默认超时
+	 * @param streamProgress 进度条
+	 *
+	 * @return 文件
+	 */
+	public static File downloadFileFromUrl(String url, File destFile, int timeout, StreamProgress streamProgress) {
+		HttpResponse response = requestDownloadFile(url, destFile, timeout);
+		
+		File outFile = response.completeFileNameFromHeader(destFile);
+		long writeBytes = response.writeBody(outFile, streamProgress);
+		return outFile;
+	}
+	
+	/**
+	 * 请求下载文件
+	 *
+	 * @param url      请求下载文件地址
+	 * @param destFile 目标目录或者目标文件
+	 * @param timeout  超时时间
+	 *
+	 * @return HttpResponse
+	 */
+	private static HttpResponse requestDownloadFile(String url, File destFile, int timeout) {
 		if (StrUtil.isBlank(url)) {
 		if (StrUtil.isBlank(url)) {
 			throw new NullPointerException("[url] is null!");
 			throw new NullPointerException("[url] is null!");
 		}
 		}
@@ -321,18 +399,19 @@ public class HttpUtil {
 			throw new NullPointerException("[destFile] is null!");
 			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 (false == response.isOk()) {
+		if (!response.isOk()) {
 			throw new HttpException("Server response error with status code: [{}]", response.getStatus());
 			throw new HttpException("Server response error with status code: [{}]", response.getStatus());
 		}
 		}
-		return response.writeBody(destFile, streamProgress);
+		return response;
 	}
 	}
-
+	
 	/**
 	/**
 	 * 下载远程文件
 	 * 下载远程文件
 	 *
 	 *
 	 * @param url        请求的url
 	 * @param url        请求的url
 	 * @param out        将下载内容写到输出流中 {@link OutputStream}
 	 * @param out        将下载内容写到输出流中 {@link OutputStream}
 	 * @param isCloseOut 是否关闭输出流
 	 * @param isCloseOut 是否关闭输出流
+	 *
 	 * @return 文件大小
 	 * @return 文件大小
 	 */
 	 */
 	public static long download(String url, OutputStream out, boolean isCloseOut) {
 	public static long download(String url, OutputStream out, boolean isCloseOut) {
@@ -355,29 +434,31 @@ public class HttpUtil {
 		if (null == out) {
 		if (null == out) {
 			throw new NullPointerException("[out] is null!");
 			throw new NullPointerException("[out] is null!");
 		}
 		}
-
+		
 		final HttpResponse response = HttpRequest.get(url).executeAsync();
 		final HttpResponse response = HttpRequest.get(url).executeAsync();
-		if (false == response.isOk()) {
+		if (!response.isOk()) {
 			throw new HttpException("Server response error with status code: [{}]", response.getStatus());
 			throw new HttpException("Server response error with status code: [{}]", response.getStatus());
 		}
 		}
 		return response.writeBody(out, isCloseOut, streamProgress);
 		return response.writeBody(out, isCloseOut, streamProgress);
 	}
 	}
-
+	
 	/**
 	/**
 	 * 下载远程文件数据,支持30x跳转
 	 * 下载远程文件数据,支持30x跳转
 	 *
 	 *
 	 * @param url 请求的url
 	 * @param url 请求的url
+	 *
 	 * @return 文件数据
 	 * @return 文件数据
+	 *
 	 * @since 5.3.6
 	 * @since 5.3.6
 	 */
 	 */
 	public static byte[] downloadBytes(String url) {
 	public static byte[] downloadBytes(String url) {
 		if (StrUtil.isBlank(url)) {
 		if (StrUtil.isBlank(url)) {
 			throw new NullPointerException("[url] is null!");
 			throw new NullPointerException("[url] is null!");
 		}
 		}
-
+		
 		final HttpResponse response = HttpRequest.get(url)
 		final HttpResponse response = HttpRequest.get(url)
 				.setFollowRedirects(true).executeAsync();
 				.setFollowRedirects(true).executeAsync();
-		if (false == response.isOk()) {
+		if (!response.isOk()) {
 			throw new HttpException("Server response error with status code: [{}]", response.getStatus());
 			throw new HttpException("Server response error with status code: [{}]", response.getStatus());
 		}
 		}
 		return response.bodyBytes();
 		return response.bodyBytes();

+ 131 - 4
hutool-http/src/test/java/cn/hutool/http/test/DownloadTest.java

@@ -1,13 +1,17 @@
 package cn.hutool.http.test;
 package cn.hutool.http.test;
 
 
-import org.junit.Ignore;
-import org.junit.Test;
-
 import cn.hutool.core.io.FileUtil;
 import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.IORuntimeException;
 import cn.hutool.core.io.StreamProgress;
 import cn.hutool.core.io.StreamProgress;
 import cn.hutool.core.lang.Console;
 import cn.hutool.core.lang.Console;
 import cn.hutool.http.HttpRequest;
 import cn.hutool.http.HttpRequest;
 import cn.hutool.http.HttpUtil;
 import cn.hutool.http.HttpUtil;
+import org.junit.Assert;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.io.File;
+import java.util.UUID;
 
 
 /**
 /**
  * 下载单元测试
  * 下载单元测试
@@ -56,11 +60,134 @@ public class DownloadTest {
 				long speed = progressSize / (System.currentTimeMillis() - time) * 1000;
 				long speed = progressSize / (System.currentTimeMillis() - time) * 1000;
 				Console.log("已下载:{}, 速度:{}/s", FileUtil.readableFileSize(progressSize), FileUtil.readableFileSize(speed));
 				Console.log("已下载:{}, 速度:{}/s", FileUtil.readableFileSize(progressSize), FileUtil.readableFileSize(speed));
 			}
 			}
-
+			
 			@Override
 			@Override
 			public void finish() {
 			public void finish() {
 				Console.log("下载完成!");
 				Console.log("下载完成!");
 			}
 			}
 		});
 		});
 	}
 	}
+	
+	@Test
+	@Ignore
+	public void downloadFileFromUrlTest1() {
+		File file = HttpUtil.downloadFileFromUrl("http://groovy-lang.org/changelogs/changelog-3.0.5.html", "d:/download/temp");
+		Assert.assertNotNull(file);
+		Assert.assertTrue(file.isFile());
+		Assert.assertTrue(file.length() > 0);
+	}
+	
+	@Test
+	@Ignore
+	public void downloadFileFromUrlTest2() {
+		File file = null;
+		try {
+			file = HttpUtil.downloadFileFromUrl("https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.4.0/hutool-all-5.4.0-sources.jar", FileUtil.file("d:/download/temp"), 1, new StreamProgress() {
+				@Override
+				public void start() {
+					System.out.println("start");
+				}
+				
+				@Override
+				public void progress(long progressSize) {
+					System.out.println("download size:" + progressSize);
+				}
+				
+				@Override
+				public void finish() {
+					System.out.println("end");
+				}
+			});
+			
+			Assert.assertNotNull(file);
+			Assert.assertTrue(file.exists());
+			Assert.assertTrue(file.isFile());
+			Assert.assertTrue(file.length() > 0);
+			Assert.assertTrue(file.getName().length() > 0);
+		} catch (Exception e) {
+			Assert.assertTrue(e instanceof IORuntimeException);
+		} finally {
+			FileUtil.del(file);
+		}
+	}
+	
+	@Test
+	@Ignore
+	public void downloadFileFromUrlTest3() {
+		File file = null;
+		try {
+			file = HttpUtil.downloadFileFromUrl("https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.4.0/hutool-all-5.4.0-sources.jar", FileUtil.file("d:/download/temp"), new StreamProgress() {
+				@Override
+				public void start() {
+					System.out.println("start");
+				}
+				
+				@Override
+				public void progress(long progressSize) {
+					System.out.println("download size:" + progressSize);
+				}
+				
+				@Override
+				public void finish() {
+					System.out.println("end");
+				}
+			});
+			
+			Assert.assertNotNull(file);
+			Assert.assertTrue(file.exists());
+			Assert.assertTrue(file.isFile());
+			Assert.assertTrue(file.length() > 0);
+			Assert.assertTrue(file.getName().length() > 0);
+		} finally {
+			FileUtil.del(file);
+		}
+	}
+	
+	@Test
+	@Ignore
+	public void downloadFileFromUrlTest4() {
+		File file = null;
+		try {
+			file = HttpUtil.downloadFileFromUrl("http://groovy-lang.org/changelogs/changelog-3.0.5.html", FileUtil.file("d:/download/temp"), 1);
+			
+			Assert.assertNotNull(file);
+			Assert.assertTrue(file.exists());
+			Assert.assertTrue(file.isFile());
+			Assert.assertTrue(file.length() > 0);
+			Assert.assertTrue(file.getName().length() > 0);
+		} catch (Exception e) {
+			Assert.assertTrue(e instanceof IORuntimeException);
+		} finally {
+			FileUtil.del(file);
+		}
+	}
+	
+	
+	@Test
+	@Ignore
+	public void downloadFileFromUrlTest5() {
+		File file = null;
+		try {
+			file = HttpUtil.downloadFileFromUrl("http://groovy-lang.org/changelogs/changelog-3.0.5.html", FileUtil.file("d:/download/temp", UUID.randomUUID().toString()));
+			
+			Assert.assertNotNull(file);
+			Assert.assertTrue(file.exists());
+			Assert.assertTrue(file.isFile());
+			Assert.assertTrue(file.length() > 0);
+		} finally {
+			FileUtil.del(file);
+		}
+		
+		File file1 = null;
+		try {
+			file1 = HttpUtil.downloadFileFromUrl("http://groovy-lang.org/changelogs/changelog-3.0.5.html", FileUtil.file("d:/download/temp"));
+			
+			Assert.assertNotNull(file1);
+			Assert.assertTrue(file1.exists());
+			Assert.assertTrue(file1.isFile());
+			Assert.assertTrue(file1.length() > 0);
+		} finally {
+			FileUtil.del(file1);
+		}
+	}
 }
 }