2015-02-01 08:10:17 +01:00
|
|
|
/*
|
|
|
|
* opsu! - an open-source osu! client
|
|
|
|
* Copyright (C) 2014, 2015 Jeffrey Han
|
|
|
|
*
|
|
|
|
* opsu! 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, either version 3 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* opsu! 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 opsu!. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
|
|
|
package itdelatrisu.opsu.downloads;
|
|
|
|
|
|
|
|
import itdelatrisu.opsu.ErrorHandler;
|
2015-02-02 06:15:16 +01:00
|
|
|
import itdelatrisu.opsu.Utils;
|
2015-02-01 08:10:17 +01:00
|
|
|
|
|
|
|
import java.io.File;
|
|
|
|
import java.io.FileOutputStream;
|
|
|
|
import java.io.IOException;
|
|
|
|
import java.io.InputStream;
|
|
|
|
import java.net.HttpURLConnection;
|
|
|
|
import java.net.MalformedURLException;
|
|
|
|
import java.net.URL;
|
|
|
|
import java.nio.channels.Channels;
|
|
|
|
import java.nio.channels.ReadableByteChannel;
|
|
|
|
import java.nio.file.Files;
|
|
|
|
import java.nio.file.Path;
|
|
|
|
import java.nio.file.StandardCopyOption;
|
|
|
|
|
2015-03-08 05:57:18 +01:00
|
|
|
import org.newdawn.slick.util.Log;
|
|
|
|
|
2015-02-01 08:10:17 +01:00
|
|
|
/**
|
|
|
|
* File download.
|
|
|
|
*/
|
|
|
|
public class Download {
|
|
|
|
/** Connection timeout, in ms. */
|
|
|
|
public static final int CONNECTION_TIMEOUT = 5000;
|
|
|
|
|
|
|
|
/** Read timeout, in ms. */
|
|
|
|
public static final int READ_TIMEOUT = 10000;
|
|
|
|
|
2015-08-09 00:08:36 +02:00
|
|
|
/** Maximum number of HTTP/HTTPS redirects to follow. */
|
|
|
|
public static final int MAX_REDIRECTS = 3;
|
|
|
|
|
2015-02-10 03:40:38 +01:00
|
|
|
/** Time between download speed and ETA updates, in ms. */
|
|
|
|
private static final int UPDATE_INTERVAL = 1000;
|
|
|
|
|
2015-02-01 08:10:17 +01:00
|
|
|
/** Download statuses. */
|
|
|
|
public enum Status {
|
|
|
|
WAITING ("Waiting"),
|
|
|
|
DOWNLOADING ("Downloading"),
|
|
|
|
COMPLETE ("Complete"),
|
|
|
|
CANCELLED ("Cancelled"),
|
|
|
|
ERROR ("Error");
|
|
|
|
|
|
|
|
/** The status name. */
|
|
|
|
private String name;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Constructor.
|
|
|
|
* @param name the status name.
|
|
|
|
*/
|
|
|
|
Status(String name) {
|
|
|
|
this.name = name;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the status name.
|
|
|
|
*/
|
|
|
|
public String getName() { return name; }
|
|
|
|
}
|
|
|
|
|
2015-03-05 21:36:36 +01:00
|
|
|
/** Download listener interface. */
|
|
|
|
public interface DownloadListener {
|
|
|
|
/** Indication that a download has completed. */
|
|
|
|
public void completed();
|
2015-03-08 05:57:18 +01:00
|
|
|
|
|
|
|
/** Indication that an error has occurred. */
|
|
|
|
public void error();
|
2015-03-05 21:36:36 +01:00
|
|
|
}
|
|
|
|
|
2015-02-01 08:10:17 +01:00
|
|
|
/** The local path. */
|
|
|
|
private String localPath;
|
|
|
|
|
|
|
|
/** The local path to rename the file to when finished. */
|
|
|
|
private String rename;
|
|
|
|
|
|
|
|
/** The download URL. */
|
|
|
|
private URL url;
|
|
|
|
|
2015-03-05 21:36:36 +01:00
|
|
|
/** The download listener. */
|
|
|
|
private DownloadListener listener;
|
|
|
|
|
2015-02-01 08:10:17 +01:00
|
|
|
/** The readable byte channel. */
|
|
|
|
private ReadableByteChannelWrapper rbc;
|
|
|
|
|
|
|
|
/** The file output stream. */
|
|
|
|
private FileOutputStream fos;
|
|
|
|
|
|
|
|
/** The size of the download. */
|
|
|
|
private int contentLength = -1;
|
|
|
|
|
|
|
|
/** The download status. */
|
|
|
|
private Status status = Status.WAITING;
|
|
|
|
|
2015-02-10 03:40:38 +01:00
|
|
|
/** Time when lastReadSoFar was updated. */
|
|
|
|
private long lastReadSoFarTime = -1;
|
|
|
|
|
|
|
|
/** Last readSoFar amount. */
|
|
|
|
private long lastReadSoFar = -1;
|
|
|
|
|
|
|
|
/** Last calculated download speed string. */
|
|
|
|
private String lastDownloadSpeed;
|
|
|
|
|
|
|
|
/** Last calculated ETA string. */
|
|
|
|
private String lastTimeRemaining;
|
|
|
|
|
2015-02-01 08:10:17 +01:00
|
|
|
/**
|
|
|
|
* Constructor.
|
|
|
|
* @param remoteURL the download URL
|
|
|
|
* @param localPath the path to save the download
|
|
|
|
*/
|
|
|
|
public Download(String remoteURL, String localPath) {
|
|
|
|
this(remoteURL, localPath, null);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Constructor.
|
|
|
|
* @param remoteURL the download URL
|
|
|
|
* @param localPath the path to save the download
|
|
|
|
* @param rename the file name to rename the download to when complete
|
|
|
|
*/
|
|
|
|
public Download(String remoteURL, String localPath, String rename) {
|
|
|
|
try {
|
|
|
|
this.url = new URL(remoteURL);
|
|
|
|
} catch (MalformedURLException e) {
|
|
|
|
this.status = Status.ERROR;
|
|
|
|
ErrorHandler.error(String.format("Bad download URL: '%s'", remoteURL), e, true);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.localPath = localPath;
|
2015-03-07 10:17:19 +01:00
|
|
|
this.rename = Utils.cleanFileName(rename, '-');
|
2015-02-01 08:10:17 +01:00
|
|
|
}
|
|
|
|
|
2015-03-07 10:17:19 +01:00
|
|
|
/**
|
|
|
|
* Returns the remote download URL.
|
|
|
|
*/
|
|
|
|
public URL getRemoteURL() { return url; }
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the local path to save the download (after renamed).
|
|
|
|
*/
|
|
|
|
public String getLocalPath() { return (rename != null) ? rename : localPath; }
|
|
|
|
|
2015-03-05 21:36:36 +01:00
|
|
|
/**
|
|
|
|
* Sets the download listener.
|
|
|
|
* @param listener the listener to set
|
|
|
|
*/
|
|
|
|
public void setListener(DownloadListener listener) { this.listener = listener; }
|
|
|
|
|
2015-02-01 08:10:17 +01:00
|
|
|
/**
|
|
|
|
* Starts the download from the "waiting" status.
|
|
|
|
*/
|
|
|
|
public void start() {
|
|
|
|
if (status != Status.WAITING)
|
|
|
|
return;
|
|
|
|
|
|
|
|
new Thread() {
|
|
|
|
@Override
|
|
|
|
public void run() {
|
2015-08-09 00:08:36 +02:00
|
|
|
// open connection
|
2015-02-01 08:10:17 +01:00
|
|
|
HttpURLConnection conn = null;
|
|
|
|
try {
|
2015-08-09 00:08:36 +02:00
|
|
|
URL downloadURL = url;
|
|
|
|
int redirectCount = 0;
|
|
|
|
boolean isRedirect = false;
|
|
|
|
do {
|
|
|
|
isRedirect = false;
|
|
|
|
|
|
|
|
conn = (HttpURLConnection) downloadURL.openConnection();
|
|
|
|
conn.setConnectTimeout(CONNECTION_TIMEOUT);
|
|
|
|
conn.setReadTimeout(READ_TIMEOUT);
|
|
|
|
conn.setUseCaches(false);
|
|
|
|
|
|
|
|
// allow HTTP <--> HTTPS redirects
|
|
|
|
// http://download.java.net/jdk7u2/docs/technotes/guides/deployment/deployment-guide/upgrade-guide/article-17.html
|
|
|
|
conn.setInstanceFollowRedirects(false);
|
|
|
|
conn.setRequestProperty("User-Agent", "Mozilla/5.0...");
|
|
|
|
|
|
|
|
// check for redirect
|
|
|
|
int status = conn.getResponseCode();
|
|
|
|
if (status == HttpURLConnection.HTTP_MOVED_TEMP || status == HttpURLConnection.HTTP_MOVED_PERM ||
|
|
|
|
status == HttpURLConnection.HTTP_SEE_OTHER || status == HttpURLConnection.HTTP_USE_PROXY) {
|
|
|
|
URL base = conn.getURL();
|
|
|
|
String location = conn.getHeaderField("Location");
|
|
|
|
URL target = null;
|
|
|
|
if (location != null)
|
|
|
|
target = new URL(base, location);
|
|
|
|
conn.disconnect();
|
|
|
|
|
|
|
|
// check for problems
|
|
|
|
String error = null;
|
|
|
|
if (location == null)
|
|
|
|
error = String.format("Download for URL '%s' is attempting to redirect without a 'location' header.", base.toString());
|
|
|
|
else if (!target.getProtocol().equals("http") && !target.getProtocol().equals("https"))
|
|
|
|
error = String.format("Download for URL '%s' is attempting to redirect to a non-HTTP/HTTPS protocol '%s'.", base.toString(), target.getProtocol());
|
|
|
|
else if (redirectCount > MAX_REDIRECTS)
|
|
|
|
error = String.format("Download for URL '%s' is attempting too many redirects (over %d).", base.toString(), MAX_REDIRECTS);
|
|
|
|
if (error != null) {
|
|
|
|
ErrorHandler.error(error, null, false);
|
|
|
|
throw new IOException();
|
|
|
|
}
|
|
|
|
|
|
|
|
// follow redirect
|
|
|
|
downloadURL = target;
|
|
|
|
redirectCount++;
|
|
|
|
isRedirect = true;
|
|
|
|
}
|
|
|
|
} while (isRedirect);
|
|
|
|
|
|
|
|
// store content length
|
2015-02-01 08:10:17 +01:00
|
|
|
contentLength = conn.getContentLength();
|
|
|
|
} catch (IOException e) {
|
|
|
|
status = Status.ERROR;
|
2015-03-08 05:57:18 +01:00
|
|
|
Log.warn("Failed to open connection.", e);
|
|
|
|
if (listener != null)
|
|
|
|
listener.error();
|
2015-02-01 08:10:17 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// download file
|
|
|
|
try (
|
|
|
|
InputStream in = conn.getInputStream();
|
|
|
|
ReadableByteChannel readableByteChannel = Channels.newChannel(in);
|
|
|
|
FileOutputStream fileOutputStream = new FileOutputStream(localPath);
|
|
|
|
) {
|
|
|
|
rbc = new ReadableByteChannelWrapper(readableByteChannel);
|
|
|
|
fos = fileOutputStream;
|
|
|
|
status = Status.DOWNLOADING;
|
2015-02-10 05:22:58 +01:00
|
|
|
updateReadSoFar();
|
2015-02-01 08:10:17 +01:00
|
|
|
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
|
|
|
|
if (status == Status.DOWNLOADING) { // not interrupted
|
2015-03-08 05:57:18 +01:00
|
|
|
// TODO: if connection is lost before a download finishes, it's still marked as "complete"
|
2015-02-01 08:10:17 +01:00
|
|
|
status = Status.COMPLETE;
|
|
|
|
rbc.close();
|
|
|
|
fos.close();
|
|
|
|
if (rename != null) {
|
|
|
|
Path source = new File(localPath).toPath();
|
2015-03-07 10:17:19 +01:00
|
|
|
Files.move(source, source.resolveSibling(rename), StandardCopyOption.REPLACE_EXISTING);
|
2015-02-01 08:10:17 +01:00
|
|
|
}
|
2015-03-05 21:36:36 +01:00
|
|
|
if (listener != null)
|
|
|
|
listener.completed();
|
2015-02-01 08:10:17 +01:00
|
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
|
|
status = Status.ERROR;
|
2015-03-08 05:57:18 +01:00
|
|
|
Log.warn("Failed to start download.", e);
|
|
|
|
if (listener != null)
|
|
|
|
listener.error();
|
2015-02-01 08:10:17 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}.start();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the download status.
|
|
|
|
*/
|
|
|
|
public Status getStatus() { return status; }
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if transfers are currently taking place.
|
|
|
|
*/
|
|
|
|
public boolean isTransferring() {
|
|
|
|
return (rbc != null && rbc.isOpen() && fos != null && fos.getChannel().isOpen());
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if the download is active.
|
|
|
|
*/
|
|
|
|
public boolean isActive() {
|
|
|
|
return (status == Status.WAITING || status == Status.DOWNLOADING);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the size of the download content in bytes, or -1 if not calculated
|
|
|
|
* (or if an error has occurred).
|
|
|
|
*/
|
|
|
|
public int contentLength() { return contentLength; }
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the download completion percentage, or -1f if an error has occurred.
|
|
|
|
*/
|
|
|
|
public float getProgress() {
|
|
|
|
switch (status) {
|
|
|
|
case WAITING:
|
|
|
|
return 0f;
|
|
|
|
case COMPLETE:
|
|
|
|
return 100f;
|
|
|
|
case DOWNLOADING:
|
|
|
|
if (rbc != null && fos != null && contentLength > 0)
|
|
|
|
return (float) rbc.getReadSoFar() / (float) contentLength * 100f;
|
|
|
|
else
|
|
|
|
return 0f;
|
|
|
|
case CANCELLED:
|
|
|
|
case ERROR:
|
|
|
|
default:
|
|
|
|
return -1f;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the number of bytes read so far.
|
|
|
|
*/
|
|
|
|
public long readSoFar() {
|
|
|
|
switch (status) {
|
|
|
|
case COMPLETE:
|
|
|
|
return contentLength;
|
|
|
|
case DOWNLOADING:
|
|
|
|
if (rbc != null)
|
|
|
|
return rbc.getReadSoFar();
|
|
|
|
// else fall through
|
|
|
|
case WAITING:
|
|
|
|
case CANCELLED:
|
|
|
|
case ERROR:
|
|
|
|
default:
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-02-10 03:40:38 +01:00
|
|
|
/**
|
|
|
|
* Returns the last calculated download speed, or null if not downloading.
|
|
|
|
*/
|
|
|
|
public String getDownloadSpeed() {
|
|
|
|
updateReadSoFar();
|
|
|
|
return lastDownloadSpeed;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the last calculated ETA, or null if not downloading.
|
|
|
|
*/
|
|
|
|
public String getTimeRemaining() {
|
|
|
|
updateReadSoFar();
|
|
|
|
return lastTimeRemaining;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Updates the last readSoFar and related fields.
|
|
|
|
*/
|
|
|
|
private void updateReadSoFar() {
|
|
|
|
// only update while downloading
|
|
|
|
if (status != Status.DOWNLOADING) {
|
|
|
|
this.lastDownloadSpeed = null;
|
|
|
|
this.lastTimeRemaining = null;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// update download speed and ETA
|
|
|
|
if (System.currentTimeMillis() > lastReadSoFarTime + UPDATE_INTERVAL) {
|
|
|
|
long readSoFar = readSoFar();
|
|
|
|
long readSoFarTime = System.currentTimeMillis();
|
|
|
|
long dlspeed = (readSoFar - lastReadSoFar) * 1000 / (readSoFarTime - lastReadSoFarTime);
|
|
|
|
if (dlspeed > 0) {
|
|
|
|
this.lastDownloadSpeed = String.format("%s/s", Utils.bytesToString(dlspeed));
|
|
|
|
long t = (contentLength - readSoFar) / dlspeed;
|
|
|
|
if (t >= 3600)
|
|
|
|
this.lastTimeRemaining = String.format("%dh%dm%ds", t / 3600, (t / 60) % 60, t % 60);
|
|
|
|
else
|
|
|
|
this.lastTimeRemaining = String.format("%dm%ds", t / 60, t % 60);
|
|
|
|
} else {
|
|
|
|
this.lastDownloadSpeed = String.format("%s/s", Utils.bytesToString(0));
|
|
|
|
this.lastTimeRemaining = "?";
|
|
|
|
}
|
|
|
|
this.lastReadSoFarTime = readSoFarTime;
|
|
|
|
this.lastReadSoFar = readSoFar;
|
|
|
|
}
|
|
|
|
|
|
|
|
// first call
|
|
|
|
else if (lastReadSoFarTime <= 0) {
|
|
|
|
this.lastReadSoFar = readSoFar();
|
|
|
|
this.lastReadSoFarTime = System.currentTimeMillis();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-02-01 08:10:17 +01:00
|
|
|
/**
|
|
|
|
* Cancels the download, if running.
|
|
|
|
*/
|
|
|
|
public void cancel() {
|
|
|
|
try {
|
|
|
|
this.status = Status.CANCELLED;
|
|
|
|
boolean transferring = isTransferring();
|
|
|
|
if (rbc != null && rbc.isOpen())
|
|
|
|
rbc.close();
|
|
|
|
if (fos != null && fos.getChannel().isOpen())
|
|
|
|
fos.close();
|
|
|
|
if (transferring) {
|
|
|
|
File f = new File(localPath);
|
|
|
|
if (f.isFile())
|
|
|
|
f.delete();
|
|
|
|
}
|
|
|
|
} catch (IOException e) {
|
|
|
|
this.status = Status.ERROR;
|
|
|
|
ErrorHandler.error("Failed to cancel download.", e, true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|