conduit r1657 - in trunk: . conduit/modules/AmazonS3Module data



Author: jstowers
Date: Sun Aug 10 05:53:57 2008
New Revision: 1657
URL: http://svn.gnome.org/viewvc/conduit?rev=1657&view=rev

Log:
	* conduit/modules/AmazonS3Module/:
	* conduit/modules/AmazonS3Module/AmazonS3Module.py:
	* conduit/modules/AmazonS3Module/config.glade:
	* data/amazon.png: Preliminary amazon S3 support contributed by
	Christian Schlotter.


Added:
   trunk/conduit/modules/AmazonS3Module/
   trunk/conduit/modules/AmazonS3Module/AmazonS3Module.py
   trunk/conduit/modules/AmazonS3Module/config.glade
   trunk/data/amazon.png
Modified:
   trunk/   (props changed)
   trunk/ChangeLog

Added: trunk/conduit/modules/AmazonS3Module/AmazonS3Module.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/AmazonS3Module/AmazonS3Module.py	Sun Aug 10 05:53:57 2008
@@ -0,0 +1,369 @@
+"""
+C{AmazonS3TwoWay} module for synchronizing with I{Amazon Simple Storage
+Service} (Amazon S3).
+
+Uses URI relative to base path as LUID. The LUID is also used as key on Amazon
+S3.
+"""
+import logging
+log = logging.getLogger("modules.AmazonS3")
+
+import conduit
+import conduit.Exceptions as Exceptions
+import conduit.utils as Utils
+import conduit.datatypes.File as File
+import conduit.dataproviders.DataProvider as DataProvider
+
+from boto.s3.connection import S3Connection
+from boto.s3.key import Key
+
+MODULES = {"AmazonS3TwoWay" : {"type": "dataprovider"}}
+
+class AmazonS3TwoWay(DataProvider.TwoWay):
+    """
+    TwoWay dataprovider for synchronizing files with Amazon S3 and vice-versa.
+    """
+
+    _name_ = "Amazon S3"
+    _description_ = "Sync with Amazon S3"
+    _category_ = conduit.dataproviders.CATEGORY_FILES
+    _module_type_ = "twoway"
+    _in_type_ = "file"
+    _out_type_ = "file"
+    _icon_ = "amazon"
+    _configurable_ = True
+
+    # default values for class variables (used by self.set_configuration())
+    DEFAULT_AWS_ACCESS_KEY = None
+    DEFAULT_AWS_SECRET_ACCESS_KEY = None
+    DEFAULT_BUCKET_NAME = ""
+    DEFAULT_USE_SSL = True
+
+    # set expire time for AWS https links
+    AWS_URL_EXPIRE_SECONDS = 60 * 15
+
+    def __init__(self):
+        """
+        Call base constructor and initialize all variables that are restored
+        from configuration.
+        """
+        DataProvider.TwoWay.__init__(self)
+
+        # configured AWS Access Key
+        self.aws_access_key = AmazonS3TwoWay.DEFAULT_AWS_ACCESS_KEY
+        # configured AWS Secret Access Key
+        self.aws_secret_access_key = \
+            AmazonS3TwoWay.DEFAULT_AWS_SECRET_ACCESS_KEY
+        # configured name of Amazon S3 bucket
+        self.bucket_name = AmazonS3TwoWay.DEFAULT_BUCKET_NAME
+        # configuration value determining use of SSL for Amazon S3 connection
+        self.use_ssl = AmazonS3TwoWay.DEFAULT_USE_SSL
+        # remote keys (equivalent to LUIDs)
+        self.keys = []
+        # for caching S3Connection object
+        self.connection = None
+        # for caching Bucket object
+        self.bucket = None
+
+    def _data_exists(self, LUID):
+        """
+        @returns: C{True} if data at the LUID exists, else C{False}.
+        """
+        return self.bucket.get_key(LUID) != None
+
+    def _get_proxyfile(self, key):
+        """
+        @param key: Key for which C{ProxyFile} should be returned.
+        @type key: C{boto.s3.key.Key}
+        @returns: C{ProxyFile} stored under supplied parameter C{key}.
+        """
+        httpurl = key.generate_url(AmazonS3TwoWay.AWS_URL_EXPIRE_SECONDS)
+        # BUG This will fail with "Access denied"
+        # (see http://bugzilla.gnome.org/show_bug.cgi?id=545000)
+        return File.ProxyFile(
+            httpurl,
+            key.name,
+            Utils.datetime_from_timestamp(long(key.get_metadata("mtime"))),
+            long(key.get_metadata("size")))
+
+    def _get_data(self, LUID):
+        """
+        @returns: ProxyFile object containing remote data with the specified
+        LUID.
+        """
+        key = self.bucket.get_key(LUID)
+        return self._get_proxyfile(key)
+
+    def _put_data(self, localfile):
+        """
+        Uploads the given File object to Amazon S3 and returns its record
+        identifier (Rid).
+
+        @returns: Rid of uploaded file.
+        """
+        filename = localfile.get_relative_uri()
+        key = Key(self.bucket)
+        # the key's name is the LUID
+        key.name = filename
+        # add a bit of metadata to key
+        # TODO store more metadata: file permissions and owner:group?
+        key.set_metadata("size", str(localfile.get_size()))
+        key.set_metadata(
+            "mtime", str(Utils.datetime_get_timestamp(localfile.get_mtime())))
+
+        # now upload the data
+        key.set_contents_from_filename(localfile.get_local_uri())
+
+        # return Rid of uploaded file
+        return self._get_proxyfile(key).get_rid()
+
+    def _replace_data(self, LUID, localfile):
+        """
+        Replaces the remote file identified by LUID with given file object.
+        """
+        # We don't assign a new LUID when replacing the file, so we can call
+        # self._put_data()
+        return self._put_data(localfile)
+
+    def _set_aws_access_key(self, key):
+        """
+        Sets variable C{self.aws_access_key} to given value.
+        """
+        # set to None if param is the empty string so that boto can figure
+        # out the access key by config file or environment variable
+        if key == "" or key == "None":
+            key = None
+        if self.aws_access_key != key:
+            self.aws_access_key = key
+            # reset connection when configuration changes
+            self.connection = None
+
+    def _set_aws_secret_access_key(self, key):
+        """
+        Sets variable C{self.aws_secret_access_key} to given value.
+        """
+        # set to None if param is the empty string so that boto can figure
+        # out the access key by config file or environment variable
+        if key == "" or key == "None":
+            key = None
+        if self.aws_secret_access_key != key:
+            self.aws_secret_access_key = key
+            # reset connection when configuration changes
+            self.connection = None
+
+    def _set_bucket_name(self, name):
+        """
+        Sets variable C{self.bucket_name} to given value.
+
+        @param name: Bucket name that C{self.bucket_name} shall be set to.
+        @type name: C{str}
+        """
+        name = str(name)
+        if self.bucket_name != name:
+            self.bucket_name = name
+            # reset bucket when configuration changes
+            self.bucket = None
+
+    def _set_use_ssl(self, use_ssl):
+        """
+        Sets variable C{self.use_ssl}.
+
+        @param use_ssl: C{True} if a secure connection should be used for
+                        communication with Amazon S3, C{False} otherwise.
+        @type use_ssl: C{bool}
+        """
+        self.use_ssl = bool(use_ssl)
+
+    def configure(self, window):
+        """
+        Show configuration dialog for this module.
+
+        @param window: The parent window (used for modal dialogs)
+        @type window: C{gtk.Window}
+        """
+        # lazily import gtk so if conduit is run from command line or a non
+        # gtk system, this module will still load. There should be no need
+        # to use gtk outside of this function
+        import gtk
+
+        def on_dialog_response(sender, response_id):
+            """
+            Response handler for configuration dialog.
+            """
+            if response_id == gtk.RESPONSE_OK:
+                self._set_aws_access_key(access_key_entry.get_text())
+                self._set_aws_secret_access_key(
+                    secret_access_key_entry.get_text())
+                self._set_bucket_name(bucket_name_entry.get_text())
+                self._set_use_ssl((True, False)[ssl_combo_box.get_active()])
+
+        tree = Utils.dataprovider_glade_get_widget(__file__,
+                                                   "config.glade",
+                                                   "AmazonS3ConfigDialog")
+
+        # get widgets
+        dialog = tree.get_widget("AmazonS3ConfigDialog")
+        access_key_entry = tree.get_widget("accessKey")
+        secret_access_key_entry = tree.get_widget("secretAccessKey")
+        bucket_name_entry = tree.get_widget("bucketName")
+        ssl_combo_box = tree.get_widget("useSsl")
+
+        # set values of widgets
+        access_key_entry.set_text(
+            (self.aws_access_key, "")[self.aws_access_key == None])
+        secret_access_key_entry.set_text((self.aws_secret_access_key, "")
+                                         [self.aws_secret_access_key == None])
+        bucket_name_entry.set_text(self.bucket_name)
+        ssl_combo_box.set_active((1, 0)[self.use_ssl])
+
+        # show dialog
+        Utils.run_dialog_non_blocking(dialog, on_dialog_response, window)
+
+    def _connect(self):
+        """
+        Connect to Amazon S3 if not already connected and makes sure that
+        variable C{self.connection} holds a valid C{S3Connection} object.
+        """
+        if self.connection != None:
+            log.debug("Already connected to Amazon S3.")
+        else:
+            log.debug("Connecting to Amazon S3.")
+            self.connection = S3Connection(self.aws_access_key,
+                                           self.aws_secret_access_key,
+                                           is_secure=self.use_ssl)
+
+    def _set_bucket(self):
+        """
+        Makes sure that variable C{self.bucket} holds a valid C{Bucket} object.
+        """
+        self._connect()
+        if self.bucket != None and self.bucket.name == self.bucket_name:
+            log.debug("Already have bucket (name = '%s')." % self.bucket.name)
+        else:
+            log.debug("Getting bucket named '%s'." % self.bucket_name)
+            # create or get configured bucket
+            # BUG this will fail with environment variable LC_TIME != "en"
+            # (see http://code.google.com/p/boto/issues/detail?id=140)
+            self.bucket = self.connection.create_bucket(self.bucket_name)
+
+    def refresh(self):
+        """
+        Connects to Amazon S3 if necessary and gets the name of all keys in the
+        configured bucket.
+        """
+        DataProvider.TwoWay.refresh(self)
+        self._set_bucket()
+        # Get LUIDs of all remote files (the keys of the remote files are the
+        # LUIDs)
+        self.keys = [key.name for key in self.bucket]
+
+    def get_all(self):
+        """
+        Returns the key names (LUIDs) of all remote files.
+        @return: A list of string LUIDs.
+        """
+        DataProvider.TwoWay.get_all(self)
+        # refresh() has been called previously, so we can just return self.keys
+        return self.keys
+
+    def get(self, LUID):
+        """
+        Stores remote file identified by supplied LUID locally and returns the
+        corresponding File object.
+        @param LUID: A LUID which uniquely represents data to return.
+        @type LUID: C{str}
+        """
+        DataProvider.TwoWay.get(self, LUID)
+        data = self._get_data(LUID)
+        data.force_new_filename(LUID)
+        # datatypes can be shared between modules. For this reason it is
+        # necessary to explicity set parameters like the LUID
+        data.set_UID(LUID)
+        return data
+
+    def put(self, localfile, overwrite, LUID):
+        """
+        Stores the given File object remotely on Amazon S3, if certain
+        conditions are met.
+        @returns: The Rid of the page at location LUID.
+        """
+        DataProvider.TwoWay.put(self, localfile, overwrite, LUID)
+        # If LUID is None, then we have once-upon-a-time uploaded this file
+        if LUID != None:
+            # Check if the remote file exists (i.e. has it been deleted)
+            if self._data_exists(LUID):
+                # The remote file exists
+                if not overwrite:
+                    # Only replace the data if it is newer than the remote one
+                    remotefile = self._get_data(LUID)
+                    comp = localfile.compare(remotefile)
+                    if comp == conduit.datatypes.COMPARISON_NEWER:
+                        return self._replace_data(LUID, localfile)
+                    elif comp == conduit.datatypes.COMPARISON_EQUAL:
+                        # We are the same, so return either rid
+                        return remotefile.get_rid()
+                    else:
+                        # If we are older than the remote page, or if the two
+                        # could not be compared, then we must ask the user what
+                        # to do via a conflict
+                        raise Exceptions.SynchronizeConflictError(comp,
+                                                                  localfile,
+                                                                  remotefile)
+
+        # If we get here then the file is new
+        return self._put_data(localfile)
+
+    def delete(self, LUID):
+        """
+        Delete remote file identified by given LUID.
+        """
+        DataProvider.TwoWay.delete(self, LUID)
+        # delete remote file
+        self.bucket.delete_key(LUID)
+
+    def get_configuration(self):
+        """
+        Returns a dict of key-value pairs. Key is the name of an internal
+        variable, and value is its current value to save.
+
+        It is important the the key is the actual name (minus the self.) of the
+        internal variable that should be restored when the user saves
+        their settings.
+        """
+        return {"aws_access_key" : self.aws_access_key,
+                "aws_secret_access_key" : self.aws_secret_access_key,
+                "bucket_name" : self.bucket_name,
+                "use_ssl" : self.use_ssl}
+
+    def set_configuration(self, config):
+        """
+        If you override this function then you are responsible for
+        checking the sanity of values in the config dict, including setting
+        any instance variables to sane defaults
+        """
+        self._set_aws_access_key(
+            config.get("aws_access_key", AmazonS3TwoWay.DEFAULT_AWS_ACCESS_KEY))
+        self._set_aws_secret_access_key(
+            config.get("aws_secret_access_key",
+                       AmazonS3TwoWay.DEFAULT_AWS_SECRET_ACCESS_KEY))
+        self._set_bucket_name(
+            config.get("bucket_name", AmazonS3TwoWay.DEFAULT_BUCKET_NAME))
+        self._set_use_ssl(config.get("use_ssl", AmazonS3TwoWay.DEFAULT_USE_SSL))
+
+    def is_configured(self, isSource, isTwoWay):
+        """
+        @returns: C{True} if this instance has been correctly configured and
+        data can be retrieved/stored into it, else C{False}.
+        """
+        # Below we also check if the AWS access and secret access key is set.
+        # boto is able to retrieve these values from its own config file or
+        # from environment variables, and we effectively disable this behavior
+        # by checking if these keys are set.
+        return self.bucket_name != None and self.aws_access_key != None and \
+            self.aws_secret_access_key != None
+
+    def get_UID(self):
+        """
+        @returns: A string uniquely representing this dataprovider.
+        """
+        return self.aws_access_key + self.bucket_name

