Skip to content

Commit

Permalink
feat(file): add a separate file plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Feb 15, 2024
1 parent d89a750 commit f0a3dd1
Show file tree
Hide file tree
Showing 13 changed files with 219 additions and 109 deletions.
1 change: 1 addition & 0 deletions packages/core/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Fetch-based axios-style HTTP client.

> "und" comes from undici, an HTTP/1.1 client officially supported by Node.js team.
>
> "ios" comes from axios, a popular HTTP client for browser and Node.js.
## Features
Expand Down
34 changes: 0 additions & 34 deletions packages/core/src/adapter/browser.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,2 @@
// Modified from https://github.com/sindresorhus/ip-regex/blob/3e220cae3eb66ecfdf4f7678bea7306ceaa41c76/index.js

import { LookupAddress } from 'dns'
import { HTTP } from '../index.js'

const { WebSocket } = globalThis
export { WebSocket }

const v4 = /^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}$/

const v6seg = '[a-fA-F\\d]{1,4}'

/* eslint-disable no-multi-spaces */
const v6core = [
`(?:${v6seg}:){7}(?:${v6seg}|:)`, // 1:2:3:4:5:6:7:: 1:2:3:4:5:6:7:8
`(?:${v6seg}:){6}(?:${v4}|:${v6seg}|:)`, // 1:2:3:4:5:6:: 1:2:3:4:5:6::8 1:2:3:4:5:6::8 1:2:3:4:5:6::1.2.3.4
`(?:${v6seg}:){5}(?::${v4}|(?::${v6seg}){1,2}|:)`, // 1:2:3:4:5:: 1:2:3:4:5::7:8 1:2:3:4:5::8 1:2:3:4:5::7:1.2.3.4
`(?:${v6seg}:){4}(?:(?::${v6seg}){0,1}:${v4}|(?::${v6seg}){1,3}|:)`, // 1:2:3:4:: 1:2:3:4::6:7:8 1:2:3:4::8 1:2:3:4::6:7:1.2.3.4
`(?:${v6seg}:){3}(?:(?::${v6seg}){0,2}:${v4}|(?::${v6seg}){1,4}|:)`, // 1:2:3:: 1:2:3::5:6:7:8 1:2:3::8 1:2:3::5:6:7:1.2.3.4
`(?:${v6seg}:){2}(?:(?::${v6seg}){0,3}:${v4}|(?::${v6seg}){1,5}|:)`, // 1:2:: 1:2::4:5:6:7:8 1:2::8 1:2::4:5:6:7:1.2.3.4
`(?:${v6seg}:){1}(?:(?::${v6seg}){0,4}:${v4}|(?::${v6seg}){1,6}|:)`, // 1:: 1::3:4:5:6:7:8 1::8 1::3:4:5:6:7:1.2.3.4
`(?::(?:(?::${v6seg}){0,5}:${v4}|(?::${v6seg}){1,7}|:))`, // ::2:3:4:5:6:7:8 ::2:3:4:5:6:7:8 ::8 ::1.2.3.4
]
/* eslint-enable no-multi-spaces */

const v6 = new RegExp(`^(?:${v6core.join('|')})(?:%[0-9a-zA-Z]{1,})?$`)

export async function lookup(address: string): Promise<LookupAddress> {
if (v4.test(address)) return { address, family: 4 }
if (v6.test(address)) return { address, family: 6 }
throw new Error('Invalid IP address')
}

export async function loadFile(url: string): Promise<HTTP.FileResponse | undefined> {
return undefined
}
6 changes: 0 additions & 6 deletions packages/core/src/adapter/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
import { LookupAddress } from 'dns'
import { HTTP } from '../index.ts'

export function loadFile(url: string): Promise<HTTP.FileResponse | undefined>
export function lookup(address: string): Promise<LookupAddress>

