HTMLとJavaScriptでMD5ハッシュ値の計算

先週、華氏度と摂氏度の換算をする WEB ページを作ってみて、HTML と JavaScript だけでページを作れば、サーバなしで簡単なツールを作れることが分かった。
そこで、いくつか同じように WEB ページで動くツールを作ってみている。今日は、ローカルファイルの MD5 ハッシュ値を計算するツール(ページ)を紹介しよう。

 cf. MD5ハッシュ値の計算

ページを作るにあたっては、次のページとそこからリンクされているページを参考にした。

 cf. ローカルバイナリファイルからMD5ハッシュ値を計算する

特にローカルファイルの扱い方が参考になった。また、MD5 ハッシュ値の計算は、次のページで公開されている JavaScript のライブラリをそのまま使わせてもらった。

 cf. 高度な JavaScript 技集

最後に HTML のソースを載せておく。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>MD5 calculator</title>
    <script type="text/javascript" src="js/md5.js"></script>
    <script type="text/javascript">
      const handleFileSelect = (evt) => {
        const files = evt.target.files;
        const output = document.getElementById("list");
        output.innerHTML = "<ul>";
        for (let i = 0, f; f = files[i]; i++) {
          const reader = new FileReader();
          reader.onload = ((theFile) => {
            return (e) => {
              const hexdigest = MD5_hexhash(e.target.result);
              output.innerHTML += "<li>" + hexdigest + "  " + theFile.name + "</li>";
            }
          })(f);
          reader.readAsBinaryString(f);
        }
        output.innerHTML += "</ul>"
      }
      document.addEventListener("DOMContentLoaded", () => {
        document.getElementById("files").addEventListener("change", handleFileSelect, false);
      }, false);
    </script>
  </head>
  <body>
    <h1>MD5 calculator</h1>
    <input type="file" id="files" name="files[]" multiple /><br />
    <output id="list"></output>
  </body>
</html>

華氏度と摂氏度の換算

「Fahrenheit 451」のTシャツを買ったんだ。ブラッドベリの「華氏451度」。白。カッコイイ。

で、華氏451度って、摂氏でいうと何度なの?という当然の疑問が。まあ、ググればすぐにわかるんだけど、せっかくなのでそれで終わりにするんじゃなくて、華氏と摂氏の換算をするページを HTML と JavaScript で書いてみた。

 cf. 華氏度↔摂氏度の換算

ソースも載せておく。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Fahrenheit / Celsius converter</title>
    <script type="text/javascript">
      const convert = () => {
        const deg = document.getElementById("degree").value;
        const dest = document.getElementById("answer");
        const elements = document.getElementsByName("temperature");
        let a;
        for (let i = 0; i < elements.length; i++) {
          if (elements[i].checked) {
            a = elements[i].value;
            break;
          }
        }
        if (a === "fahrenheit") {
          dest.innerHTML = f2c(Number(deg));
        } else {
          dest.innerHTML = c2f(Number(deg));
        }
        
      }

      const f2c = (deg) => {
        return (deg - 32) * 5 / 9;
      }

      const c2f = (deg) => {
        return deg * 9 / 5 + 32;
      }

      const clearVals = () => {
        document.getElementById("degree").value = "";
        document.getElementById("answer").innerHTML = "";
      }

      document.addEventListener("DOMContentLoaded", () => {
        document.getElementById("convertButton").addEventListener("click", convert, false);
        document.getElementById("clearButton").addEventListener("click", clearVals, false);
      }, false);
    </script>
  </head>
  <body>
    <h1>Fahrenheit / Celsius converter</h1>
    <input type="radio" name="temperature" value="fahrenheit" checked />
      Fahrenheit into Celsius<br />
    <input type="radio" name="temperature" value="celsius" />
      Celsius into Fahrenheit<br />
    <input type="text" id="degree" />
    <button type="button" id="convertButton">convert</button><br />
    <output id="answer"></output><br />
    <button type="button" id="clearButton">clear</button>
  </body>
