Bug 1220145 - DownloadContentService: Check space on disk before downloading content. r?rnewman draft
authorSebastian Kaspari <s.kaspari@gmail.com>
Sat, 19 Dec 2015 23:01:11 +0100
changeset 316572 afd0eab216f1aa8837034751e2a15a310b25681c
parent 316420 8d5e8f88ee3b298347822df6be1ad740529f3450
child 316676 7637945b99750aaa50534586ba5dfecf0a1332e2
child 316677 4816b6c6fafed397c484337d2b95b7cfdc62da77
child 316678 2edfef0634f35f68b3edf043acb8e3b1e08fa056
push id8568
push users.kaspari@gmail.com
push dateSat, 19 Dec 2015 22:01:41 +0000
reviewersrnewman
bugs1220145
milestone46.0a1
Bug 1220145 - DownloadContentService: Check space on disk before downloading content. r?rnewman
mobile/android/base/java/org/mozilla/gecko/dlc/DownloadAction.java
mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/TestDownloadAction.java
--- a/mobile/android/base/java/org/mozilla/gecko/dlc/DownloadAction.java
+++ b/mobile/android/base/java/org/mozilla/gecko/dlc/DownloadAction.java
@@ -82,16 +82,21 @@ public class DownloadAction extends Base
                 if (destinationFile.exists() && verify(destinationFile, content.getChecksum())) {
                     Log.d(LOGTAG, "Content already exists and is up-to-date.");
                     catalog.markAsDownloaded(content);
                     continue;
                 }
 
                 temporaryFile = createTemporaryFile(context, content);
 
+                if (!hasEnoughDiskSpace(content, destinationFile, temporaryFile)) {
+                    Log.d(LOGTAG, "Not enough disk space to save content. Skipping download.");
+                    continue;
+                }
+
                 // TODO: Check space on disk before downloading content (bug 1220145)
                 final String url = createDownloadURL(content);
 
                 if (!temporaryFile.exists() || temporaryFile.length() < content.getSize()) {
                     download(client, url, temporaryFile);
                 }
 
                 if (!verify(temporaryFile, content.getDownloadChecksum())) {
@@ -304,9 +309,24 @@ public class DownloadAction extends Base
             // We could not copy the temporary file to its destination: Keep the temporary file and
             // try again the next time we run.
             throw new RecoverableDownloadContentException(e);
         } finally {
             IOUtils.safeStreamClose(inputStream);
             IOUtils.safeStreamClose(outputStream);
         }
     }
+
+    protected boolean hasEnoughDiskSpace(DownloadContent content, File destinationFile, File temporaryFile) {
+        final File temporaryDirectory = temporaryFile.getParentFile();
+        if (temporaryDirectory.getUsableSpace() < content.getSize()) {
+            return false;
+        }
+
+        final File destinationDirectory = destinationFile.getParentFile();
+        // We need some more space to extract the file (getSize() returns the uncompressed size)
+        if (destinationDirectory.getUsableSpace() < content.getSize() * 2) {
+            return false;
+        }
+
+        return true;
+    }
 }
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/TestDownloadAction.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/TestDownloadAction.java
@@ -16,38 +16,29 @@ import org.mozilla.gecko.dlc.catalog.Dow
 import android.content.Context;
 
 import org.robolectric.RuntimeEnvironment;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.IOException;