export namespace WebSocket {
/** The connection is not yet open. */
export const CONNECTING = 0
Expand Down
15 changes: 0 additions & 15 deletions packages/core/src/adapter/node.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1 @@
import { fileURLToPath } from 'node:url'
import { basename } from 'node:path'
import { fromBuffer } from 'file-type'
import { HTTP } from '../index.js'
import { readFile } from 'node:fs/promises'

export { WebSocket } from 'ws'
export { lookup } from 'node:dns/promises'

export async function loadFile(url: string): Promise<HTTP.FileResponse | undefined> {
if (url.startsWith('file://')) {
const data = await readFile(fileURLToPath(url))
const result = await fromBuffer(data)
return { mime: result?.mime, name: basename(url), data }
}
}
71 changes: 17 additions & 54 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Context, Service } from 'cordis'
import { base64ToArrayBuffer, defineProperty, Dict, trimSlash } from 'cosmokit'
import { defineProperty, Dict, trimSlash } from 'cosmokit'
import { ClientOptions } from 'ws'
import { loadFile, lookup, WebSocket } from 'undios/adapter'
import { isLocalAddress } from './utils.ts'
import { WebSocket } from 'undios/adapter'

declare module 'cordis' {
interface Context {
Expand Down Expand Up @@ -53,25 +52,25 @@ export namespace HTTP {
(url: string, config?: HTTP.RequestConfig & { responseType: 'arraybuffer' }): Promise<ArrayBuffer>
(url: string, config?: HTTP.RequestConfig & { responseType: 'stream' }): Promise<ReadableStream<Uint8Array>>
(url: string, config?: HTTP.RequestConfig & { responseType: 'text' }): Promise<string>
<T>(url: string, config?: HTTP.RequestConfig): Promise<T>
<T = any>(url: string, config?: HTTP.RequestConfig): Promise<T>
}

export interface Request2 {
(url: string, data?: any, config?: HTTP.RequestConfig & { responseType: 'arraybuffer' }): Promise<ArrayBuffer>
(url: string, data?: any, config?: HTTP.RequestConfig & { responseType: 'stream' }): Promise<ReadableStream<Uint8Array>>
(url: string, data?: any, config?: HTTP.RequestConfig & { responseType: 'text' }): Promise<string>
<T>(url: string, data?: any, config?: HTTP.RequestConfig): Promise<T>
<T = any>(url: string, data?: any, config?: HTTP.RequestConfig): Promise<T>
}

export interface Config {
baseURL?: string
/** @deprecated use `baseURL` instead */
endpoint?: string
headers?: Dict
timeout?: number
}

export interface RequestConfig extends Config {
baseURL?: string
/** @deprecated use `baseURL` instead */
endpoint?: string
method?: Method
params?: Dict
data?: any
Expand All @@ -87,22 +86,12 @@ export namespace HTTP {
headers: Headers
}

export interface FileConfig {
timeout?: number | string
}

export interface FileResponse {
mime?: string
name?: string
data: ArrayBuffer
}

export type Error = HTTPError
}

export interface HTTP {
<T>(url: string | URL, config?: HTTP.RequestConfig): Promise<HTTP.Response<T>>
<T>(method: HTTP.Method, url: string | URL, config?: HTTP.RequestConfig): Promise<HTTP.Response<T>>
<T = any>(url: string | URL, config?: HTTP.RequestConfig): Promise<HTTP.Response<T>>
<T = any>(method: HTTP.Method, url: string | URL, config?: HTTP.RequestConfig): Promise<HTTP.Response<T>>
config: HTTP.Config
get: HTTP.Request1
delete: HTTP.Request1
Expand Down Expand Up @@ -258,10 +247,16 @@ export class HTTP extends Service {
}

/** @deprecated use `ctx.http()` instead */
axios<T>(url: string, config?: HTTP.Config): Promise<HTTP.Response<T>> {
axios<T = any>(config: { url: string } & HTTP.RequestConfig): Promise<HTTP.Response<T>>
axios<T = any>(url: string, config?: HTTP.RequestConfig): Promise<HTTP.Response<T>>
axios(...args: any[]) {
const caller = this[Context.current]
caller.emit('internal/warning', 'ctx.http.axios() is deprecated, use ctx.http() instead')
return this(url, config)
if (typeof args[0] === 'string') {
return this(args[0], args[1])
} else {
return this(args[0].url, args[0])
}
}

async ws(this: HTTP, url: string | URL, init?: HTTP.Config) {
Expand All @@ -285,38 +280,6 @@ export class HTTP extends Service {
})
return socket
}

async file(url: string, options: HTTP.FileConfig = {}): Promise<HTTP.FileResponse> {
const result = await loadFile(url)
if (result) return result
const capture = /^data:([\w/-]+);base64,(.*)$/.exec(url)
if (capture) {
const [, mime, base64] = capture
return { mime, data: base64ToArrayBuffer(base64) }
}
const { headers, data, url: responseUrl } = await this<ArrayBuffer>(url, {
method: 'GET',
responseType: 'arraybuffer',
timeout: +options.timeout! || undefined,
})
const mime = headers.get('content-type') ?? undefined
const [, name] = responseUrl.match(/.+\/([^/?]*)(?=\?)?/)!
return { mime, name, data }
}

async isLocal(url: string) {
let { hostname, protocol } = new URL(url)
if (protocol !== 'http:' && protocol !== 'https:') return true
if (/^\[.+\]$/.test(hostname)) {
hostname = hostname.slice(1, -1)
}
try {
const address = await lookup(hostname)
return isLocalAddress(address)
} catch {
return false
}
}
}

export default HTTP
65 changes: 65 additions & 0 deletions packages/file/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{
"name": "undios-file",
"description": "File support for undios",
"version": "0.1.0",
"type": "module",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"exports": {
".": {
"require": "./lib/index.cjs",
"import": "./lib/index.js",
"types": "./lib/index.d.ts"
},
"./adapter": {
"node": {
"require": "./lib/adapter/node.cjs",
"import": "./lib/adapter/node.js"
},
"default": {
"import": "./lib/adapter/browser.js"
},
"types": "./lib/adapter/index.d.ts"
},
"./package.json": "./package.json"
},
"files": [
"lib",
"src"
],
"author": "Shigma <shigma10826@gmail.com>",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/cordiverse/undios.git",
"directory": "packages/file"
},
"bugs": {
"url": "https://github.com/cordiverse/undios/issues"
},
"homepage": "https://github.com/cordiverse/undios",
"keywords": [
"http",
"client",
"undici",
"fetch",
"axios",
"file",
"mime",
"request",
"cordis",
"plugin"
],
"devDependencies": {
"cordis": "^3.10.3"
},
"peerDependencies": {
"cordis": "^3.10.3",
"undios": "^0.1.3"
},
"dependencies": {
"cosmokit": "^1.5.2",
"file-type": "^16.5.4",
"mime-db": "^1.52.0"
}
}
3 changes: 3 additions & 0 deletions packages/file/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# undios-file

Support `http.file()` in undios.
33 changes: 33 additions & 0 deletions packages/file/src/adapter/browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Modified from https://github.com/sindresorhus/ip-regex/blob/3e220cae3eb66ecfdf4f7678bea7306ceaa41c76/index.js

import { LookupAddress } from 'dns'
import { FileResponse } from '../index.js'

const v4 = /^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}$/

const v6seg = '[a-fA-F\\d]{1,4}'

/* eslint-disable no-multi-spaces */
const v6core = [
`(?:${v6seg}:){7}(?:${v6seg}|:)`, // 1:2:3:4:5:6:7:: 1:2:3:4:5:6:7:8
`(?:${v6seg}:){6}(?:${v4}|:${v6seg}|:)`, // 1:2:3:4:5:6:: 1:2:3:4:5:6::8 1:2:3:4:5:6::8 1:2:3:4:5:6::1.2.3.4
`(?:${v6seg}:){5}(?::${v4}|(?::${v6seg}){1,2}|:)`, // 1:2:3:4:5:: 1:2:3:4:5::7:8 1:2:3:4:5::8 1:2:3:4:5::7:1.2.3.4
`(?:${v6seg}:){4}(?:(?::${v6seg}){0,1}:${v4}|(?::${v6seg}){1,3}|:)`, // 1:2:3:4:: 1:2:3:4::6:7:8 1:2:3:4::8 1:2:3:4::6:7:1.2.3.4
`(?:${v6seg}:){3}(?:(?::${v6seg}){0,2}:${v4}|(?::${v6seg}){1,4}|:)`, // 1:2:3:: 1:2:3::5:6:7:8 1:2:3::8 1:2:3::5:6:7:1.2.3.4
`(?:${v6seg}:){2}(?:(?::${v6seg}){0,3}:${v4}|(?::${v6seg}){1,5}|:)`, // 1:2:: 1:2::4:5:6:7:8 1:2::8 1:2::4:5:6:7:1.2.3.4
`(?:${v6seg}:){1}(?:(?::${v6seg}){0,4}:${v4}|(?::${v6seg}){1,6}|:)`, // 1:: 1::3:4:5:6:7:8 1::8 1::3:4:5:6:7:1.2.3.4
`(?::(?:(?::${v6seg}){0,5}:${v4}|(?::${v6seg}){1,7}|:))`, // ::2:3:4:5:6:7:8 ::2:3:4:5:6:7:8 ::8 ::1.2.3.4
]
/* eslint-enable no-multi-spaces */

