Skip to content

Commit

Permalink
feat(serve-static): initial impl partialContentSupport
Browse files Browse the repository at this point in the history
  • Loading branch information
exoego committed Oct 1, 2024
1 parent 9c37ad2 commit 5802d0d
Show file tree
Hide file tree
Showing 8 changed files with 588 additions and 5 deletions.
61 changes: 61 additions & 0 deletions runtime-tests/bun/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,67 @@ describe('Serve Static Middleware', () => {
expect(await res.text()).toBe('Bun!')
expect(onNotFound).not.toHaveBeenCalled()
})

describe('Range Request', () => {
it('Should support a single range request', async () => {
const res = await app.request(
new Request('http://localhost/favicon.ico', { headers: { Range: 'bytes=0-4' } })
)
expect(await res.text()).toBe('\u0000\u0000\u0001\u0000\u0003')
expect(res.status).toBe(206)
expect(res.headers.get('Content-Type')).toBe('image/x-icon')
expect(res.headers.get('Content-Length')).toBe('5')
expect(res.headers.get('Content-Range')).toBe('bytes 0-4/15406')
})

it('Should support a single range where its end is larger than the actual size', async () => {
const res = await app.request(
new Request('http://localhost/favicon.ico', { headers: { Range: 'bytes=15400-20000' } })
)
expect(await res.text()).toBe('\u0000\u0000\u0000\u0000\u0000\u0000')
expect(res.status).toBe(206)
expect(res.headers.get('Content-Type')).toBe('image/x-icon')
expect(res.headers.get('Content-Length')).toBe('6')
expect(res.headers.get('Content-Range')).toBe('bytes 15400-15405/15406')
})

it('Should support omitted end', async () => {
const res = await app.request(
new Request('http://localhost/favicon.ico', { headers: { Range: 'bytes=15400-' } })
)
expect(await res.text()).toBe('\u0000\u0000\u0000\u0000\u0000\u0000')
expect(res.status).toBe(206)
expect(res.headers.get('Content-Type')).toBe('image/x-icon')
expect(res.headers.get('Content-Length')).toBe('6')
expect(res.headers.get('Content-Range')).toBe('bytes 15400-15405/15406')
})

it('Should support the last N bytes request', async () => {
const res = await app.request(
new Request('http://localhost/favicon.ico', { headers: { Range: 'bytes=-10' } })
)
expect(await res.text()).toBe('\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000')
expect(res.status).toBe(206)
expect(res.headers.get('Content-Type')).toBe('image/x-icon')
expect(res.headers.get('Content-Length')).toBe('10')
expect(res.headers.get('Content-Range')).toBe('bytes 15396-15405/15406')
})

it('Should support multiple ranges', async () => {
const res = await app.request(
new Request('http://localhost/favicon.ico', {
headers: { Range: 'bytes=151-200,351-400,15401-15500' },
})
)
await res.arrayBuffer()
expect(res.status).toBe(206)
expect(res.headers.get('Content-Type')).toBe(
'multipart/byteranges; boundary=PARTIAL_CONTENT_BOUNDARY'
)
expect(res.headers.get('Content-Length')).toBe('105')
expect(res.headers.get('Content-Range')).toBeNull()
})
})
})

