/*
* Copyright (c) 2012, the Dart project authors.
*
* Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.eclipse.org/legal/epl-v10.html
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.dart.tools.update.core.internal;
import com.google.dart.tools.update.core.Revision;
import com.google.dart.tools.update.core.UpdateCore;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.apache.commons.compress.utils.IOUtils;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileInfo;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.filesystem.URIUtil;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.util.Util;
import org.eclipse.swt.internal.Library;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.lang.reflect.Field;
import java.net.Authenticator;
import java.net.MalformedURLException;
import java.net.PasswordAuthentication;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipException;
/**
* Update utilities.
*/
@SuppressWarnings("restriction")
public class UpdateUtils {
private static enum Arch {
x32("ia32"),
x64("x64"),
UNKNOWN(null);
private final String qualifier;
Arch(String archQualifier) {
this.qualifier = archQualifier;
}
}
private static enum OS {
WIN("windows"),
OSX("macos"),
LINUX("linux"),
UNKNOWN(null);
private final String qualifier;
OS(String binaryQualifier) {
this.qualifier = binaryQualifier;
}
}
private static final Arch ARCH = getArch();
private static final OS OS = getOS();
private static int EXEC_MASK = 0111;
// Set up proxy authentication support (dartbug.com/5455).
static {
try {
Authenticator.setDefault(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
if (getRequestorType() == RequestorType.PROXY) {
String prot = getRequestingProtocol().toLowerCase();
String host = System.getProperty(prot + ".proxyHost", "");
String port = System.getProperty(prot + ".proxyPort", "80");
String user = System.getProperty(prot + ".proxyUser", "");
String password = System.getProperty(prot + ".proxyPassword", "");
if (getRequestingHost().equalsIgnoreCase(host)) {
if (Integer.parseInt(port) == getRequestingPort()) {
return new PasswordAuthentication(user, password.toCharArray());
}
}
}
return null;
}
});
} catch (SecurityException e) {
UpdateCore.logError(e);
}
}
// A sentinel file on windows to indicate that this binary is managed by an MSI installer
private static final String INSTALLER_FILE = "README-WIN";
/**
* Copy the contents of one directory to another directory recursively.
*
* @param fromDir the source
* @param toDir the target
* @param overwriteFilter a filter to determine if a given file should be overwritten in a copy
* @param monitor a monitor for providing progress feedback
* @throws IOException
*/
public static void copyDirectory(File fromDir, File toDir, FileFilter overwriteFilter,
IProgressMonitor monitor) throws IOException {
for (File f : fromDir.listFiles()) {
File toFile = new File(toDir, f.getName());
if (f.isFile()) {
if (!toFile.exists() || overwriteFilter.accept(toFile)) {
copyFile(f, toFile, monitor);
}
} else {
if (!toFile.isDirectory()) {
toFile.delete();
}
if (!toFile.exists()) {
toFile.mkdir();
}
copyDirectory(f, toFile, overwriteFilter, monitor);
}
}
}
/**
* Copy a file from one place to another, providing progress along the way.
*/
public static void copyFile(File fromFile, File toFile, IProgressMonitor monitor)
throws IOException {
byte[] data = new byte[4096];
InputStream in = new FileInputStream(fromFile);
toFile.delete();
OutputStream out = new FileOutputStream(toFile);
int count = in.read(data);
while (count != -1) {
out.write(data, 0, count);
count = in.read(data);
}
in.close();
out.close();
toFile.setLastModified(fromFile.lastModified());
if (fromFile.canExecute()) {
toFile.setExecutable(true);
}
monitor.worked(1);
}
/**
* Count the number of files in the given directory (for use in displaying progress).
*
* @param file the file/dir of interest
* @return the number of files
*/
public static int countFiles(File file) {
if (!file.isDirectory()) {
return 1;
}
int count = 0;
File[] files = file.listFiles();
if (files != null) {
for (File f : files) {
count += countFiles(f);
}
}
return count;
}
/**
* Delete the given file. If the file is a directory, recurse and delete contents.
* <p>
* If the file does not exist, ignore.
*
* @param file the file to delete
* @param monitor the progress monitor
*/
public static void delete(File file, IProgressMonitor monitor) {
if (!file.exists()) {
return;
}
if (file.isFile()) {
file.delete();
monitor.worked(1);
} else {
deleteDirectory(file, monitor);
}
}
/**
* Recurse and delete the given directory contents, providing progress along the way.
*
* @param dir the directory to delete
* @param monitor the progress monitor
*/
public static void deleteDirectory(File dir, IProgressMonitor monitor) {
if (dir == null || !dir.isDirectory()) {
return;
}
File[] files = dir.listFiles();
if (files == null) {
return;
}
for (File file : files) {
if (file.isDirectory()) {
// If it's symlinked, just delete the link - do not follow the linked dir.
if (isLinkedFile(file)) {
file.delete();
} else {
deleteDirectory(file, monitor);
}
} else {
file.delete();
monitor.worked(1);
}
}
dir.delete();
monitor.worked(1);
}
/**
* Download a URL to a local file, notifying the given monitor along the way.
*/
public static void downloadFile(URL downloadUrl, File destFile, String taskName,
IProgressMonitor monitor) throws IOException {
URLConnection connection = downloadUrl.openConnection();
FileOutputStream out = new FileOutputStream(destFile);
int length = connection.getContentLength();
monitor.beginTask(taskName, length);
copyStream(connection.getInputStream(), out, monitor, length);
monitor.done();
}
/**
* Ensure that the execute bit is set on the given files.
*
* @param files the files that should be executable
*/
public static void ensureExecutable(File... files) {
if (files != null) {
for (File file : files) {
if (file != null && !file.canExecute()) {
if (!makeExecutable(file)) {
UpdateCore.logError("Could not make " + file.getAbsolutePath() + " executable");
}
}
}
}
}
/**
* Get the file associating this binary with an installer, or <code>null</code> if there is none.
*/
public static File getInstallerMarkerFile() {
if (Util.isWindows()) {
try {
URL installLocation = Platform.getInstallLocation().getURL();
return URIUtil.toPath(org.eclipse.core.runtime.URIUtil.toURI(installLocation)).append(
INSTALLER_FILE).toFile();
} catch (URISyntaxException e) {
// Shouldn't happen and if it does, default to null
}
}
return null;
}
/**
* Build a platform-aware installer download URL for the given revision.
*
* @param revision the revision
* @return a download url
* @throws MalformedURLException
*/
public static URL getInstallerUrl(Revision revision) throws MalformedURLException {
// darteditor-windows-ia32.zip => darteditor-installer-windows-ia32.msi
String binaryName = getBinaryName();
binaryName = binaryName.replace("darteditor-windows", "darteditor-installer-windows");
binaryName = binaryName.subSequence(0, binaryName.length() - "zip".length()) + "msi";
return new URL(UpdateCore.getUpdateUrl() + revision.toString() + "/" + binaryName);
}
/**
* Parse the latest revision from the revision listing at the given url.
*
* @param url the url to check
* @return the latest revision, or <code>null</code> if none is found
* @throws IOException if an exception occurred in retrieving the revision
*/
public static Revision getLatestRevision(String url) throws IOException {
List<Revision> revisions = parseRevisions(url);
if (revisions.isEmpty()) {
return null;
}
Collections.sort(revisions);
// //TODO (pquitslund): for testing continuous, roll back one rev
// return revisions.get(revisions.size() - 2);
return revisions.get(revisions.size() - 1);
}
/**
* Convert the given revision to a path where it should be staged in the editor's updates/
* directory in the local filesystem.
*
* @param revision the revision
* @return the local file system path
*/
public static IPath getPath(Revision revision) {
IPath updateDirPath = UpdateCore.getUpdateDirPath();
return updateDirPath.append(revision.toString()).addFileExtension("zip");
}
/**
* Get the update directory.
*
* @return the update directory
*/
public static File getUpdateDir() {
IPath updateDirPath = UpdateCore.getUpdateDirPath();
File updateDir = updateDirPath.toFile();
if (!updateDir.exists()) {
updateDir.mkdirs();
}
return updateDir;
}
/**
* Get the target update install directory.
*
* @return the install directory
*/
public static File getUpdateInstallDir() {
if (UpdateCore.DEBUGGING_IN_RUNTIME_WS) {
//TODO (pquitslund): for local testing
return new File(getUpdateTempDir().getParentFile(), "install");
}
return getUpdateDir().getParentFile();
}
/**
* Get the temporary update data directory.
*
* @return the temporary update data directory
*/
public static File getUpdateTempDir() {
File updateDir = getUpdateDir();
File tmpDir = new File(updateDir, "tmp");
if (!tmpDir.isDirectory()) {
tmpDir.delete();
tmpDir = new File(updateDir, "tmp");
}
if (!tmpDir.exists()) {
tmpDir.mkdir();
}
return tmpDir;
}
/**
* Build a platform-aware download URL for the given revision.
*
* @param revision the revision
* @return a download url
* @throws MalformedURLException
*/
public static URL getUrl(Revision revision) throws MalformedURLException {
return new URL(UpdateCore.getUpdateUrl() + revision.toString() + "/" + getBinaryName());
}
/**
* Test for the presence of an installer.
*/
public static boolean isInstallerPresent() {
File installer = getInstallerMarkerFile();
return installer != null && installer.exists();
}
/**
* Check if the given zip file is valid.
*
* @param zip the zip to check
* @return <code>true</code> if it's valid, <code>false</code> otherwise.
*/
public static boolean isZipValid(final File zip) {
java.util.zip.ZipFile zipfile = null;
try {
zipfile = new java.util.zip.ZipFile(zip);
return true;
} catch (ZipException e) {
return false;
} catch (IOException e) {
return false;
} finally {
try {
if (zipfile != null) {
zipfile.close();
zipfile = null;
}
} catch (IOException e) {
}
}
}
/**
* Parse the revision number from a JSON string.
* <p>
* Sample payload:
* </p>
*
* <pre>
* {
* "revision" : "9826",
* "version" : "0.0.1_v2012070961811",
* "date" : "2012-07-09"
* }
* </pre>
*
* @param versionJSON the json
* @return a revision number or <code>null</code> if none can be found
* @throws IOException
*/
public static String parseRevisionNumberFromJSON(String versionJSON) throws IOException {
try {
JSONObject obj = new JSONObject(versionJSON);
return obj.optString("revision", null);
} catch (JSONException e) {
throw new IOException(e);
}
}
/**
* Parse the latest revision from the VERSION file at the given url.
*
* @param url the url to check
* @return the latest revision, or <code>null</code> if none is found
* @throws IOException if an exception occurred in retrieving the revision
*/
public static Revision parseVersionFile(String url) throws IOException {
String versionFileContents = readUrlStream(url);
//TODO (pquitslund): temporary check for "old" numeric format,
//to be removed once the JSON format is final
String revisionString = versionFileContents.trim().matches("-?\\d+(.\\d+)?")
? versionFileContents : parseRevisionNumberFromJSON(versionFileContents);
return Revision.forValue(revisionString);
}
/**
* Read, as a string, the stream at the given url string.
*/
public static String readUrlStream(String urlString) throws MalformedURLException, IOException {
URL url = new URL(urlString);
InputStream stream = url.openStream();
return toString(stream);
}
/**
* Unzip a zip file, notifying the given monitor along the way.
*/
public static void unzip(File zipFile, File destination, String taskName, IProgressMonitor monitor)
throws IOException {
ZipFile zip = new ZipFile(zipFile);
//TODO (pquitslund): add real progress units
if (monitor != null) {
monitor.beginTask(taskName, 1);
}
try {
Enumeration<ZipArchiveEntry> e = zip.getEntries();
while (e.hasMoreElements()) {
ZipArchiveEntry entry = e.nextElement();
File file = new File(destination, entry.getName());
if (entry.isDirectory()) {
file.mkdirs();
} else {
InputStream is = zip.getInputStream(entry);
File parent = file.getParentFile();
if (parent != null && parent.exists() == false) {
parent.mkdirs();
}
FileOutputStream os = new FileOutputStream(file);
try {
IOUtils.copy(is, os);
} finally {
os.close();
is.close();
}
file.setLastModified(entry.getTime());
int mode = entry.getUnixMode();
if ((mode & EXEC_MASK) != 0) {
if (!file.setExecutable(true)) {
UpdateCore.logError("Unable to set exec bit on: " + file.getPath());
}
}
}
}
} finally {
ZipFile.closeQuietly(zip);
}
//TODO (pquitslund): fix progress units
if (monitor != null) {
monitor.worked(1);
monitor.done();
}
}
private static void copyStream(InputStream in, FileOutputStream out, IProgressMonitor monitor,
int length) throws IOException {
byte[] data = new byte[4096];
try {
int count = in.read(data);
while (count != -1) {
if (monitor.isCanceled()) {
throw new IOException("job cancelled");
}
out.write(data, 0, count);
if (length != -1) {
monitor.worked(count);
}
count = in.read(data);
}
} finally {
in.close();
out.close();
}
}
private static Arch getArch() {
try {
return is64bitSWT() ? Arch.x64 : Arch.x32;
} catch (Exception e) {
UpdateCore.logError(e);
return Arch.UNKNOWN;
}
}
private static String getBinaryName() {
return "editor/darteditor-" + OS.qualifier + "-" + ARCH.qualifier + ".zip";
}
@SuppressWarnings("static-access")
private static OS getOS() {
if (Util.isMac()) {
return OS.OSX;
}
if (Util.isLinux()) {
return OS.LINUX;
}
if (Util.isWindows()) {
return OS.WIN;
}
return OS.UNKNOWN;
}
private static boolean is64bitSWT() throws Exception {
Class<Library> swtLibraryClass = Library.class;
Field is64 = swtLibraryClass.getDeclaredField("IS_64");
is64.setAccessible(true);
return is64.getBoolean(swtLibraryClass);
}
private static boolean isLinkedFile(File file) {
try {
IFileStore fileStore = EFS.getStore(file.toURI());
IFileInfo info = fileStore.fetchInfo();
return info.getAttribute(EFS.ATTRIBUTE_SYMLINK);
} catch (CoreException ce) {
return false;
}
}
private static boolean isNumeric(String str) {
for (char c : str.toCharArray()) {
if (!Character.isDigit(c)) {
return false;
}
}
return true;
}
private static boolean makeExecutable(File file) {
UpdateCore.logInfo("Setting execute bit for " + file.getAbsolutePath());
// First try and set executable for all users.
if (file.setExecutable(true, false)) {
// success
return true;
}
// Then try only for the current user.
return file.setExecutable(true, true);
}
private static List<Revision> parseRevisions(String urlString) throws IOException {
String str = readUrlStream(urlString);
Pattern linkPattern = Pattern.compile(
"<a[^>]+href=[\"']?([\"'>]+)[\"']?[^>]*>(.+?)</a>",
Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
Matcher matcher = linkPattern.matcher(str);
ArrayList<Revision> revisions = new ArrayList<Revision>();
while (matcher.find()) {
if (matcher.group(0).contains("dart-editor-archive")) {
String revision = matcher.group(2);
//drop trailing slash
revision = revision.replace("/", "");
//drop symbolic links (like "latest")
if (isNumeric(revision)) {
revisions.add(Revision.forValue(revision));
}
}
}
return revisions;
}
private static String toString(InputStream is) throws IOException {
final char[] buffer = new char[0x10000];
StringBuilder out = new StringBuilder();
Reader in = new InputStreamReader(is, "UTF-8");
int read;
do {
read = in.read(buffer, 0, buffer.length);
if (read > 0) {
out.append(buffer, 0, read);
}
} while (read >= 0);
return out.toString();
}
}