From 005f1fa2be743962f5b9c1cabc2b49c89eae332a Mon Sep 17 00:00:00 2001
From: Antoine Lambert <antoine.lambert@inria.fr>
Date: Tue, 5 Jun 2018 18:51:26 +0200
Subject: [PATCH] browse/vault: Handle dead link for cooked archive

A download link for an archive cooked through the vault (directory or
revision_gitfast) does not remain alive ad vitam aeternam.

As the history of previously cooked objects remains stored in the browser
local storage, we need to handle the case where one wants to download a
previously cooked archive from a dead link.

In that case, check the link availability first before proceeding to the
real download. If it is dead, display a modal asking the user if he wants
to recook the archive.

Closes T1082
---
 swh/web/assets/src/bundles/vault/vault-ui.js | 86 ++++++++++++++++++--
 swh/web/templates/browse-vault-ui.html       | 26 +++++-
 2 files changed, 105 insertions(+), 7 deletions(-)

diff --git a/swh/web/assets/src/bundles/vault/vault-ui.js b/swh/web/assets/src/bundles/vault/vault-ui.js
index 8dc7203cb..78540e413 100644
--- a/swh/web/assets/src/bundles/vault/vault-ui.js
+++ b/swh/web/assets/src/bundles/vault/vault-ui.js
@@ -5,7 +5,7 @@
  * See top-level LICENSE file for more information
  */
 
-import {handleFetchErrors} from 'utils/functions';
+import {handleFetchError, handleFetchErrors} from 'utils/functions';
 
 let progress = `<div class="progress">
                   <div class="progress-bar progress-bar-success progress-bar-striped"
@@ -36,6 +36,77 @@ function updateProgressBar(progressBar, cookingTask) {
   }
 }
 
+let recookTask;
+
+// called when the user wants to download a cooked archive
+export function fetchCookedObject(fetchUrl) {
+  recookTask = null;
+  // first, check if the link is still available from the vault
+  fetch(fetchUrl, {credentials: 'same-origin'})
+    .then(response => {
+      // link is still alive, proceed to download
+      if (response.ok) {
+        $('#vault-fetch-iframe').attr('src', fetchUrl);
+      // link is dead
+      } else {
+        // get the associated cooking task
+        let vaultCookingTasks = JSON.parse(localStorage.getItem('swh-vault-cooking-tasks'));
+        for (let i = 0; i < vaultCookingTasks.length; ++i) {
+          if (vaultCookingTasks[i].fetch_url === fetchUrl) {
+            recookTask = vaultCookingTasks[i];
+            break;
+          }
+        }
+        // display a modal asking the user if he wants to recook the archive
+        $('#vault-recook-object-modal').modal('show');
+      }
+    });
+}
+
+// called when the user wants to recook an archive
+// for which the download link is not available anymore
+export function recookObject() {
+  if (recookTask) {
+    // stop cookink tasks status polling
+    clearTimeout(checkVaultId);
+    // build cook request url
+    let cookingUrl;
+    if (recookTask.object_type === 'directory') {
+      cookingUrl = Urls.vault_cook_directory(recookTask.object_id);
+    } else {
+      cookingUrl = Urls.vault_cook_revision_gitfast(recookTask.object_id);
+    }
+    if (recookTask.email) {
+      cookingUrl += '?email=' + recookTask.email;
+    }
+    // request archive cooking
+    fetch(cookingUrl, {credentials: 'same-origin', method: 'POST'})
+      .then(handleFetchError)
+      .then(() => {
+        // update task status
+        recookTask.status = 'new';
+        let vaultCookingTasks = JSON.parse(localStorage.getItem('swh-vault-cooking-tasks'));
+        for (let i = 0; i < vaultCookingTasks.length; ++i) {
+          if (vaultCookingTasks[i].object_id === recookTask.object_id) {
+            vaultCookingTasks[i] = recookTask;
+            break;
+          }
+        }
+        // save updated tasks to local storage
+        localStorage.setItem('swh-vault-cooking-tasks', JSON.stringify(vaultCookingTasks));
+        // restart cooking tasks status polling
+        checkVaultCookingTasks();
+        // hide recook archive modal
+        $('#vault-recook-object-modal').modal('hide');
+      })
+      // something went wrong
+      .catch(() => {
+        checkVaultCookingTasks();
+        $('#vault-recook-object-modal').modal('hide');
+      });
+  }
+}
+
 function checkVaultCookingTasks() {
   let vaultCookingTasks = JSON.parse(localStorage.getItem('swh-vault-cooking-tasks'));
   if (!vaultCookingTasks || vaultCookingTasks.length === 0) {
@@ -83,6 +154,7 @@ function checkVaultCookingTasks() {
 
         let rowTask = $('#vault-task-' + cookingTask.object_id);
 
+        let downloadLinkWait = 'Waiting for download link to be available';
         if (!rowTask.length) {
 
           let browseUrl;
@@ -111,10 +183,10 @@ function checkVaultCookingTasks() {
           }
           tableRow += `<td class="vault-object-id" data-object-id="${cookingTask.object_id}"><a href="${browseUrl}">${cookingTask.object_id}</a></td>`;
           tableRow += `<td style="width: 350px">${progressBar.outerHTML}</td>`;
-          let downloadLink = 'Waiting for download link to be available';
+          let downloadLink = downloadLinkWait;
           if (cookingTask.status === 'done') {
-            downloadLink = `<a class="btn btn-default btn-sm" href="${cookingTask.fetch_url}` +
-                           '"><i class="fa fa-download fa-fw" aria-hidden="true"></i>Download</a>';
+            downloadLink = `<button class="btn btn-default btn-sm" onclick="swh.vault.fetchCookedObject('${cookingTask.fetch_url}')` +
+                           '"><i class="fa fa-download fa-fw" aria-hidden="true"></i>Download</button>';
           } else if (cookingTask.status === 'failed') {
             downloadLink = '';
           }
