1 |
|
2 | module.exports = Storage
|
3 |
|
4 | var fs = require('fs')
|
5 | var os = require('os')
|
6 | var parallel = require('run-parallel')
|
7 | var path = require('path')
|
8 | var raf = require('random-access-file')
|
9 | var randombytes = require('randombytes')
|
10 | var rimraf = require('rimraf')
|
11 | var thunky = require('thunky')
|
12 |
|
13 | var TMP
|
14 | try {
|
15 | TMP = fs.statSync('/tmp') && '/tmp'
|
16 | } catch (err) {
|
17 | TMP = os.tmpdir()
|
18 | }
|
19 |
|
20 | function 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 |
|
74 |
|
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 |
|
108 | Storage.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 |
|
141 | Storage.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 |
|
205 | Storage.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 |
|
214 | if (err) return cb(null)
|
215 | file.close(cb)
|
216 | })
|
217 | }
|
218 | })
|
219 | parallel(tasks, cb)
|
220 | }
|
221 |
|
222 | Storage.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 |
|
234 | function nextTick (cb, err, val) {
|
235 | process.nextTick(function () {
|
236 | if (cb) cb(err, val)
|
237 | })
|
238 | }
|
239 |
|
240 | function noop () {}
|