diff --git a/lib/server/index.js b/lib/server/index.js index 60ed0a4..9a669ea 100644 --- a/lib/server/index.js +++ b/lib/server/index.js @@ -216,6 +216,8 @@ class Relay { return } + const url = new URL('http://example.com' + req.url) + switch (req.method) { case 'OPTIONS': this._OPTIONS(req, res) @@ -223,9 +225,11 @@ class Relay { case 'GET': if (req.url.startsWith('/subscribe/')) { this._SUBSCRIBE(req, res) - return + } else if (url.pathname.endsWith('/')) { + this._QUERY(req, res, url) + } else { + this._GET(req, res) } - this._GET(req, res) break case 'PUT': this._PUT(req, res) @@ -491,6 +495,46 @@ class Relay { }) } + /** + * @param {string} id + * @param {string} directory + * @param {object} [options] + * @param {number} [options.limit] + * @param {number} [options.offset] + * + * @returns {lmdb.RangeIterable} + */ + _searchDirectory (id, directory, options = {}) { + const range = { start: '/' + id + directory, end: '/' + id + directory.slice(0, directory.length - 1) + '0', ...options } + + return this._recordsDB.getRange(range) + } + + /** + * @param {http.IncomingMessage} _req + * @param {http.ServerResponse} res + * @param {URL} url + */ + async _QUERY (_req, res, url) { + const [id, ...rest] = url.pathname.slice(1).split('/') + const dir = '/' + rest.join('/') + + const options = { + // Skip options until we really need them. currently, it should be cheap to return 1000s of keys at once. + + // limit: parseInt(url.searchParams.get('limit')) || 1000, + // offset: parseInt(url.searchParams.get('offset')) || 0 + } + + const keys = this._searchDirectory(id, dir, options).asArray.map(r => { + this._updateLastQueried(r.key) + return r.key + }) + + res.writeHead(200) + res.end(keys.join('\n')) + } + /** * Health check endpoint to provide server metrics. * diff --git a/test/server.js b/test/server.js index c7d35ba..df608b3 100644 --- a/test/server.js +++ b/test/server.js @@ -127,9 +127,9 @@ test('missing header', async (t) => { const response = await fetch( address + '/' + ZERO_ID + '/test.txt', { - method: 'PUT', - body: 'ffff' - }) + method: 'PUT', + body: 'ffff' + }) t.is(response.status, 400) t.is(response.statusText, `Missing or malformed header: '${HEADERS.RECORD}'`) @@ -535,11 +535,74 @@ test('save query dates to help prunning abandoned records later', async (t) => { relay.close() }) -function tmpdir () { +test('query all in a directory', async (t) => { + const relay = new Relay(tmpdir(), { _writeInterval: 1 }) + const address = await relay.listen() + + const keyPair = createKeyPair(ZERO_SEED) + + const LESSER_ID = '8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewn' + const GREATER_ID = '9pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo' + + for (let i = 0; i < 10; i++) { + const path = '/' + ZERO_ID + `/dir/subdir/foo${i}.txt` + const content = Buffer.from('foo content') + const record = await Record.create(keyPair, path, content, { timestamp: 1000 }) + + const bytes = record.serialize() + + await relay._recordsDB.put(path, bytes) + + /// False positive subdirectory + await relay._recordsDB.put('/' + ZERO_ID + `/dir/wrong/foo${i}.txt`, bytes) + + /// False positive IDs + await relay._recordsDB.put('/' + LESSER_ID + `/dir/subdir/foo${i}.txt`, bytes) + await relay._recordsDB.put('/' + GREATER_ID + `/dir/subdir/foo${i}.txt`, bytes) + } + + // let result = relay._recordsDB.getRange({ start: "/" + ZERO_ID + "/", end: "/" + ZERO_ID + "0" }).asArray.map(({ key, value }) => { + // return { key, value: Record.deserialize(value) } + // }) + // + // console.log(result) + + const headers = { + [HEADERS.CONTENT_TYPE]: 'application/octet-stream' + } + + const response = await fetch(address + '/' + ZERO_ID + '/dir/subdir/?something', { + method: 'GET', + headers + }) + + const keys = await response.text() + + const list = keys.split('\n') + + t.is(list.length, 10) + + t.alike(list, [ + '/8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo/dir/subdir/foo0.txt', + '/8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo/dir/subdir/foo1.txt', + '/8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo/dir/subdir/foo2.txt', + '/8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo/dir/subdir/foo3.txt', + '/8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo/dir/subdir/foo4.txt', + '/8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo/dir/subdir/foo5.txt', + '/8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo/dir/subdir/foo6.txt', + '/8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo/dir/subdir/foo7.txt', + '/8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo/dir/subdir/foo8.txt', + '/8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo/dir/subdir/foo9.txt' + ]) + + relay.close() +}) + +function tmpdir() { return path.join(os.tmpdir(), Math.random().toString(16).slice(2)) } /** @param {number} ms */ -function sleep (ms) { +function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)) }