UNPKG

7.31 kBJavaScriptView Raw
1/*! fs-chunk-store. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
2module.exports = Storage
3
4var fs = require('fs')
5var os = require('os')
6var parallel = require('run-parallel')
7var path = require('path')
8var raf = require('random-access-file')
9var randombytes = require('randombytes')
10var rimraf = require('rimraf')
11var thunky = require('thunky')
12
13var TMP
14try {
15 TMP = fs.statSync('/tmp') && '/tmp'
16} catch (err) {
17 TMP = os.tmpdir()
18}
19
20function Storage (chunkLength, opts) {
21 var self = this
22 if (!(self instanceof Storage)) return new Storage(chunkLength, opts)
23 if (!opts) opts = {}
24
25 self.chunkLength = Number(chunkLength)
26 if (!self.chunkLength) throw new Error('First argument must be a chunk length')
27
28 if (opts.files) {
29 if (!Array.isArray(opts.files)) {
30 throw new Error('`files` option must be an array')
31 }
32 self.files = opts.files.slice(0).map(function (file, i, files) {
33 if (file.path == null) throw new Error('File is missing `path` property')
34 if (file.length == null) throw new Error('File is missing `length` property')
35 if (file.offset == null) {
36 if (i === 0) {
37 file.offset = 0
38 } else {
39 var prevFile = files[i - 1]
40 file.offset = prevFile.offset + prevFile.length
41 }
42 }
43 return file
44 })
45 self.length = self.files.reduce(function (sum, file) { return sum + file.length }, 0)
46 if (opts.length != null && opts.length !== self.length) {
47 throw new Error('total `files` length is not equal to explicit `length` option')
48 }
49 } else {
50 var len = Number(opts.length) || Infinity
51 self.files = [{
52 offset: 0,
53 path: path.resolve(opts.path || path.join(TMP, 'fs-chunk-store', randombytes(20).toString('hex'))),
54 length: len
55 }]
56 self.length = len
57 }
58
59 self.chunkMap = []
60 self.closed = false
61
62 self.files.forEach(function (file) {
63 file.open = thunky(function (cb) {
64 if (self.closed) return cb(new Error('Storage is closed'))
65 fs.mkdir(path.dirname(file.path), { recursive: true }, function (err) {
66 if (err) return cb(err)
67 if (self.closed) return cb(new Error('Storage is closed'))
68 cb(null, raf(file.path))
69 })
70 })
71 })
72
73 // If the length is Infinity (i.e. a length was not specified) then the store will
74 // automatically grow.
75
76 if (self.length !== Infinity) {
77 self.lastChunkLength = (self.length % self.chunkLength) || self.chunkLength
78 self.lastChunkIndex = Math.ceil(self.length / self.chunkLength) - 1
79
80 self.files.forEach(function (file) {
81 var fileStart = file.offset
82 var fileEnd = file.offset + file.length
83
84 var firstChunk = Math.floor(fileStart / self.chunkLength)
85 var lastChunk = Math.floor((fileEnd - 1) / self.chunkLength)
86
87 for (var p = firstChunk; p <= lastChunk; ++p) {
88 var chunkStart = p * self.chunkLength
89 var chunkEnd = chunkStart + self.chunkLength
90
91 var from = (fileStart < chunkStart) ? 0 : fileStart - chunkStart
92 var to = (fileEnd > chunkEnd) ? self.chunkLength : fileEnd - chunkStart
93 var offset = (fileStart > chunkStart) ? 0 : chunkStart - fileStart
94
95 if (!self.chunkMap[p]) self.chunkMap[p] = []
96
97 self.chunkMap[p].push({
98 from,
99 to,
100 offset,
101 file
102 })
103 }
104 })
105 }
106}
107
108Storage.prototype.put = function (index, buf, cb) {
109 var self = this
110 if (typeof cb !== 'function') cb = noop
111 if (self.closed) return nextTick(cb, new Error('Storage is closed'))
112
113 var isLastChunk = (index === self.lastChunkIndex)
114 if (isLastChunk && buf.length !== self.lastChunkLength) {
115 return nextTick(cb, new Error('Last chunk length must be ' + self.lastChunkLength))
116 }
117 if (!isLastChunk && buf.length !== self.chunkLength) {
118 return nextTick(cb, new Error('Chunk length must be ' + self.chunkLength))
119 }
120
121 if (self.length === Infinity) {
122 self.files[0].open(function (err, file) {
123 if (err) return cb(err)
124 file.write(index * self.chunkLength, buf, cb)
125 })
126 } else {
127 var targets = self.chunkMap[index]
128 if (!targets) return nextTick(cb, new Error('no files matching the request range'))
129 var tasks = targets.map(function (target) {
130 return function (cb) {
131 target.file.open(function (err, file) {
132 if (err) return cb(err)
133 file.write(target.offset, buf.slice(target.from, target.to), cb)
134 })
135 }
136 })
137 parallel(tasks, cb)
138 }
139}
140
141Storage.prototype.get = function (index, opts, cb) {
142 var self = this
143 if (typeof opts === 'function') return self.get(index, null, opts)
144 if (self.closed) return nextTick(cb, new Error('Storage is closed'))
145
146 var chunkLength = (index === self.lastChunkIndex)
147 ? self.lastChunkLength
148 : self.chunkLength
149
150 var rangeFrom = (opts && opts.offset) || 0
151 var rangeTo = (opts && opts.length) ? rangeFrom + opts.length : chunkLength
152
153 if (rangeFrom < 0 || rangeFrom < 0 || rangeTo > chunkLength) {
154 return nextTick(cb, new Error('Invalid offset and/or length'))
155 }
156
157 if (self.length === Infinity) {
158 if (rangeFrom === rangeTo) return nextTick(cb, null, Buffer.from(0))
159 self.files[0].open(function (err, file) {
160 if (err) return cb(err)
161 var offset = (index * self.chunkLength) + rangeFrom
162 file.read(offset, rangeTo - rangeFrom, cb)
163 })
164 } else {
165 var targets = self.chunkMap[index]
166 if (!targets) return nextTick(cb, new Error('no files matching the request range'))
167 if (opts) {
168 targets = targets.filter(function (target) {
169 return target.to > rangeFrom && target.from < rangeTo
170 })
171 if (targets.length === 0) {
172 return nextTick(cb, new Error('no files matching the requested range'))
173 }
174 }
175 if (rangeFrom === rangeTo) return nextTick(cb, null, Buffer.from(0))
176
177 var tasks = targets.map(function (target) {
178 return function (cb) {
179 var from = target.from
180 var to = target.to
181 var offset = target.offset
182
183 if (opts) {
184 if (to > rangeTo) to = rangeTo
185 if (from < rangeFrom) {
186 offset += (rangeFrom - from)
187 from = rangeFrom
188 }
189 }
190
191 target.file.open(function (err, file) {
192 if (err) return cb(err)
193 file.read(offset, to - from, cb)
194 })
195 }
196 })
197
198 parallel(tasks, function (err, buffers) {
199 if (err) return cb(err)
200 cb(null, Buffer.concat(buffers))
201 })
202 }
203}
204
205Storage.prototype.close = function (cb) {
206 var self = this
207 if (self.closed) return nextTick(cb, new Error('Storage is closed'))
208 self.closed = true
209
210 var tasks = self.files.map(function (file) {
211 return function (cb) {
212 file.open(function (err, file) {
213 // an open error is okay because that means the file is not open
214 if (err) return cb(null)
215 file.close(cb)
216 })
217 }
218 })
219 parallel(tasks, cb)
220}
221
222Storage.prototype.destroy = function (cb) {
223 var self = this
224 self.close(function () {
225 var tasks = self.files.map(function (file) {
226 return function (cb) {
227 rimraf(file.path, { maxBusyTries: 10 }, cb)
228 }
229 })
230 parallel(tasks, cb)
231 })
232}
233
234function nextTick (cb, err, val) {
235 process.nextTick(function () {
236 if (cb) cb(err, val)
237 })
238}
239
240function noop () {}