Nimでリソースの埋め込み

# 概要

Nimの実行ファイルにリソースファイル(htmlやイメージファイル)を埋め込む方法の１つとしてご参考になればと思います。

## リソースの埋め込み

Javaだとresourcesフォルダ配下のファイルをJarに固めたり、Windowsのバイナリならリソースコンパイラでコンパイルしたものを実行ファイルとリンクしてくれたりという機能がありますが、Nim自体にはそのような機能がありません(よね？)。

なので、ちょっと気の利いたWebアプリ(Jesterで作成)しても、実行モジュール1本をはいっと渡すだけではなくHTML/JavaScript/CSSなどのソースも付与しないとダメなわけで、ちょっとめんどくさくなるわけです。

## GNU Binary Utilitiesのobjcopy

同じようなことを考える人は昔からいるもので、UNIX系ツールだとobjcopyというコマンドを使ってファイルをオブジェクトファイルに変換し、リンカーでリンクするといったことができるようです。

https://www.linuxjournal.com/content/embedding-file-executable-aka-hello-world-version-5967

Nimでもobjcopyを使えばよさそうなんですが、ファイルがたくさんあった場合には、１個ずつファイルして指定するのもめんどくさい・・・(Windows版Nimでもmingw64/binにobjcopyがあるので使えそうですけど)

## リソース埋め込み方式

以下2つの順番でリソース埋め込みを実装したいと思います。

1. リソースとして埋め込みたいフォルダをZipの塊にし、そのZipをBase64に変換してソースに埋め込む
2. 実行時にソースにあるZip(Base64)を実ファイル化し、Zipの中身を任意のフォルダに展開する

### リソースを埋め込む手順(nimbleにタスクを追加します)

* 今回は**html**フォルダ配下をリソースファイルとみなします。
* htmlフォルダ配下をzipに固めます。
* zip化されたリソースファイルを読み込んでBASE64の文字列に変換し、src/res/resource_file.nimを自動生成させます。

### フォルダ構成

```
APPLICATION-FOLDER
+---html              <= これをリソースフォルダとします
|   |   index.html
|   |
|   \---images
|           nim.png
|
+---src
|   |   http.nim
|   |   nim.cfg
|   |
|   +---httppkg
|   |       main.nim            <= リソースを展開する
|   |
|   \---res
|           resources.nim       <= BASE64を取得して、ZIPをファイル化して展開する
|           resource_file.nim   <= ZIP化したリソースをBASE64にして埋め込んだソース(自動生成)
|
\---util
        embed_file.nim
        make_resource.nim       <= リソースフォルダをZip→Base64化するツール(nimbleから起動される)
        nim.cfg
        rename_app.nim
```

### リソースを展開する手順

* アプリケーション起動時に、埋め込まれたリソース(BASE64化されたZIP)をTEMPフォルダにコピーし、アプリケーションがあるパスの配下に展開する
* アプリケーション側が、展開されたリソースをなんか使ったらいいんじゃない

htmlフォルダ配下は、こんなHTML

![image.png](https://qiita-image-store.s3.amazonaws.com/0/58872/b661a0e2-9e31-4e2a-1114-2cbf69ee34c8.png)

### リソースフォルダをZIP化し、ソースファイルに埋め込む

nimbleファイルにタスクを追加します。実態は、util/make_resource.nimを実行します。

```nim:http.nimble
const resourceDir = "http"
task make_resource, "リソースを作成":
  exec "nim c -r --out:bin/make_resource util/make_resource.nim " & resourceDir
```

Zipの作成＆リソース埋め込みソースの自動生成するソース

```nim:util/make_resource.nim
import os
import base64
import zip/zipfiles
import strutils

proc makeZipToSrc(dirName, srcFileName : string) : bool = 
  result = true
  # テンポラリフォルダにZIPファイルを作成し、その中にファイルを追加する
  var zipFile : ZipArchive
  let zipFilePath = getTempDir() / "sample.zip"
  if zipFilePath.existsFile() == true :
    zipFilePath.removeFile

  if zipFile.open( zipFilePath, FileMode.fmWrite ) == false:
    return

  # ディレクトリを再帰探索してZIPに追加する
  for f in walkDirRec(dirName, yieldFilter={pcFile}):
    let tokens = f.split($DirSep)
    let newFile = tokens[1..^1].join("/")
    echo "newFile=>" & newFile
    zipFile.addFile(newFile, f)
  zipFile.close
  
  # ZipFileを開いて、Base64を作成してソースを自動生成する
  block:
    let f = open(zipFilePath, FileMode.fmRead)
    let b = f.readAll()
    f.close
    # 文字列を公開形式にしてソースを生成（後述）
    let w = open(srcFileName, FileMode.fmWrite)
    w.write("const resource_string* = \"\"\"" & encode(b) & "\"\"\"")
    w.close

let src = "src/res/resource_file.nim" 

var 
  dir = "html"

if paramCount() == 1: 
  dir = $os.commandLineParams()[0]

discard makeZipToSrc(dir,src)
```

#### ZIPが埋め込まれたソース(src/res/resource_file.nim)

こんなカンジで**めっちゃ埋め込まれ**ます。今思えば、１６進データの配列でもよかったかと。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/58872/fb347ffc-4114-58e8-550e-32991bffe65e.png)

