Files
sheepit-shadow-nabber/src/main/java/com/sheepit/client/network/DownloadManager.java

194 lines
7.0 KiB
Java
Raw Normal View History

/*
* Copyright (C) 2023 Laurent CLOUET
* Author Laurent CLOUET <laurent.clouet@nopnop.net>
*
* 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.
*/
2024-12-14 13:54:56 +00:00
package com.sheepit.client.network;
2024-12-14 13:54:56 +00:00
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
2024-12-14 13:54:56 +00:00
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
2024-12-14 13:54:56 +00:00
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
2024-12-14 13:54:56 +00:00
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) {
2024-06-01 16:02:42 +02:00
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";
2024-12-14 13:54:56 +00:00
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();
}
}