/*
 *	app.c
 *
 *	FC_COPY.exe - ファイル内容を比較して一致しない場合のみコピーするツール
 *	Copyright (C) 2015-2018 Naoyuki Sawa
 *
 *	* Wed Nov 11 21:51:30 JST 2015 Naoyuki Sawa
 *	- 1st リリース。
 *	- これまで使用していたFC_COPY.batと、ほぼ同じ機能を持つexeとして再実装しました。
 *	  FC_COPY.batを置き換える事にした理由は、バッチよりもexeの方が安定動作が期待出来るからです。
 *	  FC_COPY.batもこれまで問題なく使用出来ていたので、./keep/FC_COPY.bat に保存しておきました。
 *	  FC_COPY.batはバッチの可変引数のサンプルとして参考になると思うので、今後も必要に応じて参照して下さい。
 *	- FC_COPY.batとの、動作上の相違点は、以下の通りです。
 *	  ①FC_COPY.batは、コピーを実行した場合は、コピー先ファイルのタイムスタンプは、コピー元ファイルのタイムスタンプと同じになっていました。
 *	  │FC_COPY.exeでは、コピーを実行した場合は、コピー先ファイルのタイムスタンプは、現在の時刻になります。
 *	  │尚、もしFC_COPY.exeでもFC_COPY.batと同じ動作にしたい場合は、CopyFile()APIを使えば良いのですが、必要無いと思うので対応していません。
 *	  │コピーを実行した後に確実にビルドが走るようにする事を考えると、今回作成したFC_COPY.exeの動作の方が適切だと思います。
 *	  ②FC_COPY.batは、コピー元ファイル名を最大8個までしか指定出来ません。
 *	  │FC_COPY.exeでは、SETARGV.OBJが展開出来る限り、コピー元ファイル名の指定数に制限は有りません。
 *	- ちなみに、CMake(http://cmake.org/)を利用して、「cmake -E copy_if_different in-file out-file」でも同様の事が出来るのですけれど、
 *	  P/ECEを使う環境にCMakeがインストールされているとは限らないし、FC_COPY.exeのコマンド書式の方が複数指定が出来て便利だと思います。
 *	* Sun Feb 21 21:02:01 JST 2016 Naoyuki Sawa
 *	- libclipのライブラリ化の伴い、以下の変更を行いました。
 *	  ・extclip.hとextclip.cを削除しました。extclip.hとextclip.cはlibclipに含まれるようになったからです。
 *	  ・"CLiP Files"を削除しました。CLiPライブラリの各モジュールはlibclipに含まれるようになったからです。
 *	  当ツールの動作には変更有りません。
 *	* Fri Sep 16 21:30:10 JST 2016 Naoyuki Sawa
 *	- 前回の更新以降～本日までの間に、libclipが依存するインクルードパスやライブラリパスの変更によって、当プロジェクトがビルド出来なくなっていました。
 *	  当プロジェクトにもそれらを反映して、ビルド出来るようにしました。
 *	- '-q'オプションが正しく処理出来ていなかったので、修正しました。
 *	  具体的には、subr()関数の中で、「case 'q'」の処理は有るのに、getopt()に対して"q"の指定が抜けていました。
 *	  今回、getopt()に対しても"q"を追加して、'q'オプションが正しく処理出来るように修正しました。
 *	* Mon May 08 08:35:51 JST 2017 Naoyuki Sawa
 *	- ライブラリのバグ修正に伴う、ツールのリビルドを行いました。
 *	  詳細は、clipmisc.cの'Sun May 07 09:13:01 JST 2017 Naoyuki Sawa'のコメントを参照して下さい。
 *	* Thu May 03 23:59:59 JST 2018 Naoyuki Sawa
 *	- Boehm GCを修正した事に伴い、当ツールもリビルドしました。
 *	  詳細は、/clip/tool/gc/README.TXTの'Wed May 02 23:59:59 JST 2018 Naoyuki Sawa'のコメントを参照して下さい。
 *	* Thu May 17 23:59:59 JST 2018 Naoyuki Sawa
 *	- '-o','-i'オプションを追加しました。
 *	  追加した理由は、以下の通りです。
 *	- '-o'オプションを追加した理由は、変更されたファイルだけを抽出したい事が有ったからです。
 *	  これまでは、変更されたファイルを比較先のフォルダに上書きしていたので、抽出するのが難しい問題が有りました。
 *	  '-o'オプションを追加した事により、変更されたファイルを比較先とは別のフォルダに抽出出来るようになりました。
 *	  尚、'-o'オプションを指定しなければ、比較先のフォルダをコピー先フォルダとするので、これまでと同じ動作となります。
 *	  ちなみに、内容で比較して抽出するようなツールをWeb上で探してみたのですが、良いツールが見つかりませんでした。
 *	  どのツールも、内容でなく、タイムスタンプで比較して抽出するようだからです。
 *	  欲しい機能を持ったツールが見付からなかったので、今回、自作のFC_COPY.exeに欲しい機能を追加する事にしました。
 *	- '-d'オプションを追加した理由は、以下の通りです。
 *	  setargv.objで引数のワイルドカードを展開しているのですが、setargv.objは「*.*」や「*.?*」等が、拡張子の無いフォルダ名にも一致してしまうようです。
 *	  これまでは、比較元ファイルが実際にはフォルダだった場合にエラー停止していたので、サブフォルダを含むフォルダ内の複数のファイルを指定する目的でワイルドカードが使いづらい問題が有りました。
 *	  本当はsetargv.objを修正したい所なのですが、それは難しいですし、setargv.objを使用している他のプログラムにも影響が生じるので危険です。
 *	  そこで、対処療法的ではありますが、当ツールの中だけで、比較元ファイルが実際にはフォルダだった場合にエラーとせずに無視するオプション(-i)を設ける事にしました。
 *	  これまでの動作との互換性のために、デフォルトではエラーとなる動作のままとしましたが、実際には常に無条件に-iを指定しても問題無いと思います。
 *	- 今回は変更量が多かったので、変更前のコードはコメントアウトして残していません。
 *	  変更前のコードを参照する必要が有る場合は、前回のアーカイブを参照して下さい。
 */
