/** * @name CeL file system functions * @fileoverview 本檔案包含了 file system functions。 * @since 2013/1/5 9:38:34 * @see File system */ 'use strict'; // -------------------------------------------------------------------------------------------- // 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。 typeof CeL === 'function' && CeL.run({ // module name name : 'data.file', // includes() @ data.code.compatibility. require : 'data.code.compatibility.|application.OS.Windows.new_COM' // + '|data.code.thread.Serial_execute', // 設定不匯出的子函式。 no_extend : '*', // 為了方便格式化程式碼,因此將 module 函式主體另外抽出。 code : module_code }); function module_code(library_namespace) { // requiring. var new_COM = this.r('new_COM'), Serial_execute = this.r('Serial_execute'); // ---------------------------------------------------------------------// // 基本宣告與定義。 var // cache. path_separator = library_namespace.env.path_separator, /** * FileSystemObject * * @inner * @ignore * @see FileSystemObject Object, Scripting * Run-Time Reference/FileSystemObject * http://msdn.microsoft.com/en-US/library/hww8txat%28v=VS.84%29.aspx */ FSO = new_COM("Scripting.FileSystemObject"); // 可 test FSO. var // const flag enumeration: 用於指示 type. // assert: (!!type) MUST true! FILE = 1, FOLDER = 2, // const flag enumeration: file/folder callback index. FILE_HANDLER_INDEX = 0, FOLDER_HANDLER_INDEX = 1, // const: sub files. // 必須是不會被用於目錄名之值。 FILES = '', // TODO: file/directory status/infomation, even contents. // 必須是不會被用於目錄名之值。 DATA = '.', // 預設最多處理之 folder 層數。 // directory depth limit. default_depth = 64, // const flag enumeration: default property data. fso_property = { file : { Attributes : '', DateCreated : '', DateLastAccessed : '', DateLastModified : '', Drive : '', Name : '', ParentFolder : '', Path : '', ShortName : '', ShortPath : '', Size : '', Type : '' }, folder : { Attributes : '', DateCreated : '', DateLastAccessed : '', DateLastModified : '', Drive : '', Name : '', ParentFolder : '', Path : '', ShortName : '', ShortPath : '', Size : '', Type : '', Files : '', IsRootFolder : '', SubFolders : '' }, driver : { AvailableSpace : '', DriveLetter : '', DriveType : '', FileSystem : '', FreeSpace : '', IsReady : '', Path : '', RootFolder : '', SerialNumber : '', ShareName : '', TotalSize : '', VolumeName : '' } }; // a network drive. // http://msdn.microsoft.com/en-us/library/ys4ctaz0(v=vs.84).aspx // NETWORK_DRIVE = 3, /** * 取得指定 path 之檔名/資料夾名稱。 * * @param {String}path * 指定之目標路徑。 * @returns {String} 檔名/資料夾名稱。 * @inner */ function name_of_path(path) { var match = typeof path === 'string' && path.match(/[^\\\/]+/); return match && match[0] || path || ''; } /** * 傳回新的資料夾結構。 * * @returns 新的資料夾結構。 * @inner */ function new_folder() { var folder = Object.create(null); // 檔案, sub-files. folder[FILES] = Object.create(null); // directory status/infomation. folder[DATA] = Object.create(null); return folder; } // ---------------------------------------------------------------------// // filter 處理。 /** * options 所使用到的 filter name。 */ var regular_filter_name = { // path filter. // 通常我們輸入的只會指定 path filter。 filter : 0, // file name filter. 篩選檔案. // WARNING: 若有設定,只要檔名不符合,即使 folder name 符合亦一樣會被剔除! file_filter : 1, // folder name filter. 篩選資料夾. folder_filter : 2 }; /** * 判別是否為可接受之字串篩選器。
* filter should has NO global flag. * * @param {undefined|String|RegExp|Function}filter * 字串篩選器。 * * @returns {Boolean} 為可接受之字串篩選器。 */ function is_regular_filter(filter) { return !filter || typeof filter === 'string' // RegExp || library_namespace.is_RegExp(filter) // function || typeof filter === 'function'; } /** * 檢測 options 的 filter。 * * @param {Object}options * optional flag. e.g., filter. */ function check_filter_of_options(options) { for ( var filter_name in regular_filter_name) if ((filter_name in options) && !is_regular_filter(options[filter_name])) delete options[filter_name]; } /** {Number}未發現之index。 const: 基本上與程式碼設計合一,僅表示名義,不可更改。(=== -1) */ var NOT_FOUND = ''.indexOf('_'); /** * 判斷指定字串是否合乎篩選。 * * @param {undefined|String|RegExp|Function}filter * 字串篩選器。 * @param {String}string * 欲測試之指定字串。string to test. * @param [argument] * 當篩選器為 function 時之附加引數。 * * @returns {Boolean} 為可接受之字串篩選器。 */ function match_filter(filter, string, argument) { return !filter // || (typeof filter === 'string' // String ? string.includes(filter) // RegExp : filter.test ? filter.test(string) // function : filter(string, argument)); } // ---------------------------------------------------------------------// // 檔案系統模擬結構。 /** * 建立模擬檔案系統結構 (file system structure) 之 Class。取得檔案列表。
* 注意: 在此設定的 callback,不具有防呆滯功能。
* TODO: fs.readdir or fs.readdirSync @ node.js
* TODO: follow link * * @example // 列出指定目錄下所有壓縮檔。 CeL.run('data.file', function() { var folder = new CeL.file_system_structure('D:\\a', { file_filter : /\.(zip|rar|7z|exe)$/i }); folder.each(function(fso, info) { CeL[info.is_file ? 'log' : 'info']([ info.index, '/', info.length, '[', fso.Path, ']' ]); }, { // filter : 'f', max_count : 5, 'final' : function() { CeL.log([ this.count.filtered_file, ' done']); } } ); }); * * @param {String|Array}path * 指定之目標路徑。
* 使用相對路徑,如 '..' 開頭時,須用 get_file_path() 調整過。 * @param {Object}[options] * optional flag. e.g., filter. * * @constructor * * @see FileSystemObject * * @since 2013/1/6 18:57:16 堪用。 */ function file_system_structure(path, options) { if (this === file_system_structure) throw 'Please use "new file_system_structure()"' + ' instead of "file_system_structure()"!'; // private properties. this.structure = new_folder(); this.count = Object.create(null); this.path_list = []; this.add(path, options); } /** * 解析/取得模擬結構中,指定 path 所在位置。 * * @param {String}path * 指定之目標路徑。 * @param {Number}create_type * 以 FILE or FOLDER 創建此標的。 * * @returns {Object} 檔案系統模擬結構中,指定 path 所在位置。 * @returns undefined error occurred. */ function resolve_path(path, create_type) { var base = this.structure; if (path && typeof path === 'string') { var name, i = 0, list = library_namespace.simplify_path(path) .replace(/[\\\/]+$/, ''); if (name = list.match(/^\\\\[^\\]+/)) { // a network drive. name = name[0]; list = list.slice(name.length).split(/[\\]+/); list[0] = name; } else list = list.split(/[\\\/]+/); for (; i < list.length; i++) { name = list[i]; if (name in base) base = base[name]; else if (!create_type) return undefined; else if (create_type === FOLDER // 不是最後一個。 || i + 1 < list.length) base = base[name] = new_folder(); else base[FILES][name] = null; } } return base; } /** * 將 fso 所有 data_fields 中的項目 extend 到 data 中。 * * @inner * @private * * @param {file_system_object}fso * file system object. * @param {Object}data_fields * 欲 extend 之項目。 * @param {檔案系統模擬結構}data * 所在位置。 * * @returns {Number} extend 之項目數。 */ function fill_data(fso, data_fields, data) { var name, // {Number} extend 之項目數。 // count = 0, item; for (name in data_fields) try { // Get the infomation/status of fso. if ((item = fso[name]) !== undefined) { data[name] // = typeof data_fields[name] === 'function' // ? data_fields[name](item) : item; // count++; } } catch (e) { // 在取得 RootFolder 的 .DateCreated, // .DateLastAccessed, .DateLastModified 時,會 // throw。 if (library_namespace.is_debug(3)) { library_namespace.warn('fill_data: 擷取資訊 [' + name + '] 時發生錯誤!'); library_namespace.error(e); } } // return count; return data; } /** * 將指定 path 加入模擬結構。
* 注意: base path 本身不受 filter 限制! * * @param {String|Array}path * 指定之目標路徑。
* 使用相對路徑,如 '..' 開頭時,須用 get_file_path() 調整過。 * @param {Object}[options] * optional flag. e.g., filter. */ function add_path(path, options) { library_namespace.debug('初始化+正規化。', 2, 'add_path'); // 前置處理。 if (!library_namespace.is_Object(options)) options = Object.create(null); if (isNaN(options.depth) || options.depth < 0 // || options.depth > default_depth ) options.depth = default_depth; check_filter_of_options(options); var callback_Array; if ('callback' in options) { callback_Array = options.callback; if (typeof callback_Array === 'function') options.callback = callback_Array = [ callback_Array, callback_Array ]; else if (!Array.isArray(callback_Array) || callback_Array.length === 0) { delete options.callback; callback_Array = undefined; } } // for Array [path]. if (Array.isArray(path)) { path.forEach(function(p) { this.add(p, options); }, this); return; } var base = this.structure; if (!path) { library_namespace.debug( // 省略 path 會當作所有 Drivers。 '取得各個 driver code。', 2, 'add_path'); for (var driver, drivers = new Enumerator(FSO.Drives); // !drivers.atEnd(); drivers.moveNext()) { driver = drivers.item(); // http://msdn.microsoft.com/en-us/library/ts2t8ybh(v=vs.84).aspx if (driver.IsReady) base[driver.Path] = new_folder(); else library_namespace.warn('add_path: Drive [' + driver.Path + '] 尚未就緒!'); } return; } var fso, // 類型 type; // 轉換輸入之 path 成 FSO object。 try { if (typeof path === 'string') { // 注意: 輸入 "C:" 會得到 C: 的工作目錄。 if (FSO.FolderExists(path)) { fso = FSO.GetFolder(path); type = FOLDER; } else if (FSO.FileExists(path)) { fso = FSO.GetFile(path); type = FILE; } } else if (typeof path === 'object' && path.Path) { fso = isNaN(path.DriveType) ? path : FSO.GetFolder(path.Path); type = fso.SubFolders ? FOLDER : FILE; } } catch (e) { library_namespace.error(e); } if (typeof fso !== 'object' || !(path = fso.Path)) { library_namespace.warn('add_path: 無法判別 path [' + path + ']!指定的文件不存在?'); return; } var list = this.path_list; for (var i = 0; i < list.length; i++) if (list[i].startsWith(path)) { library_namespace.debug('已處理過 path [' + path + ']。', 2, 'add_path'); return; } library_namespace.debug( // 'Adding [' + path + ']', 2, 'add_path'); list.push(path); if (base = callback_Array && callback_Array[ // type === FOLDER ? FOLDER_HANDLER_INDEX : FILE_HANDLER_INDEX]) try { base(fso, { depth : 0, is_file : type !== FOLDER }); } catch (e) { library_namespace.error(e); } base = this.get(path, type); var filter = options.filter, // file_filter = options.file_filter, // folder_filter = options.folder_filter, // folder_first = !!options.folder_first, // count = this.count, // 注意: assert(undefined|0===0) total_size = count.size | 0, // total_file_count = count.file | 0, // total_folder_count = count.folder | 0, // filtered_total_file_count = count.filtered_file | 0, // filtered_total_folder_count = count.filtered_folder | 0, // file_data_fields = library_namespace.is_Object(options.data) // ? options.data : options.data === true ? fso_property.file : false, // folder_data_fields = library_namespace.is_Object(options.folder_data) ? options.folder_data // : options.folder_data === true || options.data === true // ? fso_property.folder : file_data_fields, // 巡覽/遍歷子目錄與所包含的檔案。 traverse = function(fso, base, depth) { var item, collection, name, callback, // folder_data = base[DATA], // 有無加入 file count 功能,在 JScript 10.0.16438 差別不到 4%。 // 為求方便,不如皆加入。 size = 0, // file_count = 0, folder_count = 0, // filtered_file_count = 0, filtered_folder_count = 0, // list files of folder. 所包含的檔案. each_file = function() { library_namespace.debug('巡覽 [' + fso.Path + '] 之 sub-files。', 3, 'add_path'); try { for (collection = new Enumerator(fso.Files); // !collection.atEnd(); collection.moveNext()) { item = collection.item(); file_count++; size += item.Size; if (match_filter(filter, item.Path, depth) && match_filter(file_filter, name = item.Name, depth)) { ++filtered_file_count; library_namespace.debug( // 'Adding sub-file [' + filtered_file_count + '/' + file_count + '] [' + item.Path + ']', 4, 'add_path'); base[FILES][name] = file_data_fields // ? fill_data(item, file_data_fields, Object .create(null)) : null; // 預防 callback 動到 item,排在最後才處理。 if (callback = callback_Array // && callback_Array[FILE_HANDLER_INDEX]) try { callback(item, { depth : depth, is_file : true }); } catch (e) { library_namespace.error(e); } } } } catch (e) { // TODO: handle exception } total_size += size; total_file_count += file_count; filtered_total_file_count += filtered_file_count; }, // 處理子目錄。 each_folder = function() { library_namespace.debug('巡覽 [' + fso.Path + '] 之 sub-folders。', 3, 'add_path'); // 執行途中可能更動/刪除到此項目。try: 以防 item 已經被刪除。 try { // 因為檔案可能因改名等改變順序,因此用 .moveNext() // 的方法可能有些重複,有些漏掉未處理。 for (collection = new Enumerator(fso.SubFolders); // !collection.atEnd(); collection.moveNext()) { item = collection.item(); folder_count++; if (match_filter(filter, item.Path, depth) // && match_filter(folder_filter, name = item.Name, depth)) { filtered_folder_count++; library_namespace.debug( // 'Adding sub-folder [' + filtered_folder_count + '/' + folder_count + '] [' + item.Path + ']', 4, 'add_path'); name = base[name] = new_folder(); if (callback = callback_Array // && callback_Array[FOLDER_HANDLER_INDEX]) try { callback(item, { depth : depth, is_file : false }); } catch (e) { library_namespace.error(e); } if (depth < options.depth) traverse(item, name, depth); } } } catch (e) { // TODO: handle exception } // 可加上次一層: 會連這次一層之檔案都加上去。 total_folder_count += folder_count; filtered_total_folder_count += filtered_folder_count; }; if (folder_data_fields) fill_data(fso, folder_data_fields, folder_data); // 自身已經處理完,接下來 sub-files/sub-folders 之 depth 皆 +1。 depth++; if (folder_first) { each_folder(); each_file(); } else { each_file(); each_folder(); } library_namespace.debug('巡覽 [' + fso.Path + ']: ' + file_count + '/' + total_file_count + ' files, ' + size + '/' + total_size + ' bytes, ' + folder_count + '/' + total_folder_count + ' folders。', 2, 'add_path'); folder_data.count = { size : size, file : file_count, folder : folder_count, filtered_file : filtered_file_count, filtered_folder : filtered_folder_count }; }; if (type === FOLDER) { if (options.depth > 0) { library_namespace.debug('開始巡覽 [' + path + ']。', 2, 'add_path'); traverse(fso, base, 0); } count.size = total_size; count.file = total_file_count; // +1: base path 本身。 count.folder = total_folder_count + 1; count.filtered_file = filtered_total_file_count; count.filtered_folder = filtered_total_folder_count + 1; } else { count.size = total_size + fso.Size; // +1: base path 本身。 count.file = total_file_count + 1; count.folder = total_folder_count; // +1: base path 本身。 count.filtered_file = filtered_total_file_count + 1; count.filtered_folder = filtered_total_folder_count; } } /** * 重新整理/同步模擬結構。 * * @param {String|Array}path * 指定之目標路徑。 * @param {Object}[options] * optional flag. e.g., filter. */ function refresh_structure(path, options) { // TODO: 勿 reset。 this.structure = new_folder(); this.path_list.forEach(function(p) { this.add(p, options); }, this); } /** * 當呼叫 JSON.stringify() 時的前置處理。 */ function structure_to_JSON(key) { // hierarchy: // { // FILES:[], // DATA:{}, // 資料夾名稱(sub directory name):{} // }; var structure = Object.create(null), // traverse = function(folder, base) { if (folder[FILES].length) base[FILES] = folder[FILES]; for ( var name in folder) if (name !== FILES && name !== DATA) traverse(folder[name], base[name] = Object.create(null)); }; traverse(this.structure, structure); return structure; } // ---------------------------------------------------------------------// // 巡覽/遍歷檔案系統模擬結構之功能。 /** * 處理執行 callback 發生錯誤時的函數。 * * @inner * @private * * @since 2013/1/7 22:38:47。 */ function callback_error_handler(controller, error_Object, options, stack) { var message = [ '執行 callback 時發生錯誤!您可' ], // skip_error = options.skip_error; if (skip_error) message.push({ a : '停止執行', href : '#', onclick : function() { controller.stop(); } }, ',或者', { // 忽略錯誤 a : '不再提醒錯誤', href : '#', onclick : function() { delete options.skip_error; } }); else message.push({ a : '重試', href : '#', onclick : function() { stack.index--; controller.start(); } }, '、', { a : '跳過並繼續執行', href : '#', onclick : function() { controller.start(); } }, ',或者', { a : '忽略所有錯誤', href : '#', title : '★若忽略所有錯誤,之後發生錯誤將不會停止,而會直接忽略。', onclick : function() { options.skip_error = true; controller.start(); } }); message.push('。'); CeL.warn(message); if (error_Object) library_namespace.error(error_Object); if (!skip_error) { controller.stop(); return true; } } /** * 巡覽/遍歷檔案系統模擬結構時,實際處理的函數。 * * @param {Array}LIFO_stack * 處理堆疊。 * @param {file_system_structure}_this * file_system_structure instance. * @param {Array}callback_Array * callback_Array[] * @param {Object}options * optional flag. e.g., filter. * * @inner * @private * * @since 2013/1/6 18:57:16 堪用。 */ function travel_handler(LIFO_stack, _this, // callback_Array, options) { // 每個 folder 會跑一次 travel_handler。 // 處理順序: // 處理/執行 folder 本身 // → expand sub-files // → 處理/執行 sub-files (若檔案過多會跳出至下一循環處理) // → expand sub-folders // → 下一循環 from sub-folder[0]。 var stack, base_space, base_path, // path, name, depth, path_exists; if (LIFO_stack.length > 0) { var callback, index; stack = LIFO_stack.at(-1); // 若有設定 stack.path,必以 path_separator 作結。 base_path = stack.path || ''; if (!stack.is_file) while (stack.index < stack.length) { name = stack[stack.index++]; if (FSO.FolderExists(path = base_path // + name // 若有設定 stack.path,必以 path_separator 作結。 + path_separator)) { base_path = path; path_exists = true; base_space = stack.base[name]; library_namespace.debug('處理/執行 folder [' + path + '] 本身。', 2, 'travel_handler'); // 判別是否在標的區域內。 // depth: 正處理之 folder 本身的深度。 if (isNaN(depth = stack.depth)) if (stack.length > 1) { stack.depth = depth = 0; } else for (var i = 0, // path_list = _this.path_list; // i < path_list.length; i++) if (path.startsWith(path_list[i])) { stack.depth = depth = 0; break; } if (!isNaN(depth)) { // 無論有無 match filter,皆應計數。 index = options.single_count // ? options.index++ : options.folder_index++; if ((callback // = callback_Array[FOLDER_HANDLER_INDEX]) && match_filter(options.filter, path, depth) && match_filter(options.folder_filter, name, depth)) try { /** * 處理/執行資料夾本身。 用 folder.Size==0 可判別是否為 empty * folder。 * * @see Folder * Object */ callback.call(this, FSO.GetFolder(path), { is_file : false, depth : depth, data : base_space[DATA], index : index, length : options.folder_count }); } catch (e) { if (callback_error_handler(this, e, options, stack)) return; } } // expand sub-files. // TODO: check fso.SubFolders if (callback_Array[FILE_HANDLER_INDEX] && depth < options.depth) { stack = []; for (name in // (stack.base = base_space[FILES])) stack.push(name); if (stack.length > 0) { if (options.sort) if (typeof options.sort // === 'function') stack.sort(options.sort); else stack.sort(); library_namespace.debug('開始處理 [' + base_path + '] 之' + stack.length + ' sub-files [' + stack + '].', 2, 'travel_handler'); stack.index = 0; stack.path = path; stack.depth = depth + 1; stack.is_file = true; LIFO_stack.push(stack); } } // 預防有 sub-folder,還是先 break; break; } else { if (path.startsWith('\\\\')) { // a network drive. base_path = path; path_exists = true; } else library_namespace.warn([ 'travel_handler: ', '無法 access folder [', path, ']!', '或許是操作期間,檔案有所更動。您可能需要 refresh?' ]); } } // depth: 正處理之 files 的深度。 depth = stack.depth; callback = callback_Array[FILE_HANDLER_INDEX]; if (stack.is_file) { library_namespace.debug('處理/執行 folder [' + base_path + '] 的 sub-files。', 2, 'travel_handler'); // main loop of file. for (var max_count = options.max_count, // filter = options.filter, // base = stack.base, pass_data = { is_file : true, depth : depth, // data : stack.base[DATA], // index : options.index, length : options.count };;) if (stack.index < stack.length) { name = stack[stack.index++]; if (match_filter(options.filter, path = base_path + name, depth) && match_filter(filter, name, depth)) { /** * 處理/執行 sub-files。 * * @see File Object */ if (FSO.FileExists(path)) try { // sub-file 存在,則 parent // folder 亦存在。 path_exists = true; pass_data.index = options.index++; pass_data.data = base[name]; callback.call(this, FSO.GetFile(path), pass_data); if (--max_count === 0 // && stack.index < stack.length) return; } catch (e) { if (callback_error_handler(this, e, options, stack)) return; } else library_namespace.warn([ 'travel_handler: 檔案 [', path, '] 不存在或無法 access!', '或許是操作期間,檔案有所更動。', '您可能需要 refresh?' ]); } // 無論有無 match filter,皆應計數。 pass_data.index++; } else { // 去掉 sub-files 之stack。 LIFO_stack.pop(); options.index = pass_data.index; break; } } library_namespace.debug('已處理過 folder [' + base_path + '] 本身與 sub-files。expand sub-folders.', 2, 'travel_handler'); stack = LIFO_stack.at(-1); // depth: 正處理之 folder 本身的深度。 depth = stack.depth; if (!base_space) { base_space = stack.base[stack[stack.index - 1]]; library_namespace.debug( '可能因為 file list 長度超過 options.max_count,已經被截斷過,因此需要重設 base_space [' + stack[stack.index - 1] + '] → ' + base_space + '。', 2, 'travel_handler'); } } else { base_path = ''; base_space = _this.structure; } if (isNaN(depth) || depth < options.depth) { if (LIFO_stack.length === 0 // 確認所在的 folder 存在。 // 若不存在,也毋須 expand 了。 || path_exists // 亦可以下列檢測 base_path 的方式判別。 // || base_path !== LIFO_stack[LIFO_stack.length - // 1].path ) { library_namespace.debug('expand sub-folders.', 3, 'travel_handler'); for (name in ((stack = []).base = base_space)) if (name !== FILES && name !== DATA) stack.push(name); if (stack.length > 0) { if (options.sort) if (typeof options.sort === 'function') stack.sort(options.sort); else stack.sort(); // sub-folders / sub-directory. library_namespace.debug([ '開始處理 [', base_path, '] 之 ', stack.length, ' 個子資料夾 [', stack.join('|'), '].' ], 2, 'travel_handler'); stack.index = 0; stack.path = base_path; if (!isNaN(depth)) stack.depth = depth + 1; LIFO_stack.push(stack); return; } } if (!base_path) { // 應該只有 structure 為空時會用到。 library_namespace.warn( // 'travel_handler: structure 為空?'); return this.finish(); } } // 檢查本 stack 是否已處理完畢。 while ((stack = LIFO_stack.at(-1)) // .index === stack.length) { library_namespace.debug('Move up. stack.length = ' + LIFO_stack.length, 2, 'travel_handler'); LIFO_stack.pop(); if (LIFO_stack.length === 0) return this.finish(); } } /** * travel structure.
* 巡覽/遍歷檔案系統模擬結構的 public 公用函數。 * * TODO:
* 不區分大小寫。ignore case
* 與 dir/a/s 相較,network drive 的速度過慢。 * * @param {Function|Array}callback * file system handle function / Array [file handler, folder * handler].
* 可以安排僅對folder或是file作用之function。
* handle function 應有的長度: 2
* callback(fso, info = {is_file, depth, data : fso data, index, * length}); index = 0 to (length - 1) * @param {Object}[options] * optional flag. e.g., filter.
* options 之內容會被改動! * * @returns {Serial_execute} controller * @returns undefined error occurred. * * @since 2013/1/6 18:57:16 堪用。 */ function for_each_FSO(callback, options) { var path_length = this.path_list.length; if (path_length === 0) { if (library_namespace.is_debug()) library_namespace.warn( // 'for_each_FSO: 尚未設定可供巡覽之 path。'); return undefined; } library_namespace.debug('初始化+正規化。', 2, 'for_each_FSO'); if (typeof callback === 'function') callback = [ callback, callback ]; else if (!Array.isArray(callback) || callback.length === 0) return; // 前置處理。 if (!library_namespace.is_Object(options)) options = Object.create(null); // 計數 + 進度。 options.index = 0; // 至此 options.count 代表 file count. options.count = this.count.filtered_file; options.folder_count = this.count.filtered_folder; if (options.single_count // = callback[FOLDER_HANDLER_INDEX] // === callback[FILE_HANDLER_INDEX]) options.folder_count // = (options.count += options.folder_count); else options.folder_index = 0; check_filter_of_options(options); if (typeof options.sort !== 'function' && !(options.sort = !!options.sort)) delete options.sort; // 限定 traverse 深度幾層。深入/不深入下層子目錄及檔案。 if (isNaN(options.depth) || (options.depth |= 0) < 0) options.depth = default_depth; if (isNaN(options.max_count) || (options.max_count |= 0) < 1 || options.max_count > 1e5) // 預設一次 thread 最多處理之檔案個數。 options.max_count = 800; options.argument = [ [], this, callback, options ]; if (typeof options.final === 'function') options.final = options.final.bind(this); library_namespace.debug('開始巡覽 ' + path_length + ' paths。', 2, 'for_each_FSO'); return new Serial_execute(travel_handler, options); } // ---------------------------------------------------------------------// // public interface of file_system_structure. Object.assign(file_system_structure.prototype, { get : resolve_path, add : add_path, each : for_each_FSO, refresh : refresh_structure, list : function(options) { var list = []; // TODO: 可用萬用字元。 this.each(function(fso, info) { if (info.is_file) list.push(fso.Path); }, options); return list; }, toJSON : structure_to_JSON }); // ---------------------------------------------------------------------// // CeL.run('data.file'); // var file_handler = new CeL.file([ 'path', 'path' ]); // file_handler.merge('to_path').add('path').forEach(function(){}); function Files(path_list, options) { // private property: // this.data[path] = data of file = { // encoding : encoding cache. NOT absolutely correct. // other data. // } this.data = {}; // private property: // this.order = ['path']; if (options) { if (typeof options.comparator === 'function') // e.g., function(a, b){return a-b;}; this.comparator = options.comparator; } this.add(path_list, options); } function Files_add_object(path, data, order, filter, attributes) { var object, count = 0; // 注意: 輸入 "C:" 會得到 C: 的工作目錄。 if (FSO.FolderExists(path)) { library_namespace.debug('Add folder [' + path + ']!', 2); var fso = FSO.GetFolder(path), item, collection; for (collection = new Enumerator(fso.Files); // !collection.atEnd(); collection.moveNext()) { item = collection.item(); path = item.Path; if (!match_filter(filter, path)) continue; data[path] = object = { size : item.Size }; if (attributes) Object.assign(object, attributes); if (order) order.push(path); count++; } } else if (FSO.FileExists(path)) { library_namespace.debug('Add file [' + path + ']!', 2); if (match_filter(filter, path)) { data[path] = object = {}; if (attributes) Object.assign(object, attributes); if (order) order.push(path); count++; } } else library_namespace.warn('Files_add_object: 無法辨識 path [' + path + ']!'); library_namespace.debug('Get ' + count + ' files.', 2); } function Files_add(path_list, options) { var data = this.data, // order = Array.isArray(this.order) && this.order, // filter = is_regular_filter(options.filter) && options.filter; if (typeof path_list === 'string') Files_add_object(path_list, data, order, filter); else if (Array.isArray(path_list)) path_list.forEach(function(path) { Files_add_object(path, data, order, filter); }); else if (library_namespace.is_Object(path_list)) for ( var path in path_list) Files_add_object(path, data, order, filter, path_list[path]); else { library_namespace.warn('Files: Unknown path list [' + path_list + ']!'); order = false; } if (order && this.comparator) this.sort( // default: this.comparator ); return this; } // create and return this 之 list。 // 請勿直接使用 this.order,而應該使用 this.list(); // 因為 this.order 可能尚未準備好。 // 不想改到 this 的,得自己 .slice()。 function Files_list(rebuilt) { if (rebuilt || !Array.isArray(this.order)) { var order = []; // 重新創建 order。 for ( var path in this.data) order.push(path); this.order = order; } return this.order; } function Files_sort(comparator, options) { // 重製 order。 var order = this.list(), // comparator = comparator || this.comparator; if (options && options.no_set_order) order = order.slice(); if (typeof use_function === 'function') order.sort(use_function); else if (library_namespace.is_RegExp(use_function)) order.sort(function(key_1, key_2) { if ((key_1 = key_1.match(use_function)) // && (key_2 = key_2.match(use_function))) return key_1[1] - key_2[1]; }); else order.sort(); library_namespace.debug(order.join('
'), 2); if (options) { if (!options.no_set_order) { this.order = order; if (comparator === 'function' && !options.no_set_sort) this.comparator = comparator; } if (options.get_list) return order; } return this; } var get_file_extension, read_file, write_file, guess_encoding; function setup_Files(function_body, function_name) { if (!function_name) function_name = library_namespace.get_function_name(function_body); setup_Files.conversion[function_name] = function_body; return function() { setup_Files.setup(); return function_body.apply(this, arguments); }; } setup_Files.conversion = {}; setup_Files.setup = function() { library_namespace.debug('Trying setup Files.', 1, 'setup_Files.setup'); library_namespace.is_included([ 'application.storage.file', 'application.OS.Windows.file', 'application.locale.encoding' ], true); get_file_extension = library_namespace. // application.storage.file.get_file_extension; var file = library_namespace.application.OS.Windows.file; read_file = file.read_file; write_file = file.write_file; guess_encoding = library_namespace. // application.locale.encoding.guess_encoding; Object.assign(Files.prototype, setup_Files.conversion); library_namespace.debug('Setup Files done.', 1, 'setup_Files.setup'); }; function detect_encoding(this_Files, path, force) { var data = this_Files.data.path; if (!data) return; if (force || !data.encoding) // cache encoding data.encoding = guess_encoding(path); library_namespace.debug('(' + data.encoding + ') [' + path + ']'); return data.encoding; } function decide_target(path, options) { var target = typeof options.target === 'function' // default: overwrite / save to original file. ? options.target(path) : path; if (typeof options.target_file === 'function') { // get file name. target = target.match(/^(.*?)([^\\\/]+)$/); target = target[1] + options.target_file(target[2]); } if (target !== path) { library_namespace.debug('check extension 一致性。', 2); var has_error, // ext1 = get_file_extension(path), // ext2 = get_file_extension(target); if (ext1 !== ext2) { has_error = 1; library_namespace.error('副檔名不同! [' + ext1 + ']→[' + ext2 + ']'); } library_namespace.debug('check target_file 檔案存在與否。', 2); if (FSO.FileExists(target)) { library_namespace.debug('check target_file 檔案大小是否差異太大。', 2); var ratio = FSO.GetFile(target).Size / FSO.GetFile(path).Size; if (ratio > 8) { has_error = 2; library_namespace.error('目標檔案 [' + target_file + '] 已存在,且檔案大小差異太大!'); } else { library_namespace.warn('目標檔案 [' + target_file + '] 已存在!' + (has_error ? '' : '將覆寫之。')); } } if (has_error) return; } library_namespace.debug('[' + path + ']' + (path === target ? ': original = decided' : '
→ [' + target + ']'), 2); return target; } /** * @example // Files_encode('target encoding') // if set save_to, then backup_to will be ignored. options = { from : 'encoding', save_to : function(path) { return 'save to'; }, backup_to : function(path) { return 'save to'; } } */ function Files_encode(target_encoding, options) { if (!options) options = Object.create(null); var modify = typeof options.modify === 'function' && options.modify, // filter = is_regular_filter(options.filter) && options.filter, // handle_file = function(path) { if (!match_filter(filter, path)) return; library_namespace.debug('處理 [' + path + ']', 2, 'Files_encode'); var source_encoding = options.source_encoding || detect_encoding(this, path) || this.encoding, // source_text, converted_text, // target_file = decide_target(path, options); if (!target_file) return; source_text = read_file(path, source_encoding); converted_text = modify ? modify(text) : text; if (typeof converted_text === 'string' // && (source_text !== converted_text // || source_enc && target_encoding // && source_enc !== target_encoding)) { // TODO: !!workaround!! write_file(target_file, converted_text, // TODO: truncate file target_encoding); } else if (target_file !== path) { // copy file. library_namespace.debug(path + ' → ' + target_file); FSO.CopyFile(path, target_file); } }; if (target_encoding || modify) this.list().forEach(handle_file, this); else library_namespace.warn( // 'Files_encode: Target encoding does not specified.'); return this; } function Files_replace(RegExp_from, to) { return this.encode(null, { modify : function(text) { return text.replace(RegExp_from, to); } }); } function Files_merge(target, options) { if (!target) // TODO: auto detect. throw new Error('Files_merge: No target specified!'); target = decide_target(target, options); if (!target) return; if (!options) options = Object.create(null); else if (typeof options === 'string') options = { encoding : options }; var encoding = options.encoding, // modify = typeof options.modify === 'function' // && options.modify, // filter = is_regular_filter(options.filter) && options.filter, // text_Array = [], // add_text = function(item, path) { item = options[item]; if (!item) return; if (typeof item === 'string') text_Array.push(item); else if (typeof item === 'function') text_Array.push(item(path)); else if (Array.isArray(item)) Array.prototype.push.apply( // text_Array, item); else library_namespace.debug('Unknown text: [' + item + ']', 3); }, // handle_file = function(path) { if (!match_filter(filter, path)) return; library_namespace.debug('處理 [' + path + ']', 2, 'Files_merge'); var encoding = options.source_encoding // || detect_encoding(this, path), // text = read_file(path, encoding); library_namespace.debug('(' + encoding + ') [' + path + ']: ' + text.length + ' characters.', 3); if (modify) { text = modify(text, path); library_namespace.debug( // '→ ' + text.length + ' characters.', 3); if (text.length === 0) library_namespace.warn('[' + path + ']: 經 modify 後,無內容回傳!'); } add_text('subhead', path); text_Array.push(text); // sub-footer add_text('subfooter', path); }; if (!encoding) encoding = FSO.FileExists(target) && detect_encoding(this, target) || this.encoding; if (library_namespace.is_debug(3)) library_namespace.debug('Merge ' + this.list().length // + ' files to (' + encoding + ') [' + target + '].'); add_text('head', target); this.list().forEach(handle_file, this); add_text('footer', target); text_Array = text_Array.join(''); if (typeof options.modify_all === 'function') text_Array = options.modify_all(text_Array); library_namespace.debug('Merge ' + this.order.length + ' files, writing ' + text_Array.length + ' characters to (' + encoding + ') [' + target + '].'); write_file(target, text_Array, encoding); (this.data = {})[target] = {}; delete this.order; return this; } /** * batch operation * * @param {Function}callback * callback(path, data) * @param {Function}comparator * @returns {@Files} */ function Files_forEach(callback, comparator) { var data = this.data, // order = comparator ? this.sort(comparator, { no_set_order : true, get_list : true }) : this.list(); order.forEach(function(path) { library_namespace.debug('處理 [' + path + ']', 2, 'Files_forEach'); callback(path, data[path]); }); return this; } /** * get HTML contents.
* get contents between "body" tag. * * @param {String}HTML * HTML text * @returns {String}contents * @since 2014/7/19 23:9:41 */ function HTML_contents(HTML, use_RegExp) { if (use_RegExp) return HTML.replace(/[\s\n]*<\/body>(.+|\n)*$/i, '') // default 不用 RegExp,因為 RegExp 太慢。 .replace(/^(.+|\n)*[\s\n]*/i, ''); var foot = HTML.lastIndexOf(''), // head = HTML.indexOf(''); if (foot === NOT_FOUND) { foot = HTML.lastIndexOf(''); if (foot !== NOT_FOUND && !/^[\s\n]*$/.test(HTML.slice(foot))) foot = NOT_FOUND; } // 去頭。 if (false && head === NOT_FOUND) head = HTML.indexOf('', head) + 1; // 切割。 if (foot === NOT_FOUND) { if (head !== NOT_FOUND) HTML = HTML.slice(head); } else HTML = HTML.slice(head, foot); return HTML; } // ---------------------------------------------------------------------// // public interface of Files. Object.assign(Files.prototype, { add : Files_add, list : Files_list, sort : Files_sort, // convert encoding encode : setup_Files(Files_encode, 'encode'), merge : setup_Files(Files_merge, 'merge'), replace : setup_Files(Files_replace, 'replace'), forEach : Files_forEach, encoding : 'UTF-8' }); // ---------------------------------------------------------------------// // export. return Object.assign(Files, { HTML_contents : HTML_contents, file_system_structure : file_system_structure }); }