Deno でファイルの読み込み base64 文字列化して bundle する

「Deno Deploy で Web ページを公開したいな」と考えたものの、画像などのホストをしてくれないなどの問題発生。
もちろん対応方法はドキュメントはあるのですが、「テンプレートエンジンを使いたい」などあるとそれでも不足。

今回は調べたファイルの読み込みと、base64文字列化についてメモです。

実行環境

  • Deno 1.10.2

Deno でファイルの読み込み

読み込みの対象としてtest.txtを用意。

test.txt
1
Hello world

このファイルを Deno で読み込む。いくつか方法があるので列挙。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

> await Deno.readFile("./test.txt")
Uint8Array(11) [
72, 101, 108, 108,
111, 32, 119, 111,
114, 108, 100
]

> Deno.readFileSync("./test.txt")
Uint8Array(11) [
72, 101, 108, 108,
111, 32, 119, 111,
114, 108, 100
]

> await Deno.readTextFile("./test.txt")
"Hello world"

> Deno.readTextFileSync("./test.txt")
"Hello world"

バイナリとして読むもの、文字列として読み込むものがある。

バイナリで読んだものを文字列化する場合は次のようになる。

1
2
3
4
5
> new TextDecoder().decode( await Deno.readFile("./test.txt"))
"Hello world"

> new TextDecoder().decode( Deno.readFileSync("./test.txt"))
"Hello world"

とりあえず、ファイルの読み込みを確認。
(もちろん画像のファイルだって読み込みはできる。)

Base64 エンコード デコード

これが本題。

使う bese64 のモジュールはこちら。
Deno Standard Library deno.land / std@0.97.0 / encoding / base64.ts

3rd パーティの base64 エンコードの用のライブラリもあるが、今回の用途としてはこれで十分。

REPL でのモジュール読み込みができ何用なので、テスト用のスクリプトで、試してみます。

base64_test.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import {
encode,
decode,
} from "https://deno.land/std@0.97.0/encoding/base64.ts";

const readData = await Deno.readFile("./test.txt");

console.log(readData);

console.log(new TextDecoder().decode(readData));
// => Hello world

// バイナリ->base64変換
const encodedData = encode(readData);
console.log(encodedData);

// base64->バイナリ変換
const decodedData = decode(encodedData);
console.log(decodedData);

// デコードしたデータは元のデータと同一なのか? ==> 同一ではない
// 比較の仕方を間違っているかも
console.log(readData === decodedData);

// バイナリ->base64->バイナリ->エンコード
console.log(new TextDecoder().decode(decodedData));
// => Hello world

実行結果は次の通り。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ deno run --allow-read base64_test.ts
Check file:///usr/src/app/base64_test.ts
Uint8Array(11) [
72, 101, 108, 108,
111, 32, 119, 111,
114, 108, 100
]
Hello world
SGVsbG8gd29ybGQ=
Uint8Array(11) [
72, 101, 108, 108,
111, 32, 119, 111,
114, 108, 100
]
false
Hello world

一度 base64 への変換を経由して再度ファイル内の文字列を取得できました。

ファイルを base64 エンコードして出力してみる。

ここまでファイルの読み込み、base64 エンコード/デコードを試しました。
これができるということは、エンコードしたファイルが作れるはずということで、次のツールを作りました。

asset_builder.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import { parse } from "https://deno.land/std@0.66.0/flags/mod.ts";
import { encode } from "https://deno.land/std@0.97.0/encoding/base64.ts";

interface ImportFile {
importPath: string;
calledName: string;
}

interface ImportedFile {
content: string;
extension: string;
}

const parsedArgs = parse(Deno.args);

const importFileName =
typeof parsedArgs["inport-file"] === "string"
? parsedArgs["inport-file"]
: "asset_config.json";

let bundleList = "";

try {
const readFile = Deno.readTextFileSync(importFileName);
bundleList = readFile;
} catch (error) {
if (error.name === "NotFound") {
console.error(
`Import Config file [${importFileName}] is not Found!!\nplease confirm.`
);
Deno.exit();
}
throw error;
}

const bundleListArr: [ImportFile] = JSON.parse(bundleList).files;

let bundledObject: { [key: string]: ImportedFile } = {};

bundleListArr.forEach((file) => {
try {
const content = encode(Deno.readFileSync(file.importPath));
const extension = file.importPath.split(".").slice(-1)[0];
bundledObject[`${file.calledName}`] = { content, extension };
} catch (error) {
if (error.name === "NotFound") {
console.error(
`Import file [${file.importPath}] is not Found!!\nplease confirm.`
);
Deno.exit();
}
throw error;
}
});

const exportObject = Object.keys(bundledObject)
.map(
(key) =>
`"${key}":{content:decode("${bundledObject[key].content}"),\nextension: "${bundledObject[key].extension}"}`
)
.join(",\n");

