• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.sdklib.internal.repository;
18 
19 import com.android.annotations.VisibleForTesting;
20 import com.android.annotations.VisibleForTesting.Visibility;
21 import com.android.sdklib.SdkConstants;
22 import com.android.sdklib.SdkManager;
23 import com.android.sdklib.io.FileOp;
24 import com.android.sdklib.io.IFileOp;
25 import com.android.sdklib.repository.RepoConstants;
26 
27 import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
28 import org.apache.commons.compress.archivers.zip.ZipFile;
29 
30 import java.io.File;
31 import java.io.FileInputStream;
32 import java.io.FileNotFoundException;
33 import java.io.FileOutputStream;
34 import java.io.IOException;
35 import java.io.InputStream;
36 import java.io.OutputStream;
37 import java.security.MessageDigest;
38 import java.security.NoSuchAlgorithmException;
39 import java.util.Enumeration;
40 import java.util.HashSet;
41 import java.util.Properties;
42 import java.util.Set;
43 
44 /**
45  * Performs the work of installing a given {@link Archive}.
46  */
47 public class ArchiveInstaller {
48 
49     public static final int NUM_MONITOR_INC = 100;
50 
51     /** The current {@link FileOp} to use. Never null. */
52     private final IFileOp mFileOp;
53 
54     /**
55      * Generates an {@link ArchiveInstaller} that relies on the default {@link FileOp}.
56      */
ArchiveInstaller()57     public ArchiveInstaller() {
58         mFileOp = new FileOp();
59     }
60 
61     /**
62      * Generates an {@link ArchiveInstaller} that relies on the given {@link FileOp}.
63      *
64      * @param fileUtils An alternate version of {@link FileOp} to use for file operations.
65      */
ArchiveInstaller(IFileOp fileUtils)66     protected ArchiveInstaller(IFileOp fileUtils) {
67         mFileOp = fileUtils;
68     }
69 
70     /** Returns current {@link FileOp} to use. Never null. */
getFileOp()71     protected IFileOp getFileOp() {
72         return mFileOp;
73     }
74 
75     /**
76      * Install this {@link ArchiveReplacement}s.
77      * A "replacement" is composed of the actual new archive to install
78      * (c.f. {@link ArchiveReplacement#getNewArchive()} and an <em>optional</em>
79      * archive being replaced (c.f. {@link ArchiveReplacement#getReplaced()}.
80      * In the case of a new install, the later should be null.
81      * <p/>
82      * The new archive to install will be skipped if it is incompatible.
83      *
84      * @return True if the archive was installed, false otherwise.
85      */
install(ArchiveReplacement archiveInfo, String osSdkRoot, boolean forceHttp, SdkManager sdkManager, ITaskMonitor monitor)86     public boolean install(ArchiveReplacement archiveInfo,
87             String osSdkRoot,
88             boolean forceHttp,
89             SdkManager sdkManager,
90             ITaskMonitor monitor) {
91 
92         Archive newArchive = archiveInfo.getNewArchive();
93         Package pkg = newArchive.getParentPackage();
94 
95         File archiveFile = null;
96         String name = pkg.getShortDescription();
97 
98         if (pkg instanceof ExtraPackage && !((ExtraPackage) pkg).isPathValid()) {
99             monitor.log("Skipping %1$s: %2$s is not a valid install path.",
100                     name,
101                     ((ExtraPackage) pkg).getPath());
102             return false;
103         }
104 
105         if (newArchive.isLocal()) {
106             // This should never happen.
107             monitor.log("Skipping already installed archive: %1$s for %2$s",
108                     name,
109                     newArchive.getOsDescription());
110             return false;
111         }
112 
113         if (!newArchive.isCompatible()) {
114             monitor.log("Skipping incompatible archive: %1$s for %2$s",
115                     name,
116                     newArchive.getOsDescription());
117             return false;
118         }
119 
120         archiveFile = downloadFile(newArchive, osSdkRoot, monitor, forceHttp);
121         if (archiveFile != null) {
122             // Unarchive calls the pre/postInstallHook methods.
123             if (unarchive(archiveInfo, osSdkRoot, archiveFile, sdkManager, monitor)) {
124                 monitor.log("Installed %1$s", name);
125                 // Delete the temp archive if it exists, only on success
126                 mFileOp.deleteFileOrFolder(archiveFile);
127                 return true;
128             }
129         }
130 
131         return false;
132     }
133 
134     /**
135      * Downloads an archive and returns the temp file with it.
136      * Caller is responsible with deleting the temp file when done.
137      */
138     @VisibleForTesting(visibility=Visibility.PRIVATE)
downloadFile(Archive archive, String osSdkRoot, ITaskMonitor monitor, boolean forceHttp)139     protected File downloadFile(Archive archive,
140             String osSdkRoot,
141             ITaskMonitor monitor,
142             boolean forceHttp) {
143 
144         String pkgName = archive.getParentPackage().getShortDescription();
145         monitor.setDescription("Downloading %1$s", pkgName);
146         monitor.log("Downloading %1$s", pkgName);
147 
148         String link = archive.getUrl();
149         if (!link.startsWith("http://")                          //$NON-NLS-1$
150                 && !link.startsWith("https://")                  //$NON-NLS-1$
151                 && !link.startsWith("ftp://")) {                 //$NON-NLS-1$
152             // Make the URL absolute by prepending the source
153             Package pkg = archive.getParentPackage();
154             SdkSource src = pkg.getParentSource();
155             if (src == null) {
156                 monitor.logError("Internal error: no source for archive %1$s", pkgName);
157                 return null;
158             }
159 
160             // take the URL to the repository.xml and remove the last component
161             // to get the base
162             String repoXml = src.getUrl();
163             int pos = repoXml.lastIndexOf('/');
164             String base = repoXml.substring(0, pos + 1);
165 
166             link = base + link;
167         }
168 
169         if (forceHttp) {
170             link = link.replaceAll("https://", "http://");  //$NON-NLS-1$ //$NON-NLS-2$
171         }
172 
173         // Get the basename of the file we're downloading, i.e. the last component
174         // of the URL
175         int pos = link.lastIndexOf('/');
176         String base = link.substring(pos + 1);
177 
178         // Rather than create a real temp file in the system, we simply use our
179         // temp folder (in the SDK base folder) and use the archive name for the
180         // download. This allows us to reuse or continue downloads.
181 
182         File tmpFolder = getTempFolder(osSdkRoot);
183         if (!mFileOp.isDirectory(tmpFolder)) {
184             if (mFileOp.isFile(tmpFolder)) {
185                 mFileOp.deleteFileOrFolder(tmpFolder);
186             }
187             if (!mFileOp.mkdirs(tmpFolder)) {
188                 monitor.logError("Failed to create directory %1$s", tmpFolder.getPath());
189                 return null;
190             }
191         }
192         File tmpFile = new File(tmpFolder, base);
193 
194         // if the file exists, check its checksum & size. Use it if complete
195         if (mFileOp.exists(tmpFile)) {
196             if (mFileOp.length(tmpFile) == archive.getSize()) {
197                 String chksum = "";                             //$NON-NLS-1$
198                 try {
199                     chksum = fileChecksum(archive.getChecksumType().getMessageDigest(),
200                                           tmpFile,
201                                           monitor);
202                 } catch (NoSuchAlgorithmException e) {
203                     // Ignore.
204                 }
205                 if (chksum.equalsIgnoreCase(archive.getChecksum())) {
206                     // File is good, let's use it.
207                     return tmpFile;
208                 }
209             }
210 
211             // Existing file is either of different size or content.
212             // TODO: continue download when we support continue mode.
213             // Right now, let's simply remove the file and start over.
214             mFileOp.deleteFileOrFolder(tmpFile);
215         }
216 
217         if (fetchUrl(archive, tmpFile, link, pkgName, monitor)) {
218             // Fetching was successful, let's use this file.
219             return tmpFile;
220         } else {
221             // Delete the temp file if we aborted the download
222             // TODO: disable this when we want to support partial downloads.
223             mFileOp.deleteFileOrFolder(tmpFile);
224             return null;
225         }
226     }
227 
228     /**
229      * Computes the SHA-1 checksum of the content of the given file.
230      * Returns an empty string on error (rather than null).
231      */
fileChecksum(MessageDigest digester, File tmpFile, ITaskMonitor monitor)232     private String fileChecksum(MessageDigest digester, File tmpFile, ITaskMonitor monitor) {
233         InputStream is = null;
234         try {
235             is = new FileInputStream(tmpFile);
236 
237             byte[] buf = new byte[65536];
238             int n;
239 
240             while ((n = is.read(buf)) >= 0) {
241                 if (n > 0) {
242                     digester.update(buf, 0, n);
243                 }
244             }
245 
246             return getDigestChecksum(digester);
247 
248         } catch (FileNotFoundException e) {
249             // The FNF message is just the URL. Make it a bit more useful.
250             monitor.logError("File not found: %1$s", e.getMessage());
251 
252         } catch (Exception e) {
253             monitor.logError("%1$s", e.getMessage());   //$NON-NLS-1$
254 
255         } finally {
256             if (is != null) {
257                 try {
258                     is.close();
259                 } catch (IOException e) {
260                     // pass
261                 }
262             }
263         }
264 
265         return "";  //$NON-NLS-1$
266     }
267 
268     /**
269      * Returns the SHA-1 from a {@link MessageDigest} as an hex string
270      * that can be compared with {@link Archive#getChecksum()}.
271      */
getDigestChecksum(MessageDigest digester)272     private String getDigestChecksum(MessageDigest digester) {
273         int n;
274         // Create an hex string from the digest
275         byte[] digest = digester.digest();
276         n = digest.length;
277         String hex = "0123456789abcdef";                     //$NON-NLS-1$
278         char[] hexDigest = new char[n * 2];
279         for (int i = 0; i < n; i++) {
280             int b = digest[i] & 0x0FF;
281             hexDigest[i*2 + 0] = hex.charAt(b >>> 4);
282             hexDigest[i*2 + 1] = hex.charAt(b & 0x0f);
283         }
284 
285         return new String(hexDigest);
286     }
287 
288     /**
289      * Actually performs the download.
290      * Also computes the SHA1 of the file on the fly.
291      * <p/>
292      * Success is defined as downloading as many bytes as was expected and having the same
293      * SHA1 as expected. Returns true on success or false if any of those checks fail.
294      * <p/>
295      * Increments the monitor by {@link #NUM_MONITOR_INC}.
296      */
fetchUrl(Archive archive, File tmpFile, String urlString, String pkgName, ITaskMonitor monitor)297     private boolean fetchUrl(Archive archive,
298             File tmpFile,
299             String urlString,
300             String pkgName,
301             ITaskMonitor monitor) {
302 
303         FileOutputStream os = null;
304         InputStream is = null;
305         try {
306             is = UrlOpener.openUrl(urlString, monitor);
307             os = new FileOutputStream(tmpFile);
308 
309             MessageDigest digester = archive.getChecksumType().getMessageDigest();
310 
311             byte[] buf = new byte[65536];
312             int n;
313 
314             long total = 0;
315             long size = archive.getSize();
316             long inc = size / NUM_MONITOR_INC;
317             long next_inc = inc;
318 
319             long startMs = System.currentTimeMillis();
320             long nextMs = startMs + 2000;  // start update after 2 seconds
321 
322             while ((n = is.read(buf)) >= 0) {
323                 if (n > 0) {
324                     os.write(buf, 0, n);
325                     digester.update(buf, 0, n);
326                 }
327 
328                 long timeMs = System.currentTimeMillis();
329 
330                 total += n;
331                 if (total >= next_inc) {
332                     monitor.incProgress(1);
333                     next_inc += inc;
334                 }
335 
336                 if (timeMs > nextMs) {
337                     long delta = timeMs - startMs;
338                     if (total > 0 && delta > 0) {
339                         // percent left to download
340                         int percent = (int) (100 * total / size);
341                         // speed in KiB/s
342                         float speed = (float)total / (float)delta * (1000.f / 1024.f);
343                         // time left to download the rest at the current KiB/s rate
344                         int timeLeft = (speed > 1e-3) ?
345                                                (int)(((size - total) / 1024.0f) / speed) :
346                                                0;
347                         String timeUnit = "seconds";
348                         if (timeLeft > 120) {
349                             timeUnit = "minutes";
350                             timeLeft /= 60;
351                         }
352 
353                         monitor.setDescription(
354                                 "Downloading %1$s (%2$d%%, %3$.0f KiB/s, %4$d %5$s left)",
355                                 pkgName,
356                                 percent,
357                                 speed,
358                                 timeLeft,
359                                 timeUnit);
360                     }
361                     nextMs = timeMs + 1000;  // update every second
362                 }
363 
364                 if (monitor.isCancelRequested()) {
365                     monitor.log("Download aborted by user at %1$d bytes.", total);
366                     return false;
367                 }
368 
369             }
370 
371             if (total != size) {
372                 monitor.logError(
373                         "Download finished with wrong size. Expected %1$d bytes, got %2$d bytes.",
374                         size, total);
375                 return false;
376             }
377 
378             // Create an hex string from the digest
379             String actual   = getDigestChecksum(digester);
380             String expected = archive.getChecksum();
381             if (!actual.equalsIgnoreCase(expected)) {
382                 monitor.logError("Download finished with wrong checksum. Expected %1$s, got %2$s.",
383                         expected, actual);
384                 return false;
385             }
386 
387             return true;
388 
389         } catch (FileNotFoundException e) {
390             // The FNF message is just the URL. Make it a bit more useful.
391             monitor.logError("File not found: %1$s", e.getMessage());
392 
393         } catch (Exception e) {
394             monitor.logError("%1$s", e.getMessage());   //$NON-NLS-1$
395 
396         } finally {
397             if (os != null) {
398                 try {
399                     os.close();
400                 } catch (IOException e) {
401                     // pass
402                 }
403             }
404 
405             if (is != null) {
406                 try {
407                     is.close();
408                 } catch (IOException e) {
409                     // pass
410                 }
411             }
412         }
413 
414         return false;
415     }
416 
417     /**
418      * Install the given archive in the given folder.
419      */
unarchive(ArchiveReplacement archiveInfo, String osSdkRoot, File archiveFile, SdkManager sdkManager, ITaskMonitor monitor)420     private boolean unarchive(ArchiveReplacement archiveInfo,
421             String osSdkRoot,
422             File archiveFile,
423             SdkManager sdkManager,
424             ITaskMonitor monitor) {
425         boolean success = false;
426         Archive newArchive = archiveInfo.getNewArchive();
427         Package pkg = newArchive.getParentPackage();
428         String pkgName = pkg.getShortDescription();
429         monitor.setDescription("Installing %1$s", pkgName);
430         monitor.log("Installing %1$s", pkgName);
431 
432         // Ideally we want to always unzip in a temp folder which name depends on the package
433         // type (e.g. addon, tools, etc.) and then move the folder to the destination folder.
434         // If the destination folder exists, it will be renamed and deleted at the very
435         // end if everything succeeded. This provides a nice atomic swap and should leave the
436         // original folder untouched in case something wrong (e.g. program crash) in the
437         // middle of the unzip operation.
438         //
439         // However that doesn't work on Windows, we always end up not being able to move the
440         // new folder. There are actually 2 cases:
441         // A- A process such as a the explorer is locking the *old* folder or a file inside
442         //    (e.g. adb.exe)
443         //    In this case we really shouldn't be tried to work around it and we need to let
444         //    the user know and let it close apps that access that folder.
445         // B- A process is locking the *new* folder. Very often this turns to be a file indexer
446         //    or an anti-virus that is busy scanning the new folder that we just unzipped.
447         //
448         // So we're going to change the strategy:
449         // 1- Try to move the old folder to a temp/old folder. This might fail in case of issue A.
450         //    Note: for platform-tools, we can try killing adb first.
451         //    If it still fails, we do nothing and ask the user to terminate apps that can be
452         //    locking that folder.
453         // 2- Once the old folder is out of the way, we unzip the archive directly into the
454         //    optimal new location. We no longer unzip it in a temp folder and move it since we
455         //    know that's what fails in most of the cases.
456         // 3- If the unzip fails, remove everything and try to restore the old folder by doing
457         //    a *copy* in place and not a folder move (which will likely fail too).
458 
459         String pkgKind = pkg.getClass().getSimpleName();
460 
461         File destFolder = null;
462         File oldDestFolder = null;
463 
464         try {
465             // -0- Compute destination directory and check install pre-conditions
466 
467             destFolder = pkg.getInstallFolder(osSdkRoot, sdkManager);
468 
469             if (destFolder == null) {
470                 // this should not seriously happen.
471                 monitor.log("Failed to compute installation directory for %1$s.", pkgName);
472                 return false;
473             }
474 
475             if (!pkg.preInstallHook(newArchive, monitor, osSdkRoot, destFolder)) {
476                 monitor.log("Skipping archive: %1$s", pkgName);
477                 return false;
478             }
479 
480             // -1- move old folder.
481 
482             if (mFileOp.exists(destFolder)) {
483                 // Create a new temp/old dir
484                 if (oldDestFolder == null) {
485                     oldDestFolder = getNewTempFolder(osSdkRoot, pkgKind, "old");  //$NON-NLS-1$
486                 }
487                 if (oldDestFolder == null) {
488                     // this should not seriously happen.
489                     monitor.logError("Failed to find a temp directory in %1$s.", osSdkRoot);
490                     return false;
491                 }
492 
493                 // Try to move the current dest dir to the temp/old one. Tell the user if it failed.
494                 while(true) {
495                     if (!moveFolder(destFolder, oldDestFolder)) {
496                         monitor.logError("Failed to rename directory %1$s to %2$s.",
497                                 destFolder.getPath(), oldDestFolder.getPath());
498 
499                         if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) {
500                             String msg = String.format(
501                                     "-= Warning ! =-\n" +
502                                     "A folder failed to be moved. On Windows this " +
503                                     "typically means that a program is using that folder (for " +
504                                     "example Windows Explorer or your anti-virus software.)\n" +
505                                     "Please momentarily deactivate your anti-virus software or " +
506                                     "close any running programs that may be accessing the " +
507                                     "directory '%1$s'.\n" +
508                                     "When ready, press YES to try again.",
509                                     destFolder.getPath());
510 
511                             if (monitor.displayPrompt("SDK Manager: failed to install", msg)) {
512                                 // loop, trying to rename the temp dir into the destination
513                                 continue;
514                             } else {
515                                 return false;
516                             }
517                         }
518                     }
519                     break;
520                 }
521             }
522 
523             assert !mFileOp.exists(destFolder);
524 
525             // -2- Unzip new content directly in place.
526 
527             if (!mFileOp.mkdirs(destFolder)) {
528                 monitor.logError("Failed to create directory %1$s", destFolder.getPath());
529                 return false;
530             }
531 
532             if (!unzipFolder(archiveFile, newArchive.getSize(), destFolder, pkgName, monitor)) {
533                 return false;
534             }
535 
536             if (!generateSourceProperties(newArchive, destFolder)) {
537                 monitor.logError("Failed to generate source.properties in directory %1$s",
538                         destFolder.getPath());
539                 return false;
540             }
541 
542             // In case of success, if we were replacing an archive
543             // and the older one had a different path, remove it now.
544             Archive oldArchive = archiveInfo.getReplaced();
545             if (oldArchive != null && oldArchive.isLocal()) {
546                 String oldPath = oldArchive.getLocalOsPath();
547                 File oldFolder = oldPath == null ? null : new File(oldPath);
548                 if (oldFolder == null && oldArchive.getParentPackage() != null) {
549                     oldFolder = oldArchive.getParentPackage().getInstallFolder(
550                             osSdkRoot, sdkManager);
551                 }
552                 if (oldFolder != null && mFileOp.exists(oldFolder) &&
553                         !oldFolder.equals(destFolder)) {
554                     monitor.logVerbose("Removing old archive at %1$s", oldFolder.getAbsolutePath());
555                     mFileOp.deleteFileOrFolder(oldFolder);
556                 }
557             }
558 
559             success = true;
560             pkg.postInstallHook(newArchive, monitor, destFolder);
561             return true;
562 
563         } finally {
564             if (!success) {
565                 // In case of failure, we try to restore the old folder content.
566                 if (oldDestFolder != null) {
567                     restoreFolder(oldDestFolder, destFolder);
568                 }
569 
570                 // We also call the postInstallHool with a null directory to give a chance
571                 // to the archive to cleanup after preInstallHook.
572                 pkg.postInstallHook(newArchive, monitor, null /*installDir*/);
573             }
574 
575             // Cleanup if the unzip folder is still set.
576             mFileOp.deleteFileOrFolder(oldDestFolder);
577         }
578     }
579 
580     /**
581      * Tries to rename/move a folder.
582      * <p/>
583      * Contract:
584      * <ul>
585      * <li> When we start, oldDir must exist and be a directory. newDir must not exist. </li>
586      * <li> On successful completion, oldDir must not exists.
587      *      newDir must exist and have the same content. </li>
588      * <li> On failure completion, oldDir must have the same content as before.
589      *      newDir must not exist. </li>
590      * </ul>
591      * <p/>
592      * The simple "rename" operation on a folder can typically fail on Windows for a variety
593      * of reason, in fact as soon as a single process holds a reference on a directory. The
594      * most common case are the Explorer, the system's file indexer, Tortoise SVN cache or
595      * an anti-virus that are busy indexing a new directory having been created.
596      *
597      * @param oldDir The old location to move. It must exist and be a directory.
598      * @param newDir The new location where to move. It must not exist.
599      * @return True if the move succeeded. On failure, we try hard to not have touched the old
600      *  directory in order not to loose its content.
601      */
moveFolder(File oldDir, File newDir)602     private boolean moveFolder(File oldDir, File newDir) {
603         // This is a simple folder rename that works on Linux/Mac all the time.
604         //
605         // On Windows this might fail if an indexer is busy looking at a new directory
606         // (e.g. right after we unzip our archive), so it fails let's be nice and give
607         // it a bit of time to succeed.
608         for (int i = 0; i < 5; i++) {
609             if (mFileOp.renameTo(oldDir, newDir)) {
610                 return true;
611             }
612             try {
613                 Thread.sleep(500 /*ms*/);
614             } catch (InterruptedException e) {
615                 // ignore
616             }
617         }
618 
619         return false;
620     }
621 
622     /**
623      * Unzips a zip file into the given destination directory.
624      *
625      * The archive file MUST have a unique "root" folder.
626      * This root folder is skipped when unarchiving.
627      */
628     @SuppressWarnings("unchecked")
629     @VisibleForTesting(visibility=Visibility.PRIVATE)
unzipFolder(File archiveFile, long compressedSize, File unzipDestFolder, String pkgName, ITaskMonitor monitor)630     protected boolean unzipFolder(File archiveFile,
631             long compressedSize,
632             File unzipDestFolder,
633             String pkgName,
634             ITaskMonitor monitor) {
635 
636         ZipFile zipFile = null;
637         try {
638             zipFile = new ZipFile(archiveFile);
639 
640             // figure if we'll need to set the unix permissions
641             boolean usingUnixPerm =
642                     SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN ||
643                     SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_LINUX;
644 
645             // To advance the percent and the progress bar, we don't know the number of
646             // items left to unzip. However we know the size of the archive and the size of
647             // each uncompressed item. The zip file format overhead is negligible so that's
648             // a good approximation.
649             long incStep = compressedSize / NUM_MONITOR_INC;
650             long incTotal = 0;
651             long incCurr = 0;
652             int lastPercent = 0;
653 
654             byte[] buf = new byte[65536];
655 
656             Enumeration<ZipArchiveEntry> entries = zipFile.getEntries();
657             while (entries.hasMoreElements()) {
658                 ZipArchiveEntry entry = entries.nextElement();
659 
660                 String name = entry.getName();
661 
662                 // ZipFile entries should have forward slashes, but not all Zip
663                 // implementations can be expected to do that.
664                 name = name.replace('\\', '/');
665 
666                 // Zip entries are always packages in a top-level directory
667                 // (e.g. docs/index.html). However we want to use our top-level
668                 // directory so we drop the first segment of the path name.
669                 int pos = name.indexOf('/');
670                 if (pos < 0 || pos == name.length() - 1) {
671                     continue;
672                 } else {
673                     name = name.substring(pos + 1);
674                 }
675 
676                 File destFile = new File(unzipDestFolder, name);
677 
678                 if (name.endsWith("/")) {  //$NON-NLS-1$
679                     // Create directory if it doesn't exist yet. This allows us to create
680                     // empty directories.
681                     if (!mFileOp.isDirectory(destFile) && !mFileOp.mkdirs(destFile)) {
682                         monitor.logError("Failed to create directory %1$s",
683                                 destFile.getPath());
684                         return false;
685                     }
686                     continue;
687                 } else if (name.indexOf('/') != -1) {
688                     // Otherwise it's a file in a sub-directory.
689                     // Make sure the parent directory has been created.
690                     File parentDir = destFile.getParentFile();
691                     if (!mFileOp.isDirectory(parentDir)) {
692                         if (!mFileOp.mkdirs(parentDir)) {
693                             monitor.logError("Failed to create directory %1$s",
694                                     parentDir.getPath());
695                             return false;
696                         }
697                     }
698                 }
699 
700                 FileOutputStream fos = null;
701                 try {
702                     fos = new FileOutputStream(destFile);
703                     int n;
704                     InputStream entryContent = zipFile.getInputStream(entry);
705                     while ((n = entryContent.read(buf)) != -1) {
706                         if (n > 0) {
707                             fos.write(buf, 0, n);
708                         }
709                     }
710                 } finally {
711                     if (fos != null) {
712                         fos.close();
713                     }
714                 }
715 
716                 // if needed set the permissions.
717                 if (usingUnixPerm && mFileOp.isFile(destFile)) {
718                     // get the mode and test if it contains the executable bit
719                     int mode = entry.getUnixMode();
720                     if ((mode & 0111) != 0) {
721                         mFileOp.setExecutablePermission(destFile);
722                     }
723                 }
724 
725                 // Increment progress bar to match. We update only between files.
726                 for(incTotal += entry.getCompressedSize(); incCurr < incTotal; incCurr += incStep) {
727                     monitor.incProgress(1);
728                 }
729 
730                 int percent = (int) (100 * incTotal / compressedSize);
731                 if (percent != lastPercent) {
732                     monitor.setDescription("Unzipping %1$s (%2$d%%)", pkgName, percent);
733                     lastPercent = percent;
734                 }
735 
736                 if (monitor.isCancelRequested()) {
737                     return false;
738                 }
739             }
740 
741             return true;
742 
743         } catch (IOException e) {
744             monitor.logError("Unzip failed: %1$s", e.getMessage());
745 
746         } finally {
747             if (zipFile != null) {
748                 try {
749                     zipFile.close();
750                 } catch (IOException e) {
751                     // pass
752                 }
753             }
754         }
755 
756         return false;
757     }
758 
759     /**
760      * Returns an unused temp folder path in the form of osBasePath/temp/prefix.suffixNNN.
761      * <p/>
762      * This does not actually <em>create</em> the folder. It just scan the base path for
763      * a free folder name to use and returns the file to use to reference it.
764      * <p/>
765      * This operation is not atomic so there's no guarantee the folder can't get
766      * created in between. This is however unlikely and the caller can assume the
767      * returned folder does not exist yet.
768      * <p/>
769      * Returns null if no such folder can be found (e.g. if all candidates exist,
770      * which is rather unlikely) or if the base temp folder cannot be created.
771      */
getNewTempFolder(String osBasePath, String prefix, String suffix)772     private File getNewTempFolder(String osBasePath, String prefix, String suffix) {
773         File baseTempFolder = getTempFolder(osBasePath);
774 
775         if (!mFileOp.isDirectory(baseTempFolder)) {
776             if (mFileOp.isFile(baseTempFolder)) {
777                 mFileOp.deleteFileOrFolder(baseTempFolder);
778             }
779             if (!mFileOp.mkdirs(baseTempFolder)) {
780                 return null;
781             }
782         }
783 
784         for (int i = 1; i < 100; i++) {
785             File folder = new File(baseTempFolder,
786                     String.format("%1$s.%2$s%3$02d", prefix, suffix, i));  //$NON-NLS-1$
787             if (!mFileOp.exists(folder)) {
788                 return folder;
789             }
790         }
791         return null;
792     }
793 
794     /**
795      * Returns the single fixed "temp" folder used by the SDK Manager.
796      * This folder is always at osBasePath/temp.
797      * <p/>
798      * This does not actually <em>create</em> the folder.
799      */
getTempFolder(String osBasePath)800     private File getTempFolder(String osBasePath) {
801         File baseTempFolder = new File(osBasePath, RepoConstants.FD_TEMP);
802         return baseTempFolder;
803     }
804 
805     /**
806      * Generates a source.properties in the destination folder that contains all the infos
807      * relevant to this archive, this package and the source so that we can reload them
808      * locally later.
809      */
810     @VisibleForTesting(visibility=Visibility.PRIVATE)
generateSourceProperties(Archive archive, File unzipDestFolder)811     protected boolean generateSourceProperties(Archive archive, File unzipDestFolder) {
812         Properties props = new Properties();
813 
814         archive.saveProperties(props);
815 
816         Package pkg = archive.getParentPackage();
817         if (pkg != null) {
818             pkg.saveProperties(props);
819         }
820 
821         OutputStream fos = null;
822         try {
823             File f = new File(unzipDestFolder, SdkConstants.FN_SOURCE_PROP);
824 
825             fos = mFileOp.newFileOutputStream(f);
826 
827             props.store(fos, "## Android Tool: Source of this archive.");  //$NON-NLS-1$
828 
829             return true;
830         } catch (IOException e) {
831             e.printStackTrace();
832         } finally {
833             if (fos != null) {
834                 try {
835                     fos.close();
836                 } catch (IOException e) {
837                 }
838             }
839         }
840 
841         return false;
842     }
843 
844     /**
845      * Recursively restore srcFolder into destFolder by performing a copy of the file
846      * content rather than rename/moves.
847      *
848      * @param srcFolder The source folder to restore.
849      * @param destFolder The destination folder where to restore.
850      * @return True if the folder was successfully restored, false if it was not at all or
851      *         only partially restored.
852      */
restoreFolder(File srcFolder, File destFolder)853     private boolean restoreFolder(File srcFolder, File destFolder) {
854         boolean result = true;
855 
856         // Process sub-folders first
857         File[] srcFiles = mFileOp.listFiles(srcFolder);
858         if (srcFiles == null) {
859             // Source does not exist. That is quite odd.
860             return false;
861         }
862 
863         if (mFileOp.isFile(destFolder)) {
864             if (!mFileOp.delete(destFolder)) {
865                 // There's already a file in there where we want a directory and
866                 // we can't delete it. This is rather unexpected. Just give up on
867                 // that folder.
868                 return false;
869             }
870         } else if (!mFileOp.isDirectory(destFolder)) {
871             mFileOp.mkdirs(destFolder);
872         }
873 
874         // Get all the files and dirs of the current destination.
875         // We are not going to clean up the destination first.
876         // Instead we'll copy over and just remove any remaining files or directories.
877         Set<File> destDirs = new HashSet<File>();
878         Set<File> destFiles = new HashSet<File>();
879         File[] files = mFileOp.listFiles(destFolder);
880         if (files != null) {
881             for (File f : files) {
882                 if (mFileOp.isDirectory(f)) {
883                     destDirs.add(f);
884                 } else {
885                     destFiles.add(f);
886                 }
887             }
888         }
889 
890         // First restore all source directories.
891         for (File dir : srcFiles) {
892             if (mFileOp.isDirectory(dir)) {
893                 File d = new File(destFolder, dir.getName());
894                 destDirs.remove(d);
895                 if (!restoreFolder(dir, d)) {
896                     result = false;
897                 }
898             }
899         }
900 
901         // Remove any remaining directories not processed above.
902         for (File dir : destDirs) {
903             mFileOp.deleteFileOrFolder(dir);
904         }
905 
906         // Copy any source files over to the destination.
907         for (File file : srcFiles) {
908             if (mFileOp.isFile(file)) {
909                 File f = new File(destFolder, file.getName());
910                 destFiles.remove(f);
911                 try {
912                     mFileOp.copyFile(file, f);
913                 } catch (IOException e) {
914                     result = false;
915                 }
916             }
917         }
918 
919         // Remove any remaining files not processed above.
920         for (File file : destFiles) {
921             mFileOp.deleteFileOrFolder(file);
922         }
923 
924         return result;
925     }
926 }
927