</html>

で、結局、華氏451度が摂氏何度なのかというと、四捨五入して約233度。

Goで画像アップローダーを作ってみた

画像アップローダーっていうと、あれだ、ユーザーがアップロードした画像を保存しておいて掲示板かなんかから参照できるようになってるやつだ。あれ、なんでアップローダーっていうんだろうね。
ま、とにかく Go で WEB アプリケーションを書く練習に作ってみた。Go ってワンバイナリでサーバーが作れるからいいよね。
名前は Sulaimān (スライマーン)にした。

cf. github.com/takatoh/sulaiman

シングルページアプリケーション

複雑なシステムではないので、html を読み込むのはルートにアクセスしたときだけで、あとはすべて ajax で非同期に更新するシングルページアプリケーションにした。
おかげでサーバーサイドの Go だけじゃなくて、クライアントサイドの JavaScript も結構書いた。

サーバーサイド

サーバーサイドの WEB フレームワークには Echo というのを使った。ググってみた範囲では、結構簡単そうだったし、速度も速いらしい。それからデータベースの OR マッパーには gorm というのを採用。Ruby や Python の OR マッパーとちょっと勝手が異なるけど、データベースといってもテーブルひとつの簡単なものだし、Go の中では割とメジャーなものみたい。

cf. Echo
cf. gorm

ほかにも JSON を扱うライブラリとか、画像のサムネイルを作るのにはこないだちょっと書いた resize を使った。
コードのすべては上でリンクした GitHub を見てもらいたいけど、メインのファイルだけ載せておこう。

package main
import (
    "encoding/json"
    "io/ioutil"
    "strconv"

    "github.com/labstack/echo"
    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/sqlite"
    "github.com/takatoh/sulaiman/handler"
    "github.com/takatoh/sulaiman/data"
)

func main() {
    var config = new(data.Config)
    jsonString, err := ioutil.ReadFile("config.json")
    if err != nil {
        panic(err)
    }
    json.Unmarshal(jsonString, config)

    db, err := gorm.Open("sqlite3", "sulaiman.sqlite")
    if err != nil {
        panic(err)
    }
    defer db.Close()

    db.AutoMigrate(&data.Photo{})

    e := echo.New()
    h := handler.New(db, config)

    e.GET("/", h.Index)
    e.GET("/title", h.Title)

    e.Static("/css", "static/css")
    e.Static("/js", "static/js")
    e.Static("/img", config.PhotoDir + "/img")
    e.Static("/thumb", config.PhotoDir + "/thumb")

    e.GET("/list/:page", h.List)
    e.POST("/upload", h.Upload)

    port := ":" + strconv.Itoa(config.Port)
    e.Logger.Fatal(e.Start(port))
}

設定ファイルやデータベースの読み込みをしたあと、Echo とハンドラーのインスタンスを作って、ルーティングの設定をしている。GET とか POST とかのメソッドでルーティングの設定をするのは Ruby の Sinatra なんかと雰囲気が似ている。

クライアントサイド

上に書いた通り、シングルページアプリケーションなので、いったんページを読み込んだ後はすべて ajax で非同期に処理をする。ページ遷移はない。
で、JavaScript のライブラリには定番の jQuery と、ページ描画のフレームワークには Vue.js というを使ってみた。jQuery は以前使ったことがあるけど Vue.js は初めてだ。

cf. Vue.js

でも、Vue.js は学習コストが低いのも売りらしく、実際特に苦労することもなかった。もっとも大したことはしてないんだけども。

ちょっとひっかかった点

