/*
* Aphelion
* Copyright (c) 2013 Joris van der Wel
*
* This file is part of Aphelion
*
* Aphelion is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* Aphelion 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 Affero General Public License
* along with Aphelion. If not, see <http://www.gnu.org/licenses/>.
*
* In addition, the following supplemental terms apply, based on section 7 of
* the GNU Affero General Public License (version 3):
* a) Preservation of all legal notices and author attributions
* b) Prohibition of misrepresentation of the origin of this material, and
* modified versions are required to be marked in reasonable ways as
* different from the original version (for example by appending a copyright notice).
*
* Linking this library statically or dynamically with other modules is making a
* combined work based on this library. Thus, the terms and conditions of the
* GNU Affero General Public License cover the whole combination.
*
* As a special exception, the copyright holders of this library give you
* permission to link this library with independent modules to produce an
* executable, regardless of the license terms of these independent modules,
* and to copy and distribute the resulting executable under terms of your
* choice, provided that you also meet, for each linked independent module,
* the terms and conditions of the license of that module. An independent
* module is a module which is not derived from or based on this library.
*/
package aphelion.shared.resource;
import aphelion.shared.event.WorkerTask;
import aphelion.shared.event.promise.PromiseException;
import aphelion.shared.swissarmyknife.SwissArmyKnife;
import java.io.*;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.RandomAccess;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A task that downloads each required asset file.
* For now there is no support for parallel downloads.
* @author Joris
*/
public class DownloadAssetsTask extends WorkerTask<List<Asset>, List<Asset>>
{
private static final Logger log = Logger.getLogger("aphelion.resource");
private final int READ_TIMEOUT_MILLIS = 10 * 1000;
private final int BUFFER_SIZE = 128 * 1024;
// statistics:
private volatile long totalBytes;
private volatile long verifiedBytes;
private volatile long currentReceivedBytes;
private volatile long speed;
private volatile int totalFiles;
private volatile int verifiedFiles;
private long secondResetMillis;
private long secondBytes;
// todo: speed
/** The total amount of bytes we have to download.
* This is excluding assets that come from the cache.
* @return bytes
*/
public long getTotalBytes()
{
return totalBytes;
}
/** The total amount of bytes have been downloaded.
* This value may decrease if it turns out that an asset was invalid and has to be retried.
* @return bytes
*/
public long getCompletedBytes()
{
return verifiedBytes + currentReceivedBytes;
}
/** The current download speed.
* @return bytes per second
*/
public long getSpeed()
{
return speed;
}
public int getTotalFiles()
{
return totalFiles;
}
public int getVerifiedFiles()
{
return verifiedFiles;
}
@Override
public List<Asset> work(List<Asset> argument) throws PromiseException
{
totalBytes = 0;
ArrayList<Asset> todo = new ArrayList<>(argument.size());
for (Asset ass : argument)
{
if (ass.validateCachedEntry())
{
// We already have the file cached, no need to download
continue;
}
todo.add(ass);
++totalFiles;
totalBytes += ass.size;
}
ASSETS_LOOP: for (Asset ass : todo)
{
assert ass.mirrors instanceof RandomAccess;
// ass.mirrors is sorted by higest priority first.
// Mirror with the same priority should be chosen in a random order
for (int i = 0; i < ass.mirrors.size();)
{
Asset.Mirror first = ass.mirrors.get(i);
int prio = first.priority;
List<Asset.Mirror> shuffled = new ArrayList<>(); // list of assets with the same prio
shuffled.add(first);
for (++i; i < ass.mirrors.size(); ++i)
{
Asset.Mirror mirror = ass.mirrors.get(i);
if (prio != mirror.priority)
{
break;
}
shuffled.add(mirror);
}
Collections.shuffle(shuffled, SwissArmyKnife.random);
for (Asset.Mirror mirror : shuffled)
{
if (tryMirror(ass, mirror))
{
currentReceivedBytes = 0;
verifiedBytes += ass.size;
++verifiedFiles;
continue ASSETS_LOOP;
}
}
}
log.log(Level.SEVERE, "Exhausted mirror list for asset, unable to download asset. {0}", ass.cachedName);
throw new PromiseException("Exhausted mirror list for asset " + ass.cachedName);
}
return argument;
}
private File newTmpFile(Asset ass) throws PromiseException
{
try
{
return File.createTempFile(ass.cachedName, null);
}
catch (IOException ex)
{
log.log(Level.SEVERE, "Unable to find a temporary file!", ex);
throw new PromiseException("Unable to create a temporary file", ex);
}
}
private boolean tryMirror(Asset ass, Asset.Mirror mirror) throws PromiseException
{
try
{
log.log(Level.INFO, "Trying to download asset {0} from {1}", new Object[] {ass.cachedName, mirror.url});
currentReceivedBytes = 0;
// currentTimeMillis is much faster than nanoTime,
// but might be disrupted by the user/NTP changing the clock
// this is not harmful because these value are only used as statistics
secondResetMillis = System.currentTimeMillis();
secondBytes = 0;
HttpURLConnection conn = (HttpURLConnection) mirror.url.openConnection();
conn.setRequestMethod("GET");
conn.setReadTimeout(READ_TIMEOUT_MILLIS);
conn.setRequestProperty("User-Agent", "Aphelion Game Client");
if (mirror.refererHeader != null)
{
conn.setRequestProperty("Referer", mirror.refererHeader);
}
File tmpFile;
// Implicit connect()
try (InputStream in = conn.getInputStream())
{
int code = conn.getResponseCode();
if (code != 200)
{
throw new IOException("Server returned status code " + code + ": " + conn.getResponseMessage());
}
long size = conn.getContentLengthLong();
if (size != 0 && size != ass.size)
{
throw new IOException("Server returned unexpected content length");
}
tmpFile = newTmpFile(ass);
try (OutputStream out = new FileOutputStream(tmpFile))
{
byte buffer[] = new byte[BUFFER_SIZE];
while (true)
{
int read = in.read(buffer);
if (read < 0)
{
break;
}
currentReceivedBytes += read;
secondBytes += read;
long now = System.currentTimeMillis();
if (Math.abs(now - secondResetMillis) >= 1000)
{
speed = secondBytes;
secondResetMillis = now;
secondBytes = 0;
}
out.write(buffer, 0, read);
}
}
catch (FileNotFoundException ex)
{
log.log(Level.SEVERE, "Unable to create the temporary file", ex);
throw new PromiseException("Unable to create the temporary file", ex);
}
catch (IOException ex)
{
tmpFile.delete();
throw ex;
}
}
try
{
ass.storeAsset(tmpFile, false);
// this asset is done
return true;
}
catch (AssetCache.InvalidContentException ex)
{
log.log(Level.WARNING, "Succesfully downloaded asset, however it has invalid content. Trying next mirror");
}
}
catch (IOException ex)
{
log.log(Level.WARNING, "Error during file download, trying next mirror", ex);
}
return false;
}
}