#include "app.h"
#define VERSION		"20180517"	//最終更新日
/****************************************************************************
 *	
 ****************************************************************************/
__declspec(noreturn) static void usage();
static int subr(int argc, char* argv[]);
/****************************************************************************
 *	main
 ****************************************************************************/
int main(int argc, char* argv[]) {
	int exitCode;
#ifdef  GC_H
//	putenv("GC_LOG_FILE=CON");//ログ出力先を指定する。
//	GC_set_find_leak(1);//メモリリーク検出を開始する。
#endif//GC_H
//{{2017/05/08削除:GC_init(),及び,GC_allow_register_threads()の呼び出しを削除しました。GC_init(),及び,GC_allow_register_threads()は、だいぶん前から、必要に応じてライブラリ内のextclip.cのGC_threads_init()で行われるようになっているので、アプリケーション側での明示的な呼び出しはもう不要です。
//#if     (defined(GC_H) && defined(PTHREAD_H))
//	GC_init();//この中でGCスレッドが起動する。メインスレッドをpthread_exit()等で終了するとGCスレッドが残ってプロセスが終了しないので注意せよ。
//	GC_allow_register_threads();
//#endif//(defined(GC_H) && defined(PTHREAD_H))
//}}2017/05/08削除:GC_init(),及び,GC_allow_register_threads()の呼び出しを削除しました。GC_init(),及び,GC_allow_register_threads()は、だいぶん前から、必要に応じてライブラリ内のextclip.cのGC_threads_init()で行われるようになっているので、アプリケーション側での明示的な呼び出しはもう不要です。
#ifdef  __CLIP_SEH_H__
	SEH_init();
#endif//__CLIP_SEH_H__
#ifdef  __CLIP_REG_H__
//	if(!(TBL_RegTbl = LockResource(LoadResource(NULL, FindResource(NULL, "RegTbl", "RegTbl"))))) { DIE(); }
#endif//__CLIP_REG_H__
#ifdef  __CLIP_PRP_H__
//	if(!(pPrpKey = REG_open_key(TBL_RegTbl, RegKey_PrpTbl))) { DIE(); }
//	PrpHdr_init(phApp, pPrpKey, PrpTbl_App);
#endif//__CLIP_PRP_H__
	exitCode = subr(argc, argv);
#ifdef  GC_H
//	CHECK_LEAKS();//メモリリークを検出する。
#endif//GC_H
//	cputs("ok");
//	getch();
	return exitCode;
}
/****************************************************************************
 *	usage
 ****************************************************************************/
