SKCraft-Launcher/launcher/src/main/java/com/skcraft/launcher/update/BaseUpdater.java

322 lines
13 KiB
Java

/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> and contributors
* Please see LICENSE.txt for license information.
*/
package com.skcraft.launcher.update;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.skcraft.launcher.AssetsRoot;
import com.skcraft.launcher.Instance;
import com.skcraft.launcher.Launcher;
import com.skcraft.launcher.LauncherException;
import com.skcraft.launcher.dialog.FeatureSelectionDialog;
import com.skcraft.launcher.dialog.ProgressDialog;
import com.skcraft.launcher.install.*;
import com.skcraft.launcher.model.loader.LoaderManifest;
import com.skcraft.launcher.model.loader.LocalLoader;
import com.skcraft.launcher.model.minecraft.*;
import com.skcraft.launcher.model.modpack.DownloadableFile;
import com.skcraft.launcher.model.modpack.Feature;
import com.skcraft.launcher.model.modpack.Manifest;
import com.skcraft.launcher.model.modpack.ManifestEntry;
import com.skcraft.launcher.persistence.Persistence;
import com.skcraft.launcher.util.Environment;
import com.skcraft.launcher.util.FileUtils;
import com.skcraft.launcher.util.HttpRequest;
import com.skcraft.launcher.util.SharedLocale;
import lombok.NonNull;
import lombok.extern.java.Log;
import javax.swing.*;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.logging.Level;
import static com.skcraft.launcher.LauncherUtils.checkInterrupted;
import static com.skcraft.launcher.LauncherUtils.concat;
import static com.skcraft.launcher.util.HttpRequest.url;
/**
* The base implementation of the various routines involved in downloading
* and updating Minecraft (including the launcher's modpacks), such as asset
* downloading, .jar downloading, and so on.
* </p>
* Updating actually starts in {@link com.skcraft.launcher.update.Updater},
* which is the update worker. This class exists to allow updaters that don't
* use the launcher's default modpack format to reuse these update
* routines. (It also makes the size of the <code>Updater</code> class smaller.)
*/
@Log
public abstract class BaseUpdater {
private static final long JAR_SIZE_ESTIMATE = 5 * 1024 * 1024;
private static final long LIBRARY_SIZE_ESTIMATE = 3 * 1024 * 1024;
private final Launcher launcher;
private final Environment environment = Environment.getInstance();
private final List<Runnable> executeOnCompletion = new ArrayList<Runnable>();
protected BaseUpdater(@NonNull Launcher launcher) {
this.launcher = launcher;
}
protected void complete() {
for (Runnable runnable : executeOnCompletion) {
runnable.run();
}
}
protected Manifest installPackage(@NonNull Installer installer, @NonNull Instance instance) throws Exception {
final File contentDir = instance.getContentDir();
final File logPath = new File(instance.getDir(), "install_log.json");
final File cachePath = new File(instance.getDir(), "update_cache.json");
final File featuresPath = new File(instance.getDir(), "features.json");
// Make sure the temp dir exists
installer.getTempDir().mkdirs();
final InstallLog previousLog = Persistence.read(logPath, InstallLog.class);
final InstallLog currentLog = new InstallLog();
currentLog.setBaseDir(contentDir);
final UpdateCache updateCache = Persistence.read(cachePath, UpdateCache.class);
final FeatureCache featuresCache = Persistence.read(featuresPath, FeatureCache.class);
Manifest manifest = HttpRequest
.get(instance.getManifestURL())
.execute()
.expectResponseCode(200)
.returnContent()
.saveContent(instance.getManifestPath())
.asJson(Manifest.class);
if (manifest.getMinimumVersion() > Launcher.PROTOCOL_VERSION) {
throw new LauncherException("Update required", SharedLocale.tr("errors.updateRequiredError"));
}
if (manifest.getBaseUrl() == null) {
manifest.setBaseUrl(instance.getManifestURL());
}
final List<Feature> features = manifest.getFeatures();
if (!features.isEmpty()) {
for (Feature feature : features) {
Boolean last = featuresCache.getSelected().get(feature.getName());
if (last != null) {
feature.setSelected(last);
}
}
Collections.sort(features);
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
new FeatureSelectionDialog(ProgressDialog.getLastDialog(), features, BaseUpdater.this)
.setVisible(true);
}
});
synchronized (this) {
this.wait();
}
for (Feature feature : features) {
featuresCache.getSelected().put(Strings.nullToEmpty(feature.getName()), feature.isSelected());
}
}
// Download any extra processing files for each loader
HashMap<String, LocalLoader> loaders = Maps.newHashMap();
for (Map.Entry<String, LoaderManifest> entry : manifest.getLoaders().entrySet()) {
HashMap<String, DownloadableFile.LocalFile> localFilesMap = Maps.newHashMap();
for (DownloadableFile file : entry.getValue().getDownloadableFiles()) {
if (file.getSide() != Side.CLIENT) continue;
DownloadableFile.LocalFile localFile = file.download(installer, manifest);
localFilesMap.put(localFile.getName(), localFile);
}
loaders.put(entry.getKey(), new LocalLoader(entry.getValue(), localFilesMap));
}
InstallExtras extras = new InstallExtras(contentDir, loaders);
for (ManifestEntry entry : manifest.getTasks()) {
entry.install(installer, currentLog, updateCache, extras);
}
executeOnCompletion.add(new Runnable() {
@Override
public void run() {
for (Map.Entry<String, Set<String>> entry : previousLog.getEntrySet()) {
for (String path : entry.getValue()) {
if (!currentLog.has(path)) {
new File(contentDir, path).delete();
}
}
}
writeDataFile(logPath, currentLog);
writeDataFile(cachePath, updateCache);
writeDataFile(featuresPath, featuresCache);
}
});
return manifest;
}
protected void installJar(@NonNull Installer installer,
@NonNull VersionManifest.Artifact artifact,
@NonNull File jarFile,
@NonNull URL url) throws InterruptedException {
// If the JAR does not exist, install it
if (!jarFile.exists()) {
long size = artifact.getSize();
if (size <= 0) size = JAR_SIZE_ESTIMATE;
File tempFile = installer.getDownloader().download(url, "", size, jarFile.getName());
installer.queue(new FileMover(tempFile, jarFile));
if (artifact.getHash() != null) {
installer.queue(new FileVerify(jarFile, jarFile.getName(), artifact.getHash()));
}
log.info("Installing " + jarFile.getName() + " from " + url);
}
}
protected void installAssets(@NonNull Installer installer,
@NonNull VersionManifest versionManifest,
@NonNull URL indexUrl,
@NonNull List<URL> sources) throws IOException, InterruptedException {
AssetsRoot assetsRoot = launcher.getAssets();
AssetsIndex index = HttpRequest
.get(indexUrl)
.execute()
.expectResponseCode(200)
.returnContent()
.saveContent(assetsRoot.getIndexPath(versionManifest))
.asJson(AssetsIndex.class);
// Keep track of duplicates
Set<String> downloading = new HashSet<String>();
for (Map.Entry<String, Asset> entry : index.getObjects().entrySet()) {
checkInterrupted();
String hash = entry.getValue().getHash();
String path = String.format("%s/%s", hash.subSequence(0, 2), hash);
File targetFile = assetsRoot.getObjectPath(entry.getValue());
if (!targetFile.exists() && !downloading.contains(path)) {
List<URL> urls = new ArrayList<URL>();
for (URL sourceUrl : sources) {
try {
urls.add(concat(sourceUrl, path));
} catch (MalformedURLException e) {
log.log(Level.WARNING, "Bad source URL for library: " + sourceUrl);
}
}
File tempFile = installer.getDownloader().download(
urls, "", entry.getValue().getSize(), entry.getKey());
installer.queue(new FileMover(tempFile, targetFile));
log.info("Fetching " + path + " from " + urls);
downloading.add(path);
}
}
}
protected void installLibraries(@NonNull Installer installer,
@NonNull Manifest manifest,
@NonNull File librariesDir,
@NonNull List<URL> sources) throws InterruptedException, IOException {
VersionManifest versionManifest = manifest.getVersionManifest();
Iterable<Library> allLibraries = versionManifest.getLibraries();
for (LoaderManifest loader : manifest.getLoaders().values()) {
allLibraries = Iterables.concat(allLibraries, loader.getLibraries());
}
for (Library library : allLibraries) {
if (library.isGenerated()) continue; // Skip generated libraries.
if (library.matches(environment)) {
checkInterrupted();
Library.Artifact artifact = library.getArtifact(environment);
String path = artifact.getPath();
long size = artifact.getSize();
if (size <= 0) size = LIBRARY_SIZE_ESTIMATE;
File targetFile = new File(librariesDir, path);
if (!targetFile.exists()) {
List<URL> urls = new ArrayList<URL>();
for (URL sourceUrl : sources) {
try {
urls.add(concat(sourceUrl, path));
} catch (MalformedURLException e) {
log.log(Level.WARNING, "Bad source URL for library: " + sourceUrl);
}
}
File tempFile = installer.getDownloader().download(urls, "", size,
library.getName() + ".jar");
log.info("Fetching " + path + " from " + urls);
installer.queue(new FileMover(tempFile, targetFile));
if (artifact.getSha1() != null) {
installer.queue(new FileVerify(targetFile, library.getName(), artifact.getSha1()));
}
}
}
}
// Use our custom logging config depending on what the manifest specifies
if (versionManifest.getLogging() != null && versionManifest.getLogging().getClient() != null) {
VersionManifest.LoggingConfig config = versionManifest.getLogging().getClient();
VersionManifest.Artifact file = config.getFile();
File targetFile = new File(librariesDir, file.getId());
InputStream embeddedConfig = Launcher.class.getResourceAsStream("logging/" + file.getId());
if (embeddedConfig == null) {
// No embedded config, just use whatever the server gives us
File tempFile = installer.getDownloader().download(url(file.getUrl()), file.getHash(), file.getSize(), file.getId());
log.info("Downloading logging config " + file.getId() + " from " + file.getUrl());
installer.queue(new FileMover(tempFile, targetFile));
} else if (!targetFile.exists() || FileUtils.getShaHash(targetFile).equals(file.getHash())) {
// Use our embedded replacement
Path tempFile = installer.getTempDir().toPath().resolve(file.getId());
Files.copy(embeddedConfig, tempFile, StandardCopyOption.REPLACE_EXISTING);
log.info("Substituting embedded logging config " + file.getId());
installer.queue(new FileMover(tempFile.toFile(), targetFile));
}
}
}
private static void writeDataFile(File path, Object object) {
try {
Persistence.write(path, object);
} catch (IOException e) {
log.log(Level.WARNING, "Failed to write to " + path.getAbsolutePath() +
" for object " + object.getClass().getCanonicalName(), e);
}
}
}