それでもうまくいかなかったところはあって、ひとつにはページのタイトルがあげられる。ページタイトルは設定ファイルから読み込んで表示するようにした。まあ、当たり前の発想だよね。で、どうやって表示しようかというときに、サーバー側でテンプレートを使うことを最初に考えた。Go には html/template というライブラリが標準であって、それを使おうとしたんだけど、テンプレートの構文が Vue.js のものと同じ(データを埋め込みたいところに {{ foo }} みたいに書く)で、共存ができなかった。仕方がないので、タイトルも ajax でとってきてクライアントサイドで更新するようにした。
もうひとつもクライアントサイド。次のページを読み込む Next リンクをクリックしたときの動作を、ajax で次のようにやろうとしたところ、うまくいかない。

$("#next_link").on("click", function(event) {
    ...
});

これ、結構悩んでいろいろ調べて、結局こうなった。

$("body").on("click", "#next_link", function(event) {
    ...
});

なんでこうじゃなきゃ動かないのかわからない。あとで調べよう。
ともかくもこれで一通りはできた。

やり残したこと

たいていのアップローダーにはついている、ファイルの削除機能がない。一応、削除のためのキーをアップロードの際に入力するようにはしてあるので、今後実装するかもしれない。でもどんな UI にしようか悩んでいる。
それと、ページの下までスクロールしたら自動で次のページを読み込む、いわゆる無限スクロールも実装してみたい。
いつになるかわからないけど。

文字列間のレーベンシュタイン距離を求める

レーベンシュタイン距離というのを知った。

Wikipedia のレーベンシュタイン距離の項によると:

レーベンシュタイン距離(レーベンシュタインきょり)は、二つの文字列がどの程度異なっているかを示す距離(距離空間を参照)である編集距離(へんしゅうきょり)の一種類である。具体的には、文字の挿入や削除、置換によって、一つの文字列を別の文字列に変形するのに必要な手順の最小回数として与えられる。名称は、1965年にこれを考案したロシアの学者ウラジミール・レーベンシュタインにちなむ。

だそう。上記ページに擬似コードによる実装例が載ってるのと、次のページの Python での実装例が参考になった。なので JavaScript でやってみた。

 cf. 編集距離 (Levenshtein Distance) – naoyaのはてなダイアリー

#!/usr/bin/env node

function levenshtein_distance(a, b) {
  var m = [];
  var i, j, x;

  for (i = 0; i <= a.length; i++) {
    m[i] = [];
  }
  for (i = 0; i <= a.length; i++) {
    m[i][0] = i;
  }
  for (j = 0; j <= b.length; j++) {
    m[0][j] = j;
  }
  for (i = 1; i <= a.length; i++) {
    for (j = 1; j <= b.length; j++) {
      if (a[i-1] == b[j-1]) {
        x = 0;
      } else {
        x = 1;
      }
      m[i][j] = Math.min(m[i-1][j] + 1, m[i][j-1] + 1, m[i-1][j-1] + x);
    }
  }
  for (i = 0; i <= a.length; i++) {
    console.log(m[i]);
  }

  return m[a.length][b.length];
}

var s1 = process.argv[2];
var s2 = process.argv[3];
console.log(levenshtein_distance(s1, s2));

実行例:

takatoh@nightschool $ ./ld.js apple play
4
takatoh@nightschool $ ./ld.js perl pearl
1

文字列を指定した文字数ごとに分割する

今日は Windows マシンから更新。

 cf. ruby 文字列を指定数で分割する – それマグで!

Haskell でやってみた。

module Main where

slices :: Int -> [a] -> [[a]]
slices _ [] = []
slices n xs = h : slices n d
  where
    (h, d) = splitAt n xs

main :: IO ()
main = print $ slices 3 "abcdefghijklmnopqrstuvwxyz"
^o^ > runhaskell stringSlices.hs
["abc","def","ghi","jkl","mno","pqr","stu","vwx","yz"]

Scheme だとこう。

(define slices
  (lambda (lis n)
    (if (< (length lis) n)
      (list lis)
      (cons (take lis n) (slices (drop lis n) n)))))

(define string-slices
  (lambda (str n)
    (map list->string (slices (string->list str) n))))

