platform/server/ws/src/blobs.ts
Andrey Sobolev cc7f962b90
Few fixes to uwebsocket and backup/restore (#5698)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
2024-05-30 16:19:23 +07:00

190 lines
5.1 KiB
TypeScript

import { Analytics } from '@hcengineering/analytics'
import type { MeasureContext, WorkspaceId } from '@hcengineering/core'
import type { StorageAdapter } from '@hcengineering/server-core'
import type { Readable } from 'stream'
const cacheControlNoCache = 'public, no-store, no-cache, must-revalidate, max-age=0'
export interface BlobResponse {
aborted: boolean
writeHead: (code: number, header: Record<string, string | number>) => void
status: (code: number) => void
end: () => void
pipeFrom: (readable: Readable, size: number) => void
cork: (cb: () => void) => void
}
export async function getFile (
ctx: MeasureContext,
client: StorageAdapter,
workspace: WorkspaceId,
file: string,
res: BlobResponse
): Promise<void> {
const stat = await ctx.with('stat', {}, async () => await client.stat(ctx, workspace, file))
if (stat === undefined) {
ctx.error('No such key', { file })
res.cork(() => {
res.status(404)
res.end()
})
return
}
await ctx.with(
'write',
{ contentType: stat.contentType },
async (ctx) => {
try {
const dataStream = await ctx.with('readable', {}, async () => await client.get(ctx, workspace, file))
await new Promise<void>((resolve, reject) => {
res.cork(() => {
res.writeHead(200, {
'Content-Type': stat.contentType,
Etag: stat.etag,
'Last-Modified': new Date(stat.modifiedOn).toISOString(),
'Cache-Control': cacheControlNoCache
})
res.pipeFrom(dataStream, stat.size)
dataStream.on('end', function () {
res.cork(() => {
res.end()
})
dataStream.destroy()
resolve()
})
dataStream.on('error', function (err) {
Analytics.handleError(err)
ctx.error('error', { err })
reject(err)
})
})
})
} catch (err: any) {
ctx.error('get-file-error', { workspace: workspace.name, err })
Analytics.handleError(err)
res.cork(() => {
res.status(500)
res.end()
})
}
},
{}
)
}
function getRange (range: string, size: number): [number, number] {
const [startStr, endStr] = range.replace(/bytes=/, '').split('-')
let start = parseInt(startStr, 10)
let end = endStr !== undefined ? parseInt(endStr, 10) : size - 1
if (!isNaN(start) && isNaN(end)) {
end = size - 1
}
if (isNaN(start) && !isNaN(end)) {
start = size - end
end = size - 1
}
return [start, end]
}
export async function getFileRange (
ctx: MeasureContext,
range: string,
client: StorageAdapter,
workspace: WorkspaceId,
uuid: string,
res: BlobResponse
): Promise<void> {
const stat = await ctx.with('stats', {}, async () => await client.stat(ctx, workspace, uuid))
if (stat === undefined) {
ctx.error('No such key', { file: uuid })
res.cork(() => {
res.status(404)
res.end()
})
return
}
const size: number = stat.size
let [start, end] = getRange(range, size)
if (end >= size) {
end = size // Allow to iterative return of entire document
}
if (start >= size) {
res.cork(() => {
res.writeHead(416, {
'Content-Range': `bytes */${size}`
})
res.end()
})
return
}
await ctx.with(
'write',
{ contentType: stat.contentType },
async (ctx) => {
try {
const dataStream = await ctx.with(
'partial',
{},
async () => await client.partial(ctx, workspace, uuid, start, end - start + 1),
{}
)
await new Promise<void>((resolve, reject) => {
res.cork(() => {
res.writeHead(206, {
Connection: 'keep-alive',
'Content-Range': `bytes ${start}-${end}/${size}`,
'Accept-Ranges': 'bytes',
// 'Content-Length': end - start + 1,
'Content-Type': stat.contentType,
Etag: stat.etag,
'Last-Modified': new Date(stat.modifiedOn).toISOString()
})
res.pipeFrom(dataStream, end - start)
dataStream.on('end', function () {
res.cork(() => {
res.end()
})
dataStream.destroy()
resolve()
})
dataStream.on('error', function (err) {
Analytics.handleError(err)
ctx.error('error', { err })
res.cork(() => {
res.end()
})
reject(err)
})
})
})
} catch (err: any) {
if (err?.code === 'NoSuchKey' || err?.code === 'NotFound') {
ctx.info('No such key', { workspace: workspace.name, uuid })
res.cork(() => {
res.status(404)
res.end()
})
return
} else {
Analytics.handleError(err)
ctx.error(err)
}
res.cork(() => {
res.status(500)
res.end()
})
}
},
{ uuid, start, end: end - start + 1 }
)
}