/* * Copyright (C) 2023 Laurent CLOUET * Author Laurent CLOUET * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package com.sheepit.client.network; import com.sheepit.client.datamodel.client.Error; import com.sheepit.client.ui.Gui; import com.sheepit.client.logger.Log; import com.sheepit.client.utils.Utils; import com.sheepit.client.exception.SheepItException; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.util.Random; import java.util.concurrent.TimeUnit; public class DownloadManager { private static int maxDownloadFileAttempts = 5; // global objects private ServerRequest serverRequest; private Gui gui; private Log log; // task specific objects private String local_target; private String md5; // expected md5 of the file, for check purpose private String remote; // remote url public DownloadManager(ServerRequest serverRequest, Gui gui, Log log, String local_target, String md5, String remote) { this.serverRequest = serverRequest; this.gui = gui; this.log = log; this.local_target = local_target; this.md5 = md5; this.remote = remote; } public Error.Type download() throws SheepItException { File local_path_file = new File(this.local_target); int remaining = 1800000; // 30 minutes max timeout try { // For a maximum of 30 minutes do { // if the binary or scene already exists in the cache if (local_path_file.exists()) { this.gui.status("Reusing cached"); return Error.Type.OK; } // if the binary or scene is being downloaded by another client else if (this.lockExists()) { // Wait and check every second for file download completion but only update the GUI every 10 seconds to minimise CPU load if (remaining % 10000 == 0) { this.gui.status(String.format("Another client is downloading. Cancel in %dmin %ds", TimeUnit.MILLISECONDS.toMinutes(remaining), TimeUnit.MILLISECONDS.toSeconds(remaining) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(remaining)) )); } } else { // The file doesn't yet exist not is being downloaded by another client, so immediately create the file with zero bytes to allow early // detection by other concurrent clients and start downloading process this.createLock(); break; } // wait about 1 second on average int wait = 1 + (new Random()).nextInt(2000); Thread.sleep(wait); remaining -= wait; } while (remaining > 0); } catch (InterruptedException e) { log.debug("Error in the thread wait. Exception " + e.getMessage()); } finally { // If we have reached the timeout (30 minutes trying to download the client) delete the partial downloaded copy and try to download again if (remaining <= 0) { log.debug("ERROR while waiting for download to finish in another client. Deleting the partial file and downloading a fresh copy now!."); this.removeLock(); } } this.gui.status(String.format("Downloading")); return this.downloadActual(); } private Error.Type downloadActual() throws SheepItException { // must download the archive Error.Type ret = this.serverRequest.HTTPGetFile(this.remote, this.local_target, this.gui); if (ret == Error.Type.RENDERER_KILLED_BY_SERVER || ret == Error.Type.RENDERER_KILLED_BY_USER_OVER_TIME || ret == Error.Type.RENDERER_KILLED_BY_USER) { return ret; } // Try to check the download file even if a download error has occurred (MD5 file check will delete the file if partially downloaded) boolean md5_check = this.check(); int attempts = 1; while ((ret != Error.Type.OK || md5_check == false) && attempts < this.maxDownloadFileAttempts) { if (ret != Error.Type.OK) { this.gui.error(String.format("Unable to download (error %s). Retrying now", ret)); this.log.debug("DownloadManager::downloadActual problem with Server.HTTPGetFile (return: " + ret + ") removing local file (path: " + this.local_target + ")"); } else if (md5_check == false) { this.gui.error("Verification of downloaded file has failed. Retrying now"); this.log.debug("DownloadManager::downloadActual problem with Client::checkFile mismatch on md5, removing local file (path: " + this.local_target + ")"); } (new File(this.local_target)).delete(); this.log.debug("DownloadManager::downloadActual failed, let's try again (" + (attempts + 1) + "/" + this.maxDownloadFileAttempts + ") ..."); String partial_target = this.local_target + ".partial"; ret = this.serverRequest.HTTPGetFile(this.remote, partial_target, this.gui); md5_check = this.check(); attempts++; if ((ret != Error.Type.OK || md5_check == false) && attempts >= this.maxDownloadFileAttempts) { this.log.debug("DownloadManager::downloadActual failed after " + this.maxDownloadFileAttempts + " attempts, removing local file (path: " + this.local_target + "), stopping..."); // local_path_file.delete(); return Error.Type.DOWNLOAD_FILE; } else { return (new File (partial_target)).renameTo(new File(this.local_target)) ? Error.Type.OK : Error.Type.DOWNLOAD_FILE; } } return Error.Type.OK; } private boolean check() { File local_path_file = new File(this.local_target); if (local_path_file.exists() == false) { this.log.error("DownloadManager::check cannot check md5 on a nonexistent file (path: " + this.local_target + ")"); return false; } String md5_local = Utils.md5(this.local_target); if (md5_local.equals(this.md5) == false) { this.log.error("DownloadManager::check mismatch on md5 local: '" + md5_local + "' server: '" + this.md5 + "' (local size: " + new File(this.local_target).length() + ")"); return false; } return true; } private boolean lockExists() { return new File(this.local_target + ".partial").exists(); } private void createLock() { try { File file = new File(this.local_target + ".partial"); file.createNewFile(); file.deleteOnExit(); // if the client crashes, the temporary file will be removed } catch (IOException e) { StringWriter sw = new StringWriter(); e.printStackTrace(new PrintWriter(sw)); this.log.error("DownloadManager::createLock Unable to create .partial temp file for binary/scene " + this.local_target); this.log.error("DownloadManager::createLock Exception " + e + " stacktrace " + sw.toString()); } } private void removeLock() { new File(this.local_target + ".partial").delete(); } }