+import java.util.Arrays;
 import java.util.Collections;
 
 import ch.boye.httpclientandroidlib.HttpEntity;
 import ch.boye.httpclientandroidlib.HttpResponse;
 import ch.boye.httpclientandroidlib.HttpStatus;
 import ch.boye.httpclientandroidlib.StatusLine;
 import ch.boye.httpclientandroidlib.client.HttpClient;
 import ch.boye.httpclientandroidlib.client.methods.HttpGet;
 import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
 
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyBoolean;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
+import static org.mockito.Matchers.*;
+import static org.mockito.Mockito.*;
 
 /**
  * DownloadAction: Download content that has been scheduled during "study" or "verify".
  */
 @RunWith(TestRunner.class)
 public class TestDownloadAction {
     private static final String TEST_URL = "http://example.org";
 
@@ -168,18 +159,17 @@ public class TestDownloadAction {
                 .build();
 
         DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
         doReturn(Collections.singletonList(content)).when(catalog).getScheduledDownloads();
 
         DownloadAction action = spy(new DownloadAction(null));
         doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
 
-        File file = mock(File.class);
-        doReturn(false).when(file).exists();
+        File file = mockNotExistingFile();
         doReturn(file).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
         doReturn(file).when(action).getDestinationFile(RuntimeEnvironment.application, content);
 
         doReturn(false).when(action).verify(eq(file), anyString());
         doNothing().when(action).download(any(HttpClient.class), anyString(), eq(file));
         doReturn(true).when(action).verify(eq(file), anyString());
         doNothing().when(action).extract(eq(file), eq(file), anyString());
 
@@ -208,26 +198,26 @@ public class TestDownloadAction {
                 .build();
 
         DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
         doReturn(Collections.singletonList(content)).when(catalog).getScheduledDownloads();
 
         DownloadAction action = spy(new DownloadAction(null));
         doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
 
-        File temporaryFile = mockTemporaryFile(true, 1337L);
+        File temporaryFile = mockFileWithSize(1337L);
         doReturn(temporaryFile).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
 
         ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
         doReturn(outputStream).when(action).openFile(eq(temporaryFile), anyBoolean());
 
         HttpClient client = mockHttpClient(HttpStatus.SC_PARTIAL_CONTENT, "HelloWorld");
         doReturn(client).when(action).buildHttpClient();
 
-        File destinationFile = mock(File.class);
+        File destinationFile = mockNotExistingFile();
         doReturn(destinationFile).when(action).getDestinationFile(RuntimeEnvironment.application, content);
 
         doReturn(true).when(action).verify(eq(temporaryFile), anyString());
         doNothing().when(action).extract(eq(temporaryFile), eq(destinationFile), anyString());
 
         action.perform(RuntimeEnvironment.application, catalog);
 
         ArgumentCaptor<HttpGet> argument = ArgumentCaptor.forClass(HttpGet.class);
@@ -259,32 +249,32 @@ public class TestDownloadAction {
                 .build();
 
         DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
         doReturn(Collections.singletonList(content)).when(catalog).getScheduledDownloads();
 
         DownloadAction action = spy(new DownloadAction(null));
         doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
 
-        File temporaryFile = mockTemporaryFile(true, 1337L);
+        File temporaryFile = mockFileWithSize(1337L);
         doReturn(temporaryFile).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
 
         ByteArrayOutputStream outputStream = spy(new ByteArrayOutputStream());
         doReturn(outputStream).when(action).openFile(eq(temporaryFile), anyBoolean());
         doThrow(IOException.class).when(outputStream).write(any(byte[].class), anyInt(), anyInt());
 
         HttpClient client = mockHttpClient(HttpStatus.SC_PARTIAL_CONTENT, "HelloWorld");
         doReturn(client).when(action).buildHttpClient();
 
-        File destinationFile = mock(File.class);
-        doReturn(destinationFile).when(action).getDestinationFile(RuntimeEnvironment.application, content);
+        doReturn(mockNotExistingFile()).when(action).getDestinationFile(RuntimeEnvironment.application, content);
 
         action.perform(RuntimeEnvironment.application, catalog);
 
         verify(catalog, never()).markAsDownloaded(content);
+        verify(action, never()).verify(any(File.class), anyString());
         verify(temporaryFile, never()).delete();
     }
 
     /**
      * Scenario: Partially downloaded file is already complete.
      *
      * Verify that:
      *  * No download request is made
@@ -300,20 +290,20 @@ public class TestDownloadAction {
                 .build();
 
         DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
         doReturn(Collections.singletonList(content)).when(catalog).getScheduledDownloads();
 
         DownloadAction action = spy(new DownloadAction(null));
         doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
 
-        File temporaryFile = mockTemporaryFile(true, 1337L);
+        File temporaryFile = mockFileWithSize(1337L);
         doReturn(temporaryFile).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
 
-        File destinationFile = mock(File.class);
+        File destinationFile = mockNotExistingFile();
         doReturn(destinationFile).when(action).getDestinationFile(RuntimeEnvironment.application, content);
 
         doReturn(true).when(action).verify(eq(temporaryFile), anyString());
         doNothing().when(action).extract(eq(temporaryFile), eq(destinationFile), anyString());
 
         action.perform(RuntimeEnvironment.application, catalog);
 
         verify(action, never()).download(any(HttpClient.class), anyString(), eq(temporaryFile));
@@ -341,34 +331,142 @@ public class TestDownloadAction {
         DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
         doReturn(Collections.singletonList(content)).when(catalog).getScheduledDownloads();
 
         DownloadAction action = spy(new DownloadAction(null));
         doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
         doNothing().when(action).download(any(HttpClient.class), anyString(), any(File.class));
         doReturn(false).when(action).verify(any(File.class), anyString());
 
-        File temporaryFile = mockTemporaryFile(true, 1337L);
+        File temporaryFile = mockNotExistingFile();
         doReturn(temporaryFile).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
 
-        File destinationFile = mock(File.class);
+        File destinationFile = mockNotExistingFile();
         doReturn(destinationFile).when(action).getDestinationFile(RuntimeEnvironment.application, content);
 
         action.perform(RuntimeEnvironment.application, catalog);
 
         verify(temporaryFile).delete();
         verify(action, never()).extract(any(File.class), any(File.class), anyString());
         verify(catalog, never()).markAsDownloaded(content);
     }
 
-    private static File mockTemporaryFile(boolean exists, long length) {
-        File temporaryFile = mock(File.class);
-        doReturn(exists).when(temporaryFile).exists();
-        doReturn(length).when(temporaryFile).length();
-        return temporaryFile;
+    /**
+     * Scenario: Not enough storage space for content is available.
+     *
+     * Verify that:
+     *  * No download will per performed
+     */
+    @Test
+    public void testNoDownloadIsPerformedIfNotEnoughStorageIsAvailable() throws Exception {
+        DownloadContent content = createFontWithSize(1337L);
+        DownloadContentCatalog catalog = mockCatalogWithScheduledDownloads(content);
+
+        DownloadAction action = spy(new DownloadAction(null));
+        doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
+        doReturn(true).when(action).isConnectedToNetwork(RuntimeEnvironment.application);
+
+        File temporaryFile = mockNotExistingFile();
+        doReturn(temporaryFile).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
+
+        File destinationFile = mockNotExistingFile();
+        doReturn(destinationFile).when(action).getDestinationFile(RuntimeEnvironment.application, content);
+
+        doReturn(true).when(action).hasEnoughDiskSpace(content, destinationFile, temporaryFile);
+
+        verify(action, never()).buildHttpClient();
+        verify(action, never()).download(any(HttpClient.class), anyString(), any(File.class));
+        verify(action, never()).verify(any(File.class), anyString());
+        verify(catalog, never()).markAsDownloaded(content);
+    }
+
+    /**
+     * Scenario: Not enough storage space for temporary file available.
+     *
+     * Verify that:
+     *  * hasEnoughDiskSpace() returns false
+     */
+    @Test
+    public void testWithNotEnoughSpaceForTemporaryFile() {
+        DownloadContent content = createFontWithSize(2048);
+        File destinationFile = mockNotExistingFile();
+        File temporaryFile = mockNotExistingFileWithUsableSpace(1024);
+
+        DownloadAction action = new DownloadAction(null);
+        Assert.assertFalse(action.hasEnoughDiskSpace(content, destinationFile, temporaryFile));
+    }
+
+    /**
+     * Scenario: Not enough storage space for destination file available.
+     *
+     * Verify that:
+     *  * hasEnoughDiskSpace() returns false
+     */
+    @Test
+    public void testWithNotEnoughSpaceForDestinationFile() {
+        DownloadContent content = createFontWithSize(2048);
+        File destinationFile = mockNotExistingFileWithUsableSpace(1024);
+        File temporaryFile = mockNotExistingFile();
+
+        DownloadAction action = new DownloadAction(null);
+        Assert.assertFalse(action.hasEnoughDiskSpace(content, destinationFile, temporaryFile));
+    }
+
+    /**
+     * Scenario: Enough storage space for temporary and destination file available.
+     *
+     * Verify that:
+     *  * hasEnoughDiskSpace() returns true
+     */
+    @Test
+    public void testWithEnoughSpaceForEverything() {
+        DownloadContent content = createFontWithSize(2048);
+        File destinationFile = mockNotExistingFileWithUsableSpace(4096);
+        File temporaryFile = mockNotExistingFileWithUsableSpace(4096);
+
+        DownloadAction action = new DownloadAction(null);
+        Assert.assertTrue(action.hasEnoughDiskSpace(content, destinationFile, temporaryFile));
+    }
+
+    private DownloadContent createFontWithSize(long size) {
+        return new DownloadContent.Builder()
+                .setKind(DownloadContent.KIND_FONT)
+                .setType(DownloadContent.TYPE_ASSET_ARCHIVE)
+                .setSize(size)
+                .build();
+    }
+
+    private DownloadContentCatalog mockCatalogWithScheduledDownloads(DownloadContent... content) {
+        DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
+        doReturn(Arrays.asList(content)).when(catalog).getScheduledDownloads();
+        return catalog;
+    }
+
+    private static File mockNotExistingFile() {
+        return mockFileWithUsableSpace(false, 0, Long.MAX_VALUE);
+    }
+
+    private static File mockNotExistingFileWithUsableSpace(long usableSpace) {
+        return mockFileWithUsableSpace(false, 0, usableSpace);
+    }
+
+    private static File mockFileWithSize(long length) {
+        return mockFileWithUsableSpace(true, length, Long.MAX_VALUE);
+    }
+
+    private static File mockFileWithUsableSpace(boolean exists, long length, long usableSpace) {
+        File file = mock(File.class);
+        doReturn(exists).when(file).exists();
+        doReturn(length).when(file).length();
+
+        File parentFile = mock(File.class);
+        doReturn(usableSpace).when(parentFile).getUsableSpace();
+        doReturn(parentFile).when(file).getParentFile();
+
+        return file;
     }
 
     private static HttpClient mockHttpClient(int statusCode, String content) throws Exception {
         StatusLine status = mock(StatusLine.class);
         doReturn(statusCode).when(status).getStatusCode();
 
         HttpEntity entity = mock(HttpEntity.class);
         doReturn(new ByteArrayInputStream(content.getBytes("UTF-8"))).when(entity).getContent();