更新時刻を保持するpngquantタスク

pngquantに限った事ではないですが、grunt等で公開用のデータをビルドする場合、基本的にファイルの更新時刻はビルドした時点の物となります。ファイルを更新しているので当たり前ですね。

デプロイの方法によってはこれで全然問題はないのですが、レガシーな手法、例えばsshやftpなどでプッシュする場合に更新時刻をソースの物に合わせたい場合があります。もとい、あるとします。（差分で同期を取る際などに、時刻を基準とするケース）

そのようなケースに合わせるために、オリジナルの更新時刻を保持するpngquantタスクを書いてみます。

- `fs.stat/fs.statSync` を使って更新時刻を取得
- `pngquant`コマンドで最適化した後、`touch`コマンド実行で更新時刻を変更
- 一度に多くの処理を走らせるとエラーを吐くので、決まった数ずつ順次処理していきます

尚、touchコマンド/pngquantコマンドを使用出来る環境を前提としているので、事前にpngquantコマンドをインストールしておきます。（Windowsの場合はGit CUIなどでパスを通します）

[更新時刻を保持するpngquantタスク @ Gist](https://gist.github.com/mach3/6928970)

```js
/**
 * Grunt task for pngquant
 * -----------------------
 */

// @example:
//
// pngquant: {
//     options: {
//         preserve_mtime: true
//     },
//     dist: {
//         src: [
//             "the/path/to/*.png",
//             "the/path/to/**/*.png"
//         ]
//     }
// }

module.exports = function(grunt){

    grunt.registerMultiTask("pngquant", "", function(){

        var my = {}, fs, cp, done;

        fs = require("fs"),
        cp = require("child_process");
        done = this.async();

        /**
         * Options:
         * - preserve_mtime:Boolean ... Preserve modified time or not
         * - pngquant:String ... Template string for pngquant command
         * - touch:String ... Tempalte string for touch command
         * - verbose:Boolean ... Verbose mode
         * - separate:Number ... Separate the process by count
         */
        my.options = this.options({
            preserve_mtime: true,
            pngquant: 'pngquant <%=file %> --iebug --ext .png --force',
            touch: 'touch -d "<%=date %>" <%=file %>',
            verbose: true,
            separate: 8
        });

        /**
         * Get file list
         */
        my.count = 0;
        my.files = (function(files){
            var list = [], res = [], i = 0;
            files.forEach(function(o){
                list = list.concat(o.src);
            });
            my.count = list.length;
            while(i<=list.length){
                res.push(list.slice(i, i+my.options.separate));
                i+=my.options.separate;
            }
            return res;
        }(this.files));
        my.length = my.files.length;

        /**
         * Get modified time of file
         * - If `preserve_mtime` is false, return null
         * @param String file
         */
        my.getMTime= function(file){
            if(! this.options.preserve_mtime){ return null; }
            var date = new Date(fs.statSync(file).mtime);
            return grunt.template.date(date, "yyyy/mm/dd HH:MM:ss");
        };

        /**
         * Optimize image file by pngquant
         * @param {String} file
         * @param {Function} callback
         */
        my.optimize = function(file, callback){
            var mtime, pngquant, touch;
            mtime = my.getMTime(file);
            pngquant = grunt.template.process(my.options.pngquant, {
                data: { file: file }
            });
            touch = mtime ? grunt.template.process(my.options.touch, {
                data: {
                    file: file,
                    date: mtime
                }
            }) : "";
            cp.exec(pngquant, function(e, out, error){
                if(e){
                    return grunt.log.error(error);
                }
                mtime && cp.exec(touch);
                callback();
            });
        };

        /**
         * Run process
         */
        my.process = function(){
            var files, count;

            if(! my.files.length){
                grunt.log.writeln("All images optimized.");
                return done();
            }

            files = my.files.pop();
            count = files.length;
            files.forEach(function(file){
                my.optimize(file, function(){
                    count -= 1;
                    ! count && my.process();
                });
            });

            if(my.options.verbose){
                var percent = 100 - parseInt(my.files.length / my.length * 100, 10);
                grunt.log.writeln(
                    grunt.template.process(
                        "<%=percent %>% of <%=total %> files",
                        {data: {percent: percent, total: my.count}}
                    )
                );
            }
        };

        my.process();

    });

};
```