// Updater::updateModule [java]
	private void updateModule( final ModuleBean module, final boolean external, final boolean repair ) {
		LEnv.LOGGER.info( ( repair ? "Repairing " : "Updating " ) + module.getName() + "..." );
		launcherFrame.setStatus( StatusType.PROGRESS, ( repair ? "Repairing " : "Updating " ) + module.getName() );
		
		if ( module == modules.getLauncherMod() ) {
			// Launcher update requires restart, show RESTART action while updating the launcher
			launcherFrame.setProceedText( "<html><h2>RE_START</h2></html>" );
		}
		
		final Path tempPath = ( external ? LEnv.PATH_EXT_MODS : LEnv.PATH_MODS ).resolve( "_update" );
		try {
			// Create temp update folder
			while ( !LUtils.deletePath( tempPath ) )
				if ( !LGuiUtils.askRetry( "Could not delete folder:", tempPath ) )
					throw new Exception( "Could not delete folder: " + tempPath );
			Files.createDirectory( tempPath );
			
			final Path archivePath = tempPath.resolve( module.getArchiveFile().getPath() );
			
			// Download module archive, try mirrors if one fails
			final byte[] buffer = new byte[ 16_384 ]; // 16 KB work buffer
			for ( int urlIdx = 0; urlIdx < module.getUrlList().size(); urlIdx++ ) {
				final String archiveSource = urlIdx == 0 ? "main source" : " mirror #" + urlIdx;
				final boolean lastArchiveUrl = urlIdx == module.getUrlList().size() - 1;
				
				LEnv.LOGGER.debug( "Downloading archive from " + archiveSource + "..." );
				
				boolean downloadOk = false;
				InputStream input = null;
				OutputStream output = null;
				try {
					LEnv.LOGGER.debug( "Connecting..." );
					launcherFrame.setProgressMax( (int) module.getArchiveSize() );
					launcherFrame.setProgress( 0 );
					
					final URLConnection archiveUrlConnection = new URL( module.getUrlList().get( urlIdx ) ).openConnection();
					
					input = archiveUrlConnection.getInputStream();
					output = Files.newOutputStream( archivePath );
					
					LEnv.LOGGER.debug( "Downloading..." );
					int totalBytesRead = 0;
					int bytesRead;
					while ( ( bytesRead = input.read( buffer ) ) > 0 ) {
						output.write( buffer, 0, bytesRead );
						totalBytesRead += bytesRead;
						launcherFrame.setProgress( totalBytesRead );
					}
					output.flush();
					
					downloadOk = true;
					LEnv.LOGGER.debug( "Download complete." );
					
				} catch ( final Exception e ) {
					LEnv.LOGGER.warning(
					        "Failed to download archive from " + archiveSource + "!" + ( lastArchiveUrl ? "" : " Proceeding to the next source." ), e );
					if ( lastArchiveUrl )
						throw new Exception( "None of the archives are available!" );
				} finally {
					if ( input != null ) {
						try {
							input.close();
						} catch ( final IOException ie ) {
							// We're done, just ignore.
						}
						input = null;
					}
					if ( output != null ) {
						try {
							output.close();
						} catch ( final IOException ie ) {
							// We're done, just ignore.
						}
						output = null;
					}
				}
				
				if ( downloadOk ) {
					LEnv.LOGGER.debug( "Checking SHA-256 checksum of the archive..." );
					if ( module.getArchiveFile().getSha256().equals( LUtils.calculateFileSha256( archivePath ) ) ) {
						LEnv.LOGGER.debug( "SHA-256 checksum OK." );
						break; // Break archive URLs cycle
					} else {
						LEnv.LOGGER.debug( "SHA-256 checksum MISMATCH! The downloaded archive is discarded!" );
						while ( !LUtils.deletePath( archivePath ) )
							if ( !LGuiUtils.askRetry( "Could not delete file:", archivePath ) )
								throw new Exception( "Could not delete file: " + archivePath );
						if ( lastArchiveUrl )
							throw new Exception( "None of the archives are available!" );
						else
							LEnv.LOGGER.debug( "Proceeding to the next source." );
					}
				}
			}
			
			LEnv.LOGGER.debug( "Extracting archive..." );
			final InputStream ins = Files.newInputStream( archivePath ); // Input stream is "out-sourced" to a local var because
			                                                             // else a false resource leak is reported :S
			try ( final ZipInputStream zipInput = new ZipInputStream( ins ) ) {
				final String pathPrefix = external ? "Scelight/" + LEnv.PATH_EXT_MODS.getFileName().toString() + "/" + module.getFolder() + "/" : null;
				ZipEntry zipEntry;
				while ( ( zipEntry = zipInput.getNextEntry() ) != null ) {
					if ( external && !zipEntry.isDirectory() ) {
						// Quarantine check
						if ( zipEntry.getName().indexOf( ".." ) >= 0 || !zipEntry.getName().startsWith( pathPrefix ) )
							throw new Exception( "Invalid archive content, disallowed file entry: " + zipEntry.getName() );
					}
					final Path entryFile = tempPath.resolve( zipEntry.getName() );
					if ( zipEntry.isDirectory() )
						Files.createDirectories( entryFile );
					else {
						long size = zipEntry.getSize();
						try ( final OutputStream output = Files.newOutputStream( entryFile ) ) {
							while ( size > 0 ) {
								final int bytesRead = zipInput.read( buffer );
								output.write( buffer, 0, bytesRead );
								size -= bytesRead;
							}
							output.flush();
						}
					}
				}
			} catch ( final Exception e ) {
				throw new Exception( "Failed to extract archive!", e );
			}
			LEnv.LOGGER.debug( "Extracting done." );
			
			final Path archiveAppPath = tempPath.resolve( "Scelight" );
			if ( !Files.exists( archiveAppPath ) )
				throw new Exception( "The extracted archive does not seem to be a valid archive! Aborting " + ( repair ? "repair" : "update" ) + "!" );
			
			LEnv.LOGGER.debug( repair ? "Replacing/patching files..." : "Implanting files..." );
			Files.walkFileTree( archiveAppPath, new SimpleFileVisitor< Path >() {
				@Override
				public FileVisitResult preVisitDirectory( Path dir, BasicFileAttributes attrs ) throws IOException {
					dir = LEnv.PATH_APP.resolve( archiveAppPath.relativize( dir ) );
					attrs = null; // We changed dir, attrs do not apply to dir anymore, null it to avoid accidental use!
					
					while ( !Files.exists( dir ) || !Files.isDirectory( dir ) ) {
						try {
							// Files.createDirectories() does not always throw IOException if dir exists and is a file, do it
							// ourselves!
							if ( Files.exists( dir ) && !Files.isDirectory( dir ) )
								throw new IOException( "File exists and is not a folder: " + dir );
							Files.createDirectories( dir );
						} catch ( final IOException e ) {
							if ( !LGuiUtils.askRetry( "Could not create folder:", dir ) )
								throw new IOException( "Could not create folder: " + dir, e );
						}
					}
					return FileVisitResult.CONTINUE;
				}
				
				@Override
				public FileVisitResult visitFile( final Path file, final BasicFileAttributes attrs ) throws IOException {
					// Launcher's class path entries are locked and therefore cannot be repaired from "within"!
					// (But this is also not intended, in this case error and need of manual download is displayed to the user!)
					if ( repair && module == modules.getLauncherMod() && launcher.isClassPathEntry( file.getFileName().toString() ) )
						return FileVisitResult.CONTINUE;
					
					final Path target = LEnv.PATH_APP.resolve( archiveAppPath.relativize( file ) );
					while ( true ) {
						try {
							Files.copy( file, target, StandardCopyOption.REPLACE_EXISTING );
							return FileVisitResult.CONTINUE;
						} catch ( final IOException e ) {
							if ( !LGuiUtils.askRetry( "Could not write file:", target ) )
								throw new IOException( "Could not write file: " + target, e );
						}
					}
				}
			} );
			LEnv.LOGGER.debug( repair ? "Replace/patch complete." : "Implantation complete." );
			
		} catch ( final Exception e ) {
			LEnv.LOGGER.error( "Failed to " + ( repair ? "repair " : "update " ) + module.getName() + "!", e );
			launcherFrame.setStatus( StatusType.ERROR, "Failed to " + ( repair ? "repair " : "update " ) + module.getName() + "! See the Logs for details!" );
			throw new FinishException();
		} finally {
			LEnv.LOGGER.debug( "Cleaning up..." );
			while ( !LUtils.deletePath( tempPath ) ) {
				if ( !LGuiUtils.askRetry( "Could not delete folder:", tempPath ) ) {
					LEnv.LOGGER.error( "Could not delete folder: " + tempPath );
					launcherFrame.setStatus( StatusType.ERROR, "Failed to " + ( repair ? "repair " : "update " ) + module.getName()
					        + "! See the Logs for details!" );
					throw new FinishException();
				}
			}
			LEnv.LOGGER.debug( "Cleanup complete." );
		}
		
		// Module updated / repaired successfully.
		
		if ( module == modules.getLauncherMod() ) {
			// Launcher update requires restart
			LEnv.LOGGER.info( module.getName() + " has been " + ( repair ? "repaired" : "updated" ) + ", you must restart " + module.getName()
			        + " to continue." );
			launcherFrame.setStatus( StatusType.WARNING,
			        module.getName() + " has been " + ( repair ? "repaired" : "updated" ) + ", you must restart " + module.getName() + " to continue." );
			throw new FinishException( true );
		} else {
			LEnv.LOGGER.info( module.getName() + " has been " + ( repair ? "repaired." : "updated." ) );
			launcherFrame.setStatus( StatusType.PROGRESS, module.getName() + " has been " + ( repair ? "repaired." : "updated." ) );
		}
	}