@mangar2/persist
Version:
Persists an object in a file
261 lines (240 loc) • 8.86 kB
JavaScript
/**
* @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
*/
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