83 lines
2.3 KiB
TypeScript
83 lines
2.3 KiB
TypeScript
import type { Readable } from 'node:stream'
|
|
import { Router } from 'express'
|
|
|
|
import {
|
|
getStoredFileObject,
|
|
isPublicObjectKey,
|
|
} from '../services/file-storage/file-storage-service.js'
|
|
import { buildNotFoundPayload, sendRouteError } from '../utils/http.js'
|
|
|
|
const router = Router()
|
|
|
|
router.get('/object', async (req, res) => {
|
|
let readerToDestroy: Readable | null = null
|
|
try {
|
|
const rawKey = String(req.query.key || '').trim()
|
|
const isPublic = isPublicObjectKey(rawKey)
|
|
const object = await getStoredFileObject(req.query.key, req.query.variant)
|
|
readerToDestroy = object.reader
|
|
|
|
if (object.etag) {
|
|
res.setHeader('ETag', object.etag)
|
|
if (req.headers['if-none-match'] === object.etag) {
|
|
object.reader.destroy()
|
|
readerToDestroy = null
|
|
res.status(304).end()
|
|
return
|
|
}
|
|
}
|
|
if (object.lastModified) {
|
|
res.setHeader('Last-Modified', object.lastModified.toUTCString())
|
|
if (req.headers['if-modified-since']) {
|
|
const ifModifiedSince = new Date(req.headers['if-modified-since'])
|
|
if (!isNaN(ifModifiedSince.getTime()) && object.lastModified <= ifModifiedSince) {
|
|
object.reader.destroy()
|
|
readerToDestroy = null
|
|
res.status(304).end()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
if (object.size > 0) {
|
|
res.setHeader('Content-Length', object.size)
|
|
}
|
|
res.setHeader('Content-Type', object.contentType || 'application/octet-stream')
|
|
res.setHeader('X-Content-Type-Options', 'nosniff')
|
|
|
|
if (isPublic) {
|
|
res.setHeader('Cache-Control', 'public, max-age=2592000, stale-while-revalidate=604800')
|
|
} else {
|
|
res.setHeader('Cache-Control', 'private, max-age=300')
|
|
}
|
|
|
|
res.on('close', () => {
|
|
if (!res.writableEnded) {
|
|
object.reader.destroy()
|
|
}
|
|
})
|
|
|
|
object.reader.on('error', (error) => {
|
|
object.reader.destroy()
|
|
if (!res.headersSent) {
|
|
sendRouteError(res, error, '读取文件失败', '[files/object]')
|
|
} else {
|
|
res.destroy(error)
|
|
}
|
|
})
|
|
object.reader.pipe(res)
|
|
readerToDestroy = null
|
|
} catch (error) {
|
|
if (readerToDestroy) {
|
|
readerToDestroy.destroy()
|
|
}
|
|
sendRouteError(res, error, '读取文件失败', '[files/object]')
|
|
}
|
|
})
|
|
|
|
router.use((req, res) => {
|
|
res.status(404).json(buildNotFoundPayload(req))
|
|
})
|
|
|
|
export default router
|