(print (string-slices "abcdefghijklmnopqrstuvwxyz" 3))
^o^ > gosh string-slices.scm
(abc def ghi jkl mno pqr stu vwx yz)

最後に JavaScript。

function stringSlices(n, str) {
  var l = str.length;
  var result = [];
  for (var i = 0; i < l; i += n) {
    result.push(str.slice(i, i + n));
  }
  return result;
}

console.log(stringSlices(3, "abcdefghijklmnopqrstuvwxyz"));
^o^ > node stringSlices.js
[ 'abc', 'def', 'ghi', 'jkl', 'mno', 'pqr', 'stu', 'vwx', 'yz' ]

これ、もうちょっとスマートにいかないかな。

[追記]

JavaScript 版、Haskell 版や Scheme 版と同じように考えてみた。少しはスマートになったかな。そうでもないか?

function stringSlices(n, str) {
  if (str.length == 0) {
    return [];
  } else {
    var l = stringSlices(n, str.slice(n))
    l.unshift(str.slice(0, n))
    return l
  }
}

console.log(stringSlices(3, "abcdefghijklmnopqrstuvwxyz"));
^o^ > node stringSlices2.js
[ 'abc', 'def', 'ghi', 'jkl', 'mno', 'pqr', 'stu', 'vwx', 'yz' ]

100から1まで表示するコードすら書けないプログラマ

cf. 100から1まで表示するコードすら書けないプログラマ – 新・日々録 by TRASH BOX@Eel

詳しくはリンク先を読んでもらうとして、100から1まで表示するだけのプログラムを10分たっても書けないプログラマが14%もいたという調査結果があるんだそうだ。話の雰囲気からして職業プログラマの話だろうに、FizzBuzz の話もそうだけど、こんな簡単なプログラムも書けなくて普段どうやって仕事してるんだろう?

それはともかくとして、リンク先にはいくつかの言語で書いたコードがあるけど、JavaScript がないので書いてみた。そしたら、一発じゃできなかったよ…orz。というのが今日のオチ。
以下、恥を晒しておく。

最初に書いたのがこれ。

for var i = 100, i < 0, i-- {
  console.log(i);
}

実行してみると:

takatoh@nightschool $ node countdown.js

