UNPKG

6.3 kBJavaScriptView Raw
1const RandomAccessStorage = require('random-access-storage')
2const fs = require('fs')
3const path = require('path')
4const constants = fs.constants || require('constants') // eslint-disable-line n/no-deprecated-api
5
6let fsext = null
7try {
8 fsext = require('fs-native-extensions')
9} catch {
10 try { // tmp workaround for places where fsctl is bundled...
11 fsext = {
12 tryLock: require('fsctl').lock,
13 sparse: () => Promise.resolve()
14 }
15 } catch {}
16}
17
18const RDWR = constants.O_RDWR
19const RDONLY = constants.O_RDONLY
20const WRONLY = constants.O_WRONLY
21const CREAT = constants.O_CREAT
22
23class Pool {
24 constructor (maxSize) {
25 this.maxSize = maxSize
26 this.active = []
27 }
28
29 _onactive (file) {
30 // suspend a random one when the pool
31 if (this.active.length >= this.maxSize) {
32 const r = Math.floor(Math.random() * this.active.length)
33 this.active[r].suspend()
34 }
35
36 file._pi = this.active.push(file) - 1
37 }
38
39 _oninactive (file) {
40 const head = this.active.pop()
41 if (head !== file) {
42 head._pi = file._pi
43 this.active[head._pi] = head
44 }
45 }
46}
47
48module.exports = class RandomAccessFile extends RandomAccessStorage {
49 constructor (filename, opts = {}) {
50 const size = opts.size || (opts.truncate ? 0 : -1)
51
52 super()
53
54 if (opts.directory) filename = path.join(opts.directory, path.resolve('/', filename).replace(/^\w+:\\/, ''))
55
56 this.directory = opts.directory || null
57 this.filename = filename
58 this.fd = 0
59
60 const {
61 readable = true,
62 writable = true
63 } = opts
64
65 this.mode = readable && writable ? RDWR : (readable ? RDONLY : WRONLY)
66
67 this._pi = 0 // pool index
68 this._pool = opts.pool || null
69 this._size = size
70 this._rmdir = !!opts.rmdir
71 this._lock = opts.lock === true
72 this._sparse = opts.sparse === true
73 this._alloc = opts.alloc || Buffer.allocUnsafe
74 this._alwaysCreate = size >= 0
75 }
76
77 static createPool (maxSize) {
78 return new Pool(maxSize)
79 }
80
81 _open (req) {
82 const create = this._alwaysCreate || this.writing // .writing comes from RAS
83 const self = this
84 const mode = this.mode | (create ? CREAT : 0)
85
86 if (create) fs.mkdir(path.dirname(this.filename), { recursive: true }, ondir)
87 else ondir(null)
88
89 function ondir (err) {
90 if (err) return req.callback(err)
91 fs.open(self.filename, mode, onopen)
92 }
93
94 function onopen (err, fd) {
95 if (err) return onerror(err)
96
97 self.fd = fd
98
99 if (!self._lock || !fsext) return onlock(null)
100
101 // Should we aquire a read lock?
102 const shared = self.mode === RDONLY
103
104 if (fsext.tryLock(self.fd, { shared })) onlock(null)
105 else onlock(createLockError(self.filename))
106 }
107
108 function onlock (err) {
109 if (err) return onerrorafteropen(err)
110
111 if (!self._sparse || !fsext) return onsparse(null)
112
113 fsext.sparse(self.fd).then(onsparse, onsparse)
114 }
115
116 function onsparse (err) {
117 if (err) return onerrorafteropen(err)
118
119 if (self._size < 0) return ontruncate(null)
120
121 fs.ftruncate(self.fd, self._size, ontruncate)
122 }
123
124 function ontruncate (err) {
125 if (err) return onerrorafteropen(err)
126 if (self._pool !== null) self._pool._onactive(self)
127 req.callback(null)
128 }
129
130 function onerror (err) {
131 req.callback(err)
132 }
133
134 function onerrorafteropen (err) {
135 fs.close(self.fd, function () {
136 self.fd = 0
137 onerror(err)
138 })
139 }
140 }
141
142 _write (req) {
143 const data = req.data
144 const fd = this.fd
145
146 fs.write(fd, data, 0, req.size, req.offset, onwrite)
147
148 function onwrite (err, wrote) {
149 if (err) return req.callback(err)
150
151 req.size -= wrote
152 req.offset += wrote
153
154 if (!req.size) return req.callback(null)
155 fs.write(fd, data, data.length - req.size, req.size, req.offset, onwrite)
156 }
157 }
158
159 _read (req) {
160 const self = this
161 const data = req.data || this._alloc(req.size)
162 const fd = this.fd
163
164 if (!req.size) return process.nextTick(readEmpty, req)
165 fs.read(fd, data, 0, req.size, req.offset, onread)
166
167 function onread (err, read) {
168 if (err) return req.callback(err)
169 if (!read) return req.callback(createReadError(self.filename, req.offset, req.size))
170
171 req.size -= read
172 req.offset += read
173
174 if (!req.size) return req.callback(null, data)
175 fs.read(fd, data, data.length - req.size, req.size, req.offset, onread)
176 }
177 }
178
179 _del (req) {
180 if (req.size === Infinity) return this._truncate(req) // TODO: remove this when all callsites use truncate
181
182 if (!fsext) return req.callback(null)
183
184 fsext.trim(this.fd, req.offset, req.size).then(ontrim, ontrim)
185
186 function ontrim (err) {
187 req.callback(err)
188 }
189 }
190
191 _truncate (req) {
192 fs.ftruncate(this.fd, req.offset, ontruncate)
193
194 function ontruncate (err) {
195 req.callback(err)
196 }
197 }
198
199 _stat (req) {
200 fs.fstat(this.fd, onstat)
201
202 function onstat (err, st) {
203 req.callback(err, st)
204 }
205 }
206
207 _close (req) {
208 const self = this
209
210 fs.close(this.fd, onclose)
211
212 function onclose (err) {
213 if (err) return req.callback(err)
214 if (self._pool !== null) self._pool._oninactive(self)
215 self.fd = 0
216 req.callback(null)
217 }
218 }
219
220 _unlink (req) {
221 const self = this
222
223 const root = this.directory && path.resolve(path.join(this.directory, '.'))
224 let dir = path.resolve(path.dirname(this.filename))
225
226 fs.unlink(this.filename, onunlink)
227
228 function onunlink (err) {
229 // if the file isn't there, its already unlinked, ignore
230 if (err && err.code === 'ENOENT') err = null
231
232 if (err || !self._rmdir || !root || dir === root) return req.callback(err)
233 fs.rmdir(dir, onrmdir)
234 }
235
236 function onrmdir (err) {
237 dir = path.join(dir, '..')
238 if (err || dir === root) return req.callback(null)
239 fs.rmdir(dir, onrmdir)
240 }
241 }
242}
243
244function readEmpty (req) {
245 req.callback(null, Buffer.alloc(0))
246}
247
248function createLockError (path) {
249 const err = new Error('ELOCKED: File is locked')
250 err.code = 'ELOCKED'
251 err.path = path
252 return err
253}
254
255function createReadError (path, offset, size) {
256 const err = new Error('EPARTIALREAD: Could not satisfy length')
257 err.code = 'EPARTIALREAD'
258 err.path = path
259 err.offset = offset
260 err.size = size
261 return err
262}