Spaces:
Sleeping
Sleeping
| import fs from "fs"; | |
| import {EventEmitter} from "events"; | |
| import sha1 from "sha1"; | |
| import * as diff from "diff"; | |
| import asyncCall from "./asyncCall"; | |
| export default class FileProxy extends EventEmitter { | |
| content: string; | |
| timestamp: number; | |
| diskTimestamp: number; | |
| filePath: string; | |
| alive: boolean = true; | |
| writeFile: () => void; | |
| fileListener: (curr: fs.Stats) => Promise<void>; | |
| constructor (filePath: string) { | |
| super(); | |
| this.filePath = filePath; | |
| //console.log("File proxy created:", filePath); | |
| if (!fs.existsSync(filePath)) | |
| throw new Error(`file not exist: ${filePath}`); | |
| asyncCall(fs.stat, filePath) | |
| .then(stats => { | |
| this.diskTimestamp = stats.mtime.getTime(); | |
| this.timestamp = this.diskTimestamp; | |
| return asyncCall(fs.readFile, filePath); | |
| }) | |
| .then(buffer => { | |
| this.content = buffer.toString(); | |
| this.fullSync(); | |
| }); | |
| this.fileListener = async current => { | |
| this.diskTimestamp = current.mtime.getTime(); | |
| this.timestamp = this.diskTimestamp; | |
| const buffer = await asyncCall(fs.readFile, filePath); | |
| if (!buffer) { | |
| this.emit("error", {description: "file reading failed"}); | |
| return; | |
| } | |
| const newContent = buffer.toString(); | |
| const newHash = sha1(newContent); | |
| if (newHash !== this.hash) { | |
| const patch = diff.createPatch(filePath, this.content, newContent); | |
| this.emit("increase", { | |
| timestamp: this.timestamp, | |
| fromHash: this.hash, | |
| toHash: newHash, | |
| patch, | |
| }); | |
| this.content = newContent; | |
| } | |
| }; | |
| fs.watchFile(filePath, this.fileListener); | |
| this.keepWriteFile(); | |
| } | |
| dispose () { | |
| this.alive = false; | |
| if (this.fileListener) | |
| fs.unwatchFile(this.filePath, this.fileListener); | |
| } | |
| makeWritePromise (): Promise<void> { | |
| return new Promise(resolve => this.writeFile = resolve); | |
| } | |
| async keepWriteFile () { | |
| let writeSignal = this.makeWritePromise(); | |
| while (this.alive) { | |
| await writeSignal; | |
| writeSignal = this.makeWritePromise(); | |
| //console.debug("keepWriteFile:", this.timestamp, this.diskTimestamp); | |
| if (this.timestamp > this.diskTimestamp) { | |
| await asyncCall(fs.writeFile, this.filePath, this.content); | |
| } | |
| } | |
| } | |
| get hash (): string { | |
| return sha1(this.content); | |
| } | |
| fullSync () { | |
| this.emit("fullSync", { | |
| timestamp: this.timestamp, | |
| content: this.content, | |
| hash: this.hash, | |
| }); | |
| } | |
| increase ({timestamp, fromHash, toHash, patch}: { | |
| timestamp: number, | |
| fromHash: string, | |
| toHash: string, | |
| patch: string, | |
| }) { | |
| if (this.hash !== fromHash) { | |
| if (this.timestamp > timestamp) | |
| // web content is out of date | |
| this.fullSync(); | |
| else | |
| console.warn("[FileProxy] disk file content is behind increase base:", this.timestamp, timestamp); | |
| } | |
| else { | |
| this.content = diff.applyPatch(this.content, patch); | |
| this.timestamp = timestamp; | |
| console.assert(this.hash === toHash, "[FileProxy] verify failed:", this.hash, toHash, this.content); | |
| // trigger file writing | |
| this.writeFile(); | |
| } | |
| } | |
| }; | |