about summary refs log tree commit diff
diff options
context:
space:
mode:
authorFrederick Muriuki Muriithi2024-06-27 10:53:54 -0500
committerFrederick Muriuki Muriithi2024-06-27 10:53:54 -0500
commitdf9da3d5b5e4382976ede1b54eb1aeb04c4c45e5 (patch)
tree1049a365445628145be075e761ffbe4897295ecc
parent9a8dddab072748a70d43416ac8e6db69ad6fb0cb (diff)
downloadgn-uploader-df9da3d5b5e4382976ede1b54eb1aeb04c4c45e5.tar.gz
Upload chunking: Provide UI and code for drag&drop and chunking
* Use resumable.js to handle the drag-and-drop feature, and chunking.
* Add styling for the drag-and-drop area, and provide visual
  indication when a file is successfully dropped and added to the
  upload list.
-rw-r--r--qc_app/templates/rqtl2/upload-rqtl2-bundle-step-01.html214
1 files changed, 188 insertions, 26 deletions
diff --git a/qc_app/templates/rqtl2/upload-rqtl2-bundle-step-01.html b/qc_app/templates/rqtl2/upload-rqtl2-bundle-step-01.html
index fc87d5b..b9320ea 100644
--- a/qc_app/templates/rqtl2/upload-rqtl2-bundle-step-01.html
+++ b/qc_app/templates/rqtl2/upload-rqtl2-bundle-step-01.html
@@ -5,38 +5,92 @@
 {%block title%}Upload R/qtl2 Bundle{%endblock%}
 
 {%block contents%}
+{%macro rqtl2_file_help()%}
+<span class="form-text text-muted">
+  <p>
+    Provide a valid R/qtl2 zip file here. In particular, ensure your zip bundle
+    contains exactly one control file and the corresponding files mentioned in
+    the control file.
+  </p>
+  <p>
+    The control file can be either a YAML or JSON file. <em>ALL</em> other data
+    files in the zip bundle should be CSV files.
+  </p>
+  <p>See the
+    <a href="https://kbroman.org/qtl2/assets/vignettes/input_files.html"
+       target="_blank">
+      R/qtl2 file format specifications
+    </a>
+    for more details.
+  </p>
+</span>
+{%endmacro%}
 {{upload_progress_indicator()}}
 
+<div id="resumable-file-display-template"
+     class="panel panel-info"
+     style="display: none">
+  <div class="panel-heading"></div>
+  <div class="panel-body"></div>
+</div>
+
+
 <h2 class="heading">Upload R/qtl2 Bundle</h2>
 
+<div id="resumable-drop-area"
+     style="display:none;background:#eeeeee;min-height:12em;border-radius:0.5em;padding:1em;">
+  <p>
+    <a id="resumable-browse-button" href="#"
+       class="btn btn-info">Browse</a>
+  </p>
+  <p class="form-text text-muted">
+    You can drag and drop your file here, or click the browse button.
+    Click on the file to remove it.
+  </p>
+  {{rqtl2_file_help()}}
+  <div id="resumable-selected-files"
+       style="display:flex;flex-direction:row;flex-wrap: wrap;justify-content:space-around;gap:10px 20px;"></div>
+  <div id="resumable-class-buttons" style="text-align: right;">
+    <button id="resumable-upload-button"
+            class="btn btn-primary"
+            style="display: none">start upload</button>
+    <button id="resumable-cancel-upload-button"
+            class="btn btn-danger"
+            style="display: none">cancel upload</button>
+  </div>
+  <div class="progress" style="display: none">
+    <div class="progress-bar"
+         role="progress-bar"
+         aria-valuenow="60"
+         aria-valuemin="0"
+         aria-valuemax="100"
+         style="width: 60%;">
+      Uploading: 60%
+    </div>
+  </div>
+</div>
+
 <form id="frm-upload-rqtl2-bundle"
       action="{{url_for('upload.rqtl2.upload_rqtl2_bundle',
 	      species_id=species.SpeciesId,
 	      population_id=population.InbredSetId)}}"
       method="POST"
-      enctype="multipart/form-data">
+      enctype="multipart/form-data"
+      data-resumable-action="/no/such/endpoint">
   <input type="hidden" name="species_id" value="{{species.SpeciesId}}" />
   <input type="hidden" name="population_id"
 	 value="{{population.InbredSetId}}" />
 
   {{flash_all_messages()}}
 
-  <div form-group>
+  <div class="form-group">
     <legend class="heading">file upload</legend>
     <label for="file-rqtl2-bundle" class="form-label">R/qtl2 bundle</label>
     <input type="file" id="file-rqtl2-bundle" name="rqtl2_bundle_file"
 	   accept="application/zip, .zip"
 	   required="required"
            class="form-control" />
-    <span class="form-text text-muted"><p>Provide a valid R/qtl2 zip file here. In
-      particular, ensure your zip bundle contains exactly one control file and
-      the corresponding files mentioned in the control file.</p>
-      <p>The control file can be either a YAML or JSON file. <em>ALL</em> other
-	data files in the zip bundle should be CSV files.</p>
-      <p>See the
-	<a href="https://kbroman.org/qtl2/assets/vignettes/input_files.html"
-	   target="_blank">
-	  R/qtl2 file format specifications</a> for more details.</p></span>
+    {{rqtl2_file_help()}}
   </div>
 
   <button type="submit"