/home/takatoh/w/nodejs/countdown.js:1
(function (exports, require, module, __filename, __dirname) { for var i = 100,
                                                                  ^^^
SyntaxError: Unexpected token var
    at Module._compile (module.js:439:25)
    at Object.Module._extensions..js (module.js:474:10)
    at Module.load (module.js:356:32)
    at Function.Module._load (module.js:312:12)
    at Function.Module.runMain (module.js:497:10)
    at startup (node.js:119:16)
    at node.js:902:3

ああ、for のあとには括弧がいるんだっけ。

for (var i = 100, i < 0, i--) {
  console.log(i);
}

今度はいいだろう。

takatoh@nightschool $ node countdown2.js

/home/takatoh/w/nodejs/countdown2.js:1
orts, require, module, __filename, __dirname) { for (var i = 100, i > 0, i-- )
                                                                    ^
SyntaxError: Unexpected token >
    at Module._compile (module.js:439:25)
    at Object.Module._extensions..js (module.js:474:10)
    at Module.load (module.js:356:32)
    at Function.Module._load (module.js:312:12)
    at Function.Module.runMain (module.js:497:10)
    at startup (node.js:119:16)
    at node.js:902:3

おおい、どういうこった。
眺めてもわからないので Google 先生に聞くことに。そしたら、for の初期値とか終了条件とかはカンマじゃなくてセミコロンで区切るんだった。ああ、まだ身についてないな。というか、普段書いてる Ruby やら Python じゃこういう for ループ書かないからなあ(言い訳)。
というわけで、最終的にはこれで正解。

for (var i = 100; i < 0; i--) {
  console.log(i);
}

Node.js:コマンドラインオプションを処理する

js-opts というモジュールが使える。
npm でインストール。

^o^ > npm install opts
[email protected] node_modules\opts

js-opts というモジュール名だけど、インストールするのは opts。

昨日のファイル一覧を表示するスクリプトを拡張してみた。

var fs = require('fs');
var opts = require('opts');

var ext = false;

var options = [
  { short: "v"
  , long: "version"
  , description: "Show version and exit"
  , callback: function() { console.log("v0.1.0"); process.exit(1); }
  },
  { short: "e"
  , long: "ext"
  , description: "Specify ext"
  , value: true
  , callback: function(value) { host = value; }
  }
]

opts.parse(options, true);

var ext = opts.get("ext");
var dir = opts.args()[0];

fs.readdir(dir, function(err, files) {
  if (err) throw err;
  files = files.filter(function(file) {
    return fs.statSync(file).isFile();
  });
  if (ext) {
    var re = new RegExp(".+\." + ext + "$");
    files = files.filter(function(file) {
      return re.test(file);
    });
  }
  files.forEach(function(file) {
    console.log(file);
  });
});

オプションをオブジェクトの配列として定義しておいて、opts.parse の引数に渡してやると、オプション解析をしてくれる。true はなんだろう?

オプションの引数は opts.get(オプション名) で取得できる。それから、解析の結果残った引数は opts.args() で配列として取得できる。

実行結果:

^o^ > node listfiles2.js --version
v0.1.0

^o^ > node listfiles2.js --help
Usage: node C:\Users\takatoh\Documents\w\nodejs\listfiles2.js [options]
Show this help message
    --help
Specify ext
    -e, --ext 
Show version and exit
    -v, --version


^o^ > node listfiles2.js .
config.json
filesize.js
get_file.js
get_file2.js
hello.js
hello.txt
httphead.js
listfiles.js
listfiles2.js
read_config.js
server.js
test_argv.js
test_url.js

^o^ > node listfiles2.js --ext js .
filesize.js
get_file.js
get_file2.js
hello.js
httphead.js
listfiles.js
listfiles2.js
read_config.js
server.js
test_argv.js
test_url.js

–help オプションは定義しなくても、自動で作ってくれるみたい。

js-opts のサイト:

 cf. https://bitbucket.org/mazzarelli/js-opts/wiki/Home

[追記]

js-opts のソースを確認したところ、opts.parse の第2引数に true を指定すると、–help オプションを追加してくれるみたい。

Node.js:ファイルの一覧を取得する

指定したディレクトリ中のファイル一覧を取得する。
fs モジュールの readdir を使う。

var fs = require('fs');

var dir = process.argv[2];
fs.readdir(dir, function(err, files) {
  if (err) throw err;
  files.forEach(function(file) {
    console.log(file);
  });
});
^o^ > node listfiles.js .
config.json
filesize.js
get_file.js
get_file2.js
hello.js
hello.txt
httphead.js
listfiles.js
read_config.js
server.js
test_argv.js
test_url.js

Node.js:async.forEachSeries

昨日のプログラム、うまくいったので書く。

結果から書くと、アクションの中で callback を呼び出させばいいようだ。プログラムはこんなふうになった。

var http = require('http');
var fs = require('fs');
var path = require('path');
var mkdirp = require('mkdirp');
var async = require('async');

var host = "http://localhost:8888/";

var action = {
  new: getNewItem,
  delete: deleteItem
}

function replicate(host) {
  var req = http.get(host + "recent.json", function(res) {
    var body = "";
    res.setEncoding('utf8');
    res.on('data', function(chunk) {
      body += chunk;
    });
    res.on('end', function(res) {
      var items = JSON.parse(body);
      async.forEachSeries(items, function(item, callback) {
        action[item.action](item, callback);
      });
    });
  }).on('error', function(err) {
    colsole.log("Error: " + err.message);
  });
}

function getNewItem(item, callback) {
  console.log("New item: " + item.path);
  var filepath = "./storage/" + item.path;
  var dir = path.dirname(filepath);
  mkdirp(dir, function(e) {
    if (e) {
      console.log(e.message);
    } else {
      var itemUrl = host + item.path;
      var ws = fs.createWriteStream(filepath);
      var req = http.get(itemUrl, function(res) {
        res.pipe(ws);
        res.on('end', function() {
          ws.close();
          console.log("New: " + item.path + "...done.");
          callback(null);
        });
      });
    }
  });
}

function deleteItem(item, callback) {
  console.log("Delete item: " + item.path);
  var filepath = "./storage/" + item.path;
  fs.unlink(filepath, function(e) {});
  console.log("Delete: " + item.path + "...done.");
  callback(null);
}

replicate(host);

アクションの最後、特に getNewItem ではネストの一番深いところで callback を呼び出している。これが次のアクションを呼び出すことになっているわけだ。
実行結果。

^o^ > node replicate.js
New item: Penguins.jpg
New: Penguins.jpg...done.
New item: data/Desert.jpg
New: data/Desert.jpg...done.
Delete item: Penguins.jpg
Delete: Penguins.jpg...done.

ちゃんと指示通りの順番で実行されている。

^o^ > tree /F storage
フォルダー パスの一覧:  ボリューム OS
ボリューム シリアル番号は FE2A-F7C6 です
C:\USERS\TAKATOH\DOCUMENTS\W\REPLICATE\STORAGE
└─data
        Desert.jpg

Penguins.jpg は削除されて残っていないのがわかる。

Node.js:async.forEachSeriesって配列の順番どおりに処理してくれるんじゃないの?

本当はうまくいってから書くつもりだったけど、どうにもうまくいかないので書くだけ書いておく。

簡単な説明

サーバからの指示でファイルをダウンロードしたり削除したりするプログラムを Node.js で書いている。けど、指示通りの順番で処理してくれないので困ってしまった。async.forEachSeries を試してみたけどダメだった(←今ここ)。

より細かい説明

サーバは http://localhost:8888/ で動いていて、/recent.json にアクセスすると指示を受け取ることができる。指示は JSON 形式で、次のようなもの。

[
  { "action": "new", "path": "Penguins.jpg" },
  { "action": "new", "path": "data/Desert.jpg"},
  { "action": "delete", "path": "Penguins.jpg"}
]

action がするべき処理を示していて、new は新しくファイルをダウンロード、delete はすでにダウンロードしたファイルを削除する。上の例では、

  1. Penguins.jpg をダウンロードして保存
  2. data/Desert.jpg をダウンロードして保存
  3. Penguins.jpg を削除

となって、結果として data/Desert.jpg だけが保存されるはず。

書いてみたプログラム replicate.js を載せるよ。

var http = require('http');
var fs = require('fs');
var path = require('path');
var mkdirp = require('mkdirp');

var host = "http://localhost:8888/";

var action = {
  new: getNewItem,
  delete: deleteItem
}

function replicate(host) {
  var req = http.get(host + "recent.json", function(res) {
    var body = "";
    res.setEncoding('utf8');
    res.on('data', function(chunk) {
      body += chunk;
    });
    res.on('end', function(res) {
      var items = JSON.parse(body);
      items.forEach(function(item) {
        action[item.action](item);
      });
    });
  }).on('error', function(err) {
    colsole.log("Error: " + err.message);
  });
}

function getNewItem(item) {
  console.log("New item: " + item.path);
  var filepath = "./storage/" + item.path;
  var dir = path.dirname(filepath);
  mkdirp(dir, function(e) {
    if (e) {
      console.log(e.message);
    } else {
      var itemUrl = host + item.path;
      var ws = fs.createWriteStream(filepath);
      var req = http.get(itemUrl, function(res) {
        res.pipe(ws);
        res.on('end', function() {
          ws.close();
        });
      });
      console.log("New: " + item.path + "...done.");
    }
  });
}

function deleteItem(item) {
  console.log("Delete item: " + item.path);
  var filepath = "./storage/" + item.path;
  fs.unlink(filepath, function(e) {});
  console.log("Delete: " + item.path + "...done.");
}

replicate(host);

たいして難しいことはしてない。変数 action にアクションを登録(9~12行目)しておいた上で、サーバから JSON で指示を受け取り(15行目)、パースして配列に直し(22行目)、配列の順番どおりに処理している(23~25行目)。ついでに各処理のはじめと終わりにコンソールに出力している。

さて、これを実行してみると、期待通りには動いてくれない。

^o^ > node replicate.js
New item: Penguins.jpg
New item: data/Desert.jpg
Delete item: Penguins.jpg
Delete: Penguins.jpg...done.
New: Penguins.jpg...done.
New: data/Desert.jpg...done.

処理の開始は配列の順番どおり(最初の3行)だけど、Delete が先に終わってしまって、New があとになっている。結果として、削除するはずの Penguins.jpg が残っている。

^o^ > tree /F storage
フォルダー パスの一覧:  ボリューム OS
ボリューム シリアル番号は FE2A-F7C6 です
C:\USERS\TAKATOH\DOCUMENTS\W\REPLICATE\STORAGE
│  Penguins.jpg
│
└─data
        Desert.jpg

原因はアクションの処理が非同期なせいだ。Array.forEach 自体は非同期じゃないらしいけど(だから指示の順番どおりにアクションが始まっている)、アクション自体が非同期なので、ひとつのアクションが終わらないうちに次のアクションが始まり、終わりの順番が入れ替わってしまっているんだ。
これは困った。ちゃんと指示通りに順番に処理してくれないと、上のように Penguins.jpg のダウンロードと削除が指示通りにならない。さて、どうしよう。

async.forEachSeriesを試す

いろいろググってみた結果、async というライブラリでフロー制御ができるらしい、というのがわかった。というわけで、async.forEachSeries を試してみた。
書き換えたのがこのコード。

var http = require('http');
var fs = require('fs');
var path = require('path');
var mkdirp = require('mkdirp');
var async = require('async');

var host = "http://localhost:8888/";

var action = {
  new: getNewItem,
  delete: deleteItem
}

function replicate(host) {
  var req = http.get(host + "recent.json", function(res) {
    var body = "";
    res.setEncoding('utf8');
    res.on('data', function(chunk) {
      body += chunk;
    });
    res.on('end', function(res) {
      var items = JSON.parse(body);
      async.forEachSeries(items, function(item) {
        action[item.action](item);
      });
    });
  }).on('error', function(err) {
    colsole.log("Error: " + err.message);
  });
}

function getNewItem(item) {
  console.log("New item: " + item.path);
  var filepath = "./storage/" + item.path;
  var dir = path.dirname(filepath);
  mkdirp(dir, function(e) {
    if (e) {
      console.log(e.message);
    } else {
      var itemUrl = host + item.path;
      var ws = fs.createWriteStream(filepath);
      var req = http.get(itemUrl, function(res) {
        res.pipe(ws);
        res.on('end', function() {
          ws.close();
        });
      });
      console.log("New: " + item.path + "...done.");
    }
  });
}

function deleteItem(item) {
  console.log("Delete item: " + item.path);
  var filepath = "./storage/" + item.path;
  fs.unlink(filepath, function(e) {});
  console.log("Delete: " + item.path + "...done.");
}

replicate(host);

書き換えたのは2行だけ。5行目で async を読み込み、24行目では items.forEach(function… の代わりに、async.forEachSeries(items, function… としている。これで、うまく動いてくれるだろうか。

^o^ > node replicate.js
New item: Penguins.jpg
New: Penguins.jpg...done.

ダメだーーー!
なんだかわからないけど、最初のアクションしか実行してくれない。ぜんぜん each じゃないじゃないか。どういうことだろう?