WIP: remove async from storage module

pull/11/head
Timothée Rebours 3 years ago
parent 5f7bc1244d
commit 64bdd37c4f
  1. 117
      lib/storage.js

@ -7,16 +7,21 @@
* It's essentially fs, mkdirp and crash safe write and read functions * It's essentially fs, mkdirp and crash safe write and read functions
*/ */
const fs = require('fs') const fs = require('fs')
const fsPromises = require('fs/promises')
const path = require('path') const path = require('path')
const async = require('async') const { callbackify, promisify } = require('util')
const storage = {} const storage = {}
const { Readable } = require('stream') const { Readable } = require('stream')
// eslint-disable-next-line node/no-callback-literal // eslint-disable-next-line node/no-callback-literal
storage.exists = (path, cb) => fs.access(path, fs.constants.F_OK, (err) => { cb(!err) }) 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.rename = fs.rename
storage.renameAsync = fsPromises.rename
storage.writeFile = fs.writeFile storage.writeFile = fs.writeFile
storage.writeFileAsync = fsPromises.writeFile
storage.unlink = fs.unlink storage.unlink = fs.unlink
storage.unlinkAsync = fsPromises.unlink
storage.appendFile = fs.appendFile storage.appendFile = fs.appendFile
storage.readFile = fs.readFile storage.readFile = fs.readFile
storage.readFileStream = fs.createReadStream storage.readFileStream = fs.createReadStream
@ -25,21 +30,21 @@ storage.mkdir = fs.mkdir
/** /**
* Explicit name ... * Explicit name ...
*/ */
storage.ensureFileDoesntExist = (file, callback) => { storage.ensureFileDoesntExistAsync = async file => {
storage.exists(file, exists => { if (await storage.existsAsync(file)) await storage.unlinkAsync(file)
if (!exists) return callback(null)
storage.unlink(file, err => callback(err))
})
} }
storage.ensureFileDoesntExist = (file, callback) => callbackify(storage.ensureFileDoesntExistAsync)(file, err => callback(err))
/** /**
* Flush data in OS buffer to storage if corresponding option is set * Flush data in OS buffer to storage if corresponding option is set
* @param {String} options.filename * @param {String} options.filename
* @param {Boolean} options.isDir Optional, defaults to false * @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 * 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 filename
let flags let flags
if (typeof options === 'string') { if (typeof options === 'string') {
@ -62,23 +67,29 @@ storage.flushToStorage = (options, callback) => {
* database is loaded and a crash happens. * database is loaded and a crash happens.
*/ */
fs.open(filename, flags, (err, fd) => { let fd, errorOnFsync, errorOnClose
if (err) { try {
return callback((err.code === 'EISDIR' && options.isDir) ? null : err) fd = await fsPromises.open(filename, flags)
try {
await fd.sync()
} catch (errFS) {
errorOnFsync = errFS
} }
fs.fsync(fd, errFS => { } catch (error) {
fs.close(fd, errC => { if (error.code !== 'EISDIR' || !options.isDir) throw error
if ((errFS || errC) && !((errFS.code === 'EPERM' || errFS.code === 'EISDIR') && options.isDir)) { } finally {
const e = new Error('Failed to flush to storage') try {
e.errorOnFsync = errFS await fd.close()
e.errorOnClose = errC } catch (errC) {
return callback(e) errorOnClose = errC
} else { }
return callback(null) }
} 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) * Fully write or rewrite the datafile, immune to crashes during the write operation (data will not be lost)
* @param {String} filename * @param {String} filename
@ -116,25 +129,24 @@ storage.writeFileLines = (filename, lines, callback = () => {}) => {
* @param {Function} callback Optional callback, signature: err * @param {Function} callback Optional callback, signature: err
*/ */
storage.crashSafeWriteFileLines = (filename, lines, callback = () => {}) => { storage.crashSafeWriteFileLines = (filename, lines, callback = () => {}) => {
callbackify(storage.crashSafeWriteFileLinesAsync)(filename, lines, callback)
}
storage.crashSafeWriteFileLinesAsync = async (filename, lines) => {
const tempFilename = filename + '~' const tempFilename = filename + '~'
async.waterfall([ await storage.flushToStorageAsync({ filename: path.dirname(filename), isDir: true })
async.apply(storage.flushToStorage, { filename: path.dirname(filename), isDir: true }),
cb => { const exists = await storage.existsAsync(filename)
storage.exists(filename, exists => { if (exists) await storage.flushToStorageAsync({ filename })
if (exists) storage.flushToStorage(filename, err => cb(err))
else return cb() await storage.writeFileLinesAsync(tempFilename, lines)
})
}, await storage.flushToStorageAsync(tempFilename)
cb => {
storage.writeFileLines(tempFilename, lines, cb) await storage.renameAsync(tempFilename, filename)
},
async.apply(storage.flushToStorage, tempFilename), await storage.flushToStorageAsync({ filename: path.dirname(filename), isDir: true })
cb => {
storage.rename(tempFilename, filename, err => cb(err))
},
async.apply(storage.flushToStorage, { filename: path.dirname(filename), isDir: true })
], err => callback(err))
} }
/** /**
@ -142,21 +154,20 @@ storage.crashSafeWriteFileLines = (filename, lines, callback = () => {}) => {
* @param {String} filename * @param {String} filename
* @param {Function} callback signature: err * @param {Function} callback signature: err
*/ */
storage.ensureDatafileIntegrity = (filename, callback) => { storage.ensureDatafileIntegrity = (filename, callback) => callbackify(storage.ensureDatafileIntegrityAsync)(filename, callback)
const tempFilename = filename + '~'
storage.exists(filename, filenameExists => { storage.ensureDatafileIntegrityAsync = async filename => {
// Write was successful const tempFilename = filename + '~'
if (filenameExists) return callback(null)
storage.exists(tempFilename, oldFilenameExists => { const filenameExists = await storage.existsAsync(filename)
// New database // Write was successful
if (!oldFilenameExists) return storage.writeFile(filename, '', 'utf8', err => { callback(err) }) if (filenameExists) return
// Write failed, use old version const oldFilenameExists = await storage.existsAsync(tempFilename)
storage.rename(tempFilename, filename, err => callback(err)) // New database
}) if (!oldFilenameExists) await storage.writeFileAsync(filename, '', 'utf8')
}) // Write failed, use old version
else await storage.renameAsync(tempFilename, filename)
} }
// Interface // Interface

Loading…
Cancel
Save