| 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)); |
| } |
| |
| } |