Socket
Socket
Sign inDemoInstall

atomically

Package Overview
Dependencies
Maintainers
1
Versions
14
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

atomically - npm Package Compare versions

Comparing version 1.0.0 to 1.1.0

dist/utils/fs_handlers.d.ts

6

dist/consts.d.ts
declare const DEFAULT_ENCODING = "utf8";
declare const DEFAULT_MODE = 438;
declare const DEFAULT_OPTIONS: {};
declare const DEFAULT_TIMEOUT_ASYNC = 5000;
declare const DEFAULT_TIMEOUT_SYNC = 100;
declare const IS_POSIX = true;
declare const IS_USER_ROOT: boolean;
declare const LIMIT_BASENAME_LENGTH = 128;
declare const NOOP: () => void;
export { DEFAULT_ENCODING, DEFAULT_MODE, IS_POSIX, IS_USER_ROOT, NOOP };
export { DEFAULT_ENCODING, DEFAULT_MODE, DEFAULT_OPTIONS, DEFAULT_TIMEOUT_ASYNC, DEFAULT_TIMEOUT_SYNC, IS_POSIX, IS_USER_ROOT, LIMIT_BASENAME_LENGTH, NOOP };
"use strict";
/* CONSTS */
Object.defineProperty(exports, "__esModule", { value: true });
exports.NOOP = exports.IS_USER_ROOT = exports.IS_POSIX = exports.DEFAULT_MODE = exports.DEFAULT_ENCODING = void 0;
exports.NOOP = exports.LIMIT_BASENAME_LENGTH = exports.IS_USER_ROOT = exports.IS_POSIX = exports.DEFAULT_TIMEOUT_SYNC = exports.DEFAULT_TIMEOUT_ASYNC = exports.DEFAULT_OPTIONS = exports.DEFAULT_MODE = exports.DEFAULT_ENCODING = void 0;
const DEFAULT_ENCODING = 'utf8';

@@ -9,2 +9,8 @@ exports.DEFAULT_ENCODING = DEFAULT_ENCODING;

exports.DEFAULT_MODE = DEFAULT_MODE;
const DEFAULT_OPTIONS = {};
exports.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
const DEFAULT_TIMEOUT_ASYNC = 5000;
exports.DEFAULT_TIMEOUT_ASYNC = DEFAULT_TIMEOUT_ASYNC;
const DEFAULT_TIMEOUT_SYNC = 100;
exports.DEFAULT_TIMEOUT_SYNC = DEFAULT_TIMEOUT_SYNC;
const IS_POSIX = !!process.getuid;

@@ -14,3 +20,5 @@ exports.IS_POSIX = IS_POSIX;

exports.IS_USER_ROOT = IS_USER_ROOT;
const LIMIT_BASENAME_LENGTH = 128; //TODO: fetch the real limit from the filesystem //TODO: fetch the whole-path length limit too
exports.LIMIT_BASENAME_LENGTH = LIMIT_BASENAME_LENGTH;
const NOOP = () => { };
exports.NOOP = NOOP;

18

dist/index.d.ts

@@ -1,14 +0,18 @@

