PhilLab
PhilLab

Reputation: 5027

How to bundle multiple static files as zip archive for download

My Jekyll page is meant to provide sample solutions for a coding book's exercises. The solutions are a bunch of .cpp files (C++ code files) stored in a folder inside my Jekyll project so that I can open the same folder in an IDE.

I've managed to auto-generate one page per book chapter which displays the relevant solutions as a list of code blocks (one per each exercise). I do that by looping through site.static_files and identifying the files by numbers in their filenames (e.g. two solutions for chapter 1: 01_1_FirstSolution.cpp, 01_2_SecondSolution.cpp)

Now, I want to also provide a zip archive per book chapter containing the relevant .cpp files. I don't want to make the zip file manually because then I would not be able to simply change one of the code files anymore. Ideally, I would like to build a zip file while looping through site.static_files and filtering for the relevant files.

When searching for this, I mainly found speed-optimization plugins for bundling and compressing assets. I am running Jekyll on Windows.

Upvotes: 2

Views: 1174

Answers (3)

PhilLab
PhilLab

Reputation: 5027

There is the jekyll-zip-bundler plugin for this.

How it is used

Filenames as multiple parameters:

{% zip archiveToCreate.zip file1.txt file2.txt %}

Spaces in filenames:

{% zip archiveToCreate.zip file1.txt folder/file2.txt 'file with spaces.txt' %}

A variable to contain a list of files is also possible:

{% zip ziparchiveToCreate.zip {{ chapter_code_files }} %}

The plugin code

# frozen_string_literal: true

# Copyright 2021 by Philipp Hasper
# MIT License
# https://github.com/PhilLab/jekyll-zip-bundler

require 'jekyll'
require 'zip'
# ~ gem 'rubyzip', '~>2.3.0'

module Jekyll
  # Valid syntax:
  # {% zip archiveToCreate.zip file1.txt file2.txt %}
  # {% zip archiveToCreate.zip file1.txt folder/file2.txt 'file with spaces.txt' %}
  # {% zip {{ variableName }} file1.txt 'folder/file with spaces.txt' {{ otherVariableName }} %}
  # {% zip {{ variableName }} {{ VariableContainingAList }} %}
  class ZipBundlerTag < Liquid::Tag
    VARIABLE_SYNTAX = /[^{]*(\{\{\s*[\w\-.]+\s*(\|.*)?\}\}[^\s{}]*)/mx.freeze
    CACHE_FOLDER = '.jekyll-cache/zip_bundler/'

    def initialize(tag_name, markup, tokens)
      super
      # Split by spaces but only if the text following contains an even number of '
      # Based on https://stackoverflow.com/a/11566264
      # Extended to also not split between the curly brackets of Liquid
      # In addition, make sure the strings are stripped and not empty
      @files = markup.strip.split(/\s(?=(?:[^'}]|'[^']*'|{{[^}]*}})*$)/)
                     .map(&:strip)
                     .reject(&:empty?)
    end

    def render(context)
      # First file is the target zip archive path
      target, files = resolve_parameters(context)
      abort 'zip tag must be called with at least two files' if files.empty?

      zipfile_path = CACHE_FOLDER + target
      FileUtils.makedirs(File.dirname(zipfile_path))

      # Create the archive. Delete file, if it already exists
      File.delete(zipfile_path) if File.exist?(zipfile_path)
      Zip::File.open(zipfile_path, Zip::File::CREATE) do |zipfile|
        files.each do |file|
          # Two arguments:
          # - The name of the file as it will appear in the archive
          # - The original file, including the path to find it
          zipfile.add(File.basename(file), file)
        end
      end
      puts "Created archive #{zipfile_path}"

      # Add the archive to the site's static files
      site = context.registers[:site]
      site.static_files << Jekyll::StaticFile.new(site, "#{site.source}/#{CACHE_FOLDER}",
                                                  File.dirname(target),
                                                  File.basename(zipfile_path))
      # No rendered output
      ''
    end

    def resolve_parameters(context)
      # Resolve the given parameters to a file list
      target, files = @files.map do |file|
        next file unless file.match(VARIABLE_SYNTAX)

        # This is a variable. Look it up.
        context[file]
      end

      [target, files]
    end
  end
end

Liquid::Template.register_tag('zip', Jekyll::ZipBundlerTag)

Upvotes: 1

TechnoRed
TechnoRed

Reputation: 21

Jekyll is a primarily static website builder, so perhaps it'd be ideal to run a program or shell scripts before building with Jekyll and have them output to the site.static_files location you wish to output to as above.

You could use a script before running Jekyll, to zip your files and then run the Jekyll build process.

There are also Generator plugins which you could write to achieve similar results, using either the plugin, or Ruby's system commands to run scripts as part of your build.

Upvotes: 1

Daniil Loban
Daniil Loban

Reputation: 4381

If it's possible to get content of your files (fetch etc.) you may use JSZip library to pack them in Zip. I'm not sure that this fits for you in jekyll... but i would try doing like this.

To try an example just create an html file or open my example with fetch.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title></title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.6.0/jszip.min.js" integrity="sha512-uVSVjE7zYsGz4ag0HEzfugJ78oHCI1KhdkivjQro8ABL/PRiEO4ROwvrolYAcZnky0Fl/baWKYilQfWvESliRA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script type="module" src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.0/FileSaver.min.js" integrity="sha512-csNcFYJniKjJxRWRV1R7fvnXrycHP6qDR21mgz1ZP55xY5d+aHLfo9/FcGDQLfn2IfngbAHd8LdfsagcCqgTcQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> 
</head>
<body>
<script>
    var zip = new JSZip();
    zip.file("Hello.txt", "Hello World\n");
    zip.file("Hello2.txt", "Hello2 World\n");
    //var img = zip.folder("images");
    //img.file("smile.gif", imgData, {base64: true});
    zip.generateAsync({type:"blob"})
    .then((content) => {
        // see FileSaver.js
        saveAs(content, "example.zip");
    });
</script>
</body>
</html>

Upvotes: 2

Related Questions