├── .gitignore ├── LICENSE ├── README.md ├── README_zh-CN.md ├── pom.xml └── src ├── main ├── java │ └── org │ │ └── pdown │ │ └── core │ │ ├── DownClient.java │ │ ├── boot │ │ ├── HttpDownBootstrap.java │ │ ├── HttpDownBootstrapBuilder.java │ │ └── URLHttpDownBootstrapBuilder.java │ │ ├── constant │ │ └── HttpDownStatus.java │ │ ├── dispatch │ │ ├── ConsoleHttpDownCallback.java │ │ └── HttpDownCallback.java │ │ ├── entity │ │ ├── ChunkInfo.java │ │ ├── ConnectInfo.java │ │ ├── HttpDownConfigInfo.java │ │ ├── HttpHeadsInfo.java │ │ ├── HttpRequestInfo.java │ │ ├── HttpResponseInfo.java │ │ └── TaskInfo.java │ │ ├── exception │ │ ├── BootstrapBuildException.java │ │ ├── BootstrapCreateDirException.java │ │ ├── BootstrapException.java │ │ ├── BootstrapFileAlreadyExistsException.java │ │ ├── BootstrapNoPermissionException.java │ │ ├── BootstrapNoSpaceException.java │ │ ├── BootstrapPathEmptyException.java │ │ └── BootstrapResolveException.java │ │ ├── handle │ │ └── DownTimeoutHandler.java │ │ ├── proxy │ │ ├── ProxyConfig.java │ │ ├── ProxyHandleFactory.java │ │ └── ProxyType.java │ │ └── util │ │ ├── ByteUtil.java │ │ ├── FileUtil.java │ │ ├── HttpDownUtil.java │ │ ├── OsUtil.java │ │ └── ProtoUtil.java └── resources │ └── logback.xml └── test ├── java └── org │ └── pdown │ └── core │ └── test │ ├── ConsoleDownTestClient.java │ ├── DownClientTest.java │ ├── server │ ├── ChunkedDownTestServer.java │ └── RangeDownTestServer.java │ └── util │ └── TestUtil.java └── resources └── logback-test.xml /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | /target 3 | *.class 4 | 5 | # Log file 6 | *.log 7 | 8 | # BlueJ files 9 | *.ctxt 10 | 11 | # Mobile Tools for Java (J2ME) 12 | .mtj.tmp/ 13 | 14 | # Package Files # 15 | *.jar 16 | *.war 17 | *.nar 18 | *.ear 19 | *.zip 20 | *.tar.gz 21 | *.rar 22 | 23 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 24 | hs_err_pid* 25 | 26 | #IDEA 27 | .idea/ 28 | *.iml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 proxyee-down-org 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HTTP high speed downloader 2 | [中文](https://github.com/proxyee-down-org/pdown-core/blob/master/README_zh-CN.md) 3 | Using java NIO(netty),fast down and easy to customize. 4 | ## Guide 5 | ### Create download task 6 | To create a download task, need to construct a request and a config. 7 | ### Request 8 | Build a HTTP request. 9 | #### Method 10 | The default method is GET,We can also use POST,PUT... 11 | #### Heads 12 | The default request header refers to the following table. 13 | 14 | key | value 15 | ---|--- 16 | Host | {host} 17 | Connection | keep-alive 18 | User-Agent | Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.75 Safari/537.36 19 | Accept | text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 20 | Referer | {host} 21 | 22 | We can customize the request header,when overriding the default value, the default request header will be replaced. 23 | *{host}:is Domain name parsed from url* 24 | #### Content 25 | Set the request body,only support text format. 26 | ### Response 27 | Default will request once for response related information,such as support HTTP 206,Content-Length,attach name. 28 | If know response Content-Length and name,can create a task directly, without spending a request to resolve the task name and size. 29 | ### Option 30 | Download options. 31 | 32 | field | default | desc 33 | ---|---|--- 34 | filePath | {root} | Saved path 35 | fileName | {name} | Saved file name 36 | totalSize | 0(B) | The total size of the file to be downloaded 37 | supportRange | false | Does the server support Accept-Ranges 38 | connections | 16 | Concurrent connections 39 | timeout | 30(S) | If no response during this time,will re-initiate connection. 40 | retryCount | 5 | All connection download exceptions exceed this times,will be stop download. 41 | autoRename | false | Automatic rename when checking to download directory with duplicate file. 42 | speedLimit | 0(B/S) | Download speed limit. 43 | 44 | ## Demo 45 | ``` 46 | //Download a file with URL 47 | HttpDownBootstrap.builder("http://127.0.0.1/static/test.zip") 48 | .build() 49 | .start(); 50 | //Download a file with a custom request and response 51 | HttpDownBootstrap.builder() 52 | .request(new HttpRequestInfo(HttpMethod.GET, "http://127.0.0.1/static/test.zip")) 53 | .response(new HttpResponseInfo(2048,true)) 54 | .build() 55 | .start(); 56 | //Set download option 57 | HttpDownBootstrap.builder("http://127.0.0.1/static/test.zip") 58 | .downConfig(new HttpDownConfigInfo().setConnections(32).setAutoRename(true).setSpeedLimit(1024*1024*5L)) 59 | .build() 60 | .start(); 61 | //Set proxy config 62 | HttpDownBootstrap.builder("http://127.0.0.1/static/test.zip") 63 | .proxyConfig(new ProxyConfig(ProxyType.HTTP,"127.0.0.1",8888)) 64 | .build() 65 | .start(); 66 | //Set callback 67 | HttpDownBootstrap.builder("http://127.0.0.1/static/test.zip") 68 | .callback(new ConsoleHttpDownCallback()) 69 | .build() 70 | .start(); 71 | ``` 72 | ## Build 73 | ``` 74 | git clone git@github.com:proxyee-down-org/pdown-core.git 75 | cd pdown-core 76 | mvn clean package -Dmaven.test.skip=true -Pexec 77 | ``` 78 | ## Run 79 | ``` 80 | #See help 81 | java -jar pdown.jar -h 82 | #Download with default configuration 83 | java -jar pdown.jar "http://127.0.0.1/static/test.zip" 84 | ``` 85 | 86 | -------------------------------------------------------------------------------- /README_zh-CN.md: -------------------------------------------------------------------------------- 1 | # HTTP高速下载器 2 | [English](https://github.com/proxyee-down-org/pdown-core/blob/master/README.md) 3 | 使用JAVA NIO(netty)编写,速度快且易定制。 4 | ## 指南 5 | ### 创建一个下载任务 6 | 在创建下载任务之前,需要先构造好下载的请求,以及下载的相关选项。 7 | ### 请求 8 | 请求属性基本和HTTP协议一致,一般只需要定义一个请求URL,剩下的都使用默认值就好。 9 | #### 请求方法 10 | 请求方法默认为GET,也可以设置为POST,PUT等等。 11 | #### 请求头 12 | 默认的请求头参见下表 13 | 14 | key | value 15 | ---|--- 16 | Host | {host} 17 | Connection | keep-alive 18 | User-Agent | Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.75 Safari/537.36 19 | Accept | text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 20 | Referer | {host} 21 | 22 | 可以自定义请求头,当与默认请求头重复时将覆盖默认请求头 23 | *{host}:从请求地址中解析出来的域名* 24 | #### 请求体 25 | 可以自定义请求体,目前只支持文本格式。 26 | ### 响应 27 | 默认情况下,会发起一次请求来获得响应内容,例如判断响应是否支持HTTP 206,响应大小,文件名称。 28 | 如果已经知道响应体的大小和名称的话,可以直接创建并开始一个下载任务,可以节省一次请求去解析响应相关信息。 29 | ### 选项 30 | 下载选项用于配置下载相关的属性,具体参见下表 31 | 32 | 属性名 | 默认值 | 描述 33 | ---|---|--- 34 | filePath | {root} | 下载目录 35 | fileName | {name} | 文件名 36 | totalSize | 0(B) | 下载文件的总大小 37 | supportRange | false | 是否支持206 Accept-Ranges响应(即断点下载) 38 | connections | 16 | 下载连接数 39 | timeout | 30(S) | 响应超时时间,若超时会自动重连. 40 | retryCount | 5 | 重试次数,当所有连接请求异常都超过这个值时,任务会停止下载. 41 | autoRename | false | 是否自动重命名,当检测到下载目录有同名文件时自动重命名,否则会停止下载. 42 | speedLimit | 0(B/S) | 下载速度限制. 43 | 44 | ## 示例 45 | ``` 46 | //通过url下载一个文件 47 | HttpDownBootstrap.builder("http://127.0.0.1/static/test.zip") 48 | .build() 49 | .start(); 50 | //自定义请求和响应来下载一个文件 51 | HttpDownBootstrap.builder() 52 | .request(new HttpRequestInfo(HttpMethod.GET, "http://127.0.0.1/static/test.zip")) 53 | .response(new HttpResponseInfo(2048,true)) 54 | .build() 55 | .start(); 56 | //设置下载选项 57 | HttpDownBootstrap.builder("http://127.0.0.1/static/test.zip") 58 | .downConfig(new HttpDownConfigInfo().setConnections(32).setAutoRename(true).setSpeedLimit(1024*1024*5L)) 59 | .build() 60 | .start(); 61 | //设置下载的二级代理 62 | HttpDownBootstrap.builder("http://127.0.0.1/static/test.zip") 63 | .proxyConfig(new ProxyConfig(ProxyType.HTTP,"127.0.0.1",8888)) 64 | .build() 65 | .start(); 66 | //设置下载的回调 67 | HttpDownBootstrap.builder("http://127.0.0.1/static/test.zip") 68 | .callback(new ConsoleHttpDownCallback()) 69 | .build() 70 | .start(); 71 | ``` 72 | ## 编译 73 | ``` 74 | git clone git@github.com:proxyee-down-org/pdown-core.git 75 | cd pdown-core 76 | mvn clean package -Dmaven.test.skip=true -Pexec 77 | ``` 78 | ## 运行 79 | ``` 80 | #查看帮助 81 | java -jar pdown.jar -h 82 | #用默认选项下载 83 | java -jar pdown.jar "http://127.0.0.1/static/test.zip" 84 | ``` 85 | 86 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.pdown 8 | core 9 | 1.0.1-SNAPSHOT 10 | 11 | pdown-core 12 | HTTP high speed downloader 13 | https://github.com/proxyee-down-org/pdown-core 14 | 15 | 16 | 17 | The MIT License (MIT) 18 | http://opensource.org/licenses/mit-license.php 19 | 20 | 21 | 22 | 23 | 24 | monkeyWie 25 | liwei2633@163.com 26 | 27 | 28 | 29 | 30 | scm:git:https://github.com/proxyee-down-org/pdown-core.git 31 | scm:git:https://github.com/proxyee-down-org/pdown-core.git 32 | https://github.com/proxyee-down-org/pdown-core 33 | 34 | 35 | 36 | 37 | UTF-8 38 | 1.8 39 | 1.8 40 | 4.1.25.Final 41 | 42 | 43 | 44 | 45 | org.slf4j 46 | slf4j-api 47 | 1.7.25 48 | 49 | 50 | ch.qos.logback 51 | logback-core 52 | 1.2.3 53 | 54 | 55 | ch.qos.logback 56 | logback-classic 57 | 1.2.3 58 | 59 | 60 | io.netty 61 | netty-buffer 62 | ${netty.version} 63 | 64 | 65 | io.netty 66 | netty-codec 67 | ${netty.version} 68 | 69 | 70 | io.netty 71 | netty-codec-http 72 | ${netty.version} 73 | 74 | 75 | io.netty 76 | netty-handler 77 | ${netty.version} 78 | 79 | 80 | io.netty 81 | netty-handler-proxy 82 | ${netty.version} 83 | 84 | 85 | commons-cli 86 | commons-cli 87 | 1.4 88 | 89 | 90 | junit 91 | junit 92 | 4.12 93 | test 94 | 95 | 96 | 97 | 98 | 99 | 100 | true 101 | src/main/resources 102 | 103 | logback.xml 104 | 105 | 106 | 107 | 108 | 109 | org.apache.maven.plugins 110 | maven-compiler-plugin 111 | 112 | 113 | 114 | 115 | 116 | 117 | exec 118 | 119 | 120 | 121 | true 122 | src/main/resources 123 | 124 | logback.xml 125 | 126 | 127 | 128 | pdown 129 | 130 | 131 | maven-assembly-plugin 132 | 133 | false 134 | 135 | jar-with-dependencies 136 | 137 | 138 | 139 | org.pdown.core.DownClient 140 | 141 | 142 | 143 | 144 | 145 | make-assembly 146 | package 147 | 148 | assembly 149 | 150 | 151 | 152 | 153 | 154 | org.apache.maven.plugins 155 | maven-compiler-plugin 156 | 157 | 158 | 159 | 160 | 161 | release 162 | 163 | 164 | 165 | 166 | org.apache.maven.plugins 167 | maven-compiler-plugin 168 | 169 | 170 | 171 | org.apache.maven.plugins 172 | maven-source-plugin 173 | 174 | 175 | package 176 | 177 | jar-no-fork 178 | 179 | 180 | 181 | 182 | 183 | 184 | org.apache.maven.plugins 185 | maven-javadoc-plugin 186 | 187 | 188 | package 189 | 190 | jar 191 | 192 | 193 | 194 | 195 | 196 | 197 | org.apache.maven.plugins 198 | maven-gpg-plugin 199 | 200 | 201 | sign-artifacts 202 | verify 203 | 204 | sign 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | releases 214 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 215 | 216 | 217 | snapshots 218 | https://oss.sonatype.org/content/repositories/snapshots/ 219 | 220 | 221 | 222 | 223 | -------------------------------------------------------------------------------- /src/main/java/org/pdown/core/DownClient.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core; 2 | 3 | import io.netty.util.internal.StringUtil; 4 | import org.pdown.core.boot.HttpDownBootstrap; 5 | import org.pdown.core.boot.URLHttpDownBootstrapBuilder; 6 | import org.pdown.core.dispatch.ConsoleHttpDownCallback; 7 | import org.pdown.core.entity.HttpDownConfigInfo; 8 | import org.pdown.core.entity.HttpResponseInfo; 9 | import org.pdown.core.exception.BootstrapBuildException; 10 | import org.pdown.core.exception.BootstrapResolveException; 11 | import org.pdown.core.proxy.ProxyConfig; 12 | import org.pdown.core.proxy.ProxyType; 13 | import java.util.LinkedHashMap; 14 | import java.util.Map; 15 | import java.util.concurrent.TimeoutException; 16 | import org.apache.commons.cli.CommandLine; 17 | import org.apache.commons.cli.CommandLineParser; 18 | import org.apache.commons.cli.DefaultParser; 19 | import org.apache.commons.cli.HelpFormatter; 20 | import org.apache.commons.cli.Option; 21 | import org.apache.commons.cli.Options; 22 | import org.apache.commons.cli.ParseException; 23 | 24 | public class DownClient { 25 | 26 | public static void start(String[] args) { 27 | HelpFormatter formatter = new HelpFormatter(); 28 | Options options = new Options(); 29 | options.addOption(Option.builder("U") 30 | .longOpt("url") 31 | .hasArg() 32 | .desc("Set the request URL.") 33 | .build()); 34 | options.addOption(Option.builder("H") 35 | .longOpt("heads") 36 | .hasArg() 37 | .desc("Set the HTTP request head.") 38 | .build()); 39 | options.addOption(Option.builder("B") 40 | .longOpt("body") 41 | .hasArg() 42 | .desc("Set the HTTP request body.") 43 | .build()); 44 | options.addOption(Option.builder("P") 45 | .longOpt("path") 46 | .hasArg() 47 | .desc("Set download path.") 48 | .build()); 49 | options.addOption(Option.builder("N") 50 | .longOpt("name") 51 | .hasArg() 52 | .desc("Set the name of the download file.") 53 | .build()); 54 | options.addOption(Option.builder("C") 55 | .longOpt("connections") 56 | .hasArg() 57 | .desc("Set the number of download connections.") 58 | .build()); 59 | options.addOption(Option.builder("S") 60 | .longOpt("speedLimit") 61 | .hasArg() 62 | .desc("Set download maximum speed limit(B/S).") 63 | .build()); 64 | options.addOption(Option.builder("X") 65 | .longOpt("proxy") 66 | .hasArg() 67 | .desc("[protocol://]host:port Set proxy,support HTTP,SOCKS4,SOCKS5.") 68 | .build()); 69 | options.addOption(Option.builder("h") 70 | .longOpt("help") 71 | .desc("See the help.") 72 | .build()); 73 | if (args == null || args.length == 0) { 74 | formatter.printHelp("parse error:", options); 75 | return; 76 | } 77 | if (args[0].trim().charAt(0) != '-' && !args[0].equals("-h") && !args[0].equals("--help")) { 78 | args[0] = "-U=" + args[0]; 79 | } 80 | CommandLineParser parser = new DefaultParser(); 81 | try { 82 | CommandLine line = parser.parse(options, args); 83 | if (line.hasOption("h")) { 84 | formatter.printHelp("pdDown ", options); 85 | return; 86 | } 87 | String url = line.getOptionValue("U"); 88 | if (url == null || "".equals(url.trim())) { 89 | formatter.printHelp("URL can't be empty", options); 90 | return; 91 | } 92 | URLHttpDownBootstrapBuilder builder = HttpDownBootstrap.builder(line.getOptionValue("U")); 93 | String[] headsStr = line.getOptionValues("H"); 94 | if (headsStr != null && headsStr.length > 0) { 95 | Map heads = new LinkedHashMap<>(); 96 | for (String headStr : headsStr) { 97 | String[] headArray = headStr.split(":"); 98 | heads.put(headArray[0], headArray[1]); 99 | } 100 | builder.heads(heads); 101 | } 102 | builder.body(line.getOptionValue("B")); 103 | if (line.hasOption("N")) { 104 | builder.response(new HttpResponseInfo(line.getOptionValue("N"))); 105 | } 106 | builder.downConfig(new HttpDownConfigInfo() 107 | .setFilePath(line.getOptionValue("P")) 108 | .setConnections(Integer.parseInt(line.getOptionValue("C", "0"))) 109 | .setSpeedLimit(Integer.parseInt(line.getOptionValue("S", "0")))); 110 | String proxy = line.getOptionValue("X"); 111 | if (!StringUtil.isNullOrEmpty(proxy)) { 112 | ProxyType proxyType = ProxyType.HTTP; 113 | String[] proxyArray; 114 | int protocolIndex = proxy.indexOf("://"); 115 | if (protocolIndex != -1) { 116 | proxyType = ProxyType.valueOf(proxy.substring(0, protocolIndex).toUpperCase()); 117 | proxyArray = proxy.substring(protocolIndex + 3).split(":"); 118 | } else { 119 | proxyArray = proxy.split(":"); 120 | } 121 | builder.proxyConfig(new ProxyConfig(proxyType, proxyArray[0], Integer.parseInt(proxyArray[1]))); 122 | } 123 | HttpDownBootstrap bootstrap = builder.callback(new ConsoleHttpDownCallback()).build(); 124 | bootstrap.start(); 125 | } catch (ParseException e) { 126 | formatter.printHelp("Unrecognized option", options); 127 | } 128 | } 129 | 130 | public static void main(String[] args) { 131 | try { 132 | start(args); 133 | } catch (BootstrapBuildException e) { 134 | if (e.getCause() instanceof TimeoutException) { 135 | System.out.println("Connection failed, please check the network."); 136 | } else if (e.getCause() instanceof BootstrapResolveException) { 137 | System.out.println(e.getCause().getMessage()); 138 | } else { 139 | e.printStackTrace(); 140 | } 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/main/java/org/pdown/core/boot/HttpDownBootstrap.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.boot; 2 | 3 | import io.netty.bootstrap.Bootstrap; 4 | import io.netty.buffer.ByteBuf; 5 | import io.netty.channel.Channel; 6 | import io.netty.channel.ChannelFuture; 7 | import io.netty.channel.ChannelFutureListener; 8 | import io.netty.channel.ChannelHandlerContext; 9 | import io.netty.channel.ChannelInboundHandlerAdapter; 10 | import io.netty.channel.ChannelInitializer; 11 | import io.netty.channel.nio.NioEventLoopGroup; 12 | import io.netty.channel.socket.nio.NioSocketChannel; 13 | import io.netty.handler.codec.http.DefaultLastHttpContent; 14 | import io.netty.handler.codec.http.HttpClientCodec; 15 | import io.netty.handler.codec.http.HttpContent; 16 | import io.netty.handler.codec.http.HttpHeaderNames; 17 | import io.netty.handler.codec.http.HttpResponse; 18 | import io.netty.handler.codec.http.LastHttpContent; 19 | import io.netty.handler.timeout.ReadTimeoutException; 20 | import io.netty.resolver.NoopAddressResolverGroup; 21 | import io.netty.util.ReferenceCountUtil; 22 | import io.netty.util.internal.StringUtil; 23 | import java.io.File; 24 | import java.io.IOException; 25 | import java.io.Serializable; 26 | import java.net.URL; 27 | import java.nio.channels.SeekableByteChannel; 28 | import java.nio.file.Files; 29 | import java.nio.file.Paths; 30 | import java.nio.file.StandardOpenOption; 31 | import java.util.ArrayList; 32 | import java.util.Comparator; 33 | import java.util.List; 34 | import java.util.Map; 35 | import java.util.concurrent.TimeUnit; 36 | import org.pdown.core.constant.HttpDownStatus; 37 | import org.pdown.core.dispatch.HttpDownCallback; 38 | import org.pdown.core.entity.ChunkInfo; 39 | import org.pdown.core.entity.ConnectInfo; 40 | import org.pdown.core.entity.HttpDownConfigInfo; 41 | import org.pdown.core.entity.HttpRequestInfo; 42 | import org.pdown.core.entity.HttpResponseInfo; 43 | import org.pdown.core.entity.TaskInfo; 44 | import org.pdown.core.exception.BootstrapCreateDirException; 45 | import org.pdown.core.exception.BootstrapException; 46 | import org.pdown.core.exception.BootstrapFileAlreadyExistsException; 47 | import org.pdown.core.exception.BootstrapNoPermissionException; 48 | import org.pdown.core.exception.BootstrapNoSpaceException; 49 | import org.pdown.core.exception.BootstrapPathEmptyException; 50 | import org.pdown.core.handle.DownTimeoutHandler; 51 | import org.pdown.core.proxy.ProxyConfig; 52 | import org.pdown.core.proxy.ProxyHandleFactory; 53 | import org.pdown.core.util.FileUtil; 54 | import org.pdown.core.util.HttpDownUtil; 55 | import org.pdown.core.util.OsUtil; 56 | import org.pdown.core.util.ProtoUtil.RequestProto; 57 | import org.slf4j.Logger; 58 | import org.slf4j.LoggerFactory; 59 | 60 | public class HttpDownBootstrap implements Serializable { 61 | 62 | private static final long serialVersionUID = 7265797940598817922L; 63 | private static final Logger LOGGER = LoggerFactory.getLogger(HttpDownBootstrap.class); 64 | 65 | private static final long SIZE_1MB = 1024 * 1024L; 66 | private static final double TIME_1S_NANO = (double) TimeUnit.SECONDS.toNanos(1); 67 | 68 | private HttpRequestInfo request; 69 | private HttpResponseInfo response; 70 | private HttpDownConfigInfo downConfig; 71 | private ProxyConfig proxyConfig; 72 | private TaskInfo taskInfo; 73 | private transient HttpDownCallback callback; 74 | private transient volatile NioEventLoopGroup loopGroup; 75 | private transient volatile ProgressThread progressThread; 76 | 77 | private HttpDownBootstrap() { 78 | } 79 | 80 | public HttpDownBootstrap(HttpRequestInfo request, HttpResponseInfo response, HttpDownConfigInfo downConfig, ProxyConfig proxyConfig, TaskInfo taskInfo, 81 | HttpDownCallback callback, NioEventLoopGroup loopGroup) { 82 | this.request = request; 83 | this.response = response; 84 | this.downConfig = downConfig; 85 | this.proxyConfig = proxyConfig; 86 | this.taskInfo = taskInfo; 87 | this.callback = callback; 88 | this.loopGroup = loopGroup; 89 | } 90 | 91 | public HttpRequestInfo getRequest() { 92 | return request; 93 | } 94 | 95 | public HttpResponseInfo getResponse() { 96 | return response; 97 | } 98 | 99 | public HttpDownConfigInfo getDownConfig() { 100 | return downConfig; 101 | } 102 | 103 | public ProxyConfig getProxyConfig() { 104 | return proxyConfig; 105 | } 106 | 107 | public TaskInfo getTaskInfo() { 108 | return taskInfo; 109 | } 110 | 111 | public HttpDownCallback getCallback() { 112 | return callback; 113 | } 114 | 115 | public NioEventLoopGroup getLoopGroup() { 116 | return loopGroup; 117 | } 118 | 119 | public HttpDownBootstrap setRequest(HttpRequestInfo request) { 120 | this.request = request; 121 | return this; 122 | } 123 | 124 | public HttpDownBootstrap setResponse(HttpResponseInfo response) { 125 | this.response = response; 126 | return this; 127 | } 128 | 129 | public HttpDownBootstrap setDownConfig(HttpDownConfigInfo downConfig) { 130 | this.downConfig = downConfig; 131 | return this; 132 | } 133 | 134 | public HttpDownBootstrap setProxyConfig(ProxyConfig proxyConfig) { 135 | this.proxyConfig = proxyConfig; 136 | return this; 137 | } 138 | 139 | public HttpDownBootstrap setTaskInfo(TaskInfo taskInfo) { 140 | this.taskInfo = taskInfo; 141 | return this; 142 | } 143 | 144 | public HttpDownBootstrap setCallback(HttpDownCallback callback) { 145 | this.callback = callback; 146 | return this; 147 | } 148 | 149 | public HttpDownBootstrap setLoopGroup(NioEventLoopGroup loopGroup) { 150 | this.loopGroup = loopGroup; 151 | return this; 152 | } 153 | 154 | /** 155 | * 任务重新开始下载 156 | */ 157 | private void start(boolean isResume) { 158 | long startTime = 0; 159 | long lastPauseTime = 0; 160 | if (taskInfo != null && taskInfo.getStatus() == HttpDownStatus.WAIT) { 161 | startTime = taskInfo.getStartTime(); 162 | lastPauseTime = System.currentTimeMillis(); 163 | } 164 | taskInfo = new TaskInfo(); 165 | taskInfo.setStartTime(startTime); 166 | taskInfo.setLastPauseTime(lastPauseTime); 167 | if (downConfig.getFilePath() == null || "".equals(downConfig.getFilePath().trim())) { 168 | throw new BootstrapPathEmptyException("下载路径不能为空"); 169 | } 170 | String filePath = HttpDownUtil.getTaskFilePath(this); 171 | try { 172 | if (!FileUtil.exists(downConfig.getFilePath())) { 173 | try { 174 | Files.createDirectories(Paths.get(downConfig.getFilePath())); 175 | } catch (IOException e) { 176 | throw new BootstrapCreateDirException("创建目录失败,请重试", e); 177 | } 178 | } 179 | if (!FileUtil.canWrite(downConfig.getFilePath())) { 180 | throw new BootstrapNoPermissionException("无权访问下载路径,请修改路径或开放目录写入权限"); 181 | } 182 | //磁盘空间不足 183 | if (response.getTotalSize() > FileUtil.getDiskFreeSize(downConfig.getFilePath())) { 184 | throw new BootstrapNoSpaceException("磁盘空间不足,请修改路径"); 185 | } 186 | //有文件同名 187 | File downFile = new File(filePath); 188 | if (downFile.exists()) { 189 | //没有进度记录继续下载时,如果存在相同文件则删除重新下载 190 | if (isResume) { 191 | downFile.delete(); 192 | } else if (downConfig.isAutoRename()) { 193 | response.setFileName(FileUtil.renameIfExists(filePath)); 194 | filePath = HttpDownUtil.getTaskFilePath(this); 195 | } else { 196 | throw new BootstrapFileAlreadyExistsException("文件名已存在,请修改文件名"); 197 | } 198 | } 199 | } catch (BootstrapException e) { 200 | if (loopGroup != null) { 201 | loopGroup.shutdownGracefully(); 202 | loopGroup = null; 203 | } 204 | if (progressThread != null) { 205 | progressThread.close(); 206 | progressThread = null; 207 | } 208 | throw e; 209 | } 210 | try { 211 | //创建文件 212 | if (response.isSupportRange()) { 213 | String fileSystemType = FileUtil.getSystemFileType(filePath); 214 | if (OsUtil.isUnix() 215 | || "NTFS".equalsIgnoreCase(fileSystemType) 216 | || "UFS".equalsIgnoreCase(fileSystemType) 217 | || "APFS".equalsIgnoreCase(fileSystemType)) { 218 | FileUtil.createFileWithSparse(filePath, response.getTotalSize()); 219 | } else { 220 | FileUtil.createFileWithDefault(filePath, response.getTotalSize()); 221 | } 222 | } else { 223 | FileUtil.createFile(filePath); 224 | } 225 | } catch (IOException e) { 226 | throw new BootstrapException("创建文件失败,请重试", e); 227 | } 228 | buildChunkInfoList(); 229 | commonStart(); 230 | if (taskInfo.getStartTime() <= 0) { 231 | taskInfo.setStartTime(System.currentTimeMillis()); 232 | } 233 | //文件下载开始回调 234 | if (callback != null) { 235 | request.headers().remove(HttpHeaderNames.RANGE); 236 | callback.onStart(this); 237 | } 238 | for (int i = 0; i < taskInfo.getConnectInfoList().size(); i++) { 239 | ConnectInfo connectInfo = taskInfo.getConnectInfoList().get(i); 240 | connect(connectInfo); 241 | } 242 | } 243 | 244 | public void start() { 245 | start(false); 246 | } 247 | 248 | private void connect(ConnectInfo connectInfo) { 249 | RequestProto requestProto = request.requestProto(); 250 | LOGGER.debug("开始下载:" + connectInfo); 251 | Bootstrap bootstrap = new Bootstrap() 252 | .channel(NioSocketChannel.class) 253 | .group(loopGroup) 254 | .handler(new HttpDownInitializer(requestProto.getSsl(), connectInfo)); 255 | if (proxyConfig != null) { 256 | //代理服务器解析DNS和连接 257 | bootstrap.resolver(NoopAddressResolverGroup.INSTANCE); 258 | } 259 | ChannelFuture cf = bootstrap.connect(requestProto.getHost(), requestProto.getPort()); 260 | //重置最后下载时间 261 | connectInfo.setConnectChannel(cf.channel()); 262 | cf.addListener((ChannelFutureListener) future -> { 263 | if (future.isSuccess()) { 264 | LOGGER.debug("连接成功:" + connectInfo); 265 | if (response.isSupportRange()) { 266 | request.headers().set(HttpHeaderNames.RANGE, "bytes=" + connectInfo.getStartPosition() + "-" + connectInfo.getEndPosition()); 267 | } else { 268 | request.headers().remove(HttpHeaderNames.RANGE); 269 | } 270 | future.channel().writeAndFlush(request); 271 | if (request.content() != null) { 272 | //请求体写入 273 | HttpContent content = new DefaultLastHttpContent(); 274 | content.content().writeBytes(request.content()); 275 | future.channel().writeAndFlush(content); 276 | } 277 | } else { 278 | future.channel().close(); 279 | } 280 | }); 281 | } 282 | 283 | /** 284 | * 重新发起连接 285 | */ 286 | private void reConnect(ConnectInfo connectInfo) { 287 | reConnect(connectInfo, false); 288 | } 289 | 290 | /** 291 | * 重新发起连接 292 | * 293 | * @param connectInfo 连接相关信息 294 | * @param isHelp 是否为帮助其他分段下载发起的连接 295 | */ 296 | private void reConnect(ConnectInfo connectInfo, boolean isHelp) { 297 | if (loopGroup == null) { 298 | return; 299 | } 300 | if (!isHelp && response.isSupportRange()) { 301 | connectInfo.setStartPosition(connectInfo.getStartPosition() + connectInfo.getDownSize()); 302 | } 303 | connectInfo.setDownSize(0); 304 | if (connectInfo.getErrorCount() < downConfig.getRetryCount()) { 305 | connect(connectInfo); 306 | } else { 307 | if (callback != null) { 308 | callback.onChunkError(this, taskInfo.getChunkInfoList().get(connectInfo.getChunkIndex())); 309 | } 310 | if (taskInfo.getConnectInfoList().stream() 311 | .filter(connect -> connect.getStatus() != HttpDownStatus.DONE) 312 | .allMatch(connect -> connect.getErrorCount() >= downConfig.getRetryCount())) { 313 | taskInfo.setStatus(HttpDownStatus.ERROR); 314 | taskInfo.getChunkInfoList().stream() 315 | .filter(chunk -> chunk.getStatus() != HttpDownStatus.DONE) 316 | .forEach(chunkInfo -> chunkInfo.setStatus(HttpDownStatus.ERROR)); 317 | close(); 318 | if (callback != null) { 319 | callback.onError(this); 320 | } 321 | } 322 | } 323 | } 324 | 325 | /** 326 | * 暂停下载 327 | */ 328 | public void pause() { 329 | synchronized (taskInfo) { 330 | if (taskInfo.getStatus() == HttpDownStatus.PAUSE 331 | || taskInfo.getStatus() == HttpDownStatus.DONE) { 332 | return; 333 | } 334 | long time = System.currentTimeMillis(); 335 | taskInfo.setStatus(HttpDownStatus.PAUSE); 336 | taskInfo.setLastPauseTime(time); 337 | for (ChunkInfo chunkInfo : taskInfo.getChunkInfoList()) { 338 | if (chunkInfo.getStatus() != HttpDownStatus.DONE) { 339 | chunkInfo.setStatus(HttpDownStatus.PAUSE); 340 | chunkInfo.setLastPauseTime(time); 341 | } 342 | } 343 | close(); 344 | } 345 | if (callback != null) { 346 | callback.onPause(this); 347 | } 348 | } 349 | 350 | /** 351 | * 继续下载 352 | */ 353 | public void resume() { 354 | if (taskInfo == null || taskInfo.getChunkInfoList() == null) { 355 | //下载进度信息不存在 356 | start(true); 357 | return; 358 | } 359 | synchronized (taskInfo) { 360 | if (taskInfo.getStatus() == HttpDownStatus.RUNNING 361 | || taskInfo.getStatus() == HttpDownStatus.DONE) { 362 | return; 363 | } 364 | if (!FileUtil.exists(HttpDownUtil.getTaskFilePath(this))) { 365 | close(); 366 | start(); 367 | } else { 368 | if (taskInfo.getDownSize() >= response.getTotalSize()) { 369 | taskInfo.setStatus(HttpDownStatus.DONE); 370 | return; 371 | } 372 | long time = System.currentTimeMillis(); 373 | for (ChunkInfo chunkInfo : taskInfo.getChunkInfoList()) { 374 | if (chunkInfo.getStatus() == HttpDownStatus.PAUSE) { 375 | chunkInfo.setPauseTime(chunkInfo.getPauseTime() + (time - chunkInfo.getLastPauseTime())); 376 | } 377 | } 378 | commonStart(); 379 | for (ConnectInfo connectInfo : taskInfo.getConnectInfoList()) { 380 | if (connectInfo.getStatus() == HttpDownStatus.RUNNING) { 381 | reConnect(connectInfo); 382 | } 383 | } 384 | } 385 | } 386 | if (callback != null) { 387 | callback.onResume(this); 388 | } 389 | } 390 | 391 | private void done() { 392 | stopThreads(); 393 | if (callback != null) { 394 | callback.onProgress(this); 395 | callback.onDone(this); 396 | } 397 | } 398 | 399 | public void close() { 400 | synchronized (taskInfo) { 401 | if (taskInfo.getConnectInfoList() != null) { 402 | for (ConnectInfo connectInfo : taskInfo.getConnectInfoList()) { 403 | close(connectInfo); 404 | } 405 | } 406 | } 407 | stopThreads(); 408 | } 409 | 410 | private void close(ConnectInfo connectInfo) { 411 | try { 412 | HttpDownUtil.safeClose(connectInfo.getConnectChannel(), connectInfo.getFileChannel()); 413 | } catch (Exception e) { 414 | LOGGER.error("closeChunk error", e); 415 | } 416 | } 417 | 418 | private void stopThreads() { 419 | if (loopGroup != null) { 420 | loopGroup.shutdownGracefully(); 421 | loopGroup = null; 422 | } 423 | if (progressThread != null) { 424 | progressThread.close(); 425 | progressThread = null; 426 | } 427 | } 428 | 429 | private void commonStart() { 430 | taskInfo.setStatus(HttpDownStatus.RUNNING); 431 | taskInfo.setLastStartTime(0); 432 | taskInfo.setLastDownSize(taskInfo.getDownSize()); 433 | taskInfo.getChunkInfoList().forEach(chunkInfo -> { 434 | if (chunkInfo.getStatus() != HttpDownStatus.DONE) { 435 | chunkInfo.setStatus(HttpDownStatus.RUNNING); 436 | } 437 | }); 438 | taskInfo.getConnectInfoList().forEach(connectInfo -> { 439 | connectInfo.setErrorCount(0); 440 | if (connectInfo.getStatus() != HttpDownStatus.DONE) { 441 | connectInfo.setStatus(HttpDownStatus.RUNNING); 442 | } 443 | }); 444 | if (loopGroup == null) { 445 | loopGroup = new NioEventLoopGroup(1); 446 | } 447 | if (progressThread == null) { 448 | progressThread = new ProgressThread(); 449 | progressThread.start(); 450 | } 451 | } 452 | 453 | /** 454 | * 生成下载分段信息 455 | */ 456 | public void buildChunkInfoList() { 457 | List chunkInfoList = new ArrayList<>(); 458 | List connectInfoList = new ArrayList<>(); 459 | if (response.getTotalSize() > 0) { //非chunked编码 460 | //如果文件太小,连接数过高,则调整连接数量 461 | if (response.getTotalSize() < downConfig.getConnections()) { 462 | downConfig.setConnections((int) response.getTotalSize()); 463 | } 464 | //计算chunk列表 465 | long chunkSize = response.getTotalSize() / downConfig.getConnections(); 466 | for (int i = 0; i < downConfig.getConnections(); i++) { 467 | ChunkInfo chunkInfo = new ChunkInfo(); 468 | ConnectInfo connectInfo = new ConnectInfo(); 469 | chunkInfo.setIndex(i); 470 | long start = i * chunkSize; 471 | if (i == downConfig.getConnections() - 1) { //最后一个连接去下载多出来的字节 472 | chunkSize += response.getTotalSize() % downConfig.getConnections(); 473 | } 474 | long end = start + chunkSize - 1; 475 | chunkInfo.setTotalSize(chunkSize); 476 | if (taskInfo.getLastPauseTime() > 0) { 477 | chunkInfo.setLastPauseTime(taskInfo.getLastPauseTime()); 478 | chunkInfo.setPauseTime(taskInfo.getLastPauseTime() - taskInfo.getStartTime()); 479 | } 480 | chunkInfoList.add(chunkInfo); 481 | 482 | connectInfo.setChunkIndex(i); 483 | connectInfo.setStartPosition(start); 484 | connectInfo.setEndPosition(end); 485 | connectInfoList.add(connectInfo); 486 | } 487 | } else { //chunked下载 488 | ChunkInfo chunkInfo = new ChunkInfo(); 489 | ConnectInfo connectInfo = new ConnectInfo(); 490 | connectInfo.setChunkIndex(0); 491 | chunkInfo.setIndex(0); 492 | if (taskInfo.getLastPauseTime() > 0) { 493 | chunkInfo.setLastPauseTime(taskInfo.getLastPauseTime()); 494 | chunkInfo.setPauseTime(taskInfo.getLastPauseTime() - taskInfo.getStartTime()); 495 | } 496 | chunkInfoList.add(chunkInfo); 497 | connectInfoList.add(connectInfo); 498 | } 499 | taskInfo.setChunkInfoList(chunkInfoList); 500 | taskInfo.setConnectInfoList(connectInfoList); 501 | } 502 | 503 | @Override 504 | public String toString() { 505 | return "HttpDownBootstrap{" + 506 | "request=" + request + 507 | ", downConfig=" + downConfig + 508 | ", proxyConfig=" + proxyConfig + 509 | ", taskInfo=" + taskInfo + 510 | ", callback=" + (callback != null ? callback.getClass() : callback) + 511 | '}'; 512 | } 513 | 514 | public static HttpDownBootstrapBuilder builder() { 515 | return new HttpDownBootstrapBuilder(); 516 | } 517 | 518 | public static URLHttpDownBootstrapBuilder builder(String method, String url, Map heads, String body) { 519 | return new URLHttpDownBootstrapBuilder(method, url, heads, body); 520 | } 521 | 522 | public static URLHttpDownBootstrapBuilder builder(String url, Map heads, String body) { 523 | return new URLHttpDownBootstrapBuilder(url, heads, body); 524 | } 525 | 526 | public static URLHttpDownBootstrapBuilder builder(String url, Map heads) { 527 | return builder(url, heads, null); 528 | } 529 | 530 | public static URLHttpDownBootstrapBuilder builder(String url, String body) { 531 | return builder(url, null, body); 532 | } 533 | 534 | public static URLHttpDownBootstrapBuilder builder(String url) { 535 | return builder(url, null, null); 536 | } 537 | 538 | class ProgressThread extends Thread { 539 | 540 | private volatile boolean run = true; 541 | private int period = 1; 542 | 543 | @Override 544 | public void run() { 545 | while (run) { 546 | if (taskInfo.getStatus() != HttpDownStatus.DONE) { 547 | for (ChunkInfo chunkInfo : taskInfo.getChunkInfoList()) { 548 | synchronized (chunkInfo) { 549 | if (chunkInfo.getStatus() != HttpDownStatus.DONE) { 550 | chunkInfo.setSpeed((chunkInfo.getDownSize() - chunkInfo.getLastCountSize()) / period); 551 | chunkInfo.setLastCountSize(chunkInfo.getDownSize()); 552 | } 553 | } 554 | } 555 | //计算瞬时速度 556 | taskInfo.setSpeed(taskInfo.getChunkInfoList() 557 | .stream() 558 | .filter(chunkInfo -> chunkInfo.getStatus() != HttpDownStatus.DONE) 559 | .mapToLong(ChunkInfo::getSpeed) 560 | .sum()); 561 | callback.onProgress(HttpDownBootstrap.this); 562 | } 563 | try { 564 | TimeUnit.SECONDS.sleep(period); 565 | } catch (InterruptedException e) { 566 | e.printStackTrace(); 567 | } 568 | } 569 | } 570 | 571 | public void close() { 572 | run = false; 573 | } 574 | 575 | } 576 | 577 | class HttpDownInitializer extends ChannelInitializer { 578 | 579 | private boolean isSsl; 580 | private ConnectInfo connectInfo; 581 | 582 | public HttpDownInitializer(boolean isSsl, ConnectInfo connectInfo) { 583 | this.isSsl = isSsl; 584 | this.connectInfo = connectInfo; 585 | } 586 | 587 | @Override 588 | protected void initChannel(Channel ch) throws Exception { 589 | if (proxyConfig != null) { 590 | ch.pipeline().addLast(ProxyHandleFactory.build(proxyConfig)); 591 | } 592 | if (isSsl) { 593 | RequestProto requestProto = request.requestProto(); 594 | ch.pipeline().addLast(HttpDownUtil.getSslContext().newHandler(ch.alloc(), requestProto.getHost(), requestProto.getPort())); 595 | } 596 | ch.pipeline().addLast("downTimeout", new DownTimeoutHandler(downConfig.getTimeout())); 597 | ch.pipeline().addLast("httpCodec", new HttpClientCodec()); 598 | ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { 599 | 600 | private ChunkInfo chunkInfo = taskInfo.getChunkInfoList().get(connectInfo.getChunkIndex()); 601 | private SeekableByteChannel fileChannel; 602 | private boolean normalClose; 603 | private boolean isSuccess; 604 | 605 | @Override 606 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 607 | try { 608 | if (msg instanceof HttpContent) { 609 | synchronized (taskInfo) { 610 | if (!isSuccess || !fileChannel.isOpen() || !ctx.channel().isOpen()) { 611 | return; 612 | } 613 | HttpContent httpContent = (HttpContent) msg; 614 | ByteBuf byteBuf = httpContent.content(); 615 | int size = byteBuf.readableBytes(); 616 | //判断是否超出下载字节范围 617 | if (response.isSupportRange() && connectInfo.getDownSize() + size > connectInfo.getTotalSize()) { 618 | size = (int) (connectInfo.getTotalSize() - connectInfo.getDownSize()); 619 | } 620 | fileChannel.write(byteBuf.nioBuffer()); 621 | synchronized (chunkInfo) { 622 | chunkInfo.setDownSize(chunkInfo.getDownSize() + size); 623 | taskInfo.setDownSize(taskInfo.getDownSize() + size); 624 | connectInfo.setDownSize(connectInfo.getDownSize() + size); 625 | } 626 | if (downConfig.getSpeedLimit() > 0) { 627 | long time = System.nanoTime(); 628 | if (taskInfo.getLastStartTime() <= 0) { 629 | taskInfo.setLastStartTime(time); 630 | } else { 631 | //计算下载速度是否超过速度限制 632 | long useTime = time - taskInfo.getLastStartTime(); 633 | double speed = (taskInfo.getDownSize() - taskInfo.getLastDownSize()) / (double) useTime; 634 | double limitSpeed = downConfig.getSpeedLimit() / TIME_1S_NANO; 635 | long sleepTime = (long) ((speed - limitSpeed) * useTime); 636 | if (sleepTime > 0) { 637 | TimeUnit.NANOSECONDS.sleep(sleepTime); 638 | //刷新其他连接的lastReadTime 639 | for (ConnectInfo ci : taskInfo.getConnectInfoList()) { 640 | if (ci.getConnectChannel() != null && ci.getConnectChannel() != ch) { 641 | DownTimeoutHandler downTimeout = (DownTimeoutHandler) ci.getConnectChannel().pipeline().get("downTimeout"); 642 | if (downTimeout != null) { 643 | downTimeout.updateLastReadTime(sleepTime); 644 | } 645 | } 646 | } 647 | } 648 | } 649 | } 650 | if (!response.isSupportRange() && !(httpContent instanceof LastHttpContent)) { 651 | return; 652 | } 653 | long time = System.currentTimeMillis(); 654 | if (connectInfo.getDownSize() >= connectInfo.getTotalSize()) { 655 | LOGGER.debug("连接下载完成:" + connectInfo + "\t" + chunkInfo); 656 | normalClose = true; 657 | close(connectInfo); 658 | //判断分段是否下载完成 659 | if (isChunkDownDone(httpContent)) { 660 | LOGGER.debug("分段下载完成:" + connectInfo + "\t" + chunkInfo); 661 | if (!response.isSupportRange()) { //chunked编码最后更新文件大小 662 | chunkInfo.setTotalSize(taskInfo.getDownSize()); 663 | response.setTotalSize(taskInfo.getDownSize()); 664 | } 665 | synchronized (chunkInfo) { 666 | chunkInfo.setStatus(HttpDownStatus.DONE); 667 | //计算最后平均下载速度 668 | chunkInfo.setSpeed(calcSpeed(chunkInfo.getTotalSize(), time - taskInfo.getStartTime() - chunkInfo.getPauseTime())); 669 | } 670 | //分段下载完成回调 671 | if (callback != null) { 672 | callback.onChunkDone(HttpDownBootstrap.this, chunkInfo); 673 | } 674 | //所有分段都下载完成 675 | if (taskInfo.getChunkInfoList().stream().allMatch(chunk -> chunk.getStatus() == HttpDownStatus.DONE)) { 676 | //文件下载完成回调 677 | LOGGER.debug("任务下载完成:" + chunkInfo); 678 | connectInfo.setStatus(HttpDownStatus.DONE); 679 | taskInfo.setStatus(HttpDownStatus.DONE); 680 | //计算平均速度(所有分段的平均速度之和) 681 | taskInfo.setSpeed(taskInfo.getChunkInfoList() 682 | .stream() 683 | .mapToLong(chunk -> chunk.getSpeed()) 684 | .sum()); 685 | done(); 686 | return; 687 | } 688 | } 689 | //判断是否要去支持其他分段,找一个下载最慢的分段 690 | ChunkInfo supportChunk = taskInfo.getChunkInfoList() 691 | .stream() 692 | .filter(chunk -> chunk.getIndex() != connectInfo.getChunkIndex() && chunk.getStatus() != HttpDownStatus.DONE) 693 | .min(Comparator.comparingLong(ChunkInfo::getDownSize)) 694 | .orElse(null); 695 | if (supportChunk == null) { 696 | connectInfo.setStatus(HttpDownStatus.DONE); 697 | return; 698 | } 699 | ConnectInfo maxConnect = taskInfo.getConnectInfoList() 700 | .stream() 701 | .filter(connect -> connect.getChunkIndex() == supportChunk.getIndex() && connect.getTotalSize() - connect.getDownSize() >= SIZE_1MB) 702 | .max((c1, c2) -> (int) (c1.getStartPosition() - c2.getStartPosition())) 703 | .orElse(null); 704 | if (maxConnect == null) { 705 | connectInfo.setStatus(HttpDownStatus.DONE); 706 | return; 707 | } 708 | //把这个分段最后一个下载连接分成两个 709 | long remainingSize = maxConnect.getTotalSize() - maxConnect.getDownSize(); 710 | long endTemp = maxConnect.getEndPosition(); 711 | maxConnect.setEndPosition(maxConnect.getEndPosition() - (remainingSize / 2)); 712 | //给当前连接重新分配下载区间 713 | connectInfo.setStartPosition(maxConnect.getEndPosition() + 1); 714 | connectInfo.setEndPosition(endTemp); 715 | connectInfo.setChunkIndex(supportChunk.getIndex()); 716 | LOGGER.debug("支持下载:" + connectInfo + "\t" + chunkInfo); 717 | reConnect(connectInfo, true); 718 | } 719 | } 720 | } else { 721 | HttpResponse httpResponse = (HttpResponse) msg; 722 | Integer responseCode = httpResponse.status().code(); 723 | if (responseCode < 200 || responseCode >= 300) { 724 | LOGGER.warn("响应状态码异常:" + responseCode + "\t" + connectInfo); 725 | //处理重定向链接 726 | if (responseCode >= 300 && responseCode < 400) { 727 | String location = httpResponse.headers().get(HttpHeaderNames.LOCATION); 728 | if (!StringUtil.isNullOrEmpty(location)) { 729 | if (location.matches("^(?i)(https?).*")) { 730 | URL url = new URL(location); 731 | request.setUri(url.getFile()); 732 | request.headers().set(HttpHeaderNames.HOST, url.getHost()); 733 | request.setRequestProto(HttpDownUtil.parseRequestProto(url)); 734 | } else { 735 | request.setUri(location); 736 | } 737 | } 738 | } else { 739 | connectInfo.setErrorCount(connectInfo.getErrorCount() + 1); 740 | } 741 | normalClose = true; 742 | close(connectInfo); 743 | ch.eventLoop().schedule(() -> reConnect(connectInfo), 5, TimeUnit.SECONDS); 744 | return; 745 | } 746 | LOGGER.debug("下载响应:" + connectInfo); 747 | fileChannel = Files.newByteChannel(Paths.get(HttpDownUtil.getTaskFilePath(HttpDownBootstrap.this)), StandardOpenOption.WRITE); 748 | if (response.isSupportRange()) { 749 | fileChannel.position(connectInfo.getStartPosition() + connectInfo.getDownSize()); 750 | } 751 | connectInfo.setFileChannel(fileChannel); 752 | isSuccess = true; 753 | } 754 | } catch (Exception e) { 755 | throw e; 756 | } finally { 757 | ReferenceCountUtil.release(msg); 758 | } 759 | } 760 | 761 | private boolean isChunkDownDone(HttpContent httpContent) { 762 | if (response.isSupportRange()) { 763 | if (chunkInfo.getDownSize() >= chunkInfo.getTotalSize()) { 764 | return true; 765 | } 766 | } else if (httpContent instanceof LastHttpContent) { 767 | return true; 768 | } 769 | return false; 770 | } 771 | 772 | /** 773 | * 计算下载速度(B/S) 774 | * @param downSize 下载的字节数 775 | * @param downTime 下载耗时 776 | * @return 777 | */ 778 | private long calcSpeed(long downSize, long downTime) { 779 | return (long) (downSize / (downTime / 1000D)); 780 | } 781 | 782 | @Override 783 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 784 | if (!(cause instanceof IOException) && !(cause instanceof ReadTimeoutException)) { 785 | LOGGER.error("down onChunkError:", cause); 786 | } 787 | normalClose = true; 788 | close(connectInfo); 789 | reConnect(connectInfo); 790 | } 791 | 792 | @Override 793 | public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { 794 | super.channelUnregistered(ctx); 795 | //还未下载完成,继续下载 796 | if (!normalClose 797 | && chunkInfo.getStatus() != HttpDownStatus.PAUSE 798 | && chunkInfo.getStatus() != HttpDownStatus.ERROR) { 799 | LOGGER.debug("channelUnregistered:" + connectInfo + "\t" + chunkInfo); 800 | close(connectInfo); 801 | reConnect(connectInfo); 802 | } 803 | } 804 | }); 805 | } 806 | 807 | @Override 808 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 809 | super.exceptionCaught(ctx, cause); 810 | LOGGER.error("down onInit:", cause); 811 | } 812 | } 813 | } 814 | -------------------------------------------------------------------------------- /src/main/java/org/pdown/core/boot/HttpDownBootstrapBuilder.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.boot; 2 | 3 | import io.netty.channel.nio.NioEventLoopGroup; 4 | import io.netty.util.internal.StringUtil; 5 | import java.nio.file.Paths; 6 | import org.pdown.core.dispatch.HttpDownCallback; 7 | import org.pdown.core.entity.HttpDownConfigInfo; 8 | import org.pdown.core.entity.HttpRequestInfo; 9 | import org.pdown.core.entity.HttpResponseInfo; 10 | import org.pdown.core.entity.TaskInfo; 11 | import org.pdown.core.exception.BootstrapBuildException; 12 | import org.pdown.core.proxy.ProxyConfig; 13 | 14 | /** 15 | * 用于创建一个HTTP下载器 16 | */ 17 | public class HttpDownBootstrapBuilder { 18 | 19 | private HttpRequestInfo request; 20 | private HttpResponseInfo response; 21 | private HttpDownConfigInfo downConfig; 22 | private ProxyConfig proxyConfig; 23 | private TaskInfo taskInfo; 24 | private NioEventLoopGroup loopGroup; 25 | private HttpDownCallback callback; 26 | 27 | /** 28 | * 任务http请求信息 默认值:null 29 | */ 30 | public HttpDownBootstrapBuilder request(HttpRequestInfo request) { 31 | this.request = request; 32 | return this; 33 | } 34 | 35 | /** 36 | * 任务http响应信息 默认值:null 37 | */ 38 | public HttpDownBootstrapBuilder response(HttpResponseInfo response) { 39 | this.response = response; 40 | return this; 41 | } 42 | 43 | /** 44 | * 任务配置信息 默认:new HttpDownConfigInfo() 45 | */ 46 | public HttpDownBootstrapBuilder downConfig(HttpDownConfigInfo downConfig) { 47 | this.downConfig = downConfig; 48 | return this; 49 | } 50 | 51 | /** 52 | * 任务二级代理信息 默认:null 53 | */ 54 | public HttpDownBootstrapBuilder proxyConfig(ProxyConfig proxyConfig) { 55 | this.proxyConfig = proxyConfig; 56 | return this; 57 | } 58 | 59 | /** 60 | * 任务下载记录 默认值:null 61 | */ 62 | public HttpDownBootstrapBuilder taskInfo(TaskInfo taskInfo) { 63 | this.taskInfo = taskInfo; 64 | return this; 65 | } 66 | 67 | /** 68 | * 线程池设置 默认值:new NioEventLoopGroup(1) 69 | */ 70 | public HttpDownBootstrapBuilder loopGroup(NioEventLoopGroup loopGroup) { 71 | this.loopGroup = loopGroup; 72 | return this; 73 | } 74 | 75 | /** 76 | * 下载回调类 77 | */ 78 | public HttpDownBootstrapBuilder callback(HttpDownCallback callback) { 79 | this.callback = callback; 80 | return this; 81 | } 82 | 83 | protected HttpRequestInfo getRequest() { 84 | return request; 85 | } 86 | 87 | protected HttpResponseInfo getResponse() { 88 | return response; 89 | } 90 | 91 | protected HttpDownConfigInfo getDownConfig() { 92 | return downConfig; 93 | } 94 | 95 | protected ProxyConfig getProxyConfig() { 96 | return proxyConfig; 97 | } 98 | 99 | protected TaskInfo getTaskInfo() { 100 | return taskInfo; 101 | } 102 | 103 | protected NioEventLoopGroup getLoopGroup() { 104 | return loopGroup; 105 | } 106 | 107 | protected HttpDownCallback getCallback() { 108 | return callback; 109 | } 110 | 111 | public HttpDownBootstrap build() { 112 | try { 113 | if (request == null) { 114 | throw new BootstrapBuildException("request Can not be empty."); 115 | } 116 | if (response == null) { 117 | throw new BootstrapBuildException("response Can not be empty."); 118 | } 119 | if (loopGroup == null) { 120 | loopGroup = new NioEventLoopGroup(1); 121 | } 122 | if (downConfig == null) { 123 | downConfig = new HttpDownConfigInfo(); 124 | } 125 | if (StringUtil.isNullOrEmpty(downConfig.getFilePath())) { 126 | downConfig.setFilePath(System.getProperty("user.dir")); 127 | } else { 128 | downConfig.setFilePath(Paths.get(downConfig.getFilePath()).toFile().getPath()); 129 | } 130 | if (StringUtil.isNullOrEmpty(response.getFileName())) { 131 | response.setFileName("Unknown"); 132 | } 133 | if (downConfig.getTimeout() <= 0) { 134 | downConfig.setTimeout(30); 135 | } 136 | if (downConfig.getRetryCount() <= 0) { 137 | downConfig.setRetryCount(5); 138 | } 139 | if (!response.isSupportRange()) { 140 | downConfig.setConnections(1); 141 | } else if (downConfig.getConnections() <= 0) { 142 | downConfig.setConnections(16); 143 | } 144 | return new HttpDownBootstrap(request, response, downConfig, proxyConfig, taskInfo, callback, loopGroup); 145 | } catch (Exception e) { 146 | throw new BootstrapBuildException("build HttpDownBootstrap error", e); 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/main/java/org/pdown/core/boot/URLHttpDownBootstrapBuilder.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.boot; 2 | 3 | import io.netty.channel.nio.NioEventLoopGroup; 4 | import io.netty.util.internal.StringUtil; 5 | import org.pdown.core.entity.HttpRequestInfo; 6 | import org.pdown.core.entity.HttpResponseInfo; 7 | import org.pdown.core.exception.BootstrapBuildException; 8 | import org.pdown.core.util.HttpDownUtil; 9 | import java.util.Map; 10 | 11 | /** 12 | * 通过URL创建一个下载任务 13 | */ 14 | public class URLHttpDownBootstrapBuilder extends HttpDownBootstrapBuilder { 15 | 16 | private String method; 17 | private String url; 18 | private Map heads; 19 | private String body; 20 | 21 | public URLHttpDownBootstrapBuilder(String method, String url, Map heads, String body) { 22 | this.method = method; 23 | this.url = url; 24 | this.heads = heads; 25 | this.body = body; 26 | } 27 | 28 | public URLHttpDownBootstrapBuilder(String url, Map heads, String body) { 29 | this(null, url, heads, body); 30 | } 31 | 32 | public URLHttpDownBootstrapBuilder method(String method) { 33 | this.method = method; 34 | return this; 35 | } 36 | 37 | public URLHttpDownBootstrapBuilder url(String url) { 38 | this.url = url; 39 | return this; 40 | } 41 | 42 | public URLHttpDownBootstrapBuilder heads(Map heads) { 43 | this.heads = heads; 44 | return this; 45 | } 46 | 47 | public URLHttpDownBootstrapBuilder body(String body) { 48 | this.body = body; 49 | return this; 50 | } 51 | 52 | @Override 53 | public HttpDownBootstrap build() { 54 | try { 55 | HttpRequestInfo request = HttpDownUtil.buildRequest(method, url, heads, body); 56 | request(request); 57 | if (getLoopGroup() == null) { 58 | loopGroup(new NioEventLoopGroup(1)); 59 | } 60 | HttpResponseInfo response = HttpDownUtil.getHttpResponseInfo(request, null, getProxyConfig(), getLoopGroup()); 61 | if (getResponse() == null) { 62 | response(response); 63 | } else { 64 | if (StringUtil.isNullOrEmpty(getResponse().getFileName())) { 65 | getResponse().setFileName(response.getFileName()); 66 | } 67 | getResponse().setSupportRange(response.isSupportRange()); 68 | getResponse().setTotalSize(response.getTotalSize()); 69 | } 70 | } catch (Exception e) { 71 | if (getLoopGroup() != null) { 72 | getLoopGroup().shutdownGracefully(); 73 | } 74 | throw new BootstrapBuildException("build URLHttpDownBootstrap error", e); 75 | } 76 | return super.build(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/org/pdown/core/constant/HttpDownStatus.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.constant; 2 | 3 | public class HttpDownStatus { 4 | 5 | /** 6 | * 等待下载 7 | */ 8 | public final static int WAIT = 0; 9 | /** 10 | * 下载中 11 | */ 12 | public final static int RUNNING = 1; 13 | /** 14 | * 暂停下载 15 | */ 16 | public final static int PAUSE = 2; 17 | /** 18 | * 下载失败 19 | */ 20 | public final static int ERROR = 3; 21 | /** 22 | * 下载完成 23 | */ 24 | public final static int DONE = 4; 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/org/pdown/core/dispatch/ConsoleHttpDownCallback.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.dispatch; 2 | 3 | import org.pdown.core.boot.HttpDownBootstrap; 4 | import org.pdown.core.util.ByteUtil; 5 | 6 | public class ConsoleHttpDownCallback extends HttpDownCallback { 7 | 8 | private int progressWidth = 20; 9 | 10 | public ConsoleHttpDownCallback() { 11 | } 12 | 13 | public ConsoleHttpDownCallback(int progressWidth) { 14 | this.progressWidth = progressWidth; 15 | } 16 | 17 | @Override 18 | public void onStart(HttpDownBootstrap httpDownBootstrap) { 19 | System.out.println(httpDownBootstrap.getRequest().toString()); 20 | System.out.println(); 21 | } 22 | 23 | @Override 24 | public void onProgress(HttpDownBootstrap httpDownBootstrap) { 25 | double rate = 0; 26 | if (httpDownBootstrap.getResponse().getTotalSize() > 0) { 27 | rate = httpDownBootstrap.getTaskInfo().getDownSize() / (double) httpDownBootstrap.getResponse().getTotalSize(); 28 | } 29 | printProgress(rate, httpDownBootstrap.getTaskInfo().getSpeed()); 30 | } 31 | 32 | @Override 33 | public void onError(HttpDownBootstrap httpDownBootstrap) { 34 | System.out.println("\n\nDownload error"); 35 | } 36 | 37 | @Override 38 | public void onDone(HttpDownBootstrap httpDownBootstrap) { 39 | System.out.println("\n\nDownload completed"); 40 | } 41 | 42 | protected void printProgress(double rate, long speed) { 43 | int downWidth = (int) (progressWidth * rate); 44 | StringBuilder sb = new StringBuilder(); 45 | sb.append("\r["); 46 | for (int i = 0; i < progressWidth; i++) { 47 | if (i < downWidth) { 48 | sb.append("■"); 49 | } else { 50 | sb.append("□"); 51 | } 52 | } 53 | sb.append("] " + String.format("%.2f", rate * 100) + "% " + String.format("%8s", ByteUtil.byteFormat(speed)) + "/S"); 54 | System.out.print(sb.toString()); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/org/pdown/core/dispatch/HttpDownCallback.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.dispatch; 2 | 3 | import org.pdown.core.boot.HttpDownBootstrap; 4 | import org.pdown.core.entity.ChunkInfo; 5 | 6 | public class HttpDownCallback { 7 | 8 | public void onStart(HttpDownBootstrap httpDownBootstrap) { 9 | } 10 | 11 | public void onProgress(HttpDownBootstrap httpDownBootstrap) { 12 | } 13 | 14 | public void onPause(HttpDownBootstrap httpDownBootstrap) { 15 | } 16 | 17 | public void onResume(HttpDownBootstrap httpDownBootstrap) { 18 | } 19 | 20 | public void onChunkError(HttpDownBootstrap httpDownBootstrap, ChunkInfo chunkInfo) { 21 | } 22 | 23 | public void onError(HttpDownBootstrap httpDownBootstrap) { 24 | } 25 | 26 | public void onChunkDone(HttpDownBootstrap httpDownBootstrap, ChunkInfo chunkInfo) { 27 | } 28 | 29 | public void onDone(HttpDownBootstrap httpDownBootstrap) { 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/org/pdown/core/entity/ChunkInfo.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.entity; 2 | 3 | import java.io.Serializable; 4 | 5 | public class ChunkInfo implements Serializable { 6 | 7 | private static final long serialVersionUID = 231649750985696846L; 8 | private int index; 9 | private long downSize; 10 | private long totalSize; 11 | private long lastCountSize; 12 | private long lastPauseTime; //最后一次暂停时间 13 | private long pauseTime; //总共暂停的时间 14 | private int status; 15 | private long speed; 16 | 17 | public int getIndex() { 18 | return index; 19 | } 20 | 21 | public void setIndex(int index) { 22 | this.index = index; 23 | } 24 | 25 | public long getDownSize() { 26 | return downSize; 27 | } 28 | 29 | public void setDownSize(long downSize) { 30 | this.downSize = downSize; 31 | } 32 | 33 | public long getTotalSize() { 34 | return totalSize; 35 | } 36 | 37 | public void setTotalSize(long totalSize) { 38 | this.totalSize = totalSize; 39 | } 40 | 41 | public long getLastCountSize() { 42 | return lastCountSize; 43 | } 44 | 45 | public void setLastCountSize(long lastCountSize) { 46 | this.lastCountSize = lastCountSize; 47 | } 48 | 49 | public long getLastPauseTime() { 50 | return lastPauseTime; 51 | } 52 | 53 | public void setLastPauseTime(long lastPauseTime) { 54 | this.lastPauseTime = lastPauseTime; 55 | } 56 | 57 | public long getPauseTime() { 58 | return pauseTime; 59 | } 60 | 61 | public void setPauseTime(long pauseTime) { 62 | this.pauseTime = pauseTime; 63 | } 64 | 65 | public int getStatus() { 66 | return status; 67 | } 68 | 69 | public void setStatus(int status) { 70 | this.status = status; 71 | } 72 | 73 | public long getSpeed() { 74 | return speed; 75 | } 76 | 77 | public void setSpeed(long speed) { 78 | this.speed = speed; 79 | } 80 | 81 | @Override 82 | public String toString() { 83 | return "ChunkInfo{" + 84 | "index=" + index + 85 | ", downSize=" + downSize + 86 | ", totalSize=" + totalSize + 87 | ", lastCountSize=" + lastCountSize + 88 | ", lastPauseTime=" + lastPauseTime + 89 | ", pauseTime=" + pauseTime + 90 | ", status=" + status + 91 | ", speed=" + speed + 92 | '}'; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/org/pdown/core/entity/ConnectInfo.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.entity; 2 | 3 | import io.netty.channel.Channel; 4 | import java.io.Serializable; 5 | import java.nio.channels.SeekableByteChannel; 6 | 7 | public class ConnectInfo implements Serializable { 8 | 9 | private static final long serialVersionUID = 231649750985691346L; 10 | private long startPosition; 11 | private long endPosition; 12 | private long downSize; 13 | private int errorCount; 14 | private int chunkIndex; 15 | private int status; 16 | 17 | 18 | private transient Channel connectChannel; 19 | private transient SeekableByteChannel fileChannel; 20 | 21 | public long getStartPosition() { 22 | return startPosition; 23 | } 24 | 25 | public void setStartPosition(long startPosition) { 26 | this.startPosition = startPosition; 27 | } 28 | 29 | public long getEndPosition() { 30 | return endPosition; 31 | } 32 | 33 | public void setEndPosition(long endPosition) { 34 | this.endPosition = endPosition; 35 | } 36 | 37 | public long getDownSize() { 38 | return downSize; 39 | } 40 | 41 | public void setDownSize(long downSize) { 42 | this.downSize = downSize; 43 | } 44 | 45 | public int getErrorCount() { 46 | return errorCount; 47 | } 48 | 49 | public void setErrorCount(int errorCount) { 50 | this.errorCount = errorCount; 51 | } 52 | 53 | public int getChunkIndex() { 54 | return chunkIndex; 55 | } 56 | 57 | public void setChunkIndex(int chunkIndex) { 58 | this.chunkIndex = chunkIndex; 59 | } 60 | 61 | public int getStatus() { 62 | return status; 63 | } 64 | 65 | public void setStatus(int status) { 66 | this.status = status; 67 | } 68 | 69 | public Channel getConnectChannel() { 70 | return connectChannel; 71 | } 72 | 73 | public void setConnectChannel(Channel connectChannel) { 74 | this.connectChannel = connectChannel; 75 | } 76 | 77 | public SeekableByteChannel getFileChannel() { 78 | return fileChannel; 79 | } 80 | 81 | public void setFileChannel(SeekableByteChannel fileChannel) { 82 | this.fileChannel = fileChannel; 83 | } 84 | 85 | public long getTotalSize() { 86 | return endPosition - startPosition + 1; 87 | } 88 | 89 | @Override 90 | public String toString() { 91 | return "ConnectInfo{" + 92 | "startPosition=" + startPosition + 93 | ", endPosition=" + endPosition + 94 | ", downSize=" + downSize + 95 | ", errorCount=" + errorCount + 96 | ", chunkIndex=" + chunkIndex + 97 | ", status=" + status + 98 | ", connectChannel=" + connectChannel + 99 | ", fileChannel=" + fileChannel + 100 | '}'; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/org/pdown/core/entity/HttpDownConfigInfo.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.entity; 2 | 3 | import java.io.Serializable; 4 | 5 | public class HttpDownConfigInfo implements Serializable { 6 | 7 | private static final long serialVersionUID = -69564667309038578L; 8 | private String filePath; 9 | private int connections; 10 | private int timeout; 11 | private int retryCount; 12 | private boolean autoRename; 13 | private long speedLimit; 14 | 15 | public String getFilePath() { 16 | return filePath; 17 | } 18 | 19 | public HttpDownConfigInfo setFilePath(String filePath) { 20 | this.filePath = filePath; 21 | return this; 22 | } 23 | 24 | public int getConnections() { 25 | return connections; 26 | } 27 | 28 | public HttpDownConfigInfo setConnections(int connections) { 29 | this.connections = connections; 30 | return this; 31 | } 32 | 33 | public int getTimeout() { 34 | return timeout; 35 | } 36 | 37 | public HttpDownConfigInfo setTimeout(int timeout) { 38 | this.timeout = timeout; 39 | return this; 40 | } 41 | 42 | public int getRetryCount() { 43 | return retryCount; 44 | } 45 | 46 | public HttpDownConfigInfo setRetryCount(int retryCount) { 47 | this.retryCount = retryCount; 48 | return this; 49 | } 50 | 51 | public boolean isAutoRename() { 52 | return autoRename; 53 | } 54 | 55 | public HttpDownConfigInfo setAutoRename(boolean autoRename) { 56 | this.autoRename = autoRename; 57 | return this; 58 | } 59 | 60 | public long getSpeedLimit() { 61 | return speedLimit; 62 | } 63 | 64 | public HttpDownConfigInfo setSpeedLimit(long speedLimit) { 65 | this.speedLimit = speedLimit; 66 | return this; 67 | } 68 | 69 | @Override 70 | public String toString() { 71 | return "HttpDownConfigInfo{" + 72 | "filePath='" + filePath + '\'' + 73 | ", connections=" + connections + 74 | ", timeout=" + timeout + 75 | ", retryCount=" + retryCount + 76 | ", autoRename=" + autoRename + 77 | ", speedLimit=" + speedLimit + 78 | '}'; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/org/pdown/core/entity/HttpHeadsInfo.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.entity; 2 | 3 | import io.netty.handler.codec.http.HttpHeaders; 4 | import java.io.Serializable; 5 | import java.util.ArrayList; 6 | import java.util.Arrays; 7 | import java.util.HashMap; 8 | import java.util.Iterator; 9 | import java.util.LinkedHashMap; 10 | import java.util.List; 11 | import java.util.Map; 12 | import java.util.Map.Entry; 13 | import java.util.Optional; 14 | import java.util.Set; 15 | 16 | public class HttpHeadsInfo extends HttpHeaders implements Serializable { 17 | 18 | private static final long serialVersionUID = 9051999778842339663L; 19 | private Map map; 20 | 21 | public HttpHeadsInfo() { 22 | this.map = new LinkedHashMap<>(); 23 | } 24 | 25 | @Override 26 | public String get(String name) { 27 | Optional optional = map.keySet().stream().filter(k -> k.equalsIgnoreCase(name)) 28 | .findAny(); 29 | if (optional.isPresent()) { 30 | return map.get(optional.get()); 31 | } 32 | return null; 33 | } 34 | 35 | @Override 36 | public Integer getInt(CharSequence name) { 37 | String value = get(name); 38 | return value == null ? null : Integer.valueOf(get(name)); 39 | } 40 | 41 | @Override 42 | public int getInt(CharSequence name, int defaultValue) { 43 | Integer value = getInt(name); 44 | return value == null ? defaultValue : value; 45 | } 46 | 47 | @Override 48 | public Short getShort(CharSequence name) { 49 | String value = get(name); 50 | return value == null ? null : Short.valueOf(get(name)); 51 | } 52 | 53 | @Override 54 | public short getShort(CharSequence name, short defaultValue) { 55 | Short value = getShort(name); 56 | return value == null ? defaultValue : value; 57 | } 58 | 59 | @Override 60 | public Long getTimeMillis(CharSequence name) { 61 | return null; 62 | } 63 | 64 | @Override 65 | public long getTimeMillis(CharSequence name, long defaultValue) { 66 | return 0; 67 | } 68 | 69 | @Override 70 | public List getAll(String name) { 71 | String value = get(name); 72 | return value == null ? new ArrayList<>() : Arrays.asList(new String[]{value}); 73 | } 74 | 75 | @Override 76 | public List> entries() { 77 | return new ArrayList<>(map.entrySet()); 78 | } 79 | 80 | @Override 81 | public boolean contains(String name) { 82 | return map.keySet().stream().anyMatch(k -> k.equalsIgnoreCase(name)); 83 | } 84 | 85 | @Deprecated 86 | @Override 87 | public Iterator> iterator() { 88 | return map.entrySet().iterator(); 89 | } 90 | 91 | @Override 92 | public Iterator> iteratorCharSequence() { 93 | Map temp = new HashMap<>(); 94 | map.entrySet().forEach((entry) -> temp.put(entry.getKey(), entry.getValue())); 95 | return temp.entrySet().iterator(); 96 | } 97 | 98 | @Override 99 | public boolean isEmpty() { 100 | return map.isEmpty(); 101 | } 102 | 103 | @Override 104 | public int size() { 105 | return map.size(); 106 | } 107 | 108 | @Override 109 | public Set names() { 110 | return map.keySet(); 111 | } 112 | 113 | @Override 114 | public HttpHeaders add(String name, Object value) { 115 | map.put(name, value.toString()); 116 | return this; 117 | } 118 | 119 | @Override 120 | public HttpHeaders add(String name, Iterable values) { 121 | return add(name, values); 122 | } 123 | 124 | @Override 125 | public HttpHeaders addInt(CharSequence name, int value) { 126 | return add(name, value); 127 | } 128 | 129 | @Override 130 | public HttpHeaders addShort(CharSequence name, short value) { 131 | return add(name, value); 132 | } 133 | 134 | @Override 135 | public HttpHeaders set(String name, Object value) { 136 | return add(name, value); 137 | } 138 | 139 | @Override 140 | public HttpHeaders set(String name, Iterable values) { 141 | return add(name, values); 142 | } 143 | 144 | @Override 145 | public HttpHeaders setInt(CharSequence name, int value) { 146 | return add(name, value); 147 | } 148 | 149 | @Override 150 | public HttpHeaders setShort(CharSequence name, short value) { 151 | return add(name, value); 152 | } 153 | 154 | @Override 155 | public HttpHeaders remove(String name) { 156 | map.keySet().stream().filter(k -> k.equalsIgnoreCase(name)).findAny() 157 | .ifPresent(k -> map.remove(k)); 158 | return this; 159 | } 160 | 161 | @Override 162 | public HttpHeaders clear() { 163 | map.clear(); 164 | return this; 165 | } 166 | 167 | public Map toMap() { 168 | return map; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/main/java/org/pdown/core/entity/HttpRequestInfo.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.entity; 2 | 3 | import io.netty.handler.codec.DecoderResult; 4 | import io.netty.handler.codec.http.DefaultHttpRequest; 5 | import io.netty.handler.codec.http.HttpHeaders; 6 | import io.netty.handler.codec.http.HttpMethod; 7 | import io.netty.handler.codec.http.HttpRequest; 8 | import io.netty.handler.codec.http.HttpVersion; 9 | import org.pdown.core.util.ProtoUtil.RequestProto; 10 | import java.io.Serializable; 11 | import java.util.Map.Entry; 12 | 13 | public class HttpRequestInfo implements HttpRequest, Serializable { 14 | 15 | private static final long serialVersionUID = -4521453515739581677L; 16 | private RequestProto requestProto; 17 | private HttpVer version; 18 | private String method; 19 | private String uri; 20 | private HttpHeadsInfo headers; 21 | private byte[] content; 22 | 23 | public HttpRequestInfo() { 24 | } 25 | 26 | public HttpRequestInfo(HttpVer version, HttpMethod method, String uri) { 27 | this(version, method, uri, null, null); 28 | } 29 | 30 | public HttpRequestInfo(HttpMethod method, String uri) { 31 | this(HttpVer.HTTP_1_1, method, uri); 32 | } 33 | 34 | public HttpRequestInfo(HttpVer version, HttpMethod method, String uri, 35 | HttpHeadsInfo headers, byte[] content) { 36 | this.version = version; 37 | this.method = method.toString(); 38 | this.uri = uri; 39 | this.headers = headers; 40 | this.content = content; 41 | } 42 | 43 | public HttpRequest setContent(byte[] content) { 44 | this.content = content; 45 | return this; 46 | } 47 | 48 | public byte[] content() { 49 | return content; 50 | } 51 | 52 | public RequestProto requestProto() { 53 | return requestProto; 54 | } 55 | 56 | public HttpRequest setRequestProto(RequestProto requestProto) { 57 | this.requestProto = requestProto; 58 | return this; 59 | } 60 | 61 | @Override 62 | public HttpMethod method() { 63 | return new HttpMethod(method); 64 | } 65 | 66 | @Override 67 | @Deprecated 68 | public HttpMethod getMethod() { 69 | return method(); 70 | } 71 | 72 | @Override 73 | public HttpRequest setMethod(HttpMethod method) { 74 | this.method = method.toString(); 75 | return this; 76 | } 77 | 78 | @Override 79 | public String uri() { 80 | return uri; 81 | } 82 | 83 | @Override 84 | @Deprecated 85 | public String getUri() { 86 | return uri(); 87 | } 88 | 89 | @Override 90 | public HttpRequest setUri(String uri) { 91 | this.uri = uri; 92 | return this; 93 | } 94 | 95 | 96 | @Override 97 | public HttpVersion protocolVersion() { 98 | if (version == HttpVer.HTTP_1_0) { 99 | return HttpVersion.HTTP_1_0; 100 | } else { 101 | return HttpVersion.HTTP_1_1; 102 | } 103 | } 104 | 105 | @Deprecated 106 | @Override 107 | public HttpVersion getProtocolVersion() { 108 | return protocolVersion(); 109 | } 110 | 111 | @Override 112 | public HttpRequest setProtocolVersion(HttpVersion version) { 113 | if (version.minorVersion() == 0) { 114 | this.version = HttpVer.HTTP_1_0; 115 | } else { 116 | this.version = HttpVer.HTTP_1_1; 117 | } 118 | return this; 119 | } 120 | 121 | @Override 122 | public HttpHeaders headers() { 123 | return headers; 124 | } 125 | 126 | public HttpRequest setVersion(HttpVer version) { 127 | this.version = version; 128 | return this; 129 | } 130 | 131 | public HttpRequest setMethod(String method) { 132 | this.method = method; 133 | return this; 134 | } 135 | 136 | public HttpRequest setHeaders(HttpHeadsInfo headers) { 137 | this.headers = headers; 138 | return this; 139 | } 140 | 141 | @Deprecated 142 | @Override 143 | public DecoderResult getDecoderResult() { 144 | return null; 145 | } 146 | 147 | @Override 148 | public DecoderResult decoderResult() { 149 | return null; 150 | } 151 | 152 | @Override 153 | public void setDecoderResult(DecoderResult result) { 154 | 155 | } 156 | 157 | @Override 158 | public String toString() { 159 | StringBuilder buf = new StringBuilder(); 160 | buf.append(method()); 161 | buf.append(' '); 162 | buf.append(uri()); 163 | buf.append(' '); 164 | buf.append(protocolVersion()); 165 | buf.append('\n'); 166 | if (headers != null) { 167 | for (Entry e : headers) { 168 | buf.append(e.getKey()); 169 | buf.append(": "); 170 | buf.append(e.getValue()); 171 | buf.append('\n'); 172 | } 173 | buf.setLength(buf.length() - 1); 174 | } 175 | if (content != null) { 176 | buf.append(new String(content)); 177 | } 178 | return buf.toString(); 179 | } 180 | 181 | public enum HttpVer { 182 | HTTP_1_0, HTTP_1_1 183 | } 184 | 185 | public static HttpRequest adapter(HttpRequest httpRequest) { 186 | if (httpRequest instanceof DefaultHttpRequest) { 187 | HttpVer version; 188 | if (httpRequest.protocolVersion().minorVersion() == 0) { 189 | version = HttpVer.HTTP_1_0; 190 | } else { 191 | version = HttpVer.HTTP_1_1; 192 | } 193 | HttpHeadsInfo httpHeadsInfo = new HttpHeadsInfo(); 194 | for (Entry entry : httpRequest.headers()) { 195 | httpHeadsInfo.set(entry.getKey(), entry.getValue()); 196 | } 197 | return new HttpRequestInfo(version, httpRequest.method(), httpRequest.uri(), 198 | httpHeadsInfo, null); 199 | } 200 | return httpRequest; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/main/java/org/pdown/core/entity/HttpResponseInfo.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.entity; 2 | 3 | import java.io.Serializable; 4 | 5 | public class HttpResponseInfo implements Serializable { 6 | 7 | private static final long serialVersionUID = 4828023107331722582L; 8 | private String fileName; 9 | private long totalSize; 10 | /** 11 | * support HTTP 206 12 | */ 13 | private boolean supportRange; 14 | 15 | public HttpResponseInfo() { 16 | } 17 | 18 | public HttpResponseInfo(String fileName, long totalSize, boolean supportRange) { 19 | this.fileName = fileName; 20 | this.totalSize = totalSize; 21 | this.supportRange = supportRange; 22 | } 23 | 24 | public HttpResponseInfo(String fileName) { 25 | this.fileName = fileName; 26 | } 27 | 28 | public HttpResponseInfo(long totalSize, boolean supportRange) { 29 | this.totalSize = totalSize; 30 | this.supportRange = supportRange; 31 | } 32 | 33 | public String getFileName() { 34 | return fileName; 35 | } 36 | 37 | public HttpResponseInfo setFileName(String fileName) { 38 | this.fileName = fileName; 39 | return this; 40 | } 41 | 42 | public long getTotalSize() { 43 | return totalSize; 44 | } 45 | 46 | public HttpResponseInfo setTotalSize(long totalSize) { 47 | this.totalSize = totalSize; 48 | return this; 49 | } 50 | 51 | public boolean isSupportRange() { 52 | return supportRange; 53 | } 54 | 55 | public HttpResponseInfo setSupportRange(boolean supportRange) { 56 | this.supportRange = supportRange; 57 | return this; 58 | } 59 | 60 | @Override 61 | public String toString() { 62 | return "HttpResponseInfo{" + 63 | "fileName='" + fileName + '\'' + 64 | ", totalSize=" + totalSize + 65 | ", supportRange=" + supportRange + 66 | '}'; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/org/pdown/core/entity/TaskInfo.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.entity; 2 | 3 | import java.io.Serializable; 4 | import java.util.List; 5 | 6 | public class TaskInfo implements Serializable { 7 | 8 | private static final long serialVersionUID = 4813413517396555930L; 9 | private long downSize; 10 | private long startTime; 11 | private transient long lastStartTime; 12 | private transient long lastDownSize; 13 | private long lastPauseTime; 14 | private int status; 15 | private long speed; 16 | private List chunkInfoList; 17 | private List connectInfoList; 18 | 19 | public TaskInfo setDownSize(long downSize) { 20 | this.downSize = downSize; 21 | return this; 22 | } 23 | 24 | public long getStartTime() { 25 | return startTime; 26 | } 27 | 28 | public TaskInfo setStartTime(long startTime) { 29 | this.startTime = startTime; 30 | return this; 31 | } 32 | 33 | public long getLastStartTime() { 34 | return lastStartTime; 35 | } 36 | 37 | public TaskInfo setLastStartTime(long lastStartTime) { 38 | this.lastStartTime = lastStartTime; 39 | return this; 40 | } 41 | 42 | public long getLastDownSize() { 43 | return lastDownSize; 44 | } 45 | 46 | public TaskInfo setLastDownSize(long lastDownSize) { 47 | this.lastDownSize = lastDownSize; 48 | return this; 49 | } 50 | 51 | public long getLastPauseTime() { 52 | return lastPauseTime; 53 | } 54 | 55 | public TaskInfo setLastPauseTime(long lastPauseTime) { 56 | this.lastPauseTime = lastPauseTime; 57 | return this; 58 | } 59 | 60 | public int getStatus() { 61 | return status; 62 | } 63 | 64 | public TaskInfo setStatus(int status) { 65 | this.status = status; 66 | return this; 67 | } 68 | 69 | public long getSpeed() { 70 | return speed; 71 | } 72 | 73 | public TaskInfo setSpeed(long speed) { 74 | this.speed = speed; 75 | return this; 76 | } 77 | 78 | public List getChunkInfoList() { 79 | return chunkInfoList; 80 | } 81 | 82 | public TaskInfo setChunkInfoList(List chunkInfoList) { 83 | this.chunkInfoList = chunkInfoList; 84 | return this; 85 | } 86 | 87 | public List getConnectInfoList() { 88 | return connectInfoList; 89 | } 90 | 91 | public TaskInfo setConnectInfoList(List connectInfoList) { 92 | this.connectInfoList = connectInfoList; 93 | return this; 94 | } 95 | 96 | public long getDownSize() { 97 | return downSize; 98 | } 99 | 100 | @Override 101 | public String toString() { 102 | return "TaskInfo{" + 103 | "downSize=" + downSize + 104 | ", startTime=" + startTime + 105 | ", lastStartTime=" + lastStartTime + 106 | ", status=" + status + 107 | ", speed=" + speed + 108 | ", chunkInfoList=" + chunkInfoList + 109 | ", connectInfoList=" + connectInfoList + 110 | '}'; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/org/pdown/core/exception/BootstrapBuildException.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.exception; 2 | 3 | public class BootstrapBuildException extends RuntimeException { 4 | 5 | public BootstrapBuildException() { 6 | super(); 7 | } 8 | 9 | public BootstrapBuildException(String message) { 10 | super(message); 11 | } 12 | 13 | public BootstrapBuildException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | 17 | public BootstrapBuildException(Throwable cause) { 18 | super(cause); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/org/pdown/core/exception/BootstrapCreateDirException.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.exception; 2 | 3 | public class BootstrapCreateDirException extends BootstrapException { 4 | 5 | public BootstrapCreateDirException() { 6 | super(); 7 | } 8 | 9 | public BootstrapCreateDirException(String message) { 10 | super(message); 11 | } 12 | 13 | public BootstrapCreateDirException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | 17 | public BootstrapCreateDirException(Throwable cause) { 18 | super(cause); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/org/pdown/core/exception/BootstrapException.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.exception; 2 | 3 | public class BootstrapException extends RuntimeException { 4 | 5 | public BootstrapException() { 6 | super(); 7 | } 8 | 9 | public BootstrapException(String message) { 10 | super(message); 11 | } 12 | 13 | public BootstrapException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | 17 | public BootstrapException(Throwable cause) { 18 | super(cause); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/org/pdown/core/exception/BootstrapFileAlreadyExistsException.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.exception; 2 | 3 | public class BootstrapFileAlreadyExistsException extends BootstrapException { 4 | 5 | public BootstrapFileAlreadyExistsException() { 6 | super(); 7 | } 8 | 9 | public BootstrapFileAlreadyExistsException(String message) { 10 | super(message); 11 | } 12 | 13 | public BootstrapFileAlreadyExistsException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | 17 | public BootstrapFileAlreadyExistsException(Throwable cause) { 18 | super(cause); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/org/pdown/core/exception/BootstrapNoPermissionException.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.exception; 2 | 3 | public class BootstrapNoPermissionException extends BootstrapException { 4 | 5 | public BootstrapNoPermissionException() { 6 | super(); 7 | } 8 | 9 | public BootstrapNoPermissionException(String message) { 10 | super(message); 11 | } 12 | 13 | public BootstrapNoPermissionException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | 17 | public BootstrapNoPermissionException(Throwable cause) { 18 | super(cause); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/org/pdown/core/exception/BootstrapNoSpaceException.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.exception; 2 | 3 | public class BootstrapNoSpaceException extends BootstrapException { 4 | 5 | public BootstrapNoSpaceException() { 6 | super(); 7 | } 8 | 9 | public BootstrapNoSpaceException(String message) { 10 | super(message); 11 | } 12 | 13 | public BootstrapNoSpaceException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | 17 | public BootstrapNoSpaceException(Throwable cause) { 18 | super(cause); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/org/pdown/core/exception/BootstrapPathEmptyException.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.exception; 2 | 3 | public class BootstrapPathEmptyException extends BootstrapException { 4 | 5 | public BootstrapPathEmptyException() { 6 | super(); 7 | } 8 | 9 | public BootstrapPathEmptyException(String message) { 10 | super(message); 11 | } 12 | 13 | public BootstrapPathEmptyException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | 17 | public BootstrapPathEmptyException(Throwable cause) { 18 | super(cause); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/org/pdown/core/exception/BootstrapResolveException.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.exception; 2 | 3 | public class BootstrapResolveException extends BootstrapBuildException { 4 | 5 | public BootstrapResolveException() { 6 | super(); 7 | } 8 | 9 | public BootstrapResolveException(String message) { 10 | super(message); 11 | } 12 | 13 | public BootstrapResolveException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | 17 | public BootstrapResolveException(Throwable cause) { 18 | super(cause); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/org/pdown/core/handle/DownTimeoutHandler.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.handle; 2 | 3 | import io.netty.handler.timeout.ReadTimeoutHandler; 4 | import java.lang.reflect.Field; 5 | 6 | public class DownTimeoutHandler extends ReadTimeoutHandler { 7 | 8 | private static Field READ_TIME; 9 | 10 | public DownTimeoutHandler(int timeoutSeconds) { 11 | super(timeoutSeconds); 12 | try { 13 | READ_TIME = this.getClass().getSuperclass().getSuperclass().getDeclaredField("lastReadTime"); 14 | READ_TIME.setAccessible(true); 15 | } catch (NoSuchFieldException e) { 16 | e.printStackTrace(); 17 | } 18 | } 19 | 20 | public void updateLastReadTime(long sleepTime) throws IllegalAccessException { 21 | long readTime = READ_TIME.getLong(this); 22 | READ_TIME.set(this, readTime == -1 ? sleepTime : readTime + sleepTime); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/org/pdown/core/proxy/ProxyConfig.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.proxy; 2 | 3 | import java.io.Serializable; 4 | 5 | public class ProxyConfig implements Serializable { 6 | 7 | private static final long serialVersionUID = 1531104384359036231L; 8 | 9 | private ProxyType proxyType; 10 | private String host; 11 | private int port; 12 | private String user; 13 | private String pwd; 14 | 15 | public ProxyConfig() { 16 | } 17 | 18 | public ProxyConfig(ProxyType proxyType, String host, int port, String user, String pwd) { 19 | this.proxyType = proxyType; 20 | this.host = host; 21 | this.port = port; 22 | this.user = user; 23 | this.pwd = pwd; 24 | } 25 | 26 | public ProxyConfig(ProxyType proxyType, String host, int port) { 27 | this.proxyType = proxyType; 28 | this.host = host; 29 | this.port = port; 30 | } 31 | 32 | public ProxyType getProxyType() { 33 | return proxyType; 34 | } 35 | 36 | public void setProxyType(ProxyType proxyType) { 37 | this.proxyType = proxyType; 38 | } 39 | 40 | public String getHost() { 41 | return host; 42 | } 43 | 44 | public void setHost(String host) { 45 | this.host = host; 46 | } 47 | 48 | public int getPort() { 49 | return port; 50 | } 51 | 52 | public void setPort(int port) { 53 | this.port = port; 54 | } 55 | 56 | public String getUser() { 57 | return user; 58 | } 59 | 60 | public void setUser(String user) { 61 | this.user = user; 62 | } 63 | 64 | public String getPwd() { 65 | return pwd; 66 | } 67 | 68 | public void setPwd(String pwd) { 69 | this.pwd = pwd; 70 | } 71 | 72 | @Override 73 | public String toString() { 74 | return "ProxyConfig{" + 75 | "proxyType=" + proxyType + 76 | ", host='" + host + '\'' + 77 | ", port=" + port + 78 | ", user='" + user + '\'' + 79 | ", pwd='" + pwd + '\'' + 80 | '}'; 81 | } 82 | 83 | @Override 84 | public boolean equals(Object o) { 85 | if (this == o) { 86 | return true; 87 | } 88 | if (o == null || getClass() != o.getClass()) { 89 | return false; 90 | } 91 | 92 | ProxyConfig config = (ProxyConfig) o; 93 | 94 | if (port != config.port) { 95 | return false; 96 | } 97 | if (proxyType != config.proxyType) { 98 | return false; 99 | } 100 | if (host != null ? !host.equals(config.host) : config.host != null) { 101 | return false; 102 | } 103 | if (user != null ? !user.equals(config.user) : config.user != null) { 104 | return false; 105 | } 106 | return pwd != null ? pwd.equals(config.pwd) : config.pwd == null; 107 | } 108 | 109 | @Override 110 | public int hashCode() { 111 | int result = proxyType != null ? proxyType.hashCode() : 0; 112 | result = 31 * result + (host != null ? host.hashCode() : 0); 113 | result = 31 * result + port; 114 | result = 31 * result + (user != null ? user.hashCode() : 0); 115 | result = 31 * result + (pwd != null ? pwd.hashCode() : 0); 116 | return result; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/org/pdown/core/proxy/ProxyHandleFactory.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.proxy; 2 | 3 | import io.netty.handler.proxy.HttpProxyHandler; 4 | import io.netty.handler.proxy.ProxyHandler; 5 | import io.netty.handler.proxy.Socks4ProxyHandler; 6 | import io.netty.handler.proxy.Socks5ProxyHandler; 7 | import java.net.InetSocketAddress; 8 | 9 | public class ProxyHandleFactory { 10 | 11 | public static ProxyHandler build(ProxyConfig config) { 12 | ProxyHandler proxyHandler = null; 13 | if (config != null) { 14 | boolean isAuth = config.getUser() != null && config.getPwd() != null; 15 | InetSocketAddress inetSocketAddress = new InetSocketAddress(config.getHost(), 16 | config.getPort()); 17 | switch (config.getProxyType()) { 18 | case HTTP: 19 | if (isAuth) { 20 | proxyHandler = new HttpProxyHandler(inetSocketAddress, 21 | config.getUser(), config.getPwd()); 22 | } else { 23 | proxyHandler = new HttpProxyHandler(inetSocketAddress); 24 | } 25 | break; 26 | case SOCKS4: 27 | proxyHandler = new Socks4ProxyHandler(inetSocketAddress); 28 | break; 29 | case SOCKS5: 30 | if (isAuth) { 31 | proxyHandler = new Socks5ProxyHandler(inetSocketAddress, 32 | config.getUser(), config.getPwd()); 33 | } else { 34 | proxyHandler = new Socks5ProxyHandler(inetSocketAddress); 35 | } 36 | break; 37 | } 38 | } 39 | return proxyHandler; 40 | 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/org/pdown/core/proxy/ProxyType.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.proxy; 2 | 3 | public enum ProxyType { 4 | HTTP, SOCKS4, SOCKS5 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/org/pdown/core/util/ByteUtil.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.util; 2 | 3 | public class ByteUtil { 4 | 5 | public static String byteFormat(long size) { 6 | String[] unit = {"B", "KB", "MB", "GB", "TB", "PB"}; 7 | int pow = 0; 8 | long temp = size; 9 | while (temp / 1000D >= 1) { 10 | pow++; 11 | temp /= 1000D; 12 | } 13 | String fmt = unit[pow]; 14 | return String.format("%.2f", size / Math.pow(1024, pow)) + fmt; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/org/pdown/core/util/FileUtil.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.util; 2 | 3 | import java.io.File; 4 | import java.io.FileInputStream; 5 | import java.io.FileOutputStream; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.io.RandomAccessFile; 9 | import java.lang.reflect.Method; 10 | import java.nio.ByteBuffer; 11 | import java.nio.MappedByteBuffer; 12 | import java.nio.channels.FileChannel; 13 | import java.nio.channels.SeekableByteChannel; 14 | import java.nio.file.FileStore; 15 | import java.nio.file.FileSystem; 16 | import java.nio.file.FileSystems; 17 | import java.nio.file.Files; 18 | import java.nio.file.Path; 19 | import java.nio.file.Paths; 20 | import java.nio.file.StandardOpenOption; 21 | import java.nio.file.attribute.AclFileAttributeView; 22 | import java.nio.file.attribute.BasicFileAttributes; 23 | import java.util.Arrays; 24 | import java.util.Stack; 25 | import java.util.UUID; 26 | import java.util.zip.ZipEntry; 27 | import java.util.zip.ZipInputStream; 28 | import javax.swing.filechooser.FileSystemView; 29 | import org.pdown.core.boot.HttpDownBootstrap; 30 | 31 | public class FileUtil { 32 | 33 | /** 34 | * 文件或目录是否存在 35 | */ 36 | public static boolean exists(String path) { 37 | return new File(path).exists(); 38 | } 39 | 40 | /** 41 | * 文件是否存在 42 | */ 43 | public static boolean existsFile(String path) { 44 | File file = new File(path); 45 | return file.exists() && file.isFile(); 46 | } 47 | 48 | /** 49 | * 文件或目录是否存在 50 | */ 51 | public static boolean existsAny(String... paths) { 52 | return Arrays.stream(paths).anyMatch(path -> new File(path).exists()); 53 | } 54 | 55 | /** 56 | * 删除文件或文件夹 57 | */ 58 | public static void deleteIfExists(File file) throws IOException { 59 | if (file.exists()) { 60 | if (file.isFile()) { 61 | if (!file.delete()) { 62 | throw new IOException("Delete file failure,path:" + file.getAbsolutePath()); 63 | } 64 | } else { 65 | File[] files = file.listFiles(); 66 | if (files != null && files.length > 0) { 67 | for (File temp : files) { 68 | deleteIfExists(temp); 69 | } 70 | } 71 | if (!file.delete()) { 72 | throw new IOException("Delete file failure,path:" + file.getAbsolutePath()); 73 | } 74 | } 75 | } 76 | } 77 | 78 | /** 79 | * 删除文件或文件夹 80 | */ 81 | public static void deleteIfExists(String path) throws IOException { 82 | deleteIfExists(new File(path)); 83 | } 84 | 85 | /** 86 | * 创建文件,如果目标存在则删除 87 | */ 88 | public static File createFile(String path) throws IOException { 89 | return createFile(path, false); 90 | } 91 | 92 | /** 93 | * 创建文件夹,如果目标存在则删除 94 | */ 95 | public static File createDir(String path) throws IOException { 96 | return createDir(path, false); 97 | } 98 | 99 | /** 100 | * 创建文件,如果目标存在则删除 101 | */ 102 | public static File createFile(String path, boolean isHidden) throws IOException { 103 | File file = createFileSmart(path); 104 | if (OsUtil.isWindows()) { 105 | Files.setAttribute(file.toPath(), "dos:hidden", isHidden); 106 | } 107 | return file; 108 | } 109 | 110 | /** 111 | * 创建文件夹,如果目标存在则删除 112 | */ 113 | public static File createDir(String path, boolean isHidden) throws IOException { 114 | File file = new File(path); 115 | deleteIfExists(file); 116 | File newFile = new File(path); 117 | newFile.mkdir(); 118 | if (OsUtil.isWindows()) { 119 | Files.setAttribute(newFile.toPath(), "dos:hidden", isHidden); 120 | } 121 | return file; 122 | } 123 | 124 | /** 125 | * 查看文件或者文件夹大小 126 | */ 127 | public static long getFileSize(String path) { 128 | File file = new File(path); 129 | if (file.exists()) { 130 | if (file.isFile()) { 131 | return file.length(); 132 | } else { 133 | long size = 0; 134 | File[] files = file.listFiles(); 135 | if (files != null && files.length > 0) { 136 | for (File temp : files) { 137 | if (temp.isFile()) { 138 | size += temp.length(); 139 | } 140 | } 141 | } 142 | return size; 143 | } 144 | } 145 | return 0; 146 | } 147 | 148 | public static File createFileSmart(String path) throws IOException { 149 | try { 150 | File file = new File(path); 151 | if (file.exists()) { 152 | file.delete(); 153 | file.createNewFile(); 154 | } else { 155 | createDirSmart(file.getParent()); 156 | file.createNewFile(); 157 | } 158 | return file; 159 | } catch (IOException e) { 160 | throw new IOException("createFileSmart=" + path, e); 161 | } 162 | } 163 | 164 | public static File createDirSmart(String path) throws IOException { 165 | try { 166 | File file = new File(path); 167 | if (!file.exists()) { 168 | Stack stack = new Stack<>(); 169 | File temp = new File(path); 170 | while (temp != null) { 171 | stack.push(temp); 172 | temp = temp.getParentFile(); 173 | } 174 | while (stack.size() > 0) { 175 | File dir = stack.pop(); 176 | if (!dir.exists()) { 177 | dir.mkdir(); 178 | } 179 | } 180 | } 181 | return file; 182 | } catch (Exception e) { 183 | throw new IOException("createDirSmart=" + path, e); 184 | } 185 | } 186 | 187 | /** 188 | * 获取目录所属磁盘剩余容量 189 | */ 190 | public static long getDiskFreeSize(String path) { 191 | File file = new File(path); 192 | return file.getFreeSpace(); 193 | } 194 | 195 | public static void unmap(MappedByteBuffer mappedBuffer) throws IOException { 196 | try { 197 | Class clazz = Class.forName("sun.nio.ch.FileChannelImpl"); 198 | Method m = clazz.getDeclaredMethod("unmap", MappedByteBuffer.class); 199 | m.setAccessible(true); 200 | m.invoke(clazz, mappedBuffer); 201 | } catch (Exception e) { 202 | throw new IOException("LargeMappedByteBuffer close", e); 203 | } 204 | } 205 | 206 | /** 207 | * 去掉后缀名 208 | */ 209 | public static String getFileNameNoSuffix(String fileName) { 210 | int index = fileName.lastIndexOf("."); 211 | if (index != -1) { 212 | return fileName.substring(0, index); 213 | } 214 | return fileName; 215 | } 216 | 217 | public static void initFile(String path, boolean isHidden) throws IOException { 218 | initFile(path, null, isHidden); 219 | } 220 | 221 | public static void initFile(String path, InputStream input, boolean isHidden) throws IOException { 222 | if (exists(path)) { 223 | try ( 224 | RandomAccessFile raf = new RandomAccessFile(path, "rws") 225 | ) { 226 | raf.setLength(0); 227 | } 228 | } else { 229 | FileUtil.createFile(path, isHidden); 230 | } 231 | if (input != null) { 232 | try ( 233 | RandomAccessFile raf = new RandomAccessFile(path, "rws") 234 | ) { 235 | byte[] bts = new byte[8192]; 236 | int len; 237 | while ((len = input.read(bts)) != -1) { 238 | raf.write(bts, 0, len); 239 | } 240 | } finally { 241 | input.close(); 242 | } 243 | } 244 | } 245 | 246 | public static boolean canWrite(String path) { 247 | File file = new File(path); 248 | File test; 249 | if (file.isFile()) { 250 | test = new File( 251 | file.getParent() + File.separator + UUID.randomUUID().toString() + ".test"); 252 | } else { 253 | test = new File(file.getPath() + File.separator + UUID.randomUUID().toString() + ".test"); 254 | } 255 | try { 256 | test.createNewFile(); 257 | test.delete(); 258 | } catch (IOException e) { 259 | return false; 260 | } 261 | return true; 262 | } 263 | 264 | public static void unzip(String zipPath, String toPath, String... unzipFile) throws IOException { 265 | try ( 266 | ZipInputStream zipInputStream = new ZipInputStream(new FileInputStream(zipPath)) 267 | ) { 268 | toPath = toPath == null ? new File(zipPath).getParent() : toPath; 269 | ZipEntry entry; 270 | while ((entry = zipInputStream.getNextEntry()) != null) { 271 | final String entryName = entry.getName(); 272 | if (entry.isDirectory() || (unzipFile != null && unzipFile.length > 0 273 | && Arrays.stream(unzipFile) 274 | .noneMatch((file) -> entryName.equalsIgnoreCase(file)))) { 275 | zipInputStream.closeEntry(); 276 | continue; 277 | } 278 | File file = createFileSmart(toPath + File.separator + entryName); 279 | try ( 280 | FileOutputStream outputStream = new FileOutputStream(file) 281 | ) { 282 | byte[] bts = new byte[8192]; 283 | int len; 284 | while ((len = zipInputStream.read(bts)) != -1) { 285 | outputStream.write(bts, 0, len); 286 | } 287 | } 288 | } 289 | } 290 | } 291 | 292 | /** 293 | * 判断文件存在是重命名 294 | */ 295 | public static String renameIfExists(String path) { 296 | File file = new File(path); 297 | if (file.exists() && file.isFile()) { 298 | int index = file.getName().lastIndexOf("."); 299 | String name = file.getName().substring(0, index); 300 | String suffix = index == -1 ? "" : file.getName().substring(index); 301 | int i = 1; 302 | String newName; 303 | do { 304 | newName = name + "(" + i + ")" + suffix; 305 | i++; 306 | } 307 | while (existsFile(file.getParent() + File.separator + newName)); 308 | return newName; 309 | } 310 | return file.getName(); 311 | } 312 | 313 | /** 314 | * 创建指定大小的Sparse File 315 | */ 316 | public static void createFileWithSparse(String filePath, long length) throws IOException { 317 | Path path = Paths.get(filePath); 318 | try { 319 | Files.deleteIfExists(path); 320 | try ( 321 | SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE, StandardOpenOption.SPARSE) 322 | ) { 323 | channel.position(length - 1); 324 | channel.write(ByteBuffer.wrap(new byte[]{0})); 325 | } 326 | } catch (IOException e) { 327 | throw new IOException("create spares file fail,path:" + filePath + " length:" + length, e); 328 | } 329 | } 330 | 331 | /** 332 | * 使用RandomAccessFile创建指定大小的File 333 | */ 334 | public static void createFileWithDefault(String filePath, long length) throws IOException { 335 | Path path = Paths.get(filePath); 336 | try { 337 | Files.deleteIfExists(path); 338 | try ( 339 | RandomAccessFile randomAccessFile = new RandomAccessFile(filePath, "rw"); 340 | ) { 341 | randomAccessFile.setLength(length); 342 | } 343 | } catch (IOException e) { 344 | throw new IOException("create spares file fail,path:" + filePath + " length:" + length, e); 345 | } 346 | } 347 | 348 | public static String getSystemFileType(String filePath) throws IOException { 349 | File file = new File(filePath); 350 | if (!file.exists()) { 351 | file = file.getParentFile(); 352 | } 353 | return Files.getFileStore(file.toPath()).type(); 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /src/main/java/org/pdown/core/util/HttpDownUtil.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.util; 2 | 3 | import io.netty.bootstrap.Bootstrap; 4 | import io.netty.channel.Channel; 5 | import io.netty.channel.ChannelFuture; 6 | import io.netty.channel.ChannelFutureListener; 7 | import io.netty.channel.ChannelHandlerContext; 8 | import io.netty.channel.ChannelInboundHandlerAdapter; 9 | import io.netty.channel.ChannelInitializer; 10 | import io.netty.channel.nio.NioEventLoopGroup; 11 | import io.netty.channel.socket.nio.NioSocketChannel; 12 | import io.netty.handler.codec.http.DefaultLastHttpContent; 13 | import io.netty.handler.codec.http.HttpClientCodec; 14 | import io.netty.handler.codec.http.HttpConstants; 15 | import io.netty.handler.codec.http.HttpContent; 16 | import io.netty.handler.codec.http.HttpHeaderNames; 17 | import io.netty.handler.codec.http.HttpHeaders; 18 | import io.netty.handler.codec.http.HttpMethod; 19 | import io.netty.handler.codec.http.HttpRequest; 20 | import io.netty.handler.codec.http.HttpResponse; 21 | import io.netty.handler.codec.http.HttpResponseStatus; 22 | import io.netty.handler.ssl.SslContext; 23 | import io.netty.handler.ssl.SslContextBuilder; 24 | import io.netty.handler.ssl.util.InsecureTrustManagerFactory; 25 | import io.netty.resolver.NoopAddressResolverGroup; 26 | import io.netty.util.internal.StringUtil; 27 | import java.io.Closeable; 28 | import java.io.File; 29 | import java.io.IOException; 30 | import java.net.MalformedURLException; 31 | import java.net.URL; 32 | import java.net.URLDecoder; 33 | import java.util.List; 34 | import java.util.Map; 35 | import java.util.Map.Entry; 36 | import java.util.concurrent.CountDownLatch; 37 | import java.util.concurrent.TimeUnit; 38 | import java.util.concurrent.TimeoutException; 39 | import java.util.regex.Matcher; 40 | import java.util.regex.Pattern; 41 | import javax.net.ssl.SSLException; 42 | import org.pdown.core.boot.HttpDownBootstrap; 43 | import org.pdown.core.entity.HttpHeadsInfo; 44 | import org.pdown.core.entity.HttpRequestInfo; 45 | import org.pdown.core.entity.HttpRequestInfo.HttpVer; 46 | import org.pdown.core.entity.HttpResponseInfo; 47 | import org.pdown.core.exception.BootstrapResolveException; 48 | import org.pdown.core.proxy.ProxyConfig; 49 | import org.pdown.core.proxy.ProxyHandleFactory; 50 | import org.pdown.core.util.ProtoUtil.RequestProto; 51 | 52 | public class HttpDownUtil { 53 | 54 | private static SslContext sslContext; 55 | 56 | public static SslContext getSslContext() throws SSLException { 57 | if (sslContext == null) { 58 | synchronized (HttpDownUtil.class) { 59 | if (sslContext == null) { 60 | sslContext = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build(); 61 | } 62 | } 63 | } 64 | return sslContext; 65 | } 66 | 67 | /** 68 | * 检测响应相关信息 69 | */ 70 | public static HttpResponseInfo getHttpResponseInfo(HttpRequest httpRequest, HttpHeaders resHeaders, ProxyConfig proxyConfig, NioEventLoopGroup loopGroup) 71 | throws Exception { 72 | HttpResponse httpResponse = null; 73 | if (resHeaders == null) { 74 | httpResponse = getResponse(httpRequest, proxyConfig, loopGroup); 75 | //处理重定向 76 | if ((httpResponse.status().code() + "").indexOf("30") == 0) { 77 | //TODO 302重定向乱码 https://link.gimhoy.com/googledrive/aHR0cHM6Ly9kcml2ZS5nb29nbGUuY29tL29wZW4/aWQ9MThlVmNKeEhwaE40RUpGTUowSk10bWNXOVhCcWJhVE1k.jpg 78 | String redirectUrl = httpResponse.headers().get(HttpHeaderNames.LOCATION); 79 | HttpRequestInfo requestInfo = (HttpRequestInfo) httpRequest; 80 | //重定向cookie设置 81 | List setCookies = httpResponse.headers().getAll(HttpHeaderNames.SET_COOKIE); 82 | if (setCookies != null && setCookies.size() > 0) { 83 | StringBuilder requestCookie = new StringBuilder(); 84 | String oldRequestCookie = requestInfo.headers().get(HttpHeaderNames.COOKIE); 85 | if (oldRequestCookie != null) { 86 | requestCookie.append(oldRequestCookie); 87 | } 88 | String split = String.valueOf((char) HttpConstants.SEMICOLON) + String.valueOf(HttpConstants.SP_CHAR); 89 | for (String setCookie : setCookies) { 90 | String cookieNV = setCookie.split(split)[0]; 91 | if (requestCookie.length() > 0) { 92 | requestCookie.append(split); 93 | } 94 | requestCookie.append(cookieNV); 95 | } 96 | requestInfo.headers().set(HttpHeaderNames.COOKIE, requestCookie.toString()); 97 | } 98 | requestInfo.headers().remove(HttpHeaderNames.HOST); 99 | requestInfo.setUri(redirectUrl); 100 | RequestProto requestProto = ProtoUtil.getRequestProto(requestInfo); 101 | requestInfo.headers().set(HttpHeaderNames.HOST, requestProto.getHost()); 102 | requestInfo.setRequestProto(requestProto); 103 | return getHttpResponseInfo(httpRequest, null, proxyConfig, loopGroup); 104 | } 105 | } 106 | if (httpResponse == null) { 107 | httpResponse = getResponse(httpRequest, proxyConfig, loopGroup); 108 | } 109 | if (httpResponse.status().code() != HttpResponseStatus.OK.code() 110 | && httpResponse.status().code() != HttpResponseStatus.PARTIAL_CONTENT.code()) { 111 | throw new BootstrapResolveException("Status code exception:" + httpResponse.status().code()); 112 | } 113 | return parseResponse(httpRequest, httpResponse); 114 | } 115 | 116 | public static String getDownFileName(HttpRequest httpRequest, HttpHeaders resHeaders) { 117 | String fileName = null; 118 | String disposition = resHeaders.get(HttpHeaderNames.CONTENT_DISPOSITION); 119 | if (disposition != null) { 120 | //attachment;filename=1.rar attachment;filename=*UTF-8''1.rar 121 | Pattern pattern = Pattern.compile("^.*filename\\*?=\"?(?:.*'')?([^\"]*)\"?$"); 122 | Matcher matcher = pattern.matcher(disposition); 123 | if (matcher.find()) { 124 | char[] chs = matcher.group(1).toCharArray(); 125 | byte[] bts = new byte[chs.length]; 126 | //netty将byte转成了char,导致中文乱码 HttpObjectDecoder(:803) 127 | for (int i = 0; i < chs.length; i++) { 128 | bts[i] = (byte) chs[i]; 129 | } 130 | try { 131 | fileName = new String(bts, "UTF-8"); 132 | fileName = URLDecoder.decode(fileName, "UTF-8"); 133 | } catch (Exception e) { 134 | 135 | } 136 | } 137 | } 138 | if (fileName == null) { 139 | Pattern pattern = Pattern.compile("^.*/([^/?]*\\.[^./?]+)(\\?[^?]*)?$"); 140 | Matcher matcher = pattern.matcher(httpRequest.uri()); 141 | if (matcher.find()) { 142 | fileName = matcher.group(1); 143 | } else { 144 | fileName = "Unknown"; 145 | } 146 | } 147 | return fileName; 148 | } 149 | 150 | /** 151 | * 取当前请求的ContentLength 152 | */ 153 | public static long getDownContentSize(HttpHeaders resHeaders) { 154 | String contentRange = resHeaders.get(HttpHeaderNames.CONTENT_RANGE); 155 | if (contentRange != null) { 156 | Pattern pattern = Pattern.compile("^[^\\d]*(\\d+)-(\\d+)/.*$"); 157 | Matcher matcher = pattern.matcher(contentRange); 158 | if (matcher.find()) { 159 | long startSize = Long.parseLong(matcher.group(1)); 160 | long endSize = Long.parseLong(matcher.group(2)); 161 | return endSize - startSize + 1; 162 | } 163 | } else { 164 | String contentLength = resHeaders.get(HttpHeaderNames.CONTENT_LENGTH); 165 | if (contentLength != null) { 166 | return Long.valueOf(resHeaders.get(HttpHeaderNames.CONTENT_LENGTH)); 167 | } 168 | } 169 | return 0; 170 | } 171 | 172 | /** 173 | * 取请求下载文件的总大小 174 | */ 175 | public static long getDownFileSize(HttpHeaders resHeaders) { 176 | String contentRange = resHeaders.get(HttpHeaderNames.CONTENT_RANGE); 177 | if (contentRange != null) { 178 | Pattern pattern = Pattern.compile("^.*/(\\d+).*$"); 179 | Matcher matcher = pattern.matcher(contentRange); 180 | if (matcher.find()) { 181 | return Long.parseLong(matcher.group(1)); 182 | } 183 | } else { 184 | String contentLength = resHeaders.get(HttpHeaderNames.CONTENT_LENGTH); 185 | if (contentLength != null) { 186 | return Long.valueOf(resHeaders.get(HttpHeaderNames.CONTENT_LENGTH)); 187 | } 188 | } 189 | return 0; 190 | } 191 | 192 | public static HttpResponseInfo parseResponse(HttpRequest httpRequest, HttpResponse httpResponse) { 193 | HttpResponseInfo responseInfo = new HttpResponseInfo(); 194 | responseInfo.setFileName(getDownFileName(httpRequest, httpResponse.headers())); 195 | responseInfo.setTotalSize(getDownFileSize(httpResponse.headers())); 196 | //chunked编码不支持断点下载 197 | if (httpResponse.headers().contains(HttpHeaderNames.CONTENT_LENGTH)) { 198 | //206表示支持断点下载 199 | if (httpResponse.status().equals(HttpResponseStatus.PARTIAL_CONTENT)) { 200 | responseInfo.setSupportRange(true); 201 | } 202 | } 203 | return responseInfo; 204 | } 205 | 206 | /** 207 | * 取请求响应 208 | */ 209 | public static HttpResponse getResponse(HttpRequest httpRequest, ProxyConfig proxyConfig, NioEventLoopGroup loopGroup) throws Exception { 210 | final HttpResponse[] httpResponses = new HttpResponse[1]; 211 | CountDownLatch cdl = new CountDownLatch(1); 212 | HttpRequestInfo requestInfo = (HttpRequestInfo) httpRequest; 213 | RequestProto requestProto = requestInfo.requestProto(); 214 | Bootstrap bootstrap = new Bootstrap(); 215 | bootstrap.group(loopGroup) // 注册线程池 216 | .channel(NioSocketChannel.class) // 使用NioSocketChannel来作为连接用的channel类 217 | .handler(new ChannelInitializer() { 218 | 219 | @Override 220 | protected void initChannel(Channel ch) throws Exception { 221 | if (proxyConfig != null) { 222 | ch.pipeline().addLast(ProxyHandleFactory.build(proxyConfig)); 223 | } 224 | if (requestProto.getSsl()) { 225 | ch.pipeline().addLast(getSslContext().newHandler(ch.alloc(), requestProto.getHost(), requestProto.getPort())); 226 | } 227 | ch.pipeline().addLast("httpCodec", new HttpClientCodec()); 228 | ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { 229 | 230 | @Override 231 | public void channelRead(ChannelHandlerContext ctx0, Object msg0) 232 | throws Exception { 233 | if (msg0 instanceof HttpResponse) { 234 | HttpResponse httpResponse = (HttpResponse) msg0; 235 | httpResponses[0] = httpResponse; 236 | ctx0.channel().close(); 237 | cdl.countDown(); 238 | } 239 | } 240 | 241 | 242 | }); 243 | } 244 | 245 | }); 246 | if (proxyConfig != null) { 247 | //代理服务器解析DNS和连接 248 | bootstrap.resolver(NoopAddressResolverGroup.INSTANCE); 249 | } 250 | ChannelFuture cf = bootstrap.connect(requestProto.getHost(), requestProto.getPort()); 251 | cf.addListener((ChannelFutureListener) future -> { 252 | if (future.isSuccess()) { 253 | //请求下载一个字节测试是否支持断点下载 254 | httpRequest.headers().set(HttpHeaderNames.RANGE, "bytes=0-0"); 255 | cf.channel().writeAndFlush(httpRequest); 256 | if (requestInfo.content() != null) { 257 | //请求体写入 258 | HttpContent content = new DefaultLastHttpContent(); 259 | content.content().writeBytes(requestInfo.content()); 260 | cf.channel().writeAndFlush(content); 261 | } 262 | } else { 263 | cdl.countDown(); 264 | } 265 | }); 266 | cdl.await(60, TimeUnit.SECONDS); 267 | if (httpResponses[0] == null) { 268 | throw new TimeoutException("getResponse timeout"); 269 | } 270 | httpRequest.headers().remove(HttpHeaderNames.RANGE); 271 | return httpResponses[0]; 272 | } 273 | 274 | /** 275 | * 关闭tcp连接和文件描述符 276 | */ 277 | public static void safeClose(Channel channel, Closeable... fileChannels) throws IOException { 278 | if (channel != null && channel.isOpen()) { 279 | //关闭旧的下载连接 280 | channel.close(); 281 | } 282 | if (fileChannels != null && fileChannels.length > 0) { 283 | for (Closeable closeable : fileChannels) { 284 | if (closeable != null) { 285 | //关闭旧的下载文件连接 286 | closeable.close(); 287 | } 288 | } 289 | } 290 | } 291 | 292 | public static RequestProto parseRequestProto(String url) throws MalformedURLException { 293 | return parseRequestProto(new URL(url)); 294 | } 295 | 296 | public static RequestProto parseRequestProto(URL url) { 297 | int port = url.getPort() == -1 ? url.getDefaultPort() : url.getPort(); 298 | return new RequestProto(url.getHost(), port, url.getProtocol().equalsIgnoreCase("https")); 299 | } 300 | 301 | public static HttpRequestInfo buildRequest(String method, String url, Map heads, String body) 302 | throws MalformedURLException { 303 | URL u = new URL(url); 304 | HttpHeadsInfo headsInfo = new HttpHeadsInfo(); 305 | headsInfo.add("Host", u.getHost()); 306 | headsInfo.add("Connection", "keep-alive"); 307 | headsInfo.add("User-Agent", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.75 Safari/537.36"); 308 | headsInfo.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8"); 309 | headsInfo.add("Referer", u.getHost()); 310 | if (heads != null) { 311 | for (Entry entry : heads.entrySet()) { 312 | headsInfo.set(entry.getKey(), entry.getValue() == null ? "" : entry.getValue()); 313 | } 314 | } 315 | byte[] content = null; 316 | if (body != null && body.length() > 0) { 317 | content = body.getBytes(); 318 | headsInfo.add("Content-Length", content.length); 319 | } 320 | HttpMethod httpMethod = StringUtil.isNullOrEmpty(method) ? HttpMethod.GET : HttpMethod.valueOf(method.toUpperCase()); 321 | HttpRequestInfo requestInfo = new HttpRequestInfo(HttpVer.HTTP_1_1, httpMethod, u.getFile(), headsInfo, content); 322 | requestInfo.setRequestProto(parseRequestProto(u)); 323 | return requestInfo; 324 | } 325 | 326 | public static HttpRequestInfo buildRequest(String url, Map heads, String body) 327 | throws MalformedURLException { 328 | return buildRequest(null, url, heads, body); 329 | } 330 | 331 | public static HttpRequestInfo buildRequest(String url, Map heads) 332 | throws MalformedURLException { 333 | return buildRequest(url, heads, null); 334 | } 335 | 336 | public static HttpRequestInfo buildRequest(String url) 337 | throws MalformedURLException { 338 | return buildRequest(url, null); 339 | } 340 | 341 | public static String getUrl(HttpRequest request) { 342 | String host = request.headers().get(HttpHeaderNames.HOST); 343 | String url; 344 | if (request.uri().indexOf("/") == 0) { 345 | if (request.uri().length() > 1) { 346 | url = host + request.uri(); 347 | } else { 348 | url = host; 349 | } 350 | } else { 351 | url = request.uri(); 352 | } 353 | return url; 354 | } 355 | 356 | /** 357 | * 取下载文件绝对路径 358 | */ 359 | public static String getTaskFilePath(HttpDownBootstrap httpDownBootstrap) { 360 | return httpDownBootstrap.getDownConfig().getFilePath() + File.separator + httpDownBootstrap.getResponse().getFileName(); 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /src/main/java/org/pdown/core/util/OsUtil.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.util; 2 | 3 | import java.io.IOException; 4 | import java.net.ServerSocket; 5 | 6 | public class OsUtil { 7 | 8 | /** 9 | * 查看指定的端口号是否空闲,若空闲则返回否则返回一个随机的空闲端口号 10 | */ 11 | public static int getFreePort(int defaultPort) throws IOException { 12 | try ( 13 | ServerSocket serverSocket = new ServerSocket(defaultPort) 14 | ) { 15 | return serverSocket.getLocalPort(); 16 | } catch (IOException e) { 17 | return getFreePort(); 18 | } 19 | } 20 | 21 | /** 22 | * 获取空闲端口号 23 | */ 24 | public static int getFreePort() throws IOException { 25 | try ( 26 | ServerSocket serverSocket = new ServerSocket(0) 27 | ) { 28 | return serverSocket.getLocalPort(); 29 | } 30 | } 31 | 32 | /** 33 | * 检查端口号是否被占用 34 | */ 35 | public static boolean isBusyPort(int port) { 36 | boolean ret = true; 37 | ServerSocket serverSocket = null; 38 | try { 39 | serverSocket = new ServerSocket(port); 40 | ret = false; 41 | } catch (Exception e) { 42 | } finally { 43 | if (serverSocket != null) { 44 | try { 45 | serverSocket.close(); 46 | } catch (IOException e) { 47 | e.printStackTrace(); 48 | } 49 | } 50 | } 51 | return ret; 52 | } 53 | 54 | private static final String OS = System.getProperty("os.name").toLowerCase(); 55 | 56 | public static boolean isWindows() { 57 | return OS.indexOf("win") >= 0; 58 | } 59 | 60 | public static boolean isWindowsXP() { 61 | return OS.indexOf("win") >= 0 && OS.indexOf("xp") >= 0; 62 | } 63 | 64 | public static boolean isMac() { 65 | return OS.indexOf("mac") >= 0; 66 | } 67 | 68 | public static boolean isUnix() { 69 | return OS.indexOf("nix") >= 0 || OS.indexOf("nux") >= 0 || OS.indexOf("aix") >= 0; 70 | } 71 | 72 | public static boolean isSolaris() { 73 | return (OS.indexOf("sunos") >= 0); 74 | } 75 | 76 | private static final String ARCH = System.getProperty("sun.arch.data.model"); 77 | 78 | public static boolean is64() { 79 | return "64".equals(ARCH); 80 | } 81 | 82 | public static boolean is32() { 83 | return "32".equals(ARCH); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/org/pdown/core/util/ProtoUtil.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.util; 2 | 3 | import io.netty.handler.codec.http.HttpHeaderNames; 4 | import io.netty.handler.codec.http.HttpRequest; 5 | import java.io.Serializable; 6 | import java.util.regex.Matcher; 7 | import java.util.regex.Pattern; 8 | 9 | public class ProtoUtil { 10 | 11 | public static RequestProto getRequestProto(HttpRequest httpRequest) { 12 | RequestProto requestProto = new RequestProto(); 13 | int port = -1; 14 | String hostStr = httpRequest.headers().get(HttpHeaderNames.HOST); 15 | if (hostStr == null) { 16 | Pattern pattern = Pattern.compile("^(?:https?://)?(?[^/]*)/?.*$"); 17 | Matcher matcher = pattern.matcher(httpRequest.uri()); 18 | if (matcher.find()) { 19 | hostStr = matcher.group("host"); 20 | } else { 21 | return null; 22 | } 23 | } 24 | String uriStr = httpRequest.uri(); 25 | Pattern pattern = Pattern.compile("^(?:https?://)?(?[^:]*)(?::(?\\d+))?(/.*)?$"); 26 | Matcher matcher = pattern.matcher(hostStr); 27 | //先从host上取端口号没取到再从uri上取端口号 issues#4 28 | String portTemp = null; 29 | if (matcher.find()) { 30 | requestProto.setHost(matcher.group("host")); 31 | portTemp = matcher.group("port"); 32 | if (portTemp == null) { 33 | matcher = pattern.matcher(uriStr); 34 | if (matcher.find()) { 35 | portTemp = matcher.group("port"); 36 | } 37 | } 38 | } 39 | if (portTemp != null) { 40 | port = Integer.parseInt(portTemp); 41 | } 42 | boolean isSsl = uriStr.indexOf("https") == 0 || hostStr.indexOf("https") == 0; 43 | if (port == -1) { 44 | if (isSsl) { 45 | port = 443; 46 | } else { 47 | port = 80; 48 | } 49 | } 50 | requestProto.setPort(port); 51 | requestProto.setSsl(isSsl); 52 | return requestProto; 53 | } 54 | 55 | public static class RequestProto implements Serializable { 56 | 57 | private static final long serialVersionUID = -6471051659605127698L; 58 | private String host; 59 | private int port; 60 | private boolean ssl; 61 | 62 | public RequestProto() { 63 | } 64 | 65 | public RequestProto(String host, int port, boolean ssl) { 66 | this.host = host; 67 | this.port = port; 68 | this.ssl = ssl; 69 | } 70 | 71 | public String getHost() { 72 | return host; 73 | } 74 | 75 | public void setHost(String host) { 76 | this.host = host; 77 | } 78 | 79 | public int getPort() { 80 | return port; 81 | } 82 | 83 | public void setPort(int port) { 84 | this.port = port; 85 | } 86 | 87 | public boolean getSsl() { 88 | return ssl; 89 | } 90 | 91 | public void setSsl(boolean ssl) { 92 | this.ssl = ssl; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %msg%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/test/java/org/pdown/core/test/ConsoleDownTestClient.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.test; 2 | 3 | import org.pdown.core.boot.HttpDownBootstrap; 4 | import org.pdown.core.dispatch.ConsoleHttpDownCallback; 5 | import org.pdown.core.entity.HttpDownConfigInfo; 6 | import org.pdown.core.test.server.RangeDownTestServer; 7 | import org.pdown.core.test.util.TestUtil; 8 | import org.pdown.core.util.FileUtil; 9 | 10 | public class ConsoleDownTestClient { 11 | 12 | private static final String TEST_DIR = System.getProperty("user.dir") + "/target/test"; 13 | private static final String TEST_FILE = TEST_DIR + "/" + "test.data"; 14 | 15 | public static void main(String[] args) throws Exception { 16 | FileUtil.createDir(TEST_DIR); 17 | //正常下载 18 | new Thread(() -> { 19 | try { 20 | new RangeDownTestServer("f:/test/test.data").start(8866); 21 | } catch (InterruptedException e) { 22 | e.printStackTrace(); 23 | } 24 | }).start(); 25 | //生成下载文件 26 | TestUtil.buildRandomFile(TEST_FILE, 1024 * 1024 * 100L); 27 | HttpDownBootstrap.builder("http://127.0.0.1:8866") 28 | .downConfig(new HttpDownConfigInfo().setFilePath(TEST_DIR) 29 | .setAutoRename(true) 30 | .setConnections(5) 31 | .setSpeedLimit(5 * 1024 * 1024)) 32 | .callback(new ConsoleHttpDownCallback() { 33 | @Override 34 | public void onError(HttpDownBootstrap httpDownBootstrap) { 35 | super.onError(httpDownBootstrap); 36 | System.exit(0); 37 | } 38 | 39 | @Override 40 | public void onDone(HttpDownBootstrap httpDownBootstrap) { 41 | super.onDone(httpDownBootstrap); 42 | System.exit(0); 43 | } 44 | }).build().start(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/org/pdown/core/test/DownClientTest.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.test; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.util.Map; 6 | import java.util.concurrent.CountDownLatch; 7 | import java.util.concurrent.TimeUnit; 8 | import java.util.concurrent.atomic.AtomicBoolean; 9 | import java.util.concurrent.atomic.AtomicInteger; 10 | import org.junit.After; 11 | import org.junit.Before; 12 | import org.junit.Test; 13 | import org.pdown.core.boot.HttpDownBootstrap; 14 | import org.pdown.core.dispatch.ConsoleHttpDownCallback; 15 | import org.pdown.core.entity.HttpDownConfigInfo; 16 | import org.pdown.core.test.server.ChunkedDownTestServer; 17 | import org.pdown.core.test.server.RangeDownTestServer; 18 | import org.pdown.core.test.util.TestUtil; 19 | import org.pdown.core.util.FileUtil; 20 | import org.pdown.core.util.HttpDownUtil; 21 | 22 | public class DownClientTest { 23 | 24 | private static final String TEST_DIR = System.getProperty("user.dir") + "/target/test"; 25 | private static final String TEST_BUILD_FILE = TEST_DIR + "/build.data"; 26 | 27 | @Before 28 | public void httpServerStart() throws InterruptedException, IOException { 29 | FileUtil.createDirSmart(TEST_DIR); 30 | //生成下载文件 31 | TestUtil.buildRandomFile(TEST_BUILD_FILE, 1024 * 1024 * 500L); 32 | //正常下载 33 | new RangeDownTestServer(TEST_BUILD_FILE).start(8866); 34 | //超时下载 35 | new RangeDownTestServer(TEST_BUILD_FILE) { 36 | 37 | private AtomicInteger count = new AtomicInteger(0); 38 | 39 | @Override 40 | protected void writeHandle(Map attr) throws InterruptedException { 41 | if (count.getAndAdd(1) < 2) { 42 | String key = "flag"; 43 | Boolean flag = (Boolean) attr.get(key); 44 | if (flag == null) { 45 | attr.put(key, true); 46 | TimeUnit.SECONDS.sleep(10); 47 | } 48 | } 49 | } 50 | }.start(8867); 51 | //chunk下载 52 | new ChunkedDownTestServer(TEST_BUILD_FILE).start(8868); 53 | } 54 | 55 | @Test 56 | public void down() throws Exception { 57 | //正常下载 58 | downTest(8866, 1); 59 | downTest(8866, 4); 60 | downTest(8866, 32); 61 | downTest(8866, 64); 62 | //超时下载 63 | downTest(8867, 1); 64 | downTest(8867, 4); 65 | downTest(8867, 32); 66 | downTest(8867, 64); 67 | //chunked编码下载 68 | downTest(8868, 2); 69 | //限速5MB/S 70 | downTest(8866, 4, 1024 * 1024 * 5L, 0); 71 | //暂停5S继续 72 | downTest(8866, 4, 0, 5000L); 73 | } 74 | 75 | @After 76 | public void deleteTestFile() throws IOException { 77 | FileUtil.deleteIfExists(TEST_DIR); 78 | } 79 | 80 | public static void downTest(int port, int connections, long speedLimit, long pauseTime) throws Exception { 81 | CountDownLatch countDownLatch = new CountDownLatch(1); 82 | AtomicBoolean succ = new AtomicBoolean(false); 83 | HttpDownBootstrap httpDownBootstrap = HttpDownBootstrap.builder("http://127.0.0.1:" + port) 84 | .downConfig(new HttpDownConfigInfo().setFilePath(TEST_DIR) 85 | .setAutoRename(true) 86 | .setConnections(connections) 87 | .setTimeout(5) 88 | .setSpeedLimit(speedLimit)) 89 | .callback(new ConsoleHttpDownCallback() { 90 | 91 | @Override 92 | public void onDone(HttpDownBootstrap httpDownBootstrap) { 93 | System.out.println(""); 94 | String sourceMd5 = TestUtil.getMd5ByFile(new File(TEST_BUILD_FILE)); 95 | String downMd5 = TestUtil.getMd5ByFile(new File(HttpDownUtil.getTaskFilePath(httpDownBootstrap))); 96 | if (sourceMd5.equals(downMd5)) { 97 | succ.set(true); 98 | } 99 | try { 100 | FileUtil.deleteIfExists(HttpDownUtil.getTaskFilePath(httpDownBootstrap)); 101 | } catch (IOException e) { 102 | e.printStackTrace(); 103 | } 104 | countDownLatch.countDown(); 105 | } 106 | 107 | @Override 108 | public void onError(HttpDownBootstrap httpDownBootstrap) { 109 | countDownLatch.countDown(); 110 | } 111 | 112 | @Override 113 | public void onPause(HttpDownBootstrap httpDownBootstrap) { 114 | System.out.println("\nonPause"); 115 | } 116 | 117 | @Override 118 | public void onResume(HttpDownBootstrap httpDownBootstrap) { 119 | System.out.println("\nonResume"); 120 | } 121 | }) 122 | .build(); 123 | httpDownBootstrap.start(); 124 | if (pauseTime > 0) { 125 | Thread.sleep(233L); 126 | httpDownBootstrap.pause(); 127 | Thread.sleep(pauseTime); 128 | httpDownBootstrap.resume(); 129 | } 130 | countDownLatch.await(); 131 | assert succ.get(); 132 | } 133 | 134 | private void downTest(int port, int connections) throws Exception { 135 | downTest(port, connections, 0, 0); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/test/java/org/pdown/core/test/server/ChunkedDownTestServer.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.test.server; 2 | 3 | import io.netty.bootstrap.ServerBootstrap; 4 | import io.netty.buffer.ByteBuf; 5 | import io.netty.buffer.PooledByteBufAllocator; 6 | import io.netty.channel.Channel; 7 | import io.netty.channel.ChannelFuture; 8 | import io.netty.channel.ChannelHandlerContext; 9 | import io.netty.channel.ChannelInboundHandlerAdapter; 10 | import io.netty.channel.ChannelInitializer; 11 | import io.netty.channel.nio.NioEventLoopGroup; 12 | import io.netty.channel.socket.nio.NioServerSocketChannel; 13 | import io.netty.handler.codec.http.DefaultHttpContent; 14 | import io.netty.handler.codec.http.DefaultHttpResponse; 15 | import io.netty.handler.codec.http.DefaultLastHttpContent; 16 | import io.netty.handler.codec.http.HttpContent; 17 | import io.netty.handler.codec.http.HttpHeaderNames; 18 | import io.netty.handler.codec.http.HttpHeaderValues; 19 | import io.netty.handler.codec.http.HttpResponse; 20 | import io.netty.handler.codec.http.HttpResponseStatus; 21 | import io.netty.handler.codec.http.HttpServerCodec; 22 | import io.netty.handler.codec.http.HttpVersion; 23 | import io.netty.handler.codec.http.LastHttpContent; 24 | import io.netty.handler.stream.ChunkedWriteHandler; 25 | import io.netty.util.ReferenceCountUtil; 26 | import java.io.File; 27 | import java.io.IOException; 28 | import java.io.RandomAccessFile; 29 | import java.nio.ByteBuffer; 30 | import java.nio.channels.FileChannel; 31 | import java.util.concurrent.CountDownLatch; 32 | import java.util.concurrent.TimeUnit; 33 | 34 | public class ChunkedDownTestServer { 35 | 36 | private int count; 37 | 38 | public ChunkedDownTestServer(String downFilePath) { 39 | this.downFilePath = downFilePath; 40 | } 41 | 42 | private String downFilePath; 43 | 44 | public void start(int port) throws InterruptedException { 45 | File file = new File(downFilePath); 46 | CountDownLatch countDownLatch = new CountDownLatch(1); 47 | new Thread(() -> { 48 | ServerBootstrap bootstrap = new ServerBootstrap(); 49 | NioEventLoopGroup bossGroup = new NioEventLoopGroup(1); 50 | NioEventLoopGroup workerGroup = new NioEventLoopGroup(32); 51 | try { 52 | bootstrap.group(bossGroup, workerGroup) 53 | .channel(NioServerSocketChannel.class) 54 | .childHandler(new ChannelInitializer() { 55 | 56 | @Override 57 | protected void initChannel(Channel ch) throws Exception { 58 | ch.pipeline().addLast("httpCodec", new HttpServerCodec()); 59 | ch.pipeline().addLast("chunk", new ChunkedWriteHandler()); 60 | ch.pipeline().addLast("serverHandle", new ChannelInboundHandlerAdapter() { 61 | 62 | @Override 63 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 64 | try { 65 | if (msg instanceof LastHttpContent) { 66 | try ( 67 | FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel() 68 | ) { 69 | HttpResponse httpResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); 70 | httpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_OCTET_STREAM); 71 | httpResponse.headers().set(HttpHeaderNames.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getName() + "\""); 72 | httpResponse.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED); 73 | ctx.channel().writeAndFlush(httpResponse); 74 | ByteBuffer buffer = ByteBuffer.allocateDirect(8192); 75 | while (fileChannel.read(buffer) != -1) { 76 | buffer.flip(); 77 | ByteBuf byteBuf = PooledByteBufAllocator.DEFAULT.directBuffer(8192); 78 | byteBuf.writeBytes(buffer); 79 | HttpContent httpContent = new DefaultHttpContent(byteBuf); 80 | ctx.channel().writeAndFlush(httpContent); 81 | buffer.clear(); 82 | if (count < 2) { 83 | count++; 84 | TimeUnit.SECONDS.sleep(10); 85 | } 86 | } 87 | ctx.channel().writeAndFlush(new DefaultLastHttpContent()); 88 | } 89 | } 90 | } catch (Exception e) { 91 | if (!(e instanceof IOException)) { 92 | e.printStackTrace(); 93 | } 94 | System.out.println("test server channel close"); 95 | } finally { 96 | ReferenceCountUtil.release(msg); 97 | } 98 | } 99 | 100 | @Override 101 | public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { 102 | ctx.channel().close(); 103 | } 104 | }); 105 | } 106 | }); 107 | ChannelFuture f = bootstrap 108 | .bind(port) 109 | .sync(); 110 | f.addListener(future -> countDownLatch.countDown()); 111 | f.channel().closeFuture().sync(); 112 | } catch (InterruptedException e) { 113 | e.printStackTrace(); 114 | } finally { 115 | bossGroup.shutdownGracefully(); 116 | workerGroup.shutdownGracefully(); 117 | } 118 | }).start(); 119 | countDownLatch.await(); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/test/java/org/pdown/core/test/server/RangeDownTestServer.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.test.server; 2 | 3 | import io.netty.bootstrap.ServerBootstrap; 4 | import io.netty.buffer.ByteBuf; 5 | import io.netty.buffer.PooledByteBufAllocator; 6 | import io.netty.buffer.Unpooled; 7 | import io.netty.channel.Channel; 8 | import io.netty.channel.ChannelFuture; 9 | import io.netty.channel.ChannelFutureListener; 10 | import io.netty.channel.ChannelHandlerContext; 11 | import io.netty.channel.ChannelInboundHandlerAdapter; 12 | import io.netty.channel.ChannelInitializer; 13 | import io.netty.channel.nio.NioEventLoopGroup; 14 | import io.netty.channel.socket.nio.NioServerSocketChannel; 15 | import io.netty.handler.codec.http.DefaultHttpContent; 16 | import io.netty.handler.codec.http.DefaultHttpResponse; 17 | import io.netty.handler.codec.http.DefaultLastHttpContent; 18 | import io.netty.handler.codec.http.HttpContent; 19 | import io.netty.handler.codec.http.HttpHeaderNames; 20 | import io.netty.handler.codec.http.HttpHeaderValues; 21 | import io.netty.handler.codec.http.HttpRequest; 22 | import io.netty.handler.codec.http.HttpResponse; 23 | import io.netty.handler.codec.http.HttpResponseStatus; 24 | import io.netty.handler.codec.http.HttpServerCodec; 25 | import io.netty.handler.codec.http.HttpVersion; 26 | import io.netty.handler.codec.http.LastHttpContent; 27 | import io.netty.handler.stream.ChunkedWriteHandler; 28 | import io.netty.util.ReferenceCountUtil; 29 | import java.io.File; 30 | import java.io.IOException; 31 | import java.io.RandomAccessFile; 32 | import java.nio.ByteBuffer; 33 | import java.nio.channels.FileChannel; 34 | import java.util.HashMap; 35 | import java.util.Map; 36 | import java.util.concurrent.CountDownLatch; 37 | 38 | public class RangeDownTestServer { 39 | 40 | public RangeDownTestServer(String downFilePath) { 41 | this.downFilePath = downFilePath; 42 | } 43 | 44 | private String downFilePath; 45 | 46 | public void start(int port) throws InterruptedException { 47 | File file = new File(downFilePath); 48 | CountDownLatch countDownLatch = new CountDownLatch(1); 49 | new Thread(() -> { 50 | ServerBootstrap bootstrap = new ServerBootstrap(); 51 | NioEventLoopGroup bossGroup = new NioEventLoopGroup(1); 52 | NioEventLoopGroup workerGroup = new NioEventLoopGroup(32); 53 | try { 54 | bootstrap.group(bossGroup, workerGroup) 55 | .channel(NioServerSocketChannel.class) 56 | .childHandler(new ChannelInitializer() { 57 | 58 | @Override 59 | protected void initChannel(Channel ch) throws Exception { 60 | ch.pipeline().addLast("httpCodec", new HttpServerCodec()); 61 | ch.pipeline().addLast("serverHandle", new ChannelInboundHandlerAdapter() { 62 | 63 | private Map attr = new HashMap<>(); 64 | private String range; 65 | 66 | @Override 67 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 68 | try { 69 | if (msg instanceof HttpRequest) { 70 | HttpRequest httpRequest = (HttpRequest) msg; 71 | range = httpRequest.headers().get(HttpHeaderNames.RANGE); 72 | } else if (msg instanceof LastHttpContent) { 73 | try ( 74 | FileChannel fileChannel = new RandomAccessFile(file, "r").getChannel() 75 | ) { 76 | if (range == null) { 77 | HttpResponse httpResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); 78 | httpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_OCTET_STREAM); 79 | httpResponse.headers().set(HttpHeaderNames.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getName() + "\""); 80 | httpResponse.headers().set(HttpHeaderNames.CONTENT_LENGTH, file.length()); 81 | ctx.channel().writeAndFlush(httpResponse); 82 | ByteBuffer buffer = ByteBuffer.allocateDirect(8192); 83 | while (fileChannel.read(buffer) != -1) { 84 | buffer.flip(); 85 | ByteBuf byteBuf = PooledByteBufAllocator.DEFAULT.directBuffer(8192); 86 | byteBuf.writeBytes(buffer); 87 | HttpContent httpContent = new DefaultHttpContent(byteBuf); 88 | ctx.channel().writeAndFlush(httpContent); 89 | buffer.clear(); 90 | } 91 | ctx.channel().writeAndFlush(new DefaultLastHttpContent()); 92 | } else { 93 | String[] rangeArray = range.split("=")[1].split("-"); 94 | int start = Integer.parseInt(rangeArray[0].trim()); 95 | int end = Integer.parseInt(rangeArray[1].trim()); 96 | long total = end - start + 1; 97 | HttpResponse httpResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.PARTIAL_CONTENT); 98 | httpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_OCTET_STREAM); 99 | httpResponse.headers().set(HttpHeaderNames.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getName() + "\""); 100 | httpResponse.headers().set(HttpHeaderNames.CONTENT_LENGTH, total); 101 | httpResponse.headers().set(HttpHeaderNames.CONTENT_RANGE, start + "-" + end + "/" + file.length()); 102 | httpResponse.headers().set(HttpHeaderNames.ACCEPT_RANGES, HttpHeaderValues.BYTES); 103 | long temp = beforeSendResponse(httpResponse, start, end, file); 104 | long writeTotal = temp < 0 ? total : temp; 105 | ctx.channel().writeAndFlush(httpResponse); 106 | fileChannel.position(start); 107 | ByteBuffer buffer = ByteBuffer.allocateDirect(8192); 108 | long writeSize = 0; 109 | while (fileChannel.read(buffer) != -1) { 110 | buffer.flip(); 111 | ByteBuf byteBuf = PooledByteBufAllocator.DEFAULT.directBuffer(8192); 112 | int remaining = buffer.remaining(); 113 | writeSize += remaining; 114 | if (writeSize < writeTotal) { 115 | byteBuf.writeBytes(buffer); 116 | HttpContent httpContent = new DefaultHttpContent(byteBuf); 117 | ctx.channel().writeAndFlush(httpContent); 118 | } else { 119 | buffer.limit((int) (buffer.limit() - (writeSize - writeTotal))); 120 | byteBuf.writeBytes(buffer); 121 | HttpContent httpContent = beforeSendLastContent(byteBuf); 122 | ctx.channel().writeAndFlush(httpContent); 123 | break; 124 | } 125 | buffer.clear(); 126 | writeHandle(attr); 127 | } 128 | ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); 129 | } 130 | } 131 | } 132 | } catch (Exception e) { 133 | if (!(e instanceof IOException)) { 134 | e.printStackTrace(); 135 | } 136 | System.out.println("test server channel close"); 137 | } finally { 138 | ReferenceCountUtil.release(msg); 139 | } 140 | } 141 | 142 | @Override 143 | public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { 144 | ctx.channel().close(); 145 | } 146 | }); 147 | } 148 | }); 149 | ChannelFuture f = bootstrap 150 | .bind(port) 151 | .sync(); 152 | f.addListener(future -> countDownLatch.countDown()); 153 | f.channel().closeFuture().sync(); 154 | } catch (InterruptedException e) { 155 | e.printStackTrace(); 156 | } finally { 157 | bossGroup.shutdownGracefully(); 158 | workerGroup.shutdownGracefully(); 159 | } 160 | }).start(); 161 | countDownLatch.await(); 162 | } 163 | 164 | protected long beforeSendResponse(HttpResponse httpResponse, long start, long end, File file) { 165 | return -1; 166 | } 167 | 168 | protected HttpContent beforeSendLastContent(ByteBuf byteBuf) { 169 | return new DefaultLastHttpContent(byteBuf); 170 | } 171 | 172 | protected void writeHandle(Map attr) throws InterruptedException { 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/test/java/org/pdown/core/test/util/TestUtil.java: -------------------------------------------------------------------------------- 1 | package org.pdown.core.test.util; 2 | 3 | import java.io.File; 4 | import java.io.FileInputStream; 5 | import java.io.FileOutputStream; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.security.MessageDigest; 9 | 10 | public class TestUtil { 11 | 12 | public static void buildRandomFile(String path, long size) throws IOException { 13 | File file = new File(path); 14 | if (file.exists()) { 15 | file.delete(); 16 | } 17 | file.createNewFile(); 18 | try ( 19 | FileOutputStream outputStream = new FileOutputStream(file) 20 | ) { 21 | byte[] bts = new byte[8192]; 22 | for (int i = 0; i < bts.length; i++) { 23 | bts[i] = (byte) (Math.random() * 255); 24 | } 25 | for (long i = 0; i < size; i += bts.length) { 26 | outputStream.write(bts); 27 | } 28 | } 29 | } 30 | 31 | public static String getMd5ByFile(File file) { 32 | InputStream fis; 33 | byte[] buffer = new byte[2048]; 34 | int numRead; 35 | MessageDigest md5; 36 | 37 | try { 38 | fis = new FileInputStream(file); 39 | md5 = MessageDigest.getInstance("MD5"); 40 | while ((numRead = fis.read(buffer)) > 0) { 41 | md5.update(buffer, 0, numRead); 42 | } 43 | fis.close(); 44 | return md5ToString(md5.digest()); 45 | } catch (Exception e) { 46 | return null; 47 | } 48 | } 49 | 50 | private static String md5ToString(byte[] md5Bytes) { 51 | StringBuffer hexValue = new StringBuffer(); 52 | for (int i = 0; i < md5Bytes.length; i++) { 53 | int val = ((int) md5Bytes[i]) & 0xff; 54 | if (val < 16) { 55 | hexValue.append("0"); 56 | } 57 | hexValue.append(Integer.toHexString(val)); 58 | } 59 | return hexValue.toString(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %msg%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | --------------------------------------------------------------------------------