From 91feaa40d5537bb24154f55ddce66aa962b78267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Tue, 5 Mar 2024 10:22:08 +0000 Subject: [PATCH 1/6] feat: support listing stores in local server --- src/server.test.ts | 54 +++++++++++++++++++++++++++++++++++++++++++++- src/server.ts | 39 +++++++++++++++++++++++++++------ 2 files changed, 85 insertions(+), 8 deletions(-) diff --git a/src/server.test.ts b/src/server.test.ts index 0e7264a..e3c7429 100644 --- a/src/server.test.ts +++ b/src/server.test.ts @@ -5,7 +5,7 @@ import semver from 'semver' import tmp from 'tmp-promise' import { test, expect, beforeAll, afterEach } from 'vitest' -import { getDeployStore, getStore } from './main.js' +import { getDeployStore, getStore, listStores } from './main.js' import { BlobsServer } from './server.js' beforeAll(async () => { @@ -311,3 +311,55 @@ test('Works with a deploy-scoped store', async () => { await server.stop() await fs.rm(directory.path, { force: true, recursive: true }) }) + +test('Lists site stores', async () => { + const directory = await tmp.dir() + const server = new BlobsServer({ + directory: directory.path, + token, + }) + const { port } = await server.start() + + const store1 = getStore({ + edgeURL: `http://localhost:${port}`, + name: 'coldplay', + token, + siteID, + }) + + await store1.set('parachutes/shiver', "I'll always be waiting for you") + await store1.set('parachutes/spies', 'And the spies came out of the water') + await store1.set('parachutes/trouble', 'And I:I never meant to cause you trouble') + await store1.set('a-rush-of-blood-to-the-head/politik', 'Give me heart and give me soul') + await store1.set('a-rush-of-blood-to-the-head/in-my-place', 'How long must you wait for it?') + await store1.set('a-rush-of-blood-to-the-head/the-scientist', 'Questions of science, science and progress') + + const store2 = getStore({ + edgeURL: `http://localhost:${port}`, + name: 'phoenix', + token, + siteID, + }) + + await store2.set('united/too-young', "Oh rainfalls and hard times coming they won't leave me tonight") + await store2.set('united/party-time', 'Summertime is gone') + await store2.set('ti-amo/j-boy', 'Something in the middle of the side of the store') + await store2.set('ti-amo/fleur-de-lys', 'No rest till I get to you, no rest till I get to you') + + const store3 = getDeployStore({ + deployID: '655f77a1b48f470008e5879a', + edgeURL: `http://localhost:${port}`, + token, + siteID, + }) + + await store3.set('not-a-song', "I'm a deploy, not a song") + + const { stores } = await listStores({ + edgeURL: `http://localhost:${port}`, + token, + siteID, + }) + + expect(stores).toStrictEqual(['coldplay', 'phoenix']) +}) diff --git a/src/server.ts b/src/server.ts index cd13e20..576ab5d 100644 --- a/src/server.ts +++ b/src/server.ts @@ -141,13 +141,19 @@ export class BlobsServer { const { dataPath, key, metadataPath, rootPath } = this.getLocalPaths(url) - if (!dataPath || !metadataPath) { + // If there's no root path, the request is invalid. + if (!rootPath) { return this.sendResponse(req, res, 400) } + // If there's no data or metadata paths, it means we're listing stores. + if (!dataPath || !metadataPath) { + return this.listStores(req, res, rootPath, url.searchParams.get('prefix') ?? '') + } + // If there is no key in the URL, it means a `list` operation. if (!key) { - return this.list({ dataPath, metadataPath, rootPath, req, res, url }) + return this.listBlobs({ dataPath, metadataPath, rootPath, req, res, url }) } this.onRequest({ type: Operation.GET }) @@ -213,7 +219,7 @@ export class BlobsServer { res.end() } - async list(options: { + async listBlobs(options: { dataPath: string metadataPath: string rootPath: string @@ -248,6 +254,19 @@ export class BlobsServer { return this.sendResponse(req, res, 200, JSON.stringify(result)) } + async listStores(req: http.IncomingMessage, res: http.ServerResponse, rootPath: string, prefix: string) { + try { + const allStores = await fs.readdir(rootPath) + const filteredStores = allStores.filter((store) => store.startsWith(prefix)) + + return this.sendResponse(req, res, 200, JSON.stringify({ stores: filteredStores })) + } catch (error) { + this.logDebug('Could not list stores:', error) + + return this.sendResponse(req, res, 500) + } + } + async put(req: http.IncomingMessage, res: http.ServerResponse) { const apiMatch = this.parseAPIRequest(req) @@ -304,18 +323,24 @@ export class BlobsServer { const [, siteID, rawStoreName, ...key] = url.pathname.split('/') - if (!siteID || !rawStoreName) { + if (!siteID) { return {} } + const rootPath = resolve(this.directory, 'entries', siteID) + + if (!rawStoreName) { + return { rootPath } + } + // On Windows, file paths can't include the `:` character, which is used in // deploy-scoped stores. const storeName = platform === 'win32' ? encodeURIComponent(rawStoreName) : rawStoreName - const rootPath = resolve(this.directory, 'entries', siteID, storeName) - const dataPath = resolve(rootPath, ...key) + const storePath = resolve(rootPath, storeName) + const dataPath = resolve(storePath, ...key) const metadataPath = resolve(this.directory, 'metadata', siteID, storeName, ...key) - return { dataPath, key: key.join('/'), metadataPath, rootPath } + return { dataPath, key: key.join('/'), metadataPath, rootPath: storePath } } handleRequest(req: http.IncomingMessage, res: http.ServerResponse) { From 50cb0713dcabe91a5277444d4ead4254cb3f87d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Tue, 5 Mar 2024 10:24:31 +0000 Subject: [PATCH 2/6] chore: cleanup test --- src/server.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/server.test.ts b/src/server.test.ts index e3c7429..68692c7 100644 --- a/src/server.test.ts +++ b/src/server.test.ts @@ -361,5 +361,8 @@ test('Lists site stores', async () => { siteID, }) + await server.stop() + await fs.rm(directory.path, { force: true, recursive: true }) + expect(stores).toStrictEqual(['coldplay', 'phoenix']) }) From 64368f0e5439eee555813bba3cf53d711261e4d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Tue, 5 Mar 2024 10:28:12 +0000 Subject: [PATCH 3/6] fix: encode directory for Windows --- src/server.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/server.ts b/src/server.ts index 576ab5d..c2685c2 100644 --- a/src/server.ts +++ b/src/server.ts @@ -257,7 +257,9 @@ export class BlobsServer { async listStores(req: http.IncomingMessage, res: http.ServerResponse, rootPath: string, prefix: string) { try { const allStores = await fs.readdir(rootPath) - const filteredStores = allStores.filter((store) => store.startsWith(prefix)) + const filteredStores = allStores.filter((store) => + store.startsWith(platform === 'win32' ? encodeURIComponent(prefix) : prefix), + ) return this.sendResponse(req, res, 200, JSON.stringify({ stores: filteredStores })) } catch (error) { From e08bd0500f98aab35b7ad09349b98a4279548ebb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Tue, 5 Mar 2024 10:32:34 +0000 Subject: [PATCH 4/6] fix: fix Windows paths --- src/server.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/server.ts b/src/server.ts index c2685c2..bab541e 100644 --- a/src/server.ts +++ b/src/server.ts @@ -257,9 +257,9 @@ export class BlobsServer { async listStores(req: http.IncomingMessage, res: http.ServerResponse, rootPath: string, prefix: string) { try { const allStores = await fs.readdir(rootPath) - const filteredStores = allStores.filter((store) => - store.startsWith(platform === 'win32' ? encodeURIComponent(prefix) : prefix), - ) + const filteredStores = allStores + .map((store) => (platform === 'win32' ? decodeURIComponent(store) : store)) + .filter((store) => store.startsWith(prefix)) return this.sendResponse(req, res, 200, JSON.stringify({ stores: filteredStores })) } catch (error) { From 723e398ff6c8dede4f3c4cfe97c6e33747a0c6f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Tue, 5 Mar 2024 10:36:29 +0000 Subject: [PATCH 5/6] chore: add comments --- src/server.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/server.ts b/src/server.ts index bab541e..803aa3e 100644 --- a/src/server.ts +++ b/src/server.ts @@ -258,6 +258,7 @@ export class BlobsServer { try { const allStores = await fs.readdir(rootPath) const filteredStores = allStores + // Store names are URI-encoded on Windows, so we must decode them first. .map((store) => (platform === 'win32' ? decodeURIComponent(store) : store)) .filter((store) => store.startsWith(prefix)) @@ -335,8 +336,8 @@ export class BlobsServer { return { rootPath } } - // On Windows, file paths can't include the `:` character, which is used in - // deploy-scoped stores. + // On Windows, file paths can't include the `:` character, so we URI-encode + // them. const storeName = platform === 'win32' ? encodeURIComponent(rawStoreName) : rawStoreName const storePath = resolve(rootPath, storeName) const dataPath = resolve(storePath, ...key) From dcef17491e925c4e95848fbbe8ad21743e8c6af5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Tue, 5 Mar 2024 10:37:31 +0000 Subject: [PATCH 6/6] chore: update comment --- src/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server.ts b/src/server.ts index 803aa3e..4570ffe 100644 --- a/src/server.ts +++ b/src/server.ts @@ -151,7 +151,7 @@ export class BlobsServer { return this.listStores(req, res, rootPath, url.searchParams.get('prefix') ?? '') } - // If there is no key in the URL, it means a `list` operation. + // If there is no key in the URL, it means we're listing blobs. if (!key) { return this.listBlobs({ dataPath, metadataPath, rootPath, req, res, url }) }