Skip to content

Commit 91feaa4

Browse files
committed
feat: support listing stores in local server
1 parent 713e2be commit 91feaa4

File tree

2 files changed

+85
-8
lines changed

2 files changed

+85
-8
lines changed

src/server.test.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import semver from 'semver'
55
import tmp from 'tmp-promise'
66
import { test, expect, beforeAll, afterEach } from 'vitest'
77

8-
import { getDeployStore, getStore } from './main.js'
8+
import { getDeployStore, getStore, listStores } from './main.js'
99
import { BlobsServer } from './server.js'
1010

1111
beforeAll(async () => {
@@ -311,3 +311,55 @@ test('Works with a deploy-scoped store', async () => {
311311
await server.stop()
312312
await fs.rm(directory.path, { force: true, recursive: true })
313313
})
314+
315+
test('Lists site stores', async () => {
316+
const directory = await tmp.dir()
317+
const server = new BlobsServer({
318+
directory: directory.path,
319+
token,
320+
})
321+
const { port } = await server.start()
322+
323+
const store1 = getStore({
324+
edgeURL: `http://localhost:${port}`,
325+
name: 'coldplay',
326+
token,
327+
siteID,
328+
})
329+
330+
await store1.set('parachutes/shiver', "I'll always be waiting for you")
331+
await store1.set('parachutes/spies', 'And the spies came out of the water')
332+
await store1.set('parachutes/trouble', 'And I:I never meant to cause you trouble')
333+
await store1.set('a-rush-of-blood-to-the-head/politik', 'Give me heart and give me soul')
334+
await store1.set('a-rush-of-blood-to-the-head/in-my-place', 'How long must you wait for it?')
335+
await store1.set('a-rush-of-blood-to-the-head/the-scientist', 'Questions of science, science and progress')
336+
337+
const store2 = getStore({
338+
edgeURL: `http://localhost:${port}`,
339+
name: 'phoenix',
340+
token,
341+
siteID,
342+
})
343+
344+
await store2.set('united/too-young', "Oh rainfalls and hard times coming they won't leave me tonight")
345+
await store2.set('united/party-time', 'Summertime is gone')
346+
await store2.set('ti-amo/j-boy', 'Something in the middle of the side of the store')
347+
await store2.set('ti-amo/fleur-de-lys', 'No rest till I get to you, no rest till I get to you')
348+
349+
const store3 = getDeployStore({
350+
deployID: '655f77a1b48f470008e5879a',
351+
edgeURL: `http://localhost:${port}`,
352+
token,
353+
siteID,
354+
})
355+
356+
await store3.set('not-a-song', "I'm a deploy, not a song")
357+
358+
const { stores } = await listStores({
359+
edgeURL: `http://localhost:${port}`,
360+
token,
361+
siteID,
362+
})
363+
364+
expect(stores).toStrictEqual(['coldplay', 'phoenix'])
365+
})

src/server.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -141,13 +141,19 @@ export class BlobsServer {
141141

142142
const { dataPath, key, metadataPath, rootPath } = this.getLocalPaths(url)
143143

144-
if (!dataPath || !metadataPath) {
144+
// If there's no root path, the request is invalid.
145+
if (!rootPath) {
145146
return this.sendResponse(req, res, 400)
146147
}
147148

149+
// If there's no data or metadata paths, it means we're listing stores.
150+
if (!dataPath || !metadataPath) {
151+
return this.listStores(req, res, rootPath, url.searchParams.get('prefix') ?? '')
152+
}
153+
148154
// If there is no key in the URL, it means a `list` operation.
149155
if (!key) {
150-
return this.list({ dataPath, metadataPath, rootPath, req, res, url })
156+
return this.listBlobs({ dataPath, metadataPath, rootPath, req, res, url })
151157
}
152158

153159
this.onRequest({ type: Operation.GET })
@@ -213,7 +219,7 @@ export class BlobsServer {
213219
res.end()
214220
}
215221

216-
async list(options: {
222+
async listBlobs(options: {
217223
dataPath: string
218224
metadataPath: string
219225
rootPath: string
@@ -248,6 +254,19 @@ export class BlobsServer {
248254
return this.sendResponse(req, res, 200, JSON.stringify(result))
249255
}
250256

257+
async listStores(req: http.IncomingMessage, res: http.ServerResponse, rootPath: string, prefix: string) {
258+
try {
259+
const allStores = await fs.readdir(rootPath)
260+
const filteredStores = allStores.filter((store) => store.startsWith(prefix))
261+
262+
return this.sendResponse(req, res, 200, JSON.stringify({ stores: filteredStores }))
263+
} catch (error) {
264+
this.logDebug('Could not list stores:', error)
265+
266+
return this.sendResponse(req, res, 500)
267+
}
268+
}
269+
251270
async put(req: http.IncomingMessage, res: http.ServerResponse) {
252271
const apiMatch = this.parseAPIRequest(req)
253272

@@ -304,18 +323,24 @@ export class BlobsServer {
304323

305324
const [, siteID, rawStoreName, ...key] = url.pathname.split('/')
306325

307-
if (!siteID || !rawStoreName) {
326+
if (!siteID) {
308327
return {}
309328
}
310329

330+
const rootPath = resolve(this.directory, 'entries', siteID)
331+
332+
if (!rawStoreName) {
333+
return { rootPath }
334+
}
335+
311336
// On Windows, file paths can't include the `:` character, which is used in
312337
// deploy-scoped stores.
313338
const storeName = platform === 'win32' ? encodeURIComponent(rawStoreName) : rawStoreName
314-
const rootPath = resolve(this.directory, 'entries', siteID, storeName)
315-
const dataPath = resolve(rootPath, ...key)
339+
const storePath = resolve(rootPath, storeName)
340+
const dataPath = resolve(storePath, ...key)
316341
const metadataPath = resolve(this.directory, 'metadata', siteID, storeName, ...key)
317342

318-
return { dataPath, key: key.join('/'), metadataPath, rootPath }
343+
return { dataPath, key: key.join('/'), metadataPath, rootPath: storePath }
319344
}
320345

321346
handleRequest(req: http.IncomingMessage, res: http.ServerResponse) {

0 commit comments

Comments
 (0)