@@ -126,10 +198,12 @@ function checkVaultCookingTasks() {
           updateProgressBar(progressBar, cookingTask);
           let downloadLink = rowTask.find('.vault-dl-link');
           if (cookingTask.status === 'done') {
-            downloadLink[0].innerHTML = `<a class="btn btn-default btn-sm" href="${cookingTask.fetch_url}` +
-                                        '"><i class="fa fa-download fa-fw" aria-hidden="true"></i>Download</a>';
+            downloadLink[0].innerHTML = `<button class="btn btn-default btn-sm" onclick="swh.vault.fetchCookedObject('${cookingTask.fetch_url}')` +
+                                        '"><i class="fa fa-download fa-fw" aria-hidden="true"></i>Download</button>';
           } else if (cookingTask.status === 'failed') {
             downloadLink[0].innerHTML = '';
+          } else if (cookingTask.status === 'new') {
+            downloadLink[0].innerHTML = downloadLinkWait;
           }
         }
       }
diff --git a/swh/web/templates/browse-vault-ui.html b/swh/web/templates/browse-vault-ui.html
index d013821fa..8ec181223 100644
--- a/swh/web/templates/browse-vault-ui.html
+++ b/swh/web/templates/browse-vault-ui.html
@@ -37,7 +37,31 @@ See top-level LICENSE file for more information
     <tbody></tbody>
   </table>
 </div>
-
+<iframe id="vault-fetch-iframe" style="display:none;"></iframe>
+<div class="modal fade" id="vault-recook-object-modal" tabindex="-1" role="dialog" aria-labelledby="vault-recook-object-modal-label" aria-hidden="true">
+  <div class="modal-dialog">
+    <div class="modal-content">
+      <div class="modal-header">
+        <h6 class="modal-title" id="vault-recook-object-modal-label">Download link no more available</h6>
+        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+          <span aria-hidden="true">&times;</span>
+        </button>
+      </div>
+      <div class="modal-body">
+        <p>
+          The requested archive is no more available to download from the Sofware Heritage Vault.
+        </p>
+        <p>
+          Do you want to cook it again ?
+        </p>
+      </div>
+      <div class="modal-footer">
+        <button type="button" class="btn btn-default btn-sm" data-dismiss="modal">Cancel</button>
+        <button type="button" class="btn btn-default btn-sm" onclick="swh.vault.recookObject()">Ok</button>
+      </div>
+    </div>
+  </div>
+</div>
 <script>
   swh.webapp.initPage('vault');
   swh.vault.initUi();
-- 
GitLab