const exportText = `
import { decode } from "https://deno.land/std@0.97.0/encoding/base64.ts";

const bundledObject = {
files:{
${exportObject}
}
}

export default bundledObject
`;
console.log(exportText);

このツールを使うために、asset_config.jsonを用意します。

asset_config.json
1
2
3
4
5
6
7
8
{
"files": [
{
"importPath": "./test.txt",
"calledName": "text"
}
]
}

次のコマンドで実行。
deno run --allow-read asset_builder.ts > asset.ts

すると、asset.ts が作成されます。

asset.ts
1
2
3
4
5
6
7
8
9
import { decode } from "https://deno.land/std@0.97.0/encoding/base64.ts";

const bundledObject = {
files: {
text: { content: decode("SGVsbG8gd29ybGQ="), extension: "txt" },
},
};

export default bundledObject;

これを読み込んでみます。

base64_test2.ts
1
2
3
4
5
6
7
import asset from "./asset.ts";

console.log(asset);

console.log(asset.files["text"]);

console.log(new TextDecoder().decode(asset.files["text"].content));

実行結果は次の通り。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ deno run base64_test2.ts
{
files: {
text: {
content: Uint8Array(11) [
72, 101, 108, 108,
111, 32, 119, 111,
114, 108, 100
],
extension: "txt"
}
}
}
{
content: Uint8Array(11) [
72, 101, 108, 108,
111, 32, 119, 111,
114, 108, 100
],
extension: "txt"
}
Hello world

使いたいファイルを base64 文字列で、.ts ファイルにまとめておくことで、–allow-read のパーミッションが要らない。
欲しいものができた。

そして、外部のファイルを読むわけでは無いので、このままバンドルできる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ deno bundle base64_test2.ts > bundled_base64_test2.js
Check file:///usr/src/app/base64_test2.ts
Bundle file:///usr/src/app/base64_test2.ts

$ deno run bundled_base64_test2.js
{
files: {
text: {
content: Uint8Array(11) [
72, 101, 108, 108,
111, 32, 119, 111,
114, 108, 100
],
extension: "txt"
}
}
}
{
content: Uint8Array(11) [
72, 101, 108, 108,
111, 32, 119, 111,
114, 108, 100
],
extension: "txt"
}
Hello world

Deno Deploy で使ってみる

Deno Deploy 用の asset_server_deploy.ts を用意して、動作確認する。

asset_server_deploy.ts
1
2
3
4
5
6
7
8
import asset from "./asset.ts";

addEventListener("fetch", async (event) => {
const response = new Response(asset.files["text"].content, {
headers: { "content-type": "text/plain" },
});
event.respondWith(response);
});

deployctl run asset_server_deploy.ts で起動して、localhost:8080にアクセスする。
Helloworld が返ってくることを確認できます。

Deno Deploy 用のファイルを bundle してみる

先に用意した asset_server_deploy.ts をこのまま bundle するとエラーになる。

asset_server_deploy.tsをbundle
1
2
3
4
5
6
$ deno bundle asset_server_deploy.ts > bundled_asset_server_deploy.js
Check file:///usr/src/app/asset_server_deploy.ts
error: TS2339 [ERROR]: Property 'respondWith' does not exist on type 'Event'.
event.respondWith(response);
~~~~~~~~~~~
at file:///usr/src/app/asset_server_deploy.ts:7:9

このあたりの対応について issue が挙がっていました。
add instructions on how to make deno lsp happy with deno deploy types #24

Deno Deploy用の型ファイルを参照する必要があるようです。こちらを参考に以下のように対応します。

型ファイルの取得

1
$ deployctl types > deploy.d.ts

型の参照を記述

asset_server_deploy.ts(修正)
1
2
3
4
5
6
7
8
9
/// <reference path="./deploy.d.ts" />
import asset from "./asset.ts";

addEventListener("fetch", async (event) => {
const response = new Response(asset.files["text"].content, {
headers: { "content-type": "text/plain" },
});
event.respondWith(response);
});

改めて起動

deployctl run bundled_asset_server_deploy.js で起動して、localhost:8080にアクセスする。
Helloworld が返ってくることを確認できます。

画像を base64 でエンコードしたファイルを使って bundle したファイルを作ると、Deno Deploy 環境でソースコードだけで、画像も配信可能です。
実際デプロイして確認してみたところ全く問題なく動作できました。

注意事項

base64 変換した文字列が長くなった上で / があると フォーマッターが気を利かせて、改行してくれたりします。
だいたいファイルが壊れるので、フォーマッターを効かせないように注意。


今回、ファイルをバンドルして、base64 への変換をしてみたのですが、思った以上に上手く動いてくれました。
毎回これを手元に用意したくないので 3rd パーティのライブラリとして公開したいと考えてます。

ではでは。