UNPKG

@mangar2/persist

Version:

Persists an object in a file

261 lines (240 loc) 8.86 kB
/** * @license * This software is licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3. It is furnished * "as is", without any support, and with no warranty, express or implied, as to its usefulness for * any purpose. * * @author Volker Böhm * @copyright Copyright (c) 2020 Volker Böhm * @overview persists JSON objects to a file. Keeps several files and loads older files if the latest is * broken */ 'use strict' const fs = require('fs') const assert = require('assert') const errorLog = require('@mangar2/errorlog') /** * Creates a new persistance support class * @param {Object} configuration configuration options * @param {number} configuration.keepFiles amount of file versions to keep (including the recently written file) * @example * const persist = new Persist( { keepFiles: 5 }) * persist.saveObjectToFile('.', 'helloworld.json', { message: 'hello world' } ) * const dataRead = persist.readData('.', 'helloworld.json') * // prints 'hello world' * console.log(dataRead.message) */ class Persist { constructor (configuration = {}) { this.keepFiles = Number(configuration.keepFiles) || 5 this.writeTimestamp = Date.now() } /** * @private * @description * Checks, if we are currently writing a file */ isWritingFile () { return this.writeTimestamp === undefined } /** * @private * @description * Checks, if the last write timestamp is longer ago than a privided amount of seconds. * @param {number} timeoutInSeconds if the file is older than ... * @returns {boolean} true, if the file is outdated */ fileIsOutdated (timeoutInSeconds) { let fileIsOutdated = false if (!this.isWritingFile()) { const ONE_SECOND_IN_MILLISECONDS = 1000 fileIsOutdated = (Date.now() - this.writeTimestamp) > timeoutInSeconds * ONE_SECOND_IN_MILLISECONDS } return fileIsOutdated } /** * @private * @description * Pauses the execution for a while (needs to "wait") for the result. * @param {number} timeInMilliseconds delay in milliseconds */ static delay (timeInMilliseconds) { return new Promise(resolve => setTimeout(resolve, timeInMilliseconds)) } /** * @private * @description * Gets the local time in ISO string format * @returns {string} local time in ISO string format */ static getLocalTimeAsISOString () { const tzoffsetInMinutes = (new Date()).getTimezoneOffset() * 60000 const localISOTime = (new Date(Date.now() - tzoffsetInMinutes)).toISOString().slice(0, -1) return localISOTime } /** * @private * @description * deletes a file * @param {string} filePath string with filename (including path) * @returns {promise} */ static async deleteFile (filePath) { return new Promise((resolve, reject) => { fs.unlink(filePath, err => { if (err) { reject(err) } else { resolve() } }) }) } /** * @private * @description * writes a file * @param {string} fileAndPathName filename (including path) * @param {string} data data to be saved * @returns {promise} */ static async writeFile (fileAndPathName, data) { return new Promise((resolve, reject) => { fs.writeFile(fileAndPathName, data, (err) => { if (err) { reject(err) } else { resolve() } }) }) } /** * @private * @description * Reads a directory and sorts it * @param {string} directory directory to read and sort files * @returns {promise} sorted list of files */ static async readDir (directory) { return new Promise((resolve, reject) => { fs.readdir(directory, (err, files) => { if (err) { reject(err) } else { files.sort() resolve(files) } }) }) } /** * @private * @description * Generates a regular expression to check, if a filename matches * @param {string} filenameBasis basis file name */ static genFileMatch (filenameBasis) { return new RegExp('^' + filenameBasis + '\\d{4}-\\d{2}-\\d{2}') } /** * @private * @description * Deletes old files from the data directory * @param {string} directory directory to delete file * @param {string} filenameBasis basis filename of the file. * @param {number} keepFiles amount of files to keep */ static async deleteOldFiles (directory, filenameBasis, keepFiles) { const fileMatch = Persist.genFileMatch(filenameBasis) const files = await Persist.readDir(directory) for (let index = files.length - 1; index >= 0; index--) { const filename = files[index] if (!filename.match(fileMatch)) { continue } keepFiles-- if (keepFiles <= 0) { await Persist.deleteFile(directory + '/' + filename) } } } /** * Stringifies a JSON and writes it to a file. * It will automatically add a timestamp to the provided "base" filename * It does not throws errors, but logs write errors to the console * @param {string} directory directory to delete file * @param {string} filenameBasis basis filename of the file. The * @param {Object} objectToSave object to save as JSON * @returns {undefined} */ async saveObjectToFile (directory, filenameBasis, objectToSave) { assert(typeof (filenameBasis) === 'string', 'saveObjectToFile without filename') assert(typeof (directory) === 'string', 'saveObjectToFile without directory') assert(typeof (objectToSave) !== 'undefined', 'object to save is undefined') let dateString = Persist.getLocalTimeAsISOString() dateString = dateString.replace(/:/g, '') const filePath = directory + '/' + filenameBasis + dateString + '.json' try { if (!this.isWritingFile()) { this.writeTimestamp = undefined const dataString = JSON.stringify(objectToSave) await Persist.writeFile(filePath, dataString) await Persist.deleteOldFiles(directory, filenameBasis, this.keepFiles) this.writeTimestamp = Date.now() } } catch (err) { errorLog(err) this.writeTimestamp = Date.now() } } /** * @private * @description * Reads the newest file from an array of files, beginning with the last filename in the array * It stops, when one file could be read successfully * @param {string} directory directory to delete file * @param {string} filenameBasis basis filename of the file. The * @param {Array} files array of filenames in the current directory * @returns {Object} read data as object (created with JSON.parse) */ static readNewestFile (directory, filenameBasis, files) { const fileMatch = Persist.genFileMatch(filenameBasis) let result for (let index = files.length - 1; index >= 0; index--) { const filename = files[index] const filePath = directory + '/' + filename if (filename.match(fileMatch)) { try { const contents = fs.readFileSync(filePath) if (contents !== undefined) { result = JSON.parse(contents) break } } catch (err) { errorLog(err) } } } return result } /** * Reads data from a file * @param {string} directory directory to delete file * @param {string} filenameBasis basis filename of the file. The * @returns {Object} the object read. */ readData (directory, filenameBasis) { let data try { const files = fs.readdirSync(directory + '/') files.sort() data = Persist.readNewestFile(directory, filenameBasis, files) } catch (err) { data = undefined console.error(err) } return data } } module.exports = Persist