UNPKG

s3leveldown

Version:

An implementation of LevelDOWN that uses Amazon S3. Turn your S3 bucket into a DB

344 lines (290 loc) 9.64 kB
const AbstractLevelDOWN = require('abstract-leveldown').AbstractLevelDOWN , AbstractIterator = require('abstract-leveldown').AbstractIterator , ltgt = require('ltgt') , debug = require('debug')('S3LevelDown') , AWS = require('@aws-sdk/client-s3') const staticS3 = new AWS.S3Client({ apiVersion: '2006-03-01' }) function lt(value) { return ltgt.compare(value, this._finish) < 0 } function lte(value) { return ltgt.compare(value, this._finish) <= 0 } function getStartAfterKey(key) { const keyMinusOneNum = (key.charCodeAt(key.length - 1) - 1) const keyMinusOne = keyMinusOneNum >= 0 ? (String.fromCharCode(keyMinusOneNum) + '\uFFFF') : '' return key.substring(0, key.length - 1) + keyMinusOne } function nullEmptyUndefined(v) { return typeof v === 'undefined' || v === null || v === '' } class S3Iterator extends AbstractIterator { constructor(db, options) { super(db) const self = this self._limit = options.limit if (self._limit === -1) self._limit = Infinity self.keyAsBuffer = options.keyAsBuffer !== false self.valueAsBuffer = options.valueAsBuffer !== false self.fetchValues = options.values self._reverse = options.reverse self._options = options self._done = 0 self.bucket = db.bucket self.db = db self.s3ListObjectMaxKeys = options.s3ListObjectMaxKeys || 1000 if (!self._reverse && self._limit < self.s3ListObjectMaxKeys) { self.s3ListObjectMaxKeys = self._limit } self._start = ltgt.lowerBound(options) self._finish = ltgt.upperBound(options) if (!nullEmptyUndefined(self._finish)) { if (ltgt.upperBoundInclusive(options)) self._test = lte else self._test = lt } if (!nullEmptyUndefined(self._start)) self.startAfter = ltgt.lowerBoundInclusive(options) ? getStartAfterKey(self._start) : self._start debug('new iterator %o', self._options) } _next(callback) { const self = this if (self._done++ >= self._limit || (self.data && self.dataUpto == self.data.length && !self.s3nextContinuationToken)) return setImmediate(callback) if (!self.data || self.dataUpto == self.data.length) { listObjects() } else { fireCallback() } function listObjects() { const params = { Bucket: self.bucket, MaxKeys: self.s3ListObjectMaxKeys } if (self.db.folderPrefix !== '') { params.Prefix = self.db.folderPrefix } if (self.s3nextContinuationToken) { params.ContinuationToken = self.s3nextContinuationToken debug('listObjectsV2 ContinuationToken %s', params.ContinuationToken) } else if (typeof self.startAfter !== 'undefined') { params.StartAfter = self.db.folderPrefix + self.startAfter } self.db.s3.send(new AWS.ListObjectsV2Command(params), function (err, data) { if (err) { debug('listObjectsV2 error %s', err.message) callback(err) } else { if (data.KeyCount === 0) { debug('listObjectsV2 empty') return setImmediate(callback) } debug('listObjectsV2 %d keys', data.KeyCount) if (self.data && self.dataUpto === 0) { self.data = self.data.concat(data.Contents) } else { self.data = data.Contents } self.dataUpto = 0 self.s3nextContinuationToken = data.NextContinuationToken if (self._reverse && self.s3nextContinuationToken && data.Contents.every(function (x) { return self._test(x.Key.substring(self.db.folderPrefix.length, x.Key.length)) })) { listObjects() } else { fireCallback() } } }) } function fireCallback() { let index, key for (; ;) { index = (!self._reverse) ? self.dataUpto : (self.data.length - 1 - self.dataUpto) const awskey = self.data[index].Key key = awskey.substring(self.db.folderPrefix.length, awskey.length) debug('iterator data index %d: %s', index, key) self.dataUpto++ if (self._test(key)) { break } if (!self._reverse || self.dataUpto === self.data.length) { return setImmediate(callback) } } if (self.fetchValues) { if (self.data[index].Size === 0) getCallback(null, '') else self.db._get(key, { asBuffer: self.valueAsBuffer }, getCallback) } else getCallback() function getCallback(err, value) { debug('iterator data getCallback %s = %s', key, value) if (err) { if (err.message == 'NotFound') { // collection changed while we were iterating, skip this key return setImmediate(function () { self._next(callback) }) } return setImmediate(function () { callback(err) }) } if (self.keyAsBuffer && !(key instanceof Buffer)) key = Buffer.from(key) if (!self.keyAsBuffer && (key instanceof Buffer)) key = key.toString('utf8') if (self.fetchValues) { if (self.valueAsBuffer && !(value instanceof Buffer)) value = Buffer.from(value) if (!self.valueAsBuffer && (value instanceof Buffer)) value = value.toString('utf8') } setImmediate(function () { debug('_next result %s=%s', key, value) callback(null, key, value) }) } } } _test() { return true } } class S3LevelDown extends AbstractLevelDOWN { constructor(location, s3) { super() if (typeof location !== 'string') { throw new Error('constructor requires a location string argument') } this.s3 = s3 || staticS3 if (location.indexOf('/') !== -1) { this.folderPrefix = location.substring(location.indexOf('/') + 1, location.length) + '/' this.bucket = location.substring(0, location.indexOf('/')) } else { this.folderPrefix = '' this.bucket = location } debug('db init %s %s', this.bucket, this.folderPrefix) } _open(options, callback) { this.s3.send(new AWS.HeadBucketCommand({ Bucket: this.bucket }), (err) => { if (err) { // error, bucket is not found if (options.createIfMissing && err['$metadata'].httpStatusCode === 404) { // try to create it this.s3.send(new AWS.CreateBucketCommand({ Bucket: this.bucket }), (err) => { if (err) { setImmediate(() => callback(err)) } else { setImmediate(callback) } }) } else { setImmediate(() => callback(new Error(`Bucket ${this.bucket} does not exists or is inaccessible`))) } } else { setImmediate(callback) } }) } _put(key, value, options, callback) { if (nullEmptyUndefined(value)) value = Buffer.from('') if (!(value instanceof Buffer || value instanceof String)) value = String(value) this.s3.send(new AWS.PutObjectCommand({ Bucket: this.bucket, Key: this.folderPrefix + key, Body: value }), function (err) { if (err) { debug('Error s3 upload: %s %s', key, err.message) callback(err) } else { debug('Successful s3 upload: %s', key) callback() } }) } _get(key, options, callback) { this.s3.send(new AWS.GetObjectCommand({ Bucket: this.bucket, Key: this.folderPrefix + key }), async function (err, data) { if (err) { debug('Error s3 getObject: %s %s', key, err.message) if (err.Code === 'NoSuchKey') { callback(new Error('NotFound')) } else { callback(err) } } else { let value try { debug('s3 getObject callback as %s: %s', options.asBuffer ? 'buf' : 'string', key) if (options && options.asBuffer) { const byteArray = await data.Body?.transformToByteArray() value = Buffer.from(byteArray.buffer, byteArray.byteOffset, byteArray.byteLength) } else { value = await data.Body?.transformToString('utf8') } } catch (err) { callback(err, null) return } callback(null, value) } }) } _del(key, options, callback) { this.s3.send(new AWS.DeleteObjectCommand({ Bucket: this.bucket, Key: this.folderPrefix + key }), function (err) { if (err) { debug('Error s3 delete: %s %s', key, err.message) callback(err) } else { debug('Successful s3 delete: %s', key) callback() } }) } _batch(array, options, callback) { const len = array.length, self = this let i = 0; function act(action, cb) { if (!action) { return setImmediate(cb) } const key = (action.key instanceof Buffer) ? action.key : String(action.key) const value = action.value if (action.type === 'put') { self._put(key, value, null, cb) } else if (action.type === 'del') { self._del(key, null, cb) } } function actCallback(err) { if (err) { return setImmediate(function () { callback(err) }) } if (++i >= len) { return setImmediate(callback) } act(array[i], actCallback) } act(array[i], actCallback) } _iterator(options) { return new S3Iterator(this, options) } } module.exports = S3LevelDown