2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

はじめに

最近、仕事でGoを書き始めたので、勉強がてらファイルをZIP圧縮するWebAssemblyを実装してみました。
ソースコードは、こちら で公開してます。

やったこと

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

Image from Gyazo

もう少し詳しく

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

  • 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
}

参考にしたリンク

おわりに

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

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?