### 埋め込みリソースを展開する

アプリケーション起動時時(いわゆるMain処理の前)に**リソースを展開**します。
BASE64文字列(recource_file.resource_string)をZIPに戻して、任意のフォルダに展開します。

```nim:src/res/resources.nim
import os
import base64
import resource_file
import zip/zipfiles

proc expandResource* (dir: string) : bool = 
  # base64をzipファイルとして保存する
  let s = decode(resource_string)
  let zipFileName = getTempDir() / "resource.zip"
  let f = open(zipFileName, FileMode.fmWrite)
  f.write(s)
  f.close()

  # ZIPを任意のフォルダに解凍する
  var z: ZipArchive
  if not z.open(zipFileName):
    echo "open zip fail"
  z.extractAll(dir)
  z.close()

  # resource.zipは削除しても良いかも　　

```

メイン実行時に、(res.resources.)expandResourceを呼び出して、リソースを展開します。

```nim:main.nim
import os
import docopt
import httppkg/main
import res/resources

when isMainModule:
  # リソースを展開（アプリケーションのあるフォルダに展開する）
  if expandResource(getAppDir()) == true :
    let args = docopt(doc, version = "http 0.1.0")
    # echo "args=>", args
    let retCode = main(args)
    quit(retCode)
  else:
    echo "expand resources failed"
```

### テストしてみる

Windows/Linux/Macで動作確認

```shell-session
> git clone -b for-embed-file https://github.com/6in/simple-http.git resource_test
> cd resource_test
> mkdir bin
> nimble make_resource
> nimble build
> cd bin
> dir

12/16/2018  08:35 PM    <DIR>          .
12/16/2018  08:35 PM    <DIR>          ..
12/16/2018  08:35 PM         1,287,457 http.exe
12/16/2018  08:34 PM           363,535 make_resource.exe

> http --port=8888 --root=.
[file]=index.html
[file]=images/nim.png
root=>.
INFO Jester is making jokes at http://127.0.0.1:8888 (all interfaces)

# Linux/Macは、こちら
$ http --port=8888 --root=$PWD
```

ブラウザで表示してみると出ました。やったね。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/58872/cb76e2b2-c6fb-54b9-0a80-aa0b56c9f018.png)

Ctrl + C で停止してDir/Treeで確認すると、リソースが展開されています。

```
> dir
12/16/2018  08:36 PM    <DIR>          .
12/16/2018  08:36 PM    <DIR>          ..
12/16/2018  08:35 PM         1,287,457 http.exe
12/16/2018  08:36 PM    <DIR>          images
12/16/2018  08:36 PM                70 index.html
12/16/2018  08:34 PM           363,535 make_resource.exe

> tree /F /a
C:.
|   http.exe
|   index.html           <= 展開されたリソース
|   make_resource.exe
|
\---images               <= 展開されたリソースフォルダ
        nim.png          <= 展開されたリソース

```

## まとめ

* この手のやり方はいろいろとあるので、一つの例としてみてもらえればと思います。
  * nimxあたりのソースでリソース(イメージだったかな)を埋め込むソースをちらと見かけたことがあります。
  * GUI系のF/Wならなんかしら手段ありそうですけど未調査です。
* Webサーバー＋サーバーロジックとそのコンテンツを１つの実行モジュールにパッケージングできるとなると、Dockerコンテナへのデプロイとか楽になるとかですかね・・・
* 途中にも書いていますが、Base64ではなく16進配列でも良かったかなと。

```
let resource_data : seq[int] = @[ 0x01, 0x02 , ... ]     <= こんな感じ
```

* 作ってはみたものの、自分的に使う場面が思い当たらず。しいて言えば、ちょっとした管理系のツールをWebベースで作るとかでしょうかね。
  * NimにはGUI系ライブラリがいくつかあるんですけど、どれも私的にあまりピンとこず（というか目移りしやすくてどれを選んだらよいかわからない）、各OS毎の動作確認も面倒だなと思うこともあり、ローカルのWebアプリにして配布しちゃうのもアリかなと最近感じています。
* 今回の仕組みを利用して、CUI系ツールだけど設定ファイルはWebの画面で編集できるハイブリッドなツールとかを手軽に作れるといいなあと妄想したりしています。

## おまけ

* 最初は、[zipモジュール](https://github.com/nim-lang/zip)を利用していたのですが、Windowsだとビルドできない箇所もあったので、リポジトリに組み込んでしまっています。