[gnome-boxes/wip/download: 12/12] downloader: Move downloading to download class



commit 4912910263f98f6c7772a80563316d1f596efe5f
Author: Lasse Schuirmann <lasse schuirmann gmail com>
Date:   Tue Oct 14 16:53:35 2014 +0200

    downloader: Move downloading to download class
    
    The Downloader got rather complex lately. It is time that the Download
    object takes the part of downloading things while the Downloader only
    manages several downloads.
    
    This widely extends the (now abstract) Download object so that it
    handles construction of temporary files and the await_download
    functionality. The actual download is done by the subclasses.
    
    This approach allows extending this download system later with other
    download methods (e.g. samba file shares among others) without changing
    anything outside the download.vala file.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=738168

 src/Makefile.am     |    1 +
 src/download.vala   |  251 +++++++++++++++++++++++++++++++++++++++++++++++++++
 src/downloader.vala |  187 +++-----------------------------------
 3 files changed, 266 insertions(+), 173 deletions(-)
---
diff --git a/src/Makefile.am b/src/Makefile.am
index 7709d21..3c8d43e 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -146,6 +146,7 @@ gnome_boxes_SOURCES =                               \
        wizard-source.vala                      \
        wizard-toolbar.vala                     \
        wizard.vala                             \
+       download.vala                           \
        downloader.vala                         \
        empty-boxes.vala                        \
        tracker-iso-query.vala                  \
