-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into blobstore-createreadstream-options-#80
- Loading branch information
Showing
14 changed files
with
302 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import fs from 'node:fs' | ||
import { pipeline } from 'node:stream/promises' | ||
import sodium from 'sodium-universal' | ||
import b4a from 'b4a' | ||
|
||
import { getPort } from './blob-server/index.js' | ||
|
||
/** @typedef {import('./types.js').BlobId} BlobId */ | ||
/** @typedef {import('./types.js').BlobType} BlobType */ | ||
/** @typedef {import('./types.js').BlobVariant<BlobType>} BlobVariant */ | ||
|
||
export class BlobApi { | ||
/** | ||
* @param {object} options | ||
* @param {string} options.projectId | ||
* @param {import('./blob-store/index.js').BlobStore} options.blobStore | ||
* @param {import('fastify').FastifyInstance} options.blobServer | ||
*/ | ||
constructor({ projectId, blobStore, blobServer }) { | ||
this.projectId = projectId | ||
this.blobStore = blobStore | ||
this.blobServer = blobServer | ||
} | ||
|
||
/** | ||
* Get a url for a blob based on its BlobId | ||
* @param {import('./types.js').BlobId} blobId | ||
* @returns {Promise<string>} | ||
*/ | ||
async getUrl(blobId) { | ||
const { driveId, type, variant, name } = blobId | ||
const port = await getPort(this.blobServer.server) | ||
return `http://127.0.0.1:${port}/${this.projectId}/${driveId}/${type}/${variant}/${name}` | ||
} | ||
|
||
/** | ||
* Write blobs for provided variants of a file | ||
* @param {{ original: string, preview?: string, thumbnail?: string }} filepaths | ||
* @param {{ mimeType: string }} metadata | ||
* @returns {Promise<{ driveId: string, name: string, type: 'photo' | 'video' | 'audio' }>} | ||
*/ | ||
async create(filepaths, metadata) { | ||
const { original, preview, thumbnail } = filepaths | ||
const { mimeType } = metadata | ||
const blobType = getType(mimeType) | ||
const hash = b4a.alloc(8) | ||
sodium.randombytes_buf(hash) | ||
const name = hash.toString('hex') | ||
|
||
await this.writeFile( | ||
original, | ||
{ | ||
name: `${name}`, | ||
variant: 'original', | ||
type: blobType, | ||
}, | ||
metadata | ||
) | ||
|
||
if (preview) { | ||
await this.writeFile( | ||
preview, | ||
{ | ||
name: `${name}`, | ||
variant: 'preview', | ||
type: blobType, | ||
}, | ||
metadata | ||
) | ||
} | ||
|
||
if (thumbnail) { | ||
await this.writeFile( | ||
thumbnail, | ||
{ | ||
name: `${name}`, | ||
variant: 'thumbnail', | ||
type: blobType, | ||
}, | ||
metadata | ||
) | ||
} | ||
|
||
return { | ||
driveId: this.blobStore.writerDriveId, | ||
name, | ||
type: blobType, | ||
} | ||
} | ||
|
||
/** | ||
* @param {string} filepath | ||
* @param {Omit<BlobId, 'driveId'>} options | ||
* @param {object} metadata | ||
* @param {string} metadata.mimeType | ||
*/ | ||
async writeFile(filepath, { name, variant, type }, metadata) { | ||
// @ts-ignore TODO: return value types don't match pipeline's expectations, though they should | ||
await pipeline( | ||
fs.createReadStream(filepath), | ||
this.blobStore.createWriteStream({ type, variant, name }, { metadata }) | ||
) | ||
|
||
return { name, variant, type } | ||
} | ||
} | ||
|
||
/** | ||
* @param {string} mimeType | ||
* @returns {BlobType} | ||
*/ | ||
function getType(mimeType) { | ||
if (mimeType.startsWith('image')) return 'photo' | ||
if (mimeType.startsWith('video')) return 'video' | ||
if (mimeType.startsWith('audio')) return 'audio' | ||
|
||
throw new Error(`Unsupported mimeType: ${mimeType}`) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import { join } from 'node:path' | ||
import { fileURLToPath } from 'url' | ||
import test from 'brittle' | ||
import { BlobApi } from '../src/blob-api.js' | ||
import { createBlobServer, getPort } from '../src/blob-server/index.js' | ||
import { createBlobStore } from './helpers/blob-store.js' | ||
import { timeoutException } from './helpers/index.js' | ||
|
||
test('get port after listening event with explicit port', async (t) => { | ||
const blobStore = createBlobStore() | ||
const server = await createBlobServer({ blobStore }) | ||
|
||
t.ok(await timeoutException(getPort(server.server))) | ||
|
||
await new Promise((resolve) => { | ||
server.listen({ port: 3456 }, (err, address) => { | ||
resolve(address) | ||
}) | ||
}) | ||
|
||
const port = await getPort(server.server) | ||
|
||
t.is(typeof port, 'number') | ||
t.is(port, 3456) | ||
|
||
t.teardown(async () => { | ||
await server.close() | ||
}) | ||
}) | ||
|
||
test('get port after listening event with unset port', async (t) => { | ||
const blobStore = createBlobStore() | ||
const server = await createBlobServer({ blobStore }) | ||
|
||
t.ok(await timeoutException(getPort(server.server))) | ||
|
||
await new Promise((resolve) => { | ||
server.listen({ port: 0 }, (err, address) => { | ||
resolve(address) | ||
}) | ||
}) | ||
|
||
const port = await getPort(server.server) | ||
|
||
t.is(typeof port, 'number', 'port is a number') | ||
t.teardown(async () => { | ||
await server.close() | ||
}) | ||
}) | ||
|
||
test('get url from blobId', async (t) => { | ||
const projectId = '1234' | ||
const type = 'image' | ||
const variant = 'original' | ||
const name = '1234' | ||
|
||
const blobStore = createBlobStore() | ||
const blobServer = await createBlobServer({ blobStore }) | ||
const blobApi = new BlobApi({ projectId: '1234', blobStore, blobServer }) | ||
|
||
await new Promise((resolve) => { | ||
blobServer.listen({ port: 0 }, (err, address) => { | ||
resolve(address) | ||
}) | ||
}) | ||
|
||
const url = await blobApi.getUrl({ type, variant, name }) | ||
|
||
t.is( | ||
url, | ||
`http://127.0.0.1:${ | ||
blobServer.server.address().port | ||
}/${projectId}/${blobStore.writerDriveId}/${type}/${variant}/${name}` | ||
) | ||
t.teardown(async () => { | ||
await blobServer.close() | ||
}) | ||
}) | ||
|
||
test('create blobs', async (t) => { | ||
const { blobStore } = createBlobStore() | ||
const blobServer = createBlobServer({ blobStore }) | ||
const blobApi = new BlobApi({ projectId: '1234', blobStore, blobServer }) | ||
|
||
await new Promise((resolve) => { | ||
blobServer.listen({ port: 0 }, (err, address) => { | ||
resolve(address) | ||
}) | ||
}) | ||
|
||
const directory = fileURLToPath( | ||
new URL('./fixtures/blob-api/', import.meta.url) | ||
) | ||
|
||
const attachment = await blobApi.create( | ||
{ | ||
original: join(directory, 'original.png'), | ||
preview: join(directory, 'preview.png'), | ||
thumbnail: join(directory, 'thumbnail.png'), | ||
}, | ||
{ | ||
mimeType: 'image/png', | ||
} | ||
) | ||
|
||
t.is(attachment.driveId, blobStore.writerDriveId) | ||
t.is(attachment.type, 'photo') | ||
|
||
t.teardown(async () => { | ||
await blobServer.close() | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters