From 64bdd37c4f51b493781e1a543e8a5de08a30c713 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Rebours?= Date: Thu, 21 Oct 2021 10:29:59 +0200 Subject: [PATCH] WIP: remove async from storage module --- lib/storage.js | 117 +++++++++++++++++++++++++++---------------------- 1 file changed, 64 insertions(+), 53 deletions(-) diff --git a/lib/storage.js b/lib/storage.js index fa8f12a..8e8d2eb 100755 --- a/lib/storage.js +++ b/lib/storage.js @@ -7,16 +7,21 @@ * It's essentially fs, mkdirp and crash safe write and read functions */ const fs = require('fs') +const fsPromises = require('fs/promises') const path = require('path') -const async = require('async') +const { callbackify, promisify } = require('util') const storage = {} const { Readable } = require('stream') // eslint-disable-next-line node/no-callback-literal storage.exists = (path, cb) => fs.access(path, fs.constants.F_OK, (err) => { cb(!err) }) +storage.existsAsync = path => fsPromises.access(path, fs.constants.F_OK).then(() => true, () => false) storage.rename = fs.rename +storage.renameAsync = fsPromises.rename storage.writeFile = fs.writeFile +storage.writeFileAsync = fsPromises.writeFile storage.unlink = fs.unlink +storage.unlinkAsync = fsPromises.unlink storage.appendFile = fs.appendFile storage.readFile = fs.readFile storage.readFileStream = fs.createReadStream @@ -25,21 +30,21 @@ storage.mkdir = fs.mkdir /** * Explicit name ... */ -storage.ensureFileDoesntExist = (file, callback) => { - storage.exists(file, exists => { - if (!exists) return callback(null) - - storage.unlink(file, err => callback(err)) - }) +storage.ensureFileDoesntExistAsync = async file => { + if (await storage.existsAsync(file)) await storage.unlinkAsync(file) } +storage.ensureFileDoesntExist = (file, callback) => callbackify(storage.ensureFileDoesntExistAsync)(file, err => callback(err)) + /** * Flush data in OS buffer to storage if corresponding option is set * @param {String} options.filename * @param {Boolean} options.isDir Optional, defaults to false * If options is a string, it is assumed that the flush of the file (not dir) called options was requested */ -storage.flushToStorage = (options, callback) => { +storage.flushToStorage = (options, callback) => callbackify(storage.flushToStorageAsync)(options, callback) + +storage.flushToStorageAsync = async (options) => { let filename let flags if (typeof options === 'string') { @@ -62,23 +67,29 @@ storage.flushToStorage = (options, callback) => { * database is loaded and a crash happens. */ - fs.open(filename, flags, (err, fd) => { - if (err) { - return callback((err.code === 'EISDIR' && options.isDir) ? null : err) + let fd, errorOnFsync, errorOnClose + try { + fd = await fsPromises.open(filename, flags) + try { + await fd.sync() + } catch (errFS) { + errorOnFsync = errFS } - fs.fsync(fd, errFS => { - fs.close(fd, errC => { - if ((errFS || errC) && !((errFS.code === 'EPERM' || errFS.code === 'EISDIR') && options.isDir)) { - const e = new Error('Failed to flush to storage') - e.errorOnFsync = errFS - e.errorOnClose = errC - return callback(e) - } else { - return callback(null) - } - }) - }) - }) + } catch (error) { + if (error.code !== 'EISDIR' || !options.isDir) throw error + } finally { + try { + await fd.close() + } catch (errC) { + errorOnClose = errC + } + } + if ((errorOnFsync || errorOnClose) && !((errorOnFsync.code === 'EPERM' || errorOnClose.code === 'EISDIR') && options.isDir)) { + const e = new Error('Failed to flush to storage') + e.errorOnFsync = errorOnFsync + e.errorOnClose = errorOnClose + throw e + } } /** @@ -109,6 +120,8 @@ storage.writeFileLines = (filename, lines, callback = () => {}) => { } } +storage.writeFileLinesAsync = (filename, lines) => promisify(storage.writeFileLines)(filename, lines) + /** * Fully write or rewrite the datafile, immune to crashes during the write operation (data will not be lost) * @param {String} filename @@ -116,25 +129,24 @@ storage.writeFileLines = (filename, lines, callback = () => {}) => { * @param {Function} callback Optional callback, signature: err */ storage.crashSafeWriteFileLines = (filename, lines, callback = () => {}) => { + callbackify(storage.crashSafeWriteFileLinesAsync)(filename, lines, callback) +} + +storage.crashSafeWriteFileLinesAsync = async (filename, lines) => { const tempFilename = filename + '~' - async.waterfall([ - async.apply(storage.flushToStorage, { filename: path.dirname(filename), isDir: true }), - cb => { - storage.exists(filename, exists => { - if (exists) storage.flushToStorage(filename, err => cb(err)) - else return cb() - }) - }, - cb => { - storage.writeFileLines(tempFilename, lines, cb) - }, - async.apply(storage.flushToStorage, tempFilename), - cb => { - storage.rename(tempFilename, filename, err => cb(err)) - }, - async.apply(storage.flushToStorage, { filename: path.dirname(filename), isDir: true }) - ], err => callback(err)) + await storage.flushToStorageAsync({ filename: path.dirname(filename), isDir: true }) + + const exists = await storage.existsAsync(filename) + if (exists) await storage.flushToStorageAsync({ filename }) + + await storage.writeFileLinesAsync(tempFilename, lines) + + await storage.flushToStorageAsync(tempFilename) + + await storage.renameAsync(tempFilename, filename) + + await storage.flushToStorageAsync({ filename: path.dirname(filename), isDir: true }) } /** @@ -142,21 +154,20 @@ storage.crashSafeWriteFileLines = (filename, lines, callback = () => {}) => { * @param {String} filename * @param {Function} callback signature: err */ -storage.ensureDatafileIntegrity = (filename, callback) => { - const tempFilename = filename + '~' +storage.ensureDatafileIntegrity = (filename, callback) => callbackify(storage.ensureDatafileIntegrityAsync)(filename, callback) - storage.exists(filename, filenameExists => { - // Write was successful - if (filenameExists) return callback(null) +storage.ensureDatafileIntegrityAsync = async filename => { + const tempFilename = filename + '~' - storage.exists(tempFilename, oldFilenameExists => { - // New database - if (!oldFilenameExists) return storage.writeFile(filename, '', 'utf8', err => { callback(err) }) + const filenameExists = await storage.existsAsync(filename) + // Write was successful + if (filenameExists) return - // Write failed, use old version - storage.rename(tempFilename, filename, err => callback(err)) - }) - }) + const oldFilenameExists = await storage.existsAsync(tempFilename) + // New database + if (!oldFilenameExists) await storage.writeFileAsync(filename, '', 'utf8') + // Write failed, use old version + else await storage.renameAsync(tempFilename, filename) } // Interface