├── .autod.conf.js ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── History.md ├── LICENSE ├── README.md ├── appveyor.yml ├── index.js ├── package.json └── test ├── fixtures └── source.txt └── index.test.js /.autod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | write: true, 5 | prefix: '^', 6 | test: [ 7 | 'test', 8 | 'benchmark', 9 | ], 10 | devdep: [ 11 | ], 12 | exclude: [ 13 | './test/fixtures', 14 | './docs', 15 | './coverage', 16 | ], 17 | }; 18 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-egg" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs/ 2 | npm-debug.log 3 | node_modules/ 4 | coverage/ 5 | .idea/ 6 | run/ 7 | .DS_Store 8 | *.swp 9 | test/fixtures/tmp/ 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '8' 5 | - '10' 6 | install: 7 | - npm i npminstall && npminstall 8 | script: 9 | - npm run ci 10 | after_script: 11 | - npminstall codecov && codecov 12 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 1.0.0 / 2018-08-15 3 | ================== 4 | 5 | **feat** 6 | * [[`20a1623`](http://github.com/node-modules/save-file-atomic/commit/20a1623ad8b319431dd7e3894d18cd7529698d40)] - first release (fengmk2 <>) 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-present node-modules and other contributors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # save-file-atomic 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![build status][travis-image]][travis-url] 5 | [![Test coverage][codecov-image]][codecov-url] 6 | [![David deps][david-image]][david-url] 7 | [![Known Vulnerabilities][snyk-image]][snyk-url] 8 | [![npm download][download-image]][download-url] 9 | 10 | [npm-image]: https://img.shields.io/npm/v/save-file-atomic.svg?style=flat-square 11 | [npm-url]: https://npmjs.org/package/save-file-atomic 12 | [travis-image]: https://img.shields.io/travis/node-modules/save-file-atomic.svg?style=flat-square 13 | [travis-url]: https://travis-ci.org/node-modules/save-file-atomic 14 | [codecov-image]: https://img.shields.io/codecov/c/github/node-modules/save-file-atomic.svg?style=flat-square 15 | [codecov-url]: https://codecov.io/github/node-modules/save-file-atomic?branch=master 16 | [david-image]: https://img.shields.io/david/node-modules/save-file-atomic.svg?style=flat-square 17 | [david-url]: https://david-dm.org/node-modules/save-file-atomic 18 | [snyk-image]: https://snyk.io/test/npm/save-file-atomic/badge.svg?style=flat-square 19 | [snyk-url]: https://snyk.io/test/npm/save-file-atomic 20 | [download-image]: https://img.shields.io/npm/dm/save-file-atomic.svg?style=flat-square 21 | [download-url]: https://npmjs.org/package/save-file-atomic 22 | 23 | Save anything to file atomic. 24 | 25 | ## Install 26 | 27 | ```bash 28 | $ npm i save-file-atomic --save 29 | ``` 30 | 31 | ## Usage 32 | 33 | ```js 34 | const savefile = require('save-file-atomic'); 35 | 36 | // anything can be: ReadStream, Buffer, String 37 | // target is the target file path 38 | await savefile(anything, target); 39 | 40 | // copy file atomic 41 | await savefile.copy(source, target); 42 | ``` 43 | 44 | ## Questions & Suggestions 45 | 46 | Please open an issue [here](https://github.com/node-modules/save-file-atomic/issues). 47 | 48 | ## License 49 | 50 | [MIT](LICENSE) 51 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: '8' 4 | - nodejs_version: '10' 5 | 6 | install: 7 | - ps: Install-Product node $env:nodejs_version 8 | - npm i npminstall && node_modules\.bin\npminstall 9 | 10 | test_script: 11 | - node --version 12 | - npm --version 13 | - npm run test 14 | 15 | build: off 16 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Readable = require('stream').Readable; 4 | const path = require('path'); 5 | const uuid = require('uuid'); 6 | const fs = require('mz/fs'); 7 | const mkdirp = require('mz-modules/mkdirp'); 8 | const rimraf = require('mz-modules/rimraf'); 9 | const pump = require('mz-modules/pump'); 10 | 11 | /** 12 | * Save anything to file 13 | * @param {String|Buffer|Readable} source - source content 14 | * @param {String} target - target file path 15 | */ 16 | async function savefile(source, target) { 17 | const isStream = source instanceof Readable; 18 | let sourceError; 19 | let onError; 20 | if (isStream) { 21 | onError = err => { 22 | sourceError = err; 23 | }; 24 | source.once('error', onError); 25 | } 26 | 27 | const exists = await fs.exists(target); 28 | if (!exists) { 29 | // make sure target parent dir exists 30 | const parent = path.dirname(target); 31 | const parentExists = await fs.exists(parent); 32 | if (!parentExists) { 33 | await mkdirp(parent); 34 | } 35 | } 36 | 37 | if (isStream) { 38 | // source stream has emit error, throw it 39 | if (sourceError) { 40 | throw sourceError; 41 | } else { 42 | // remove error listener 43 | source.removeListener('error', onError); 44 | } 45 | } 46 | 47 | const tempfile = `${target}.${uuid.v1()}.savefile.tmp`; 48 | try { 49 | if (isStream) { 50 | const targetStream = fs.createWriteStream(tempfile); 51 | await pump(source, targetStream); 52 | } else { 53 | if (typeof source === 'string') { 54 | source = Buffer.from(source); 55 | } 56 | await fs.writeFile(tempfile, source); 57 | } 58 | // if exists, remove it first 59 | if (await fs.exists(target)) { 60 | await rimraf(target); 61 | } 62 | // atomic rename 63 | await fs.rename(tempfile, target); 64 | } catch (err) { 65 | // make sure tempfile remove 66 | await rimraf(tempfile); 67 | throw err; 68 | } 69 | } 70 | 71 | async function copy(source, target) { 72 | const stream = fs.createReadStream(source); 73 | await savefile(stream, target); 74 | } 75 | 76 | module.exports = savefile; 77 | module.exports.copy = copy; 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "save-file-atomic", 3 | "version": "1.0.0", 4 | "description": "Save anything to file atomic", 5 | "keywords": [ 6 | "writeStream", 7 | "safe-to", 8 | "atomically", 9 | "atomic" 10 | ], 11 | "dependencies": { 12 | "mz": "^2.7.0", 13 | "mz-modules": "^2.1.0", 14 | "uuid": "^3.3.2" 15 | }, 16 | "devDependencies": { 17 | "autod": "^3.0.0", 18 | "egg-bin": "^4.3.0", 19 | "egg-ci": "^1.8.0", 20 | "egg-mock": "^3.13.0", 21 | "eslint": "^4.11.0", 22 | "eslint-config-egg": "^5.1.0", 23 | "mm": "^2.4.0" 24 | }, 25 | "engines": { 26 | "node": ">=8.0.0" 27 | }, 28 | "scripts": { 29 | "test": "npm run lint -- --fix && egg-bin pkgfiles && npm run test-local", 30 | "test-local": "egg-bin test --full-trace", 31 | "cov": "egg-bin cov", 32 | "lint": "eslint .", 33 | "ci": "egg-bin pkgfiles --check && npm run lint && npm run cov", 34 | "pkgfiles": "egg-bin pkgfiles", 35 | "autod": "autod" 36 | }, 37 | "files": [ 38 | "index.js" 39 | ], 40 | "ci": { 41 | "version": "8, 10" 42 | }, 43 | "repository": { 44 | "type": "git", 45 | "url": "git+https://github.com/node-modules/save-file-atomic.git" 46 | }, 47 | "bugs": { 48 | "url": "https://github.com/node-modules/save-file-atomic/issues" 49 | }, 50 | "homepage": "https://github.com/node-modules/save-file-atomic#readme", 51 | "author": "fengmk2", 52 | "license": "MIT" 53 | } 54 | -------------------------------------------------------------------------------- /test/fixtures/source.txt: -------------------------------------------------------------------------------- 1 | This is a source file. 2 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mm = require('mm'); 4 | const path = require('path'); 5 | const assert = require('assert'); 6 | const fs = require('mz/fs'); 7 | const rimraf = require('mz-modules/rimraf'); 8 | const savefile = require('..'); 9 | 10 | const fixtures = path.join(__dirname, 'fixtures'); 11 | const temp = path.join(fixtures, 'tmp'); 12 | 13 | describe('test/index.test.js', () => { 14 | function cleanup() { 15 | return rimraf(temp); 16 | } 17 | 18 | beforeEach(cleanup); 19 | afterEach(cleanup); 20 | afterEach(mm.restore); 21 | 22 | describe('savefile()', () => { 23 | it('should support String', async () => { 24 | const source = 'This is a ascii string with emoji 😊, and 哈哈, 123'; 25 | const target = path.join(temp, 'string-target.txt'); 26 | await savefile(source, target); 27 | const targetContent = await fs.readFile(target, 'utf8'); 28 | assert(source === targetContent); 29 | }); 30 | 31 | it('should support Buffer', async () => { 32 | const source = Buffer.from('This is a ascii string with emoji 😊, and 哈哈, 123'); 33 | const target = path.join(temp, 'foo/bar/buffer-target.txt'); 34 | await savefile(source, target); 35 | const targetContent = await fs.readFile(target, 'utf8'); 36 | assert(source.toString() === targetContent); 37 | }); 38 | 39 | it('should support Stream', async () => { 40 | const sourceFile = path.join(fixtures, 'source.txt'); 41 | const source = fs.createReadStream(sourceFile); 42 | const target = path.join(temp, 'stream-target.txt'); 43 | await savefile(source, target); 44 | const sourceContent = await fs.readFile(sourceFile, 'utf8'); 45 | const targetContent = await fs.readFile(target, 'utf8'); 46 | assert(sourceContent === targetContent); 47 | }); 48 | 49 | it('should override exists target', async () => { 50 | const sourceFile = path.join(fixtures, 'source.txt'); 51 | const source = fs.createReadStream(sourceFile); 52 | const target = path.join(temp, 'stream-target.txt'); 53 | await savefile(source, target); 54 | const sourceContent = await fs.readFile(sourceFile, 'utf8'); 55 | const targetContent = await fs.readFile(target, 'utf8'); 56 | assert(sourceContent === targetContent); 57 | 58 | // again should work 59 | const source2 = fs.createReadStream(sourceFile); 60 | await savefile(source2, target); 61 | const target2Content = await fs.readFile(target, 'utf8'); 62 | assert(sourceContent === target2Content); 63 | }); 64 | 65 | it('should concurrency savefile same file', async () => { 66 | const sourceFile = path.join(fixtures, 'source.txt'); 67 | const target = path.join(temp, '1/2/3/4/5/6/7/8/9/10/concurrency-target.txt'); 68 | 69 | await Promise.all([ 70 | savefile(fs.createReadStream(sourceFile), target), 71 | savefile(fs.createReadStream(sourceFile), target), 72 | savefile(fs.createReadStream(sourceFile), target), 73 | savefile(fs.createReadStream(sourceFile), target), 74 | savefile(fs.createReadStream(sourceFile), target), 75 | savefile(fs.createReadStream(sourceFile), target), 76 | ]); 77 | 78 | const sourceContent = await fs.readFile(sourceFile, 'utf8'); 79 | const targetContent = await fs.readFile(target, 'utf8'); 80 | assert(sourceContent === targetContent); 81 | }); 82 | 83 | it('should throw error when source stream error', async () => { 84 | const sourceFile = path.join(fixtures, 'source-not-exists.txt'); 85 | const target = path.join(temp, 'stream-target.txt'); 86 | try { 87 | await savefile(fs.createReadStream(sourceFile), target); 88 | throw new Error('should not run this'); 89 | } catch (err) { 90 | assert(err.code === 'ENOENT'); 91 | assert(err.message.includes('no such file or directory')); 92 | } 93 | }); 94 | 95 | it('should remove tempfile when rename error', async () => { 96 | const source = path.join(fixtures, 'source.txt'); 97 | const target = path.join(temp, 'stream-target.txt'); 98 | mm(fs, 'rename', async () => { 99 | throw new Error('mock error'); 100 | }); 101 | try { 102 | await savefile(source, target); 103 | throw new Error('should not run this'); 104 | } catch (err) { 105 | assert(err.message === 'mock error'); 106 | } 107 | assert((await fs.readdir(temp)).length === 0); 108 | }); 109 | }); 110 | 111 | describe('.copy()', () => { 112 | it('should copy file success', async () => { 113 | const source = path.join(fixtures, 'source.txt'); 114 | const target = path.join(temp, 'copy-target.txt'); 115 | await savefile.copy(source, target); 116 | const sourceContent = await fs.readFile(source, 'utf8'); 117 | const targetContent = await fs.readFile(target, 'utf8'); 118 | assert(sourceContent === targetContent); 119 | }); 120 | 121 | it('should support not exists parent dir', async () => { 122 | const source = path.join(fixtures, 'source.txt'); 123 | const target = path.join(temp, '1/2/3/4/5/6/7/9/10/copy-target.txt'); 124 | await savefile.copy(source, target); 125 | let sourceContent = await fs.readFile(source, 'utf8'); 126 | let targetContent = await fs.readFile(target, 'utf8'); 127 | assert(sourceContent === targetContent); 128 | 129 | // again should work 130 | await rimraf(target); 131 | await savefile.copy(source, target); 132 | sourceContent = await fs.readFile(source, 'utf8'); 133 | targetContent = await fs.readFile(target, 'utf8'); 134 | assert(sourceContent === targetContent); 135 | }); 136 | }); 137 | }); 138 | --------------------------------------------------------------------------------