|
@@ -8,6 +8,8 @@ import { LoggingService } from '../../logging'
|
|
|
import { ContentService, DEFAULT_CONTENT_TYPE } from '../../content/ContentService'
|
|
|
import proxy from 'express-http-proxy'
|
|
|
|
|
|
+const CACHE_MAX_AGE = 31536000
|
|
|
+
|
|
|
export class PublicApiController {
|
|
|
private logger: Logger
|
|
|
private networking: NetworkingService
|
|
@@ -26,20 +28,27 @@ export class PublicApiController {
|
|
|
this.content = content
|
|
|
}
|
|
|
|
|
|
- private serveAvailableAsset(
|
|
|
+ private serveAssetFromFilesystem(
|
|
|
req: express.Request,
|
|
|
res: express.Response,
|
|
|
next: express.NextFunction,
|
|
|
contentHash: string
|
|
|
): void {
|
|
|
|
|
|
+
|
|
|
+
|
|
|
this.stateCache.useContent(contentHash)
|
|
|
|
|
|
const path = this.content.path(contentHash)
|
|
|
- const stream = send(req, path)
|
|
|
+ const stream = send(req, path, {
|
|
|
+ maxAge: CACHE_MAX_AGE,
|
|
|
+ lastModified: false,
|
|
|
+ })
|
|
|
const mimeType = this.stateCache.getContentMimeType(contentHash)
|
|
|
|
|
|
stream.on('headers', (res) => {
|
|
|
+ res.setHeader('x-cache', 'hit')
|
|
|
+ res.setHeader('x-data-source', 'cache')
|
|
|
res.setHeader('content-disposition', 'inline')
|
|
|
res.setHeader('content-type', mimeType || DEFAULT_CONTENT_TYPE)
|
|
|
})
|
|
@@ -70,16 +79,59 @@ export class PublicApiController {
|
|
|
throw new Error('Trying to serve pending download asset that is not pending download!')
|
|
|
}
|
|
|
|
|
|
- const { promise } = pendingDownload
|
|
|
+ const { promise, objectSize } = pendingDownload
|
|
|
const response = await promise
|
|
|
const source = new URL(response.config.url!)
|
|
|
+
|
|
|
+ const contentType = response.headers['content-type'] || DEFAULT_CONTENT_TYPE
|
|
|
+ res.setHeader('content-type', contentType)
|
|
|
+
|
|
|
+
|
|
|
+ res.setHeader('cache-control', `max-age=180, must-revalidate`)
|
|
|
+
|
|
|
+
|
|
|
+ if (this.content.exists(contentHash)) {
|
|
|
+ const range = req.range(objectSize)
|
|
|
+ if (!range || range === -1 || range === -2 || range.length !== 1 || range.type !== 'bytes') {
|
|
|
+
|
|
|
+ return this.servePendingDownloadAssetFromFile(req, res, next, contentHash, objectSize)
|
|
|
+ } else if (range[0].start === 0) {
|
|
|
+
|
|
|
+ return this.servePendingDownloadAssetFromFile(req, res, next, contentHash, objectSize, range[0].end)
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- this.logger.info(`Proxying request to ${source.href}`, { source: source.href })
|
|
|
+
|
|
|
+ this.logger.info(`Forwarding request to ${source.href}`, { source: source.href })
|
|
|
+ res.setHeader('x-data-source', 'external')
|
|
|
+ return proxy(source.origin, { proxyReqPathResolver: () => source.pathname })(req, res, next)
|
|
|
+ }
|
|
|
|
|
|
-
|
|
|
- await proxy(source.origin, {
|
|
|
- proxyReqPathResolver: () => source.pathname,
|
|
|
- })(req, res, next)
|
|
|
+ private async servePendingDownloadAssetFromFile(
|
|
|
+ req: express.Request,
|
|
|
+ res: express.Response,
|
|
|
+ next: express.NextFunction,
|
|
|
+ contentHash: string,
|
|
|
+ objectSize: number,
|
|
|
+ rangeEnd?: number
|
|
|
+ ) {
|
|
|
+ const isRange = rangeEnd !== undefined
|
|
|
+ this.logger.info(`Serving pending download asset from file`, { contentHash, isRange, objectSize, rangeEnd })
|
|
|
+ const stream = this.content.createContinousReadStream(contentHash, {
|
|
|
+ end: isRange ? rangeEnd || 0 : objectSize - 1,
|
|
|
+ })
|
|
|
+ req.on('close', () => {
|
|
|
+ res.end()
|
|
|
+ stream.destroy()
|
|
|
+ })
|
|
|
+ res.status(isRange ? 206 : 200)
|
|
|
+ res.setHeader('accept-ranges', 'bytes')
|
|
|
+ res.setHeader('x-data-source', 'partial-cache')
|
|
|
+ res.setHeader('content-disposition', 'inline')
|
|
|
+ if (isRange) {
|
|
|
+ res.setHeader('content-range', `bytes 0-${rangeEnd}/${objectSize}`)
|
|
|
+ }
|
|
|
+ stream.pipe(res)
|
|
|
}
|
|
|
|
|
|
public async asset(
|
|
@@ -100,9 +152,10 @@ export class PublicApiController {
|
|
|
if (contentHash && !pendingDownload && this.content.exists(contentHash)) {
|
|
|
this.logger.info('Requested file found in filesystem', { path: this.content.path(contentHash) })
|
|
|
this.stateCache.useContent(contentHash)
|
|
|
- return this.serveAvailableAsset(req, res, next, contentHash)
|
|
|
+ return this.serveAssetFromFilesystem(req, res, next, contentHash)
|
|
|
} else if (contentHash && pendingDownload) {
|
|
|
this.logger.info('Requested file is in pending download state', { path: this.content.path(contentHash) })
|
|
|
+ res.setHeader('x-cache', 'pending')
|
|
|
return this.servePendingDownloadAsset(req, res, next, contentHash)
|
|
|
} else {
|
|
|
this.logger.info('Requested file not found in filesystem')
|
|
@@ -112,7 +165,7 @@ export class PublicApiController {
|
|
|
message: 'Data object does not exist',
|
|
|
}
|
|
|
res.status(404).json(errorRes)
|
|
|
-
|
|
|
+
|
|
|
|
|
|
|
|
|
|
|
@@ -124,11 +177,16 @@ export class PublicApiController {
|
|
|
if (!objectData) {
|
|
|
throw new Error('Missing data object data')
|
|
|
}
|
|
|
- const { contentHash } = objectData
|
|
|
+ const { contentHash, size } = objectData
|
|
|
+
|
|
|
const downloadResponse = await this.networking.downloadDataObject(objectData)
|
|
|
|
|
|
if (downloadResponse) {
|
|
|
- this.content.handleNewContent(contentHash, downloadResponse.data)
|
|
|
+
|
|
|
+ await this.content.handleNewContent(contentHash, size, downloadResponse.data)
|
|
|
+ res.setHeader('x-cache', 'fetch-triggered')
|
|
|
+ } else {
|
|
|
+ res.setHeader('x-cache', 'pending')
|
|
|
}
|
|
|
return this.servePendingDownloadAsset(req, res, next, contentHash)
|
|
|
}
|