diff --git a/src/download.vala b/src/download.vala
new file mode 100644
index 0000000..650634c
--- /dev/null
+++ b/src/download.vala
@@ -0,0 +1,251 @@
+// This file is part of GNOME Boxes. License: LGPLv2+
+
+private abstract class Boxes.Download : GLib.Object {
+    protected string uri { public get; set; }
+    protected File remote_file;
+    protected File local_file;
+    protected File cache_file;
+
+    protected ActivityProgress progress;
+    protected Cancellable? cancellable;
+
+    protected bool downloading { public get; set; }
+
+    public signal void download_successful ();
+    public signal void download_failed (GLib.Error error);
+
+    protected Mutex mutex = Mutex ();
+
+    /**
+     * Creates the Download derivative of the appropriate type.
+     * Arguments are the same as the Download constructor.
+     */
+    public static Download create_download (File              remote_file,
+                                            File              local_file,
+                                            File?             cache_file  = null,
+                                            ActivityProgress? progress    = new ActivityProgress (),
+                                            Cancellable?      cancellable = null) {
+         if (remote_file.has_uri_scheme ("http") || remote_file.has_uri_scheme ("https")) {
+            return new HTTPDownload (remote_file, local_file, cache_file, progress, cancellable);
+         } else {
+            return new FilesystemDownload (remote_file, local_file, cache_file, progress, cancellable);
+         }
+    }
+
+    /**
+     * Can be used to download files.
+     *
+     * @param remote_file: The file to download.
+     * @param local_file: The target file. (Will be overwritten.)
+     * @param cache_file: The temporary download file. Defaults to null which will be a file corresponding 
to the
+     *                    local_file.get_uri () + "~" path.
+     * @param progress: The ActivityProgress object to report progress to.
+     * @param cancellable: The Cancellable object for cancellation.
+     */
+    public Download (File              remote_file,
+                     File              local_file,
+                     File?             cache_file  = null,
+                     ActivityProgress? progress    = new ActivityProgress (),
+                     Cancellable?      cancellable = null) {
+        this.remote_file = remote_file;
+        this.uri = remote_file.get_uri ();
+        this.local_file  = local_file;
+        this.cache_file  = cache_file ?? GLib.File.new_for_path (local_file.get_path () + "~");
+        this.progress = progress;
+        this.cancellable = cancellable;
+        this.downloading = false;
+    }
+
+    /**
+     * Downloads the file.
+     *
+     * If you want to override how the download works, override the download_cached () method and take a 
look at its
+     * documentation comment.
+     *
+     * @return The File object corresponding to the downloaded file.
+     */
+    public async File download () throws GLib.Error {
+        mutex.lock ();
+        {
+            downloading = true;
+        }
+        mutex.unlock ();
+
+        try {
+            yield download_cached ();
+
+            cache_file.move (local_file, FileCopyFlags.OVERWRITE, cancellable);
+        } catch (GLib.Error error) {
+            mutex.lock ();
+            {
+                downloading = false;
+                download_failed (error);
+            }
+            mutex.unlock ();
+
+            throw error;
+        }
+
+        mutex.lock ();
+        {
+            downloading = false;
+            download_successful ();
+        }
+        mutex.unlock ();
+
+        return local_file;
+    }
+
+    /**
+     * Waits until the current download is finished. This allows also to monitor the download.
+     *
+     * @param progress: An ActivityProgress object to monitor the download. It will not be connected to the 
download if
+     *                  the download is not active.
+     * @return The downloaded File object. Will be null if the download is not downloading anything.
+     */
+    public async File? await_download (ActivityProgress? progress = null) throws GLib.Error {
+        // Make sure that download_complete isnt called while we're preparing to listen
+        mutex.lock ();
+
+        if (!downloading) {
+            mutex.unlock ();
+            return null;
+        }
+
+        ulong successful_id = 0;
+        ulong failed_id = 0;
+        GLib.Error download_error = null;
+        try {
+            if (progress != null)
+                this.progress.bind_property ("progress", progress, "progress", BindingFlags.SYNC_CREATE);
+
+            SourceFunc callback = await_download.callback;
+            successful_id = download_successful.connect (() => { callback (); });
+            failed_id = download_failed.connect((error) => { download_error = error; callback ();});
+        } finally {
+            mutex.unlock ();
+        }
+
+        debug ("'%s' already being downloaded. Waiting for download to complete..", uri);
+        // ATTENTION: Dont lock this.mutex below this yield statement since this will cause a deadlock
+        yield;
+        debug ("Finished waiting for '%s' to download.", uri);
+
+        disconnect (successful_id);
+        disconnect (failed_id);
+
+        if (download_error != null)
+            throw download_error;
+
+        return local_file;
+    }
+
+    /**
+     * Downloads the remote_file to cache_file.
+     */
+    protected async abstract void download_cached () throws GLib.Error;
+}
+
+private class Boxes.FilesystemDownload : Boxes.Download {
+    public FilesystemDownload (File              remote_file,
+                               File              local_file,
+                               File?             cache_file  = null,
+                               ActivityProgress? progress    = new ActivityProgress (),
+                               Cancellable?      cancellable = null) {
+        base (remote_file, local_file, cache_file, progress, cancellable);
+    }
+
+    public async override void download_cached () throws GLib.Error {
+        debug ("Copying '%s' to '%s'..", remote_file.get_path (), cache_file.get_path ());
+        yield remote_file.copy_async (cache_file,
+                                      FileCopyFlags.OVERWRITE,
+                                      Priority.DEFAULT,
+                                      cancellable,
+                                      (current, total) => {
+            progress.progress = (double) current / total;
+        });
+        debug ("Copied '%s' to '%s'.", remote_file.get_path (), cache_file.get_path ());
+    }
+}
+
+private class Boxes.HTTPDownload : Boxes.Download {
+    private Soup.Session soup_session;
+
+    public HTTPDownload (File              remote_file,
+                         File              local_file,
+                         File?             cache_file  = null,
+                         ActivityProgress? progress    = new ActivityProgress (),
+                         Cancellable?      cancellable = null) {
+        base (remote_file, local_file, cache_file, progress, cancellable);
+
+        soup_session = new Soup.Session ();
+        soup_session.add_feature_by_type (typeof (Soup.ProxyResolverDefault));
+        if (Environment.get_variable ("SOUP_DEBUG") != null)
+            soup_session.add_feature (new Soup.Logger (Soup.LoggerLogLevel.HEADERS, -1));
+    }
+
+    public async override void download_cached () throws GLib.Error {
+        // FIXME: invoke a function that resolves the http 302 forwarding here
+
+        var msg = new Soup.Message ("GET", uri);
+        msg.response_body.set_accumulate (false);
+        var address = msg.get_address ();
+        var connectable = new NetworkAddress (address.name, (uint16) address.port);
+        var network_monitor = NetworkMonitor.get_default ();
+        if (!(yield network_monitor.can_reach_async (connectable)))
+            throw new Boxes.Error.INVALID ("Failed to reach host '%s' on port '%d'", address.name, 
address.port);
+
+        GLib.Error? err = null;
+        ulong cancelled_id = 0;
+        if (cancellable != null)
+            cancelled_id = cancellable.connect (() => {
+                err = new GLib.IOError.CANCELLED ("Cancelled by cancellable.");
+                soup_session.cancel_message (msg, Soup.Status.CANCELLED);
+            });
+
+        int64 total_num_bytes = 0;
+        msg.got_headers.connect (() => {
+            total_num_bytes =  msg.response_headers.get_content_length ();
+        });
+
+        var cached_file_stream = yield cache_file.replace_async (null, false, 
FileCreateFlags.REPLACE_DESTINATION);
+
+        int64 current_num_bytes = 0;
+        // FIXME: Reduce lambda nesting by splitting out downloading to Download class
+        msg.got_chunk.connect ((msg, chunk) => {
+            current_num_bytes += chunk.length;
+            try {
+                // Write synchronously as we have no control over order of async
+                // calls and we'll end up writing bytes out in wrong order. Besides
+                // we are writing small chunks so it wouldn't really block the UI.
+                cached_file_stream.write (chunk.data);
+                if (total_num_bytes > 0)
+                    // Don't report progress if there is no way to determine it
+                    progress.progress = (double) current_num_bytes / total_num_bytes;
+            } catch (GLib.Error e) {
+                err = e;
+                soup_session.cancel_message (msg, Soup.Status.CANCELLED);
+            }
+        });
+
+        soup_session.queue_message (msg, (soup_session, msg) => {
+            download_cached.callback ();
+        });
+
+        yield;
+
+        if (cancelled_id != 0)
+            cancellable.disconnect (cancelled_id);
+
+        yield cached_file_stream.close_async (Priority.DEFAULT, cancellable);
+
+        if (msg.status_code != Soup.Status.OK) {
+            cache_file.delete ();
+            if (err == null)
+                err = new GLib.Error (Soup.http_error_quark (), (int)msg.status_code, msg.reason_phrase);
+
+            throw err;
+        }
+    }
+}
+
diff --git a/src/downloader.vala b/src/downloader.vala
index ac90260..8537482 100644
--- a/src/downloader.vala
+++ b/src/downloader.vala
@@ -1,28 +1,10 @@
 // This file is part of GNOME Boxes. License: LGPLv2+
 