import { Path, Data, Options, Callback } from './types';
import { Path, Data, Disposer, Options, Callback } from './types';
declare const writeFile: (filePath: Path, data: Data, options?: string | {
encoding?: string | null | undefined;
flag?: string | undefined;
mode?: string | number | undefined;
chown?: {
chown?: false | {
gid: number;
uid: number;
gid: number;
} | undefined;
encoding?: string | null | undefined;
fsync?: boolean | undefined;
tmpfileCreated?: ((filePath: string) => any) | undefined;
fsyncWait?: boolean | undefined;
mode?: string | number | false | undefined;
schedule?: ((filePath: string) => Promise<Disposer>) | undefined;
timeout?: number | undefined;
tmpCreate?: ((filePath: string) => string) | undefined;
tmpCreated?: ((filePath: string) => any) | undefined;
tmpPurge?: boolean | undefined;
} | Callback | undefined, callback?: Callback | undefined) => Promise<void>;
declare const writeFileSync: (filePath: Path, data: Data, options?: Options) => void;
export { writeFile, writeFileSync };

@@ -8,3 +8,3 @@ "use strict";

const lang_1 = require("./utils/lang");
const tasker_1 = require("./utils/tasker");
const scheduler_1 = require("./utils/scheduler");
const temp_1 = require("./utils/temp");

@@ -14,3 +14,3 @@ /* ATOMICALLY */

if (lang_1.default.isFunction(options))
return writeFile(filePath, data, {}, options);
return writeFile(filePath, data, consts_1.DEFAULT_OPTIONS, options);
const promise = writeFileAsync(filePath, data, options);

@@ -22,14 +22,18 @@ if (callback)

exports.writeFile = writeFile;
const writeFileAsync = async (filePath, data, options = {}) => {
const writeFileAsync = async (filePath, data, options = consts_1.DEFAULT_OPTIONS) => {
if (lang_1.default.isString(options))
return writeFileAsync(filePath, data, { encoding: options });
let taskDisposer = null, tempDisposer = null, tempPath = null, fd = null;
const timeout = Date.now() + (options.timeout || consts_1.DEFAULT_TIMEOUT_ASYNC);
let schedulerCustomDisposer = null, schedulerDisposer = null, tempDisposer = null, tempPath = null, fd = null;
try {
taskDisposer = await tasker_1.default.task(filePath);
filePath = await fs_1.default.realpath(filePath).catch(consts_1.NOOP) || filePath;
[tempPath, tempDisposer] = temp_1.default.get(filePath);
if (options.schedule)
schedulerCustomDisposer = await options.schedule(filePath);
schedulerDisposer = await scheduler_1.default.schedule(filePath);
filePath = await fs_1.default.realpathAttempt(filePath) || filePath;
[tempPath, tempDisposer] = temp_1.default.get(filePath, options.tmpCreate || temp_1.default.create, !(options.tmpPurge === false));
const useStatChown = consts_1.IS_POSIX && lang_1.default.isUndefined(options.chown), useStatMode = lang_1.default.isUndefined(options.mode);
if (useStatChown || useStatMode) {
const stat = await fs_1.default.stat(filePath).catch(consts_1.NOOP);
const stat = await fs_1.default.statAttempt(filePath);
if (stat) {
options = { ...options };
if (useStatChown)

@@ -41,20 +45,33 @@ options.chown = { uid: stat.uid, gid: stat.gid };

}
fd = await fs_1.default.open(tempPath, 'w', options.mode || consts_1.DEFAULT_MODE);
if (options.tmpfileCreated)
options.tmpfileCreated(tempPath);
fd = await fs_1.default.openRetry(timeout)(tempPath, 'w', options.mode || consts_1.DEFAULT_MODE);
if (options.tmpCreated)
options.tmpCreated(tempPath);
if (lang_1.default.isString(data)) {
await fs_1.default.write(fd, data, 0, options.encoding || consts_1.DEFAULT_ENCODING);
await fs_1.default.writeRetry(timeout)(fd, data, 0, options.encoding || consts_1.DEFAULT_ENCODING);
}
else if (!lang_1.default.isUndefined(data)) {
await fs_1.default.write(fd, data, 0, data.length, 0);
await fs_1.default.writeRetry(timeout)(fd, data, 0, data.length, 0);
}
if (options.fsync !== false)
await fs_1.default.fsync(fd);
await fs_1.default.close(fd);
if (options.fsync !== false) {
if (options.fsyncWait !== false) {
await fs_1.default.fsyncRetry(timeout)(fd);
}
else {
fs_1.default.fsyncAttempt(fd);
}
}
await fs_1.default.closeRetry(timeout)(fd);
fd = null;
if (options.chown)
await fs_1.default.chown(tempPath, options.chown.uid, options.chown.gid).catch(fs_1.default.onChownError);
await fs_1.default.chownAttempt(tempPath, options.chown.uid, options.chown.gid);
if (options.mode)
await fs_1.default.chmod(tempPath, options.mode).catch(fs_1.default.onChownError);
await fs_1.default.rename(tempPath, filePath);
await fs_1.default.chmodAttempt(tempPath, options.mode);
try {
await fs_1.default.renameRetry(timeout)(tempPath, filePath);
}
catch (error) {
if (error.code !== 'ENAMETOOLONG')
throw error;
await fs_1.default.renameRetry(timeout)(tempPath, temp_1.default.truncate(filePath));
}
tempDisposer();

@@ -65,20 +82,24 @@ tempPath = null;

if (fd)
await fs_1.default.close(fd).catch(consts_1.NOOP);
await fs_1.default.closeAttempt(fd);
if (tempPath)
temp_1.default.purge(tempPath);
if (taskDisposer)
taskDisposer();
if (schedulerCustomDisposer)
schedulerCustomDisposer();
if (schedulerDisposer)
schedulerDisposer();
}
};
const writeFileSync = (filePath, data, options = {}) => {
const writeFileSync = (filePath, data, options = consts_1.DEFAULT_OPTIONS) => {
if (lang_1.default.isString(options))
return writeFileSync(filePath, data, { encoding: options });
const timeout = Date.now() + (options.timeout || consts_1.DEFAULT_TIMEOUT_SYNC);
let tempDisposer = null, tempPath = null, fd = null;
try {
filePath = fs_1.default.realpathSync(filePath) || filePath;
[tempPath, tempDisposer] = temp_1.default.get(filePath);
filePath = fs_1.default.realpathSyncAttempt(filePath) || filePath;
[tempPath, tempDisposer] = temp_1.default.get(filePath, options.tmpCreate || temp_1.default.create, !(options.tmpPurge === false));
const useStatChown = consts_1.IS_POSIX && lang_1.default.isUndefined(options.chown), useStatMode = lang_1.default.isUndefined(options.mode);
if (useStatChown || useStatMode) {
const stat = fs_1.default.statSync(filePath);
const stat = fs_1.default.statSyncAttempt(filePath);
if (stat) {
options = { ...options };
if (useStatChown)

@@ -90,20 +111,33 @@ options.chown = { uid: stat.uid, gid: stat.gid };

}
fd = fs_1.default.openSync(tempPath, 'w', options.mode || consts_1.DEFAULT_MODE);
if (options.tmpfileCreated)
options.tmpfileCreated(tempPath);
fd = fs_1.default.openSyncRetry(timeout)(tempPath, 'w', options.mode || consts_1.DEFAULT_MODE);
if (options.tmpCreated)
options.tmpCreated(tempPath);
if (lang_1.default.isString(data)) {
fs_1.default.writeSync(fd, data, 0, options.encoding || consts_1.DEFAULT_ENCODING);
fs_1.default.writeSyncRetry(timeout)(fd, data, 0, options.encoding || consts_1.DEFAULT_ENCODING);
}
else if (!lang_1.default.isUndefined(data)) {
fs_1.default.writeSync(fd, data, 0, data.length, 0);
fs_1.default.writeSyncRetry(timeout)(fd, data, 0, data.length, 0);
}
if (options.fsync !== false)
fs_1.default.fsyncSync(fd);
fs_1.default.closeSync(fd);
if (options.fsync !== false) {
if (options.fsyncWait !== false) {
fs_1.default.fsyncSyncRetry(timeout)(fd);
}
else {
fs_1.default.fsyncAttempt(fd);
}
}
fs_1.default.closeSyncRetry(timeout)(fd);
fd = null;
if (options.chown)
fs_1.default.chownSync(tempPath, options.chown.uid, options.chown.gid);
fs_1.default.chownSyncAttempt(tempPath, options.chown.uid, options.chown.gid);
if (options.mode)
fs_1.default.chmodSync(tempPath, options.mode);
fs_1.default.renameSync(tempPath, filePath);
fs_1.default.chmodSyncAttempt(tempPath, options.mode);
try {
fs_1.default.renameSyncRetry(timeout)(tempPath, filePath);
}
catch (error) {
if (error.code !== 'ENAMETOOLONG')
throw error;
fs_1.default.renameSyncRetry(timeout)(tempPath, temp_1.default.truncate(filePath));
}
tempDisposer();

@@ -114,3 +148,3 @@ tempPath = null;

if (fd)
fs_1.default.closeSyncLoose(fd);
fs_1.default.closeSyncAttempt(fd);
if (tempPath)

@@ -117,0 +151,0 @@ temp_1.default.purge(tempPath);

/// <reference types="node" />
declare type Callback = (err: Exception | void) => any;
declare type Callback = (error: Exception | void) => any;
declare type Data = Buffer | string | undefined;
declare type Disposer = () => void;
declare type Exception = NodeJS.ErrnoException;
declare type FN<Arguments extends any[] = any[], Return = any> = (...args: Arguments) => Return;
declare type Options = string | {
encoding?: string | null;
flag?: string;
mode?: string | number;
chown?: {
gid: number;
uid: number;
gid: number;
};
} | false;
encoding?: string | null;
fsync?: boolean;
tmpfileCreated?: (filePath: string) => any;
fsyncWait?: boolean;
mode?: string | number | false;
schedule?: (filePath: string) => Promise<Disposer>;
timeout?: number;
tmpCreate?: (filePath: string) => string;
tmpCreated?: (filePath: string) => any;
tmpPurge?: boolean;
};
declare type Path = string;
export { Callback, Data, Disposer, Exception, Options, Path };
export { Callback, Data, Disposer, Exception, FN, Options, Path };

@@ -1,3 +0,4 @@

import { Exception } from '../types';
declare const attemptify: <FN extends (...args: any[]) => any>(fn: FN, handler?: ((error: Exception) => any) | undefined) => FN;
export default attemptify;
import { Exception, FN } from '../types';
declare const attemptifyAsync: <T extends FN<any[], any>>(fn: T, onError?: FN<[Exception]>) => T;
declare const attemptifySync: <T extends FN<any[], any>>(fn: T, onError?: FN<[Exception]>) => T;
export { attemptifyAsync, attemptifySync };
"use strict";
/* IMPORT */
Object.defineProperty(exports, "__esModule", { value: true });
exports.attemptifySync = exports.attemptifyAsync = void 0;
const consts_1 = require("../consts");
/* ATTEMPTIFY */
const attemptify = (fn, handler) => {
return function attemptWrapper() {
//TODO: Maybe publish this as a standalone package
//FIXME: The type castings here aren't exactly correct
const attemptifyAsync = (fn, onError = consts_1.NOOP) => {
return function () {
return fn.apply(undefined, arguments).catch(onError);
};
};
exports.attemptifyAsync = attemptifyAsync;
const attemptifySync = (fn, onError = consts_1.NOOP) => {
return function () {
try {

@@ -11,8 +21,6 @@ return fn.apply(undefined, arguments);

catch (error) {
if (handler)
handler(error);
return onError(error);
}
};
};
/* EXPORT */
exports.default = attemptify;
exports.attemptifySync = attemptifySync;
/// <reference types="node" />
import * as fs from 'fs';
import { Exception } from '../types';
declare const FS: {
chmod: typeof fs.chmod.__promisify__;
chown: typeof fs.chown.__promisify__;
close: typeof fs.close.__promisify__;
fsync: typeof fs.fsync.__promisify__;
open: typeof fs.open.__promisify__;
realpath: typeof fs.realpath.__promisify__;
rename: typeof fs.rename.__promisify__;
stat: typeof fs.stat.__promisify__;
write: typeof fs.write.__promisify__;
chmodSync: typeof fs.chmodSync;
chownSync: typeof fs.chownSync;
closeSync: typeof fs.closeSync;
closeSyncLoose: typeof fs.closeSync;
fsyncSync: typeof fs.fsyncSync;
openSync: typeof fs.openSync;
realpathSync: typeof fs.realpathSync;
renameSync: typeof fs.renameSync;
statSync: typeof fs.statSync;
writeSync: typeof fs.writeSync;
onChownError: (error: Exception) => void;
chmodAttempt: typeof fs.chmod.__promisify__;
chownAttempt: typeof fs.chown.__promisify__;
closeAttempt: typeof fs.close.__promisify__;
fsyncAttempt: typeof fs.fsync.__promisify__;
realpathAttempt: typeof fs.realpath.__promisify__;
statAttempt: typeof fs.stat.__promisify__;
unlinkAttempt: typeof fs.unlink.__promisify__;
closeRetry: import("../types").FN<[number], typeof fs.close.__promisify__>;
fsyncRetry: import("../types").FN<[number], typeof fs.fsync.__promisify__>;
openRetry: import("../types").FN<[number], typeof fs.open.__promisify__>;
renameRetry: import("../types").FN<[number], typeof fs.rename.__promisify__>;
writeRetry: import("../types").FN<[number], typeof fs.write.__promisify__>;
chmodSyncAttempt: typeof fs.chmodSync;
chownSyncAttempt: typeof fs.chownSync;
closeSyncAttempt: typeof fs.closeSync;
realpathSyncAttempt: typeof fs.realpathSync;
statSyncAttempt: typeof fs.statSync;
unlinkSyncAttempt: typeof fs.unlinkSync;
closeSyncRetry: import("../types").FN<[number], typeof fs.closeSync>;
fsyncSyncRetry: import("../types").FN<[number], typeof fs.fsyncSync>;
openSyncRetry: import("../types").FN<[number], typeof fs.openSync>;
renameSyncRetry: import("../types").FN<[number], typeof fs.renameSync>;
writeSyncRetry: import("../types").FN<[number], typeof fs.writeSync>;
};
export default FS;

@@ -6,36 +6,32 @@ "use strict";

const util_1 = require("util");
const consts_1 = require("../consts");
const attemptify_1 = require("./attemptify");
const fs_handlers_1 = require("./fs_handlers");
const retryify_1 = require("./retryify");
/* FS */
const onChownError = (error) => {
const { code } = error;
if (code === 'ENOSYS')
return;
if (!consts_1.IS_USER_ROOT && (code === 'EINVAL' || code === 'EPERM'))
return;
throw error;
};
const FS = {
chmod: util_1.promisify(fs.chmod),
chown: util_1.promisify(fs.chown),
close: util_1.promisify(fs.close),
fsync: util_1.promisify(fs.fsync),
open: util_1.promisify(fs.open),
realpath: util_1.promisify(fs.realpath),
rename: util_1.promisify(fs.rename),
stat: util_1.promisify(fs.stat),
write: util_1.promisify(fs.write),
chmodSync: attemptify_1.default(fs.chmodSync, onChownError),
chownSync: attemptify_1.default(fs.chownSync, onChownError),
closeSync: fs.closeSync,
closeSyncLoose: attemptify_1.default(fs.closeSync),
fsyncSync: fs.fsyncSync,
openSync: fs.openSync,
realpathSync: attemptify_1.default(fs.realpathSync),
renameSync: fs.renameSync,
statSync: attemptify_1.default(fs.statSync),
writeSync: fs.writeSync,
onChownError
chmodAttempt: attemptify_1.attemptifyAsync(util_1.promisify(fs.chmod), fs_handlers_1.default.onChangeError),
chownAttempt: attemptify_1.attemptifyAsync(util_1.promisify(fs.chown), fs_handlers_1.default.onChangeError),
closeAttempt: attemptify_1.attemptifyAsync(util_1.promisify(fs.close)),
fsyncAttempt: attemptify_1.attemptifyAsync(util_1.promisify(fs.fsync)),
realpathAttempt: attemptify_1.attemptifyAsync(util_1.promisify(fs.realpath)),
statAttempt: attemptify_1.attemptifyAsync(util_1.promisify(fs.stat)),
unlinkAttempt: attemptify_1.attemptifyAsync(util_1.promisify(fs.unlink)),
closeRetry: retryify_1.retryifyAsync(util_1.promisify(fs.close), fs_handlers_1.default.isRetriableError),
fsyncRetry: retryify_1.retryifyAsync(util_1.promisify(fs.fsync), fs_handlers_1.default.isRetriableError),
openRetry: retryify_1.retryifyAsync(util_1.promisify(fs.open), fs_handlers_1.default.isRetriableError),
renameRetry: retryify_1.retryifyAsync(util_1.promisify(fs.rename), fs_handlers_1.default.isRetriableError),
writeRetry: retryify_1.retryifyAsync(util_1.promisify(fs.write), fs_handlers_1.default.isRetriableError),
chmodSyncAttempt: attemptify_1.attemptifySync(fs.chmodSync, fs_handlers_1.default.onChangeError),
chownSyncAttempt: attemptify_1.attemptifySync(fs.chownSync, fs_handlers_1.default.onChangeError),
closeSyncAttempt: attemptify_1.attemptifySync(fs.closeSync),
realpathSyncAttempt: attemptify_1.attemptifySync(fs.realpathSync),
statSyncAttempt: attemptify_1.attemptifySync(fs.statSync),
unlinkSyncAttempt: attemptify_1.attemptifySync(fs.unlinkSync),
closeSyncRetry: retryify_1.retryifySync(fs.closeSync, fs_handlers_1.default.isRetriableError),
fsyncSyncRetry: retryify_1.retryifySync(fs.fsyncSync, fs_handlers_1.default.isRetriableError),
openSyncRetry: retryify_1.retryifySync(fs.openSync, fs_handlers_1.default.isRetriableError),
renameSyncRetry: retryify_1.retryifySync(fs.renameSync, fs_handlers_1.default.isRetriableError),
writeSyncRetry: retryify_1.retryifySync(fs.writeSync, fs_handlers_1.default.isRetriableError)
};
/* EXPORT */
exports.default = FS;
declare const Lang: {
isFunction: (x: any) => x is Function;
isNil: (x: any) => x is null | undefined;
isString: (x: any) => x is string;

@@ -5,0 +4,0 @@ isUndefined: (x: any) => x is undefined;

@@ -8,5 +8,2 @@ "use strict";

},
isNil: (x) => {
return x == null;
},
isString: (x) => {

@@ -13,0 +10,0 @@ return typeof x === 'string';

import { Disposer } from '../types';
declare const Temp: {
store: Record<string, boolean>;
get: (filePath: string, purge?: boolean) => [string, Disposer];
create: (filePath: string) => string;
get: (filePath: string, creator: (filePath: string) => string, purge?: boolean) => [string, Disposer];
purge: (filePath: string) => void;
purgeAll: () => void;
purgeSync: (filePath: string) => void;
purgeSyncAll: () => void;
truncate: (filePath: string) => string;
};
export default Temp;

@@ -5,4 +5,5 @@ "use strict";

const crypto = require("crypto");
const fs = require("fs");
const path = require("path");
const consts_1 = require("../consts");
const fs_1 = require("./fs");
/* TEMP */

@@ -12,8 +13,12 @@ //TODO: Maybe publish this as a standalone package

store: {},
get: (filePath, purge = true) => {
create: (filePath) => {
const hash = crypto.randomBytes(3).toString('hex'), // 6 random hex characters
timestamp = Date.now().toString().slice(-10), // 10 precise timestamp digits
prefix = 'tmp-', suffix = `.${prefix}${timestamp}${hash}`, tempPath = `${filePath}${suffix}`;
return tempPath;
},
get: (filePath, creator, purge = true) => {
const tempPath = Temp.truncate(creator(filePath));
if (tempPath in Temp.store)
return Temp.get(filePath); // Collision found, try again
return Temp.get(filePath, creator, purge); // Collision found, try again
Temp.store[tempPath] = purge;

@@ -24,20 +29,32 @@ const disposer = () => delete Temp.store[tempPath];

purge: (filePath) => {
if (!Temp.store[filePath])
return;
delete Temp.store[filePath];
fs.unlink(filePath, consts_1.NOOP);
fs_1.default.unlinkAttempt(filePath);
},
purgeAll: () => {
purgeSync: (filePath) => {
if (!Temp.store[filePath])
return;
delete Temp.store[filePath];
fs_1.default.unlinkSyncAttempt(filePath);
},
purgeSyncAll: () => {
for (const filePath in Temp.store) {
if (!Temp.store[filePath])
continue;
delete Temp.store[filePath];
try {
fs.unlinkSync(filePath);
}
catch (_a) { }
Temp.purgeSync(filePath);
}
},
truncate: (filePath) => {
const basename = path.basename(filePath);
if (basename.length <= consts_1.LIMIT_BASENAME_LENGTH)
return filePath; //FIXME: Rough and quick attempt at detecting ok lengths
const truncable = /^(\.?)(.*?)((?:\.[^.]+)?(?:\.tmp-\d{10}[a-f0-9]{6})?)$/.exec(basename);
if (!truncable)
return filePath; //FIXME: No truncable part detected, can't really do much without also changing the parent path, which is unsafe, hoping for the best here
const truncationLength = basename.length - consts_1.LIMIT_BASENAME_LENGTH;
return `${filePath.slice(0, -basename.length)}${truncable[1]}${truncable[2].slice(0, -truncationLength)}${truncable[3]}`; //FIXME: The truncable part might be shorter than needed here
}
};
/* INIT */
process.on('exit', Temp.purgeAll); // Ensuring purgeable temp files are purged on exit
process.on('exit', Temp.purgeSyncAll); // Ensuring purgeable temp files are purged on exit
/* EXPORT */
exports.default = Temp;
{
"name": "atomically",
"description": "Write files atomically and reliably.",
"version": "1.0.0",
"version": "1.1.0",
"main": "dist/index.js",

@@ -41,2 +41,3 @@ "types": "dist/index.d.ts",

"mkdirp": "^1.0.4",
"promise-resolve-timeout": "^1.2.1",
"require-inject": "^1.4.4",

@@ -43,0 +44,0 @@ "rimraf": "^3.0.2",

@@ -7,3 +7,55 @@ # Atomically

//TODO
- Overview:
- This library is a rewrite of [`write-file-atomic`](https://github.com/npm/write-file-atomic), with some important enhancements on top, you can largely use this as a drop-in replacement.
- This library is written in TypeScript, so types aren't an afterthought but come with library.
- This library is slightly faster than [`write-file-atomic`](https://github.com/npm/write-file-atomic), and it can be 10x faster, while being essentially just as safe, by using the `fsyncWait` option.
- This library has 0 dependencies, so there's less code to vet and the entire thing is roughly 30% smaller than [`write-file-atomic`](https://github.com/npm/write-file-atomic).
- This library tries harder to write files on disk than [`write-file-atomic`](https://github.com/npm/write-file-atomic) does, by default retrying some failed operations and handling some more errors.
- Reliability:
- Writes are atomic, meaning that first a temporary file containing the new content is written, then this file is renamed to the final path, this way it's impossible to get a corrupt/partially-written file.
- Writes happening to the same path are queued, ensuring they don't interfere with each other.
- Temporary files can be configured to not be purged from disk if the write operation fails, which is useful for when keeping the temporary file is better than just losing data.
- Symlinks are resolved automatically.
- `ENOSYS` errors on `chmod`/`chown` operations are ignored.
- `EINVAL`/`EPERM` errors on `chmod`/`chown` operations, in POSIX systems where the user is not root, are ignored.
- `EMFILE`/`ENFILE`/`EAGAIN`/`EBUSY`/`EACCESS`/`EACCS`/`EPERM` errors happening during necessary operations are caught and the operations are retried until they succeed or the timeout is reached.
- `ENAMETOOLONG` errors, both appening because of the final path or the temporary path, are attempted to be worked around by smartly truncating paths.
- Temporary files:
- By default they are purged automatically once the write operation is completed or if the process exits (cleanly or not).
- By default they are created by appending a `.tmp-[timestamp][randomness]` suffix to destination paths:
- The `tmp-` part gives users a hint about the nature of these files, if they happen to see them.
- The `[timestamp]` part consists of the 10 least significant digits of a milliseconds-precise timestamp, making it likely that if more than one of these files are kept on disk the user will see them in chronological order.
- The `[randomness]` part consists of 6 random hex characters.
- If by any chance a collision is found then another suffix is generated.
- Custom options:
- `chown`: it allows you to specify custom group and user ids:
- by default the old file's ids are copied over.
- if custom ids are provided they will be used.
- if `false` the default ids are used.
- `encoding`: it allows you to specify the encoding of the file content:
- by default `utf8` is used.
- `fsync`: it allows you to control whether the `fsync` syscall is triggered right after writing the file or not:
- by default the syscall is triggered immediately after writing the file, increasing the chances that the file will actually be written to disk in case of imminent catastrophic failures, like power outages.
- if `false` the syscall won't be triggered.
- `fsyncWait`: it allows you to control whether the triggered `fsync` is waited or not:
- by default the syscall is waited.
- if `false` the syscall will still be triggered but not be waited.
- this increases performance 10x in some cases, and at the end of the day often there's no plan B if `fsync` fails anyway.
- `mode`: it allows you to specify the mode for the file:
- by default the old file's mode is copied over.
- if `false` then `0o666` is used.
- `schedule`: it's a function that returns a promise that resolves to a disposer function, basically it allows you to provide some custom queueing logic for the writing operation, allowing you to perhaps wire `atomically` with your app's main filesystem job scheduler:
- even when a custom `schedule` function is provided write operations will still be queued internally by the library too.
- `timeout`: it allows you to specify the amount of maximum milliseconds within which the library will retry some failed operations:
- when writing asynchronously by default it will keep retrying for 5000 milliseconds.
- when writing synchronously by default it will keep retrying for 100 milliseconds.
- if `0` or `-1` no failed operations will be retried.
- if another number is provided that will be the timeout interval.
- `tmpCreate`: it's a function that will be used to create the custom temporary file path in place of the default one:
- even when a custom function is provided the final temporary path will still be truncated if the library thinks that it may lead to `ENAMETOOLONG` errors.
- paths by default are truncated in a way that preserves an eventual existing leading dot and trailing extension.
- `tmpCreated`: it's a function that will be called with the newly created temporary file path.
- `tmpPurge`: it allows you to control whether the temporary file will be purged from the filesystem or not if the write fails:
- by default it will be purged.
- if `false` it will be kept on disk.

@@ -18,6 +70,59 @@ ## Install

This is the shape of the optional options object:
```ts
type Disposer = () => void;
type Options = string | {
chown?: { gid: number, uid: number } | false,
encoding?: string | null,
fsync?: boolean,
fsyncWait?: boolean,
mode?: string | number | false,
schedule?: ( filePath: string ) => Promise<Disposer>,
timeout?: number,
tmpCreate?: ( filePath: string ) => string,
tmpCreated?: ( filePath: string ) => any,
tmpPurge?: boolean
};
```
This is the shape of the provided functions:
```ts
function writeFile ( filePath: string, data: Buffer | string | undefined, options?: Options ): Promise<void>;
function writeFileSync ( filePath: string, data: Buffer | string | undefined, options?: Options ): void;
```
This is how to use the library:
```ts
import {writeFile, writeFileSync} from 'atomically';
//TODO
// Asynchronous write with default options
await writeFile ( '/foo.txt', 'my_data' );
// Asynchronous write that doesn't prod the old file for a stat object at all
await writeFile ( '/foo.txt', 'my_data', { chown: false, mode: false } );
// 10x faster asynchronous write that's less resilient against imminent catastrophies
await writeFile ( '/foo.txt', 'my_data', { fsync: false } );
// 10x faster asynchronous write that's essentially still as resilient against imminent catastrophies
await writeFile ( '/foo.txt', 'my_data', { fsyncWait: false } );
// Asynchronous write with a custom schedule function
await writeFile ( '/foo.txt', 'my_data', {
schedule: filePath => {
return new Promise ( resolve => { // When this returned promise will resolve the write operation will begin
MyScheduler.schedule ( filePath, () => { // Hypothetical scheduler function that will eventually tell us to go on with this write operation
const disposer = () => {}; // Hypothetical function that contains eventual clean-up logic, it will be called after the write operation has been completed (successfully or not)
resolve ( disposer ); // Resolving the promise with a disposer, beginning the write operation
})
});
}
});
// Synchronous write with default options
writeFileSync ( '/foo.txt', 'my_data' );
```

@@ -24,0 +129,0 @@

@@ -8,2 +8,8 @@

const DEFAULT_OPTIONS = {};
const DEFAULT_TIMEOUT_ASYNC = 5000;
const DEFAULT_TIMEOUT_SYNC = 100;
const IS_POSIX = !!process.getuid;

@@ -13,2 +19,4 @@

const LIMIT_BASENAME_LENGTH = 128; //TODO: fetch the real limit from the filesystem //TODO: fetch the whole-path length limit too
const NOOP = () => {};

@@ -18,2 +26,2 @@

export {DEFAULT_ENCODING, DEFAULT_MODE, IS_POSIX, IS_USER_ROOT, NOOP};
export {DEFAULT_ENCODING, DEFAULT_MODE, DEFAULT_OPTIONS, DEFAULT_TIMEOUT_ASYNC, DEFAULT_TIMEOUT_SYNC, IS_POSIX, IS_USER_ROOT, LIMIT_BASENAME_LENGTH, NOOP};
/* IMPORT */
import {DEFAULT_ENCODING, DEFAULT_MODE, IS_POSIX, NOOP} from './consts';
import {DEFAULT_ENCODING, DEFAULT_MODE, DEFAULT_OPTIONS, DEFAULT_TIMEOUT_ASYNC, DEFAULT_TIMEOUT_SYNC, IS_POSIX} from './consts';
import FS from './utils/fs';
import Lang from './utils/lang';
import Tasker from './utils/tasker';
import Scheduler from './utils/scheduler';
import Temp from './utils/temp';

@@ -15,3 +15,3 @@ import {Path, Data, Disposer, Options, Callback} from './types';

if ( Lang.isFunction ( options ) ) return writeFile ( filePath, data, {}, options );
if ( Lang.isFunction ( options ) ) return writeFile ( filePath, data, DEFAULT_OPTIONS, options );

@@ -26,7 +26,10 @@ const promise = writeFileAsync ( filePath, data, options );

const writeFileAsync = async ( filePath: Path, data: Data, options: Options = {} ): Promise<void> => {
const writeFileAsync = async ( filePath: Path, data: Data, options: Options = DEFAULT_OPTIONS ): Promise<void> => {
if ( Lang.isString ( options ) ) return writeFileAsync ( filePath, data, { encoding: options } );
let taskDisposer: Disposer | null = null,
const timeout = Date.now () + ( options.timeout || DEFAULT_TIMEOUT_ASYNC );
let schedulerCustomDisposer: Disposer | null = null,
schedulerDisposer: Disposer | null = null,
tempDisposer: Disposer | null = null,

@@ -38,8 +41,10 @@ tempPath: string | null = null,

taskDisposer = await Tasker.task ( filePath );
if ( options.schedule ) schedulerCustomDisposer = await options.schedule ( filePath );
filePath = await FS.realpath ( filePath ).catch ( NOOP ) || filePath;
schedulerDisposer = await Scheduler.schedule ( filePath );
[tempPath, tempDisposer] = Temp.get ( filePath );
filePath = await FS.realpathAttempt ( filePath ) || filePath;
[tempPath, tempDisposer] = Temp.get ( filePath, options.tmpCreate || Temp.create, !( options.tmpPurge === false ) );
const useStatChown = IS_POSIX && Lang.isUndefined ( options.chown ),

@@ -50,6 +55,8 @@ useStatMode = Lang.isUndefined ( options.mode );

const stat = await FS.stat ( filePath ).catch ( NOOP );
const stat = await FS.statAttempt ( filePath );
if ( stat ) {
options = { ...options };
if ( useStatChown ) options.chown = { uid: stat.uid, gid: stat.gid };

@@ -63,28 +70,50 @@

fd = await FS.open ( tempPath, 'w', options.mode || DEFAULT_MODE );
fd = await FS.openRetry ( timeout )( tempPath, 'w', options.mode || DEFAULT_MODE );
if ( options.tmpfileCreated ) options.tmpfileCreated ( tempPath );
if ( options.tmpCreated ) options.tmpCreated ( tempPath );
if ( Lang.isString ( data ) ) {
await FS.write ( fd, data, 0, options.encoding || DEFAULT_ENCODING );
await FS.writeRetry ( timeout )( fd, data, 0, options.encoding || DEFAULT_ENCODING );
} else if ( !Lang.isUndefined ( data ) ) {
await FS.write ( fd, data, 0, data.length, 0 );
await FS.writeRetry ( timeout )( fd, data, 0, data.length, 0 );
}
if ( options.fsync !== false ) await FS.fsync ( fd );
if ( options.fsync !== false ) {
await FS.close ( fd );
if ( options.fsyncWait !== false ) {
await FS.fsyncRetry ( timeout )( fd );
} else {
FS.fsyncAttempt ( fd );
}
}
await FS.closeRetry ( timeout )( fd );
fd = null;
if ( options.chown ) await FS.chown ( tempPath, options.chown.uid, options.chown.gid ).catch ( FS.onChownError );
if ( options.chown ) await FS.chownAttempt ( tempPath, options.chown.uid, options.chown.gid );
if ( options.mode ) await FS.chmod ( tempPath, options.mode ).catch ( FS.onChownError );
if ( options.mode ) await FS.chmodAttempt ( tempPath, options.mode );
await FS.rename ( tempPath, filePath );
try {
await FS.renameRetry ( timeout )( tempPath, filePath );
} catch ( error ) {
if ( error.code !== 'ENAMETOOLONG' ) throw error;
await FS.renameRetry ( timeout )( tempPath, Temp.truncate ( filePath ) );
}
tempDisposer ();

@@ -96,8 +125,10 @@

if ( fd ) await FS.close ( fd ).catch ( NOOP );
if ( fd ) await FS.closeAttempt ( fd );
if ( tempPath ) Temp.purge ( tempPath );
if ( taskDisposer ) taskDisposer ();
if ( schedulerCustomDisposer ) schedulerCustomDisposer ();
if ( schedulerDisposer ) schedulerDisposer ();
}

@@ -107,6 +138,8 @@

const writeFileSync = ( filePath: Path, data: Data, options: Options = {} ): void => {
const writeFileSync = ( filePath: Path, data: Data, options: Options = DEFAULT_OPTIONS ): void => {
if ( Lang.isString ( options ) ) return writeFileSync ( filePath, data, { encoding: options } );
const timeout = Date.now () + ( options.timeout || DEFAULT_TIMEOUT_SYNC );
let tempDisposer: Disposer | null = null,

@@ -118,5 +151,5 @@ tempPath: string | null = null,

filePath = FS.realpathSync ( filePath ) || filePath;
filePath = FS.realpathSyncAttempt ( filePath ) || filePath;
[tempPath, tempDisposer] = Temp.get ( filePath );
[tempPath, tempDisposer] = Temp.get ( filePath, options.tmpCreate || Temp.create, !( options.tmpPurge === false ) );

@@ -128,6 +161,8 @@ const useStatChown = IS_POSIX && Lang.isUndefined ( options.chown ),

const stat = FS.statSync ( filePath );
const stat = FS.statSyncAttempt ( filePath );
if ( stat ) {
options = { ...options };
if ( useStatChown ) options.chown = { uid: stat.uid, gid: stat.gid };

@@ -141,28 +176,50 @@

fd = FS.openSync ( tempPath, 'w', options.mode || DEFAULT_MODE );
fd = FS.openSyncRetry ( timeout )( tempPath, 'w', options.mode || DEFAULT_MODE );
if ( options.tmpfileCreated ) options.tmpfileCreated ( tempPath );
if ( options.tmpCreated ) options.tmpCreated ( tempPath );
if ( Lang.isString ( data ) ) {
FS.writeSync ( fd, data, 0, options.encoding || DEFAULT_ENCODING );
FS.writeSyncRetry ( timeout )( fd, data, 0, options.encoding || DEFAULT_ENCODING );
} else if ( !Lang.isUndefined ( data ) ) {
FS.writeSync ( fd, data, 0, data.length, 0 );
FS.writeSyncRetry ( timeout )( fd, data, 0, data.length, 0 );
}
if ( options.fsync !== false ) FS.fsyncSync ( fd );
if ( options.fsync !== false ) {
FS.closeSync ( fd );
if ( options.fsyncWait !== false ) {
FS.fsyncSyncRetry ( timeout )( fd );
} else {
FS.fsyncAttempt ( fd );
}
}
FS.closeSyncRetry ( timeout )( fd );
fd = null;
if ( options.chown ) FS.chownSync ( tempPath, options.chown.uid, options.chown.gid );
if ( options.chown ) FS.chownSyncAttempt ( tempPath, options.chown.uid, options.chown.gid );
if ( options.mode ) FS.chmodSync ( tempPath, options.mode );
if ( options.mode ) FS.chmodSyncAttempt ( tempPath, options.mode );
FS.renameSync ( tempPath, filePath );
try {
FS.renameSyncRetry ( timeout )( tempPath, filePath );
} catch ( error ) {
if ( error.code !== 'ENAMETOOLONG' ) throw error;
FS.renameSyncRetry ( timeout )( tempPath, Temp.truncate ( filePath ) );
}
tempDisposer ();

@@ -174,3 +231,3 @@

if ( fd ) FS.closeSyncLoose ( fd );
if ( fd ) FS.closeSyncAttempt ( fd );

@@ -177,0 +234,0 @@ if ( tempPath ) Temp.purge ( tempPath );

/* TYPES */
type Callback = ( err: Exception | void ) => any;
type Callback = ( error: Exception | void ) => any;

@@ -12,14 +12,15 @@ type Data = Buffer | string | undefined;

type FN<Arguments extends any[] = any[], Return = any> = ( ...args: Arguments ) => Return;
type Options = string | {
/* BUILT-INS */
chown?: { gid: number, uid: number } | false,
encoding?: string | null,
flag?: string,
mode?: string | number,
/* EXTRAS */ //TODO
chown?: {
uid: number,
gid: number
},
fsync?: boolean,
tmpfileCreated?: ( filePath: string ) => any
fsyncWait?: boolean,
mode?: string | number | false,
schedule?: ( filePath: string ) => Promise<Disposer>,
timeout?: number,
tmpCreate?: ( filePath: string ) => string,
tmpCreated?: ( filePath: string ) => any,
tmpPurge?: boolean
};

@@ -31,2 +32,2 @@

export {Callback, Data, Disposer, Exception, Options, Path};
export {Callback, Data, Disposer, Exception, FN, Options, Path};
/* IMPORT */
import {Exception} from '../types';
import {NOOP} from '../consts';
import {Exception, FN} from '../types';
/* ATTEMPTIFY */
const attemptify = <FN extends ( ...args: any[] ) => any> ( fn: FN, handler?: ( error: Exception ) => any ): FN => {
//TODO: Maybe publish this as a standalone package
//FIXME: The type castings here aren't exactly correct
return function attemptWrapper () {
const attemptifyAsync = <T extends FN> ( fn: T, onError: FN<[Exception]> = NOOP ): T => {
return function () {
return fn.apply ( undefined, arguments ).catch ( onError );
} as T;
};
const attemptifySync = <T extends FN> ( fn: T, onError: FN<[Exception]> = NOOP ): T => {
return function () {
try {

@@ -18,7 +32,7 @@

if ( handler ) handler ( error );
return onError ( error );
}
} as FN;
} as T;

@@ -29,2 +43,2 @@ };

export default attemptify;
export {attemptifyAsync, attemptifySync};

@@ -6,49 +6,41 @@

import {promisify} from 'util';
import {IS_USER_ROOT} from '../consts';
import {Exception} from '../types';
import attemptify from './attemptify';
import {attemptifyAsync, attemptifySync} from './attemptify';
import Handlers from './fs_handlers';
import {retryifyAsync, retryifySync} from './retryify';
/* FS */
const onChownError = ( error: Exception ): void => { //URL: https://github.com/isaacs/node-graceful-fs/blob/master/polyfills.js#L315-L342
const FS = {
const {code} = error;
chmodAttempt: attemptifyAsync ( promisify ( fs.chmod ), Handlers.onChangeError ),
chownAttempt: attemptifyAsync ( promisify ( fs.chown ), Handlers.onChangeError ),
closeAttempt: attemptifyAsync ( promisify ( fs.close ) ),
fsyncAttempt: attemptifyAsync ( promisify ( fs.fsync ) ),
realpathAttempt: attemptifyAsync ( promisify ( fs.realpath ) ),
statAttempt: attemptifyAsync ( promisify ( fs.stat ) ),
unlinkAttempt: attemptifyAsync ( promisify ( fs.unlink ) ),
if ( code === 'ENOSYS' ) return;
closeRetry: retryifyAsync ( promisify ( fs.close ), Handlers.isRetriableError ),
fsyncRetry: retryifyAsync ( promisify ( fs.fsync ), Handlers.isRetriableError ),
openRetry: retryifyAsync ( promisify ( fs.open ), Handlers.isRetriableError ),
renameRetry: retryifyAsync ( promisify ( fs.rename ), Handlers.isRetriableError ),
writeRetry: retryifyAsync ( promisify ( fs.write ), Handlers.isRetriableError ),
if ( !IS_USER_ROOT && ( code === 'EINVAL' || code === 'EPERM' ) ) return;
chmodSyncAttempt: attemptifySync ( fs.chmodSync, Handlers.onChangeError ),
chownSyncAttempt: attemptifySync ( fs.chownSync, Handlers.onChangeError ),
closeSyncAttempt: attemptifySync ( fs.closeSync ),
realpathSyncAttempt: attemptifySync ( fs.realpathSync ),
statSyncAttempt: attemptifySync ( fs.statSync ),
unlinkSyncAttempt: attemptifySync ( fs.unlinkSync ),
throw error;
closeSyncRetry: retryifySync ( fs.closeSync, Handlers.isRetriableError ),
fsyncSyncRetry: retryifySync ( fs.fsyncSync, Handlers.isRetriableError ),
openSyncRetry: retryifySync ( fs.openSync, Handlers.isRetriableError ),
renameSyncRetry: retryifySync ( fs.renameSync, Handlers.isRetriableError ),
writeSyncRetry: retryifySync ( fs.writeSync, Handlers.isRetriableError )
};
const FS = {
chmod: promisify ( fs.chmod ),
chown: promisify ( fs.chown ),
close: promisify ( fs.close ),
fsync: promisify ( fs.fsync ),
open: promisify ( fs.open ),
realpath: promisify ( fs.realpath ),
rename: promisify ( fs.rename ),
stat: promisify ( fs.stat ),
write: promisify ( fs.write ),
chmodSync: attemptify ( fs.chmodSync, onChownError ),
chownSync: attemptify ( fs.chownSync, onChownError ),
closeSync: fs.closeSync,
closeSyncLoose: attemptify ( fs.closeSync ),
fsyncSync: fs.fsyncSync,
openSync: fs.openSync,
realpathSync: attemptify ( fs.realpathSync ),
renameSync: fs.renameSync,
statSync: attemptify ( fs.statSync ),
writeSync: fs.writeSync,
onChownError
};
/* EXPORT */
export default FS;

@@ -12,8 +12,2 @@

isNil: ( x: any ): x is null | undefined => {
return x == null;
},
isString: ( x: any ): x is string => {

@@ -20,0 +14,0 @@

@@ -5,5 +5,6 @@

import * as crypto from 'crypto';
import * as fs from 'fs';
import {NOOP} from '../consts';
import * as path from 'path';
import {LIMIT_BASENAME_LENGTH} from '../consts';
import {Disposer} from '../types';
import FS from './fs';

@@ -18,3 +19,3 @@ /* TEMP */

get: ( filePath: string, purge: boolean = true ): [string, Disposer] => {
create: ( filePath: string ): string => {

@@ -27,4 +28,12 @@ const hash = crypto.randomBytes ( 3 ).toString ( 'hex' ), // 6 random hex characters

if ( tempPath in Temp.store ) return Temp.get ( filePath ); // Collision found, try again
return tempPath;
},
get: ( filePath: string, creator: ( filePath: string ) => string, purge: boolean = true ): [string, Disposer] => {
const tempPath = Temp.truncate ( creator ( filePath ) );
if ( tempPath in Temp.store ) return Temp.get ( filePath, creator, purge ); // Collision found, try again
Temp.store[tempPath] = purge;

@@ -40,24 +49,44 @@

if ( !Temp.store[filePath] ) return;
delete Temp.store[filePath];
fs.unlink ( filePath, NOOP );
FS.unlinkAttempt ( filePath );
},
purgeAll: (): void => {
purgeSync: ( filePath: string ): void => {
for ( const filePath in Temp.store ) {
if ( !Temp.store[filePath] ) return;
if ( !Temp.store[filePath] ) continue;
delete Temp.store[filePath];
delete Temp.store[filePath];
FS.unlinkSyncAttempt ( filePath );
try {
},
fs.unlinkSync ( filePath );
purgeSyncAll: (): void => {
} catch {}
for ( const filePath in Temp.store ) {
Temp.purgeSync ( filePath );
}
},
truncate: ( filePath: string ): string => { // Truncating paths to avoid getting an "ENAMETOOLONG" error //FIXME: This doesn't really always work, the actual filesystem limits must be detected for this to be implemented correctly
const basename = path.basename ( filePath );
if ( basename.length <= LIMIT_BASENAME_LENGTH ) return filePath; //FIXME: Rough and quick attempt at detecting ok lengths
const truncable = /^(\.?)(.*?)((?:\.[^.]+)?(?:\.tmp-\d{10}[a-f0-9]{6})?)$/.exec ( basename );
if ( !truncable ) return filePath; //FIXME: No truncable part detected, can't really do much without also changing the parent path, which is unsafe, hoping for the best here
const truncationLength = basename.length - LIMIT_BASENAME_LENGTH;
return `${filePath.slice ( 0, - basename.length )}${truncable[1]}${truncable[2].slice ( 0, - truncationLength )}${truncable[3]}`; //FIXME: The truncable part might be shorter than needed here
}

@@ -69,3 +98,3 @@

process.on ( 'exit', Temp.purgeAll ); // Ensuring purgeable temp files are purged on exit
process.on ( 'exit', Temp.purgeSyncAll ); // Ensuring purgeable temp files are purged on exit

@@ -72,0 +101,0 @@ /* EXPORT */

/* IMPORT */
const os = require ( 'os' ),
const fs = require ( 'fs' ),
os = require ( 'os' ),
path = require ( 'path' ),
delay = require ( 'promise-resolve-timeout' ),
writeFileAtomic = require ( 'write-file-atomic' ),

@@ -11,42 +13,61 @@ {writeFile, writeFileSync} = require ( '../dist' );

const DST = path.join ( os.tmpdir (), 'atomically-temp.txt' ),
const TEMP = os.tmpdir (),
DST = i => path.join ( TEMP, `atomically-temp-${i}.txt` ),
ITERATIONS = 250;
const runSingleAsync = async ( name, fn, buffer ) => {
const runSingleAsync = async ( name, fn, buffer, options ) => {
console.time ( name );
for ( let i = 0; i < ITERATIONS; i++ ) {
await new Promise ( cb => fn ( DST, buffer, cb ) );
await fn ( DST ( i ), buffer, options );
}
console.timeEnd ( name );
await delay ( 1000 );
};
const runSingleSync = async ( name, fn, buffer ) => {
const runSingleSync = async ( name, fn, buffer, options ) => {
console.time ( name );
for ( let i = 0; i < ITERATIONS; i++ ) {
fn ( DST, buffer );
fn ( DST ( i ), buffer, options );
}
console.timeEnd ( name );
await delay ( 1000 );
};
const runAll = async ( name, buffer ) => {
const runAllDummy = () => { // Preparation run
runSingleSync ( 'dummy', fs.writeFileSync, '' );
};
const runAllAsync = async ( name, buffer ) => {
await runSingleAsync ( `${name} -> async -> write-file-atomic`, writeFileAtomic, buffer );
await runSingleAsync ( `${name} -> async -> write-file-atomic (fast)`, ( p, b, c ) => writeFileAtomic ( p, b, { mode: false, chown: false, fsync: false }, c ), buffer );
await runSingleAsync ( `${name} -> async -> write-file-atomic (fastest)`, writeFileAtomic, buffer, { fsync: false } );
await runSingleAsync ( `${name} -> async -> atomically`, writeFile, buffer );
await runSingleAsync ( `${name} -> async -> atomically (fast)`, ( p, b, c ) => writeFile ( p, b, { mode: false, chown: false, fsync: false }, c ), buffer );
await runSingleAsync ( `${name} -> async -> atomically (faster)`, writeFile, buffer, { mode: false, chown: false, fsyncWait: false } );
await runSingleAsync ( `${name} -> async -> atomically (fastest)`, writeFile, buffer, { mode: false, chown: false, fsync: false } );
};
const runAllSync = ( name, buffer ) => {
runSingleSync ( `${name} -> sync -> write-file-atomic`, writeFileAtomic.sync, buffer );
runSingleSync ( `${name} -> sync -> write-file-atomic (fast)`, ( p, b ) => writeFileAtomic.sync ( p, b, { mode: false, chown: false, fsync: false } ), buffer );
runSingleSync ( `${name} -> sync -> write-file-atomic (fastest)`, writeFileAtomic.sync, buffer, { fsync: false } );
runSingleSync ( `${name} -> sync -> atomically`, writeFileSync, buffer );
runSingleSync ( `${name} -> sync -> atomically (fast)`, ( p, b ) => writeFileSync ( p, b, { mode: false, chown: false, fsync: false } ), buffer );
runSingleSync ( `${name} -> sync -> atomically (faster)`, writeFileSync, buffer, { mode: false, chown: false, fsyncWait: false } );
runSingleSync ( `${name} -> sync -> atomically (fastest)`, writeFileSync, buffer, { mode: false, chown: false, fsync: false } );
};
const runAll = async ( name, buffer ) => {
await runAllAsync ( name, buffer );
console.log ( '-------------------' );
runAllSync ( name, buffer );
};
const run = async () => {
await runAll ( '1000kb', Buffer.allocUnsafe ( 1000 * 1024 ) );
console.log ( '-------------------' );
runAllDummy ();
console.log ( '===================' );
await runAll ( '100kb', Buffer.allocUnsafe ( 100 * 1024 ) );
console.log ( '-------------------' );
console.log ( '===================' );
await runAll ( '10kb', Buffer.allocUnsafe ( 10 * 1024 ) );
console.log ( '-------------------' );
console.log ( '===================' );
await runAll ( '1kb', Buffer.allocUnsafe ( 1024 ) );
console.log ( '===================' );
};
run ();

@@ -5,3 +5,6 @@ 'use strict'

const _ = require('lodash')
const fs = require('fs')
const os = require('os')
const path = require('path')
const {test} = require('tap')

@@ -65,2 +68,3 @@ const requireInject = require('require-inject')

if (/nostat/.test(tmpfile)) return cb(createErr('ENOSTAT'))
if (/statful/.test(tmpfile)) return cb(null, fs.statSync('/'));
cb()

@@ -107,5 +111,39 @@ },

if (/nostat/.test(tmpfile)) throw createErr('ENOSTAT')
if (/statful/.test(tmpfile)) return fs.statSync('/');
}
});
const makeUnstableAsyncFn = function () {
return function () {
if ( Math.random () <= .9 ) {
const code = _.shuffle ([ 'EMFILE', 'ENFILE', 'EAGAIN', 'EBUSY', 'EACCESS', 'EPERM' ])[0];
throw createErr ( code );
}
return arguments[arguments.length -1](null, arguments[0]);
};
};
const makeUnstableSyncFn = function ( fn ) {
return function () {
if ( Math.random () <= .9 ) {
const code = _.shuffle ([ 'EMFILE', 'ENFILE', 'EAGAIN', 'EBUSY', 'EACCESS', 'EPERM' ])[0];
throw createErr ( code );
}
return fn.apply(undefined, arguments)
};
};
const fsMockUnstable = Object.assign ( {}, fsMock, {
open: makeUnstableAsyncFn (),
write: makeUnstableAsyncFn (),
fsync: makeUnstableAsyncFn (),
close: makeUnstableAsyncFn (),
rename: makeUnstableAsyncFn (),
openSync: makeUnstableSyncFn ( _.identity ),
writeSync: makeUnstableSyncFn ( _.noop ),
fsyncSync: makeUnstableSyncFn ( _.noop ),
closeSync: makeUnstableSyncFn ( _.noop ),
renameSync: makeUnstableSyncFn ( _.noop )
});
const {writeFile: writeFileAtomic, writeFileSync: writeFileAtomicSync} = requireInject('../dist', { fs: fsMock });

@@ -125,3 +163,3 @@

t.test('non-root tests', t => {
t.plan(19)
t.plan(27)

@@ -185,2 +223,28 @@ writeFileAtomic('good', 'test', { mode: '0777' }, err => {

})
const optionsImmutable = {};
writeFileAtomic('statful', 'test', optionsImmutable, err => {
t.notOk(err);
t.deepEquals(optionsImmutable, {});
});
const schedule = filePath => {
t.is(filePath, 'good');
return new Promise ( resolve => {
resolve ( () => {
t.is(true,true);
});
});
};
writeFileAtomic('good','test', {schedule}, err => {
t.notOk(err);
});
const tmpCreate = filePath => `.${filePath}.custom`;
const tmpCreated = filePath => t.is ( filePath, '.good.custom' );
writeFileAtomic('good','test', {tmpCreate, tmpCreated}, err => {
t.notOk(err)
})
const longPath = path.join(os.tmpdir(),'.012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789.txt');
const {writeFile: writeFileAtomicNative} = requireInject('../dist', { fs });
writeFileAtomicNative(longPath,'test', err => {
t.notOk(err)
})
})

@@ -205,2 +269,10 @@

test('unstable async tests', t => {
t.plan(1);
const {writeFile: writeFileAtomic} = requireInject('../dist', { fs: fsMockUnstable });
writeFileAtomic('good', 'test', err => {
t.notOk(err, 'No errors occur when retryable errors are thrown')
})
});
test('sync tests', t => {

@@ -229,3 +301,3 @@ t.plan(2)

t.test('non-root', t => {
t.plan(22)
t.plan(37)
noexception(t, 'No errors occur when passing in options', () => {

@@ -243,9 +315,10 @@ writeFileAtomicSync('good', 'test', { mode: '0777' })

})
noexception(t, 'tmpfileCreated is called on success', () => {
noexception(t, 'tmpCreated is called on success', () => {
writeFileAtomicSync('good', 'test', {
tmpfileCreated (gottmpfile) {
tmpCreated (gottmpfile) {
tmpfile = gottmpfile
}
})
t.match(tmpfile, /^good\.tmp-\w+$/, 'tmpfileCreated called for success')
t.match(tmpfile, /^good\.tmp-\w+$/, 'tmpCreated called for success')
t.match(tmpfile, /^good\.tmp-\d{10}[a-f0-9]{6}$/, 'tmpCreated format')
})

@@ -256,3 +329,3 @@

writeFileAtomicSync('noopen', 'test', {
tmpfileCreated (gottmpfile) {
tmpCreated (gottmpfile) {
tmpfile = gottmpfile

@@ -262,7 +335,7 @@ }

})
t.is(tmpfile, undefined, 'tmpfileCreated not called for open failure')
t.is(tmpfile, undefined, 'tmpCreated not called for open failure')
throws(t, 'ENOWRITE', 'fs.writeSync failures propagate', () => {
writeFileAtomicSync('nowrite', 'test', {
tmpfileCreated (gottmpfile) {
tmpCreated (gottmpfile) {
tmpfile = gottmpfile

@@ -272,3 +345,3 @@ }

})
t.match(tmpfile, /^nowrite\.tmp-\w+$/, 'tmpfileCreated called for failure after open')
t.match(tmpfile, /^nowrite\.tmp-\w+$/, 'tmpCreated called for failure after open')

@@ -312,2 +385,41 @@ throws(t, 'ENOCHOWN', 'Chown failures propagate', () => {

})
const optionsImmutable = {};
noexception(t, 'options are immutable', () => {
writeFileAtomicSync('statful', 'test', optionsImmutable)
})
t.deepEquals(optionsImmutable, {});
const tmpCreate = filePath => `.${filePath}.custom`;
const tmpCreated = filePath => t.is ( filePath, '.good.custom' );
noexception(t, 'custom temp creator', () => {
writeFileAtomicSync('good', 'test', {tmpCreate, tmpCreated})
})
const path0 = path.join(os.tmpdir(),'atomically-test-0');
const tmpPath0 = path0 + '.temp';
noexception(t, 'temp files are purged on success', () => {
const {writeFileSync: writeFileAtomicSync} = requireInject('../dist', { fs });
writeFileAtomicSync(path0, 'test', {tmpCreate: () => tmpPath0})
})
t.is(true,fs.existsSync(path0));
t.is(false,fs.existsSync(tmpPath0));
const path1 = path.join(os.tmpdir(),'atomically-test-norename-1');
const tmpPath1 = path1 + '.temp';
throws(t, 'ENORENAME', 'temp files are purged on error', () => {
const {writeFileSync: writeFileAtomicSync} = requireInject('../dist', { fs: Object.assign ( {}, fs, { renameSync: fsMock.renameSync })});
writeFileAtomicSync(path1, 'test', {tmpCreate: () => tmpPath1})
})
t.is(false,fs.existsSync(path1));
t.is(false,fs.existsSync(tmpPath1));
const path2 = path.join(os.tmpdir(),'atomically-test-norename-2');
const tmpPath2 = path2 + '.temp';
throws(t, 'ENORENAME', 'temp files can also not be purged on error', () => {
const {writeFileSync: writeFileAtomicSync} = requireInject('../dist', { fs: Object.assign ( {}, fs, { renameSync: fsMock.renameSync })});
writeFileAtomicSync(path2, 'test', {tmpCreate: () => tmpPath2,tmpPurge: false})
})
t.is(false,fs.existsSync(path2));
t.is(true,fs.existsSync(tmpPath2));
const longPath = path.join(os.tmpdir(),'.012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789.txt');
noexception(t, 'temp files are truncated', () => {
const {writeFileSync: writeFileAtomicSync} = requireInject('../dist', { fs });
writeFileAtomicSync(longPath, 'test')
})
})

@@ -332,2 +444,17 @@

test('unstable sync tests', t => {
t.plan(1);
const noexception = function (t, msg, todo) {
let err
try { todo() } catch (e) { err = e }
t.ifError(err, msg)
}
noexception(t, 'No errors occur when retryable errors are thrown', () => {
const {writeFileSync: writeFileAtomicSync} = requireInject('../dist', { fs: fsMockUnstable });
writeFileAtomicSync('good', 'test')
})
});
test('promises', async t => {

@@ -344,10 +471,10 @@ let tmpfile

await writeFileAtomic('good', 'test', {
tmpfileCreated (gottmpfile) {
tmpCreated (gottmpfile) {
tmpfile = gottmpfile
}
})
t.match(tmpfile, /^good\.tmp-\w+$/, 'tmpfileCreated is called for success')
t.match(tmpfile, /^good\.tmp-\w+$/, 'tmpCreated is called for success')
await writeFileAtomic('good', 'test', {
tmpfileCreated (gottmpfile) {
tmpCreated (gottmpfile) {
return Promise.resolve()

@@ -359,14 +486,14 @@ }

await t.rejects(writeFileAtomic('noopen', 'test', {
tmpfileCreated (gottmpfile) {
tmpCreated (gottmpfile) {
tmpfile = gottmpfile
}
}))
t.is(tmpfile, undefined, 'tmpfileCreated is not called on open failure')
t.is(tmpfile, undefined, 'tmpCreated is not called on open failure')
await t.rejects(writeFileAtomic('nowrite', 'test', {
tmpfileCreated (gottmpfile) {
tmpCreated (gottmpfile) {
tmpfile = gottmpfile
}
}))
t.match(tmpfile, /^nowrite\.tmp-\w+$/, 'tmpfileCreated is called if failure is after open')
t.match(tmpfile, /^nowrite\.tmp-\w+$/, 'tmpCreated is called if failure is after open')
})
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc