diff --git a/API.md b/API.md
index 325d046..9c2171c 100644
--- a/API.md
+++ b/API.md
@@ -51,7 +51,7 @@ with appendfsync
option set to no
.
Generic async function.
GenericCallback : function
Callback with generic parameters.
-document : Object.<string, *>
+document : object
Generic document in NeDB.
It consists of an Object with anything you want inside.
query : Object.<string, *>
@@ -309,6 +309,10 @@ automatically considered in-memory only. It cannot end with a ~
whi
perform crash-safe writes. Not used if options.inMemoryOnly
is true
.
- [.inMemoryOnly] boolean
= false
- If set to true, no data will be written in storage. This option has
priority over options.filename
.
+ - [.mode] object
- Permissions to use for FS. Only used for
+Node.js storage module.
+ - [.fileMode] number
= 0o644
- Permissions to use for database files
+ - [.dirMode] number
= 0o755
- Permissions to use for database directories
- [.timestampData] boolean
= false
- If set to true, createdAt and updatedAt will be created and
populated automatically (if not specified by user)
- [.autoload] boolean
= false
- If used, the database will automatically be loaded from the datafile
@@ -834,6 +838,9 @@ with appendfsync
option set to no
.
- [.corruptAlertThreshold] Number
- Optional, threshold after which an alert is thrown if too much data is corrupt
- [.beforeDeserialization] [serializationHook
](#serializationHook) - Hook you can use to transform data after it was serialized and before it is written to disk.
- [.afterSerialization] [serializationHook
](#serializationHook) - Inverse of afterSerialization
.
+ - [.mode] object
- Modes to use for FS permissions.
+ - [.fileMode] number
= 0o644
- Mode to use for files.
+ - [.dirMode] number
= 0o755
- Mode to use for directories.
@@ -934,7 +941,7 @@ with appendfsync
option set to no
.
-## document : Object.<string, \*>
+## document : object
Generic document in NeDB.
It consists of an Object with anything you want inside.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e1b27a6..561dd7e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- An auto-generated JSDoc file is generated: [API.md](./API.md).
- Added `Datastore#dropDatabaseAsync` and its callback equivalent.
- The Error given when the `Datastore#corruptAlertThreshold` is reached now has three properties: `dataLength` which is the amount of lines in the database file (excluding empty lines), `corruptItems` which is the amount of corrupted lines, `corruptionRate` which the rate of corruption between 0 and 1.
+- Added a `mode` option which allows to set the file and / or directory modes, by default, it uses `0o644` for files and `0o755` for directories, which may be breaking.
### Changed
- The `corruptionAlertThreshold` now doesn't take into account empty lines, and the error message is slightly changed.
diff --git a/lib/datastore.js b/lib/datastore.js
index 38629ea..da39c05 100755
--- a/lib/datastore.js
+++ b/lib/datastore.js
@@ -70,7 +70,7 @@ const { isDate } = require('./utils.js')
* @typedef document
* @property {?string} [_id] Internal `_id` of the document, which can be `null` or undefined at some points (when not
* inserted yet for example).
- * @type {Object.}
+ * @type {object}
*/
/**
@@ -165,6 +165,10 @@ class Datastore extends EventEmitter {
* perform crash-safe writes. Not used if `options.inMemoryOnly` is `true`.
* @param {boolean} [options.inMemoryOnly = false] If set to true, no data will be written in storage. This option has
* priority over `options.filename`.
+ * @param {object} [options.mode] Permissions to use for FS. Only used for
+ * Node.js storage module.
+ * @param {number} [options.mode.fileMode = 0o644] Permissions to use for database files
+ * @param {number} [options.mode.dirMode = 0o755] Permissions to use for database directories
* @param {boolean} [options.timestampData = false] If set to true, createdAt and updatedAt will be created and
* populated automatically (if not specified by user)
* @param {boolean} [options.autoload = false] If used, the database will automatically be loaded from the datafile
@@ -258,7 +262,8 @@ class Datastore extends EventEmitter {
db: this,
afterSerialization: options.afterSerialization,
beforeDeserialization: options.beforeDeserialization,
- corruptAlertThreshold: options.corruptAlertThreshold
+ corruptAlertThreshold: options.corruptAlertThreshold,
+ mode: options.mode
})
// This new executor is ready if we don't use persistence
diff --git a/lib/persistence.js b/lib/persistence.js
index 0efd7b1..b110ba5 100755
--- a/lib/persistence.js
+++ b/lib/persistence.js
@@ -6,6 +6,9 @@ const Index = require('./indexes.js')
const model = require('./model.js')
const storage = require('./storage.js')
+const DEFAULT_DIR_MODE = 0o755
+const DEFAULT_FILE_MODE = 0o644
+
/**
* Under the hood, NeDB's persistence uses an append-only format, meaning that all
* updates and deletes actually result in lines added at the end of the datafile,
@@ -43,13 +46,18 @@ class Persistence {
* @param {Number} [options.corruptAlertThreshold] Optional, threshold after which an alert is thrown if too much data is corrupt
* @param {serializationHook} [options.beforeDeserialization] Hook you can use to transform data after it was serialized and before it is written to disk.
* @param {serializationHook} [options.afterSerialization] Inverse of `afterSerialization`.
+ * @param {object} [options.mode] Modes to use for FS permissions.
+ * @param {number} [options.mode.fileMode=0o644] Mode to use for files.
+ * @param {number} [options.mode.dirMode=0o755] Mode to use for directories.
*/
constructor (options) {
this.db = options.db
this.inMemoryOnly = this.db.inMemoryOnly
this.filename = this.db.filename
this.corruptAlertThreshold = options.corruptAlertThreshold !== undefined ? options.corruptAlertThreshold : 0.1
-
+ this.mode = options.mode !== undefined ? options.mode : { fileMode: DEFAULT_FILE_MODE, dirMode: DEFAULT_DIR_MODE }
+ if (this.mode.fileMode === undefined) this.mode.fileMode = DEFAULT_FILE_MODE
+ if (this.mode.dirMode === undefined) this.mode.dirMode = DEFAULT_DIR_MODE
if (
!this.inMemoryOnly &&
this.filename &&
@@ -104,7 +112,7 @@ class Persistence {
}
})
- await storage.crashSafeWriteFileLinesAsync(this.filename, lines)
+ await storage.crashSafeWriteFileLinesAsync(this.filename, lines, this.mode)
this.db.emit('compaction.done')
}
@@ -155,7 +163,7 @@ class Persistence {
if (toPersist.length === 0) return
- await storage.appendFileAsync(this.filename, toPersist, 'utf8')
+ await storage.appendFileAsync(this.filename, toPersist, { encoding: 'utf8', mode: this.mode.fileMode })
}
/**
@@ -297,17 +305,17 @@ class Persistence {
// In-memory only datastore
if (this.inMemoryOnly) return
- await Persistence.ensureDirectoryExistsAsync(path.dirname(this.filename))
- await storage.ensureDatafileIntegrityAsync(this.filename)
+ await Persistence.ensureDirectoryExistsAsync(path.dirname(this.filename), this.mode.dirMode)
+ await storage.ensureDatafileIntegrityAsync(this.filename, this.mode.fileMode)
let treatedData
if (storage.readFileStream) {
// Server side
- const fileStream = storage.readFileStream(this.filename, { encoding: 'utf8' })
+ const fileStream = storage.readFileStream(this.filename, { encoding: 'utf8', mode: this.mode.fileMode })
treatedData = await this.treatRawStreamAsync(fileStream)
} else {
// Browser
- const rawData = await storage.readFileAsync(this.filename, 'utf8')
+ const rawData = await storage.readFileAsync(this.filename, { encoding: 'utf8', mode: this.mode.fileMode })
treatedData = this.treatRawData(rawData)
}
// Recreate all indexes in the datafile
@@ -357,11 +365,12 @@ class Persistence {
/**
* Check if a directory stat and create it on the fly if it is not the case.
* @param {string} dir
+ * @param {number} [mode=0o777]
* @return {Promise}
* @private
*/
- static async ensureDirectoryExistsAsync (dir) {
- await storage.mkdirAsync(dir, { recursive: true })
+ static async ensureDirectoryExistsAsync (dir, mode = DEFAULT_DIR_MODE) {
+ await storage.mkdirAsync(dir, { recursive: true, mode })
}
}
diff --git a/lib/storage.js b/lib/storage.js
index b6d72cc..32a9ca3 100755
--- a/lib/storage.js
+++ b/lib/storage.js
@@ -13,6 +13,9 @@ const fsPromises = fs.promises
const path = require('path')
const { Readable } = require('stream')
+const DEFAULT_DIR_MODE = 0o755
+const DEFAULT_FILE_MODE = 0o644
+
/**
* Returns true if file exists.
* @param {string} file
@@ -126,6 +129,7 @@ const ensureFileDoesntExistAsync = async file => {
* @param {object|string} options If options is a string, it is assumed that the flush of the file (not dir) called options was requested
* @param {string} [options.filename]
* @param {boolean} [options.isDir = false] Optional, defaults to false
+ * @param {number} [options.mode = 0o644] Optional, defaults to 0o644
* @return {Promise}
* @alias module:storage.flushToStorageAsync
* @async
@@ -133,14 +137,16 @@ const ensureFileDoesntExistAsync = async file => {
const flushToStorageAsync = async (options) => {
let filename
let flags
+ let mode
if (typeof options === 'string') {
filename = options
flags = 'r+'
+ mode = DEFAULT_FILE_MODE
} else {
filename = options.filename
flags = options.isDir ? 'r' : 'r+'
+ mode = options.mode || DEFAULT_FILE_MODE
}
-
/**
* Some OSes and/or storage backends (augmented node fs) do not support fsync (FlushFileBuffers) directories,
* or calling open() on directories at all. Flushing fails silently in this case, supported by following heuristics:
@@ -155,7 +161,7 @@ const flushToStorageAsync = async (options) => {
let filehandle, errorOnFsync, errorOnClose
try {
- filehandle = await fsPromises.open(filename, flags)
+ filehandle = await fsPromises.open(filename, flags, mode)
try {
await filehandle.sync()
} catch (errFS) {
@@ -182,13 +188,14 @@ const flushToStorageAsync = async (options) => {
* Fully write or rewrite the datafile.
* @param {string} filename
* @param {string[]} lines
+ * @param {number} [mode=0o644]
* @return {Promise}
* @alias module:storage.writeFileLinesAsync
* @async
*/
-const writeFileLinesAsync = (filename, lines) => new Promise((resolve, reject) => {
+const writeFileLinesAsync = (filename, lines, mode = DEFAULT_FILE_MODE) => new Promise((resolve, reject) => {
try {
- const stream = writeFileStream(filename)
+ const stream = writeFileStream(filename, { mode: mode })
const readable = Readable.from(lines)
readable.on('data', (line) => {
try {
@@ -220,33 +227,37 @@ const writeFileLinesAsync = (filename, lines) => new Promise((resolve, reject) =
* Fully write or rewrite the datafile, immune to crashes during the write operation (data will not be lost).
* @param {string} filename
* @param {string[]} lines
+ * @param {object} [mode={ fileMode: 0o644, dirMode: 0o755 }]
+ * @param {number} mode.dirMode
+ * @param {number} mode.fileMode
* @return {Promise}
* @alias module:storage.crashSafeWriteFileLinesAsync
*/
-const crashSafeWriteFileLinesAsync = async (filename, lines) => {
+const crashSafeWriteFileLinesAsync = async (filename, lines, mode = { fileMode: DEFAULT_FILE_MODE, dirMode: DEFAULT_DIR_MODE }) => {
const tempFilename = filename + '~'
- await flushToStorageAsync({ filename: path.dirname(filename), isDir: true })
+ await flushToStorageAsync({ filename: path.dirname(filename), isDir: true, mode: mode.dirMode })
const exists = await existsAsync(filename)
- if (exists) await flushToStorageAsync({ filename })
+ if (exists) await flushToStorageAsync({ filename, mode: mode.fileMode })
- await writeFileLinesAsync(tempFilename, lines)
+ await writeFileLinesAsync(tempFilename, lines, mode.fileMode)
- await flushToStorageAsync(tempFilename)
+ await flushToStorageAsync({ filename: tempFilename, mode: mode.fileMode })
await renameAsync(tempFilename, filename)
- await flushToStorageAsync({ filename: path.dirname(filename), isDir: true })
+ await flushToStorageAsync({ filename: path.dirname(filename), isDir: true, mode: mode.dirMode })
}
/**
* Ensure the datafile contains all the data, even if there was a crash during a full file write.
* @param {string} filename
+ * @param {number} [mode=0o644]
* @return {Promise}
* @alias module:storage.ensureDatafileIntegrityAsync
*/
-const ensureDatafileIntegrityAsync = async filename => {
+const ensureDatafileIntegrityAsync = async (filename, mode = DEFAULT_FILE_MODE) => {
const tempFilename = filename + '~'
const filenameExists = await existsAsync(filename)
@@ -255,7 +266,7 @@ const ensureDatafileIntegrityAsync = async filename => {
const oldFilenameExists = await existsAsync(tempFilename)
// New database
- if (!oldFilenameExists) await writeFileAsync(filename, '', 'utf8')
+ if (!oldFilenameExists) await writeFileAsync(filename, '', { encoding: 'utf8', mode })
// Write failed, use old version
else await renameAsync(tempFilename, filename)
}
diff --git a/test/persistence.async.test.js b/test/persistence.async.test.js
index c414a36..dc709d3 100755
--- a/test/persistence.async.test.js
+++ b/test/persistence.async.test.js
@@ -999,3 +999,81 @@ describe('Persistence async', function () {
})
}) // ==== End of 'dropDatabase' ====
})
+
+const getMode = async path => {
+ const { mode } = await fs.lstat(path)
+ const octalString = mode.toString('8')
+ return parseInt(octalString.substring(octalString.length - 4), '8')
+}
+
+const testPermissions = async (db, fileMode, dirMode) => {
+ assert.equal(db.persistence.mode.fileMode, fileMode)
+ assert.equal(db.persistence.mode.dirMode, dirMode)
+ await db.loadDatabaseAsync()
+ assert.equal(await getMode(db.filename), fileMode)
+ assert.equal(await getMode(path.dirname(db.filename)), dirMode)
+ await db.ensureIndex({ fieldName: 'foo' })
+ assert.equal(await getMode(db.filename), fileMode)
+ assert.equal(await getMode(path.dirname(db.filename)), dirMode)
+ await db.insertAsync({ hello: 'world' })
+ assert.equal(await getMode(db.filename), fileMode)
+ assert.equal(await getMode(path.dirname(db.filename)), dirMode)
+ await db.removeAsync({ hello: 'world' })
+ assert.equal(await getMode(db.filename), fileMode)
+ assert.equal(await getMode(path.dirname(db.filename)), dirMode)
+ await db.updateAsync({ hello: 'world2' })
+ assert.equal(await getMode(db.filename), fileMode)
+ assert.equal(await getMode(path.dirname(db.filename)), dirMode)
+ await db.removeIndex({ fieldName: 'foo' })
+ assert.equal(await getMode(db.filename), fileMode)
+ assert.equal(await getMode(path.dirname(db.filename)), dirMode)
+ await db.compactDatafileAsync()
+ assert.equal(await getMode(db.filename), fileMode)
+ assert.equal(await getMode(path.dirname(db.filename)), dirMode)
+}
+describe('permissions', function () {
+ const testDb = 'workspace/permissions/test.db'
+
+ beforeEach('cleanup', async () => {
+ try {
+ await fs.chmod(path.dirname(testDb), 0o755)
+ await fs.rm(testDb, { force: true })
+ await fs.rmdir(path.dirname(testDb, { recursive: true }))
+ } catch (err) {
+ if (err.code !== 'ENOENT') throw err
+ }
+ })
+
+ it('ensureDirectoryExists forwards mode argument', async () => {
+ await Persistence.ensureDirectoryExistsAsync(path.dirname(testDb), 0o700)
+ assert.equal(await getMode(path.dirname(testDb)), 0o700)
+ })
+
+ it('Setting nothing', async () => {
+ const FILE_MODE = 0o644
+ const DIR_MODE = 0o755
+ const db = new Datastore({ filename: testDb })
+ await testPermissions(db, FILE_MODE, DIR_MODE)
+ })
+
+ it('Setting only fileMode', async () => {
+ const FILE_MODE = 0o600
+ const DIR_MODE = 0o755
+ const db = new Datastore({ filename: testDb, mode: { fileMode: FILE_MODE } })
+ await testPermissions(db, FILE_MODE, DIR_MODE)
+ })
+
+ it('Setting only dirMode', async () => {
+ const FILE_MODE = 0o644
+ const DIR_MODE = 0o700
+ const db = new Datastore({ filename: testDb, mode: { dirMode: DIR_MODE } })
+ await testPermissions(db, FILE_MODE, DIR_MODE)
+ })
+
+ it('Setting fileMode & dirMode', async () => {
+ const FILE_MODE = 0o600
+ const DIR_MODE = 0o700
+ const db = new Datastore({ filename: testDb, mode: { dirMode: DIR_MODE, fileMode: FILE_MODE } })
+ await testPermissions(db, FILE_MODE, DIR_MODE)
+ })
+})