Client-side zipping on the fly

7 min. read

Recently I needed to find a solution to Zip some files on the fly (client-side) when the user clicked a download link on a static site.

Context

The site used Jekyll, was served through GitHub pages, and contained mostly tutorials. Some of those tutorials had example files to download. Since it was an open-source project with many contributors, these example files where spread all over GitHub in gists in the contributors personal accounts. This made it very difficult to update those example files when the tutorial was updated, as you can't submit a PR to a gist file.

Problem

The exercise files of the tutorials where in gists where they could not be updated or new files added. We wanted to bring these files into the tutorials repo to version-control them and generate a zip from the tutorial page itself.

Constrains

  • Jekyll plugins are not allowed in GitHub pages.
  • Version control of a zip file is considered bad practice.
  • Client-side JavaScript does not have read access to files in the server.
  • Markdown doesn't allow html attributes in tags, so we are left with just the href of download links.
  • We wanted a simple solution, to be able to have a single commit that updates the instructions and example code together.
  • No workflow, to be able to push changes and have them get served from the site through GitHub pages.

Possible solution

The solution we agreed was to move those gists to the main repo, together with their tutorial, and zip them on the fly when a link was clicked. After doing some research, I found JSZip.

So we used JSZip and some liquid to automatically generate a zip file containing the tutorial downloadable files, when clicking the download link.

The workflow looks like this:

  • You add a folder with your exercise files inside of the tutorial folder:
    
    javascript_tutorial/lesson3/
        ├── files/
        │   ├── index.html
        │   ├── jquery.js
        │   ├── script.js
        │   └── style.css
        └── tutorial.md
  • Then, add an array variable files to the frontmatter of the tutorial page, containing a list with the paths of the files you added, including folder name:
    
        ---
        layout: page
        title: 'Lesson 3: Introduction to jQuery'
        files:
          - files/index.html
          - files/jquery.js
          - files/script.js
          - files/style.css
        ---
  • In the markdown of the tutorial page, add a download link and make it point to generate-zip ("generate-zip" is just to differenciate download links from other links):
    
    Download the files that you will need to work through the example [here](generate-zip).

Caveats

At the moment this solution allows only one download link per page. This means that tutorials with more than one set of downloadables should be split into two. This can be fixed easily though, but I wanted to go one step at a time.

Also, JSZip relies on blobs to work. The only problematic browsers that didn't have support for blobs seemed to be IE9 and Opera Mini.

Blob support
Blob support according to "Can I use..."

However, JSZip provides a IE version that can be added like this:


  <!--[if IE]>
  <script type="text/javascript" src="http://stuk.github.io/jszip-utils/dist/jszip-utils-ie.js"></script>
  <![endif]-->

Challenges

I had some challenges testing promises and asynchronous methods. The manual testing worked though.

How it works

Liquid reads the variable in the frontmatter with the file names, and makes them available to the JS. Then the JS adds an event listener to links having an href of generate-zip that tells JSZip to generate the zip on the fly when the link is clicked. Files are downloaded asynchronously from the repo.


  {% if page.files %}
  {% assign pageurl = page.url | split: '/' | pop | join: '/' %}
  {% capture files %}[{% for file in page.files %}
  "{{ pageurl }}/{{ file }}"{% if forloop.last %}{% else %},{% endif %}{% endfor %}
  ]{% endcapture %}
  {% endif %}

  <script>
  var Files = {
    all : {{ files }}
  }
  </script>

This is how we share the files from the frontmatter with the JavaScript when Jekyll generates the page.


  var addFiles = function() {
    var filename;
    Files.all.map(function(fileurl) {
      filename = fileurl.replace(/.*\//g, "");
      Zipper.zip.file(filename, downloader.getFile(fileurl), {binary:true});
    });
  };

  var generateZip = function() {
    if (JSZip.support.blob) {
      addFiles();
      generateAsync();
    } else {
      console.log("Blob is not supported")
    }
  }

  var registerListener = function(downloadLink) {
    downloadLink.addEventListener("click", function(event) {
      event.preventDefault();
      zipper.createZip(downloader);
    }, false);
  };

Then we can query all the download links:

var downloadLink = document.body.querySelector('a[href=generate-zip]');

Files are bundled to reduce HTTP requests.

Comments