// Bun support WebCrypto since v0.2.2
Expand Down
55 changes: 55 additions & 0 deletions runtime-tests/deno/middleware.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,61 @@ Deno.test('Serve Static middleware', async () => {
res = await app.request('http://localhost/static-absolute-root/plain.txt')
assertEquals(res.status, 200)
assertEquals(await res.text(), 'Deno!')

// Should support a single range request
res = await app.request(
new Request('http://localhost/favicon.ico', { headers: { Range: 'bytes=0-4' } })
)
assertEquals(await res.text(), '\u0000\u0000\u0001\u0000\u0003')
assertEquals(res.status, 206)
assertEquals(res.headers.get('Content-Type'), 'image/x-icon')
assertEquals(res.headers.get('Content-Length'), '5')
assertEquals(res.headers.get('Content-Range'), 'bytes 0-4/15406')

// Should support a single range where its end is larger than the actual size
res = await app.request(
new Request('http://localhost/favicon.ico', { headers: { Range: 'bytes=15400-20000' } })
)
assertEquals(await res.text(), '\u0000\u0000\u0000\u0000\u0000\u0000')
assertEquals(res.status, 206)
assertEquals(res.headers.get('Content-Type'), 'image/x-icon')
assertEquals(res.headers.get('Content-Length'), '6')
assertEquals(res.headers.get('Content-Range'), 'bytes 15400-15405/15406')

// Should support omitted end
res = await app.request(
new Request('http://localhost/favicon.ico', { headers: { Range: 'bytes=15400-' } })
)
assertEquals(await res.text(), '\u0000\u0000\u0000\u0000\u0000\u0000')
assertEquals(res.status, 206)
assertEquals(res.headers.get('Content-Type'), 'image/x-icon')
assertEquals(res.headers.get('Content-Length'), '6')
assertEquals(res.headers.get('Content-Range'), 'bytes 15400-15405/15406')

// Should support the last N bytes request
res = await app.request(
new Request('http://localhost/favicon.ico', { headers: { Range: 'bytes=-10' } })
)
assertEquals(await res.text(), '\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000')
assertEquals(res.status, 206)
assertEquals(res.headers.get('Content-Type'), 'image/x-icon')
assertEquals(res.headers.get('Content-Length'), '10')
assertEquals(res.headers.get('Content-Range'), 'bytes 15396-15405/15406')

// Should support multiple ranges
res = await app.request(
new Request('http://localhost/favicon.ico', {
headers: { Range: 'bytes=151-200,351-400,15401-15500' },
})
)
await res.arrayBuffer()
assertEquals(res.status, 206)
assertEquals(
res.headers.get('Content-Type'),
'multipart/byteranges; boundary=PARTIAL_CONTENT_BOUNDARY'
)
assertEquals(res.headers.get('Content-Length'), '105')
assertEquals(res.headers.get('Content-Range'), null)
})