const v6 = new RegExp(`^(?:${v6core.join('|')})(?:%[0-9a-zA-Z]{1,})?$`)

export async function lookup(address: string): Promise<LookupAddress> {
if (v4.test(address)) return { address, family: 4 }
if (v6.test(address)) return { address, family: 6 }
throw new Error('Invalid IP address')
}

export async function loadFile(url: string): Promise<FileResponse | undefined> {
return undefined
}
5 changes: 5 additions & 0 deletions packages/file/src/adapter/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { LookupAddress } from 'dns'
import { HTTP } from '../index.ts'

export function loadFile(url: string): Promise<HTTP.FileResponse | undefined>
export function lookup(address: string): Promise<LookupAddress>
15 changes: 15 additions & 0 deletions packages/file/src/adapter/node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { fileURLToPath } from 'node:url'
import { basename } from 'node:path'
import { fromBuffer } from 'file-type'
import { FileResponse } from '../index.js'
import { readFile } from 'node:fs/promises'

export { lookup } from 'node:dns/promises'

export async function loadFile(url: string): Promise<FileResponse | undefined> {
if (url.startsWith('file://')) {
const data = await readFile(fileURLToPath(url))
const result = await fromBuffer(data)
return { mime: result?.mime, name: basename(url), data }
}
}
70 changes: 70 additions & 0 deletions packages/file/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import HTTP, {} from 'undios'
import { loadFile, lookup } from 'undios-file/adapter'
import { Context, z } from 'cordis'
import { base64ToArrayBuffer } from 'cosmokit'
import { isLocalAddress } from './utils.ts'

declare module 'undios' {
interface HTTP {
file(url: string, options?: FileConfig): Promise<FileResponse>
isLocal(url: string): Promise<boolean>
}
}

export interface FileConfig {
timeout?: number | string
}

export interface FileResponse {
mime?: string
name?: string
data: ArrayBuffer
}

export const name = 'undios-file'

export interface Config {}

export const Config: z<Config> = z.object({})

export function apply(ctx: Context, config: Config) {
ctx.provide('http.file')
ctx.provide('http.local')

ctx['http.file'] = async function file(this: HTTP, url: string, options: FileConfig = {}): Promise<FileResponse> {
const result = await loadFile(url)
if (result) return result
const capture = /^data:([\w/-]+);base64,(.*)$/.exec(url)
if (capture) {
const [, mime, base64] = capture
return { mime, data: base64ToArrayBuffer(base64) }
}
const { headers, data, url: responseUrl } = await this<ArrayBuffer>(url, {
method: 'GET',
responseType: 'arraybuffer',
timeout: +options.timeout! || undefined,
})
const mime = headers.get('content-type') ?? undefined
const [, name] = responseUrl.match(/.+\/([^/?]*)(?=\?)?/)!
return { mime, name, data }
}

ctx['http.local'] = async function isLocal(url: string) {
let { hostname, protocol } = new URL(url)
if (protocol !== 'http:' && protocol !== 'https:') return true
if (/^\[.+\]$/.test(hostname)) {
hostname = hostname.slice(1, -1)
}
try {
const address = await lookup(hostname)
return isLocalAddress(address)
} catch {
return false
}
}

ctx.on('dispose', () => {
ctx['http.file'] = undefined
ctx['http.local'] = undefined
})
}
File renamed without changes.
Loading

0 comments on commit f0a3dd1

Please sign in to comment.