diff --git a/src/parts/CreateWebViewServerHandler/CreateWebViewServerHandler.ts b/src/parts/CreateWebViewServerHandler/CreateWebViewServerHandler.ts index 34fd9e4f..290796b8 100644 --- a/src/parts/CreateWebViewServerHandler/CreateWebViewServerHandler.ts +++ b/src/parts/CreateWebViewServerHandler/CreateWebViewServerHandler.ts @@ -1,8 +1,8 @@ import type { IncomingMessage, ServerResponse } from 'node:http' +import { emptyResponse } from '../EmptyResponse/EmptyResponse.ts' import * as GetPathName from '../GetPathName/GetPathName.ts' import * as GetResponse from '../GetResponse/GetResponse.ts' import * as SendResponse from '../SendResponse/SendResponse.ts' -import * as GetResponseInfo from '../GetResponseInfo/GetResponseInfo.ts' // TODO deprecated frame ancestors export const createHandler = ( @@ -16,7 +16,19 @@ export const createHandler = ( if (pathName === '/') { pathName += 'index.html' } - const result = await GetResponse.getResponse(pathName, frameAncestors, webViewRoot, contentSecurityPolicy, iframeContent) + const range = request.headers.range + const result = await GetResponse.getResponse( + pathName, + frameAncestors, + webViewRoot, + contentSecurityPolicy, + iframeContent, + range, + response, + ) + if (result === emptyResponse) { + return + } await SendResponse.sendResponse(response, result) } diff --git a/src/parts/EmptyResponse/EmptyResponse.ts b/src/parts/EmptyResponse/EmptyResponse.ts new file mode 100644 index 00000000..c9f816de --- /dev/null +++ b/src/parts/EmptyResponse/EmptyResponse.ts @@ -0,0 +1 @@ +export const emptyResponse = new Response() diff --git a/src/parts/GetResponse/GetResponse.ts b/src/parts/GetResponse/GetResponse.ts index 3086a9b5..b6e873ce 100644 --- a/src/parts/GetResponse/GetResponse.ts +++ b/src/parts/GetResponse/GetResponse.ts @@ -1,3 +1,4 @@ +import { ServerResponse } from 'node:http' import * as HandleIndexHtml from '../HandleIndexHtml/HandleIndexHtml.ts' import * as HandleOther from '../HandleOther/HandleOther.ts' import * as HandlePreviewInjected from '../HandlePreviewInjected/HandlePreviewInjected.ts' @@ -9,6 +10,8 @@ export const getResponse = async ( webViewRoot: string, contentSecurityPolicy: string, iframeContent: string, + range: any, + response: ServerResponse, ): Promise => { const filePath = ResolveFilePath.resolveFilePath(pathName, webViewRoot) const isHtml = filePath.endsWith('index.html') @@ -18,5 +21,5 @@ export const getResponse = async ( if (filePath.endsWith('preview-injected.js')) { return HandlePreviewInjected.handlePreviewInjected() } - return HandleOther.handleOther(filePath) + return HandleOther.handleOther(filePath, range, response) } diff --git a/src/parts/HandleOther/HandleOther.ts b/src/parts/HandleOther/HandleOther.ts index 86523658..7ff76390 100644 --- a/src/parts/HandleOther/HandleOther.ts +++ b/src/parts/HandleOther/HandleOther.ts @@ -1,11 +1,18 @@ +import { ServerResponse } from 'node:http' +import { emptyResponse } from '../EmptyResponse/EmptyResponse.ts' import * as FileSystem from '../FileSystem/FileSystem.ts' import * as GetContentType from '../GetContentType/GetContentType.ts' +import * as HandleRangeRequest from '../HandleRangeRequest/HandleRangeRequest.ts' import * as HttpHeader from '../HttpHeader/HttpHeader.ts' import * as HttpStatusCode from '../HttpStatusCode/HttpStatusCode.ts' import * as IsEnoentError from '../IsEnoentError/IsEnoentError.ts' -export const handleOther = async (filePath: string) => { +export const handleOther = async (filePath: string, range: any, res: ServerResponse) => { try { + if (range) { + await HandleRangeRequest.handleRangeRequest(filePath, range, res) + return emptyResponse + } const contentType = GetContentType.getContentType(filePath) // TODO figure out which of these headers are actually needed const content = await FileSystem.readFile(filePath) diff --git a/src/parts/HandleRangeRequest/HandleRangeRequest.ts b/src/parts/HandleRangeRequest/HandleRangeRequest.ts new file mode 100644 index 00000000..444c72b2 --- /dev/null +++ b/src/parts/HandleRangeRequest/HandleRangeRequest.ts @@ -0,0 +1,47 @@ +import { createReadStream } from 'node:fs' +import { stat } from 'node:fs/promises' +import { ServerResponse } from 'node:http' +import { pipeline } from 'node:stream/promises' +import * as HttpHeader from '../HttpHeader/HttpHeader.ts' +import * as HttpStatusCode from '../HttpStatusCode/HttpStatusCode.ts' +import * as IsStreamPrematureCloseError from '../IsStreamPrematureCloseError/IsStreamPrematureCloseError.ts' + +// TODO add lots of tests for this +export const handleRangeRequest = async (filePath: string, range: string, res: ServerResponse) => { + const stats = await stat(filePath) + let code = 206 + let [x, y] = range.replace('bytes=', '').split('-') + let end = parseInt(y, 10) || stats.size - 1 + let start = parseInt(x, 10) || 0 + + const options = { + start, + end, + } + + if (end >= stats.size) { + end = stats.size - 1 + } + + if (start >= stats.size) { + res.setHeader(HttpHeader.ContentRange, `bytes */${stats.size}`) + res.statusCode = HttpStatusCode.OtherError + return res.end() + } + const headers: any = {} + + headers[HttpHeader.ContentRange] = `bytes ${start}-${end}/${stats.size}` + headers[HttpHeader.ContentLength] = end - start + 1 + headers[HttpHeader.AcceptRanges] = 'bytes' + + res.writeHead(code, headers) + const readStream = createReadStream(filePath, options) + try { + await pipeline(readStream, res) + } catch (error) { + if (IsStreamPrematureCloseError.isStreamPrematureCloseError(error)) { + return + } + console.error(`[preview-process] ${error}`) + } +} diff --git a/src/parts/HttpHeader/HttpHeader.ts b/src/parts/HttpHeader/HttpHeader.ts index 37361715..6edf7f4f 100644 --- a/src/parts/HttpHeader/HttpHeader.ts +++ b/src/parts/HttpHeader/HttpHeader.ts @@ -1,9 +1,12 @@ +export const AcceptRanges = 'Accept-Ranges' +export const AccessControlAllowOrigin = 'Access-Control-Allow-Origin' export const CacheControl = 'Cache-Control' -export const ContentType = 'Content-Type' +export const ContentLength = 'Content-Length' +export const ContentRange = 'Content-Range' export const ContentSecurityPolicy = 'Content-Security-Policy' +export const ContentType = 'Content-Type' export const CrossOriginEmbedderPolicy = 'Cross-Origin-Embedder-Policy' export const CrossOriginOpenerPolicy = 'Cross-Origin-Opener-Policy' export const CrossOriginResourcePolicy = 'Cross-Origin-Resource-Policy' -export const AccessControlAllowOrigin = 'Access-Control-Allow-Origin' export const Etag = 'Etag' export const IfNotMatch = 'if-none-match' diff --git a/src/parts/HttpStatusCode/HttpStatusCode.ts b/src/parts/HttpStatusCode/HttpStatusCode.ts index 92ed1d03..9fc471b8 100644 --- a/src/parts/HttpStatusCode/HttpStatusCode.ts +++ b/src/parts/HttpStatusCode/HttpStatusCode.ts @@ -4,3 +4,4 @@ export const NotFound = 404 export const NotModifed = 304 export const Ok = 200 export const ServerError = 500 +export const OtherError = 416 diff --git a/test/HandleOther.test.ts b/test/HandleOther.test.ts index 6330c36e..330923a3 100644 --- a/test/HandleOther.test.ts +++ b/test/HandleOther.test.ts @@ -1,4 +1,5 @@ import { beforeEach, expect, jest, test } from '@jest/globals' +import { ServerResponse } from 'http' beforeEach(() => { jest.resetAllMocks() @@ -23,14 +24,18 @@ class FileNotFoundError extends Error { test('not found', async () => { jest.spyOn(FileSystem, 'readFile').mockRejectedValue(new FileNotFoundError()) - const response = await HandleOther.handleOther('/test/not-found.txt') + const range = '' + const res = new ServerResponse({} as any) + const response = await HandleOther.handleOther('/test/not-found.txt', range, res) expect(response.status).toBe(404) expect(await response.text()).toBe('not found') }) test('normal file', async () => { jest.spyOn(FileSystem, 'readFile').mockResolvedValue(Buffer.from('ok')) - const response = await HandleOther.handleOther('/test/not-found.txt') + const range = '' + const res = new ServerResponse({} as any) + const response = await HandleOther.handleOther('/test/not-found.txt', range, res) expect(response.status).toBe(200) expect(await response.text()).toBe('ok') })