PageRenderTime 59ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 0ms

/core/src/com/cloud/storage/template/HttpTemplateDownloader.java

https://github.com/rietn/CloudStack
Java | 450 lines | 353 code | 67 blank | 30 comment | 73 complexity | 9b875c3836138a1d45757b9a3eaa5085 MD5 | raw file
  1. /**
  2. * Copyright (C) 2010 Cloud.com, Inc. All rights reserved.
  3. *
  4. * This software is licensed under the GNU General Public License v3 or later.
  5. *
  6. * It is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU General Public License as published by
  8. * the Free Software Foundation, either version 3 of the License, or any later version.
  9. * This program is distributed in the hope that it will be useful,
  10. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. * GNU General Public License for more details.
  13. *
  14. * You should have received a copy of the GNU General Public License
  15. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. *
  17. */
  18. package com.cloud.storage.template;
  19. import java.io.BufferedInputStream;
  20. import java.io.File;
  21. import java.io.IOException;
  22. import java.io.InputStream;
  23. import java.io.RandomAccessFile;
  24. import java.net.Inet6Address;
  25. import java.net.InetAddress;
  26. import java.net.URI;
  27. import java.net.URISyntaxException;
  28. import java.net.UnknownHostException;
  29. import java.util.Date;
  30. import org.apache.commons.httpclient.ChunkedInputStream;
  31. import org.apache.commons.httpclient.Credentials;
  32. import org.apache.commons.httpclient.Header;
  33. import org.apache.commons.httpclient.HttpClient;
  34. import org.apache.commons.httpclient.HttpException;
  35. import org.apache.commons.httpclient.HttpMethod;
  36. import org.apache.commons.httpclient.HttpMethodRetryHandler;
  37. import org.apache.commons.httpclient.HttpStatus;
  38. import org.apache.commons.httpclient.NoHttpResponseException;
  39. import org.apache.commons.httpclient.UsernamePasswordCredentials;
  40. import org.apache.commons.httpclient.auth.AuthScope;
  41. import org.apache.commons.httpclient.methods.GetMethod;
  42. import org.apache.commons.httpclient.params.HttpMethodParams;
  43. import org.apache.log4j.Logger;
  44. import com.cloud.storage.StorageLayer;
  45. import com.cloud.utils.exception.CloudRuntimeException;
  46. import com.cloud.utils.Pair;
  47. /**
  48. * Download a template file using HTTP
  49. * @author Chiradeep
  50. *
  51. */
  52. public class HttpTemplateDownloader implements TemplateDownloader {
  53. public static final Logger s_logger = Logger.getLogger(HttpTemplateDownloader.class.getName());
  54. private static final int CHUNK_SIZE = 1024*1024; //1M
  55. private String downloadUrl;
  56. private String toFile;
  57. public TemplateDownloader.Status status= TemplateDownloader.Status.NOT_STARTED;
  58. public String errorString = " ";
  59. private long remoteSize = 0;
  60. public long downloadTime = 0;
  61. public long totalBytes;
  62. private final HttpClient client;
  63. private GetMethod request;
  64. private boolean resume = false;
  65. private DownloadCompleteCallback completionCallback;
  66. StorageLayer _storage;
  67. boolean inited = true;
  68. private String toDir;
  69. private long MAX_TEMPLATE_SIZE_IN_BYTES;
  70. private final HttpMethodRetryHandler myretryhandler;
  71. public HttpTemplateDownloader (StorageLayer storageLayer, String downloadUrl, String toDir, DownloadCompleteCallback callback, long maxTemplateSizeInBytes, String user, String password) {
  72. this._storage = storageLayer;
  73. this.downloadUrl = downloadUrl;
  74. this.setToDir(toDir);
  75. this.status = TemplateDownloader.Status.NOT_STARTED;
  76. this.MAX_TEMPLATE_SIZE_IN_BYTES = maxTemplateSizeInBytes;
  77. this.totalBytes = 0;
  78. this.client = new HttpClient();
  79. myretryhandler = new HttpMethodRetryHandler() {
  80. public boolean retryMethod(
  81. final HttpMethod method,
  82. final IOException exception,
  83. int executionCount) {
  84. if (executionCount >= 2) {
  85. // Do not retry if over max retry count
  86. return false;
  87. }
  88. if (exception instanceof NoHttpResponseException) {
  89. // Retry if the server dropped connection on us
  90. return true;
  91. }
  92. if (!method.isRequestSent()) {
  93. // Retry if the request has not been sent fully or
  94. // if it's OK to retry methods that have been sent
  95. return true;
  96. }
  97. // otherwise do not retry
  98. return false;
  99. }
  100. };
  101. try {
  102. this.request = new GetMethod(downloadUrl);
  103. this.request.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, myretryhandler);
  104. this.completionCallback = callback;
  105. //this.request.setFollowRedirects(false);
  106. File f = File.createTempFile("dnld", "tmp_", new File(toDir));
  107. if (_storage != null) {
  108. _storage.setWorldReadableAndWriteable(f);
  109. }
  110. toFile = f.getAbsolutePath();
  111. Pair<String, Integer> hostAndPort = validateUrl(downloadUrl);
  112. if ((user != null) && (password != null)) {
  113. client.getParams().setAuthenticationPreemptive(true);
  114. Credentials defaultcreds = new UsernamePasswordCredentials(user, password);
  115. client.getState().setCredentials(new AuthScope(hostAndPort.first(), hostAndPort.second(), AuthScope.ANY_REALM), defaultcreds);
  116. s_logger.info("Added username=" + user + ", password=" + password + "for host " + hostAndPort.first() + ":" + hostAndPort.second());
  117. } else {
  118. s_logger.info("No credentials configured for host=" + hostAndPort.first() + ":" + hostAndPort.second());
  119. }
  120. } catch (IllegalArgumentException iae) {
  121. errorString = iae.getMessage();
  122. status = TemplateDownloader.Status.UNRECOVERABLE_ERROR;
  123. inited = false;
  124. } catch (Exception ex){
  125. errorString = "Unable to start download -- check url? ";
  126. status = TemplateDownloader.Status.UNRECOVERABLE_ERROR;
  127. s_logger.warn("Exception in constructor -- " + ex.toString());
  128. } catch (Throwable th) {
  129. s_logger.warn("throwable caught ", th);
  130. }
  131. }
  132. private Pair<String, Integer> validateUrl(String url) throws IllegalArgumentException {
  133. try {
  134. URI uri = new URI(url);
  135. if (!uri.getScheme().equalsIgnoreCase("http") && !uri.getScheme().equalsIgnoreCase("https") ) {
  136. throw new IllegalArgumentException("Unsupported scheme for url");
  137. }
  138. int port = uri.getPort();
  139. if (!(port == 80 || port == 443 || port == -1)) {
  140. throw new IllegalArgumentException("Only ports 80 and 443 are allowed");
  141. }
  142. if (port == -1 && uri.getScheme().equalsIgnoreCase("https")) {
  143. port = 443;
  144. } else if (port == -1 && uri.getScheme().equalsIgnoreCase("http")) {
  145. port = 80;
  146. }
  147. String host = uri.getHost();
  148. try {
  149. InetAddress hostAddr = InetAddress.getByName(host);
  150. if (hostAddr.isAnyLocalAddress() || hostAddr.isLinkLocalAddress() || hostAddr.isLoopbackAddress() || hostAddr.isMulticastAddress()) {
  151. throw new IllegalArgumentException("Illegal host specified in url");
  152. }
  153. if (hostAddr instanceof Inet6Address) {
  154. throw new IllegalArgumentException("IPV6 addresses not supported (" + hostAddr.getHostAddress() + ")");
  155. }
  156. return new Pair<String, Integer>(host, port);
  157. } catch (UnknownHostException uhe) {
  158. throw new IllegalArgumentException("Unable to resolve " + host);
  159. }
  160. } catch (IllegalArgumentException iae) {
  161. s_logger.warn("Failed uri validation check: " + iae.getMessage());
  162. throw iae;
  163. } catch (URISyntaxException use) {
  164. s_logger.warn("Failed uri syntax check: " + use.getMessage());
  165. throw new IllegalArgumentException(use.getMessage());
  166. }
  167. }
  168. @Override
  169. public long download(boolean resume, DownloadCompleteCallback callback) {
  170. switch (status) {
  171. case ABORTED:
  172. case UNRECOVERABLE_ERROR:
  173. case DOWNLOAD_FINISHED:
  174. return 0;
  175. default:
  176. }
  177. int bytes=0;
  178. File file = new File(toFile);
  179. try {
  180. long localFileSize = 0;
  181. if (file.exists() && resume) {
  182. localFileSize = file.length();
  183. s_logger.info("Resuming download to file (current size)=" + localFileSize);
  184. }
  185. Date start = new Date();
  186. int responseCode=0;
  187. if (localFileSize > 0 ) {
  188. // require partial content support for resume
  189. request.addRequestHeader("Range", "bytes=" + localFileSize + "-");
  190. if (client.executeMethod(request) != HttpStatus.SC_PARTIAL_CONTENT) {
  191. errorString = "HTTP Server does not support partial get";
  192. status = TemplateDownloader.Status.UNRECOVERABLE_ERROR;
  193. return 0;
  194. }
  195. } else if ((responseCode = client.executeMethod(request)) != HttpStatus.SC_OK) {
  196. status = TemplateDownloader.Status.UNRECOVERABLE_ERROR;
  197. errorString = " HTTP Server returned " + responseCode + " (expected 200 OK) ";
  198. return 0; //FIXME: retry?
  199. }
  200. Header contentLengthHeader = request.getResponseHeader("Content-Length");
  201. boolean chunked = false;
  202. long remoteSize2 = 0;
  203. if (contentLengthHeader == null) {
  204. Header chunkedHeader = request.getResponseHeader("Transfer-Encoding");
  205. if (chunkedHeader == null || !"chunked".equalsIgnoreCase(chunkedHeader.getValue())) {
  206. status = TemplateDownloader.Status.UNRECOVERABLE_ERROR;
  207. errorString=" Failed to receive length of download ";
  208. return 0; //FIXME: what status do we put here? Do we retry?
  209. } else if ("chunked".equalsIgnoreCase(chunkedHeader.getValue())){
  210. chunked = true;
  211. }
  212. } else {
  213. remoteSize2 = Long.parseLong(contentLengthHeader.getValue());
  214. }
  215. if (remoteSize == 0) {
  216. remoteSize = remoteSize2;
  217. }
  218. if (remoteSize > MAX_TEMPLATE_SIZE_IN_BYTES) {
  219. s_logger.info("Remote size is too large: " + remoteSize + " , max=" + MAX_TEMPLATE_SIZE_IN_BYTES);
  220. status = Status.UNRECOVERABLE_ERROR;
  221. errorString = "Download file size is too large";
  222. return 0;
  223. }
  224. if (remoteSize == 0) {
  225. remoteSize = MAX_TEMPLATE_SIZE_IN_BYTES;
  226. }
  227. InputStream in = !chunked?new BufferedInputStream(request.getResponseBodyAsStream())
  228. : new ChunkedInputStream(request.getResponseBodyAsStream());
  229. RandomAccessFile out = new RandomAccessFile(file, "rwd");
  230. out.seek(localFileSize);
  231. s_logger.info("Starting download from " + getDownloadUrl() + " to " + toFile + " remoteSize=" + remoteSize + " , max size=" + MAX_TEMPLATE_SIZE_IN_BYTES);
  232. byte[] block = new byte[CHUNK_SIZE];
  233. long offset=0;
  234. boolean done=false;
  235. status = TemplateDownloader.Status.IN_PROGRESS;
  236. while (!done && status != Status.ABORTED && offset <= remoteSize) {
  237. if ( (bytes = in.read(block, 0, CHUNK_SIZE)) > -1) {
  238. out.write(block, 0, bytes);
  239. offset +=bytes;
  240. out.seek(offset);
  241. totalBytes += bytes;
  242. } else {
  243. done = true;
  244. }
  245. }
  246. Date finish = new Date();
  247. String downloaded = "(incomplete download)";
  248. if (totalBytes >= remoteSize) {
  249. status = TemplateDownloader.Status.DOWNLOAD_FINISHED;
  250. downloaded = "(download complete remote=" + remoteSize + "bytes)";
  251. }
  252. errorString = "Downloaded " + totalBytes + " bytes " + downloaded;
  253. downloadTime += finish.getTime() - start.getTime();
  254. out.close();
  255. return totalBytes;
  256. }catch (HttpException hte) {
  257. status = TemplateDownloader.Status.UNRECOVERABLE_ERROR;
  258. errorString = hte.getMessage();
  259. } catch (IOException ioe) {
  260. status = TemplateDownloader.Status.UNRECOVERABLE_ERROR; //probably a file write error?
  261. errorString = ioe.getMessage();
  262. } finally {
  263. if (status == Status.UNRECOVERABLE_ERROR && file.exists() && !file.isDirectory()) {
  264. file.delete();
  265. }
  266. request.releaseConnection();
  267. if (callback != null) {
  268. callback.downloadComplete(status);
  269. }
  270. }
  271. return 0;
  272. }
  273. public String getDownloadUrl() {
  274. return downloadUrl;
  275. }
  276. public String getToFile() {
  277. File file = new File(toFile);
  278. return file.getAbsolutePath();
  279. }
  280. public TemplateDownloader.Status getStatus() {
  281. return status;
  282. }
  283. public long getDownloadTime() {
  284. return downloadTime;
  285. }
  286. public long getDownloadedBytes() {
  287. return totalBytes;
  288. }
  289. @Override
  290. @SuppressWarnings("fallthrough")
  291. public boolean stopDownload() {
  292. switch (getStatus()) {
  293. case IN_PROGRESS:
  294. if (request != null) {
  295. request.abort();
  296. }
  297. status = TemplateDownloader.Status.ABORTED;
  298. return true;
  299. case UNKNOWN:
  300. case NOT_STARTED:
  301. case RECOVERABLE_ERROR:
  302. case UNRECOVERABLE_ERROR:
  303. case ABORTED:
  304. status = TemplateDownloader.Status.ABORTED;
  305. case DOWNLOAD_FINISHED:
  306. File f = new File(toFile);
  307. if (f.exists()) {
  308. f.delete();
  309. }
  310. return true;
  311. default:
  312. return true;
  313. }
  314. }
  315. @Override
  316. public int getDownloadPercent() {
  317. if (remoteSize == 0) {
  318. return 0;
  319. }
  320. return (int)(100.0*totalBytes/remoteSize);
  321. }
  322. @Override
  323. public void run() {
  324. try {
  325. download(resume, completionCallback);
  326. } catch (Throwable t) {
  327. s_logger.warn("Caught exception during download "+ t.getMessage(), t);
  328. errorString = "Failed to install: " + t.getMessage();
  329. status = TemplateDownloader.Status.UNRECOVERABLE_ERROR;
  330. }
  331. }
  332. @Override
  333. public void setStatus(TemplateDownloader.Status status) {
  334. this.status = status;
  335. }
  336. public boolean isResume() {
  337. return resume;
  338. }
  339. @Override
  340. public String getDownloadError() {
  341. return errorString;
  342. }
  343. @Override
  344. public String getDownloadLocalPath() {
  345. return getToFile();
  346. }
  347. public void setResume(boolean resume) {
  348. this.resume = resume;
  349. }
  350. public void setToDir(String toDir) {
  351. this.toDir = toDir;
  352. }
  353. public String getToDir() {
  354. return toDir;
  355. }
  356. public long getMaxTemplateSizeInBytes() {
  357. return this.MAX_TEMPLATE_SIZE_IN_BYTES;
  358. }
  359. public static void main(String[] args) {
  360. String url ="http:// dev.mysql.com/get/Downloads/MySQL-5.0/mysql-noinstall-5.0.77-win32.zip/from/http://mirror.services.wisc.edu/mysql/";
  361. try {
  362. URI uri = new java.net.URI(url);
  363. } catch (URISyntaxException e) {
  364. // TODO Auto-generated catch block
  365. e.printStackTrace();
  366. }
  367. TemplateDownloader td = new HttpTemplateDownloader(null, url,"/tmp/mysql", null, TemplateDownloader.DEFAULT_MAX_TEMPLATE_SIZE_IN_BYTES, null, null);
  368. long bytes = td.download(true, null);
  369. if (bytes > 0) {
  370. System.out.println("Downloaded (" + bytes + " bytes)" + " in " + td.getDownloadTime()/1000 + " secs");
  371. } else {
  372. System.out.println("Failed download");
  373. }
  374. }
  375. @Override
  376. public void setDownloadError(String error) {
  377. errorString = error;
  378. }
  379. @Override
  380. public boolean isInited() {
  381. return inited;
  382. }
  383. }