Skip to content

Commit cf6ff92

Browse files
authored
UBERF-8546 Refactor datalake routing (#7051)
Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
1 parent a2f9eb4 commit cf6ff92

File tree

6 files changed

+122
-101
lines changed

6 files changed

+122
-101
lines changed

workers/datalake/src/blob.ts

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import postgres from 'postgres'
1818
import * as db from './db'
1919
import { toUUID } from './encodings'
2020
import { selectStorage } from './storage'
21-
import { type UUID } from './types'
21+
import { type BlobRequest, type WorkspaceRequest, type UUID } from './types'
2222
import { copyVideo, deleteVideo } from './video'
2323

2424
const expires = 86400
@@ -39,13 +39,9 @@ export function getBlobURL (request: Request, workspace: string, name: string):
3939
return new URL(path, request.url).toString()
4040
}
4141

42-
export async function handleBlobGet (
43-
request: Request,
44-
env: Env,
45-
ctx: ExecutionContext,
46-
workspace: string,
47-
name: string
48-
): Promise<Response> {
42+
export async function handleBlobGet (request: BlobRequest, env: Env, ctx: ExecutionContext): Promise<Response> {
43+
const { workspace, name } = request
44+
4945
const sql = postgres(env.HYPERDRIVE.connectionString)
5046
const { bucket } = selectStorage(env, workspace)
5147

@@ -82,13 +78,9 @@ export async function handleBlobGet (
8278
return response
8379
}
8480

85-
export async function handleBlobHead (
86-
request: Request,
87-
env: Env,
88-
ctx: ExecutionContext,
89-
workspace: string,
90-
name: string
91-
): Promise<Response> {
81+
export async function handleBlobHead (request: BlobRequest, env: Env, ctx: ExecutionContext): Promise<Response> {
82+
const { workspace, name } = request
83+
9284
const sql = postgres(env.HYPERDRIVE.connectionString)
9385
const { bucket } = selectStorage(env, workspace)
9486

@@ -106,7 +98,9 @@ export async function handleBlobHead (
10698
return new Response(null, { headers, status: 200 })
10799
}
108100

109-
export async function deleteBlob (env: Env, workspace: string, name: string): Promise<Response> {
101+
export async function handleBlobDelete (request: BlobRequest, env: Env, ctx: ExecutionContext): Promise<Response> {
102+
const { workspace, name } = request
103+
110104
const sql = postgres(env.HYPERDRIVE.connectionString)
111105

112106
try {
@@ -120,13 +114,15 @@ export async function deleteBlob (env: Env, workspace: string, name: string): Pr
120114
}
121115
}
122116

123-
export async function postBlobFormData (request: Request, env: Env, workspace: string): Promise<Response> {
117+
export async function handleUploadFormData (request: WorkspaceRequest, env: Env): Promise<Response> {
124118
const contentType = request.headers.get('Content-Type')
125119
if (contentType === null || !contentType.includes('multipart/form-data')) {
126120
console.error({ error: 'expected multipart/form-data' })
127121
return error(400, 'expected multipart/form-data')
128122
}
129123

124+
const { workspace } = request
125+
130126
const sql = postgres(env.HYPERDRIVE.connectionString)
131127

132128
let formData: FormData

workers/datalake/src/image.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,17 @@
1414
//
1515

1616
import { getBlobURL } from './blob'
17+
import { type BlobRequest } from './types'
1718

1819
const prefferedImageFormats = ['webp', 'avif', 'jpeg', 'png']
1920

20-
export async function getImage (
21-
request: Request,
22-
workspace: string,
23-
name: string,
24-
transform: string
25-
): Promise<Response> {
21+
export async function handleImageGet (request: BlobRequest): Promise<Response> {
22+
const {
23+
workspace,
24+
name,
25+
params: { transform }
26+
} = request
27+
2628
const Accept = request.headers.get('Accept') ?? 'image/*'
2729
const image: Record<string, string> = {}
2830

workers/datalake/src/index.ts

Lines changed: 77 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -13,61 +13,90 @@
1313
// limitations under the License.
1414
//
1515

16-
import { type IRequest, Router, error, html } from 'itty-router'
17-
import {
18-
deleteBlob as handleBlobDelete,
19-
handleBlobGet,
20-
handleBlobHead,
21-
postBlobFormData as handleUploadFormData
22-
} from './blob'
16+
import { WorkerEntrypoint } from 'cloudflare:workers'
17+
import { type IRequestStrict, type RequestHandler, Router, error, html } from 'itty-router'
18+
19+
import { handleBlobDelete, handleBlobGet, handleBlobHead, handleUploadFormData } from './blob'
2320
import { cors } from './cors'
24-
import { getImage as handleImageGet } from './image'
25-
import { getVideoMeta as handleVideoMetaGet } from './video'
21+
import { handleImageGet } from './image'
22+
import { handleVideoMetaGet } from './video'
2623
import { handleSignAbort, handleSignComplete, handleSignCreate } from './sign'
24+
import { type BlobRequest, type WorkspaceRequest } from './types'
2725

2826
const { preflight, corsify } = cors({
2927
maxAge: 86400
3028
})
3129

32-
export default {
33-
async fetch (request, env, ctx): Promise<Response> {
34-
const router = Router<IRequest>({
35-
before: [preflight],
36-
finally: [corsify]
37-
})
30+
const router = Router<IRequestStrict, [Env, ExecutionContext], Response>({
31+
before: [preflight],
32+
finally: [corsify]
33+
})
34+
35+
const withWorkspace: RequestHandler<WorkspaceRequest> = (request: WorkspaceRequest) => {
36+
if (request.params.workspace === undefined || request.params.workspace === '') {
37+
return error(400, 'Missing workspace')
38+
}
39+
request.workspace = decodeURIComponent(request.params.workspace)
40+
}
41+
42+
const withBlob: RequestHandler<BlobRequest> = (request: BlobRequest) => {
43+
if (request.params.name === undefined || request.params.name === '') {
44+
return error(400, 'Missing blob name')
45+
}
46+
request.workspace = decodeURIComponent(request.params.name)
47+
}
48+
49+
router
50+
.get('/blob/:workspace/:name', withBlob, handleBlobGet)
51+
.head('/blob/:workspace/:name', withBlob, handleBlobHead)
52+
.delete('/blob/:workspace/:name', withBlob, handleBlobDelete)
53+
// Image
54+
.get('/image/:transform/:workspace/:name', withBlob, handleImageGet)
55+
// Video
56+
.get('/video/:workspace/:name/meta', withBlob, handleVideoMetaGet)
57+
// Form Data
58+
.post('/upload/form-data/:workspace', withWorkspace, handleUploadFormData)
59+
// Signed URL
60+
.post('/upload/signed-url/:workspace/:name', withBlob, handleSignCreate)
61+
.put('/upload/signed-url/:workspace/:name', withBlob, handleSignComplete)
62+
.delete('/upload/signed-url/:workspace/:name', withBlob, handleSignAbort)
63+
.all('/', () =>
64+
html(
65+
`Huly&reg; Datalake&trade; <a href="https://huly.io">https://huly.io</a>
66+
&copy; 2024 <a href="https://hulylabs.com">Huly Labs</a>`
67+
)
68+
)
69+
.all('*', () => error(404))
70+
71+
export default class DatalakeWorker extends WorkerEntrypoint<Env> {
72+
async fetch (request: Request): Promise<Response> {
73+
return await router.fetch(request, this.env, this.ctx).catch(error)
74+
}
75+
76+
async getBlob (workspace: string, name: string): Promise<ArrayBuffer> {
77+
const request = new Request(`https://datalake/blob/${workspace}/${name}`)
78+
const response = await router.fetch(request)
79+
80+
if (!response.ok) {
81+
console.error({ error: 'datalake error: ' + response.statusText, workspace, name })
82+
throw new Error(`Failed to fetch blob: ${response.statusText}`)
83+
}
84+
85+
return await response.arrayBuffer()
86+
}
87+
88+
async putBlob (workspace: string, name: string, data: ArrayBuffer | Blob | string, type: string): Promise<void> {
89+
const request = new Request(`https://datalake/upload/form-data/${workspace}`)
90+
91+
const body = new FormData()
92+
const blob = new Blob([data], { type })
93+
body.set('file', blob, name)
3894

39-
router
40-
.get('/blob/:workspace/:name', ({ params }) => handleBlobGet(request, env, ctx, params.workspace, params.name))
41-
.head('/blob/:workspace/:name', ({ params }) => handleBlobHead(request, env, ctx, params.workspace, params.name))
42-
.delete('/blob/:workspace/:name', ({ params }) => handleBlobDelete(env, params.workspace, params.name))
43-
// Image
44-
.get('/image/:transform/:workspace/:name', ({ params }) =>
45-
handleImageGet(request, params.workspace, params.name, params.transform)
46-
)
47-
// Video
48-
.get('/video/:workspace/:name/meta', ({ params }) =>
49-
handleVideoMetaGet(request, env, ctx, params.workspace, params.name)
50-
)
51-
// Form Data
52-
.post('/upload/form-data/:workspace', ({ params }) => handleUploadFormData(request, env, params.workspace))
53-
// Signed URL
54-
.post('/upload/signed-url/:workspace/:name', ({ params }) =>
55-
handleSignCreate(request, env, ctx, params.workspace, params.name)
56-
)
57-
.put('/upload/signed-url/:workspace/:name', ({ params }) =>
58-
handleSignComplete(request, env, ctx, params.workspace, params.name)
59-
)
60-
.delete('/upload/signed-url/:workspace/:name', ({ params }) =>
61-
handleSignAbort(request, env, ctx, params.workspace, params.name)
62-
)
63-
.all('/', () =>
64-
html(
65-
`Huly&reg; Datalake&trade; <a href="https://huly.io">https://huly.io</a>
66-
&copy; 2024 <a href="https://hulylabs.com">Huly Labs</a>`
67-
)
68-
)
69-
.all('*', () => error(404))
95+
const response = await router.fetch(request, { method: 'POST', body })
7096

71-
return await router.fetch(request).catch(error)
97+
if (!response.ok) {
98+
console.error({ error: 'datalake error: ' + response.statusText, workspace, name })
99+
throw new Error(`Failed to fetch blob: ${response.statusText}`)
100+
}
72101
}
73-
} satisfies ExportedHandler<Env>
102+
}

workers/datalake/src/sign.ts

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { AwsClient } from 'aws4fetch'
1717
import { error } from 'itty-router'
1818

1919
import { handleBlobUploaded } from './blob'
20-
import { type UUID } from './types'
20+
import { type BlobRequest, type UUID } from './types'
2121
import { selectStorage, type Storage } from './storage'
2222

2323
const S3_SIGNED_LINK_TTL = 3600
@@ -39,13 +39,8 @@ function getS3Client (storage: Storage): AwsClient {
3939
})
4040
}
4141

42-
export async function handleSignCreate (
43-
request: Request,
44-
env: Env,
45-
ctx: ExecutionContext,
46-
workspace: string,
47-
name: string
48-
): Promise<Response> {
42+
export async function handleSignCreate (request: BlobRequest, env: Env, ctx: ExecutionContext): Promise<Response> {
43+
const { workspace, name } = request
4944
const storage = selectStorage(env, workspace)
5045
const accountId = env.R2_ACCOUNT_ID
5146

@@ -78,13 +73,9 @@ export async function handleSignCreate (
7873
return new Response(signed.url, { status: 200, headers })
7974
}
8075

81-
export async function handleSignComplete (
82-
request: Request,
83-
env: Env,
84-
ctx: ExecutionContext,
85-
workspace: string,
86-
name: string
87-
): Promise<Response> {
76+
export async function handleSignComplete (request: BlobRequest, env: Env, ctx: ExecutionContext): Promise<Response> {
77+
const { workspace, name } = request
78+
8879
const { bucket } = selectStorage(env, workspace)
8980
const key = signBlobKey(workspace, name)
9081

@@ -117,13 +108,9 @@ export async function handleSignComplete (
117108
return new Response(null, { status: 201 })
118109
}
119110

120-
export async function handleSignAbort (
121-
request: Request,
122-
env: Env,
123-
ctx: ExecutionContext,
124-
workspace: string,
125-
name: string
126-
): Promise<Response> {
111+
export async function handleSignAbort (request: BlobRequest, env: Env, ctx: ExecutionContext): Promise<Response> {
112+
const { workspace, name } = request
113+
127114
const key = signBlobKey(workspace, name)
128115

129116
// Check if the blob has been uploaded

workers/datalake/src/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,21 @@
1313
// limitations under the License.
1414
//
1515

16+
import { type IRequestStrict } from 'itty-router'
17+
1618
export type Location = 'weur' | 'eeur' | 'wnam' | 'enam' | 'apac'
1719

1820
export type UUID = string & { __uuid: true }
1921

22+
export type WorkspaceRequest = {
23+
workspace: string
24+
} & IRequestStrict
25+
26+
export type BlobRequest = {
27+
workspace: string
28+
name: string
29+
} & IRequestStrict
30+
2031
export interface CloudflareResponse {
2132
success: boolean
2233
errors: any

workers/datalake/src/video.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
import { error, json } from 'itty-router'
1717

18-
import { type CloudflareResponse, type StreamUploadResponse } from './types'
18+
import { type BlobRequest, type CloudflareResponse, type StreamUploadResponse } from './types'
1919

2020
export type StreamUploadState = 'ready' | 'error' | 'inprogress' | 'queued' | 'downloading' | 'pendingupload'
2121

@@ -42,13 +42,9 @@ function streamBlobKey (workspace: string, name: string): string {
4242
return `v/${workspace}/${name}`
4343
}
4444

45-
export async function getVideoMeta (
46-
request: Request,
47-
env: Env,
48-
ctx: ExecutionContext,
49-
workspace: string,
50-
name: string
51-
): Promise<Response> {
45+
export async function handleVideoMetaGet (request: BlobRequest, env: Env, ctx: ExecutionContext): Promise<Response> {
46+
const { workspace, name } = request
47+
5248
const key = streamBlobKey(workspace, name)
5349

5450
const streamInfo = await env.datalake_blobs.get<StreamBlobInfo>(key, { type: 'json' })

0 commit comments

Comments
 (0)