From dfb319704780fbfb1829d9549a5652a0808d6105 Mon Sep 17 00:00:00 2001 From: Ahmed Harhash Date: Mon, 20 May 2024 12:05:45 +0300 Subject: [PATCH] easy-installer: Refactor DownloadTask --- .../easy/installer/tasks/DownloadTask.java | 442 ++++++------------ 1 file changed, 154 insertions(+), 288 deletions(-) diff --git a/src/main/java/ecorp/easy/installer/tasks/DownloadTask.java b/src/main/java/ecorp/easy/installer/tasks/DownloadTask.java index 5e26d1eb..037867be 100644 --- a/src/main/java/ecorp/easy/installer/tasks/DownloadTask.java +++ b/src/main/java/ecorp/easy/installer/tasks/DownloadTask.java @@ -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; @@ -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; @@ -54,57 +55,37 @@ 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{ +public class DownloadTask extends Task { 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"), - new DecimalFormat("#,##0"), - new DecimalFormat("#,##0.00"), - new DecimalFormat("#,##0.000") + new DecimalFormat("#0"), + new DecimalFormat("##0"), + new DecimalFormat("#,##0"), + new DecimalFormat("#,##0.00"), + 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){ + 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; @@ -151,33 +132,14 @@ public class DownloadTask extends Task{ } } - // 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)) { - + + 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) @@ -187,7 +149,6 @@ public class DownloadTask extends Task{ } private String fetchLatestBuildFilename(String archiveUrl) { - // Extract codeName and codeType from the archiveUrl Pair codeInfo = getCodeNameFromUrl(archiveUrl); if (codeInfo == null) { logger.error("Failed to fetch latest build filename: codeName or codeType is null."); @@ -197,7 +158,6 @@ public class DownloadTask extends Task{ 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."); @@ -205,8 +165,7 @@ public class DownloadTask extends Task{ } try { - String latestBuildFilename = processApiResponse(apiConnection); - return latestBuildFilename; + return processApiResponse(apiConnection); } catch (IOException e) { logger.error("Error processing API response: {}", e.getMessage()); return null; @@ -262,15 +221,11 @@ public class DownloadTask extends Task{ 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); } @@ -300,249 +255,160 @@ public class DownloadTask extends Task{ 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{ - logger.debug("downloadFile({}, {})",fileURL,localFilePath ); + 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); - //Update UI - final int unitIndex = getDownloadUnit(fullFileSize); - final String formattedFileSize = formatFileSize(fullFileSize, unitIndex); //used for UI - updateProgress(-1, fullFileSize); - updateMessage(formatFileSize(previouslyDownloadedAmount, unitIndex)+" / "+formattedFileSize ); + HttpURLConnection httpConnection = (HttpURLConnection) new URL(fileURL).openConnection(); + httpConnection.setRequestProperty("Range", "bytes=" + previouslyDownloadedAmount + "-"); - boolean downloaded = false; + if (lastModified > 0) { + httpConnection.setIfModifiedSince(lastModified); + } - 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(); + 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; + } + + try (ReadableByteChannel rbc = Channels.newChannel(httpConnection.getInputStream()); + FileOutputStream fos = new FileOutputStream(localFilePath, previouslyDownloadedAmount > 0)) { + + totalSize = httpConnection.getContentLengthLong() + previouslyDownloadedAmount; + final long totalSizeFinal = totalSize; - long downloadAmount = previouslyDownloadedAmount; + updateProgress(previouslyDownloadedAmount, totalSizeFinal); - while ( rbc.isOpen() && !isCancelled() && ! downloaded ){ - - final long precedentAmount = downloadAmount; - downloadAmount += fos.getChannel().transferFrom(rbc,downloadAmount,1 << 20); //~1MB - - 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 - - updateProgress(downloadAmount, fullFileSize); - updateMessage( formatFileSize(downloadAmount, unitIndex)+" / "+formattedFileSize); - - fos.flush(); - downloaded = (downloadAmount == fullFileSize); + 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(); + + position += read; + + long currentTime = System.currentTimeMillis(); + if (currentTime - lastUpdateTime >= 1000) { + updateProgress(position, totalSizeFinal); + lastUpdateTime = currentTime; + } + + if (isCancelled()) { + fos.close(); + return false; } } - //end of download, stop the timeout thread - timeoutRunnable.stop(); - } - - if(downloaded) lmdFile.delete(); //Download complete, so we could remove this file - return (!isCancelled() && downloaded); - } - - /** - * 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; - } + + updateProgress(position, totalSizeFinal); + + writeLastModified(lmdFile, httpConnection.getLastModified()); + + return position == totalSizeFinal; + } finally { + if (httpConnection != null) { + httpConnection.disconnect(); + } } - 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] ; } - - //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 null; - } + private boolean validChecksum(String checksumFilePath) { + try { + File checksumFile = new File(checksumFilePath); + if (!checksumFile.exists()) { + updateMessage(i18n.getString("download_lbl_checksumFileNotFound")); + return false; + } + + logger.debug("Checksum file path: " + checksumFilePath); + + String checksumLine = readChecksumFile(checksumFilePath); + if (checksumLine == null) return false; - /** - * 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 - 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 - } - - //read content of file containing checksum to extract hash and filename - 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; + String[] splittedLine = checksumLine.split(" "); + String expectedChecksum = splittedLine[0]; + String filenameInChecksum = splittedLine[1]; + + 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; + } + + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] buffer = new byte[8192]; + int read; + + 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 (byte b : fileHash) { + sb.append(String.format("%02x", b)); + } + String calculatedChecksum = sb.toString(); + + logger.debug("Calculated checksum: " + calculatedChecksum); + logger.debug("Expected checksum: " + expectedChecksum); + + return calculatedChecksum.equals(expectedChecksum); + } catch (IOException | NoSuchAlgorithmException e) { + logger.error("Error verifying checksum: {}", e.getMessage()); } - updateProgress(-1,1); //@todo should be call elsewhere. probably in Controller - String computedChecksum = createFileChecksum(file); - - logger.debug("compare checksum: "+computedChecksum+" vs "+splittedLine[0]); - return computedChecksum.equals(splittedLine[0]); + 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; - - while ((read = fis.read(data)) != -1) { sha256.update(data, 0, read); } - byte[] hashBytes = sha256.digest(); - - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < hashBytes.length; i++) { - sb.append(Integer.toString((hashBytes[i] & 0xff) + 0x100, 16).substring(1)); + 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()); } - return sb.toString(); + return null; } - - - /** - * 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; + @Override + protected void succeeded() { + super.succeeded(); + updateMessage(i18n.getString("download_lbl_completed")); + } - synchronized void stop(){ - this.stop = true; - } + @Override + protected void cancelled() { + super.cancelled(); + updateMessage(i18n.getString("download_lbl_cancelled")); + } - @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")); - } - }catch(Exception e){ - stop = true; - logger.error("TimeoutThread crashed: "+e.toString()); - } - } - logger.debug("timeoutThread is over!"); - } - //Signal that an amount was increased - synchronized private void amountIncreased(){ - currentTime = System.currentTimeMillis(); - } - }; -} \ No newline at end of file + @Override + protected void failed() { + super.failed(); + updateMessage(i18n.getString("download_lbl_failed")); + } +} -- GitLab