Added: trunk/conduit/modules/AmazonS3Module/config.glade
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/AmazonS3Module/config.glade	Sun Aug 10 05:53:57 2008
@@ -0,0 +1,185 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd">
+<!--*- mode: xml -*-->
+<glade-interface>
+  <widget class="GtkDialog" id="AmazonS3ConfigDialog">
+    <property name="visible">True</property>
+    <property name="title" translatable="yes">Amazon S3</property>
+    <property name="resizable">False</property>
+    <property name="default_width">250</property>
+    <property name="default_height">300</property>
+    <property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property>
+    <child internal-child="vbox">
+      <widget class="GtkVBox" id="AmazonS3ConfigBox">
+        <property name="visible">True</property>
+        <property name="spacing">5</property>
+        <child>
+          <widget class="GtkLabel" id="accountDetailsLabel">
+            <property name="visible">True</property>
+            <property name="label" translatable="yes">&lt;b&gt;Account Details&lt;/b&gt;</property>
+            <property name="use_markup">True</property>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">False</property>
+            <property name="position">2</property>
+          </packing>
+        </child>
+        <child>
+          <widget class="GtkLabel" id="accessKeyLabel">
+            <property name="visible">True</property>
+            <property name="xalign">0</property>
+            <property name="label" translatable="yes">AWS Access Key:</property>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">False</property>
+            <property name="position">3</property>
+          </packing>
+        </child>
+        <child>
+          <widget class="GtkEntry" id="accessKey">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="invisible_char">â</property>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">False</property>
+            <property name="position">4</property>
+          </packing>
+        </child>
+        <child>
+          <widget class="GtkLabel" id="secretAccessKeyLabel">
+            <property name="visible">True</property>
+            <property name="xalign">0</property>
+            <property name="label" translatable="yes">AWS Secret Access Key:</property>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">False</property>
+            <property name="position">5</property>
+          </packing>
+        </child>
+        <child>
+          <widget class="GtkEntry" id="secretAccessKey">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="visibility">False</property>
+            <property name="invisible_char">â</property>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">False</property>
+            <property name="position">6</property>
+          </packing>
+        </child>
+        <child>
+          <widget class="GtkLabel" id="storageLocationLabel">
+            <property name="visible">True</property>
+            <property name="label" translatable="yes">&lt;b&gt;Storage Location&lt;/b&gt;</property>
+            <property name="use_markup">True</property>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">False</property>
+            <property name="position">7</property>
+          </packing>
+        </child>
+        <child>
+          <widget class="GtkLabel" id="bucketNameLabel">
+            <property name="visible">True</property>
+            <property name="xalign">0</property>
+            <property name="label" translatable="yes">Bucket name:</property>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">False</property>
+            <property name="position">8</property>
+          </packing>
+        </child>
+        <child>
+          <widget class="GtkEntry" id="bucketName">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">False</property>
+            <property name="position">9</property>
+          </packing>
+        </child>
+        <child>
+          <widget class="GtkLabel" id="connectionSettingsLabel">
+            <property name="visible">True</property>
+            <property name="label" translatable="yes">&lt;b&gt;Connection Settings&lt;/b&gt;</property>
+            <property name="use_markup">True</property>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">False</property>
+            <property name="position">10</property>
+          </packing>
+        </child>
+        <child>
+          <widget class="GtkLabel" id="useSslLabel">
+            <property name="visible">True</property>
+            <property name="xalign">0</property>
+            <property name="label" translatable="yes">Use SSL:</property>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">False</property>
+            <property name="position">11</property>
+          </packing>
+        </child>
+        <child>
+          <widget class="GtkComboBox" id="useSsl">
+            <property name="visible">True</property>
+            <property name="active">0</property>
+            <property name="items" translatable="yes">Yes
+No</property>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">False</property>
+            <property name="position">12</property>
+          </packing>
+        </child>
+        <child internal-child="action_area">
+          <widget class="GtkHButtonBox" id="okCancelButtonBox">
+            <property name="visible">True</property>
+            <property name="layout_style">GTK_BUTTONBOX_END</property>
+            <child>
+              <widget class="GtkButton" id="cancelButton">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="can_default">True</property>
+                <property name="label">gtk-cancel</property>
+                <property name="use_stock">True</property>
+                <property name="response_id">-6</property>
+              </widget>
+            </child>
+            <child>
+              <widget class="GtkButton" id="okButton">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="can_default">True</property>
+                <property name="label">gtk-ok</property>
+                <property name="use_stock">True</property>
+                <property name="response_id">-5</property>
+              </widget>
+              <packing>
+                <property name="position">1</property>
+              </packing>
+            </child>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="pack_type">GTK_PACK_END</property>
+          </packing>
+        </child>
+      </widget>
+    </child>
+  </widget>
+</glade-interface>

Added: trunk/data/amazon.png
==============================================================================
Binary files (empty file) and trunk/data/amazon.png	Sun Aug 10 05:53:57 2008 differ



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