はじめに
最近、仕事でGo
を書き始めたので、勉強がてらファイルをZIP圧縮するWebAssembly
を実装してみました。
ソースコードは、こちら で公開してます。
やったこと
ローカルのファイルを選択して、「Archive」ボタンをクリックすると、WebAssembly
側でファイルをZIP圧縮し、ダウンロードリンクを生成します。
もう少し詳しく
処理のフローをざっくり説明すると、以下のようになります。
-
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
を使うわけではないのですが、話のネタにはできるかなと思いました。
誰かのお役に立てれば幸いです。
ではでは。