GoでファイルをZIP圧縮するWebAssemblyを実装してみた

## はじめに
最近、仕事で`Go`を書き始めたので、勉強がてらファイルをZIP圧縮する`WebAssembly`を実装してみました。
ソースコードは、[こちら](https://github.com/imunew/file-archive-wasm) で公開してます。

## やったこと
ローカルのファイルを選択して、「Archive」ボタンをクリックすると、`WebAssembly` 側でファイルをZIP圧縮し、ダウンロードリンクを生成します。

[![Image from Gyazo](https://i.gyazo.com/858ed4928ae9bb853d818bbf0b433275.gif)](https://gyazo.com/858ed4928ae9bb853d818bbf0b433275)

## もう少し詳しく
処理のフローをざっくり説明すると、以下のようになります。

- `WebAssembly`を読み込む
- 「Archive」ボタンをクリックしたときに、`javascript`側でファイルを読み込み（`readAsDataURL`）
- `base64`で読み込まれたファイルを`WebAssembly` 側でZIP圧縮する
- ZIPファイルを`base64`でエンコーディングし、`Data URI`にして、`a`タグにセットする

## 詳しくはソースコード参照

```index.html
<!doctype html>
<!--
Copyright 2018 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
-->
<html lang="en">

<head>
	<meta charset="utf-8">
	<title>Go wasm</title>
	<style>
		#archive-form {
			margin: 10vw 15vw;
		}
		div.row {
			margin-top: 16px;
		}
		div.row label {
			display: block;
		}
		div.form-control {
			margin-top: 8px;
			margin-left: 1rem;
		}
		input[type="file"] {
			width: 100%;
		}
	</style>
</head>

<body>
	<section id="archive-form">
		<div class="row">
			<label>1. Select files</label>
			<div class="form-control">
				<input type="file" id="src-file" multiple onchange="hideDownloadLink()">
			</div>
		</div>
		<div class="row">
			<label>2. Zip Archive</label>
			<div class="form-control">
				<button onClick="onClickArchiveButton();" id="archive-button" disabled>Archive</button>
			</div>
		</div>
		<div id="download-link" class="row">
			<label>3. Download zip file</label>
			<div class="form-control">
				<a href="#" id="download-zip" download="archive.zip">archive</a>
			</div>
		</div>
	</section>
	<!--
	Add the following polyfill for Microsoft Edge 17/18 support:
	<script src="https://cdn.jsdelivr.net/npm/text-encoding@0.7.0/lib/encoding.min.js"></script>
	(see https://caniuse.com/#feat=textencoder)
	-->
	<script src="wasm_exec.js"></script>
	<script>
		if (!WebAssembly.instantiateStreaming) { // polyfill
			WebAssembly.instantiateStreaming = async (resp, importObject) => {
				const source = await (await resp).arrayBuffer();
				return await WebAssembly.instantiate(source, importObject);
			};
		}

		const go = new Go();
		let module, wasm;
		WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
			module = result.module;
			wasm = result.instance;
			enableArchiveButton();
			hideDownloadLink();
			go.run(wasm);
		}).catch((err) => {
			console.error(err);
		});

		function onClickArchiveButton() {
			try {
				const srcFile = document.getElementById('src-file');
				const promises = Array.from(srcFile.files).map((file) => {
					return new Promise((resolve, _) => {
						const reader = new FileReader();
						reader.addEventListener('load', () => {
							resolve({
								fileName: file.name,
								lastModified: file.lastModified,
								base64: reader.result.split('base64,')[1]
							});
						});
						reader.readAsDataURL(file);
					});
				});
				Promise.all(promises).then((data) => {
					// console.info({ data });
					archive(...data);
					showDownloadLink();
				});
			} finally {
				wasm = WebAssembly.instantiate(module, go.importObject); // reset instance
			}
		}

		function enableArchiveButton() {
			document.getElementById('archive-button').disabled = false;
		}

		function showDownloadLink() {
			document.getElementById('download-link').style = undefined;
		}

		function hideDownloadLink() {
			document.getElementById('download-link').style.display = 'none';
		}
	</script>

</body>

</html>
```

```main.go
package main

import (
	"archive/zip"
	"bytes"
	"encoding/base64"
	"fmt"
	"syscall/js"
	"time"
)

type SourceFile struct {
	FileName string
	Modified time.Time
	Blob     []byte
}

func main() {
	c := make(chan struct{}, 0)
	println("Go WebAssembly Initialized")
	registerCallbacks()
	<-c
}

func registerCallbacks() {
	js.Global().Set("archive", js.FuncOf(archive))
}

func archive(this js.Value, args []js.Value) interface{} {

	var sourceFiles []SourceFile
	for _, arg := range args {
		//fmt.Printf("%s\n", arg.Get("fileName"))
		//fmt.Printf("%s\n", arg.Get("lastModified"))
		//fmt.Printf("%s\n", arg.Get("base64"))
		blob, err := base64.StdEncoding.DecodeString(arg.Get("base64").String())
		if err != nil {
			panic(err)
		}
		//fmt.Printf("%s\n", string(blob))
		jsTime := arg.Get("lastModified").Int()
		modified := time.Unix(int64(jsTime/1000), int64((jsTime%1000)*1000*1000))
		sourceFiles = append(sourceFiles, SourceFile{
			FileName: arg.Get("fileName").String(),
			Modified: modified,
			Blob:     blob,
		})
	}

	buf := compress(sourceFiles)
	encoded := base64.StdEncoding.EncodeToString(buf.Bytes())
	//fmt.Printf("%s\n", encoded)

	document := js.Global().Get("document")
	anchor := document.Call("getElementById", "download-zip")
	dataUri := fmt.Sprintf("data:%s;base64,%s", "application/zip", encoded)
	anchor.Set("href", dataUri)

	return nil
}

func compress(files []SourceFile) *bytes.Buffer {
	b := new(bytes.Buffer)
	w := zip.NewWriter(b)

	for _, file := range files {
		hdr := zip.FileHeader{
			Name:     "/" + file.FileName,
			Modified: file.Modified,
			Method:   zip.Deflate,
		}
		f, err := w.CreateHeader(&hdr)
		if err != nil {
			panic(err)
		}

		_, err = f.Write(file.Blob)
		if err != nil {
			panic(err)
		}
	}

	err := w.Close()
	if err != nil {
		panic(err)
	}

	return b
}
```

## 参考にしたリンク
- https://github.com/golang/go/wiki/WebAssembly
- https://buildersbox.corp-sansan.com/entry/2019/02/14/113000
- https://zenn.dev/tomi/articles/2020-11-10-go-web11

## おわりに
本当はファイルのパスを渡すだけで、それ以降の処理は全部`WebAssembly`側でZIP圧縮したかったのですが、ブラウザ側でローカルのパスが取得できないので、ファイル名と最終更新日時と`Base64`にエンコーディングしたバイト配列を渡すようにしました。  
また、ZIPファイルの保存先も指定したかったのですが、これも、ブラウザではできないので、　`Data URI`でダウンロードリンクを作る方法にしました。  
仕事で`WebAssembly`を使うわけではないのですが、話のネタにはできるかなと思いました。 　
誰かのお役に立てれば幸いです。 　
ではでは。