From 860f36143932d1fbc2314e1fc4397294bc056ab0 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Mon, 9 Dec 2024 10:02:10 -0600 Subject: [PATCH] refactor: move set-content-type function (#162) --- packages/verified-fetch/src/index.ts | 15 +------- packages/verified-fetch/src/types.ts | 14 +++++++ .../src/utils/set-content-type.ts | 38 +++++++++++++++++++ .../verified-fetch/src/utils/type-guards.ts | 3 ++ .../verified-fetch/src/utils/walk-path.ts | 2 +- packages/verified-fetch/src/verified-fetch.ts | 38 ++----------------- 6 files changed, 62 insertions(+), 48 deletions(-) create mode 100644 packages/verified-fetch/src/utils/set-content-type.ts create mode 100644 packages/verified-fetch/src/utils/type-guards.ts diff --git a/packages/verified-fetch/src/index.ts b/packages/verified-fetch/src/index.ts index 9b554eaa..1d4dd2c7 100644 --- a/packages/verified-fetch/src/index.ts +++ b/packages/verified-fetch/src/index.ts @@ -628,6 +628,7 @@ import { type Libp2p, type ServiceMap } from '@libp2p/interface' import { dns } from '@multiformats/dns' import { createHelia, type HeliaInit } from 'helia' import { createLibp2p, type Libp2pOptions } from 'libp2p' +import { type ContentTypeParser } from './types.js' import { getLibp2pConfig } from './utils/libp2p-defaults.js' import { VerifiedFetch as VerifiedFetchClass } from './verified-fetch.js' import type { GetBlockProgressEvents, Helia, Routing } from '@helia/interface' @@ -751,19 +752,7 @@ export interface CreateVerifiedFetchOptions { sessionTTLms?: number } -/** - * A ContentTypeParser attempts to return the mime type of a given file. It - * receives the first chunk of the file data and the file name, if it is - * available. The function can be sync or async and if it returns/resolves to - * `undefined`, `application/octet-stream` will be used. - */ -export interface ContentTypeParser { - /** - * Attempt to determine a mime type, either via of the passed bytes or the - * filename if it is available. - */ - (bytes: Uint8Array, fileName?: string): Promise | string | undefined -} +export type { ContentTypeParser } from './types.js' export type BubbledProgressEvents = // unixfs-exporter diff --git a/packages/verified-fetch/src/types.ts b/packages/verified-fetch/src/types.ts index e80350c1..ce753674 100644 --- a/packages/verified-fetch/src/types.ts +++ b/packages/verified-fetch/src/types.ts @@ -30,3 +30,17 @@ export interface FetchHandlerFunctionArg { */ resource: string } + +/** + * A ContentTypeParser attempts to return the mime type of a given file. It + * receives the first chunk of the file data and the file name, if it is + * available. The function can be sync or async and if it returns/resolves to + * `undefined`, `application/octet-stream` will be used. + */ +export interface ContentTypeParser { + /** + * Attempt to determine a mime type, either via of the passed bytes or the + * filename if it is available. + */ + (bytes: Uint8Array, fileName?: string): Promise | string | undefined +} diff --git a/packages/verified-fetch/src/utils/set-content-type.ts b/packages/verified-fetch/src/utils/set-content-type.ts new file mode 100644 index 00000000..d3b9af4d --- /dev/null +++ b/packages/verified-fetch/src/utils/set-content-type.ts @@ -0,0 +1,38 @@ +import { type Logger } from '@libp2p/interface' +import { type ContentTypeParser } from '../types.js' +import { isPromise } from './type-guards.js' + +export interface SetContentTypeOptions { + bytes: Uint8Array + path: string + response: Response + defaultContentType?: string + contentTypeParser: ContentTypeParser | undefined + log: Logger +} + +export async function setContentType ({ bytes, path, response, contentTypeParser, log, defaultContentType = 'application/octet-stream' }: SetContentTypeOptions): Promise { + let contentType: string | undefined + + if (contentTypeParser != null) { + try { + let fileName = path.split('/').pop()?.trim() + fileName = fileName === '' ? undefined : fileName + const parsed = contentTypeParser(bytes, fileName) + + if (isPromise(parsed)) { + const result = await parsed + + if (result != null) { + contentType = result + } + } else if (parsed != null) { + contentType = parsed + } + } catch (err) { + log.error('error parsing content type', err) + } + } + log.trace('setting content type to "%s"', contentType ?? defaultContentType) + response.headers.set('content-type', contentType ?? defaultContentType) +} diff --git a/packages/verified-fetch/src/utils/type-guards.ts b/packages/verified-fetch/src/utils/type-guards.ts new file mode 100644 index 00000000..288a1b84 --- /dev/null +++ b/packages/verified-fetch/src/utils/type-guards.ts @@ -0,0 +1,3 @@ +export function isPromise (p?: any): p is Promise { + return p?.then != null +} diff --git a/packages/verified-fetch/src/utils/walk-path.ts b/packages/verified-fetch/src/utils/walk-path.ts index 44922d07..c76d7698 100644 --- a/packages/verified-fetch/src/utils/walk-path.ts +++ b/packages/verified-fetch/src/utils/walk-path.ts @@ -19,7 +19,7 @@ export interface PathWalkerFn { (blockstore: ReadableStorage, path: string, options?: PathWalkerOptions): Promise } -export async function walkPath (blockstore: ReadableStorage, path: string, options?: PathWalkerOptions): Promise { +async function walkPath (blockstore: ReadableStorage, path: string, options?: PathWalkerOptions): Promise { const ipfsRoots: CID[] = [] let terminalElement: UnixFSEntry | undefined diff --git a/packages/verified-fetch/src/verified-fetch.ts b/packages/verified-fetch/src/verified-fetch.ts index 81311580..a721f9aa 100644 --- a/packages/verified-fetch/src/verified-fetch.ts +++ b/packages/verified-fetch/src/verified-fetch.ts @@ -6,7 +6,7 @@ import { code as dagPbCode } from '@ipld/dag-pb' import { type AbortOptions, type Logger, type PeerId } from '@libp2p/interface' import { Record as DHTRecord } from '@libp2p/kad-dht' import { Key } from 'interface-datastore' -import { exporter } from 'ipfs-unixfs-exporter' +import { exporter, type ObjectNode } from 'ipfs-unixfs-exporter' import toBrowserReadableStream from 'it-to-browser-readablestream' import { LRUCache } from 'lru-cache' import { type CID } from 'multiformats/cid' @@ -32,12 +32,12 @@ import { resourceToSessionCacheKey } from './utils/resource-to-cache-key.js' import { setCacheControlHeader, setIpfsRoots } from './utils/response-headers.js' import { badRequestResponse, movedPermanentlyResponse, notAcceptableResponse, notSupportedResponse, okResponse, badRangeResponse, okRangeResponse, badGatewayResponse, notFoundResponse } from './utils/responses.js' import { selectOutputType } from './utils/select-output-type.js' +import { setContentType } from './utils/set-content-type.js' import { handlePathWalking, isObjectNode } from './utils/walk-path.js' import type { CIDDetail, ContentTypeParser, CreateVerifiedFetchOptions, Resource, ResourceDetail, VerifiedFetchInit as VerifiedFetchOptions } from './index.js' import type { FetchHandlerFunctionArg, RequestFormatShorthand } from './types.js' import type { Helia, SessionBlockstore } from '@helia/interface' import type { Blockstore } from 'interface-blockstore' -import type { ObjectNode } from 'ipfs-unixfs-exporter' const SESSION_CACHE_MAX_SIZE = 100 const SESSION_CACHE_TTL_MS = 60 * 1000 @@ -398,7 +398,7 @@ export class VerifiedFetch { redirected }) - await this.setContentType(firstChunk, path, response) + await setContentType({ bytes: firstChunk, path, response, contentTypeParser: this.contentTypeParser, log: this.log }) setIpfsRoots(response, ipfsRoots) return response @@ -434,37 +434,11 @@ export class VerifiedFetch { // if the user has specified an `Accept` header that corresponds to a raw // type, honour that header, so for example they don't request // `application/vnd.ipld.raw` but get `application/octet-stream` - await this.setContentType(result, path, response, getOverridenRawContentType({ headers: options?.headers, accept })) + await setContentType({ bytes: result, path, response, defaultContentType: getOverridenRawContentType({ headers: options?.headers, accept }), contentTypeParser: this.contentTypeParser, log: this.log }) return response } - private async setContentType (bytes: Uint8Array, path: string, response: Response, defaultContentType = 'application/octet-stream'): Promise { - let contentType: string | undefined - - if (this.contentTypeParser != null) { - try { - let fileName = path.split('/').pop()?.trim() - fileName = fileName === '' ? undefined : fileName - const parsed = this.contentTypeParser(bytes, fileName) - - if (isPromise(parsed)) { - const result = await parsed - - if (result != null) { - contentType = result - } - } else if (parsed != null) { - contentType = parsed - } - } catch (err) { - this.log.error('error parsing content type', err) - } - } - this.log.trace('setting content type to "%s"', contentType ?? defaultContentType) - response.headers.set('content-type', contentType ?? defaultContentType) - } - /** * If the user has not specified an Accept header or format query string arg, * use the CID codec to choose an appropriate handler for the block data. @@ -614,7 +588,3 @@ export class VerifiedFetch { await this.helia.stop() } } - -function isPromise (p?: any): p is Promise { - return p?.then != null -}