├── .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 |
--------------------------------------------------------------------------------