blob: 02ddd0b646087d09768b2e16dff050b80a92e660 [file] [log] [blame]
package com.intellij.platform.templates.github;
import com.intellij.openapi.application.ex.ApplicationInfoEx;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.util.ObjectUtils;
import com.intellij.util.Producer;
import com.intellij.util.containers.Predicate;
import com.intellij.util.net.HttpConfigurable;
import com.intellij.util.net.NetUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URLConnection;
import java.util.Locale;
import java.util.concurrent.Callable;
/**
* @author Sergey Simonchik
*/
public class DownloadUtil {
public static final String CONTENT_LENGTH_TEMPLATE = "${content-length}";
private static final Logger LOG = Logger.getInstance(DownloadUtil.class);
/**
* Downloads content of {@code url} to {@code outputFile} atomically.<br/>
* {@code outputFile} isn't modified if an I/O error occurs or {@code contentChecker} is provided and returns false on the downloaded content.
* More formally, the steps are:
* <ol>
* <li>Download {@code url} to {@code tempFile}. Stop in case of any I/O errors.</li>
* <li>Stop if {@code contentChecker} is provided, and it returns false on the downloaded content.</li>
* <li>Move {@code tempFile} to {@code outputFile}. On most OS this operation is done atomically.</li>
* </ol>
*
* Motivation: some web filtering products return pure HTML with HTTP 200 OK status instead of
* the asked content.
*
* @param indicator progress indicator
* @param url url to download
* @param outputFile output file
* @param tempFile temporary file to download to. This file is deleted on method exit.
* @param contentChecker checks whether the downloaded content is OK or not
* @returns true if no {@code contentChecker} is provided or the provided one returned true
* @throws IOException if an I/O error occurs
*/
public static boolean downloadAtomically(@Nullable ProgressIndicator indicator,
@NotNull String url,
@NotNull File outputFile,
@NotNull File tempFile,
@Nullable Predicate<String> contentChecker) throws IOException
{
try {
downloadContentToFile(indicator, url, tempFile);
if (contentChecker != null) {
String content = FileUtil.loadFile(tempFile);
if (!contentChecker.apply(content)) {
return false;
}
}
FileUtil.rename(tempFile, outputFile);
return true;
} finally {
FileUtil.delete(tempFile);
}
}
/**
* Downloads content of {@code url} to {@code outputFile} atomically.
* {@code outputFile} won't be modified in case of any I/O download errors.
*
* @param indicator progress indicator
* @param url url to download
* @param outputFile output file
*/
public static void downloadAtomically(@Nullable ProgressIndicator indicator,
@NotNull String url,
@NotNull File outputFile) throws IOException
{
File tempFile = FileUtil.createTempFile("for-actual-downloading-", null);
downloadAtomically(indicator, url, outputFile, tempFile, null);
}
/**
* Downloads content of {@code url} to {@code outputFile} atomically.
* {@code outputFile} won't be modified in case of any I/O download errors.
*
* @param indicator progress indicator
* @param url url to download
* @param outputFile output file
* @param tempFile temporary file to download to. This file is deleted on method exit.
*/
public static void downloadAtomically(@Nullable ProgressIndicator indicator,
@NotNull String url,
@NotNull File outputFile,
@NotNull File tempFile) throws IOException
{
downloadAtomically(indicator, url, outputFile, tempFile, null);
}
@NotNull
public static <V> Outcome<V> provideDataWithProgressSynchronously(
@Nullable Project project,
@NotNull String progressTitle,
@NotNull final String actionShortDescription,
@NotNull final Callable<V> supplier,
@Nullable Producer<Boolean> tryAgainProvider)
{
int attemptNumber = 1;
while (true) {
final Ref<V> dataRef = Ref.create(null);
final Ref<Exception> innerExceptionRef = Ref.create(null);
boolean completed = ProgressManager.getInstance().runProcessWithProgressSynchronously(new Runnable() {
@Override
public void run() {
ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator();
indicator.setText(actionShortDescription);
try {
V data = supplier.call();
dataRef.set(data);
}
catch (Exception ex) {
innerExceptionRef.set(ex);
}
}
}, progressTitle, true, project);
if (!completed) {
return Outcome.createAsCancelled();
}
Exception latestInnerException = innerExceptionRef.get();
if (latestInnerException == null) {
return Outcome.createNormal(dataRef.get());
}
LOG.warn("[attempt#" + attemptNumber + "] Can not '" + actionShortDescription + "'", latestInnerException);
boolean onceMore = false;
if (tryAgainProvider != null) {
onceMore = Boolean.TRUE.equals(tryAgainProvider.produce());
}
if (!onceMore) {
return Outcome.createAsException(latestInnerException);
}
attemptNumber++;
}
}
public static void downloadContentToFile(@Nullable ProgressIndicator progress,
@NotNull String url,
@NotNull File outputFile) throws IOException {
boolean parentDirExists = FileUtil.createParentDirs(outputFile);
if (!parentDirExists) {
throw new IOException("Parent dir of '" + outputFile.getAbsolutePath() + "' can not be created!");
}
OutputStream out = new FileOutputStream(outputFile);
try {
download(progress, url, out);
} finally {
out.close();
}
}
private static void download(@Nullable ProgressIndicator progress,
@NotNull String location,
@NotNull OutputStream output) throws IOException {
String originalText = progress != null ? progress.getText() : null;
substituteContentLength(progress, originalText, -1);
if (progress != null) {
progress.setText2("Downloading " + location);
}
URLConnection urlConnection = HttpConfigurable.getInstance().openConnection(location);
HttpURLConnection httpURLConnection = ObjectUtils.tryCast(urlConnection, HttpURLConnection.class);
try {
urlConnection.setRequestProperty("User-Agent", ApplicationInfoEx.getInstanceEx().getFullApplicationName());
urlConnection.connect();
InputStream in = urlConnection.getInputStream();
int contentLength = urlConnection.getContentLength();
substituteContentLength(progress, originalText, contentLength);
NetUtils.copyStreamContent(progress, in, output, contentLength);
} catch (IOException e) {
String errorMessage = "Can not download '" + location + ", headers: " + urlConnection.getHeaderFields();
if (httpURLConnection != null) {
errorMessage += "', response code: " + httpURLConnection.getResponseCode()
+ ", response message: " + httpURLConnection.getResponseMessage();
}
throw new IOException(errorMessage, e);
}
finally {
if (httpURLConnection != null) {
try {
httpURLConnection.disconnect();
} catch (Exception e) {
LOG.warn("Exception at disconnect()", e);
}
}
}
}
private static void substituteContentLength(@Nullable ProgressIndicator progress, @Nullable String text, int contentLengthInBytes) {
if (progress != null && text != null) {
int ind = text.indexOf(CONTENT_LENGTH_TEMPLATE);
if (ind != -1) {
String mes = formatContentLength(contentLengthInBytes);
String newText = text.substring(0, ind) + mes + text.substring(ind + CONTENT_LENGTH_TEMPLATE.length());
progress.setText(newText);
}
}
}
private static String formatContentLength(int contentLengthInBytes) {
if (contentLengthInBytes < 0) {
return "";
}
final int kilo = 1024;
if (contentLengthInBytes < kilo) {
return ", " + contentLengthInBytes + " bytes";
}
if (contentLengthInBytes < kilo * kilo) {
return String.format(Locale.US, ", %.1f kB", contentLengthInBytes / (1.0 * kilo));
}
return String.format(Locale.US, ", %.1f MB", contentLengthInBytes / (1.0 * kilo * kilo));
}
}