@milaboratories/pframes-rs-serv 1.0.64 → 1.0.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -59
- package/dist/handler.cjs +2 -2
- package/dist/handler.cjs.map +1 -1
- package/dist/handler.d.ts +1 -1
- package/dist/handler.js +2 -2
- package/dist/handler.js.map +1 -1
- package/dist/index.cjs +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/parquet-server.cjs +57 -4
- package/dist/parquet-server.cjs.map +1 -1
- package/dist/parquet-server.d.ts +17 -0
- package/dist/parquet-server.d.ts.map +1 -1
- package/dist/parquet-server.js +56 -5
- package/dist/parquet-server.js.map +1 -1
- package/dist/utils/headers.cjs +1 -0
- package/dist/utils/headers.cjs.map +1 -1
- package/dist/utils/headers.js +1 -0
- package/dist/utils/headers.js.map +1 -1
- package/package.json +10 -2
- package/src/handler.ts +4 -4
- package/src/parquet-server.ts +90 -9
package/README.md
CHANGED
|
@@ -4,15 +4,15 @@ A high-performance HTTP server for serving Parquet files with full RFC 9110 comp
|
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
-
This package provides HTTP server functionality for the PFrames ecosystem, specifically designed for serving Parquet files with proper HTTP semantics. It implements a complete HTTP/1.1 server that handles range requests, conditional requests, authentication, and
|
|
7
|
+
This package provides HTTP(S) server functionality for the PFrames ecosystem, specifically designed for serving Parquet files with proper HTTP semantics. It implements a complete HTTP/1.1 server that handles range requests, conditional requests, Bearer token authentication, and range requests caching.
|
|
8
8
|
|
|
9
9
|
## Architecture
|
|
10
10
|
|
|
11
11
|
### Core Components
|
|
12
12
|
|
|
13
|
-
- **Server** (`src/serve.ts`) - Generic HTTP server with lifecycle management
|
|
14
|
-
- **HTTP Handler** (`src/handler.ts`) - RFC 9110 compliant HTTP request handler
|
|
15
|
-
- **File System Store** (`src/fs-store.ts`) -
|
|
13
|
+
- **Server** (`src/serve.ts`) - Generic HTTP server with lifecycle management and optional authentication
|
|
14
|
+
- **HTTP Handler** (`src/handler.ts`) - RFC 9110 compliant HTTP request handler with object store integration
|
|
15
|
+
- **File System Store** (`src/fs-store.ts`) - Object store implementation for local filesystem
|
|
16
16
|
|
|
17
17
|
### Exports
|
|
18
18
|
|
|
@@ -22,17 +22,18 @@ This package provides HTTP server functionality for the PFrames ecosystem, speci
|
|
|
22
22
|
- `createRequestHandler()` - HTTP request handler factory
|
|
23
23
|
- `FileSystemStore` - Local file system object store implementation
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
The `HttpHelpers` export contains utility functions intended for re-export through the `@milaboratories/pframes-node` package and consumption by PFrameDriver in public monorepo:
|
|
25
|
+
The `HttpHelpers` export contains functions intended for re-export through the `@milaboratories/pframes-rs-node` package.
|
|
28
26
|
|
|
29
27
|
### Binary Script
|
|
30
28
|
|
|
31
|
-
The `bin/parquet-server.mjs` script
|
|
29
|
+
The `bin/parquet-server.mjs` script can be used for integration testing. It provides a standalone server that can be spawned programmatically and serve Parquet files over HTTP(S) from a local directory.
|
|
32
30
|
|
|
33
31
|
## HTTP Handler Flow
|
|
34
32
|
|
|
35
|
-
The request handler implements a complete HTTP/1.1 server following [RFC 9110 (HTTP Semantics)](https://datatracker.ietf.org/doc/html/rfc9110) and [RFC 9111 (HTTP Caching)](https://datatracker.ietf.org/doc/html/rfc9111).
|
|
33
|
+
The request handler implements a complete HTTP/1.1 server following [RFC 9110 (HTTP Semantics)](https://datatracker.ietf.org/doc/html/rfc9110) and [RFC 9111 (HTTP Caching)](https://datatracker.ietf.org/doc/html/rfc9111). The handler consists of two layers:
|
|
34
|
+
|
|
35
|
+
1. **Authorization Layer** (`authorizeRequestHandler`) - Optional Bearer token authentication
|
|
36
|
+
2. **Core Handler** (`handleRequest`) - Main HTTP processing logic
|
|
36
37
|
|
|
37
38
|
## Request Processing Flow
|
|
38
39
|
|
|
@@ -48,25 +49,31 @@ The request handler implements a complete HTTP/1.1 server following [RFC 9110 (H
|
|
|
48
49
|
└─────────┬───────────┘
|
|
49
50
|
│
|
|
50
51
|
▼
|
|
51
|
-
┌─────────────────────┐ [FAIL]
|
|
52
|
-
│ Check
|
|
52
|
+
┌─────────────────────┐ [FAIL] Invalid/Missing Token
|
|
53
|
+
│ Check Bearer Token │────────────┐
|
|
54
|
+
│ (Timing-Safe) │ │
|
|
53
55
|
└─────────┬───────────┘ │
|
|
54
|
-
│ [PASS]
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
│
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
│ [PASS] Valid Token ▼
|
|
57
|
+
│ ┌──────────────┐
|
|
58
|
+
│ │ Return 401 │ ← WWW-Authenticate: Bearer
|
|
59
|
+
│ │ Unauthorized │
|
|
60
|
+
▼ └──────────────┘
|
|
61
|
+
┌─────────────────────┐
|
|
62
|
+
│ Set Cache Control │ ← Cache-Control: public, max-age=31536000
|
|
63
|
+
│ Headers │
|
|
64
|
+
└─────────┬───────────┘
|
|
65
|
+
│
|
|
60
66
|
▼
|
|
61
67
|
┌─────────────────────┐ [FAIL] Not GET/HEAD
|
|
62
68
|
│ Check HTTP Method │────────────┐
|
|
63
69
|
└─────────┬───────────┘ │
|
|
64
|
-
│ [PASS]
|
|
70
|
+
│ [PASS] GET or HEAD │
|
|
65
71
|
▼ ▼
|
|
66
72
|
┌─────────────────────┐ ┌──────────────┐
|
|
67
|
-
│ Parse URL
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
│ Parse URL & │ │ Return 405 │ ← Allow: GET, HEAD
|
|
74
|
+
│ Extract Filename │ │ Not Allowed │
|
|
75
|
+
└─────────┬───────────┘ └──────────────┘
|
|
76
|
+
│
|
|
70
77
|
▼
|
|
71
78
|
┌─────────────────────┐ [FAIL] Invalid Pattern
|
|
72
79
|
│ Validate .parquet │────────────┐
|
|
@@ -75,68 +82,69 @@ The request handler implements a complete HTTP/1.1 server following [RFC 9110 (H
|
|
|
75
82
|
│ [PASS] Valid .parquet │
|
|
76
83
|
▼ ▼
|
|
77
84
|
┌─────────────────────┐ ┌──────────────┐
|
|
78
|
-
│ Set Content Headers │ │ Return 410 │
|
|
85
|
+
│ Set Content Headers │ │ Return 410 │
|
|
79
86
|
│ (Accept-Ranges, │ │ Gone │
|
|
80
|
-
│ Content-Type)
|
|
87
|
+
│ Content-Type), │ └──────────────┘
|
|
88
|
+
│ Generate ETag & │ ← ETag from filename
|
|
89
|
+
│ Set Cache Headers │ Last-Modified: UTC(0)
|
|
81
90
|
└─────────┬───────────┘
|
|
82
91
|
│
|
|
83
92
|
▼
|
|
84
|
-
┌─────────────────────┐ [FAIL] File Missing
|
|
85
|
-
│ Check File Exists │────────────┐
|
|
86
|
-
└─────────┬───────────┘ │
|
|
87
|
-
│ [PASS] File Found │
|
|
88
|
-
▼ ▼
|
|
89
|
-
┌─────────────────────┐ ┌──────────────┐
|
|
90
|
-
│ Generate ETag │ │ Return 404 │ ← File Not Found
|
|
91
|
-
│ Set ETag Header │ │ Not Found │
|
|
92
|
-
└─────────┬───────────┘ └──────────────┘
|
|
93
|
-
│
|
|
94
|
-
▼
|
|
95
93
|
┌─────────────────────┐ [FAIL] Precondition Failed
|
|
96
|
-
│ Check If-Match
|
|
97
|
-
│
|
|
94
|
+
│ Check If-Match & │────────────┐
|
|
95
|
+
│ If-Unmodified-Since │ │
|
|
98
96
|
└─────────┬───────────┘ │
|
|
99
|
-
│
|
|
97
|
+
│ │
|
|
100
98
|
▼ ▼
|
|
101
|
-
┌─────────────────────┐
|
|
102
|
-
│ Check If-None-Match │ │ Return 412
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
▼ └──────────────┘
|
|
99
|
+
┌─────────────────────┐ ┌─────────────────────┐
|
|
100
|
+
│ Check If-None-Match │ │ Return 412 │
|
|
101
|
+
│ & If-Modified-Since │ │ Precondition Failed │
|
|
102
|
+
└─────────┬───────────┘ └─────────────────────┘
|
|
106
103
|
│ [FAIL] Not Modified
|
|
107
104
|
├────────────────────────┐
|
|
108
105
|
│ ▼
|
|
109
106
|
│ ┌──────────────┐
|
|
110
|
-
│ │ Return 304 │
|
|
107
|
+
│ │ Return 304 │
|
|
111
108
|
│ │ Not Modified │
|
|
112
109
|
│ └──────────────┘
|
|
113
|
-
│ [PASS] Modified
|
|
114
|
-
▼
|
|
115
|
-
┌─────────────────────┐
|
|
116
|
-
│ Parse Range Header │
|
|
117
|
-
└─────────┬───────────┘
|
|
118
|
-
│
|
|
119
110
|
▼
|
|
120
111
|
┌─────────────────────┐ [FAIL] Invalid Range
|
|
121
|
-
│
|
|
122
|
-
│ Against File Size │ │
|
|
112
|
+
│ Parse Range Header │────────────┐
|
|
123
113
|
└─────────┬───────────┘ │
|
|
124
114
|
│ [PASS] Valid Range │
|
|
125
115
|
▼ ▼
|
|
126
116
|
┌─────────────────────┐ ┌──────────────┐
|
|
127
|
-
│
|
|
128
|
-
│
|
|
129
|
-
│ 206 (partial) │ │ Satisfiable │
|
|
117
|
+
│ Proxy Request to │ │ Return 400 │
|
|
118
|
+
│ Object Store │ │ Bad Request │
|
|
130
119
|
└─────────┬───────────┘ └──────────────┘
|
|
120
|
+
│
|
|
121
|
+
├─────────┬─────────┬─────────┐
|
|
122
|
+
│ │ │ │
|
|
123
|
+
▼ ▼ ▼ ▼
|
|
124
|
+
┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐
|
|
125
|
+
│ Ok │ │ Not │ │Invalid│ │ Server│
|
|
126
|
+
│Success│ │ Found │ │ Range │ │ Error │
|
|
127
|
+
└───┬───┘ └───┬───┘ └───┬───┘ └───┬───┘
|
|
128
|
+
│ │ │ │
|
|
129
|
+
▼ ▼ ▼ ▼
|
|
130
|
+
┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐
|
|
131
|
+
│200/206│ │ 404 │ │ 416 │ │ 500 │
|
|
132
|
+
└───┬───┘ └───────┘ └───────┘ └───────┘
|
|
133
|
+
│
|
|
134
|
+
▼
|
|
135
|
+
┌─────────────────────┐
|
|
136
|
+
│ Set Content-Length │ ← Full size or range size
|
|
137
|
+
│ & Content-Range │ Content-Range for 206 responses
|
|
138
|
+
└─────────┬───────────┘
|
|
131
139
|
│
|
|
132
140
|
▼
|
|
133
|
-
┌─────────────────────┐ [HEAD]
|
|
134
|
-
│ Check Request
|
|
141
|
+
┌─────────────────────┐ [HEAD] Headers Only
|
|
142
|
+
│ Check Request │────────────┐
|
|
143
|
+
│ Method │ │
|
|
135
144
|
└─────────┬───────────┘ │
|
|
136
|
-
│ [GET]
|
|
145
|
+
│ [GET] Send Body │
|
|
137
146
|
▼ ▼
|
|
138
147
|
┌─────────────────────┐ ┌──────────────┐
|
|
139
|
-
│ Stream File Content │ │ Send Headers │
|
|
140
|
-
│ (with range support)│ │ Only (HEAD) │
|
|
148
|
+
│ Stream File Content │ │ Send Headers │
|
|
141
149
|
└─────────────────────┘ └──────────────┘
|
|
142
150
|
```
|
package/dist/handler.cjs
CHANGED
|
@@ -112,7 +112,7 @@ async function handleRequest(request, response, store) {
|
|
|
112
112
|
* - <https://datatracker.ietf.org/doc/html/rfc9110>
|
|
113
113
|
* - <https://datatracker.ietf.org/doc/html/rfc9111>
|
|
114
114
|
*
|
|
115
|
-
* Accepts only paths of the form `/<filename>.parquet`, returns
|
|
115
|
+
* Accepts only paths of the form `/<filename>.parquet`, returns 410 Gone otherwise
|
|
116
116
|
* Assumes that files are immutable (and sets cache headers accordingly)
|
|
117
117
|
*/
|
|
118
118
|
function createRequestHandler(options) {
|
|
@@ -127,7 +127,7 @@ function authorizeRequest(request, response, handler, authHeader) {
|
|
|
127
127
|
response.strictContentLength = true;
|
|
128
128
|
response.setHeader(headers.HeaderName.ContentLength, 0);
|
|
129
129
|
// Note: setting Content-Length disables Node.js default Transfer-Encoding: chunked
|
|
130
|
-
const actualHeader = request.headers[
|
|
130
|
+
const actualHeader = request.headers[headers.HeaderName.Authorization];
|
|
131
131
|
// Early length check to avoid unnecessary processing
|
|
132
132
|
if (!actualHeader || actualHeader.length !== authHeader.length) {
|
|
133
133
|
// RFC 9110 section 11.6.1: WWW-Authenticate header field
|
package/dist/handler.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handler.cjs","sources":["../src/handler.ts"],"sourcesContent":["import type {\n IncomingMessage,\n RequestListener,\n ServerResponse\n} from 'node:http';\nimport { pipeline } from 'node:stream/promises';\nimport { timingSafeEqual } from 'node:crypto';\nimport type { PFrameInternal } from '@milaboratories/pl-model-middle-layer';\nimport {\n createETag,\n getFilenameFromUrl,\n parseRange,\n isGetOrHead,\n isHead,\n Options,\n StatusCode,\n HeaderName,\n HeaderValue,\n isGet\n} from './utils';\n\n/** Main request handler for parquet files */\nasync function handleRequest(\n request: IncomingMessage,\n response: ServerResponse,\n store: PFrameInternal.ObjectStore\n): Promise<void> {\n // RFC 9110 section 6.6.1: Date header should be present in all responses\n response.sendDate = true;\n // RFC 9110 section 8.6: Content-Length 0 as default for error responses\n response.strictContentLength = true;\n response.setHeader(HeaderName.ContentLength, 0);\n // Note: setting Content-Length disables Node.js default Transfer-Encoding: chunked\n\n // RFC 9111 section 5.2: Cache-Control header with public allows to cache authenticated responses\n response.setHeader(HeaderName.CacheControl, HeaderValue.CacheControl);\n\n // RFC 9110 section 15.5.6: Method not allowed\n const method = request.method;\n if (!isGetOrHead(method)) {\n response.setHeader(HeaderName.Allow, HeaderValue.Allow);\n return void response.writeHead(StatusCode.MethodNotAllowed).end();\n }\n\n const filename = getFilenameFromUrl(request);\n if (filename === null) {\n return void response.writeHead(StatusCode.Gone).end();\n }\n\n // From now on we are sure that the response would be a Parquet file\n response.setHeader(HeaderName.AcceptRanges, HeaderValue.AcceptRanges);\n response.setHeader(HeaderName.ContentType, HeaderValue.ContentType);\n\n // RFC 9110 section 8.8.3: ETag header is used for cache versioning\n const etag = createETag(filename);\n // RFC 9110 section 8.8.2: Last-Modified header field for cache validation\n const mtime = new Date(0); // Using fake fixed date since files are immutable\n // RFC 9111 section 5.2: Cache-Control header with public allows to cache authenticated responses\n response.setHeader(HeaderName.CacheControl, HeaderValue.CacheControl);\n response.setHeader(HeaderName.ETag, etag);\n response.setHeader(HeaderName.LastModified, mtime.toUTCString());\n\n const options = new Options(request);\n // RFC 9110 section 13.1.1: If-Match precondition evaluation\n // RFC 9110 section 13.1.4: If-Unmodified-Since precondition evaluation\n if (options.preconditionFailed(etag, mtime)) {\n return void response.writeHead(StatusCode.PreconditionFailed).end();\n }\n // RFC 9110 section 13.1.2: If-None-Match precondition evaluation\n // RFC 9110 section 13.1.3: If-Modified-Since precondition evaluation\n else if (options.notModified(etag, mtime)) {\n return void response.writeHead(StatusCode.NotModified).end();\n }\n\n const range = parseRange(request);\n if (range === null) {\n return void response.writeHead(StatusCode.BadRequest).end();\n }\n\n const abortController = new AbortController();\n request.on('close', () => abortController.abort());\n const signal = abortController.signal;\n\n store.request(filename, {\n method: 'GET',\n range,\n signal,\n // pipeline automatically destroys the streams if they were not gracefully closed\n callback: async (result) => {\n if (response.destroyed) return void response.destroy();\n\n switch (result.type) {\n case 'InternalError':\n // object store encountered network error, retry by client can help\n return void response.writeHead(StatusCode.InternalServerError).end();\n case 'NotFound':\n // RFC 9110 section 15.4.5: Not found\n return void response.writeHead(StatusCode.NotFound).end();\n case 'RangeNotSatisfiable':\n // RFC 9110 section 15.5.17: Range not satisfiable\n response.setHeader(HeaderName.ContentRange, `bytes */${result.size}`);\n return void response.writeHead(StatusCode.RangeNotSatisfiable).end();\n case 'Ok':\n break;\n }\n\n if (isGet(method) && !result.data) {\n // object store implementation is incorrect, retry by client cannot help\n return void response.writeHead(StatusCode.GatewayTimeout).end();\n }\n\n if (range) {\n // RFC 9110 section 14.4: Partial content response\n response.setHeader(\n HeaderName.ContentLength,\n result.range.end - result.range.start + 1\n );\n response.setHeader(\n HeaderName.ContentRange,\n `bytes ${result.range.start}-${result.range.end}/${result.size}`\n );\n response.writeHead(StatusCode.PartialContent);\n } else {\n // RFC 9110 section 15.3.1: OK response\n response.setHeader(HeaderName.ContentLength, result.size);\n response.writeHead(StatusCode.Ok);\n }\n\n // RFC 9110 section 9.3.2: HEAD method must not return message body\n if (isHead(method)) {\n return void response.end();\n }\n\n return await pipeline(result.data!, response, { signal }).catch(() => {\n // Pipeline errors are expected when request is aborted or connection is lost\n // Response head was already written, so we can't change status code\n // Just mute the error - pipeline destroys the response stream\n });\n }\n });\n}\n\n/**\n * Create a request handler for serving files from an object store\n * compatible with HTTP/1.1 as defined in RFC 9110 and RFC 9111:\n * - <https://datatracker.ietf.org/doc/html/rfc9110>\n * - <https://datatracker.ietf.org/doc/html/rfc9111>\n *\n * Accepts only paths of the form `/<filename>.parquet`, returns 404 otherwise\n * Assumes that files are immutable (and sets cache headers accordingly)\n */\nexport function createRequestHandler(\n options: PFrameInternal.RequestHandlerOptions\n): RequestListener {\n const { store } = options;\n return (request, response) => void handleRequest(request, response, store);\n}\n\n/** Request authorization middleware */\nfunction authorizeRequest(\n request: IncomingMessage,\n response: ServerResponse,\n handler: RequestListener,\n authHeader: string\n): void {\n // RFC 9110 section 6.6.1: Date header should be present in all responses\n response.sendDate = true;\n // RFC 9110 section 8.6: Content-Length 0 as default for error responses\n response.strictContentLength = true;\n response.setHeader(HeaderName.ContentLength, 0);\n // Note: setting Content-Length disables Node.js default Transfer-Encoding: chunked\n\n const actualHeader = request.headers['authorization'];\n\n // Early length check to avoid unnecessary processing\n if (!actualHeader || actualHeader.length !== authHeader.length) {\n // RFC 9110 section 11.6.1: WWW-Authenticate header field\n response.setHeader(HeaderName.WWWAuthenticate, HeaderValue.WWWAuthenticate);\n return void response.writeHead(StatusCode.Unauthorized).end();\n }\n\n // Use timing-safe comparison to prevent timing attacks\n // <https://developers.cloudflare.com/workers/examples/protect-against-timing-attacks/>\n const encoder = new TextEncoder();\n const receivedBuffer = encoder.encode(actualHeader);\n const expectedBuffer = encoder.encode(authHeader);\n\n if (\n receivedBuffer.byteLength !== expectedBuffer.byteLength ||\n !timingSafeEqual(receivedBuffer, expectedBuffer)\n ) {\n response.setHeader(HeaderName.WWWAuthenticate, HeaderValue.WWWAuthenticate);\n return void response.writeHead(StatusCode.Unauthorized).end();\n }\n\n return handler(request, response);\n}\n\n/** Apply Bearer token authorization to @param handler */\nexport function authorizeRequestHandler(\n handler: RequestListener,\n authToken: PFrameInternal.HttpAuthorizationToken\n): RequestListener {\n const authHeader = `Bearer ${authToken}`;\n return (request, response) =>\n authorizeRequest(request, response, handler, authHeader);\n}\n"],"names":["HeaderName","HeaderValue","method","isGetOrHead","StatusCode","filename","getFilenameFromUrl","etag","createETag","options","Options","range","parseRange","isGet","isHead","pipeline","timingSafeEqual"],"mappings":";;;;;;;;;;;;AAqBA;AACA,eAAe,aAAa,CAC1B,OAAwB,EACxB,QAAwB,EACxB,KAAiC,EAAA;;AAGjC,IAAA,QAAQ,CAAC,QAAQ,GAAG,IAAI;;AAExB,IAAA,QAAQ,CAAC,mBAAmB,GAAG,IAAI;IACnC,QAAQ,CAAC,SAAS,CAACA,kBAAU,CAAC,aAAa,EAAE,CAAC,CAAC;;;IAI/C,QAAQ,CAAC,SAAS,CAACA,kBAAU,CAAC,YAAY,EAAEC,mBAAW,CAAC,YAAY,CAAC;;AAGrE,IAAA,MAAMC,QAAM,GAAG,OAAO,CAAC,MAAM;AAC7B,IAAA,IAAI,CAACC,kBAAW,CAACD,QAAM,CAAC,EAAE;QACxB,QAAQ,CAAC,SAAS,CAACF,kBAAU,CAAC,KAAK,EAAEC,mBAAW,CAAC,KAAK,CAAC;AACvD,QAAA,OAAO,KAAK,QAAQ,CAAC,SAAS,CAACG,iBAAU,CAAC,gBAAgB,CAAC,CAAC,GAAG,EAAE;IACnE;AAEA,IAAA,MAAMC,UAAQ,GAAGC,2BAAkB,CAAC,OAAO,CAAC;AAC5C,IAAA,IAAID,UAAQ,KAAK,IAAI,EAAE;AACrB,QAAA,OAAO,KAAK,QAAQ,CAAC,SAAS,CAACD,iBAAU,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE;IACvD;;IAGA,QAAQ,CAAC,SAAS,CAACJ,kBAAU,CAAC,YAAY,EAAEC,mBAAW,CAAC,YAAY,CAAC;IACrE,QAAQ,CAAC,SAAS,CAACD,kBAAU,CAAC,WAAW,EAAEC,mBAAW,CAAC,WAAW,CAAC;;AAGnE,IAAA,MAAMM,MAAI,GAAGC,eAAU,CAACH,UAAQ,CAAC;;IAEjC,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC;;IAE1B,QAAQ,CAAC,SAAS,CAACL,kBAAU,CAAC,YAAY,EAAEC,mBAAW,CAAC,YAAY,CAAC;IACrE,QAAQ,CAAC,SAAS,CAACD,kBAAU,CAAC,IAAI,EAAEO,MAAI,CAAC;AACzC,IAAA,QAAQ,CAAC,SAAS,CAACP,kBAAU,CAAC,YAAY,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC;AAEhE,IAAA,MAAMS,SAAO,GAAG,IAAIC,eAAO,CAAC,OAAO,CAAC;;;IAGpC,IAAID,SAAO,CAAC,kBAAkB,CAACF,MAAI,EAAE,KAAK,CAAC,EAAE;AAC3C,QAAA,OAAO,KAAK,QAAQ,CAAC,SAAS,CAACH,iBAAU,CAAC,kBAAkB,CAAC,CAAC,GAAG,EAAE;IACrE;;;SAGK,IAAIK,SAAO,CAAC,WAAW,CAACF,MAAI,EAAE,KAAK,CAAC,EAAE;AACzC,QAAA,OAAO,KAAK,QAAQ,CAAC,SAAS,CAACH,iBAAU,CAAC,WAAW,CAAC,CAAC,GAAG,EAAE;IAC9D;AAEA,IAAA,MAAMO,OAAK,GAAGC,gBAAU,CAAC,OAAO,CAAC;AACjC,IAAA,IAAID,OAAK,KAAK,IAAI,EAAE;AAClB,QAAA,OAAO,KAAK,QAAQ,CAAC,SAAS,CAACP,iBAAU,CAAC,UAAU,CAAC,CAAC,GAAG,EAAE;IAC7D;AAEA,IAAA,MAAM,eAAe,GAAG,IAAI,eAAe,EAAE;AAC7C,IAAA,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,eAAe,CAAC,KAAK,EAAE,CAAC;AAClD,IAAA,MAAM,MAAM,GAAG,eAAe,CAAC,MAAM;AAErC,IAAA,KAAK,CAAC,OAAO,CAACC,UAAQ,EAAE;AACtB,QAAA,MAAM,EAAE,KAAK;eACbM,OAAK;QACL,MAAM;;AAEN,QAAA,QAAQ,EAAE,OAAO,MAAM,KAAI;YACzB,IAAI,QAAQ,CAAC,SAAS;AAAE,gBAAA,OAAO,KAAK,QAAQ,CAAC,OAAO,EAAE;AAEtD,YAAA,QAAQ,MAAM,CAAC,IAAI;AACjB,gBAAA,KAAK,eAAe;;AAElB,oBAAA,OAAO,KAAK,QAAQ,CAAC,SAAS,CAACP,iBAAU,CAAC,mBAAmB,CAAC,CAAC,GAAG,EAAE;AACtE,gBAAA,KAAK,UAAU;;AAEb,oBAAA,OAAO,KAAK,QAAQ,CAAC,SAAS,CAACA,iBAAU,CAAC,QAAQ,CAAC,CAAC,GAAG,EAAE;AAC3D,gBAAA,KAAK,qBAAqB;;AAExB,oBAAA,QAAQ,CAAC,SAAS,CAACJ,kBAAU,CAAC,YAAY,EAAE,CAAA,QAAA,EAAW,MAAM,CAAC,IAAI,CAAA,CAAE,CAAC;AACrE,oBAAA,OAAO,KAAK,QAAQ,CAAC,SAAS,CAACI,iBAAU,CAAC,mBAAmB,CAAC,CAAC,GAAG,EAAE;;YAKxE,IAAIS,YAAK,CAACX,QAAM,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE;;AAEjC,gBAAA,OAAO,KAAK,QAAQ,CAAC,SAAS,CAACE,iBAAU,CAAC,cAAc,CAAC,CAAC,GAAG,EAAE;YACjE;YAEA,IAAIO,OAAK,EAAE;;gBAET,QAAQ,CAAC,SAAS,CAChBX,kBAAU,CAAC,aAAa,EACxB,MAAM,CAAC,KAAK,CAAC,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAC1C;gBACD,QAAQ,CAAC,SAAS,CAChBA,kBAAU,CAAC,YAAY,EACvB,CAAA,MAAA,EAAS,MAAM,CAAC,KAAK,CAAC,KAAK,CAAA,CAAA,EAAI,MAAM,CAAC,KAAK,CAAC,GAAG,CAAA,CAAA,EAAI,MAAM,CAAC,IAAI,CAAA,CAAE,CACjE;AACD,gBAAA,QAAQ,CAAC,SAAS,CAACI,iBAAU,CAAC,cAAc,CAAC;YAC/C;iBAAO;;gBAEL,QAAQ,CAAC,SAAS,CAACJ,kBAAU,CAAC,aAAa,EAAE,MAAM,CAAC,IAAI,CAAC;AACzD,gBAAA,QAAQ,CAAC,SAAS,CAACI,iBAAU,CAAC,EAAE,CAAC;YACnC;;AAGA,YAAA,IAAIU,aAAM,CAACZ,QAAM,CAAC,EAAE;AAClB,gBAAA,OAAO,KAAK,QAAQ,CAAC,GAAG,EAAE;YAC5B;AAEA,YAAA,OAAO,MAAMa,iBAAQ,CAAC,MAAM,CAAC,IAAK,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,MAAK;;;;AAIrE,YAAA,CAAC,CAAC;QACJ;AACD,KAAA,CAAC;AACJ;AAEA;;;;;;;;AAQG;AACG,SAAU,oBAAoB,CAClC,OAA6C,EAAA;AAE7C,IAAA,MAAM,EAAE,KAAK,EAAE,GAAG,OAAO;AACzB,IAAA,OAAO,CAAC,OAAO,EAAE,QAAQ,KAAK,KAAK,aAAa,CAAC,OAAO,EAAE,QAAQ,EAAE,KAAK,CAAC;AAC5E;AAEA;AACA,SAAS,gBAAgB,CACvB,OAAwB,EACxB,QAAwB,EACxB,OAAwB,EACxB,UAAkB,EAAA;;AAGlB,IAAA,QAAQ,CAAC,QAAQ,GAAG,IAAI;;AAExB,IAAA,QAAQ,CAAC,mBAAmB,GAAG,IAAI;IACnC,QAAQ,CAAC,SAAS,CAACf,kBAAU,CAAC,aAAa,EAAE,CAAC,CAAC;;IAG/C,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,eAAe,CAAC;;IAGrD,IAAI,CAAC,YAAY,IAAI,YAAY,CAAC,MAAM,KAAK,UAAU,CAAC,MAAM,EAAE;;QAE9D,QAAQ,CAAC,SAAS,CAACA,kBAAU,CAAC,eAAe,EAAEC,mBAAW,CAAC,eAAe,CAAC;AAC3E,QAAA,OAAO,KAAK,QAAQ,CAAC,SAAS,CAACG,iBAAU,CAAC,YAAY,CAAC,CAAC,GAAG,EAAE;IAC/D;;;AAIA,IAAA,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE;IACjC,MAAM,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC;IACnD,MAAM,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC;AAEjD,IAAA,IACE,cAAc,CAAC,UAAU,KAAK,cAAc,CAAC,UAAU;AACvD,QAAA,CAACY,2BAAe,CAAC,cAAc,EAAE,cAAc,CAAC,EAChD;QACA,QAAQ,CAAC,SAAS,CAAChB,kBAAU,CAAC,eAAe,EAAEC,mBAAW,CAAC,eAAe,CAAC;AAC3E,QAAA,OAAO,KAAK,QAAQ,CAAC,SAAS,CAACG,iBAAU,CAAC,YAAY,CAAC,CAAC,GAAG,EAAE;IAC/D;AAEA,IAAA,OAAO,OAAO,CAAC,OAAO,EAAE,QAAQ,CAAC;AACnC;AAEA;AACM,SAAU,uBAAuB,CACrC,OAAwB,EACxB,SAAgD,EAAA;AAEhD,IAAA,MAAM,UAAU,GAAG,CAAA,OAAA,EAAU,SAAS,EAAE;AACxC,IAAA,OAAO,CAAC,OAAO,EAAE,QAAQ,KACvB,gBAAgB,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,CAAC;AAC5D;;;;;"}
|
|
1
|
+
{"version":3,"file":"handler.cjs","sources":["../src/handler.ts"],"sourcesContent":["import type {\n IncomingMessage,\n RequestListener,\n ServerResponse\n} from 'node:http';\nimport { pipeline } from 'node:stream/promises';\nimport { timingSafeEqual } from 'node:crypto';\nimport type { PFrameInternal } from '@milaboratories/pl-model-middle-layer';\nimport {\n createETag,\n getFilenameFromUrl,\n parseRange,\n isGetOrHead,\n isGet,\n isHead,\n Options,\n StatusCode,\n HeaderName,\n HeaderValue\n} from './utils';\n\n/** Main request handler for parquet files */\nasync function handleRequest(\n request: IncomingMessage,\n response: ServerResponse,\n store: PFrameInternal.ObjectStore\n): Promise<void> {\n // RFC 9110 section 6.6.1: Date header should be present in all responses\n response.sendDate = true;\n // RFC 9110 section 8.6: Content-Length 0 as default for error responses\n response.strictContentLength = true;\n response.setHeader(HeaderName.ContentLength, 0);\n // Note: setting Content-Length disables Node.js default Transfer-Encoding: chunked\n\n // RFC 9111 section 5.2: Cache-Control header with public allows to cache authenticated responses\n response.setHeader(HeaderName.CacheControl, HeaderValue.CacheControl);\n\n // RFC 9110 section 15.5.6: Method not allowed\n const method = request.method;\n if (!isGetOrHead(method)) {\n response.setHeader(HeaderName.Allow, HeaderValue.Allow);\n return void response.writeHead(StatusCode.MethodNotAllowed).end();\n }\n\n const filename = getFilenameFromUrl(request);\n if (filename === null) {\n return void response.writeHead(StatusCode.Gone).end();\n }\n\n // From now on we are sure that the response would be a Parquet file\n response.setHeader(HeaderName.AcceptRanges, HeaderValue.AcceptRanges);\n response.setHeader(HeaderName.ContentType, HeaderValue.ContentType);\n\n // RFC 9110 section 8.8.3: ETag header is used for cache versioning\n const etag = createETag(filename);\n // RFC 9110 section 8.8.2: Last-Modified header field for cache validation\n const mtime = new Date(0); // Using fake fixed date since files are immutable\n // RFC 9111 section 5.2: Cache-Control header with public allows to cache authenticated responses\n response.setHeader(HeaderName.CacheControl, HeaderValue.CacheControl);\n response.setHeader(HeaderName.ETag, etag);\n response.setHeader(HeaderName.LastModified, mtime.toUTCString());\n\n const options = new Options(request);\n // RFC 9110 section 13.1.1: If-Match precondition evaluation\n // RFC 9110 section 13.1.4: If-Unmodified-Since precondition evaluation\n if (options.preconditionFailed(etag, mtime)) {\n return void response.writeHead(StatusCode.PreconditionFailed).end();\n }\n // RFC 9110 section 13.1.2: If-None-Match precondition evaluation\n // RFC 9110 section 13.1.3: If-Modified-Since precondition evaluation\n else if (options.notModified(etag, mtime)) {\n return void response.writeHead(StatusCode.NotModified).end();\n }\n\n const range = parseRange(request);\n if (range === null) {\n return void response.writeHead(StatusCode.BadRequest).end();\n }\n\n const abortController = new AbortController();\n request.on('close', () => abortController.abort());\n const signal = abortController.signal;\n\n store.request(filename, {\n method: 'GET',\n range,\n signal,\n // pipeline automatically destroys the streams if they were not gracefully closed\n callback: async (result) => {\n if (response.destroyed) return void response.destroy();\n\n switch (result.type) {\n case 'InternalError':\n // object store encountered network error, retry by client can help\n return void response.writeHead(StatusCode.InternalServerError).end();\n case 'NotFound':\n // RFC 9110 section 15.4.5: Not found\n return void response.writeHead(StatusCode.NotFound).end();\n case 'RangeNotSatisfiable':\n // RFC 9110 section 15.5.17: Range not satisfiable\n response.setHeader(HeaderName.ContentRange, `bytes */${result.size}`);\n return void response.writeHead(StatusCode.RangeNotSatisfiable).end();\n case 'Ok':\n break;\n }\n\n if (isGet(method) && !result.data) {\n // object store implementation is incorrect, retry by client cannot help\n return void response.writeHead(StatusCode.GatewayTimeout).end();\n }\n\n if (range) {\n // RFC 9110 section 14.4: Partial content response\n response.setHeader(\n HeaderName.ContentLength,\n result.range.end - result.range.start + 1\n );\n response.setHeader(\n HeaderName.ContentRange,\n `bytes ${result.range.start}-${result.range.end}/${result.size}`\n );\n response.writeHead(StatusCode.PartialContent);\n } else {\n // RFC 9110 section 15.3.1: OK response\n response.setHeader(HeaderName.ContentLength, result.size);\n response.writeHead(StatusCode.Ok);\n }\n\n // RFC 9110 section 9.3.2: HEAD method must not return message body\n if (isHead(method)) {\n return void response.end();\n }\n\n return await pipeline(result.data!, response, { signal }).catch(() => {\n // Pipeline errors are expected when request is aborted or connection is lost\n // Response head was already written, so we can't change status code\n // Just mute the error - pipeline destroys the response stream\n });\n }\n });\n}\n\n/**\n * Create a request handler for serving files from an object store\n * compatible with HTTP/1.1 as defined in RFC 9110 and RFC 9111:\n * - <https://datatracker.ietf.org/doc/html/rfc9110>\n * - <https://datatracker.ietf.org/doc/html/rfc9111>\n *\n * Accepts only paths of the form `/<filename>.parquet`, returns 410 Gone otherwise\n * Assumes that files are immutable (and sets cache headers accordingly)\n */\nexport function createRequestHandler(\n options: PFrameInternal.RequestHandlerOptions\n): RequestListener {\n const { store } = options;\n return (request, response) => void handleRequest(request, response, store);\n}\n\n/** Request authorization middleware */\nfunction authorizeRequest(\n request: IncomingMessage,\n response: ServerResponse,\n handler: RequestListener,\n authHeader: string\n): void {\n // RFC 9110 section 6.6.1: Date header should be present in all responses\n response.sendDate = true;\n // RFC 9110 section 8.6: Content-Length 0 as default for error responses\n response.strictContentLength = true;\n response.setHeader(HeaderName.ContentLength, 0);\n // Note: setting Content-Length disables Node.js default Transfer-Encoding: chunked\n\n const actualHeader = request.headers[HeaderName.Authorization];\n\n // Early length check to avoid unnecessary processing\n if (!actualHeader || actualHeader.length !== authHeader.length) {\n // RFC 9110 section 11.6.1: WWW-Authenticate header field\n response.setHeader(HeaderName.WWWAuthenticate, HeaderValue.WWWAuthenticate);\n return void response.writeHead(StatusCode.Unauthorized).end();\n }\n\n // Use timing-safe comparison to prevent timing attacks\n // <https://developers.cloudflare.com/workers/examples/protect-against-timing-attacks/>\n const encoder = new TextEncoder();\n const receivedBuffer = encoder.encode(actualHeader);\n const expectedBuffer = encoder.encode(authHeader);\n\n if (\n receivedBuffer.byteLength !== expectedBuffer.byteLength ||\n !timingSafeEqual(receivedBuffer, expectedBuffer)\n ) {\n response.setHeader(HeaderName.WWWAuthenticate, HeaderValue.WWWAuthenticate);\n return void response.writeHead(StatusCode.Unauthorized).end();\n }\n\n return handler(request, response);\n}\n\n/** Apply Bearer token authorization to @param handler */\nexport function authorizeRequestHandler(\n handler: RequestListener,\n authToken: PFrameInternal.HttpAuthorizationToken\n): RequestListener {\n const authHeader = `Bearer ${authToken}`;\n return (request, response) =>\n authorizeRequest(request, response, handler, authHeader);\n}\n"],"names":["HeaderName","HeaderValue","method","isGetOrHead","StatusCode","filename","getFilenameFromUrl","etag","createETag","options","Options","range","parseRange","isGet","isHead","pipeline","timingSafeEqual"],"mappings":"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"}
|
package/dist/handler.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { PFrameInternal } from '@milaboratories/pl-model-middle-layer';
|
|
|
6
6
|
* - <https://datatracker.ietf.org/doc/html/rfc9110>
|
|
7
7
|
* - <https://datatracker.ietf.org/doc/html/rfc9111>
|
|
8
8
|
*
|
|
9
|
-
* Accepts only paths of the form `/<filename>.parquet`, returns
|
|
9
|
+
* Accepts only paths of the form `/<filename>.parquet`, returns 410 Gone otherwise
|
|
10
10
|
* Assumes that files are immutable (and sets cache headers accordingly)
|
|
11
11
|
*/
|
|
12
12
|
export declare function createRequestHandler(options: PFrameInternal.RequestHandlerOptions): RequestListener;
|
package/dist/handler.js
CHANGED
|
@@ -110,7 +110,7 @@ async function handleRequest(request, response, store) {
|
|
|
110
110
|
* - <https://datatracker.ietf.org/doc/html/rfc9110>
|
|
111
111
|
* - <https://datatracker.ietf.org/doc/html/rfc9111>
|
|
112
112
|
*
|
|
113
|
-
* Accepts only paths of the form `/<filename>.parquet`, returns
|
|
113
|
+
* Accepts only paths of the form `/<filename>.parquet`, returns 410 Gone otherwise
|
|
114
114
|
* Assumes that files are immutable (and sets cache headers accordingly)
|
|
115
115
|
*/
|
|
116
116
|
function createRequestHandler(options) {
|
|
@@ -125,7 +125,7 @@ function authorizeRequest(request, response, handler, authHeader) {
|
|
|
125
125
|
response.strictContentLength = true;
|
|
126
126
|
response.setHeader(HeaderName.ContentLength, 0);
|
|
127
127
|
// Note: setting Content-Length disables Node.js default Transfer-Encoding: chunked
|
|
128
|
-
const actualHeader = request.headers[
|
|
128
|
+
const actualHeader = request.headers[HeaderName.Authorization];
|
|
129
129
|
// Early length check to avoid unnecessary processing
|
|
130
130
|
if (!actualHeader || actualHeader.length !== authHeader.length) {
|
|
131
131
|
// RFC 9110 section 11.6.1: WWW-Authenticate header field
|
package/dist/handler.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handler.js","sources":["../src/handler.ts"],"sourcesContent":["import type {\n IncomingMessage,\n RequestListener,\n ServerResponse\n} from 'node:http';\nimport { pipeline } from 'node:stream/promises';\nimport { timingSafeEqual } from 'node:crypto';\nimport type { PFrameInternal } from '@milaboratories/pl-model-middle-layer';\nimport {\n createETag,\n getFilenameFromUrl,\n parseRange,\n isGetOrHead,\n isHead,\n Options,\n StatusCode,\n HeaderName,\n HeaderValue,\n isGet\n} from './utils';\n\n/** Main request handler for parquet files */\nasync function handleRequest(\n request: IncomingMessage,\n response: ServerResponse,\n store: PFrameInternal.ObjectStore\n): Promise<void> {\n // RFC 9110 section 6.6.1: Date header should be present in all responses\n response.sendDate = true;\n // RFC 9110 section 8.6: Content-Length 0 as default for error responses\n response.strictContentLength = true;\n response.setHeader(HeaderName.ContentLength, 0);\n // Note: setting Content-Length disables Node.js default Transfer-Encoding: chunked\n\n // RFC 9111 section 5.2: Cache-Control header with public allows to cache authenticated responses\n response.setHeader(HeaderName.CacheControl, HeaderValue.CacheControl);\n\n // RFC 9110 section 15.5.6: Method not allowed\n const method = request.method;\n if (!isGetOrHead(method)) {\n response.setHeader(HeaderName.Allow, HeaderValue.Allow);\n return void response.writeHead(StatusCode.MethodNotAllowed).end();\n }\n\n const filename = getFilenameFromUrl(request);\n if (filename === null) {\n return void response.writeHead(StatusCode.Gone).end();\n }\n\n // From now on we are sure that the response would be a Parquet file\n response.setHeader(HeaderName.AcceptRanges, HeaderValue.AcceptRanges);\n response.setHeader(HeaderName.ContentType, HeaderValue.ContentType);\n\n // RFC 9110 section 8.8.3: ETag header is used for cache versioning\n const etag = createETag(filename);\n // RFC 9110 section 8.8.2: Last-Modified header field for cache validation\n const mtime = new Date(0); // Using fake fixed date since files are immutable\n // RFC 9111 section 5.2: Cache-Control header with public allows to cache authenticated responses\n response.setHeader(HeaderName.CacheControl, HeaderValue.CacheControl);\n response.setHeader(HeaderName.ETag, etag);\n response.setHeader(HeaderName.LastModified, mtime.toUTCString());\n\n const options = new Options(request);\n // RFC 9110 section 13.1.1: If-Match precondition evaluation\n // RFC 9110 section 13.1.4: If-Unmodified-Since precondition evaluation\n if (options.preconditionFailed(etag, mtime)) {\n return void response.writeHead(StatusCode.PreconditionFailed).end();\n }\n // RFC 9110 section 13.1.2: If-None-Match precondition evaluation\n // RFC 9110 section 13.1.3: If-Modified-Since precondition evaluation\n else if (options.notModified(etag, mtime)) {\n return void response.writeHead(StatusCode.NotModified).end();\n }\n\n const range = parseRange(request);\n if (range === null) {\n return void response.writeHead(StatusCode.BadRequest).end();\n }\n\n const abortController = new AbortController();\n request.on('close', () => abortController.abort());\n const signal = abortController.signal;\n\n store.request(filename, {\n method: 'GET',\n range,\n signal,\n // pipeline automatically destroys the streams if they were not gracefully closed\n callback: async (result) => {\n if (response.destroyed) return void response.destroy();\n\n switch (result.type) {\n case 'InternalError':\n // object store encountered network error, retry by client can help\n return void response.writeHead(StatusCode.InternalServerError).end();\n case 'NotFound':\n // RFC 9110 section 15.4.5: Not found\n return void response.writeHead(StatusCode.NotFound).end();\n case 'RangeNotSatisfiable':\n // RFC 9110 section 15.5.17: Range not satisfiable\n response.setHeader(HeaderName.ContentRange, `bytes */${result.size}`);\n return void response.writeHead(StatusCode.RangeNotSatisfiable).end();\n case 'Ok':\n break;\n }\n\n if (isGet(method) && !result.data) {\n // object store implementation is incorrect, retry by client cannot help\n return void response.writeHead(StatusCode.GatewayTimeout).end();\n }\n\n if (range) {\n // RFC 9110 section 14.4: Partial content response\n response.setHeader(\n HeaderName.ContentLength,\n result.range.end - result.range.start + 1\n );\n response.setHeader(\n HeaderName.ContentRange,\n `bytes ${result.range.start}-${result.range.end}/${result.size}`\n );\n response.writeHead(StatusCode.PartialContent);\n } else {\n // RFC 9110 section 15.3.1: OK response\n response.setHeader(HeaderName.ContentLength, result.size);\n response.writeHead(StatusCode.Ok);\n }\n\n // RFC 9110 section 9.3.2: HEAD method must not return message body\n if (isHead(method)) {\n return void response.end();\n }\n\n return await pipeline(result.data!, response, { signal }).catch(() => {\n // Pipeline errors are expected when request is aborted or connection is lost\n // Response head was already written, so we can't change status code\n // Just mute the error - pipeline destroys the response stream\n });\n }\n });\n}\n\n/**\n * Create a request handler for serving files from an object store\n * compatible with HTTP/1.1 as defined in RFC 9110 and RFC 9111:\n * - <https://datatracker.ietf.org/doc/html/rfc9110>\n * - <https://datatracker.ietf.org/doc/html/rfc9111>\n *\n * Accepts only paths of the form `/<filename>.parquet`, returns 404 otherwise\n * Assumes that files are immutable (and sets cache headers accordingly)\n */\nexport function createRequestHandler(\n options: PFrameInternal.RequestHandlerOptions\n): RequestListener {\n const { store } = options;\n return (request, response) => void handleRequest(request, response, store);\n}\n\n/** Request authorization middleware */\nfunction authorizeRequest(\n request: IncomingMessage,\n response: ServerResponse,\n handler: RequestListener,\n authHeader: string\n): void {\n // RFC 9110 section 6.6.1: Date header should be present in all responses\n response.sendDate = true;\n // RFC 9110 section 8.6: Content-Length 0 as default for error responses\n response.strictContentLength = true;\n response.setHeader(HeaderName.ContentLength, 0);\n // Note: setting Content-Length disables Node.js default Transfer-Encoding: chunked\n\n const actualHeader = request.headers['authorization'];\n\n // Early length check to avoid unnecessary processing\n if (!actualHeader || actualHeader.length !== authHeader.length) {\n // RFC 9110 section 11.6.1: WWW-Authenticate header field\n response.setHeader(HeaderName.WWWAuthenticate, HeaderValue.WWWAuthenticate);\n return void response.writeHead(StatusCode.Unauthorized).end();\n }\n\n // Use timing-safe comparison to prevent timing attacks\n // <https://developers.cloudflare.com/workers/examples/protect-against-timing-attacks/>\n const encoder = new TextEncoder();\n const receivedBuffer = encoder.encode(actualHeader);\n const expectedBuffer = encoder.encode(authHeader);\n\n if (\n receivedBuffer.byteLength !== expectedBuffer.byteLength ||\n !timingSafeEqual(receivedBuffer, expectedBuffer)\n ) {\n response.setHeader(HeaderName.WWWAuthenticate, HeaderValue.WWWAuthenticate);\n return void response.writeHead(StatusCode.Unauthorized).end();\n }\n\n return handler(request, response);\n}\n\n/** Apply Bearer token authorization to @param handler */\nexport function authorizeRequestHandler(\n handler: RequestListener,\n authToken: PFrameInternal.HttpAuthorizationToken\n): RequestListener {\n const authHeader = `Bearer ${authToken}`;\n return (request, response) =>\n authorizeRequest(request, response, handler, authHeader);\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"handler.js","sources":["../src/handler.ts"],"sourcesContent":["import type {\n IncomingMessage,\n RequestListener,\n ServerResponse\n} from 'node:http';\nimport { pipeline } from 'node:stream/promises';\nimport { timingSafeEqual } from 'node:crypto';\nimport type { PFrameInternal } from '@milaboratories/pl-model-middle-layer';\nimport {\n createETag,\n getFilenameFromUrl,\n parseRange,\n isGetOrHead,\n isGet,\n isHead,\n Options,\n StatusCode,\n HeaderName,\n HeaderValue\n} from './utils';\n\n/** Main request handler for parquet files */\nasync function handleRequest(\n request: IncomingMessage,\n response: ServerResponse,\n store: PFrameInternal.ObjectStore\n): Promise<void> {\n // RFC 9110 section 6.6.1: Date header should be present in all responses\n response.sendDate = true;\n // RFC 9110 section 8.6: Content-Length 0 as default for error responses\n response.strictContentLength = true;\n response.setHeader(HeaderName.ContentLength, 0);\n // Note: setting Content-Length disables Node.js default Transfer-Encoding: chunked\n\n // RFC 9111 section 5.2: Cache-Control header with public allows to cache authenticated responses\n response.setHeader(HeaderName.CacheControl, HeaderValue.CacheControl);\n\n // RFC 9110 section 15.5.6: Method not allowed\n const method = request.method;\n if (!isGetOrHead(method)) {\n response.setHeader(HeaderName.Allow, HeaderValue.Allow);\n return void response.writeHead(StatusCode.MethodNotAllowed).end();\n }\n\n const filename = getFilenameFromUrl(request);\n if (filename === null) {\n return void response.writeHead(StatusCode.Gone).end();\n }\n\n // From now on we are sure that the response would be a Parquet file\n response.setHeader(HeaderName.AcceptRanges, HeaderValue.AcceptRanges);\n response.setHeader(HeaderName.ContentType, HeaderValue.ContentType);\n\n // RFC 9110 section 8.8.3: ETag header is used for cache versioning\n const etag = createETag(filename);\n // RFC 9110 section 8.8.2: Last-Modified header field for cache validation\n const mtime = new Date(0); // Using fake fixed date since files are immutable\n // RFC 9111 section 5.2: Cache-Control header with public allows to cache authenticated responses\n response.setHeader(HeaderName.CacheControl, HeaderValue.CacheControl);\n response.setHeader(HeaderName.ETag, etag);\n response.setHeader(HeaderName.LastModified, mtime.toUTCString());\n\n const options = new Options(request);\n // RFC 9110 section 13.1.1: If-Match precondition evaluation\n // RFC 9110 section 13.1.4: If-Unmodified-Since precondition evaluation\n if (options.preconditionFailed(etag, mtime)) {\n return void response.writeHead(StatusCode.PreconditionFailed).end();\n }\n // RFC 9110 section 13.1.2: If-None-Match precondition evaluation\n // RFC 9110 section 13.1.3: If-Modified-Since precondition evaluation\n else if (options.notModified(etag, mtime)) {\n return void response.writeHead(StatusCode.NotModified).end();\n }\n\n const range = parseRange(request);\n if (range === null) {\n return void response.writeHead(StatusCode.BadRequest).end();\n }\n\n const abortController = new AbortController();\n request.on('close', () => abortController.abort());\n const signal = abortController.signal;\n\n store.request(filename, {\n method: 'GET',\n range,\n signal,\n // pipeline automatically destroys the streams if they were not gracefully closed\n callback: async (result) => {\n if (response.destroyed) return void response.destroy();\n\n switch (result.type) {\n case 'InternalError':\n // object store encountered network error, retry by client can help\n return void response.writeHead(StatusCode.InternalServerError).end();\n case 'NotFound':\n // RFC 9110 section 15.4.5: Not found\n return void response.writeHead(StatusCode.NotFound).end();\n case 'RangeNotSatisfiable':\n // RFC 9110 section 15.5.17: Range not satisfiable\n response.setHeader(HeaderName.ContentRange, `bytes */${result.size}`);\n return void response.writeHead(StatusCode.RangeNotSatisfiable).end();\n case 'Ok':\n break;\n }\n\n if (isGet(method) && !result.data) {\n // object store implementation is incorrect, retry by client cannot help\n return void response.writeHead(StatusCode.GatewayTimeout).end();\n }\n\n if (range) {\n // RFC 9110 section 14.4: Partial content response\n response.setHeader(\n HeaderName.ContentLength,\n result.range.end - result.range.start + 1\n );\n response.setHeader(\n HeaderName.ContentRange,\n `bytes ${result.range.start}-${result.range.end}/${result.size}`\n );\n response.writeHead(StatusCode.PartialContent);\n } else {\n // RFC 9110 section 15.3.1: OK response\n response.setHeader(HeaderName.ContentLength, result.size);\n response.writeHead(StatusCode.Ok);\n }\n\n // RFC 9110 section 9.3.2: HEAD method must not return message body\n if (isHead(method)) {\n return void response.end();\n }\n\n return await pipeline(result.data!, response, { signal }).catch(() => {\n // Pipeline errors are expected when request is aborted or connection is lost\n // Response head was already written, so we can't change status code\n // Just mute the error - pipeline destroys the response stream\n });\n }\n });\n}\n\n/**\n * Create a request handler for serving files from an object store\n * compatible with HTTP/1.1 as defined in RFC 9110 and RFC 9111:\n * - <https://datatracker.ietf.org/doc/html/rfc9110>\n * - <https://datatracker.ietf.org/doc/html/rfc9111>\n *\n * Accepts only paths of the form `/<filename>.parquet`, returns 410 Gone otherwise\n * Assumes that files are immutable (and sets cache headers accordingly)\n */\nexport function createRequestHandler(\n options: PFrameInternal.RequestHandlerOptions\n): RequestListener {\n const { store } = options;\n return (request, response) => void handleRequest(request, response, store);\n}\n\n/** Request authorization middleware */\nfunction authorizeRequest(\n request: IncomingMessage,\n response: ServerResponse,\n handler: RequestListener,\n authHeader: string\n): void {\n // RFC 9110 section 6.6.1: Date header should be present in all responses\n response.sendDate = true;\n // RFC 9110 section 8.6: Content-Length 0 as default for error responses\n response.strictContentLength = true;\n response.setHeader(HeaderName.ContentLength, 0);\n // Note: setting Content-Length disables Node.js default Transfer-Encoding: chunked\n\n const actualHeader = request.headers[HeaderName.Authorization];\n\n // Early length check to avoid unnecessary processing\n if (!actualHeader || actualHeader.length !== authHeader.length) {\n // RFC 9110 section 11.6.1: WWW-Authenticate header field\n response.setHeader(HeaderName.WWWAuthenticate, HeaderValue.WWWAuthenticate);\n return void response.writeHead(StatusCode.Unauthorized).end();\n }\n\n // Use timing-safe comparison to prevent timing attacks\n // <https://developers.cloudflare.com/workers/examples/protect-against-timing-attacks/>\n const encoder = new TextEncoder();\n const receivedBuffer = encoder.encode(actualHeader);\n const expectedBuffer = encoder.encode(authHeader);\n\n if (\n receivedBuffer.byteLength !== expectedBuffer.byteLength ||\n !timingSafeEqual(receivedBuffer, expectedBuffer)\n ) {\n response.setHeader(HeaderName.WWWAuthenticate, HeaderValue.WWWAuthenticate);\n return void response.writeHead(StatusCode.Unauthorized).end();\n }\n\n return handler(request, response);\n}\n\n/** Apply Bearer token authorization to @param handler */\nexport function authorizeRequestHandler(\n handler: RequestListener,\n authToken: PFrameInternal.HttpAuthorizationToken\n): RequestListener {\n const authHeader = `Bearer ${authToken}`;\n return (request, response) =>\n authorizeRequest(request, response, handler, authHeader);\n}\n"],"names":[],"mappings":"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"}
|
package/dist/index.cjs
CHANGED
|
@@ -13,5 +13,6 @@ exports.createRequestHandler = handler.createRequestHandler;
|
|
|
13
13
|
exports.FileSystemStore = fsStore.FileSystemStore;
|
|
14
14
|
exports.serve = serve.serve;
|
|
15
15
|
exports.HttpHelpers = _export.HttpHelpers;
|
|
16
|
+
exports.ParquetServer = parquetServer.ParquetServer;
|
|
16
17
|
exports.runParquetServer = parquetServer.runParquetServer;
|
|
17
18
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;"}
|
package/dist/index.js
CHANGED
|
@@ -2,5 +2,5 @@ export { authorizeRequestHandler, createRequestHandler } from './handler.js';
|
|
|
2
2
|
export { FileSystemStore } from './fs-store.js';
|
|
3
3
|
export { serve } from './serve.js';
|
|
4
4
|
export { HttpHelpers } from './export.js';
|
|
5
|
-
export { runParquetServer } from './parquet-server.js';
|
|
5
|
+
export { ParquetServer, runParquetServer } from './parquet-server.js';
|
|
6
6
|
//# sourceMappingURL=index.js.map
|
package/dist/parquet-server.cjs
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var node_child_process = require('node:child_process');
|
|
4
|
+
var promises = require('node:readline/promises');
|
|
5
|
+
var node_path = require('node:path');
|
|
6
|
+
var node_url = require('node:url');
|
|
3
7
|
var commander = require('commander');
|
|
4
8
|
var fsStore = require('./fs-store.cjs');
|
|
5
9
|
var handler = require('./handler.cjs');
|
|
6
10
|
var serve = require('./serve.cjs');
|
|
11
|
+
var plModelCommon = require('@milaboratories/pl-model-common');
|
|
7
12
|
|
|
13
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
14
|
+
const Options = {
|
|
15
|
+
Http: '--http',
|
|
16
|
+
NoAuth: '--no-auth'
|
|
17
|
+
};
|
|
8
18
|
/**
|
|
9
19
|
* Serves parquet files from the given root directory.
|
|
10
20
|
* Manages the server lifecycle with graceful shutdown.
|
|
@@ -15,8 +25,8 @@ async function runParquetServer() {
|
|
|
15
25
|
.name('parquet-server')
|
|
16
26
|
.description('Serve parquet files from a directory over HTTP(S)')
|
|
17
27
|
.argument('<root-directory>', 'Root directory containing parquet files')
|
|
18
|
-
.option(
|
|
19
|
-
.option(
|
|
28
|
+
.option(Options.Http, 'Use HTTP instead of HTTPS', false)
|
|
29
|
+
.option(Options.NoAuth, 'Disable authentication')
|
|
20
30
|
.action(async (rootDir, options) => {
|
|
21
31
|
const abortController = new AbortController();
|
|
22
32
|
process
|
|
@@ -39,15 +49,58 @@ async function runParquetServer() {
|
|
|
39
49
|
});
|
|
40
50
|
abortController.signal.onabort = () => server.stop();
|
|
41
51
|
abortController.signal.throwIfAborted();
|
|
42
|
-
|
|
52
|
+
const serverConfig = plModelCommon.stringifyJson({
|
|
43
53
|
url: server.address,
|
|
44
54
|
...(server.authToken && { authToken: server.authToken }),
|
|
45
55
|
...(server.encodedCaCert && { caCert: server.encodedCaCert })
|
|
46
|
-
})
|
|
56
|
+
});
|
|
57
|
+
console.log(serverConfig);
|
|
47
58
|
await server.stopped;
|
|
48
59
|
});
|
|
49
60
|
await program.parseAsync();
|
|
50
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Reference implementation of a parquet server runner for tests:
|
|
64
|
+
* - Reads the server configuration from the spawned process stdout
|
|
65
|
+
* - Forwards the server logs to the console
|
|
66
|
+
* - Shuts down the server on dispose
|
|
67
|
+
*/
|
|
68
|
+
class ParquetServer {
|
|
69
|
+
#process;
|
|
70
|
+
#config;
|
|
71
|
+
#lineReader;
|
|
72
|
+
constructor(process, config, lineReader) {
|
|
73
|
+
this.#process = process;
|
|
74
|
+
this.#config = config;
|
|
75
|
+
this.#lineReader = lineReader;
|
|
76
|
+
}
|
|
77
|
+
get config() {
|
|
78
|
+
return this.#config;
|
|
79
|
+
}
|
|
80
|
+
static async serve(rootDir, options) {
|
|
81
|
+
const nodeFileUrl = (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('parquet-server.cjs', document.baseURI).href));
|
|
82
|
+
const nodeDirname = node_path.dirname(node_url.fileURLToPath(nodeFileUrl));
|
|
83
|
+
const binPath = node_path.join(nodeDirname, '..', 'bin', 'parquet-server.mjs');
|
|
84
|
+
const serverProcess = node_child_process.spawn('node', [
|
|
85
|
+
binPath,
|
|
86
|
+
rootDir,
|
|
87
|
+
...(options?.http ? [Options.Http] : []),
|
|
88
|
+
...(options?.noAuth ? [Options.NoAuth] : [])
|
|
89
|
+
], {
|
|
90
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
91
|
+
});
|
|
92
|
+
const lineReader = promises.createInterface({ input: serverProcess.stdout });
|
|
93
|
+
const firstLine = await lineReader[Symbol.asyncIterator]().next();
|
|
94
|
+
const serverConfig = plModelCommon.parseJson(firstLine.value);
|
|
95
|
+
lineReader.on('line', console.log);
|
|
96
|
+
return new ParquetServer(serverProcess, serverConfig, lineReader);
|
|
97
|
+
}
|
|
98
|
+
[Symbol.dispose]() {
|
|
99
|
+
this.#lineReader.close();
|
|
100
|
+
this.#process.kill();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
51
103
|
|
|
104
|
+
exports.ParquetServer = ParquetServer;
|
|
52
105
|
exports.runParquetServer = runParquetServer;
|
|
53
106
|
//# sourceMappingURL=parquet-server.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parquet-server.cjs","sources":["../src/parquet-server.ts"],"sourcesContent":["import { Command } from 'commander';\nimport { FileSystemStore } from './fs-store';\nimport { createRequestHandler } from './handler';\nimport { serve } from './serve';\n\n/**\n * Serves parquet files from the given root directory.\n * Manages the server lifecycle with graceful shutdown.\n */\nexport async function runParquetServer(): Promise<void> {\n const program = new Command();\n\n program\n .name('parquet-server')\n .description('Serve parquet files from a directory over HTTP(S)')\n .argument('<root-directory>', 'Root directory containing parquet files')\n .option(
|
|
1
|
+
{"version":3,"file":"parquet-server.cjs","sources":["../src/parquet-server.ts"],"sourcesContent":["import { type ChildProcess, spawn } from 'node:child_process';\nimport { createInterface, type Interface } from 'node:readline/promises';\nimport { join, dirname } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { Command } from 'commander';\nimport { FileSystemStore } from './fs-store';\nimport { createRequestHandler } from './handler';\nimport { serve } from './serve';\nimport {\n parseJson,\n type StringifiedJson,\n stringifyJson\n} from '@milaboratories/pl-model-common';\nimport { type PFrameInternal } from '@milaboratories/pl-model-middle-layer';\n\nconst Options = {\n Http: '--http',\n NoAuth: '--no-auth'\n} as const;\n\ntype Config = StringifiedJson<PFrameInternal.ParquetServerConfig>;\n\n/**\n * Serves parquet files from the given root directory.\n * Manages the server lifecycle with graceful shutdown.\n */\nexport async function runParquetServer(): Promise<void> {\n const program = new Command();\n\n program\n .name('parquet-server')\n .description('Serve parquet files from a directory over HTTP(S)')\n .argument('<root-directory>', 'Root directory containing parquet files')\n .option(Options.Http, 'Use HTTP instead of HTTPS', false)\n .option(Options.NoAuth, 'Disable authentication')\n .action(\n async (rootDir: string, options: { http: boolean; auth: boolean }) => {\n const abortController = new AbortController();\n process\n .on('SIGINT', () => abortController.abort())\n .on('SIGTERM', () => abortController.abort());\n abortController.signal.throwIfAborted();\n\n const store = await FileSystemStore.init({\n rootDir,\n logger: (level, message) => {\n const timestamp = new Date(Date.now()).toISOString();\n console.log(`[${timestamp}] [${level}] ${message}`);\n }\n });\n abortController.signal.throwIfAborted();\n const handler = createRequestHandler({ store });\n\n const server = await serve({\n handler,\n ...(options.http && { http: true }),\n ...(!options.auth && { noAuth: true })\n });\n abortController.signal.onabort = () => server.stop();\n abortController.signal.throwIfAborted();\n\n const serverConfig: Config = stringifyJson({\n url: server.address,\n ...(server.authToken && { authToken: server.authToken }),\n ...(server.encodedCaCert && { caCert: server.encodedCaCert })\n });\n console.log(serverConfig);\n\n await server.stopped;\n }\n );\n\n await program.parseAsync();\n}\n\n/**\n * Reference implementation of a parquet server runner for tests:\n * - Reads the server configuration from the spawned process stdout\n * - Forwards the server logs to the console\n * - Shuts down the server on dispose\n */\nexport class ParquetServer implements Disposable {\n readonly #process: ChildProcess;\n readonly #config: PFrameInternal.ParquetServerConfig;\n readonly #lineReader: Interface;\n\n private constructor(\n process: ChildProcess,\n config: PFrameInternal.ParquetServerConfig,\n lineReader: Interface\n ) {\n this.#process = process;\n this.#config = config;\n this.#lineReader = lineReader;\n }\n\n get config(): PFrameInternal.ParquetServerConfig {\n return this.#config;\n }\n\n static async serve(\n rootDir: string,\n options?: {\n http?: boolean;\n noAuth?: boolean;\n }\n ): Promise<ParquetServer> {\n const nodeFileUrl = import.meta.url;\n const nodeDirname = dirname(fileURLToPath(nodeFileUrl));\n const binPath = join(nodeDirname, '..', 'bin', 'parquet-server.mjs');\n\n const serverProcess = spawn(\n 'node',\n [\n binPath,\n rootDir,\n ...(options?.http ? [Options.Http] : []),\n ...(options?.noAuth ? [Options.NoAuth] : [])\n ],\n {\n stdio: ['ignore', 'pipe', 'ignore']\n }\n );\n\n const lineReader = createInterface({ input: serverProcess.stdout! });\n\n const firstLine = await lineReader[Symbol.asyncIterator]().next();\n const serverConfig = parseJson(firstLine.value as Config);\n\n lineReader.on('line', console.log);\n\n return new ParquetServer(serverProcess, serverConfig, lineReader);\n }\n\n [Symbol.dispose](): void {\n this.#lineReader.close();\n this.#process.kill();\n }\n}\n"],"names":["Command","FileSystemStore","handler","createRequestHandler","serve","stringifyJson","dirname","fileURLToPath","join","spawn","createInterface","parseJson"],"mappings":"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"}
|
package/dist/parquet-server.d.ts
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
|
+
import { type PFrameInternal } from '@milaboratories/pl-model-middle-layer';
|
|
1
2
|
/**
|
|
2
3
|
* Serves parquet files from the given root directory.
|
|
3
4
|
* Manages the server lifecycle with graceful shutdown.
|
|
4
5
|
*/
|
|
5
6
|
export declare function runParquetServer(): Promise<void>;
|
|
7
|
+
/**
|
|
8
|
+
* Reference implementation of a parquet server runner for tests:
|
|
9
|
+
* - Reads the server configuration from the spawned process stdout
|
|
10
|
+
* - Forwards the server logs to the console
|
|
11
|
+
* - Shuts down the server on dispose
|
|
12
|
+
*/
|
|
13
|
+
export declare class ParquetServer implements Disposable {
|
|
14
|
+
#private;
|
|
15
|
+
private constructor();
|
|
16
|
+
get config(): PFrameInternal.ParquetServerConfig;
|
|
17
|
+
static serve(rootDir: string, options?: {
|
|
18
|
+
http?: boolean;
|
|
19
|
+
noAuth?: boolean;
|
|
20
|
+
}): Promise<ParquetServer>;
|
|
21
|
+
[Symbol.dispose](): void;
|
|
22
|
+
}
|
|
6
23
|
//# sourceMappingURL=parquet-server.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parquet-server.d.ts","sourceRoot":"","sources":["../src/parquet-server.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"parquet-server.d.ts","sourceRoot":"","sources":["../src/parquet-server.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,uCAAuC,CAAC;AAS5E;;;GAGG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CA+CtD;AAED;;;;;GAKG;AACH,qBAAa,aAAc,YAAW,UAAU;;IAK9C,OAAO;IAUP,IAAI,MAAM,IAAI,cAAc,CAAC,mBAAmB,CAE/C;WAEY,KAAK,CAChB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;QACR,IAAI,CAAC,EAAE,OAAO,CAAC;QACf,MAAM,CAAC,EAAE,OAAO,CAAC;KAClB,GACA,OAAO,CAAC,aAAa,CAAC;IA4BzB,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI;CAIzB"}
|
package/dist/parquet-server.js
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createInterface } from 'node:readline/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
1
5
|
import { Command } from 'commander';
|
|
2
6
|
import { FileSystemStore } from './fs-store.js';
|
|
3
7
|
import { createRequestHandler } from './handler.js';
|
|
4
8
|
import { serve } from './serve.js';
|
|
9
|
+
import { stringifyJson, parseJson } from '@milaboratories/pl-model-common';
|
|
5
10
|
|
|
11
|
+
const Options = {
|
|
12
|
+
Http: '--http',
|
|
13
|
+
NoAuth: '--no-auth'
|
|
14
|
+
};
|
|
6
15
|
/**
|
|
7
16
|
* Serves parquet files from the given root directory.
|
|
8
17
|
* Manages the server lifecycle with graceful shutdown.
|
|
@@ -13,8 +22,8 @@ async function runParquetServer() {
|
|
|
13
22
|
.name('parquet-server')
|
|
14
23
|
.description('Serve parquet files from a directory over HTTP(S)')
|
|
15
24
|
.argument('<root-directory>', 'Root directory containing parquet files')
|
|
16
|
-
.option(
|
|
17
|
-
.option(
|
|
25
|
+
.option(Options.Http, 'Use HTTP instead of HTTPS', false)
|
|
26
|
+
.option(Options.NoAuth, 'Disable authentication')
|
|
18
27
|
.action(async (rootDir, options) => {
|
|
19
28
|
const abortController = new AbortController();
|
|
20
29
|
process
|
|
@@ -37,15 +46,57 @@ async function runParquetServer() {
|
|
|
37
46
|
});
|
|
38
47
|
abortController.signal.onabort = () => server.stop();
|
|
39
48
|
abortController.signal.throwIfAborted();
|
|
40
|
-
|
|
49
|
+
const serverConfig = stringifyJson({
|
|
41
50
|
url: server.address,
|
|
42
51
|
...(server.authToken && { authToken: server.authToken }),
|
|
43
52
|
...(server.encodedCaCert && { caCert: server.encodedCaCert })
|
|
44
|
-
})
|
|
53
|
+
});
|
|
54
|
+
console.log(serverConfig);
|
|
45
55
|
await server.stopped;
|
|
46
56
|
});
|
|
47
57
|
await program.parseAsync();
|
|
48
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Reference implementation of a parquet server runner for tests:
|
|
61
|
+
* - Reads the server configuration from the spawned process stdout
|
|
62
|
+
* - Forwards the server logs to the console
|
|
63
|
+
* - Shuts down the server on dispose
|
|
64
|
+
*/
|
|
65
|
+
class ParquetServer {
|
|
66
|
+
#process;
|
|
67
|
+
#config;
|
|
68
|
+
#lineReader;
|
|
69
|
+
constructor(process, config, lineReader) {
|
|
70
|
+
this.#process = process;
|
|
71
|
+
this.#config = config;
|
|
72
|
+
this.#lineReader = lineReader;
|
|
73
|
+
}
|
|
74
|
+
get config() {
|
|
75
|
+
return this.#config;
|
|
76
|
+
}
|
|
77
|
+
static async serve(rootDir, options) {
|
|
78
|
+
const nodeFileUrl = import.meta.url;
|
|
79
|
+
const nodeDirname = dirname(fileURLToPath(nodeFileUrl));
|
|
80
|
+
const binPath = join(nodeDirname, '..', 'bin', 'parquet-server.mjs');
|
|
81
|
+
const serverProcess = spawn('node', [
|
|
82
|
+
binPath,
|
|
83
|
+
rootDir,
|
|
84
|
+
...(options?.http ? [Options.Http] : []),
|
|
85
|
+
...(options?.noAuth ? [Options.NoAuth] : [])
|
|
86
|
+
], {
|
|
87
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
88
|
+
});
|
|
89
|
+
const lineReader = createInterface({ input: serverProcess.stdout });
|
|
90
|
+
const firstLine = await lineReader[Symbol.asyncIterator]().next();
|
|
91
|
+
const serverConfig = parseJson(firstLine.value);
|
|
92
|
+
lineReader.on('line', console.log);
|
|
93
|
+
return new ParquetServer(serverProcess, serverConfig, lineReader);
|
|
94
|
+
}
|
|
95
|
+
[Symbol.dispose]() {
|
|
96
|
+
this.#lineReader.close();
|
|
97
|
+
this.#process.kill();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
49
100
|
|
|
50
|
-
export { runParquetServer };
|
|
101
|
+
export { ParquetServer, runParquetServer };
|
|
51
102
|
//# sourceMappingURL=parquet-server.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parquet-server.js","sources":["../src/parquet-server.ts"],"sourcesContent":["import { Command } from 'commander';\nimport { FileSystemStore } from './fs-store';\nimport { createRequestHandler } from './handler';\nimport { serve } from './serve';\n\n/**\n * Serves parquet files from the given root directory.\n * Manages the server lifecycle with graceful shutdown.\n */\nexport async function runParquetServer(): Promise<void> {\n const program = new Command();\n\n program\n .name('parquet-server')\n .description('Serve parquet files from a directory over HTTP(S)')\n .argument('<root-directory>', 'Root directory containing parquet files')\n .option(
|
|
1
|
+
{"version":3,"file":"parquet-server.js","sources":["../src/parquet-server.ts"],"sourcesContent":["import { type ChildProcess, spawn } from 'node:child_process';\nimport { createInterface, type Interface } from 'node:readline/promises';\nimport { join, dirname } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { Command } from 'commander';\nimport { FileSystemStore } from './fs-store';\nimport { createRequestHandler } from './handler';\nimport { serve } from './serve';\nimport {\n parseJson,\n type StringifiedJson,\n stringifyJson\n} from '@milaboratories/pl-model-common';\nimport { type PFrameInternal } from '@milaboratories/pl-model-middle-layer';\n\nconst Options = {\n Http: '--http',\n NoAuth: '--no-auth'\n} as const;\n\ntype Config = StringifiedJson<PFrameInternal.ParquetServerConfig>;\n\n/**\n * Serves parquet files from the given root directory.\n * Manages the server lifecycle with graceful shutdown.\n */\nexport async function runParquetServer(): Promise<void> {\n const program = new Command();\n\n program\n .name('parquet-server')\n .description('Serve parquet files from a directory over HTTP(S)')\n .argument('<root-directory>', 'Root directory containing parquet files')\n .option(Options.Http, 'Use HTTP instead of HTTPS', false)\n .option(Options.NoAuth, 'Disable authentication')\n .action(\n async (rootDir: string, options: { http: boolean; auth: boolean }) => {\n const abortController = new AbortController();\n process\n .on('SIGINT', () => abortController.abort())\n .on('SIGTERM', () => abortController.abort());\n abortController.signal.throwIfAborted();\n\n const store = await FileSystemStore.init({\n rootDir,\n logger: (level, message) => {\n const timestamp = new Date(Date.now()).toISOString();\n console.log(`[${timestamp}] [${level}] ${message}`);\n }\n });\n abortController.signal.throwIfAborted();\n const handler = createRequestHandler({ store });\n\n const server = await serve({\n handler,\n ...(options.http && { http: true }),\n ...(!options.auth && { noAuth: true })\n });\n abortController.signal.onabort = () => server.stop();\n abortController.signal.throwIfAborted();\n\n const serverConfig: Config = stringifyJson({\n url: server.address,\n ...(server.authToken && { authToken: server.authToken }),\n ...(server.encodedCaCert && { caCert: server.encodedCaCert })\n });\n console.log(serverConfig);\n\n await server.stopped;\n }\n );\n\n await program.parseAsync();\n}\n\n/**\n * Reference implementation of a parquet server runner for tests:\n * - Reads the server configuration from the spawned process stdout\n * - Forwards the server logs to the console\n * - Shuts down the server on dispose\n */\nexport class ParquetServer implements Disposable {\n readonly #process: ChildProcess;\n readonly #config: PFrameInternal.ParquetServerConfig;\n readonly #lineReader: Interface;\n\n private constructor(\n process: ChildProcess,\n config: PFrameInternal.ParquetServerConfig,\n lineReader: Interface\n ) {\n this.#process = process;\n this.#config = config;\n this.#lineReader = lineReader;\n }\n\n get config(): PFrameInternal.ParquetServerConfig {\n return this.#config;\n }\n\n static async serve(\n rootDir: string,\n options?: {\n http?: boolean;\n noAuth?: boolean;\n }\n ): Promise<ParquetServer> {\n const nodeFileUrl = import.meta.url;\n const nodeDirname = dirname(fileURLToPath(nodeFileUrl));\n const binPath = join(nodeDirname, '..', 'bin', 'parquet-server.mjs');\n\n const serverProcess = spawn(\n 'node',\n [\n binPath,\n rootDir,\n ...(options?.http ? [Options.Http] : []),\n ...(options?.noAuth ? [Options.NoAuth] : [])\n ],\n {\n stdio: ['ignore', 'pipe', 'ignore']\n }\n );\n\n const lineReader = createInterface({ input: serverProcess.stdout! });\n\n const firstLine = await lineReader[Symbol.asyncIterator]().next();\n const serverConfig = parseJson(firstLine.value as Config);\n\n lineReader.on('line', console.log);\n\n return new ParquetServer(serverProcess, serverConfig, lineReader);\n }\n\n [Symbol.dispose](): void {\n this.#lineReader.close();\n this.#process.kill();\n }\n}\n"],"names":[],"mappings":"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"}
|
package/dist/utils/headers.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"headers.cjs","sources":["../../src/utils/headers.ts"],"sourcesContent":["/** HTTP header names used in the parquet server handler */\nexport const HeaderName = {\n Accept: 'accept',\n AcceptRanges: 'accept-ranges',\n Allow: 'allow',\n Authorization: 'authorization',\n CacheControl: 'cache-control',\n ContentLength: 'content-length',\n ContentRange: 'content-range',\n ContentType: 'content-type',\n Date: 'date',\n ETag: 'etag',\n IfMatch: 'if-match',\n IfModifiedSince: 'if-modified-since',\n IfNoneMatch: 'if-none-match',\n IfUnmodifiedSince: 'if-unmodified-since',\n LastModified: 'last-modified',\n Range: 'range',\n WWWAuthenticate: 'www-authenticate'\n} as const;\n\n/** HTTP header values used in the parquet server handler */\nexport const HeaderValue = {\n AcceptRanges: 'bytes',\n Allow: 'GET, HEAD',\n CacheControl: 'public, immutable, max-age=31536000',\n ContentType: 'application/octet-stream',\n WWWAuthenticate: 'Bearer realm=\"parquet-server\"'\n} as const;\n"],"names":[],"mappings":";;AAAA;AACO,MAAM,UAAU,GAAG;AACxB,IACA,YAAY,EAAE,eAAe;AAC7B,IAAA,KAAK,EAAE,OAAO;AACd,
|
|
1
|
+
{"version":3,"file":"headers.cjs","sources":["../../src/utils/headers.ts"],"sourcesContent":["/** HTTP header names used in the parquet server handler */\nexport const HeaderName = {\n Accept: 'accept',\n AcceptRanges: 'accept-ranges',\n Allow: 'allow',\n Authorization: 'authorization',\n CacheControl: 'cache-control',\n ContentLength: 'content-length',\n ContentRange: 'content-range',\n ContentType: 'content-type',\n Date: 'date',\n ETag: 'etag',\n IfMatch: 'if-match',\n IfModifiedSince: 'if-modified-since',\n IfNoneMatch: 'if-none-match',\n IfUnmodifiedSince: 'if-unmodified-since',\n LastModified: 'last-modified',\n Range: 'range',\n WWWAuthenticate: 'www-authenticate'\n} as const;\n\n/** HTTP header values used in the parquet server handler */\nexport const HeaderValue = {\n AcceptRanges: 'bytes',\n Allow: 'GET, HEAD',\n CacheControl: 'public, immutable, max-age=31536000',\n ContentType: 'application/octet-stream',\n WWWAuthenticate: 'Bearer realm=\"parquet-server\"'\n} as const;\n"],"names":[],"mappings":";;AAAA;AACO,MAAM,UAAU,GAAG;AACxB,IACA,YAAY,EAAE,eAAe;AAC7B,IAAA,KAAK,EAAE,OAAO;AACd,IAAA,aAAa,EAAE,eAAe;AAC9B,IAAA,YAAY,EAAE,eAAe;AAC7B,IAAA,aAAa,EAAE,gBAAgB;AAC/B,IAAA,YAAY,EAAE,eAAe;AAC7B,IAAA,WAAW,EAAE,cAAc;AAC3B,IACA,IAAI,EAAE,MAAM;AACZ,IAIA,YAAY,EAAE,eAAe;AAC7B,IACA,eAAe,EAAE;;AAGnB;AACO,MAAM,WAAW,GAAG;AACzB,IAAA,YAAY,EAAE,OAAO;AACrB,IAAA,KAAK,EAAE,WAAW;AAClB,IAAA,YAAY,EAAE,qCAAqC;AACnD,IAAA,WAAW,EAAE,0BAA0B;AACvC,IAAA,eAAe,EAAE;;;;;;"}
|
package/dist/utils/headers.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"headers.js","sources":["../../src/utils/headers.ts"],"sourcesContent":["/** HTTP header names used in the parquet server handler */\nexport const HeaderName = {\n Accept: 'accept',\n AcceptRanges: 'accept-ranges',\n Allow: 'allow',\n Authorization: 'authorization',\n CacheControl: 'cache-control',\n ContentLength: 'content-length',\n ContentRange: 'content-range',\n ContentType: 'content-type',\n Date: 'date',\n ETag: 'etag',\n IfMatch: 'if-match',\n IfModifiedSince: 'if-modified-since',\n IfNoneMatch: 'if-none-match',\n IfUnmodifiedSince: 'if-unmodified-since',\n LastModified: 'last-modified',\n Range: 'range',\n WWWAuthenticate: 'www-authenticate'\n} as const;\n\n/** HTTP header values used in the parquet server handler */\nexport const HeaderValue = {\n AcceptRanges: 'bytes',\n Allow: 'GET, HEAD',\n CacheControl: 'public, immutable, max-age=31536000',\n ContentType: 'application/octet-stream',\n WWWAuthenticate: 'Bearer realm=\"parquet-server\"'\n} as const;\n"],"names":[],"mappings":"AAAA;AACO,MAAM,UAAU,GAAG;AACxB,IACA,YAAY,EAAE,eAAe;AAC7B,IAAA,KAAK,EAAE,OAAO;AACd,
|
|
1
|
+
{"version":3,"file":"headers.js","sources":["../../src/utils/headers.ts"],"sourcesContent":["/** HTTP header names used in the parquet server handler */\nexport const HeaderName = {\n Accept: 'accept',\n AcceptRanges: 'accept-ranges',\n Allow: 'allow',\n Authorization: 'authorization',\n CacheControl: 'cache-control',\n ContentLength: 'content-length',\n ContentRange: 'content-range',\n ContentType: 'content-type',\n Date: 'date',\n ETag: 'etag',\n IfMatch: 'if-match',\n IfModifiedSince: 'if-modified-since',\n IfNoneMatch: 'if-none-match',\n IfUnmodifiedSince: 'if-unmodified-since',\n LastModified: 'last-modified',\n Range: 'range',\n WWWAuthenticate: 'www-authenticate'\n} as const;\n\n/** HTTP header values used in the parquet server handler */\nexport const HeaderValue = {\n AcceptRanges: 'bytes',\n Allow: 'GET, HEAD',\n CacheControl: 'public, immutable, max-age=31536000',\n ContentType: 'application/octet-stream',\n WWWAuthenticate: 'Bearer realm=\"parquet-server\"'\n} as const;\n"],"names":[],"mappings":"AAAA;AACO,MAAM,UAAU,GAAG;AACxB,IACA,YAAY,EAAE,eAAe;AAC7B,IAAA,KAAK,EAAE,OAAO;AACd,IAAA,aAAa,EAAE,eAAe;AAC9B,IAAA,YAAY,EAAE,eAAe;AAC7B,IAAA,aAAa,EAAE,gBAAgB;AAC/B,IAAA,YAAY,EAAE,eAAe;AAC7B,IAAA,WAAW,EAAE,cAAc;AAC3B,IACA,IAAI,EAAE,MAAM;AACZ,IAIA,YAAY,EAAE,eAAe;AAC7B,IACA,eAAe,EAAE;;AAGnB;AACO,MAAM,WAAW,GAAG;AACzB,IAAA,YAAY,EAAE,OAAO;AACrB,IAAA,KAAK,EAAE,WAAW;AAClB,IAAA,YAAY,EAAE,qCAAqC;AACnD,IAAA,WAAW,EAAE,0BAA0B;AACvC,IAAA,eAAe,EAAE;;;;;"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@milaboratories/pframes-rs-serv",
|
|
3
3
|
"description": "PFrames - Node.js HTTP(S) Parquet Server",
|
|
4
|
-
"
|
|
4
|
+
"keywords": [
|
|
5
|
+
"http",
|
|
6
|
+
"https",
|
|
7
|
+
"parquet",
|
|
8
|
+
"proxy",
|
|
9
|
+
"server"
|
|
10
|
+
],
|
|
11
|
+
"version": "1.0.67",
|
|
5
12
|
"type": "module",
|
|
6
13
|
"types": "./dist/index.d.ts",
|
|
7
14
|
"main": "./dist/index.js",
|
|
@@ -23,7 +30,7 @@
|
|
|
23
30
|
"dependencies": {
|
|
24
31
|
"@milaboratories/helpers": "1.6.22",
|
|
25
32
|
"@milaboratories/pl-model-common": "1.19.14",
|
|
26
|
-
"@milaboratories/pl-model-middle-layer": "1.8.
|
|
33
|
+
"@milaboratories/pl-model-middle-layer": "1.8.17",
|
|
27
34
|
"commander": "^14.0.0",
|
|
28
35
|
"selfsigned": "^3.0.1"
|
|
29
36
|
},
|
|
@@ -34,6 +41,7 @@
|
|
|
34
41
|
"@types/node": "^20.19.11",
|
|
35
42
|
"@vitest/coverage-istanbul": "^3.2.4",
|
|
36
43
|
"autocannon": "^8.0.0",
|
|
44
|
+
"tslib": "^2.8.1",
|
|
37
45
|
"typescript": "^5.9.2",
|
|
38
46
|
"undici": "^7.14.0",
|
|
39
47
|
"vite": "^7.1.3",
|
package/src/handler.ts
CHANGED
|
@@ -11,12 +11,12 @@ import {
|
|
|
11
11
|
getFilenameFromUrl,
|
|
12
12
|
parseRange,
|
|
13
13
|
isGetOrHead,
|
|
14
|
+
isGet,
|
|
14
15
|
isHead,
|
|
15
16
|
Options,
|
|
16
17
|
StatusCode,
|
|
17
18
|
HeaderName,
|
|
18
|
-
HeaderValue
|
|
19
|
-
isGet
|
|
19
|
+
HeaderValue
|
|
20
20
|
} from './utils';
|
|
21
21
|
|
|
22
22
|
/** Main request handler for parquet files */
|
|
@@ -146,7 +146,7 @@ async function handleRequest(
|
|
|
146
146
|
* - <https://datatracker.ietf.org/doc/html/rfc9110>
|
|
147
147
|
* - <https://datatracker.ietf.org/doc/html/rfc9111>
|
|
148
148
|
*
|
|
149
|
-
* Accepts only paths of the form `/<filename>.parquet`, returns
|
|
149
|
+
* Accepts only paths of the form `/<filename>.parquet`, returns 410 Gone otherwise
|
|
150
150
|
* Assumes that files are immutable (and sets cache headers accordingly)
|
|
151
151
|
*/
|
|
152
152
|
export function createRequestHandler(
|
|
@@ -170,7 +170,7 @@ function authorizeRequest(
|
|
|
170
170
|
response.setHeader(HeaderName.ContentLength, 0);
|
|
171
171
|
// Note: setting Content-Length disables Node.js default Transfer-Encoding: chunked
|
|
172
172
|
|
|
173
|
-
const actualHeader = request.headers[
|
|
173
|
+
const actualHeader = request.headers[HeaderName.Authorization];
|
|
174
174
|
|
|
175
175
|
// Early length check to avoid unnecessary processing
|
|
176
176
|
if (!actualHeader || actualHeader.length !== authHeader.length) {
|
package/src/parquet-server.ts
CHANGED
|
@@ -1,7 +1,24 @@
|
|
|
1
|
+
import { type ChildProcess, spawn } from 'node:child_process';
|
|
2
|
+
import { createInterface, type Interface } from 'node:readline/promises';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
1
5
|
import { Command } from 'commander';
|
|
2
6
|
import { FileSystemStore } from './fs-store';
|
|
3
7
|
import { createRequestHandler } from './handler';
|
|
4
8
|
import { serve } from './serve';
|
|
9
|
+
import {
|
|
10
|
+
parseJson,
|
|
11
|
+
type StringifiedJson,
|
|
12
|
+
stringifyJson
|
|
13
|
+
} from '@milaboratories/pl-model-common';
|
|
14
|
+
import { type PFrameInternal } from '@milaboratories/pl-model-middle-layer';
|
|
15
|
+
|
|
16
|
+
const Options = {
|
|
17
|
+
Http: '--http',
|
|
18
|
+
NoAuth: '--no-auth'
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
type Config = StringifiedJson<PFrameInternal.ParquetServerConfig>;
|
|
5
22
|
|
|
6
23
|
/**
|
|
7
24
|
* Serves parquet files from the given root directory.
|
|
@@ -14,8 +31,8 @@ export async function runParquetServer(): Promise<void> {
|
|
|
14
31
|
.name('parquet-server')
|
|
15
32
|
.description('Serve parquet files from a directory over HTTP(S)')
|
|
16
33
|
.argument('<root-directory>', 'Root directory containing parquet files')
|
|
17
|
-
.option(
|
|
18
|
-
.option(
|
|
34
|
+
.option(Options.Http, 'Use HTTP instead of HTTPS', false)
|
|
35
|
+
.option(Options.NoAuth, 'Disable authentication')
|
|
19
36
|
.action(
|
|
20
37
|
async (rootDir: string, options: { http: boolean; auth: boolean }) => {
|
|
21
38
|
const abortController = new AbortController();
|
|
@@ -42,13 +59,12 @@ export async function runParquetServer(): Promise<void> {
|
|
|
42
59
|
abortController.signal.onabort = () => server.stop();
|
|
43
60
|
abortController.signal.throwIfAborted();
|
|
44
61
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
);
|
|
62
|
+
const serverConfig: Config = stringifyJson({
|
|
63
|
+
url: server.address,
|
|
64
|
+
...(server.authToken && { authToken: server.authToken }),
|
|
65
|
+
...(server.encodedCaCert && { caCert: server.encodedCaCert })
|
|
66
|
+
});
|
|
67
|
+
console.log(serverConfig);
|
|
52
68
|
|
|
53
69
|
await server.stopped;
|
|
54
70
|
}
|
|
@@ -56,3 +72,68 @@ export async function runParquetServer(): Promise<void> {
|
|
|
56
72
|
|
|
57
73
|
await program.parseAsync();
|
|
58
74
|
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Reference implementation of a parquet server runner for tests:
|
|
78
|
+
* - Reads the server configuration from the spawned process stdout
|
|
79
|
+
* - Forwards the server logs to the console
|
|
80
|
+
* - Shuts down the server on dispose
|
|
81
|
+
*/
|
|
82
|
+
export class ParquetServer implements Disposable {
|
|
83
|
+
readonly #process: ChildProcess;
|
|
84
|
+
readonly #config: PFrameInternal.ParquetServerConfig;
|
|
85
|
+
readonly #lineReader: Interface;
|
|
86
|
+
|
|
87
|
+
private constructor(
|
|
88
|
+
process: ChildProcess,
|
|
89
|
+
config: PFrameInternal.ParquetServerConfig,
|
|
90
|
+
lineReader: Interface
|
|
91
|
+
) {
|
|
92
|
+
this.#process = process;
|
|
93
|
+
this.#config = config;
|
|
94
|
+
this.#lineReader = lineReader;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
get config(): PFrameInternal.ParquetServerConfig {
|
|
98
|
+
return this.#config;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
static async serve(
|
|
102
|
+
rootDir: string,
|
|
103
|
+
options?: {
|
|
104
|
+
http?: boolean;
|
|
105
|
+
noAuth?: boolean;
|
|
106
|
+
}
|
|
107
|
+
): Promise<ParquetServer> {
|
|
108
|
+
const nodeFileUrl = import.meta.url;
|
|
109
|
+
const nodeDirname = dirname(fileURLToPath(nodeFileUrl));
|
|
110
|
+
const binPath = join(nodeDirname, '..', 'bin', 'parquet-server.mjs');
|
|
111
|
+
|
|
112
|
+
const serverProcess = spawn(
|
|
113
|
+
'node',
|
|
114
|
+
[
|
|
115
|
+
binPath,
|
|
116
|
+
rootDir,
|
|
117
|
+
...(options?.http ? [Options.Http] : []),
|
|
118
|
+
...(options?.noAuth ? [Options.NoAuth] : [])
|
|
119
|
+
],
|
|
120
|
+
{
|
|
121
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const lineReader = createInterface({ input: serverProcess.stdout! });
|
|
126
|
+
|
|
127
|
+
const firstLine = await lineReader[Symbol.asyncIterator]().next();
|
|
128
|
+
const serverConfig = parseJson(firstLine.value as Config);
|
|
129
|
+
|
|
130
|
+
lineReader.on('line', console.log);
|
|
131
|
+
|
|
132
|
+
return new ParquetServer(serverProcess, serverConfig, lineReader);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
[Symbol.dispose](): void {
|
|
136
|
+
this.#lineReader.close();
|
|
137
|
+
this.#process.kill();
|
|
138
|
+
}
|
|
139
|
+
}
|