Page MenuHomeSoftware Heritage

Gandi drag & drop
ArchivedPublic

Authored by jbertran on Jun 15 2016, 12:22 PM.
From a8f26192676da86032fc80ffe440875d2c7b2023 Mon Sep 17 00:00:00 2001
From: "Jordi BERTRAN DE BALANDA (swhintern)" <j.debalanda@gmail.com>
Date: Wed, 15 Jun 2016 12:16:35 +0200
Subject: [PATCH] swhsearchbox: provide drag & drop user interface
---
.../themes/layerswp-swh/assets/js/search.js | 279 +++++++++++++++++++--
.../wp-content/themes/layerswp-swh/functions.php | 48 +++-
2 files changed, 304 insertions(+), 23 deletions(-)
diff --git a/htdocs/wp-content/themes/layerswp-swh/assets/js/search.js b/htdocs/wp-content/themes/layerswp-swh/assets/js/search.js
index 91acb2d..2c087de 100644
--- a/htdocs/wp-content/themes/layerswp-swh/assets/js/search.js
+++ b/htdocs/wp-content/themes/layerswp-swh/assets/js/search.js
@@ -4,7 +4,7 @@ var CHECKSUM_RE = /^([0-9a-f]{40}|[0-9a-f]{64})$/i;
function swh_search_msg(msg, cls) {
var html;
html = '<span class="search-result ' + cls + '">' + msg + '</span>';
- jQuery('#swh-search-result').html(html);
+ jQuery('#swh-search-message').html(html);
}
function swh_search_show_results(j, status) {
@@ -24,21 +24,270 @@ function swh_fail_result() {
swh_search_msg('The Software Heritage API server is unavailable. Please try again later.', 'error');
}
+/**
+ * Client-side hashing utils
+ */
+var nameList = []; /** Avoid adding the same file twice **/
+
+function handleFiles(myfiles, fileLister, searchForm) {
+ for (var i = 0; i < myfiles.length; i++) {
+ var file = myfiles.item(i);
+ if (nameList.indexOf(file.name) == -1) {
+ nameList.push(file.name);
+ var fr = new FileReader();
+ fileLister.append(make_row(file.name));
+ bind_reader(fr, file.name, searchForm);
+ fr.readAsArrayBuffer(file);
+ }
+ }
+};
+
+function bind_reader(filereader, filename, searchForm) {
+ filereader.onloadend = function(evt) {
+ if (evt.target.readyState == FileReader.DONE){
+ return fileReadDone(evt.target.result, filename, searchForm);
+ }
+ };
+}
+
+function make_row(name) {
+ return '<div class="span3">'+name+'</div>';
+}
+
+function fileReadDone(buffer, fname, searchForm) {
+ var wordArray = CryptoJS.lib.WordArray.create(buffer);
+ var sha1 = CryptoJS.SHA1(wordArray);
+ /**
+ var git_hd = "blob " + wordArray.length + "\0";
+ var git_array = CryptoJS.enc.utf8.parse(git_hd).concat(wordArray);
+ var sha256 = CryptoJS.SHA256(wordArray);
+ var sha1_git = CryptoJS.SHA1(git_array);
+ **/
+ searchForm.append($("<input>", {type: "hidden",
+ name: fname,
+ value: sha1}
+ ));
+}
+
+/**
+ * JQuery setup
+ */
+
jQuery(document).ready(function($){
- $('#swh-search-submit').click(function(event){
- event.preventDefault();
- var q = $('#swh-search-query').val();
- if (!q || !q.match(CHECKSUM_RE))
- swh_search_msg('Please enter a file checksum in the search box.', 'error');
- else
- $.ajax({
- url: SWH_API_URL + '/search/' + q + '/',
- dataType: 'json',
- beforeSend: function (xhr) {
- swh_search_msg('Searching for a file with hash ' + q + '...', '');
- },
- success: swh_search_show_results,
- error: swh_fail_result,
+
+ $.fn.extend({
+ /**
+ * Call on any HTMLElement to make that element the recipient of files
+ * drag & dropped into it.
+ * Files then have their sha1 checksum calculated
+ * and searched in SWH.
+ * Args:
+ * resultDiv: the table where the result should be displayed
+ * errorDiv: the element where the error message should be displayed
+ */
+ filedrop: function(fileLister, searchForm) {
+
+ return this.each(function() {
+
+ var dragwin = $(this);
+ var fileshovering = false;
+
+ dragwin.on('dragover', function(event) {
+ event.stopPropagation();
+ event.preventDefault();
+ });
+
+ dragwin.on('dragenter', function(event) {
+ event.stopPropagation();
+ event.preventDefault();
+ if (!fileshovering) {
+ dragwin.css("border-style", "solid");
+ dragwin.css("box-shadow", "inset 0 3px 4px");
+ fileshovering = true;
+ }
+ });
+
+ dragwin.on('dragover', function(event) {
+ event.stopPropagation();
+ event.preventDefault();
+ if (!fileshovering) {
+ dragwin.css("border-style", "solid");
+ dragwin.css("box-shadow", "inset 0 3px 4px");
+ fileshovering = true;
+ }
+ });
+
+ dragwin.on('dragleave', function(event) {
+ event.stopPropagation();
+ event.preventDefault();
+ if (fileshovering) {
+ dragwin.css("border-style", "dashed");
+ dragwin.css("box-shadow", "none");
+ fileshovering = false;
+ }
+ });
+
+ dragwin.on('drop', function(event) {
+ event.stopPropagation();
+ event.preventDefault();
+ if (fileshovering) {
+ dragwin.css("border-style", "dashed");
+ dragwin.css("box-shadow", "none");
+ fileshovering = false;
+ }
+ var myfiles = event.originalEvent.dataTransfer.files;
+ if (myfiles.length >= 1) {
+ handleFiles(myfiles, fileLister, searchForm);
+ }
+ });
});
+ },
+ /**
+ * Call on a jQuery-selected input to make it sensitive to
+ * the reception of new files, and have it process received
+ * files.
+ * Args:
+ * fileLister: the element keeping track of the files
+ * searchForm: the form whose submission will POST the file
+ * information
+ */
+ filedialog: function(fileLister, searchForm) {
+ return this.each(function() {
+ var elem = $(this);
+ elem.on('change', function(){
+ handleFiles(this.files, fileLister, searchForm);
+ });
+ });
+ },
+ /**
+ * Call on a jQuery-selected element to delegate its click
+ * event to the given input instead.
+ * Args:
+ * input: the element to be clicked when the caller is clicked.
+ */
+ inputclick: function(input) {
+ return this.each(function() {
+ $(this).click(function(event) {
+ event.preventDefault();
+ input.click();
+ });
+ });
+ },
+ /**
+ * Call on a search form to delegate its click event to search
+ * the files via an AJAX call
+ * Args:
+ * resultElem: the element containing the result table and
+ * the result text after completion of the AJAX call
+ */
+ ajaxResults: function(resultElem) {
+ return this.each(function() {
+ $(this).submit(function(e) {
+ e.preventDefault();
+ var q = $('#swh-search-query').val();
+ if (q && !q.match(CHECKSUM_RE))
+ swh_search_msg('Please enter a file checksum in the search box.', 'error');
+ else
+ $.ajax({
+ method: 'POST',
+ url: 'http://localhost:6543/api/1/search/',
+ dataType: 'json',
+ data: $(this).serialize(),
+ beforeSend: function (xhr) {
+ swh_search_msg('Searching for your files...', '');
+ },
+ success: function(data) {
+ ajaxSearchSuccess(data, resultElem);
+ swh_search_msg('Search complete!', '');
+ },
+ error: swh_fail_result,
+ });
+ });
+ });
+ },
+ /**
+ * Update an element to reflect the search result stats
+ * Args:
+ * statsObj: the stats object {nbfile:, pct:}
+ */
+ updateStats: function(statsObj) {
+ var pct = statsObj.pct;
+ var nbf = statsObj.nbfiles;
+ var hue=(pct*120).toString(10);
+ $(this).css('background-color', ["hsl(",hue,",45%,48%)"].join(""));
+ $(this).text('Found ' + pct.toFixed(2) + '% of ' + nbf + ' files (click to expand).');
+ },
+ /**
+ * Update an element to contain a new result in the result table
+ * Args:
+ * resultObj: the result object {filename:, sha1:, found:}
+ */
+ addResult: function(resultObj) {
+ var found = resultObj.found?'Yes':'No';
+ var classfound = resultObj.found?'found':'not-found';
+ var checksum = resultObj.sha1;
+ var fname = !resultObj.filename?'From input': resultObj.filename;
+ $(this).append($('<tr></tr>')
+ .append($('<td></td>').append(fname))
+ .append($('<td></td>').append(checksum))
+ .append($('<td></td>').append($('<span></span>')
+ .addClass(classfound)
+ .text(found))));
+ },
+ /**
+ * Call on an element to make it collapsible on a hijacked click from the
+ * target button
+ * Args:
+ * collapser: the element that will toggle the collapse
+ */
+ collapses: function(collapsed) {
+ return this.each(function() {
+ collapsed.css('display', 'none');
+ $(this).click(function (event) {
+ event.preventDefault();
+ collapsed.slideToggle('fast');
+ });
+ });
+ }
});
+
+ function ajaxSearchSuccess(data, resultElem) {
+ // Prepare stats and result
+ var statsDiv = $('<button></button>')
+ .attr('id', 'collapse-button')
+ .css('border-style', 'none');
+ statsDiv.updateStats(data.search_stats);
+ var resultTab = $('<table></table>')
+ .attr('id', 'collapsible-search-results')
+ .append($('<thead></thead>')
+ .append($('<th></th>').text('Origin'))
+ .append($('<th></th>').text('SHA1 checksum'))
+ .append($('<th></th>').text('Found in SWH')))
+ for (let resE of data.search_res) {
+ if (resE.filename == 'q')
+ resE.filename = 'From text input';
+ resultTab.addResult(resE);
+ }
+ // Change the DOM
+ resultElem.empty();
+ resultElem.append(statsDiv);
+ resultElem.append(resultTab);
+ // Set the click listener
+ $("#collapse-button").collapses($("#collapsible-search-results"));
+ }
+
+ function urlizeSHA1(sha1){
+ return $('<a></a>')
+ .attr('href', '/browse/' + sha1)
+ .text(sha1);
+ }
+
+ // Delegate dropbox click
+ $("#swh-search-dropbox").inputclick($("#files-input"));
+ // Make the dropbox available for drag & drop
+ $("#files-input").filedialog($("#hashed-file-list"), $("#swh-search"));
+ // Make the submission button receptive to files
+ $("#swh-search-dropbox").filedrop($("#hashed-files-list"), $("#swh-search"));
+ // Hijack the form submission for ajax
+ $("#swh-search").ajaxResults($("#swh-search-results"));
});
diff --git a/htdocs/wp-content/themes/layerswp-swh/functions.php b/htdocs/wp-content/themes/layerswp-swh/functions.php
index 8b8fe1c..336c90b 100644
--- a/htdocs/wp-content/themes/layerswp-swh/functions.php
+++ b/htdocs/wp-content/themes/layerswp-swh/functions.php
@@ -84,23 +84,55 @@ add_shortcode('swhcounter', 'swh_shortcode_counter');
// template for generating SWH search box
// Usage: [swhsearchbox]
function swh_shortcode_search_box( $atts, $content = null ) {
- wp_enqueue_script( // load client-side search code
+ wp_enqueue_script( // load client-side search code
'layers-swh-search',
get_stylesheet_directory_uri() . '/assets/js/search.js',
array()
- );
- ob_start();
- ?>
- <div class="swh-search">
- <form id="swh-search" action="//archive.softwareheritage.org/search" method="get">
+ );
+ wp_enqueue_script( // load client-side hashing dependency
+ 'layers-swh-search',
+ get_stylesheet_directory_uri() . '/assets/js/core.js',
+ array()
+ );
+ wp_enqueue_script( // load client-side hashing dependency
+ 'layers-swh-search',
+ get_stylesheet_directory_uri() . '/assets/js/lib-typedarrays.js',
+ array()
+ );
+ wp_enqueue_script( // load client-side hashing dependency
+ 'layers-swh-search',
+ get_stylesheet_directory_uri() . '/assets/js/sha1.js',
+ array()
+ );
+ ob_start();
+ ?>
+ <div class="swh-search">
+ <form id="swh-search-text">
<input id="swh-search-query" type="text" name="q"
placeholder="enter the sha1 of some (source code) file to check if it is already in our archive"
/>
+ <div id="swh-search-dropbox" style="border: 1px dashed black">
+ Drag and drop or click here to hash files and search for them.
+ Your files will NOT be uploaded, hashing is done locally.
+ Filesizes over 20Mb may be slow to process, use with care.
+ <div id="hashed-files-list">
+ </div>
+ </div>
<input id="swh-search-submit" type="submit" value="Search" />
</form>
- <div id="swh-search-result"></div>
+ <input type="file"
+ id="files-input"
+ multiple
+ name="filename"
+ value=""
+ style="display:none"
+ placeholder="File(s) to upload, hash and search for SHA-1 or SHA-256 checksum" />
+ <div id="swh-search-message">
+ </div>
+ <div id="swh-search-results">
+ </div>
</div>
<?php
return ob_get_clean();
}
-add_shortcode('swhsearchbox', 'swh_shortcode_search_box');
\ No newline at end of file
+add_shortcode('swhsearchbox', 'swh_shortcode_search_box');
--
2.8.1

Event Timeline

jbertran created this object in space S1 Public.