__declspec(noreturn) static void usage() {
	fprintf(stderr, "FC_COPY.exe - ファイル内容を比較して一致しない場合のみコピーするツール (%s)\n", VERSION);
	fprintf(stderr, "Copyright (C) 2015-2018 Naoyuki Sawa\n");
	fprintf(stderr, "\n");
	fprintf(stderr, "USAGE:\n");
	fprintf(stderr, "  FC_COPY.exe [option ...] srcfile1 [srcfile2 ...] dstdir\n");
	fprintf(stderr, "  ・ファイル名は、ワイルドカードによる複数指定も可能です。\n");
	fprintf(stderr, "\n");
	fprintf(stderr, "OPTIONS:\n");
	fprintf(stderr, "  -oコピー先フォルダ  コピー先フォルダを指定します。\n");
	fprintf(stderr, "                      このオプションを指定しなければ、比較先フォルダ内のファイルに上書きコピーします。\n");
	fprintf(stderr, "  -i                  指定された比較元ファイルが実際にはフォルダだった場合、エラーとせずに無視します。\n");
	fprintf(stderr, "                      このオプションを指定しなければ、エラーとして停止します。\n");
	fprintf(stderr, "  -q                  進捗表示を抑制します。\n");
	fprintf(stderr, "  -h                  使い方を表示します。\n");
	fprintf(stderr, "\n");
	fprintf(stderr, "EXAMPLE:\n");
	fprintf(stderr, "  ①FC_COPY.exe foo\\*.txt bar\\\n");
	fprintf(stderr, "    fooフォルダ内の全ての*.txtファイルと、barフォルダ内の同名のファイルの内容を比較します。\n");
	fprintf(stderr, "    barフォルダ内に同じ名前で同じ内容のファイルが存在すれば、何もしません。\n");
	fprintf(stderr, "    barフォルダ内に同じ名前で違う内容のファイルが存在するか、同じ名前のファイルが存在しなければ、barフォルダへコピーします。\n");
	fprintf(stderr, "  ②FC_COPY.exe -iobaz\\ foo\\*.* bar\\\n");
	fprintf(stderr, "    fooフォルダ内の全てのファイルと、barフォルダ内の同名のファイルの内容を比較します。\n");
	fprintf(stderr, "    barフォルダ内に同じ名前で同じ内容のファイルが存在すれば、何もしません。\n");
	fprintf(stderr, "    barフォルダ内に同じ名前で違う内容のファイルが存在するか、同じ名前のファイルが存在しなければ、bazフォルダへコピーします。\n");
	fprintf(stderr, "    fooフォルダ内にサブフォルダが有った場合、foo\\*.*のワイルドカードに一致しますが、エラーとせずに無視します。\n");
	exit(EXIT_FAILURE);
}
/****************************************************************************
 *	subr
 ****************************************************************************/
