I'm making a file downloader using Java. The project is on GitHub; I have included the most relevant excerpts here.
I separate the file into parts and download them using HTTP Range request, with each part being handled by a thread. All of the threads share an object, which is used to keep track of the progress and print the progress bar. The client is something like this:
public class DownloadThread implements Runnable {
// Constructors and helper methods go here
// ...
public void downloadToFile(HttpURLConnection conn) throws IOException {
//...
while (mDownloadedSize < contentLength) {
/*
* Code to get the data from the input stream goes here.
*/
// After getting the data, the thread updates the progress and
// notifies the other threads.
synchronized (mProgress) {
// mProgress is the object used to update the progress
// result is the number of bytes read from the input stream
mProgress.downloadedCount += result;
// ...
mProgress.updateProgressBar();
mProgress.notifyAll();
}
}
}
// ...
}
This is the main download class, which is used to manage the smaller download thread objects.
public class Download implements Runnable {
// Other functions go here.
// ...
@Override
public void run() {
try {
// Get the file name and create the URL object
String fileName = new File(mUrl).getName();
URL url = new URL(mUrl);
// Check the validity of the URL
HttpResult result = checkURLValidity(url);
long contentSize = result.contentLength;
int responseCode = result.responseCode;
if (contentSize == -1 || responseCode != 200) {
String errMessage = "Error while checking URL validity!";
errMessage += "\nResponse code: " + responseCode;
errMessage += "\nContent size: " + contentSize;
throw new RuntimeException(errMessage);
}
// Notify the progress object of the result of the check
synchronized (mProgress) {
mProgress.mURLVerifyResult.contentLength = contentSize;
mProgress.mURLVerifyResult.responseCode = responseCode;
mProgress.notifyAll();
}
// Start threads to download.
ArrayList<DownloadThread> downloadParts;
mProgress.startDownloadTimeStamp = Instant.now();
try {
downloadParts = startDownloadThreads(url, contentSize,
mPartsCount, mProgress);
} catch (RuntimeException ex) {
throw ex;
}
// Wait for the threads to finish downloading
for (int i = 0; i < downloadParts.size(); i++) {
DownloadThread currentThread = downloadParts.get(i);
currentThread.joinThread();
if (currentThread.getDownloadedSize() != currentThread.getPartSize()) {
throw new RuntimeException("Download incompleted at part "
+ (i + 1) + ": " + currentThread.getDownloadedSize());
}
}
// Notify that all parts have finished downloading
synchronized (mProgress) {
mProgress.downloadFinished = true;
mProgress.notifyAll();
}
// Join the mPartsCount together
joinDownloadedParts(fileName, downloadParts);
// Delete part files
try {
for (int i = 0; i < downloadParts.size(); i++) {
String partName = "." + fileName + ".part" + (i + 1);
Path filePath = Paths.get(partName);
Files.deleteIfExists(filePath);
}
} catch (IOException ex) {
// If failed to delete then just ignore the exception.
// What can we do?
}
// Notify that all parts have finished joining.
synchronized (mProgress) {
mProgress.joinPartsFinished = true;
mProgress.notifyAll();
}
} catch (RuntimeException | InterruptedException | IOException ex) {
// If an exception is thrown, put it in the progress object and
// notify the other threads.
synchronized (mProgress) {
mProgress.ex = ex;
mProgress.notifyAll();
}
}
}
}
The object used to keep track of the progress (mProgress
) also has a member ex
, which is used to store the exception thrown by any of the thread.
This is the main class:
public class Main {
public static String mURL;
private static final String PROGRAM_DIR = System.getenv("HOME")
+ "/.QTDownloader";
private static final String DOWNLOADED_LIST_FILENAME = PROGRAM_DIR
+ "/.filelist.csv";
/**
*
* @param args Array of arguments.
* @throws java.lang.InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
if (args.length == 0) {
printUsage(args);
System.exit(0);
}
// Read the arguments
HashMap<String, String> userOptions = new HashMap<>();
try {
userOptions = readArgumentOptions(args);
} catch (RuntimeException ex) {
printErrorMessage(ex);
}
mURL = args[args.length - 1]; // The url is the last argument.
int partsCount = 8; // Number of parts to divide to download.
// Get the list of downloaded files
HashMap<String, DownloadSession> downloadSessionList = null;
try {
downloadSessionList = getListOfDownloadedFiles();
} catch (IOException ex) {
printErrorMessage(ex);
}
// Check if the file has been downloaded or not
String fileName = new File(mURL).getName();
DownloadSession currentDownloadSession;
currentDownloadSession = checkIfFileWasDownloaded(downloadSessionList, fileName, mURL);
// If the file was downloaded before.
boolean downloaded = currentDownloadSession.alreadyDownloaded;
// If the last attempt to download the file was interrupted and
// the user chose to resume downloading.
userOptions.put("resume", currentDownloadSession.resumeDownload ? "y" : "n");
// If the user chooses to cancel downloading, exit the program
if (currentDownloadSession.cancelDownload) {
return;
}
System.out.print("\n");
// If failed to read from the download list file
// create a new hashmap for the download sessions list
if (downloadSessionList == null) {
downloadSessionList = new HashMap<>();
}
if (downloaded) {
currentDownloadSession = downloadSessionList.get(mURL);
currentDownloadSession.setDownloadSize(-1);
} else {
currentDownloadSession = new DownloadSession(fileName, mURL, -1);
downloadSessionList.put(mURL, currentDownloadSession);
}
try {
writeInfo(downloadSessionList);
} catch (IOException ex) {
printErrorMessage(ex);
}
// Create a Progress object to keep track of the download
Progress progress = new Progress();
// Start new download with the given URL
DateFormat dateFormat = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss");
Date date = new Date();
System.out.println("--- " + dateFormat.format(date) + " ---\n");
System.out.println("Downloading from: " + mURL);
Download newDownload = new Download(mURL, partsCount, progress, userOptions);
// Start the download.
Instant start = Instant.now();
newDownload.startThread();
// Verify URL
System.out.println("Sending HTTP request...");
synchronized (progress) {
// Wait until verification of the URL succeeds or an exception is thrown.
while (progress.mURLVerifyResult.responseCode == 0
&& progress.ex == null) {
progress.wait();
}
if (progress.ex == null) {
// If no exception was thrown, URL verification succeeds.
System.out.println("Response code: "
+ progress.mURLVerifyResult.responseCode);
System.out.println("Fize size: "
+ Utility.readableFileSize(progress.mURLVerifyResult.contentLength));
} else {
// Else print the error message and exit.
printErrorMessage(progress.ex);
}
}
System.out.println();
// Wait for the download to finish
Instant downloadFinish = null;
synchronized (progress) {
// Wait until the download finishes or an exception is thrown.
while (!progress.downloadFinished && progress.ex == null) {
progress.wait();
}
if (progress.ex == null) {
// If no exception was thrown. the file was downloaded successfully.
downloadFinish = Instant.now();
double downloadTime = ((double) (Duration.between(start,
downloadFinish).toMillis())) / 1000;
System.out.println("\n\nTotal download time: " + downloadTime);
} else {
// Else print the error message and exit.
printErrorMessage(progress.ex);
}
}
// Wait for the parts to finish joining.
Instant joinFinishedTime;
synchronized (progress) {
// Wait until all parts finish joining or an exception is thrown.
while (!progress.joinPartsFinished && progress.ex == null) {
progress.wait();
}
if (progress.ex == null) {
// If no exception is thrown, parts joining succeeds.
joinFinishedTime = Instant.now();
double joinTime = ((double) (Duration.between(downloadFinish,
joinFinishedTime).toMillis())) / 1000;
System.out.println("Total join time: " + joinTime);
} else {
// Else print the error message and exit.
printErrorMessage(progress.ex);
}
}
// Wait for the main download thread to end.
try {
newDownload.joinThread();
} catch (InterruptedException ex) {
printErrorMessage(ex);
}
// Save the download to the downloaded file list
currentDownloadSession.setDownloadSize(progress.downloadedCount);
try {
writeInfo(downloadSessionList);
} catch (IOException ex) {
printErrorMessage(ex);
}
// Print the current time
date = new Date();
System.out.println("Finished downloading!");
System.out.println("\n--- " + dateFormat.format(date) + " ---");
}
// Other functions go here.
}
As you can see, each time after the thread reads from the input stream (or download from the source), it updates the number of downloaded bytes count in the keeping track of the progress object, then notifies the other threads.
I suspect that this does slow down the download a little bit (just a little bit, since the download speed is still good enough compare to wget and chrome). Is this design flawed, and can it be improved in any way?