2023-12-07 12:27:53 +00:00
/ *
* 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 ;
2023-12-07 12:27:53 +00:00
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 ;
2023-12-07 12:27:53 +00:00
import com.sheepit.client.exception.SheepItException ;
import java.io.File ;
import java.io.IOException ;
import java.io.PrintWriter ;
import java.io.StringWriter ;
2025-01-09 16:13:04 +00:00
import java.nio.file.Files ;
import java.nio.file.Path ;
2023-12-07 12:27:53 +00:00
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 ;
2023-12-07 12:27:53 +00:00
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 ;
2023-12-07 12:27:53 +00:00
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 ( ) ) {
2024-04-11 15:24:50 +00:00
this . gui . status ( " Reusing cached " ) ;
2023-12-07 12:27:53 +00:00
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 ) {
2024-04-11 15:24:50 +00:00
this . gui . status ( String . format ( " Another client is downloading. Cancel in %dmin %ds " ,
2023-12-07 12:27:53 +00:00
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 ( ) ;
}
}
2024-04-11 15:24:50 +00:00
this . gui . status ( String . format ( " Downloading " ) ) ;
2023-12-07 12:27:53 +00:00
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 ) ;
2023-12-07 12:27:53 +00:00
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 ) {
2024-04-11 15:24:50 +00:00
this . gui . error ( String . format ( " Unable to download (error %s). Retrying now " , ret ) ) ;
2023-12-07 12:27:53 +00:00
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 " ) ;
2023-12-07 12:27:53 +00:00
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 ) ;
2023-12-07 12:27:53 +00:00
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 ( ) ) ;
2025-01-09 16:13:04 +00:00
String current_path = this . local_target ;
String test_path ;
while ( ( test_path = ( new File ( current_path ) . getParent ( ) ) ) ! = null ) {
this . log . error ( " DownloadManager::createLock checking path info " + test_path + " exists? " + ( new File ( test_path ) . exists ( ) ? " yes " : " no " ) + " is writeable? " + ( Files . isWritable ( Path . of ( test_path ) ) ? " yes " : " no " ) ) ;
current_path = test_path ;
}
2023-12-07 12:27:53 +00:00
}
}
private void removeLock ( ) {
new File ( this . local_target + " .partial " ) . delete ( ) ;
}
}