| 
									
										
										
										
											2018-09-27 13:22:19 +08:00
										 |  |  | /* | 
					
						
							|  |  |  | 	MIT License http://www.opensource.org/licenses/mit-license.php
 | 
					
						
							|  |  |  | 	Author Tobias Koppers @sokra | 
					
						
							|  |  |  | */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | "use strict"; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const AsyncQueue = require("./util/AsyncQueue"); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | let FS_ACCURACY = 2000; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * @typedef {Object} FileSystemInfoEntry | 
					
						
							|  |  |  |  * @property {number} safeTime | 
					
						
							|  |  |  |  * @property {number} timestamp | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /* istanbul ignore next */ | 
					
						
							|  |  |  | const applyMtime = mtime => { | 
					
						
							|  |  |  | 	if (FS_ACCURACY > 1 && mtime % 2 !== 0) FS_ACCURACY = 1; | 
					
						
							|  |  |  | 	else if (FS_ACCURACY > 10 && mtime % 20 !== 0) FS_ACCURACY = 10; | 
					
						
							|  |  |  | 	else if (FS_ACCURACY > 100 && mtime % 200 !== 0) FS_ACCURACY = 100; | 
					
						
							|  |  |  | 	else if (FS_ACCURACY > 1000 && mtime % 2000 !== 0) FS_ACCURACY = 1000; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class FileSystemInfo { | 
					
						
							|  |  |  | 	constructor(fs) { | 
					
						
							|  |  |  | 		this.fs = fs; | 
					
						
							| 
									
										
										
										
											2019-01-05 02:17:37 +08:00
										 |  |  | 		/** @type {Map<string, FileSystemInfoEntry | null>} */ | 
					
						
							| 
									
										
										
										
											2018-09-27 13:22:19 +08:00
										 |  |  | 		this._fileTimestamps = new Map(); | 
					
						
							| 
									
										
										
										
											2019-01-05 02:17:37 +08:00
										 |  |  | 		/** @type {Map<string, FileSystemInfoEntry | null>} */ | 
					
						
							| 
									
										
										
										
											2018-09-27 13:22:19 +08:00
										 |  |  | 		this._contextTimestamps = new Map(); | 
					
						
							|  |  |  | 		this.fileTimestampQueue = new AsyncQueue({ | 
					
						
							|  |  |  | 			name: "file timestamp", | 
					
						
							|  |  |  | 			parallelism: 30, | 
					
						
							|  |  |  | 			processor: this._readFileTimestamp.bind(this) | 
					
						
							|  |  |  | 		}); | 
					
						
							|  |  |  | 		this.contextTimestampQueue = new AsyncQueue({ | 
					
						
							|  |  |  | 			name: "context timestamp", | 
					
						
							|  |  |  | 			parallelism: 2, | 
					
						
							|  |  |  | 			processor: this._readContextTimestamp.bind(this) | 
					
						
							|  |  |  | 		}); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	/** | 
					
						
							| 
									
										
										
										
											2019-01-05 02:17:37 +08:00
										 |  |  | 	 * @param {Map<string, FileSystemInfoEntry | null>} map timestamps | 
					
						
							| 
									
										
										
										
											2018-09-27 13:22:19 +08:00
										 |  |  | 	 * @returns {void} | 
					
						
							|  |  |  | 	 */ | 
					
						
							|  |  |  | 	addFileTimestamps(map) { | 
					
						
							|  |  |  | 		for (const [path, ts] of map) { | 
					
						
							|  |  |  | 			this._fileTimestamps.set(path, ts); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	/** | 
					
						
							| 
									
										
										
										
											2019-01-05 02:17:37 +08:00
										 |  |  | 	 * @param {Map<string, FileSystemInfoEntry | null>} map timestamps | 
					
						
							| 
									
										
										
										
											2018-09-27 13:22:19 +08:00
										 |  |  | 	 * @returns {void} | 
					
						
							|  |  |  | 	 */ | 
					
						
							|  |  |  | 	addContextTimestamps(map) { | 
					
						
							|  |  |  | 		for (const [path, ts] of map) { | 
					
						
							|  |  |  | 			this._contextTimestamps.set(path, ts); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	/** | 
					
						
							|  |  |  | 	 * @param {string} path file path | 
					
						
							|  |  |  | 	 * @param {function(Error=, FileSystemInfoEntry=): void} callback callback function | 
					
						
							|  |  |  | 	 * @returns {void} | 
					
						
							|  |  |  | 	 */ | 
					
						
							|  |  |  | 	getFileTimestamp(path, callback) { | 
					
						
							|  |  |  | 		const cache = this._fileTimestamps.get(path); | 
					
						
							| 
									
										
										
										
											2018-09-28 03:28:07 +08:00
										 |  |  | 		if (cache !== undefined) return callback(null, cache); | 
					
						
							| 
									
										
										
										
											2018-09-27 13:22:19 +08:00
										 |  |  | 		this.fileTimestampQueue.add(path, callback); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	/** | 
					
						
							|  |  |  | 	 * @param {string} path context path | 
					
						
							|  |  |  | 	 * @param {function(Error=, FileSystemInfoEntry=): void} callback callback function | 
					
						
							|  |  |  | 	 * @returns {void} | 
					
						
							|  |  |  | 	 */ | 
					
						
							|  |  |  | 	getContextTimestamp(path, callback) { | 
					
						
							|  |  |  | 		const cache = this._contextTimestamps.get(path); | 
					
						
							| 
									
										
										
										
											2018-09-28 03:28:07 +08:00
										 |  |  | 		if (cache !== undefined) return callback(null, cache); | 
					
						
							| 
									
										
										
										
											2018-09-27 13:22:19 +08:00
										 |  |  | 		this.contextTimestampQueue.add(path, callback); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-01-05 21:58:06 +08:00
										 |  |  | 	createSnapshot(startTime, files, directories, missing, options, callback) { | 
					
						
							|  |  |  | 		const fileTimestamps = new Map(); | 
					
						
							|  |  |  | 		const contextTimestamps = new Map(); | 
					
						
							|  |  |  | 		const missingTimestamps = new Map(); | 
					
						
							|  |  |  | 		let jobs = 1; | 
					
						
							|  |  |  | 		const jobDone = () => { | 
					
						
							|  |  |  | 			if (--jobs === 0) { | 
					
						
							|  |  |  | 				callback(null, { | 
					
						
							|  |  |  | 					startTime, | 
					
						
							|  |  |  | 					fileTimestamps, | 
					
						
							|  |  |  | 					contextTimestamps, | 
					
						
							|  |  |  | 					missingTimestamps | 
					
						
							|  |  |  | 				}); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		}; | 
					
						
							| 
									
										
										
										
											2019-01-09 20:23:26 +08:00
										 |  |  | 		if (files) { | 
					
						
							|  |  |  | 			for (const path of files) { | 
					
						
							|  |  |  | 				const cache = this._fileTimestamps.get(path); | 
					
						
							|  |  |  | 				if (cache !== undefined) { | 
					
						
							|  |  |  | 					fileTimestamps.set(path, cache); | 
					
						
							|  |  |  | 				} else { | 
					
						
							|  |  |  | 					jobs++; | 
					
						
							|  |  |  | 					this.fileTimestampQueue.add(path, (err, entry) => { | 
					
						
							|  |  |  | 						if (err) { | 
					
						
							|  |  |  | 							fileTimestamps.set(path, "error"); | 
					
						
							|  |  |  | 						} else { | 
					
						
							|  |  |  | 							fileTimestamps.set(path, entry); | 
					
						
							|  |  |  | 						} | 
					
						
							|  |  |  | 						jobDone(); | 
					
						
							|  |  |  | 					}); | 
					
						
							|  |  |  | 				} | 
					
						
							| 
									
										
										
										
											2019-01-05 21:58:06 +08:00
										 |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2019-01-09 20:23:26 +08:00
										 |  |  | 		if (directories) { | 
					
						
							|  |  |  | 			for (const path of directories) { | 
					
						
							|  |  |  | 				contextTimestamps.set(path, "error"); | 
					
						
							|  |  |  | 				// TODO: getContextTimestamp is not implemented yet
 | 
					
						
							|  |  |  | 			} | 
					
						
							| 
									
										
										
										
											2019-01-05 21:58:06 +08:00
										 |  |  | 		} | 
					
						
							| 
									
										
										
										
											2019-01-09 20:23:26 +08:00
										 |  |  | 		if (missing) { | 
					
						
							|  |  |  | 			for (const path of missing) { | 
					
						
							|  |  |  | 				const cache = this._fileTimestamps.get(path); | 
					
						
							|  |  |  | 				if (cache !== undefined) { | 
					
						
							|  |  |  | 					missingTimestamps.set(path, cache); | 
					
						
							|  |  |  | 				} else { | 
					
						
							|  |  |  | 					jobs++; | 
					
						
							|  |  |  | 					this.fileTimestampQueue.add(path, (err, entry) => { | 
					
						
							|  |  |  | 						if (err) { | 
					
						
							|  |  |  | 							missingTimestamps.set(path, "error"); | 
					
						
							|  |  |  | 						} else { | 
					
						
							|  |  |  | 							missingTimestamps.set(path, entry); | 
					
						
							|  |  |  | 						} | 
					
						
							|  |  |  | 						jobDone(); | 
					
						
							|  |  |  | 					}); | 
					
						
							|  |  |  | 				} | 
					
						
							| 
									
										
										
										
											2019-01-05 21:58:06 +08:00
										 |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		jobDone(); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	checkSnapshotValid(snapshot, callback) { | 
					
						
							|  |  |  | 		const { | 
					
						
							|  |  |  | 			startTime, | 
					
						
							|  |  |  | 			fileTimestamps, | 
					
						
							|  |  |  | 			contextTimestamps, | 
					
						
							|  |  |  | 			missingTimestamps | 
					
						
							|  |  |  | 		} = snapshot; | 
					
						
							|  |  |  | 		let jobs = 1; | 
					
						
							|  |  |  | 		const jobDone = () => { | 
					
						
							|  |  |  | 			if (--jobs === 0) { | 
					
						
							|  |  |  | 				callback(null, true); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		}; | 
					
						
							|  |  |  | 		const invalid = () => { | 
					
						
							|  |  |  | 			if (jobs > 0) { | 
					
						
							|  |  |  | 				jobs = NaN; | 
					
						
							|  |  |  | 				callback(null, false); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		}; | 
					
						
							|  |  |  | 		const checkExistance = (current, snap) => { | 
					
						
							|  |  |  | 			if (snap === "error") { | 
					
						
							| 
									
										
										
										
											2019-01-09 20:23:26 +08:00
										 |  |  | 				// If there was an error while snapshotting (i. e. EBUSY)
 | 
					
						
							|  |  |  | 				// we can't compare further data and assume it's invalid
 | 
					
						
							|  |  |  | 				return false; | 
					
						
							| 
									
										
										
										
											2019-01-05 21:58:06 +08:00
										 |  |  | 			} | 
					
						
							|  |  |  | 			return !current === !snap; | 
					
						
							|  |  |  | 		}; | 
					
						
							|  |  |  | 		const checkFile = (current, snap) => { | 
					
						
							|  |  |  | 			if (snap === "error") { | 
					
						
							| 
									
										
										
										
											2019-01-09 20:23:26 +08:00
										 |  |  | 				// If there was an error while snapshotting (i. e. EBUSY)
 | 
					
						
							|  |  |  | 				// we can't compare further data and assume it's invalid
 | 
					
						
							|  |  |  | 				return false; | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			if (current && current.safeTime > startTime) { | 
					
						
							|  |  |  | 				// If a change happened after starting reading the item
 | 
					
						
							|  |  |  | 				// this may no longer be valid
 | 
					
						
							|  |  |  | 				return false; | 
					
						
							| 
									
										
										
										
											2019-01-05 21:58:06 +08:00
										 |  |  | 			} | 
					
						
							| 
									
										
										
										
											2019-01-09 20:23:26 +08:00
										 |  |  | 			if (!current !== !snap) { | 
					
						
							|  |  |  | 				// If existance of item differs
 | 
					
						
							|  |  |  | 				// it's invalid
 | 
					
						
							|  |  |  | 				return false; | 
					
						
							| 
									
										
										
										
											2019-01-05 21:58:06 +08:00
										 |  |  | 			} | 
					
						
							| 
									
										
										
										
											2019-01-09 20:23:26 +08:00
										 |  |  | 			if (current) { | 
					
						
							|  |  |  | 				// For existing items only
 | 
					
						
							|  |  |  | 				if ( | 
					
						
							|  |  |  | 					snap.timestamp !== undefined && | 
					
						
							|  |  |  | 					current.timestamp !== snap.timestamp | 
					
						
							|  |  |  | 				) { | 
					
						
							|  |  |  | 					// If we have a timestamp (it was a file or symlink) and it differs from current timestamp
 | 
					
						
							|  |  |  | 					// it's invalid
 | 
					
						
							|  |  |  | 					return false; | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			return true; | 
					
						
							| 
									
										
										
										
											2019-01-05 21:58:06 +08:00
										 |  |  | 		}; | 
					
						
							|  |  |  | 		for (const [path, ts] of fileTimestamps) { | 
					
						
							|  |  |  | 			const cache = this._fileTimestamps.get(path); | 
					
						
							|  |  |  | 			if (cache !== undefined) { | 
					
						
							|  |  |  | 				if (!checkFile(cache, ts)) { | 
					
						
							|  |  |  | 					invalid(); | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} else { | 
					
						
							|  |  |  | 				jobs++; | 
					
						
							|  |  |  | 				this.fileTimestampQueue.add(path, (err, entry) => { | 
					
						
							| 
									
										
										
										
											2019-01-09 20:23:26 +08:00
										 |  |  | 					if (err) return invalid(); | 
					
						
							| 
									
										
										
										
											2019-01-05 21:58:06 +08:00
										 |  |  | 					if (!checkFile(entry, ts)) { | 
					
						
							|  |  |  | 						invalid(); | 
					
						
							|  |  |  | 					} else { | 
					
						
							|  |  |  | 						jobDone(); | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 				}); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		if (contextTimestamps.size > 0) { | 
					
						
							|  |  |  | 			// TODO: getContextTimestamp is not implemented yet
 | 
					
						
							|  |  |  | 			invalid(); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		for (const [path, ts] of missingTimestamps) { | 
					
						
							|  |  |  | 			const cache = this._fileTimestamps.get(path); | 
					
						
							|  |  |  | 			if (cache !== undefined) { | 
					
						
							|  |  |  | 				if (!checkExistance(cache, ts)) { | 
					
						
							|  |  |  | 					invalid(); | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} else { | 
					
						
							|  |  |  | 				jobs++; | 
					
						
							|  |  |  | 				this.fileTimestampQueue.add(path, (err, entry) => { | 
					
						
							| 
									
										
										
										
											2019-01-09 20:23:26 +08:00
										 |  |  | 					if (err) return invalid(); | 
					
						
							| 
									
										
										
										
											2019-01-05 21:58:06 +08:00
										 |  |  | 					if (!checkExistance(entry, ts)) { | 
					
						
							|  |  |  | 						invalid(); | 
					
						
							|  |  |  | 					} else { | 
					
						
							|  |  |  | 						jobDone(); | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 				}); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		jobDone(); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-27 13:22:19 +08:00
										 |  |  | 	// TODO getFileHash(path, callback)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	_readFileTimestamp(path, callback) { | 
					
						
							|  |  |  | 		this.fs.stat(path, (err, stat) => { | 
					
						
							|  |  |  | 			if (err) { | 
					
						
							|  |  |  | 				if (err.code === "ENOENT") { | 
					
						
							|  |  |  | 					this._fileTimestamps.set(path, null); | 
					
						
							|  |  |  | 					return callback(null, null); | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 				return callback(err); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-01-09 20:23:26 +08:00
										 |  |  | 			const mtime = +stat.mtime; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			if (mtime) applyMtime(mtime); | 
					
						
							| 
									
										
										
										
											2018-09-27 13:22:19 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 			const ts = { | 
					
						
							| 
									
										
										
										
											2019-01-09 20:23:26 +08:00
										 |  |  | 				safeTime: mtime ? mtime + FS_ACCURACY : Infinity, | 
					
						
							|  |  |  | 				timestamp: stat.isDirectory() ? undefined : mtime | 
					
						
							| 
									
										
										
										
											2018-09-27 13:22:19 +08:00
										 |  |  | 			}; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			this._fileTimestamps.set(path, ts); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			callback(null, ts); | 
					
						
							|  |  |  | 		}); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	_readContextTimestamp(path, callback) { | 
					
						
							|  |  |  | 		// TODO read whole folder
 | 
					
						
							|  |  |  | 		this._contextTimestamps.set(path, null); | 
					
						
							|  |  |  | 		callback(null, null); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	getDeprecatedFileTimestamps() { | 
					
						
							|  |  |  | 		const map = new Map(); | 
					
						
							|  |  |  | 		for (const [path, info] of this._fileTimestamps) { | 
					
						
							|  |  |  | 			if (info) map.set(path, info.safeTime); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		return map; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	getDeprecatedContextTimestamps() { | 
					
						
							|  |  |  | 		const map = new Map(); | 
					
						
							|  |  |  | 		for (const [path, info] of this._contextTimestamps) { | 
					
						
							|  |  |  | 			if (info) map.set(path, info.safeTime); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		return map; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | module.exports = FileSystemInfo; |