├── test ├── fixture │ ├── shared.txt │ └── exclusive.txt ├── lock.js └── basic.js ├── .gitignore ├── browser.js ├── example.js ├── .github └── workflows │ └── test-node.yml ├── package.json ├── README.md └── index.js /test/fixture/shared.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixture/exclusive.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | sandbox 3 | coverage 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /browser.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | throw new Error('random-access-file is not supported in the browser') 3 | } 4 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | const RAF = require('.') 2 | const file = new RAF('hello.txt') 3 | 4 | const max = 500 * 1024 * 1024 5 | const buf = Buffer.alloc(1024) 6 | buf.fill('lo') 7 | 8 | let offset = 0 9 | write() 10 | 11 | function write () { 12 | file.write(offset, buf, afterWrite) 13 | } 14 | 15 | function afterWrite (err) { 16 | if (err) throw err 17 | if (offset >= max) return done() 18 | offset += buf.length 19 | write() 20 | } 21 | 22 | function done () { 23 | console.log('wrote hello.txt') 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/test-node.yml: -------------------------------------------------------------------------------- 1 | name: Build Status 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | node-version: [lts/*] 14 | os: [ubuntu-latest, macos-latest, windows-latest] 15 | runs-on: ${{ matrix.os }} 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v2 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | - run: npm install 23 | - run: npm test 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "random-access-file", 3 | "version": "4.1.2", 4 | "description": "Continuous reading or writing to a file using random offsets and lengths", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "standard && brittle test/*.js" 8 | }, 9 | "browser": "./browser.js", 10 | "files": [ 11 | "index.js", 12 | "browser.js" 13 | ], 14 | "imports": { 15 | "fs": { 16 | "bare": "bare-fs", 17 | "default": "fs" 18 | }, 19 | "path": { 20 | "bare": "bare-path", 21 | "default": "path" 22 | } 23 | }, 24 | "dependencies": { 25 | "bare-fs": "^4.0.1", 26 | "bare-path": "^3.0.0", 27 | "random-access-storage": "^3.0.0" 28 | }, 29 | "optionalDependencies": { 30 | "fs-native-extensions": "^1.3.1" 31 | }, 32 | "devDependencies": { 33 | "brittle": "^3.3.0", 34 | "standard": "^17.0.0" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "https://github.com/random-access-storage/random-access-file.git" 39 | }, 40 | "author": "Mathias Buus (@mafintosh)", 41 | "license": "MIT", 42 | "bugs": { 43 | "url": "https://github.com/random-access-storage/random-access-file/issues" 44 | }, 45 | "homepage": "https://github.com/random-access-storage/random-access-file" 46 | } 47 | -------------------------------------------------------------------------------- /test/lock.js: -------------------------------------------------------------------------------- 1 | const test = require('brittle') 2 | const RAF = require('..') 3 | 4 | test('2 writers', function (t) { 5 | t.plan(4) 6 | 7 | const file = 'test/fixture/exclusive.txt' 8 | 9 | const a = new RAF(file, { lock: true }) 10 | const b = new RAF(file, { lock: true }) 11 | 12 | a.open(function (err) { 13 | t.absent(err, 'a granted lock') 14 | 15 | b.open(function (err) { 16 | t.ok(err, 'b denied lock') 17 | 18 | a.close(() => t.pass('a closed')) 19 | b.close(() => t.pass('b closed')) 20 | }) 21 | }) 22 | }) 23 | 24 | test('2 readers', function (t) { 25 | t.plan(4) 26 | 27 | const file = 'test/fixture/shared.txt' 28 | 29 | const a = new RAF(file, { lock: true, writable: false }) 30 | const b = new RAF(file, { lock: true, writable: false }) 31 | 32 | a.open(function (err) { 33 | t.absent(err, 'a granted lock') 34 | 35 | b.open(function (err) { 36 | t.absent(err, 'b granted lock') 37 | 38 | a.close(() => t.pass('a closed')) 39 | b.close(() => t.pass('b closed')) 40 | }) 41 | }) 42 | }) 43 | 44 | test('2 readers + 1 writer', function (t) { 45 | t.plan(6) 46 | 47 | const file = 'test/fixture/shared.txt' 48 | 49 | const a = new RAF(file, { lock: true, writable: false }) 50 | const b = new RAF(file, { lock: true, writable: false }) 51 | const c = new RAF(file, { lock: true }) 52 | 53 | a.open(function (err) { 54 | t.absent(err, 'a granted lock') 55 | 56 | b.open(function (err) { 57 | t.absent(err, 'b granted lock') 58 | 59 | c.open(function (err) { 60 | t.ok(err, 'c denied lock') 61 | 62 | a.close(() => t.pass('a closed')) 63 | b.close(() => t.pass('b closed')) 64 | c.close(() => t.pass('c closed')) 65 | }) 66 | }) 67 | }) 68 | }) 69 | 70 | test('1 writer + 1 reader', function (t) { 71 | t.plan(4) 72 | 73 | const file = 'test/fixture/exclusive.txt' 74 | 75 | const a = new RAF(file, { lock: true }) 76 | const b = new RAF(file, { lock: true, writable: false }) 77 | 78 | a.open(function (err) { 79 | t.absent(err, 'a granted lock') 80 | 81 | b.open(function (err) { 82 | t.ok(err, 'b denied lock') 83 | 84 | a.close(() => t.pass('a closed')) 85 | b.close(() => t.pass('b closed')) 86 | }) 87 | }) 88 | }) 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # random-access-file 2 | 3 | Continuous reading or writing to a file using random offsets and lengths 4 | 5 | ``` 6 | npm install random-access-file 7 | ``` 8 | 9 | ## Why? 10 | 11 | If you are receiving a file in multiple pieces in a distributed system it can be useful to write these pieces to disk one by one in various places throughout the file without having to open and close a file descriptor all the time. 12 | 13 | random-access-file allows you to do just this. 14 | 15 | ## Usage 16 | 17 | ``` js 18 | const RandomAccessFile = require('random-access-file') 19 | 20 | const file = new RandomAccessFile('my-file.txt') 21 | 22 | file.write(10, Buffer.from('hello'), function(err) { 23 | // write a buffer to offset 10 24 | file.read(10, 5, function(err, buffer) { 25 | console.log(buffer) // read 5 bytes from offset 10 26 | file.close(function() { 27 | console.log('file is closed') 28 | }) 29 | }) 30 | }) 31 | ``` 32 | 33 | file will use an open file descriptor. When you are done with the file you should call `file.close()`. 34 | 35 | ## API 36 | 37 | #### `const file = new RandomAccessFile(filename, [options])` 38 | 39 | Create a new file. Options include: 40 | 41 | ``` js 42 | { 43 | truncate: false, // truncate the file before reading / writing 44 | size: someSize, // truncate the file to this size first 45 | readable: true, // should the file be opened as readable? 46 | writable: true, // should the file be opened as writable? 47 | lock: false, // lock the file 48 | sparse: false // mark the file as sparse 49 | } 50 | ``` 51 | 52 | #### `file.write(offset, buffer, [callback])` 53 | 54 | Write a buffer at a specific offset. 55 | 56 | #### `file.read(offset, length, callback)` 57 | 58 | Read a buffer at a specific offset. Callback is called with the buffer read. 59 | 60 | #### `file.del(offset, length, callback)` 61 | 62 | Delete a portion of the file. Any partial file blocks in the deleted portion are zeroed and, if the file is sparse, the remaining file blocks unlinked in-place. 63 | 64 | #### `file.truncate(offset, callback)` 65 | 66 | Truncate the file length to this offset. 67 | 68 | #### `file.stat(callback)` 69 | 70 | Stat the storage. Should return an object with useful information about the underlying storage, including: 71 | 72 | ```js 73 | { 74 | size: number // how many bytes of data is stored? 75 | } 76 | ``` 77 | 78 | #### `file.close([callback])` 79 | 80 | Close the underlying file descriptor. 81 | 82 | #### `file.unlink([callback])` 83 | 84 | Unlink the underlying file. 85 | 86 | #### `file.on('open')` 87 | 88 | Emitted when the file descriptor has been opened. You can access the fd using `file.fd`. 89 | You do not need to wait for this event before doing any reads/writes. 90 | 91 | #### `file.on('close')` 92 | 93 | Emitted when the file has been closed. 94 | 95 | ## License 96 | 97 | MIT 98 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const RandomAccessStorage = require('random-access-storage') 2 | const fs = require('fs') 3 | const path = require('path') 4 | const constants = fs.constants 5 | 6 | let fsext = null 7 | try { 8 | fsext = require('fs-native-extensions') 9 | } catch {} 10 | 11 | const RDWR = constants.O_RDWR 12 | const RDONLY = constants.O_RDONLY 13 | const WRONLY = constants.O_WRONLY 14 | const CREAT = constants.O_CREAT 15 | 16 | class Pool { 17 | constructor (maxSize) { 18 | this.maxSize = maxSize 19 | this.active = [] 20 | } 21 | 22 | _onactive (file) { 23 | // suspend a random one when the pool 24 | if (this.active.length >= this.maxSize) { 25 | const r = Math.floor(Math.random() * this.active.length) 26 | this.active[r].suspend() 27 | } 28 | 29 | file._pi = this.active.push(file) - 1 30 | } 31 | 32 | _oninactive (file) { 33 | const head = this.active.pop() 34 | if (head !== file) { 35 | head._pi = file._pi 36 | this.active[head._pi] = head 37 | } 38 | } 39 | } 40 | 41 | module.exports = class RandomAccessFile extends RandomAccessStorage { 42 | constructor (filename, opts = {}) { 43 | const size = opts.size || (opts.truncate ? 0 : -1) 44 | 45 | super() 46 | 47 | if (opts.directory) filename = path.join(opts.directory, path.resolve('/', filename).replace(/^\w+:\\/, '')) 48 | 49 | this.directory = opts.directory || null 50 | this.filename = filename 51 | this.fd = 0 52 | 53 | const { 54 | readable = true, 55 | writable = true 56 | } = opts 57 | 58 | this.mode = readable && writable ? RDWR : (readable ? RDONLY : WRONLY) 59 | 60 | this._pi = 0 // pool index 61 | this._pool = opts.pool || null 62 | this._size = size 63 | this._rmdir = !!opts.rmdir 64 | this._lock = opts.lock === true 65 | this._sparse = opts.sparse === true 66 | this._alloc = opts.alloc || Buffer.allocUnsafe 67 | this._alwaysCreate = size >= 0 68 | } 69 | 70 | static createPool (maxSize) { 71 | return new Pool(maxSize) 72 | } 73 | 74 | _open (req) { 75 | const create = this._alwaysCreate || this.writing // .writing comes from RAS 76 | const self = this 77 | const mode = this.mode | (create ? CREAT : 0) 78 | 79 | if (create) fs.mkdir(path.dirname(this.filename), { recursive: true }, ondir) 80 | else ondir(null) 81 | 82 | function ondir (err) { 83 | if (err) return req.callback(err) 84 | fs.open(self.filename, mode, onopen) 85 | } 86 | 87 | function onopen (err, fd) { 88 | if (err) return onerror(err) 89 | 90 | self.fd = fd 91 | 92 | if (!self._lock || !fsext) return onlock(null) 93 | 94 | // Should we aquire a read lock? 95 | const shared = self.mode === RDONLY 96 | 97 | if (fsext.tryLock(self.fd, { shared })) onlock(null) 98 | else onlock(createLockError(self.filename)) 99 | } 100 | 101 | function onlock (err) { 102 | if (err) return onerrorafteropen(err) 103 | 104 | if (!self._sparse || !fsext || self.mode === RDONLY) return onsparse(null) 105 | 106 | fsext.sparse(self.fd).then(onsparse, onsparse) 107 | } 108 | 109 | function onsparse (err) { 110 | if (err) return onerrorafteropen(err) 111 | 112 | if (self._size < 0) return ontruncate(null) 113 | 114 | fs.ftruncate(self.fd, self._size, ontruncate) 115 | } 116 | 117 | function ontruncate (err) { 118 | if (err) return onerrorafteropen(err) 119 | if (self._pool !== null) self._pool._onactive(self) 120 | req.callback(null) 121 | } 122 | 123 | function onerror (err) { 124 | req.callback(err) 125 | } 126 | 127 | function onerrorafteropen (err) { 128 | fs.close(self.fd, function () { 129 | self.fd = 0 130 | onerror(err) 131 | }) 132 | } 133 | } 134 | 135 | _write (req) { 136 | const data = req.data 137 | const fd = this.fd 138 | 139 | fs.write(fd, data, 0, req.size, req.offset, onwrite) 140 | 141 | function onwrite (err, wrote) { 142 | if (err) return req.callback(err) 143 | 144 | req.size -= wrote 145 | req.offset += wrote 146 | 147 | if (!req.size) return req.callback(null) 148 | fs.write(fd, data, data.length - req.size, req.size, req.offset, onwrite) 149 | } 150 | } 151 | 152 | _read (req) { 153 | const self = this 154 | const data = req.data || this._alloc(req.size) 155 | const fd = this.fd 156 | 157 | if (!req.size) return process.nextTick(readEmpty, req) 158 | fs.read(fd, data, 0, req.size, req.offset, onread) 159 | 160 | function onread (err, read) { 161 | if (err) return req.callback(err) 162 | if (!read) return req.callback(createReadError(self.filename, req.offset, req.size)) 163 | 164 | req.size -= read 165 | req.offset += read 166 | 167 | if (!req.size) return req.callback(null, data) 168 | fs.read(fd, data, data.length - req.size, req.size, req.offset, onread) 169 | } 170 | } 171 | 172 | _del (req) { 173 | if (req.size === Infinity) return this._truncate(req) // TODO: remove this when all callsites use truncate 174 | 175 | if (!fsext) return req.callback(null) 176 | 177 | fsext.trim(this.fd, req.offset, req.size).then(ontrim, ontrim) 178 | 179 | function ontrim (err) { 180 | req.callback(err) 181 | } 182 | } 183 | 184 | _truncate (req) { 185 | fs.ftruncate(this.fd, req.offset, ontruncate) 186 | 187 | function ontruncate (err) { 188 | req.callback(err) 189 | } 190 | } 191 | 192 | _stat (req) { 193 | fs.fstat(this.fd, onstat) 194 | 195 | function onstat (err, st) { 196 | req.callback(err, st) 197 | } 198 | } 199 | 200 | _close (req) { 201 | const self = this 202 | 203 | fs.close(this.fd, onclose) 204 | 205 | function onclose (err) { 206 | if (err) return req.callback(err) 207 | if (self._pool !== null) self._pool._oninactive(self) 208 | self.fd = 0 209 | req.callback(null) 210 | } 211 | } 212 | 213 | _unlink (req) { 214 | const self = this 215 | 216 | const root = this.directory && path.resolve(path.join(this.directory, '.')) 217 | let dir = path.resolve(path.dirname(this.filename)) 218 | 219 | fs.unlink(this.filename, onunlink) 220 | 221 | function onunlink (err) { 222 | // if the file isn't there, its already unlinked, ignore 223 | if (err && err.code === 'ENOENT') err = null 224 | 225 | if (err || !self._rmdir || !root || dir === root) return req.callback(err) 226 | fs.rmdir(dir, onrmdir) 227 | } 228 | 229 | function onrmdir (err) { 230 | dir = path.join(dir, '..') 231 | if (err || dir === root) return req.callback(null) 232 | fs.rmdir(dir, onrmdir) 233 | } 234 | } 235 | } 236 | 237 | function readEmpty (req) { 238 | req.callback(null, Buffer.alloc(0)) 239 | } 240 | 241 | function createLockError (path) { 242 | const err = new Error('ELOCKED: File is locked') 243 | err.code = 'ELOCKED' 244 | err.path = path 245 | return err 246 | } 247 | 248 | function createReadError (path, offset, size) { 249 | const err = new Error('EPARTIALREAD: Could not satisfy length') 250 | err.code = 'EPARTIALREAD' 251 | err.path = path 252 | err.offset = offset 253 | err.size = size 254 | return err 255 | } 256 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | const test = require('brittle') 2 | const os = require('os') 3 | const path = require('path') 4 | const fs = require('fs') 5 | const RAF = require('..') 6 | 7 | const tmp = path.join(os.tmpdir(), 'random-access-file-' + process.pid + '-' + Date.now()) 8 | let i = 0 9 | 10 | fs.mkdirSync(tmp, { recursive: true }) 11 | 12 | test('write and read', function (t) { 13 | t.plan(4) 14 | 15 | const file = new RAF(gen()) 16 | 17 | file.write(0, Buffer.from('hello'), function (err) { 18 | t.absent(err, 'no error') 19 | file.read(0, 5, function (err, buf) { 20 | t.absent(err, 'no error') 21 | t.alike(buf, Buffer.from('hello')) 22 | file.unlink(() => t.pass()) 23 | }) 24 | }) 25 | }) 26 | 27 | test('read before write', function (t) { 28 | t.plan(2) 29 | 30 | const file = new RAF(gen()) 31 | 32 | file.read(0, 0, function (err, buf) { 33 | t.ok(err, 'not created') 34 | file.unlink(() => t.pass()) 35 | }) 36 | }) 37 | 38 | test('read range before write', function (t) { 39 | t.plan(2) 40 | 41 | const file = new RAF(gen()) 42 | 43 | file.read(0, 5, function (err, buf) { 44 | t.ok(err, 'not created') 45 | file.unlink(() => t.pass()) 46 | }) 47 | }) 48 | 49 | test('read range > file', function (t) { 50 | t.plan(3) 51 | 52 | const file = new RAF(gen()) 53 | 54 | file.write(0, Buffer.from('hello'), function (err) { 55 | t.absent(err, 'no error') 56 | file.read(0, 10, function (err, buf) { 57 | t.ok(err, 'not satisfiable') 58 | file.unlink(() => t.pass()) 59 | }) 60 | }) 61 | }) 62 | 63 | test('random access write and read', function (t) { 64 | t.plan(8) 65 | 66 | const file = new RAF(gen()) 67 | 68 | file.write(10, Buffer.from('hi'), function (err) { 69 | t.absent(err, 'no error') 70 | file.write(0, Buffer.from('hello'), function (err) { 71 | t.absent(err, 'no error') 72 | file.read(10, 2, function (err, buf) { 73 | t.absent(err, 'no error') 74 | t.alike(buf, Buffer.from('hi')) 75 | file.read(0, 5, function (err, buf) { 76 | t.absent(err, 'no error') 77 | t.alike(buf, Buffer.from('hello')) 78 | file.read(5, 5, function (err, buf) { 79 | t.absent(err, 'no error') 80 | t.alike(buf, Buffer.from([0, 0, 0, 0, 0])) 81 | }) 82 | }) 83 | }) 84 | }) 85 | }) 86 | }) 87 | 88 | test('re-open', function (t) { 89 | t.plan(4) 90 | 91 | const name = gen() 92 | const file = new RAF(name) 93 | 94 | file.write(10, Buffer.from('hello'), function (err) { 95 | t.absent(err, 'no error') 96 | file.close(function (err) { 97 | t.absent(err, 'no error') 98 | const file2 = new RAF(name) 99 | file2.read(10, 5, function (err, buf) { 100 | t.absent(err, 'no error') 101 | t.alike(buf, Buffer.from('hello')) 102 | }) 103 | }) 104 | }) 105 | }) 106 | 107 | test('re-open and truncate', function (t) { 108 | t.plan(3) 109 | 110 | const name = gen() 111 | const file = new RAF(name) 112 | 113 | file.write(10, Buffer.from('hello'), function (err) { 114 | t.absent(err, 'no error') 115 | file.close(function (err) { 116 | t.absent(err, 'no error') 117 | const file2 = new RAF(name, { truncate: true }) 118 | file2.read(10, 5, function (err, buf) { 119 | t.ok(err, 'file should be truncated') 120 | }) 121 | }) 122 | }) 123 | }) 124 | 125 | test('truncate with size', function (t) { 126 | t.plan(3) 127 | 128 | const file = new RAF(gen(), { size: 100 }) 129 | 130 | file.stat(function (err, st) { 131 | t.absent(err, 'no error') 132 | t.is(st.size, 100) 133 | file.unlink(() => t.pass()) 134 | }) 135 | }) 136 | 137 | test('bad open', { 138 | // windows apparently allow you to open dirs :/ 139 | skip: process.platform === 'win32' 140 | }, function (t) { 141 | t.plan(2) 142 | 143 | const file = new RAF(tmp) 144 | 145 | file.open(function (err) { 146 | t.ok(err) 147 | file.close(() => t.pass()) 148 | }) 149 | }) 150 | 151 | test('mkdir path', function (t) { 152 | t.plan(4) 153 | 154 | const name = path.join(tmp, ++i + '-folder', 'test.txt') 155 | const file = new RAF(name) 156 | 157 | file.write(0, Buffer.from('hello'), function (err) { 158 | t.absent(err, 'no error') 159 | file.read(0, 5, function (err, buf) { 160 | t.absent(err, 'no error') 161 | t.alike(buf, Buffer.from('hello')) 162 | file.unlink(() => t.pass()) 163 | }) 164 | }) 165 | }) 166 | 167 | test('write/read big chunks', async function (t) { 168 | t.plan(2) 169 | 170 | const file = new RAF(gen()) 171 | const bigBuffer = Buffer.alloc(10 * 1024 * 1024) 172 | 173 | bigBuffer.fill('hey. hey. how are you doing?. i am good thanks how about you? i am good') 174 | 175 | const io = t.test('write and read') 176 | io.plan(6) 177 | 178 | file.write(0, bigBuffer, function (err) { 179 | io.absent(err, 'no error') 180 | file.read(0, bigBuffer.length, function (err, buf) { 181 | io.absent(err, 'no error') 182 | io.ok(buf.equals(bigBuffer)) 183 | }) 184 | }) 185 | file.write(bigBuffer.length * 2, bigBuffer, function (err) { 186 | io.absent(err, 'no error') 187 | file.read(bigBuffer.length * 2, bigBuffer.length, function (err, buf) { 188 | io.absent(err, 'no error') 189 | io.ok(buf.equals(bigBuffer)) 190 | }) 191 | }) 192 | 193 | await io 194 | 195 | file.unlink(() => t.pass()) 196 | }) 197 | 198 | test('read tons of small chunks', function (t) { 199 | t.plan(1) 200 | const file = new RAF(gen()) 201 | const bigBuffer = Buffer.alloc(10 * 1024 * 1024) 202 | let same = true 203 | 204 | bigBuffer.fill('hey. hey. how are you doing?. i am good thanks how about you? i am good') 205 | 206 | file.write(0, bigBuffer, function () { 207 | let offset = 0 208 | file.read(offset, 128, function loop (_, buf) { 209 | if (same) same = buf.equals(bigBuffer.subarray(offset, offset + 128)) 210 | offset += 128 211 | if (offset >= bigBuffer.byteLength) { 212 | t.ok(same, 'all sub chunks match') 213 | t.end() 214 | } else { 215 | file.read(offset, 128, loop) 216 | } 217 | }) 218 | }) 219 | }) 220 | 221 | test('rmdir option', function (t) { 222 | t.plan(5) 223 | 224 | const name = path.join('rmdir', ++i + '', 'folder', 'test.txt') 225 | const file = new RAF(name, { rmdir: true, directory: tmp }) 226 | 227 | file.write(0, Buffer.from('hi'), function (err) { 228 | t.absent(err, 'no error') 229 | file.read(0, 2, function (err, buf) { 230 | t.absent(err, 'no error') 231 | t.alike(buf, Buffer.from('hi')) 232 | file.unlink(onunlink) 233 | }) 234 | }) 235 | 236 | function onunlink (err) { 237 | t.absent(err, 'no error') 238 | fs.stat(path.join(tmp, 'rmdir'), function (err) { 239 | t.is(err && err.code, 'ENOENT', 'should be removed') 240 | }) 241 | } 242 | }) 243 | 244 | test('rmdir option with non empty parent', function (t) { 245 | t.plan(7) 246 | 247 | const name = path.join('rmdir', ++i + '', 'folder', 'test.txt') 248 | const nonEmpty = path.join(tmp, name, '../..') 249 | const file = new RAF(name, { rmdir: true, directory: tmp }) 250 | 251 | file.write(0, Buffer.from('hi'), function (err) { 252 | t.absent(err, 'no error') 253 | fs.writeFileSync(path.join(nonEmpty, 'thing'), '') 254 | file.read(0, 2, function (err, buf) { 255 | t.absent(err, 'no error') 256 | t.alike(buf, Buffer.from('hi')) 257 | file.unlink(onunlink) 258 | }) 259 | }) 260 | 261 | function onunlink (err) { 262 | t.absent(err, 'no error') 263 | fs.stat(path.join(tmp, 'rmdir'), function (err) { 264 | t.absent(err, 'should not be removed') 265 | fs.readdir(nonEmpty, function (err, list) { 266 | t.absent(err, 'no error') 267 | t.alike(list, ['thing'], 'should only be one entry') 268 | }) 269 | }) 270 | } 271 | }) 272 | 273 | test('del, partial file block', function (t) { 274 | t.plan(8) 275 | 276 | const file = new RAF(gen()) 277 | 278 | file.write(0, Buffer.alloc(100, 0xff), function (err) { 279 | t.absent(err, 'no error') 280 | file.del(0, 40, function (err) { 281 | t.absent(err, 'no error') 282 | file.read(0, 40, function (err, buf) { 283 | t.absent(err, 'no error') 284 | t.alike(buf, Buffer.alloc(40)) 285 | file.del(50, 50, function (err) { 286 | t.absent(err, 'no error') 287 | file.read(50, 50, function (err, buf) { 288 | t.absent(err, 'no error') 289 | t.alike(buf, Buffer.alloc(50)) 290 | file.unlink(() => t.pass()) 291 | }) 292 | }) 293 | }) 294 | }) 295 | }) 296 | }) 297 | 298 | test('del, whole file block', function (t) { 299 | t.plan(7) 300 | 301 | const file = new RAF(gen(), { truncate: true, sparse: true }) 302 | 303 | file.stat(function (err, st) { 304 | t.absent(err, 'no error') 305 | file.write(0, Buffer.alloc(st.blksize * 100), function (err) { 306 | t.absent(err, 'no error') 307 | file.stat(function (err, before) { 308 | t.absent(err, 'no error') 309 | file.del(st.blksize * 20, st.blksize * 50, function (err) { 310 | t.absent(err, 'no error') 311 | file.stat(function (err, after) { 312 | t.absent(err, 'no error') 313 | t.comment(before.blocks + ' -> ' + after.blocks + ' blocks') 314 | t.ok(after.blocks < before.blocks, 'fewer blocks') 315 | file.unlink(() => t.pass()) 316 | }) 317 | }) 318 | }) 319 | }) 320 | }) 321 | }) 322 | 323 | test('del, partial and whole', function (t) { 324 | t.plan(7) 325 | 326 | const file = new RAF(gen(), { truncate: true, sparse: true }) 327 | 328 | file.stat(function (err, st) { 329 | t.absent(err, 'no error') 330 | file.write(0, Buffer.alloc(st.blksize * 100), function (err) { 331 | t.absent(err, 'no error') 332 | file.stat(function (err, before) { 333 | t.absent(err, 'no error') 334 | file.del(st.blksize * 20 - 483, st.blksize * 50 + 851, function (err) { 335 | t.absent(err, 'no error') 336 | file.stat(function (err, after) { 337 | t.absent(err, 'no error') 338 | t.comment(before.blocks + ' -> ' + after.blocks + ' blocks') 339 | t.ok(after.blocks < before.blocks, 'fewer blocks') 340 | file.unlink(() => t.pass()) 341 | }) 342 | }) 343 | }) 344 | }) 345 | }) 346 | }) 347 | 348 | // TODO: remove this test when we deprecate this usage of delete 349 | test('del, infinity', function (t) { 350 | t.plan(4) 351 | 352 | const file = new RAF(gen(), { size: 100 }) 353 | 354 | file.del(0, Infinity, function (err) { 355 | t.absent(err, 'no error') 356 | file.stat(function (err, st) { 357 | t.absent(err, 'no error') 358 | t.is(st.size, 0) 359 | file.unlink(() => t.pass()) 360 | }) 361 | }) 362 | }) 363 | 364 | test('truncate', function (t) { 365 | t.plan(7) 366 | 367 | const file = new RAF(gen(), { size: 100 }) 368 | 369 | file.truncate(50, function (err) { 370 | t.absent(err, 'no error') 371 | file.stat(function (err, st) { 372 | t.absent(err, 'no error') 373 | t.is(st.size, 50) 374 | file.truncate(20, function (err) { 375 | t.absent(err, 'no error') 376 | file.stat(function (err, st) { 377 | t.absent(err, 'no error') 378 | t.is(st.size, 20) 379 | file.unlink(() => t.pass()) 380 | }) 381 | }) 382 | }) 383 | }) 384 | }) 385 | 386 | test('open and close many times', function (t) { 387 | t.timeout(120000) // on ci sometimes this takes a while 388 | t.plan(3) 389 | 390 | const name = gen() 391 | const file = new RAF(name) 392 | const buf = Buffer.alloc(4) 393 | 394 | file.write(0, buf, function (err) { 395 | t.absent(err, 'no error') 396 | file.close(function (err) { 397 | t.absent(err, 'no error') 398 | loop(5000, function (err) { 399 | t.absent(err, 'no error') 400 | }) 401 | }) 402 | }) 403 | 404 | function loop (n, cb) { 405 | const file = new RAF(name) 406 | file.read(0, 4, function (err, buffer) { 407 | if (err) return cb(err) 408 | if (!buf.equals(buffer)) { 409 | t.alike(buffer, buf) 410 | return cb() 411 | } 412 | buf.writeUInt32BE(n) 413 | file.write(0, buf, function (err) { 414 | if (err) return cb(err) 415 | file.close(function (err) { 416 | if (!n || err) return cb(err) 417 | loop(n - 1, cb) 418 | }) 419 | }) 420 | }) 421 | } 422 | }) 423 | 424 | test('cannot escape directory', function (t) { 425 | t.plan(2) 426 | 427 | const name = '../../../../../../../../../../../../../tmp' 428 | const file = new RAF(name, { truncate: true, directory: tmp }) 429 | 430 | file.open(function (err) { 431 | t.absent(err, 'no error') 432 | t.is(file.filename, path.join(tmp, 'tmp')) 433 | }) 434 | }) 435 | 436 | test('directory filename resolves correctly', function (t) { 437 | const name = 'test.txt' 438 | const file = new RAF(name, { directory: tmp }) 439 | t.is(file.filename, path.join(tmp, name)) 440 | }) 441 | 442 | test('unlink', async function (t) { 443 | t.plan(5) 444 | 445 | const name = gen() 446 | const file = new RAF(name) 447 | 448 | file.write(0, Buffer.from('hi'), function (err) { 449 | t.absent(err, 'no error') 450 | file.read(0, 2, function (err, buf) { 451 | t.absent(err, 'no error') 452 | t.alike(buf, Buffer.from('hi')) 453 | file.unlink(onunlink) 454 | }) 455 | }) 456 | 457 | function onunlink (err) { 458 | t.absent(err, 'no error') 459 | fs.stat(name, function (err) { 460 | t.is(err && err.code, 'ENOENT', 'should be removed') 461 | }) 462 | } 463 | }) 464 | 465 | test('unlink on uncreated file does not reject', async function (t) { 466 | t.plan(2) 467 | const name = gen() 468 | 469 | const file = new RAF(name) 470 | t.is(fs.existsSync(file.filename), false) // Not yet created, since no write 471 | 472 | file.unlink(onunlink) 473 | 474 | function onunlink (err) { 475 | t.absent(err, 'no error') 476 | } 477 | }) 478 | 479 | test('pool', function (t) { 480 | t.plan(8) 481 | 482 | const pool = RAF.createPool(2) 483 | 484 | const a = new RAF(gen(), { pool }) 485 | const b = new RAF(gen(), { pool }) 486 | const c = new RAF(gen(), { pool }) 487 | 488 | a.write(0, Buffer.from('hello'), function (err) { 489 | t.absent(err, 'no error') 490 | b.write(0, Buffer.from('hello'), function (err) { 491 | t.absent(err, 'no error') 492 | c.write(0, Buffer.from('hello'), function (err) { 493 | t.absent(err, 'no error') 494 | setTimeout(function () { 495 | t.is(pool.active.length, 2) 496 | const all = [a, b, c] 497 | t.is(all.filter(f => f.suspended).length, 1) 498 | 499 | for (const f of all) { 500 | f.read(0, 5, function (_, buf) { 501 | t.alike(buf, Buffer.from('hello')) 502 | }) 503 | } 504 | }, 100) 505 | }) 506 | }) 507 | }) 508 | }) 509 | 510 | test('readonly mode', function (t) { 511 | t.plan(1) 512 | 513 | const filename = gen() 514 | const f = new RAF(filename) 515 | 516 | f.write(0, Buffer.from('hello world'), function () { 517 | f.close(function () { 518 | const r = new RAF(filename, { writable: false }) 519 | 520 | r.read(0, 5, function (_, data) { 521 | t.alike(data, Buffer.from('hello')) 522 | r.close() 523 | }) 524 | }) 525 | }) 526 | }) 527 | 528 | test('readonly mode + sparse', function (t) { 529 | t.plan(1) 530 | 531 | const filename = gen() 532 | const f = new RAF(filename, { sparse: true }) 533 | 534 | f.write(0, Buffer.from('hello world'), function () { 535 | f.close(function () { 536 | const r = new RAF(filename, { writable: false, sparse: true }) 537 | 538 | r.read(0, 5, function (_, data) { 539 | t.alike(data, Buffer.from('hello')) 540 | r.close() 541 | }) 542 | }) 543 | }) 544 | }) 545 | 546 | function gen () { 547 | return path.join(tmp, ++i + '.txt') 548 | } 549 | --------------------------------------------------------------------------------