-private class Boxes.Download {
-    public string uri;
-    public File remote_file;
-    public File cached_file;
-    public ActivityProgress progress;
-
-    public Download (File remote_file, File cached_file, ActivityProgress progress) {
-        this.remote_file = remote_file;
-        this.uri = remote_file.get_uri ();
-        this.cached_file = cached_file;
-        this.progress = progress;
-    }
-}
-
 private class Boxes.Downloader : GLib.Object {
     private static Downloader downloader;
-    private Soup.Session session;
 
     private GLib.HashTable<string,Download> downloads;
 
-    public signal void downloaded (Download download);
-    public signal void download_failed (Download download, GLib.Error error);
-
     public static Downloader get_instance () {
         if (downloader == null)
             downloader = new Downloader ();
@@ -51,11 +33,6 @@ private class Boxes.Downloader : GLib.Object {
 
     private Downloader () {
         downloads = new GLib.HashTable <string,Download> (str_hash, str_equal);
-
-        session = new Soup.Session ();
-        session.add_feature_by_type (typeof (Soup.ProxyResolverDefault));
-        if (Environment.get_variable ("SOUP_DEBUG") != null)
-            session.add_feature (new Soup.Logger (Soup.LoggerLogLevel.HEADERS, -1));
     }
 
     /**
@@ -75,107 +52,31 @@ private class Boxes.Downloader : GLib.Object {
                                 Cancellable?     cancellable = null) throws GLib.Error {
         var uri = remote_file.get_uri ();
         var download = downloads.get (uri);
-        var cached_path = cached_paths[0];
-
+        File? result = null;
         if (download != null)
             // Already being downloaded
-            return yield await_download (download, cached_path, progress);
+            result = yield download.await_download (progress);
+            if (result != null)
+                return result;
 
         var cached_file = get_cached_file (remote_file, cached_paths);
         if (cached_file != null)
             return cached_file;
 
-        var tmp_path = cached_path + "~";
-        var tmp_file = GLib.File.new_for_path (tmp_path);
         debug ("Downloading '%s'...", uri);
-        download = new Download (remote_file, tmp_file, progress);
-        downloads.set (uri, download);
-
-        try {
-            if (remote_file.has_uri_scheme ("http") || remote_file.has_uri_scheme ("https"))
-                yield download_from_http (download, cancellable);
-            else
-                yield download_from_filesystem (download, cancellable);
-        } catch (GLib.Error error) {
-            download_failed (download, error);
-
-            throw error;
-        } finally {
-            downloads.remove (uri);
-        }
-
-        cached_file = GLib.File.new_for_path (cached_path);
-        tmp_file.move (cached_file, FileCopyFlags.NONE, cancellable);
-        download.cached_file = cached_file;
-
-        debug ("Downloaded '%s' and its now locally available at '%s'.", uri, cached_path);
-        downloaded (download);
-
-        return cached_file;
-    }
-
-    private async void download_from_http (Download download, Cancellable? cancellable = null) throws 
GLib.Error {
-        var msg = new Soup.Message ("GET", download.uri);
-        msg.response_body.set_accumulate (false);
-        var address = msg.get_address ();
-        var connectable = new NetworkAddress (address.name, (uint16) address.port);
-        var network_monitor = NetworkMonitor.get_default ();
-        if (!(yield network_monitor.can_reach_async (connectable)))
-            throw new Boxes.Error.INVALID ("Failed to reach host '%s' on port '%d'", address.name, 
address.port);
-
-        GLib.Error? err = null;
-        ulong cancelled_id = 0;
-        if (cancellable != null)
-            cancelled_id = cancellable.connect (() => {
-                err = new GLib.IOError.CANCELLED ("Cancelled by cancellable.");
-                session.cancel_message (msg, Soup.Status.CANCELLED);
-            });
-
-        int64 total_num_bytes = 0;
-        msg.got_headers.connect (() => {
-            total_num_bytes =  msg.response_headers.get_content_length ();
-        });
+        download = Download.create_download (remote_file,
+                                             GLib.File.new_for_path (cached_paths[0]),
+                                             null,
+                                             progress,
+                                             cancellable);
 
-        var cached_file_stream = yield download.cached_file.replace_async (null,
-                                                                           false,
-                                                                           
FileCreateFlags.REPLACE_DESTINATION);
-
-        int64 current_num_bytes = 0;
-        // FIXME: Reduce lambda nesting by splitting out downloading to Download class
-        msg.got_chunk.connect ((msg, chunk) => {
-            current_num_bytes += chunk.length;
-            try {
-                // Write synchronously as we have no control over order of async
-                // calls and we'll end up writing bytes out in wrong order. Besides
-                // we are writing small chunks so it wouldn't really block the UI.
-                cached_file_stream.write (chunk.data);
-                if (total_num_bytes > 0)
-                    // Don't report progress if there is no way to determine it
-                    download.progress.progress = (double) current_num_bytes / total_num_bytes;
-            } catch (GLib.Error e) {
-                err = e;
-                session.cancel_message (msg, Soup.Status.CANCELLED);
-            }
-        });
-
-        session.queue_message (msg, (session, msg) => {
-            download_from_http.callback ();
-        });
-
-        yield;
-
-        if (cancelled_id != 0)
-            cancellable.disconnect (cancelled_id);
-
-        yield cached_file_stream.close_async (Priority.DEFAULT, cancellable);
+        downloads.set (uri, download);
+        result = yield download.download ();
+        downloads.remove (uri);
 
-        if (msg.status_code != Soup.Status.OK) {
-            download.cached_file.delete ();
-            if (err == null)
-                err = new GLib.Error (Soup.http_error_quark (), (int)msg.status_code, msg.reason_phrase);
+        debug ("Downloaded '%s' and its now locally available at '%s'.", uri, result.get_path ());
 
-            throw err;
-        }
+        return result;
     }
 
     public static async void fetch_os_logo (Gtk.Image image, Osinfo.Os os, int size) {
@@ -221,66 +122,6 @@ private class Boxes.Downloader : GLib.Object {
         return cache;
     }
 
-    private async File? await_download (Download         download,
-                                        string           cached_path,
-                                        ActivityProgress progress) throws GLib.Error {
-        File downloaded_file = null;
-        GLib.Error download_error = null;
-
-        download.progress.bind_property ("progress", progress, "progress", BindingFlags.SYNC_CREATE);
-        SourceFunc callback = await_download.callback;
-        var downloaded_id = downloaded.connect ((downloader, downloaded) => {
-            if (downloaded.uri != download.uri)
-                return;
-
-            downloaded_file = downloaded.cached_file;
-            callback ();
-        });
-        var downloaded_failed_id = download_failed.connect ((downloader, failed_download, error) => {
-            if (failed_download.uri != download.uri)
-                return;
-
-            download_error = error;
-            callback ();
-        });
-
-        debug ("'%s' already being downloaded. Waiting for download to complete..", download.uri);
-        yield; // Wait for it
-        debug ("Finished waiting for '%s' to download.", download.uri);
-        disconnect (downloaded_id);
-        disconnect (downloaded_failed_id);
-
-        if (download_error != null)
-            throw download_error;
-
-        File cached_file;
-        if (downloaded_file.get_path () != cached_path) {
-            cached_file = File.new_for_path (cached_path);
-            yield downloaded_file.copy_async (cached_file, FileCopyFlags.OVERWRITE);
-        } else
-            cached_file = downloaded_file;
-
-        return cached_file;
-    }
-
-    private async void download_from_filesystem (Download     download,
-                                                 Cancellable? cancellable = null) throws GLib.Error {
-        var src_file = download.remote_file;
-        var dest_file = download.cached_file;
-
-        try {
-            debug ("Copying '%s' to '%s'..", src_file.get_path (), dest_file.get_path ());
-            yield src_file.copy_async (dest_file,
-                                       FileCopyFlags.OVERWRITE,
-                                       Priority.DEFAULT,
-                                       cancellable,
-                                       (current, total) => {
-                download.progress.progress = (double) current / total;
-            });
-            debug ("Copied '%s' to '%s'.", src_file.get_path (), dest_file.get_path ());
-        } catch (IOError.EXISTS error) {}
-    }
-
     private File? get_cached_file (File remote_file, string[] cached_paths) {
         foreach (var path in cached_paths) {
             var cached_file = File.new_for_path (path);


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]