Skip to content

Commit 8820cc8

Browse files
feat: support listing stores in local server (#151)
1 parent 713e2be commit 8820cc8

File tree

2 files changed

+94
-11
lines changed

2 files changed

+94
-11
lines changed

src/server.test.ts

+56-1
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,58 @@ 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+
await server.stop()
365+
await fs.rm(directory.path, { force: true, recursive: true })
366+
367+
expect(stores).toStrictEqual(['coldplay', 'phoenix'])
368+
})

src/server.ts

+38-10
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

148-
// If there is no key in the URL, it means a `list` operation.
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+
154+
// If there is no key in the URL, it means we're listing blobs.
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,22 @@ 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
261+
// Store names are URI-encoded on Windows, so we must decode them first.
262+
.map((store) => (platform === 'win32' ? decodeURIComponent(store) : store))
263+
.filter((store) => store.startsWith(prefix))
264+
265+
return this.sendResponse(req, res, 200, JSON.stringify({ stores: filteredStores }))
266+
} catch (error) {
267+
this.logDebug('Could not list stores:', error)
268+
269+
return this.sendResponse(req, res, 500)
270+
}
271+
}
272+
251273
async put(req: http.IncomingMessage, res: http.ServerResponse) {
252274
const apiMatch = this.parseAPIRequest(req)
253275

@@ -304,18 +326,24 @@ export class BlobsServer {
304326

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

307-
if (!siteID || !rawStoreName) {
329+
if (!siteID) {
308330
return {}
309331
}
310332

311-
// On Windows, file paths can't include the `:` character, which is used in
312-
// deploy-scoped stores.
333+
const rootPath = resolve(this.directory, 'entries', siteID)
334+
335+
if (!rawStoreName) {
336+
return { rootPath }
337+
}
338+
339+
// On Windows, file paths can't include the `:` character, so we URI-encode
340+
// them.
313341
const storeName = platform === 'win32' ? encodeURIComponent(rawStoreName) : rawStoreName
314-
const rootPath = resolve(this.directory, 'entries', siteID, storeName)
315-
const dataPath = resolve(rootPath, ...key)
342+
const storePath = resolve(rootPath, storeName)
343+
const dataPath = resolve(storePath, ...key)
316344
const metadataPath = resolve(this.directory, 'metadata', siteID, storeName, ...key)
317345

318-
return { dataPath, key: key.join('/'), metadataPath, rootPath }
346+
return { dataPath, key: key.join('/'), metadataPath, rootPath: storePath }
319347
}
320348

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

0 commit comments

Comments
 (0)