Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Verified Commit dfb31970 authored by Ahmed Harhash's avatar Ahmed Harhash
Browse files

easy-installer: Refactor DownloadTask

parent fdfa2c81
Loading
Loading
Loading
Loading
+154 −288
Original line number Diff line number Diff line
@@ -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,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"),
@@ -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;
@@ -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)
@@ -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.");
@@ -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.");
@@ -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;
@@ -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);
            }
@@ -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"));
    }
    };
}