Loading src/main/java/ecorp/easy/installer/tasks/DownloadTask.java +154 −288 Original line number Diff line number Diff line Loading @@ -33,8 +33,8 @@ import java.net.URL; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.DecimalFormat; Loading @@ -45,6 +45,7 @@ import java.util.Date; import java.util.Locale; import java.util.ResourceBundle; import java.util.Scanner; import java.nio.ByteBuffer; import javafx.concurrent.Task; import org.slf4j.Logger; import org.slf4j.LoggerFactory; Loading @@ -54,23 +55,15 @@ import org.json.JSONObject; import org.json.JSONException; /** * this class verify the checksum of a file and download it * @author vincent Bourgmayer * @author Ingo * This class verifies the checksum of a file and downloads it. * Author: Vincent Bourgmayer, Ingo */ public class DownloadTask extends Task<Boolean> { private final static String checkSumExtension = ".sha256sum"; private final static Logger logger = LoggerFactory.getLogger(DownloadTask.class); /** * Constant size */ private static final long[] CST_SIZE = {1, 1024, 1024*1024, 1024*1024*1024, 1024*1024*1024*1024}; /** * Constants units */ private static final String[] CST_UNITS = {"B", "KB", "MB", "GB", "TB"}; private static final DecimalFormat[] CST_FORMAT = { new DecimalFormat("#0"), new DecimalFormat("##0"), Loading @@ -79,32 +72,20 @@ public class DownloadTask extends Task<Boolean>{ new DecimalFormat("#,##0.000") }; final private ResourceBundle i18n; final private String targetUrl; private final ResourceBundle i18n; private final String targetUrl; private String fileName; /** * COnstruction of the download task * @param targetUrl the web path to the resource * @param fileName name of the file * @param resources used to send already translated message */ public DownloadTask(String targetUrl, String fileName, ResourceBundle resources) { this.targetUrl = targetUrl; this.fileName = fileName; this.i18n = resources; } /** * @inheritDoc * @return Boolean object * @throws Exception */ @Override protected Boolean call() throws Exception { final String latestBuildFilename = fetchLatestBuildFilename(targetUrl); final String localFilePath = AppConstants.getSourcesFolderPath() + fileName; final String checksumFilePath = localFilePath + checkSumExtension; if (isCancelled()) return false; Loading Loading @@ -151,33 +132,14 @@ public class DownloadTask extends Task<Boolean>{ } } // method link to file downloading /** * Read lastmodified date of the remote file at previous download of the file * Only uncompletly downloaded files are concerned * @param lmdFile * @return * @throws FileNotFoundException * @throws IOException */ private String readLastModifiedFrom(File lmdFile) throws FileNotFoundException, IOException{ String line; private String readLastModifiedFrom(File lmdFile) throws IOException { try (BufferedReader reader = new BufferedReader(new FileReader(lmdFile))) { line = reader.readLine(); return reader.readLine(); } return line; } /** * Write the last modified of the remote file for future use * @param lmdFile * @param timestamp * @throws IOException */ private void writeLastModified(File lmdFile, long timestamp) throws IOException { try (FileWriter fileWriter = new FileWriter(lmdFile, false)) { String lmd = DateTimeFormatter.RFC_1123_DATE_TIME .withZone(ZoneId.of("GMT")) .withLocale(Locale.ENGLISH) Loading @@ -187,7 +149,6 @@ public class DownloadTask extends Task<Boolean>{ } private String fetchLatestBuildFilename(String archiveUrl) { // Extract codeName and codeType from the archiveUrl Pair<String, String> codeInfo = getCodeNameFromUrl(archiveUrl); if (codeInfo == null) { logger.error("Failed to fetch latest build filename: codeName or codeType is null."); Loading @@ -197,7 +158,6 @@ public class DownloadTask extends Task<Boolean>{ String codeName = codeInfo.getKey(); String codeType = codeInfo.getValue(); // Construct API URL using codeName and codeType HttpURLConnection apiConnection = getApiConnection(codeName, codeType); if (apiConnection == null) { logger.error("Failed to fetch latest build filename: API connection is null."); Loading @@ -205,8 +165,7 @@ public class DownloadTask extends Task<Boolean>{ } try { String latestBuildFilename = processApiResponse(apiConnection); return latestBuildFilename; return processApiResponse(apiConnection); } catch (IOException e) { logger.error("Error processing API response: {}", e.getMessage()); return null; Loading Loading @@ -262,15 +221,11 @@ public class DownloadTask extends Task<Boolean>{ if (responseCode == HttpURLConnection.HTTP_OK) { String responseString = inputStreamToString(apiConnection.getInputStream()); // Try parsing the response as JSON new JSONObject(responseString); JSONObject responseJson = new JSONObject(responseString); JSONArray responses = responseJson.getJSONArray("response"); JSONObject responseObject = responses.getJSONObject(0); String filename = responseObject.getString("filename"); return filename; return responseObject.getString("filename"); } else { logger.error("Error fetching latest build filename: HTTP response code {}", responseCode); } Loading Loading @@ -300,249 +255,160 @@ public class DownloadTask extends Task<Boolean>{ return responseString.toString(); } /** * Perform the downloading of the specified file * If a part is already downloaded, then the download resume from previous state * @return boolean true if file has been successfully downloaded, false either */ private boolean downloadFile(final String fileURL, final String localFilePath, File lmdFile) throws MalformedURLException, IOException, InterruptedException{ private boolean downloadFile(final String fileURL, final String localFilePath, File lmdFile) throws IOException, InterruptedException { logger.debug("downloadFile({}, {})", fileURL, localFilePath); long previouslyDownloadedAmount = 0; long totalSize = 0; long lastModified = -1; //Build the query HttpURLConnection connect = (HttpURLConnection) new URL(fileURL).openConnection(); connect.setReadTimeout(30000); connect.setConnectTimeout(30000); File localFile = new File(localFilePath); if(localFile.exists()){ previouslyDownloadedAmount = localFile.length(); logger.debug("local file exist, size is {}", localFile.length()); String lmd = readLastModifiedFrom(lmdFile); String lastModifiedDate = (lmd != null) ? lmd : new Date(localFile.lastModified() ).toString(); logger.debug("last modified date = {}", lastModifiedDate); connect.setRequestProperty("If-Range", lastModifiedDate ); connect.setRequestProperty("Range", "bytes=" + previouslyDownloadedAmount + "-"); } //Perform the query and analyze result final int responseCode = connect.getResponseCode(); final boolean canAppendBytes = (responseCode == HttpURLConnection.HTTP_PARTIAL); logger.debug("response code: {}, {}", connect.getResponseCode(), connect.getResponseMessage()); final long lastModified = connect.getLastModified(); if( !canAppendBytes ){ if( responseCode == HttpURLConnection.HTTP_OK ) { //return false it resources is unreachable writeLastModified(lmdFile, lastModified); }else{ return false; try { lastModified = Long.parseLong(readLastModifiedFrom(lmdFile)); File existingFile = new File(localFilePath); if (existingFile.exists()) { previouslyDownloadedAmount = existingFile.length(); } previouslyDownloadedAmount = 0; //set it back to 0 in case it contains size of old content } catch (IOException e) { logger.warn("Unable to read last modified date from lmd file: {}", e.getMessage()); } catch (NumberFormatException e) { logger.warn("Invalid last modified date format in lmd file: {}", e.getMessage()); } //Get remote file Size final double fileSize = connect.getContentLengthLong(); logger.debug("remote fileSize = {}", fileSize); final double fullFileSize = fileSize+previouslyDownloadedAmount; logger.debug("full file size = {}", fullFileSize); HttpURLConnection httpConnection = (HttpURLConnection) new URL(fileURL).openConnection(); httpConnection.setRequestProperty("Range", "bytes=" + previouslyDownloadedAmount + "-"); //Update UI final int unitIndex = getDownloadUnit(fullFileSize); final String formattedFileSize = formatFileSize(fullFileSize, unitIndex); //used for UI updateProgress(-1, fullFileSize); updateMessage(formatFileSize(previouslyDownloadedAmount, unitIndex)+" / "+formattedFileSize ); boolean downloaded = false; try(FileOutputStream fos = new FileOutputStream(localFilePath,canAppendBytes); InputStream is = connect.getInputStream(); ReadableByteChannel rbc = Channels.newChannel(connect.getInputStream()); ){ //Start the timeOutThread which will stop a blocked download TimeOutRunnable timeoutRunnable = new TimeOutRunnable(); Thread timeoutThread = new Thread(timeoutRunnable); timeoutThread.setDaemon(true); timeoutThread.start(); long downloadAmount = previouslyDownloadedAmount; if (lastModified > 0) { httpConnection.setIfModifiedSince(lastModified); } while ( rbc.isOpen() && !isCancelled() && ! downloaded ){ if (httpConnection.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) { logger.debug("HTTP 304 Not Modified: File on server has not been modified since last download."); return true; } final long precedentAmount = downloadAmount; downloadAmount += fos.getChannel().transferFrom(rbc,downloadAmount,1 << 20); //~1MB try (ReadableByteChannel rbc = Channels.newChannel(httpConnection.getInputStream()); FileOutputStream fos = new FileOutputStream(localFilePath, previouslyDownloadedAmount > 0)) { if(precedentAmount == downloadAmount){ //it means nothing had been downloaded logger.warn("precedent amount = downloaded amount"); downloaded = false; rbc.close(); connect.disconnect(); }else{ timeoutRunnable.amountIncreased(); //delay the timeout totalSize = httpConnection.getContentLengthLong() + previouslyDownloadedAmount; final long totalSizeFinal = totalSize; updateProgress(downloadAmount, fullFileSize); updateMessage( formatFileSize(downloadAmount, unitIndex)+" / "+formattedFileSize); updateProgress(previouslyDownloadedAmount, totalSizeFinal); fos.flush(); downloaded = (downloadAmount == fullFileSize); } } //end of download, stop the timeout thread timeoutRunnable.stop(); long read; long position = previouslyDownloadedAmount; long startTime = System.currentTimeMillis(); long lastUpdateTime = startTime; final int bufferSize = 16 * 1024; ByteBuffer buffer = ByteBuffer.allocate(bufferSize); while ((read = rbc.read(buffer)) > 0) { buffer.flip(); while (buffer.hasRemaining()) { fos.write(buffer.get()); } buffer.clear(); if(downloaded) lmdFile.delete(); //Download complete, so we could remove this file return (!isCancelled() && downloaded); } position += read; /** * Get the download file unit index (mb, gb, ...) (1,2...) * @param value the file size * @return the index */ private final int getDownloadUnit(final double value){ double size = 0; for (int i = 0; i < CST_SIZE.length; i++) { size=value/CST_SIZE[i]; if (size <= 1024) { return i; long currentTime = System.currentTimeMillis(); if (currentTime - lastUpdateTime >= 1000) { updateProgress(position, totalSizeFinal); lastUpdateTime = currentTime; } if (isCancelled()) { fos.close(); return false; } return -1; } /** * Format file size to use correct size name (mb, gb, ...) * @todo definitively should be in the UI * @param value the file size * @param unitIndex info about unit * @return */ private final String formatFileSize(final double value, final int unitIndex){ return CST_FORMAT[unitIndex].format(value/CST_SIZE[unitIndex]) + " " + CST_UNITS[unitIndex] ; } updateProgress(position, totalSizeFinal); writeLastModified(lmdFile, httpConnection.getLastModified()); //Method about file checking /** * read the content of the checksum file * @param fileChecksum file containing checksum * @return null if no content. else a line in following format: checksum relativefilePath */ private String readChecksumFile(String fileChecksum) throws IOException{ Scanner sc = new Scanner(new FileReader(fileChecksum)); if(sc.hasNextLine()){ return sc.nextLine(); return position == totalSizeFinal; } finally { if (httpConnection != null) { httpConnection.disconnect(); } } return null; } /** * Verify the integrity of the downloaded file * source: http://www.sha1-online.com/sha256-java/ * @param checksumFilePath path of the checksum file * @return true if integrity has been validated * @throws NoSuchAlgorithmException * @throws IOException */ private boolean validChecksum( String checksumFilePath) throws NoSuchAlgorithmException, IOException{ logger.debug("validChecksum("+checksumFilePath+")"); //get file containing checksum private boolean validChecksum(String checksumFilePath) { try { File checksumFile = new File(checksumFilePath); if (!checksumFile.exists()) { logger.debug("checksum file doesn't exist"); return false; //If checksum file doesn't exist we can't validate checksum updateMessage(i18n.getString("download_lbl_checksumFileNotFound")); return false; } //read content of file containing checksum to extract hash and filename logger.debug("Checksum file path: " + checksumFilePath); String checksumLine = readChecksumFile(checksumFilePath); if (checksumLine == null) return false; logger.debug(" ChecksumLine = "+checksumLine); String[] splittedLine = checksumLine.split("\\s+"); //@todo use pattern & matcher //check local file exist File file = new File(AppConstants.getSourcesFolderPath()+splittedLine[1]); if(!file.exists()){ //if file concerned by checksum doesn't exist we can't validate updateMessage(i18n.getString("download_lbl_localFileNotFound")); //@todo not sure it is required... logger.debug(" "+splittedLine[1]+" do not exists"); return false; } updateProgress(-1,1); //@todo should be call elsewhere. probably in Controller String computedChecksum = createFileChecksum(file); String[] splittedLine = checksumLine.split(" "); String expectedChecksum = splittedLine[0]; String filenameInChecksum = splittedLine[1]; logger.debug("compare checksum: "+computedChecksum+" vs "+splittedLine[0]); return computedChecksum.equals(splittedLine[0]); File file = new File(AppConstants.getSourcesFolderPath() + filenameInChecksum); if (!file.exists()) { updateMessage(i18n.getString("download_lbl_localFileNotFound")); logger.debug("File does not exist: " + AppConstants.getSourcesFolderPath() + filenameInChecksum); return false; } /** * Compute checksum of the given file * @param file File for which we want the checksum * @return the checksum of the file * @throws NoSuchAlgorithmException * @throws IOException */ private String createFileChecksum(File file) throws NoSuchAlgorithmException, IOException{ logger.debug("createFileChecksum()"); MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); FileInputStream fis = new FileInputStream(file); byte[] data = new byte[1024]; int read = 0; MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] buffer = new byte[8192]; int read; while ((read = fis.read(data)) != -1) { sha256.update(data, 0, read); } byte[] hashBytes = sha256.digest(); try (FileInputStream fis = new FileInputStream(file)) { while ((read = fis.read(buffer)) > 0) { digest.update(buffer, 0, read); } } byte[] fileHash = digest.digest(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < hashBytes.length; i++) { sb.append(Integer.toString((hashBytes[i] & 0xff) + 0x100, 16).substring(1)); } return sb.toString(); for (byte b : fileHash) { sb.append(String.format("%02x", b)); } String calculatedChecksum = sb.toString(); logger.debug("Calculated checksum: " + calculatedChecksum); logger.debug("Expected checksum: " + expectedChecksum); /** * Private inner class used to set a timeout on File's download */ private class TimeOutRunnable implements Runnable{ final long timeout = 10000; //10secondes long currentTime; boolean stop = false; return calculatedChecksum.equals(expectedChecksum); } catch (IOException | NoSuchAlgorithmException e) { logger.error("Error verifying checksum: {}", e.getMessage()); } synchronized void stop(){ this.stop = true; return false; } @Override public void run() { currentTime = System.currentTimeMillis(); long previousTime; while(!stop){ previousTime = currentTime; //isCancelled() is a method of the containing DownloadTask.java if(Thread.interrupted() || isCancelled() ) stop = true; try{ Thread.sleep(timeout); if(!stop && currentTime == previousTime){ logger.info("No updates"); //updateProgress & updateMessage are methos of DownloadTask.java updateProgress(-1, 1); updateMessage(i18n.getString("download_lbl_connectionLost")); private String readChecksumFile(String checksumFilePath) { try (BufferedReader reader = new BufferedReader(new FileReader(checksumFilePath))) { return reader.readLine(); } catch (IOException e) { logger.error("Error reading checksum file: {}", e.getMessage()); } }catch(Exception e){ stop = true; logger.error("TimeoutThread crashed: "+e.toString()); return null; } @Override protected void succeeded() { super.succeeded(); updateMessage(i18n.getString("download_lbl_completed")); } logger.debug("timeoutThread is over!"); @Override protected void cancelled() { super.cancelled(); updateMessage(i18n.getString("download_lbl_cancelled")); } //Signal that an amount was increased synchronized private void amountIncreased(){ currentTime = System.currentTimeMillis(); @Override protected void failed() { super.failed(); updateMessage(i18n.getString("download_lbl_failed")); } }; } Loading
src/main/java/ecorp/easy/installer/tasks/DownloadTask.java +154 −288 Original line number Diff line number Diff line Loading @@ -33,8 +33,8 @@ import java.net.URL; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.DecimalFormat; Loading @@ -45,6 +45,7 @@ import java.util.Date; import java.util.Locale; import java.util.ResourceBundle; import java.util.Scanner; import java.nio.ByteBuffer; import javafx.concurrent.Task; import org.slf4j.Logger; import org.slf4j.LoggerFactory; Loading @@ -54,23 +55,15 @@ import org.json.JSONObject; import org.json.JSONException; /** * this class verify the checksum of a file and download it * @author vincent Bourgmayer * @author Ingo * This class verifies the checksum of a file and downloads it. * Author: Vincent Bourgmayer, Ingo */ public class DownloadTask extends Task<Boolean> { private final static String checkSumExtension = ".sha256sum"; private final static Logger logger = LoggerFactory.getLogger(DownloadTask.class); /** * Constant size */ private static final long[] CST_SIZE = {1, 1024, 1024*1024, 1024*1024*1024, 1024*1024*1024*1024}; /** * Constants units */ private static final String[] CST_UNITS = {"B", "KB", "MB", "GB", "TB"}; private static final DecimalFormat[] CST_FORMAT = { new DecimalFormat("#0"), new DecimalFormat("##0"), Loading @@ -79,32 +72,20 @@ public class DownloadTask extends Task<Boolean>{ new DecimalFormat("#,##0.000") }; final private ResourceBundle i18n; final private String targetUrl; private final ResourceBundle i18n; private final String targetUrl; private String fileName; /** * COnstruction of the download task * @param targetUrl the web path to the resource * @param fileName name of the file * @param resources used to send already translated message */ public DownloadTask(String targetUrl, String fileName, ResourceBundle resources) { this.targetUrl = targetUrl; this.fileName = fileName; this.i18n = resources; } /** * @inheritDoc * @return Boolean object * @throws Exception */ @Override protected Boolean call() throws Exception { final String latestBuildFilename = fetchLatestBuildFilename(targetUrl); final String localFilePath = AppConstants.getSourcesFolderPath() + fileName; final String checksumFilePath = localFilePath + checkSumExtension; if (isCancelled()) return false; Loading Loading @@ -151,33 +132,14 @@ public class DownloadTask extends Task<Boolean>{ } } // method link to file downloading /** * Read lastmodified date of the remote file at previous download of the file * Only uncompletly downloaded files are concerned * @param lmdFile * @return * @throws FileNotFoundException * @throws IOException */ private String readLastModifiedFrom(File lmdFile) throws FileNotFoundException, IOException{ String line; private String readLastModifiedFrom(File lmdFile) throws IOException { try (BufferedReader reader = new BufferedReader(new FileReader(lmdFile))) { line = reader.readLine(); return reader.readLine(); } return line; } /** * Write the last modified of the remote file for future use * @param lmdFile * @param timestamp * @throws IOException */ private void writeLastModified(File lmdFile, long timestamp) throws IOException { try (FileWriter fileWriter = new FileWriter(lmdFile, false)) { String lmd = DateTimeFormatter.RFC_1123_DATE_TIME .withZone(ZoneId.of("GMT")) .withLocale(Locale.ENGLISH) Loading @@ -187,7 +149,6 @@ public class DownloadTask extends Task<Boolean>{ } private String fetchLatestBuildFilename(String archiveUrl) { // Extract codeName and codeType from the archiveUrl Pair<String, String> codeInfo = getCodeNameFromUrl(archiveUrl); if (codeInfo == null) { logger.error("Failed to fetch latest build filename: codeName or codeType is null."); Loading @@ -197,7 +158,6 @@ public class DownloadTask extends Task<Boolean>{ String codeName = codeInfo.getKey(); String codeType = codeInfo.getValue(); // Construct API URL using codeName and codeType HttpURLConnection apiConnection = getApiConnection(codeName, codeType); if (apiConnection == null) { logger.error("Failed to fetch latest build filename: API connection is null."); Loading @@ -205,8 +165,7 @@ public class DownloadTask extends Task<Boolean>{ } try { String latestBuildFilename = processApiResponse(apiConnection); return latestBuildFilename; return processApiResponse(apiConnection); } catch (IOException e) { logger.error("Error processing API response: {}", e.getMessage()); return null; Loading Loading @@ -262,15 +221,11 @@ public class DownloadTask extends Task<Boolean>{ if (responseCode == HttpURLConnection.HTTP_OK) { String responseString = inputStreamToString(apiConnection.getInputStream()); // Try parsing the response as JSON new JSONObject(responseString); JSONObject responseJson = new JSONObject(responseString); JSONArray responses = responseJson.getJSONArray("response"); JSONObject responseObject = responses.getJSONObject(0); String filename = responseObject.getString("filename"); return filename; return responseObject.getString("filename"); } else { logger.error("Error fetching latest build filename: HTTP response code {}", responseCode); } Loading Loading @@ -300,249 +255,160 @@ public class DownloadTask extends Task<Boolean>{ return responseString.toString(); } /** * Perform the downloading of the specified file * If a part is already downloaded, then the download resume from previous state * @return boolean true if file has been successfully downloaded, false either */ private boolean downloadFile(final String fileURL, final String localFilePath, File lmdFile) throws MalformedURLException, IOException, InterruptedException{ private boolean downloadFile(final String fileURL, final String localFilePath, File lmdFile) throws IOException, InterruptedException { logger.debug("downloadFile({}, {})", fileURL, localFilePath); long previouslyDownloadedAmount = 0; long totalSize = 0; long lastModified = -1; //Build the query HttpURLConnection connect = (HttpURLConnection) new URL(fileURL).openConnection(); connect.setReadTimeout(30000); connect.setConnectTimeout(30000); File localFile = new File(localFilePath); if(localFile.exists()){ previouslyDownloadedAmount = localFile.length(); logger.debug("local file exist, size is {}", localFile.length()); String lmd = readLastModifiedFrom(lmdFile); String lastModifiedDate = (lmd != null) ? lmd : new Date(localFile.lastModified() ).toString(); logger.debug("last modified date = {}", lastModifiedDate); connect.setRequestProperty("If-Range", lastModifiedDate ); connect.setRequestProperty("Range", "bytes=" + previouslyDownloadedAmount + "-"); } //Perform the query and analyze result final int responseCode = connect.getResponseCode(); final boolean canAppendBytes = (responseCode == HttpURLConnection.HTTP_PARTIAL); logger.debug("response code: {}, {}", connect.getResponseCode(), connect.getResponseMessage()); final long lastModified = connect.getLastModified(); if( !canAppendBytes ){ if( responseCode == HttpURLConnection.HTTP_OK ) { //return false it resources is unreachable writeLastModified(lmdFile, lastModified); }else{ return false; try { lastModified = Long.parseLong(readLastModifiedFrom(lmdFile)); File existingFile = new File(localFilePath); if (existingFile.exists()) { previouslyDownloadedAmount = existingFile.length(); } previouslyDownloadedAmount = 0; //set it back to 0 in case it contains size of old content } catch (IOException e) { logger.warn("Unable to read last modified date from lmd file: {}", e.getMessage()); } catch (NumberFormatException e) { logger.warn("Invalid last modified date format in lmd file: {}", e.getMessage()); } //Get remote file Size final double fileSize = connect.getContentLengthLong(); logger.debug("remote fileSize = {}", fileSize); final double fullFileSize = fileSize+previouslyDownloadedAmount; logger.debug("full file size = {}", fullFileSize); HttpURLConnection httpConnection = (HttpURLConnection) new URL(fileURL).openConnection(); httpConnection.setRequestProperty("Range", "bytes=" + previouslyDownloadedAmount + "-"); //Update UI final int unitIndex = getDownloadUnit(fullFileSize); final String formattedFileSize = formatFileSize(fullFileSize, unitIndex); //used for UI updateProgress(-1, fullFileSize); updateMessage(formatFileSize(previouslyDownloadedAmount, unitIndex)+" / "+formattedFileSize ); boolean downloaded = false; try(FileOutputStream fos = new FileOutputStream(localFilePath,canAppendBytes); InputStream is = connect.getInputStream(); ReadableByteChannel rbc = Channels.newChannel(connect.getInputStream()); ){ //Start the timeOutThread which will stop a blocked download TimeOutRunnable timeoutRunnable = new TimeOutRunnable(); Thread timeoutThread = new Thread(timeoutRunnable); timeoutThread.setDaemon(true); timeoutThread.start(); long downloadAmount = previouslyDownloadedAmount; if (lastModified > 0) { httpConnection.setIfModifiedSince(lastModified); } while ( rbc.isOpen() && !isCancelled() && ! downloaded ){ if (httpConnection.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) { logger.debug("HTTP 304 Not Modified: File on server has not been modified since last download."); return true; } final long precedentAmount = downloadAmount; downloadAmount += fos.getChannel().transferFrom(rbc,downloadAmount,1 << 20); //~1MB try (ReadableByteChannel rbc = Channels.newChannel(httpConnection.getInputStream()); FileOutputStream fos = new FileOutputStream(localFilePath, previouslyDownloadedAmount > 0)) { if(precedentAmount == downloadAmount){ //it means nothing had been downloaded logger.warn("precedent amount = downloaded amount"); downloaded = false; rbc.close(); connect.disconnect(); }else{ timeoutRunnable.amountIncreased(); //delay the timeout totalSize = httpConnection.getContentLengthLong() + previouslyDownloadedAmount; final long totalSizeFinal = totalSize; updateProgress(downloadAmount, fullFileSize); updateMessage( formatFileSize(downloadAmount, unitIndex)+" / "+formattedFileSize); updateProgress(previouslyDownloadedAmount, totalSizeFinal); fos.flush(); downloaded = (downloadAmount == fullFileSize); } } //end of download, stop the timeout thread timeoutRunnable.stop(); long read; long position = previouslyDownloadedAmount; long startTime = System.currentTimeMillis(); long lastUpdateTime = startTime; final int bufferSize = 16 * 1024; ByteBuffer buffer = ByteBuffer.allocate(bufferSize); while ((read = rbc.read(buffer)) > 0) { buffer.flip(); while (buffer.hasRemaining()) { fos.write(buffer.get()); } buffer.clear(); if(downloaded) lmdFile.delete(); //Download complete, so we could remove this file return (!isCancelled() && downloaded); } position += read; /** * Get the download file unit index (mb, gb, ...) (1,2...) * @param value the file size * @return the index */ private final int getDownloadUnit(final double value){ double size = 0; for (int i = 0; i < CST_SIZE.length; i++) { size=value/CST_SIZE[i]; if (size <= 1024) { return i; long currentTime = System.currentTimeMillis(); if (currentTime - lastUpdateTime >= 1000) { updateProgress(position, totalSizeFinal); lastUpdateTime = currentTime; } if (isCancelled()) { fos.close(); return false; } return -1; } /** * Format file size to use correct size name (mb, gb, ...) * @todo definitively should be in the UI * @param value the file size * @param unitIndex info about unit * @return */ private final String formatFileSize(final double value, final int unitIndex){ return CST_FORMAT[unitIndex].format(value/CST_SIZE[unitIndex]) + " " + CST_UNITS[unitIndex] ; } updateProgress(position, totalSizeFinal); writeLastModified(lmdFile, httpConnection.getLastModified()); //Method about file checking /** * read the content of the checksum file * @param fileChecksum file containing checksum * @return null if no content. else a line in following format: checksum relativefilePath */ private String readChecksumFile(String fileChecksum) throws IOException{ Scanner sc = new Scanner(new FileReader(fileChecksum)); if(sc.hasNextLine()){ return sc.nextLine(); return position == totalSizeFinal; } finally { if (httpConnection != null) { httpConnection.disconnect(); } } return null; } /** * Verify the integrity of the downloaded file * source: http://www.sha1-online.com/sha256-java/ * @param checksumFilePath path of the checksum file * @return true if integrity has been validated * @throws NoSuchAlgorithmException * @throws IOException */ private boolean validChecksum( String checksumFilePath) throws NoSuchAlgorithmException, IOException{ logger.debug("validChecksum("+checksumFilePath+")"); //get file containing checksum private boolean validChecksum(String checksumFilePath) { try { File checksumFile = new File(checksumFilePath); if (!checksumFile.exists()) { logger.debug("checksum file doesn't exist"); return false; //If checksum file doesn't exist we can't validate checksum updateMessage(i18n.getString("download_lbl_checksumFileNotFound")); return false; } //read content of file containing checksum to extract hash and filename logger.debug("Checksum file path: " + checksumFilePath); String checksumLine = readChecksumFile(checksumFilePath); if (checksumLine == null) return false; logger.debug(" ChecksumLine = "+checksumLine); String[] splittedLine = checksumLine.split("\\s+"); //@todo use pattern & matcher //check local file exist File file = new File(AppConstants.getSourcesFolderPath()+splittedLine[1]); if(!file.exists()){ //if file concerned by checksum doesn't exist we can't validate updateMessage(i18n.getString("download_lbl_localFileNotFound")); //@todo not sure it is required... logger.debug(" "+splittedLine[1]+" do not exists"); return false; } updateProgress(-1,1); //@todo should be call elsewhere. probably in Controller String computedChecksum = createFileChecksum(file); String[] splittedLine = checksumLine.split(" "); String expectedChecksum = splittedLine[0]; String filenameInChecksum = splittedLine[1]; logger.debug("compare checksum: "+computedChecksum+" vs "+splittedLine[0]); return computedChecksum.equals(splittedLine[0]); File file = new File(AppConstants.getSourcesFolderPath() + filenameInChecksum); if (!file.exists()) { updateMessage(i18n.getString("download_lbl_localFileNotFound")); logger.debug("File does not exist: " + AppConstants.getSourcesFolderPath() + filenameInChecksum); return false; } /** * Compute checksum of the given file * @param file File for which we want the checksum * @return the checksum of the file * @throws NoSuchAlgorithmException * @throws IOException */ private String createFileChecksum(File file) throws NoSuchAlgorithmException, IOException{ logger.debug("createFileChecksum()"); MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); FileInputStream fis = new FileInputStream(file); byte[] data = new byte[1024]; int read = 0; MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] buffer = new byte[8192]; int read; while ((read = fis.read(data)) != -1) { sha256.update(data, 0, read); } byte[] hashBytes = sha256.digest(); try (FileInputStream fis = new FileInputStream(file)) { while ((read = fis.read(buffer)) > 0) { digest.update(buffer, 0, read); } } byte[] fileHash = digest.digest(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < hashBytes.length; i++) { sb.append(Integer.toString((hashBytes[i] & 0xff) + 0x100, 16).substring(1)); } return sb.toString(); for (byte b : fileHash) { sb.append(String.format("%02x", b)); } String calculatedChecksum = sb.toString(); logger.debug("Calculated checksum: " + calculatedChecksum); logger.debug("Expected checksum: " + expectedChecksum); /** * Private inner class used to set a timeout on File's download */ private class TimeOutRunnable implements Runnable{ final long timeout = 10000; //10secondes long currentTime; boolean stop = false; return calculatedChecksum.equals(expectedChecksum); } catch (IOException | NoSuchAlgorithmException e) { logger.error("Error verifying checksum: {}", e.getMessage()); } synchronized void stop(){ this.stop = true; return false; } @Override public void run() { currentTime = System.currentTimeMillis(); long previousTime; while(!stop){ previousTime = currentTime; //isCancelled() is a method of the containing DownloadTask.java if(Thread.interrupted() || isCancelled() ) stop = true; try{ Thread.sleep(timeout); if(!stop && currentTime == previousTime){ logger.info("No updates"); //updateProgress & updateMessage are methos of DownloadTask.java updateProgress(-1, 1); updateMessage(i18n.getString("download_lbl_connectionLost")); private String readChecksumFile(String checksumFilePath) { try (BufferedReader reader = new BufferedReader(new FileReader(checksumFilePath))) { return reader.readLine(); } catch (IOException e) { logger.error("Error reading checksum file: {}", e.getMessage()); } }catch(Exception e){ stop = true; logger.error("TimeoutThread crashed: "+e.toString()); return null; } @Override protected void succeeded() { super.succeeded(); updateMessage(i18n.getString("download_lbl_completed")); } logger.debug("timeoutThread is over!"); @Override protected void cancelled() { super.cancelled(); updateMessage(i18n.getString("download_lbl_cancelled")); } //Signal that an amount was increased synchronized private void amountIncreased(){ currentTime = System.currentTimeMillis(); @Override protected void failed() { super.failed(); updateMessage(i18n.getString("download_lbl_failed")); } }; }