Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: support range requests #62

Merged
merged 2 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/parts/CreateWebViewServerHandler/CreateWebViewServerHandler.ts
Original file line number Diff line number Diff line change
@@ -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 = (
Expand All @@ -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)
}

Expand Down
1 change: 1 addition & 0 deletions src/parts/EmptyResponse/EmptyResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const emptyResponse = new Response()
5 changes: 4 additions & 1 deletion src/parts/GetResponse/GetResponse.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -9,6 +10,8 @@ export const getResponse = async (
webViewRoot: string,
contentSecurityPolicy: string,
iframeContent: string,
range: any,
response: ServerResponse,
): Promise<Response> => {
const filePath = ResolveFilePath.resolveFilePath(pathName, webViewRoot)
const isHtml = filePath.endsWith('index.html')
Expand All @@ -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)
}
9 changes: 8 additions & 1 deletion src/parts/HandleOther/HandleOther.ts
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
47 changes: 47 additions & 0 deletions src/parts/HandleRangeRequest/HandleRangeRequest.ts
Original file line number Diff line number Diff line change
@@ -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}`)
}
}
7 changes: 5 additions & 2 deletions src/parts/HttpHeader/HttpHeader.ts
Original file line number Diff line number Diff line change
@@ -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'
1 change: 1 addition & 0 deletions src/parts/HttpStatusCode/HttpStatusCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export const NotFound = 404
export const NotModifed = 304
export const Ok = 200
export const ServerError = 500
export const OtherError = 416
9 changes: 7 additions & 2 deletions test/HandleOther.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { beforeEach, expect, jest, test } from '@jest/globals'
import { ServerResponse } from 'http'

beforeEach(() => {
jest.resetAllMocks()
Expand All @@ -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')
})