Deno.test('JWT Authentication middleware', async () => {
Expand Down
34 changes: 33 additions & 1 deletion src/adapter/bun/serve-static.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { stat } from 'node:fs/promises'
import { open, stat } from 'node:fs/promises'
import { serveStatic as baseServeStatic } from '../../middleware/serve-static'
import type { ServeStaticOptions } from '../../middleware/serve-static'
import type { Env, MiddlewareHandler } from '../../types'
Expand Down Expand Up @@ -30,6 +30,38 @@ export const serveStatic = <E extends Env = Env>(
getContent,
pathResolve,
isDir,
partialContentSupport: async (path: string) => {
path = path.startsWith('/') ? path : `./${path}`
const handle = await open(path)
const size = (await handle.stat()).size
return {
size,
getPartialContent: function getPartialContent(start: number, end: number) {
const readStream = handle.createReadStream({ start, end })
const data = new ReadableStream({
start(controller) {
readStream.on('data', (chunk) => {
controller.enqueue(chunk)
})
readStream.on('end', () => {
controller.close()
})
readStream.on('error', (e) => {
controller.error(e)

Check warning on line 50 in src/adapter/bun/serve-static.ts

View check run for this annotation

Codecov / codecov/patch

src/adapter/bun/serve-static.ts#L49-L50

Added lines #L49 - L50 were not covered by tests
})
},

Check warning on line 52 in src/adapter/bun/serve-static.ts

View check run for this annotation

Codecov / codecov/patch

src/adapter/bun/serve-static.ts#L52

Added line #L52 was not covered by tests
})
return {
start,
end,
data,
}
},

Check warning on line 59 in src/adapter/bun/serve-static.ts

View check run for this annotation

Codecov / codecov/patch

src/adapter/bun/serve-static.ts#L59

Added line #L59 was not covered by tests
close: () => {
handle.close()
},

Check warning on line 62 in src/adapter/bun/serve-static.ts

View check run for this annotation

Codecov / codecov/patch

src/adapter/bun/serve-static.ts#L62

Added line #L62 was not covered by tests
}
},

Check warning on line 64 in src/adapter/bun/serve-static.ts

View check run for this annotation

Codecov / codecov/patch

src/adapter/bun/serve-static.ts#L64

Added line #L64 was not covered by tests
})(c, next)
}
}
1 change: 1 addition & 0 deletions src/adapter/cloudflare-workers/serve-static.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const serveStatic = <E extends Env = Env>(
: undefined,
})
}
// partialContentSupport is not implemented since this middleware is deprecated
return baseServeStatic({
...options,
getContent,
Expand Down
20 changes: 20 additions & 0 deletions src/adapter/deno/deno.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ declare namespace Deno {
*/
export function mkdir(path: string, options?: { recursive?: boolean }): Promise<void>

export function lstatSync(path: string): {
isDirectory: boolean
size: number
}

/**
* Write a new file, with the specified path and data.
*
Expand All @@ -25,4 +30,19 @@ declare namespace Deno {
response: Response
socket: WebSocket
}

export function open(path: string): Promise<FsFile>

export enum SeekMode {
Start = 0,
}

export function seekSync(rid: number, offset: number, whence: SeekMode): number
export function readSync(rid: number, buffer: Uint8Array): number

export type FsFile = {
rid: number
readable: ReadableStream<Uint8Array>
close(): void
}
}
27 changes: 24 additions & 3 deletions src/adapter/deno/serve-static.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Env, MiddlewareHandler } from '../../types'

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const { open, lstatSync } = Deno
const { open, lstatSync, seekSync, readSync, SeekMode } = Deno

Check warning on line 7 in src/adapter/deno/serve-static.ts

View check run for this annotation

Codecov / codecov/patch

src/adapter/deno/serve-static.ts#L7

Added line #L7 was not covered by tests

export const serveStatic = <E extends Env = Env>(
options: ServeStaticOptions<E>
Expand All @@ -13,10 +13,10 @@ export const serveStatic = <E extends Env = Env>(
const getContent = async (path: string) => {
try {
const file = await open(path)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return file ? (file.readable as any) : null
return file?.readable ?? null

Check warning on line 16 in src/adapter/deno/serve-static.ts

View check run for this annotation

Codecov / codecov/patch

src/adapter/deno/serve-static.ts#L16

Added line #L16 was not covered by tests
} catch (e) {
console.warn(`${e}`)
return null

Check warning on line 19 in src/adapter/deno/serve-static.ts

View check run for this annotation

Codecov / codecov/patch

src/adapter/deno/serve-static.ts#L19

Added line #L19 was not covered by tests
}
}
const pathResolve = (path: string) => {
Expand All @@ -35,6 +35,27 @@ export const serveStatic = <E extends Env = Env>(
getContent,
pathResolve,
isDir,
partialContentSupport: async (path: string) => {
path = path.startsWith('/') ? path : `./${path}`
const handle = await open(path)
const size = lstatSync(path).size
return {
size,
getPartialContent: function getPartialContent(start: number, end: number) {
seekSync(handle.rid, start, SeekMode.Start)
const data = new Uint8Array(end - start + 1)
readSync(handle.rid, data)
return {
start,
end,
data,
}
},
close: () => {
handle.close()
},
}
},

Check warning on line 58 in src/adapter/deno/serve-static.ts

View check run for this annotation

Codecov / codecov/patch

src/adapter/deno/serve-static.ts#L38-L58

Added lines #L38 - L58 were not covered by tests
})(c, next)
}
}
Loading

0 comments on commit 5802d0d

Please sign in to comment.