2018-10-09 20:30:59 +08:00
|
|
|
/*
|
|
|
|
MIT License http://www.opensource.org/licenses/mit-license.php
|
|
|
|
*/
|
|
|
|
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
const fs = require("fs");
|
|
|
|
const mkdirp = require("mkdirp");
|
|
|
|
const path = require("path");
|
2018-12-21 19:02:37 +08:00
|
|
|
const Queue = require("../util/Queue");
|
|
|
|
const memorize = require("../util/memorize");
|
2018-10-09 20:30:59 +08:00
|
|
|
const SerializerMiddleware = require("./SerializerMiddleware");
|
|
|
|
|
2019-01-24 23:51:05 +08:00
|
|
|
/** @typedef {import("./types").BufferSerializableType} BufferSerializableType */
|
|
|
|
|
2018-10-09 20:30:59 +08:00
|
|
|
class Section {
|
|
|
|
constructor(items) {
|
|
|
|
this.items = items;
|
|
|
|
this.parts = undefined;
|
|
|
|
this.length = NaN;
|
|
|
|
this.offset = NaN;
|
|
|
|
}
|
|
|
|
|
|
|
|
resolve() {
|
|
|
|
let hasPromise = false;
|
|
|
|
let lastPart = undefined;
|
|
|
|
const parts = [];
|
|
|
|
let length = 0;
|
|
|
|
for (const item of this.items) {
|
|
|
|
if (typeof item === "function") {
|
|
|
|
const r = item();
|
|
|
|
if (r instanceof Promise) {
|
|
|
|
parts.push(r.then(items => new Section(items).resolve()));
|
|
|
|
hasPromise = true;
|
2018-10-18 21:52:22 +08:00
|
|
|
} else if (r) {
|
2018-10-09 20:30:59 +08:00
|
|
|
parts.push(new Section(r).resolve());
|
2018-10-18 21:52:22 +08:00
|
|
|
} else {
|
|
|
|
return null;
|
2018-10-09 20:30:59 +08:00
|
|
|
}
|
|
|
|
length += 12; // 0, offset, size
|
|
|
|
lastPart = undefined;
|
|
|
|
} else if (lastPart) {
|
|
|
|
lastPart.push(item);
|
|
|
|
length += item.length;
|
|
|
|
} else {
|
|
|
|
length += 4; // size
|
|
|
|
length += item.length;
|
|
|
|
lastPart = [item];
|
|
|
|
parts.push(lastPart);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.length = length;
|
|
|
|
if (hasPromise) {
|
|
|
|
return Promise.all(parts).then(parts => {
|
|
|
|
this.parts = parts;
|
2018-10-18 21:52:22 +08:00
|
|
|
if (!parts.every(Boolean)) return null;
|
2018-10-09 20:30:59 +08:00
|
|
|
return this;
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
this.parts = parts;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
getSections() {
|
|
|
|
return this.parts.filter(p => p instanceof Section);
|
|
|
|
}
|
|
|
|
|
|
|
|
emit(out) {
|
|
|
|
for (const part of this.parts) {
|
|
|
|
if (part instanceof Section) {
|
2019-01-21 19:45:56 +08:00
|
|
|
const pointerBuf = Buffer.allocUnsafe(12);
|
2018-10-09 20:30:59 +08:00
|
|
|
pointerBuf.writeUInt32LE(0, 0);
|
|
|
|
pointerBuf.writeUInt32LE(part.offset, 4);
|
|
|
|
pointerBuf.writeUInt32LE(part.length, 8);
|
|
|
|
out.push(pointerBuf);
|
|
|
|
} else {
|
2019-01-21 19:45:56 +08:00
|
|
|
const sizeBuf = Buffer.allocUnsafe(4);
|
2018-10-09 20:30:59 +08:00
|
|
|
out.push(sizeBuf);
|
|
|
|
let len = 0;
|
|
|
|
for (const buf of part) {
|
|
|
|
len += buf.length;
|
|
|
|
out.push(buf);
|
|
|
|
}
|
|
|
|
sizeBuf.writeUInt32LE(len, 0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-12-21 19:02:37 +08:00
|
|
|
/**
|
|
|
|
* @typedef {Object} FileJob
|
|
|
|
* @property {boolean} write
|
|
|
|
* @property {(fileHandle: number, callback: (err: Error?) => void) => void} fn
|
|
|
|
* @property {(err: Error?) => void} errorHandler
|
|
|
|
*/
|
2018-10-09 20:30:59 +08:00
|
|
|
|
2018-12-21 19:02:37 +08:00
|
|
|
class FileManager {
|
|
|
|
constructor() {
|
|
|
|
/** @type {Map<string, Queue<FileJob>>} */
|
|
|
|
this.jobs = new Map();
|
|
|
|
this.processing = new Map();
|
|
|
|
}
|
2018-10-09 20:30:59 +08:00
|
|
|
|
2018-12-21 19:02:37 +08:00
|
|
|
addJob(filename, write, fn, errorHandler) {
|
|
|
|
let queue = this.jobs.get(filename);
|
|
|
|
let start = false;
|
|
|
|
if (queue === undefined) {
|
|
|
|
queue = new Queue();
|
|
|
|
this.jobs.set(filename, queue);
|
|
|
|
start = true;
|
|
|
|
}
|
|
|
|
queue.enqueue({
|
|
|
|
write,
|
|
|
|
fn,
|
|
|
|
errorHandler
|
|
|
|
});
|
|
|
|
if (start) {
|
|
|
|
this._startProcessing(filename, queue);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} filename the filename
|
|
|
|
* @param {Queue<FileJob>} queue the job queue
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
_startProcessing(filename, queue) {
|
|
|
|
let fileHandle;
|
|
|
|
let write = false;
|
|
|
|
/** @type {FileJob | undefined} */
|
|
|
|
let currentJob = undefined;
|
|
|
|
/**
|
|
|
|
* Pull the next job from the queue, and process it
|
|
|
|
* When queue empty and file open, close it
|
|
|
|
* When queue (still) empty and file closed, exit processing
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
const next = () => {
|
|
|
|
if (queue.length === 0) {
|
|
|
|
if (fileHandle !== undefined) {
|
|
|
|
closeFile(next);
|
|
|
|
} else {
|
|
|
|
this.jobs.delete(filename);
|
|
|
|
this.processing.delete(filename);
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
currentJob = queue.dequeue();
|
|
|
|
// If file is already open but in the wrong mode
|
|
|
|
// close it and open it the other way
|
|
|
|
if (fileHandle !== undefined && write !== currentJob.write) {
|
|
|
|
closeFile(openFile);
|
|
|
|
} else {
|
|
|
|
openFile();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
/**
|
|
|
|
* Close the file and continue with the passed next step
|
|
|
|
* @param {function(): void} next next step
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
const closeFile = next => {
|
|
|
|
fs.close(fileHandle, err => {
|
|
|
|
if (err) return handleError(err);
|
|
|
|
fileHandle = undefined;
|
|
|
|
next();
|
|
|
|
});
|
|
|
|
};
|
|
|
|
/**
|
|
|
|
* Open the file if needed and continue with job processing
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
const openFile = () => {
|
|
|
|
if (fileHandle === undefined) {
|
|
|
|
write = currentJob.write;
|
|
|
|
fs.open(filename, write ? "w" : "r", (err, file) => {
|
|
|
|
if (err) return handleError(err);
|
|
|
|
fileHandle = file;
|
|
|
|
process();
|
2018-10-09 20:30:59 +08:00
|
|
|
});
|
2018-12-21 19:02:37 +08:00
|
|
|
} else {
|
|
|
|
process();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
/**
|
|
|
|
* Process the job function and continue with the next job
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
const process = () => {
|
|
|
|
currentJob.fn(fileHandle, err => {
|
|
|
|
if (err) return handleError(err);
|
|
|
|
currentJob = undefined;
|
|
|
|
next();
|
2018-10-09 20:30:59 +08:00
|
|
|
});
|
2018-12-21 19:02:37 +08:00
|
|
|
};
|
|
|
|
/**
|
|
|
|
* Handle any error, continue with the next job
|
|
|
|
* @param {Error} err occured error
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
const handleError = err => {
|
|
|
|
if (currentJob !== undefined) {
|
|
|
|
currentJob.errorHandler(err);
|
|
|
|
} else {
|
|
|
|
console.error(`Error in FileManager: ${err.message}`);
|
|
|
|
}
|
|
|
|
next();
|
|
|
|
};
|
|
|
|
next();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const fileManager = new FileManager();
|
|
|
|
|
|
|
|
const createPointer = (filename, offset, size) => {
|
|
|
|
return memorize(() => {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
fileManager.addJob(
|
|
|
|
filename,
|
|
|
|
false,
|
|
|
|
(file, callback) => {
|
|
|
|
readSection(filename, file, offset, size, (err, parts) => {
|
|
|
|
if (err) return callback(err);
|
|
|
|
resolve(parts);
|
|
|
|
callback();
|
|
|
|
});
|
|
|
|
},
|
|
|
|
reject
|
|
|
|
);
|
2018-10-09 20:30:59 +08:00
|
|
|
});
|
2018-12-21 19:02:37 +08:00
|
|
|
});
|
2018-10-09 20:30:59 +08:00
|
|
|
};
|
|
|
|
|
2019-01-19 18:49:30 +08:00
|
|
|
const readFileSectionToBuffer = (fd, buffer, offset, position, callback) => {
|
|
|
|
const remaining = buffer.length - offset;
|
|
|
|
fs.read(fd, buffer, offset, remaining, position, (err, bytesRead) => {
|
|
|
|
if (err) return callback(err);
|
|
|
|
if (bytesRead === 0) {
|
|
|
|
return callback(
|
|
|
|
new Error(
|
|
|
|
`Unexpected end of file (${remaining} bytes missing at pos ${position}`
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
if (bytesRead < remaining) {
|
|
|
|
return readFileSectionToBuffer(
|
|
|
|
fd,
|
|
|
|
buffer,
|
|
|
|
offset + bytesRead,
|
|
|
|
position + bytesRead,
|
|
|
|
callback
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return callback();
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2018-10-09 20:30:59 +08:00
|
|
|
const readSection = (filename, file, offset, size, callback) => {
|
2019-01-21 19:45:56 +08:00
|
|
|
const buffer = Buffer.allocUnsafe(size);
|
2019-01-19 18:49:30 +08:00
|
|
|
readFileSectionToBuffer(file, buffer, 0, offset, err => {
|
2018-10-09 20:30:59 +08:00
|
|
|
if (err) return callback(err);
|
|
|
|
|
|
|
|
const result = [];
|
|
|
|
let pos = 0;
|
|
|
|
while (pos < buffer.length) {
|
|
|
|
const len = buffer.readUInt32LE(pos);
|
|
|
|
pos += 4;
|
|
|
|
if (len === 0) {
|
|
|
|
const pOffset = buffer.readUInt32LE(pos);
|
|
|
|
pos += 4;
|
|
|
|
const pSize = buffer.readUInt32LE(pos);
|
|
|
|
pos += 4;
|
|
|
|
result.push(createPointer(filename, pOffset, pSize));
|
|
|
|
} else {
|
|
|
|
const buf = buffer.slice(pos, pos + len);
|
|
|
|
pos += len;
|
|
|
|
result.push(buf);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
callback(null, result);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2019-01-15 00:31:33 +08:00
|
|
|
const writeBuffers = (fileHandle, buffers, callback) => {
|
|
|
|
const stream = fs.createWriteStream(null, {
|
|
|
|
fd: fileHandle,
|
|
|
|
autoClose: false
|
|
|
|
});
|
|
|
|
let index = 0;
|
|
|
|
|
|
|
|
const doWrite = function() {
|
|
|
|
let canWriteMore = true;
|
|
|
|
while (canWriteMore && index < buffers.length) {
|
|
|
|
const chunk = buffers[index++];
|
|
|
|
canWriteMore = stream.write(chunk);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (index < buffers.length) {
|
|
|
|
stream.once("drain", doWrite);
|
|
|
|
} else {
|
|
|
|
stream.end();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
stream.on("error", err => callback(err));
|
|
|
|
stream.on("finish", () => callback(null));
|
|
|
|
doWrite();
|
|
|
|
};
|
|
|
|
|
2019-01-24 23:51:05 +08:00
|
|
|
/**
|
|
|
|
* @typedef {BufferSerializableType[]} DeserializedType
|
|
|
|
* @typedef {undefined} SerializedType
|
|
|
|
* @extends {SerializerMiddleware<DeserializedType, SerializedType>}
|
|
|
|
*/
|
2018-10-09 20:30:59 +08:00
|
|
|
class FileMiddleware extends SerializerMiddleware {
|
|
|
|
/**
|
2019-01-24 23:51:05 +08:00
|
|
|
* @param {DeserializedType} data data
|
|
|
|
* @param {Object} context context object
|
|
|
|
* @returns {SerializedType|Promise<SerializedType>} serialized data
|
2018-10-09 20:30:59 +08:00
|
|
|
*/
|
|
|
|
serialize(data, { filename }) {
|
|
|
|
const root = new Section(data);
|
|
|
|
|
|
|
|
const r = root.resolve();
|
|
|
|
|
2018-10-18 21:52:22 +08:00
|
|
|
return Promise.resolve(r).then(root => {
|
|
|
|
if (!root) return null;
|
2018-10-09 20:30:59 +08:00
|
|
|
// calc positions in file
|
|
|
|
let currentOffset = 4;
|
|
|
|
const processOffsets = section => {
|
|
|
|
section.offset = currentOffset;
|
|
|
|
currentOffset += section.length;
|
|
|
|
for (const child of section.getSections()) {
|
|
|
|
processOffsets(child);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
processOffsets(root);
|
|
|
|
|
|
|
|
// get buffers to write
|
2019-01-21 19:45:56 +08:00
|
|
|
const sizeBuf = Buffer.allocUnsafe(4);
|
2018-10-09 20:30:59 +08:00
|
|
|
sizeBuf.writeUInt32LE(root.length, 0);
|
|
|
|
const buffers = [sizeBuf];
|
|
|
|
const emit = (section, out) => {
|
|
|
|
section.emit(out);
|
|
|
|
for (const child of section.getSections()) {
|
|
|
|
emit(child, out);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
emit(root, buffers);
|
|
|
|
|
|
|
|
// write to file
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
mkdirp(path.dirname(filename), err => {
|
|
|
|
if (err) return reject(err);
|
2018-12-21 19:02:37 +08:00
|
|
|
fileManager.addJob(filename, true, (file, callback) => {
|
2019-01-15 00:31:33 +08:00
|
|
|
writeBuffers(file, buffers, err => {
|
2018-12-21 19:02:37 +08:00
|
|
|
if (err) return callback(err);
|
|
|
|
resolve(true);
|
|
|
|
callback();
|
|
|
|
});
|
2018-10-09 20:30:59 +08:00
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-01-24 23:51:05 +08:00
|
|
|
* @param {SerializedType} data data
|
|
|
|
* @param {Object} context context object
|
|
|
|
* @returns {DeserializedType|Promise<DeserializedType>} deserialized data
|
2018-10-09 20:30:59 +08:00
|
|
|
*/
|
|
|
|
deserialize(data, { filename }) {
|
|
|
|
return new Promise((resolve, reject) => {
|
2018-12-21 19:02:37 +08:00
|
|
|
fileManager.addJob(
|
|
|
|
filename,
|
|
|
|
false,
|
|
|
|
(file, callback) => {
|
2019-01-21 19:45:56 +08:00
|
|
|
const sizeBuf = Buffer.allocUnsafe(4);
|
2019-01-19 18:49:30 +08:00
|
|
|
readFileSectionToBuffer(file, sizeBuf, 0, 0, err => {
|
2018-12-21 19:02:37 +08:00
|
|
|
if (err) return callback(err);
|
2018-10-09 20:30:59 +08:00
|
|
|
|
2018-12-21 19:02:37 +08:00
|
|
|
const rootSize = sizeBuf.readUInt32LE(0);
|
2018-10-09 20:30:59 +08:00
|
|
|
|
2018-12-21 19:02:37 +08:00
|
|
|
readSection(filename, file, 4, rootSize, (err, parts) => {
|
|
|
|
if (err) return callback(err);
|
2018-10-09 20:30:59 +08:00
|
|
|
|
|
|
|
resolve(parts);
|
2018-12-21 19:02:37 +08:00
|
|
|
callback();
|
2018-10-09 20:30:59 +08:00
|
|
|
});
|
|
|
|
});
|
2018-12-21 19:02:37 +08:00
|
|
|
},
|
|
|
|
reject
|
|
|
|
);
|
2018-10-09 20:30:59 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = FileMiddleware;
|