infocalypse
 
(Steve Dougherty)
2013-08-08: Track the latest known repo list edition.

Track the latest known repo list edition. This takes some responsibility off the node for fetching an up-to-date edition, because it is no longer always given edition 0 to start from. This does introduce some clumsiness as if the node follows redirects it does not return the used edition. This should probably be replaced with a WoT property once WoT properties can be inserted without causing an insert, as it will avoid starting the repo list at edition 0.

diff --git a/infocalypse/config.py b/infocalypse/config.py
--- a/infocalypse/config.py
+++ b/infocalypse/config.py
@@ -128,8 +128,13 @@ class Config:
         # repo id -> publisher WoT identity
         self.wot_identities = {}
         # TODO: Should this be keyed by str(WoT_ID) ?
-        # WoT public key hash -> Freemail password
+        # WoT identity ID -> Freemail password
         self.freemail_passwords = {}
+        # WoT identity ID -> last known repo list edition.
+        # TODO: Once WoT allows setting a property without triggering an
+        # immediate insert, this can move to a WoT property. (Can then query
+        # remote identities! Easier bootstrapping than from edition 0.)
+        self.repo_list_editions = {}
         # fms_id -> (usk_hash, ...) map
         self.fmsread_trust_map = DEFAULT_TRUST.copy()
         self.fmsread_groups = DEFAULT_GROUPS
@@ -269,6 +274,23 @@ class Config:
                              "Run hg fn-setupfreemail --truster {0}\n"
                              .format(wot_identity))
 
+    def set_repo_list_edition(self, wot_identity, edition):
+        """
+        Set the repository list edition for the given WoT identity.
+        :type wot_identity: WoT_ID
+        """
+        self.repo_list_editions[wot_identity.identity_id] = edition
+
+    def get_repo_list_edition(self, wot_identity):
+        """
+        Return the repository list edition associated with the given WoT
+        identity. Return 0 if one is not set.
+        """
+        if wot_identity.identity_id in self.repo_list_editions:
+            return self.repo_list_editions[wot_identity.identity_id]
+        else:
+            return 0
+
     # Hmmm... really nescessary?
     def get_dir_insert_uri(self, repo_dir):
         """ Return the insert USK for repo_dir or None. """
@@ -383,6 +405,11 @@ class Config:
                 cfg.freemail_passwords[wot_id] = parser.get(
                     'freemail_passwords', wot_id)
 
+        if parser.has_section('repo_list_editions'):
+            for wot_id in parser.options('repo_list_editions'):
+                cfg.repo_list_editions[wot_id] = int(parser.get(
+                    'repo_list_editions', wot_id))
+
         # ignored = fms_id|usk_hash|usk_hash|...
         if parser.has_section('fmsread_trust_map'):
             cfg.fmsread_trust_map.clear() # Wipe defaults.
@@ -467,6 +494,10 @@ class Config:
         for wot_id in cfg.freemail_passwords:
             parser.set('freemail_passwords', wot_id, cfg.freemail_passwords[
                 wot_id])
+        parser.add_section('repo_list_editions')
+        for wot_id in cfg.repo_list_editions:
+            parser.set('repo_list_editions', wot_id, cfg.repo_list_editions[
+                wot_id])
         parser.add_section('fmsread_trust_map')
         for index, fms_id in enumerate(cfg.fmsread_trust_map):
             entry = cfg.fmsread_trust_map[fms_id]
diff --git a/infocalypse/wot.py b/infocalypse/wot.py
--- a/infocalypse/wot.py
+++ b/infocalypse/wot.py
@@ -311,8 +311,10 @@ def update_repo_listing(ui, for_identity
     # TODO: Somehow store the edition, perhaps in ~/.infocalypse. WoT
     # properties are apparently not appropriate.
 
+    cfg = Config.from_ui(ui)
+
     insert_uri.name = 'vcs'
-    insert_uri.edition = '0'
+    insert_uri.edition = cfg.get_repo_list_edition(for_identity)
 
     ui.status("Inserting with URI:\n{0}\n".format(insert_uri))
     uri = node.put(uri=str(insert_uri), mimetype='application/xml',
@@ -322,6 +324,8 @@ def update_repo_listing(ui, for_identity
         ui.warn("Failed to update repository listing.")
     else:
         ui.status("Updated repository listing:\n{0}\n".format(uri))
+        cfg.set_repo_list_edition(for_identity, USK(uri).edition)
+        Config.to_file(cfg)
 
 
 def build_repo_list(ui, for_identity):
@@ -369,19 +373,19 @@ def read_repo_listing(ui, identity):
 
     :type identity: WoT_ID
     """
+    cfg = Config.from_ui(ui)
     uri = identity.request_uri.clone()
     uri.name = 'vcs'
-    uri.edition = 0
+    uri.edition = cfg.get_repo_list_edition(identity)
 
     # TODO: Set and read vcs edition property.
-    node = fcp.FCPNode()
-    ui.status("Fetching {0}\n".format(uri))
-    # TODO: What exception can this throw on failure? Catch it,
-    # print its description, and return None.
-    mime_type, repo_xml, msg = node.get(str(uri), priority=1,
-                                        followRedirect=True)
+    ui.status("Fetching.\n")
+    mime_type, repo_xml, msg = fetch_edition(uri)
+    ui.status("Fetched {0}.\n".format(uri))
 
-    ui.status("Parsing.\n")
+    cfg.set_repo_list_edition(identity, uri.edition)
+    Config.to_file(cfg)
+
     repositories = {}
     root = fromstring(repo_xml)
     for repository in root.iterfind('repository'):
@@ -395,6 +399,30 @@ def read_repo_listing(ui, identity):
     return repositories
 
 
+def fetch_edition(uri):
+    """
+    Fetch a USK uri, following redirects. Change the uri edition to the one
+    fetched.
+    :type uri: USK
+    """
+    node = fcp.FCPNode()
+    # Following a redirect automatically does not provide the edition used,
+    # so manually following redirects is required.
+    # TODO: Is there ever legitimately more than one redirect?
+    try:
+        return node.get(str(uri), priority=1)
+    except fcp.FCPGetFailed, e:
+        # Error code 27 is permanent redirect: there's a newer edition of
+        # the USK.
+        # https://wiki.freenetproject.org/FCPv2/GetFailed#Fetch_Error_Codes
+        if not e.info['Code'] == 27:
+            raise
+
+        uri.edition = USK(e.info['RedirectURI']).edition
+
+        return node.get(str(uri), priority=1)
+
+
 def resolve_pull_uri(ui, path, truster):
         """
         Return a pull URI for the given path.