io.webfolder.cdp.ChromiumDownloader Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of cdp4j Show documentation
Show all versions of cdp4j Show documentation
cdp4j - Chrome DevTools Protocol for Java
/**
* cdp4j Commercial License
*
* Copyright 2017, 2019 WebFolder OÜ
*
* Permission is hereby granted, to "____" obtaining a copy of this software and
* associated documentation files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use, copy, modify, merge,
* publish, distribute and sublicense of the Software, and to permit persons to whom
* the Software is furnished to do so, subject to the following conditions:
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.webfolder.cdp;
import static java.io.File.pathSeparator;
import static java.lang.Integer.compare;
import static java.lang.Integer.parseInt;
import static java.lang.Math.round;
import static java.lang.String.format;
import static java.lang.String.valueOf;
import static java.lang.System.getProperty;
import static java.lang.Thread.sleep;
import static java.nio.file.Files.copy;
import static java.nio.file.Files.createDirectories;
import static java.nio.file.Files.delete;
import static java.nio.file.Files.exists;
import static java.nio.file.Files.getPosixFilePermissions;
import static java.nio.file.Files.isDirectory;
import static java.nio.file.Files.isExecutable;
import static java.nio.file.Files.list;
import static java.nio.file.Files.setPosixFilePermissions;
import static java.nio.file.Files.size;
import static java.nio.file.Paths.get;
import static java.nio.file.attribute.PosixFilePermission.GROUP_EXECUTE;
import static java.nio.file.attribute.PosixFilePermission.GROUP_READ;
import static java.nio.file.attribute.PosixFilePermission.GROUP_WRITE;
import static java.nio.file.attribute.PosixFilePermission.OTHERS_EXECUTE;
import static java.nio.file.attribute.PosixFilePermission.OTHERS_READ;
import static java.nio.file.attribute.PosixFilePermission.OTHERS_WRITE;
import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE;
import static java.nio.file.attribute.PosixFilePermission.OWNER_READ;
import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE;
import static java.util.Locale.ENGLISH;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFilePermission;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.apache.commons.compress.utils.IOUtils;
import io.webfolder.cdp.exception.CdpException;
import io.webfolder.cdp.logger.CdpLogger;
import io.webfolder.cdp.logger.CdpLoggerFactory;
import io.webfolder.cdp.logger.LoggerFactory;
public class ChromiumDownloader implements Downloader {
private static final String OS = getProperty("os.name").toLowerCase(ENGLISH);
private static final boolean WINDOWS = ";".equals(pathSeparator);
private static final boolean MAC = OS.contains("mac");
private static final boolean LINUX = OS.contains("linux");
private static final String DOWNLOAD_HOST = "https://storage.googleapis.com/chromium-browser-snapshots";
private static final int TIMEOUT = 10 * 1000; // 10 seconds
private static final PosixFilePermission[] DECODE_MAP = {
OTHERS_EXECUTE,
OTHERS_WRITE,
OTHERS_READ,
GROUP_EXECUTE,
GROUP_WRITE,
GROUP_READ,
OWNER_EXECUTE,
OWNER_WRITE, OWNER_READ
};
private final CdpLogger logger;
public ChromiumDownloader() {
this(new CdpLoggerFactory());
}
public ChromiumDownloader(LoggerFactory loggerFactory) {
this.logger = loggerFactory.getLogger("cdp4j.downloader");
}
@Override
public Path download() {
return download(getLatestVersion());
}
public static ChromiumVersion getLatestVersion() {
String url = DOWNLOAD_HOST;
if ( WINDOWS ) {
url += "/Win_x64/LAST_CHANGE";
} else if ( LINUX ) {
url += "/Linux_x64/LAST_CHANGE";
} else if ( MAC ) {
url += "/Mac/LAST_CHANGE";
} else {
throw new CdpException("Unsupported OS found - " + OS);
}
try {
URL u = new URL(url);
HttpURLConnection conn = (HttpURLConnection) u.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(TIMEOUT);
conn.setReadTimeout(TIMEOUT);
if ( conn.getResponseCode() != 200 ) {
throw new CdpException(conn.getResponseCode() + " - " + conn.getResponseMessage());
}
String result = null;
try (Scanner s = new Scanner(conn.getInputStream())) {
s.useDelimiter("\\A");
result = s.hasNext() ? s.next() : "";
}
return new ChromiumVersion(Integer.parseInt(result));
} catch (IOException e) {
throw new CdpException(e);
}
}
public static Path getChromiumPath(ChromiumVersion version) {
Path destinationRoot = get(getProperty("user.home"))
.resolve(".cdp4j")
.resolve("chromium-" + valueOf(version.getRevision()));
return destinationRoot;
}
public static Path getExecutable(ChromiumVersion version) {
Path destinationRoot = getChromiumPath(version);
Path executable = destinationRoot.resolve("chrome.exe");
if ( LINUX ) {
executable = destinationRoot.resolve("chrome");
} else if ( MAC ) {
executable = destinationRoot.resolve("Chromium.app/Contents/MacOS/Chromium");
}
return executable;
}
public Path download(ChromiumVersion version) {
final Path destinationRoot = getChromiumPath(version);
final Path executable = getExecutable(version);
String url;
if ( WINDOWS ) {
url = format("%s/Win_x64/%d/chrome-win.zip", DOWNLOAD_HOST, version.getRevision());
} else if ( LINUX ) {
url = format("%s/Linux_x64/%d/chrome-linux.zip", DOWNLOAD_HOST, version.getRevision());
} else if ( MAC ) {
url = format("%s/Mac/%d/chrome-mac.zip", DOWNLOAD_HOST, version.getRevision());
} else {
throw new CdpException("Unsupported OS found - " + OS);
}
try {
URL u = new URL(url);
HttpURLConnection conn = (HttpURLConnection) u.openConnection();
conn.setRequestMethod("HEAD");
conn.setConnectTimeout(TIMEOUT);
conn.setReadTimeout(TIMEOUT);
if ( conn.getResponseCode() != 200 ) {
throw new CdpException(conn.getResponseCode() + " - " + conn.getResponseMessage());
}
long contentLength = conn.getHeaderFieldLong("x-goog-stored-content-length", 0);
String fileName = url.substring(url.lastIndexOf("/") + 1, url.lastIndexOf(".")) + "-r" + version.getRevision() + ".zip";
Path archive = get(getProperty("java.io.tmpdir")).resolve(fileName);
if ( exists(archive) && contentLength != size(archive) ) {
delete(archive);
}
if ( ! exists(archive) ) {
logger.info("Downloading Chromium [revision=" + version.getRevision() + "] 0%");
u = new URL(url);
if ( conn.getResponseCode() != 200 ) {
throw new CdpException(conn.getResponseCode() + " - " + conn.getResponseMessage());
}
conn = (HttpURLConnection) u.openConnection();
conn.setConnectTimeout(TIMEOUT);
conn.setReadTimeout(TIMEOUT);
Thread thread = null;
AtomicBoolean halt = new AtomicBoolean(false);
Runnable progress = () -> {
try {
long fileSize = size(archive);
logger.info("Downloading Chromium [revision={}] {}%",
version.getRevision(), round((fileSize * 100L) / contentLength));
} catch (IOException e) {
// ignore
}
};
try (InputStream is = conn.getInputStream()) {
logger.info("Download location: " + archive.toString());
thread = new Thread(() -> {
while (true) {
try {
if (halt.get()) {
break;
}
progress.run();
sleep(1000);
} catch (Throwable e) {
// ignore
}
}
});
thread.setName("cdp4j");
thread.setDaemon(true);
thread.start();
copy(conn.getInputStream(), archive);
} finally {
if ( thread != null ) {
progress.run();
halt.set(true);
}
}
}
logger.info("Extracting to: " + destinationRoot.toString());
if (exists(archive)) {
createDirectories(destinationRoot);
unpack(archive.toFile(), destinationRoot.toFile());
}
if ( ! exists(executable) || ! isExecutable(executable) ) {
throw new CdpException("Chromium executable not found: " + executable.toString());
}
if ( ! WINDOWS ) {
Set permissions = getPosixFilePermissions(executable);
if ( ! permissions.contains(OWNER_EXECUTE)) {
permissions.add(OWNER_EXECUTE);
setPosixFilePermissions(executable, permissions);
}
if ( ! permissions.contains(GROUP_EXECUTE) ) {
permissions.add(GROUP_EXECUTE);
setPosixFilePermissions(executable, permissions);
}
}
} catch (IOException e) {
throw new CdpException(e);
}
return executable;
}
public static List getInstalledVersions() {
Path chromiumRootPath = get(getProperty("user.home")).resolve(".cdp4j");
if ( ! Files.exists(chromiumRootPath) ) {
return Collections.emptyList();
}
try {
List list = list(chromiumRootPath)
.filter(p -> isDirectory(p))
.filter(p -> p.getFileName().toString().startsWith("chromium-"))
.map(p -> new ChromiumVersion(parseInt(p.getFileName().toString().split("-")[1])))
.collect(Collectors.toList());
list.sort((o1, o2) -> compare(o2.getRevision(), o1.getRevision()));
return list;
} catch (IOException e) {
throw new CdpException(e);
}
}
public static ChromiumVersion getLatestInstalledVersion() {
List versions = getInstalledVersions();
return ! versions.isEmpty() ? versions.get(0) : null;
}
private static void unpack(File archive, File destionation) throws IOException {
try (ZipFile zip = new ZipFile(archive)) {
Map symLinks = new LinkedHashMap<>();
Enumeration iterator = zip.getEntries();
// Top directory name we are going to ignore
String parentDirectory = iterator.nextElement().getName();
// Iterate files & folders
while (iterator.hasMoreElements()) {
ZipArchiveEntry entry = iterator.nextElement();
String name = entry.getName().substring(parentDirectory.length());
File outputFile = new File(destionation, name);
if ( name.startsWith("interactive_ui_tests") ) {
continue;
}
if (entry.isUnixSymlink()) {
symLinks.put(outputFile, zip.getUnixSymlink(entry));
} else if ( ! entry.isDirectory() ) {
if ( ! outputFile.getParentFile().isDirectory() ) {
outputFile.getParentFile().mkdirs();
}
try (FileOutputStream outStream = new FileOutputStream(outputFile)) {
IOUtils.copy(zip.getInputStream(entry), outStream);
}
}
// Set permission
if ( ! entry.isUnixSymlink() && outputFile.exists() )
try {
Files.setPosixFilePermissions(outputFile.toPath(), modeToPosixPermissions(entry.getUnixMode()));
} catch (Exception e) {
// ignore
}
}
for (Map.Entry entry : symLinks.entrySet()) {
try {
Path source = Paths.get(entry.getKey().getAbsolutePath());
Path target = source.getParent().resolve(entry.getValue());
if ( !source.toFile().exists() )
Files.createSymbolicLink(source, target);
} catch (Exception e) {
// ignore
}
}
}
}
private static Set modeToPosixPermissions(final int mode) {
int mask = 1;
Set perms = EnumSet.noneOf(PosixFilePermission.class);
for (PosixFilePermission flag : DECODE_MAP) {
if ( (mask & mode) != 0 ) {
perms.add(flag);
}
mask = mask << 1;
}
return perms;
}
}