@@ -48,22 +102,130 @@
 {%endblock%}
 
 {%block javascript%}
+<script src="{{url_for('base.node_modules',
+             filename='resumablejs/resumable.js')}}"></script>
 <script type="text/javascript" src="/static/js/upload_progress.js"></script>
 <script type="text/javascript">
-  setup_upload_handlers(
-      "frm-upload-rqtl2-bundle", make_data_uploader(
-	  function (form) {
-	      var formdata = new FormData();
-	      formdata.append(
-		  "species_id",
-		  form.querySelector('input[name="species_id"]').value);
-	      formdata.append(
-		  "population_id",
-		  form.querySelector('input[name="population_id"]').value);
-	      formdata.append(
-		  "rqtl2_bundle_file",
-		  form.querySelector("#file-rqtl2-bundle").files[0]);
-	      return formdata;
-	  }));
+  function readBinaryFile(file) {
+      return new Promise((resolve, reject) => {
+          var _reader = new FileReader();
+          _reader.onload = (event) => {resolve(_reader.result);};
+          _reader.readAsArrayBuffer(file);
+      });
+  }
+
+  function computeFileChecksum(file) {
+      return readBinaryFile(file)
+          .then((content) => {
+              return window.crypto.subtle.digest(
+                  "SHA-256", new Uint8Array(content));
+          }).then((digest) => {
+              return Uint8ArrayToHex(new Uint8Array(digest))
+          });
+  }
+
+  function Uint8ArrayToHex(arr) {
+      var toHex = (val) => {
+          _hex = val.toString(16);
+          if(_hex.length < 2) {
+              return "0" + val;
+          }
+          return _hex;
+      };
+      _hexstr = ""
+      arr.forEach((val) => {_hexstr += toHex(val)});
+      return _hexstr
+  }
+
+  var r = Resumable({
+      target: $("#frm-upload-rqtl2-bundle").attr("data-resumable-action"),
+      fileType: ["zip"],
+      maxFiles: 1,
+      forceChunkSize: true,
+      generateUniqueIdentifier: (file, event) => {
+          return computeFileChecksum(file).then((checksum) => {
+              var _relativePath = (file.webkitRelativePath
+                                   || file.relativePath
+                                   || file.fileName
+                                   || file.name);
+              return checksum + "-" + _relativePath.replace(
+                  /[^a-zA-Z0-9_-]/img, "");
+          });
+      }
+  });
+
+  if(r.support) {
+      //Hide form and display drag&drop UI
+      $("#frm-upload-rqtl2-bundle").css("display", "none");
+      $("#resumable-drop-area").css("display", "block");
+
+      // Define UI elements for browse and drag&drop
+      r.assignDrop(document.getElementById("resumable-drop-area"));
+      r.assignBrowse(document.getElementById("resumable-browse-button"));
+
+      // Event handlers
+      r.on("filesAdded", function(files) {
+          displayArea = $("#resumable-selected-files")
+          displayArea.empty();
+          files.forEach((file) => {
+              var displayElement = $(
+                  "#resumable-file-display-template").clone();
+              displayElement.removeAttr("id");
+              displayElement.css("display", "");
+              displayElement.find(".panel-heading").text(file.fileName);
+              list = $("<ul></ul>");
+              list.append($("<li><strong>Name</strong>: "
+                            + (file.name
+                               || file.fileName
+                               || file.relativePath
+                               || file.webkitRelativePath)
+                            + "</li>"));
+              list.append($("<li><strong>Size</strong>: "
+                            + (file.size / (1024*1024)).toFixed(2)
+                            + " MB</li>"));
+              list.append($("<li><strong>Unique Identifier</strong>: "
+                            + file.uniqueIdentifier + "</li>"));
+              list.append($("<li><strong>Mime</strong>: "
+                            + file.file.type
+                            + "</li>"));
+              displayElement.find(".panel-body").append(list);
+              displayElement.appendTo("#resumable-selected-files");
+              $("#resumable-upload-button").css("display", "");
+              $("#resumable-upload-button").on("click", (event) => {
+                  r.upload()
+              });
+          });
+      });
+
+      r.on("uploadStart", (event) => {
+          $("#resumable-upload-button").css("display", "none");
+          $("#resumable-cancel-upload-button").css("display", "");
+          $("#resumable-cancel-upload-button").on("click", (event) => {
+              r.files.forEach((file) => {
+                  if(file.isUploading()) {
+                      file.abort();
+                  }
+              });
+              $("#resumable-cancel-upload-button").css("display", "none");
+              $("#resumable-upload-button").css("display", "");
+          });
+      });
+  } else {
+      setup_upload_handlers(
+          "frm-upload-rqtl2-bundle", make_data_uploader(
+	      function (form) {
+	          var formdata = new FormData();
+	          formdata.append(
+		      "species_id",
+		      form.querySelector('input[name="species_id"]').value);
+	          formdata.append(
+		      "population_id",
+		      form.querySelector('input[name="population_id"]').value);
+	          formdata.append(
+		      "rqtl2_bundle_file",
+		      form.querySelector("#file-rqtl2-bundle").files[0]);
+	          return formdata;
+	      }));
+  }
 </script>
 {%endblock%}