#define BUFFER_SIZE	(1024*1024)	//バッファサイズ(調整可)
static unsigned char srcBuf[BUFFER_SIZE], dstBuf[BUFFER_SIZE];
/*--------------------------------------------------------------------------*/
static int subr(int argc, char* argv[]) {
	char* outDir = NULL;
	int opt_i = 0;
	int opt_q = 0;
	{
		int opt;
		opterr = 0; //オプションエラーは当プログラム内で処理するので、getopt()内でのエラー表示は不要。
		while((opt = getopt(argc, argv, "o:iqh")) != -1) {	//コマンドラインオプションを処理する。
			switch(opt) {
			case 'o':
				outDir = optarg;
				break;
			case 'i':
				opt_i = 1;
				break;
			case 'q':
				opt_q = 1;
				break;
			case 'h':
			default:
				usage();
			}
		}
	}
	{
		char srcFname[MAX_FNAME], srcExt[MAX_EXT];
		char *srcPath, *dstDir;
		struct stat st;
		FILE* fpSrc;
		//少なくとも1個以上の各比較元ファイルと、1個の比較先ディレクトリが必要である。
		if(optind >= (argc - 1)) { usage(); }
		//比較先ディレクトリが存在する事を確認する。
		dstDir = argv[argc - 1];
//{{サービス
		//ディレクトリ名の末尾に'\\'が付いていると、stat()がエラーを返すようだ。(VC++6.0にて確認)
		//それだけの原因で「比較先ディレクトリ'～\'が存在しません。」というエラーを表示すると、ユーザーがなぜエラーなのか判らずに混乱すると思うので、
		//サービスとして、当ツール内の処理で、ディレクトリ名の末尾の'\'を確実に削除する事にした。
		dstDir = strtrim(dstDir, 2/*右端にある指定文字を除去する*/, "\\");
//}}サービス
		if(stat(dstDir, &st) || !(st.st_mode & S_IFDIR)) { error(EXIT_FAILURE, 0, "比較先ディレクトリ'%s'が存在しません。", dstDir); }
		//コピー先ディレクトリが存在しなければ、作成する。
		if(!outDir) { outDir = dstDir; }
//{{サービス
		//ディレクトリ名の末尾に'\\'が付いていると、stat()がエラーを返すようだ。(VC++6.0にて確認)
		//それだけの原因で「コピー先ディレクトリ'～\'が作成出来ません。」というエラーを表示すると、ユーザーがなぜエラーなのか判らずに混乱すると思うので、
		//サービスとして、当ツール内の処理で、ディレクトリ名の末尾の'\'を確実に削除する事にした。
		outDir = strtrim(outDir, 2/*右端にある指定文字を除去する*/, "\\");
//}}サービス
		if(!stat(outDir, &st)) {
			if(!(st.st_mode & S_IFDIR)) {
				error(EXIT_FAILURE, 0, "コピー先ディレクトリ'%s'と同名のファイルが存在します。", outDir);
			}
		} else {
			if(_mkdir(outDir)) {	//Unixでは「int mkdir(const char* path, mode_t mode);」ですが、Windowsでは「int _mkdir(const char* path);」です。
				error(EXIT_FAILURE, 0, "コピー先ディレクトリ'%s'が作成出来ません。", outDir);
			}
		}
		//各比較元ファイルについて…
		do {
			int bSkip = 0;	//コピー処理を飛ばすフラグ
			//比較元ファイルを開く。
			srcPath = argv[optind];
			fpSrc = fopen(srcPath, "rb");	//バイナリモード
			//比較元ファイルが開けなければ…
			if(!fpSrc) {
				//比較元ファイルが開けない理由が、ディレクトリだったからならば…
				if(!stat(dstDir, &st) && (st.st_mode & S_IFDIR)) {
					//'-i'オプションが指定されていたら、エラーとせずに無視する。
					if(opt_i) {
						if(!opt_q) {
							const char* suffix = "";
							//分かり易さのために、ディレクトリパスの末尾に、パスセパレータを補って表示する。
							if(!str_has_suffix(srcPath, "/") &&	//┬ディレクトリパスの末尾にパスセパレータが無ければ…
							   !str_has_suffix(srcPath, "\\")) {	//┘
								if(strchr(srcPath, '/')) {	//┬ディレクトリパスの中に'/'が有れば、末尾にも'/'を補う。
									suffix = "/";		//┘
								} else {			//┬ディレクトリパスの中に'\'が有るか、'/'も'\'も無ければ、末尾には'\'を補う。
									suffix = "\\";		//┘
								}
							}
							printf("ignore\t%s%s\n", srcPath, suffix);	//進捗表示
						}
						continue;	//この比較元ファイルの処理を飛ばす。
					//'-i'オプションが指定されていなければ、エラーとして停止する。
					} else {
						error(EXIT_FAILURE, errno, "比較元ファイルにディレクトリ'%s'が指定されました。このエラーを無視するには-iオプションを指定して下さい。", srcPath);
					}
				}
				error(EXIT_FAILURE, errno, "比較元ファイル'%s'が開けません。", srcPath);
			}
			{
				char dstPath[MAX_PATH];
				FILE* fpDst;
				//比較先ファイルのフルパス名を作成する。
				splitpath(srcPath, NULL, NULL, srcFname, srcExt);
				makepath(dstPath, NULL, dstDir, srcFname, srcExt);
				//比較先ファイルを読み取りモードで開いてみる。
				fpDst = fopen(dstPath, "rb");	//バイナリモード
				//比較先ファイルが存在すなるならば…
				if(fpDst) {
					//比較元ファイルと比較先ファイルを比較し、完全に一致したら、コピー処理を飛ばす。
					for(;;) {
						int srcLen = fread(srcBuf, 1, BUFFER_SIZE, fpSrc);
						int dstLen = fread(dstBuf, 1, BUFFER_SIZE, fpDst);
						if((srcLen != dstLen) || memcmp(srcBuf, dstBuf, srcLen)) { break; }	//不一致
						if(srcLen < BUFFER_SIZE) { bSkip = 1; break; }	//一致
					}
					//比較先ファイルを閉じる。
					fclose(fpDst);
				}
			}
			//コピー処理を飛ばすならば…
			if(bSkip) {
				if(!opt_q) { printf("skip\t%s\n", srcPath); }	//進捗表示
			//コピー処理を実行するならば…
			} else {
				char outPath[MAX_PATH];
				FILE* fpOut;
				//比較元ファイルを巻き戻す。
				rewind(fpSrc);
				//コピー先ファイルのフルパス名を作成する。
				splitpath(srcPath, NULL, NULL, srcFname, srcExt);
				makepath(outPath, NULL, outDir, srcFname, srcExt);
				//コピー先ファイルを書き込みモードで開く。
				fpOut = fopen(outPath, "wb");	//バイナリモード
				if(!fpOut) { error(EXIT_FAILURE, errno, "コピー先ファイル'%s'が作成出来ません。", outPath); }
				//比較元ファイルをコピー先ファイルにコピーする。
				if(!opt_q) { printf("copy\t%s\t%s\n", srcPath, outPath); }	//進捗表示
				for(;;) {
					int srcLen = fread( srcBuf, 1, BUFFER_SIZE, fpSrc);
					int outLen = fwrite(srcBuf, 1,      srcLen, fpOut);
					if(srcLen != outLen) { DIE(); }	//容量不足,又は,IOエラー。通常は起こり得ない。
					if(srcLen < BUFFER_SIZE) { break; }
				}
				//コピー先ファイルを閉じる。
				fclose(fpOut);
			}
			//比較元ファイルを閉じる。
			fclose(fpSrc);
		} while(++optind < (argc - 1));
	}
	return EXIT_SUCCESS;
}
