Skip to content

Commit

Permalink
feat(debug): Better Debug (#3)
Browse files Browse the repository at this point in the history
* feat(debug): 📈 Better logs

Signed-off-by: Nishant Arora <1895906+whizzzkid@users.noreply.github.com>

* feat(debug): 📦️ Add debug package

Signed-off-by: Nishant Arora <1895906+whizzzkid@users.noreply.github.com>

* fix(debug): other improvements.

Signed-off-by: Nishant Arora <1895906+whizzzkid@users.noreply.github.com>

* fix(debug): ⚡️ better builds

Signed-off-by: Nishant Arora <1895906+whizzzkid@users.noreply.github.com>

* docs(debug): ✏️ Adding relevant readme.

Signed-off-by: Nishant Arora <1895906+whizzzkid@users.noreply.github.com>

* fix(docker): ⬇️ Don't use alpine.

Signed-off-by: Nishant Arora <1895906+whizzzkid@users.noreply.github.com>

---------

Signed-off-by: Nishant Arora <1895906+whizzzkid@users.noreply.github.com>
  • Loading branch information
whizzzkid authored Oct 10, 2023
1 parent 86e865e commit 6bbed9a
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 41 deletions.
3 changes: 1 addition & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ RUN apt-get install -y build-essential cmake git libssl-dev
WORKDIR /app

COPY . .
RUN npm install
RUN npm ci --quiet
RUN npm run build
EXPOSE 8080
CMD [ "npm", "start" ]
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,15 @@ Docker images for Helia.

This container image hosts helia in a node container. It implements [HTTP IPFS-gateway API](https://docs.ipfs.tech/concepts/ipfs-gateway/#gateway-types) and responds to the incoming requests using helia to fetch the content from IPFS.

## Building
## Run Using Docker Compose

```sh
$ docker-compose up
```

## Run Using Docker

### Build
```sh
$ docker build . --tag helia
```
Expand All @@ -18,12 +25,20 @@ Pass the explicit platform when building on a Mac.
$ docker build . --tag helia --platform linux/arm64
```

## Running
### Running

```sh
$ docker run -it -p 8080:8080 helia
$ docker run -it -p 8080:8080 -e DEBUG="helia-server" helia
```

## Supported Environment Variables

| Variable | Description | Default |
| --- | --- | --- |
| `DEBUG` | Debug level | `''`|
| `PORT` | Port to listen on | `8080` |
| `HOST` | Host to listen on | `0.0.0.0` |

## Author

- [whizzzkid](https://github.com/whizzzkid)
10 changes: 10 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version: "3.8"

services:
server:
build: .
restart: always
ports:
- "8080:8080"
environment:
- DEBUG="helia-server*"
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@types/mime-types": "2.x",
"@types/node": "20.x",
"aegir": "40.x",
"debug": "4.3.4",
"typescript": "5.x"
},
"dependencies": {
Expand Down
59 changes: 46 additions & 13 deletions src/heliaFetch.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { type UnixFS, unixfs } from '@helia/unixfs'
import { unixfs, type UnixFS } from '@helia/unixfs'
import { MemoryBlockstore } from 'blockstore-core'
import { MemoryDatastore } from 'datastore-core'
import { type Helia, createHelia } from 'helia'
import debug from 'debug'
import { createHelia, type Helia } from 'helia'
import { LRUCache } from 'lru-cache'
import { CID } from 'multiformats/cid'
import pTryEach from 'p-try-each'
Expand All @@ -18,8 +19,9 @@ const DELEGATED_ROUTING_API = 'https://node3.delegate.ipfs.io/api/v0/name/resolv
* Fetches files from IPFS or IPNS
*/
export class HeliaFetch {
private readonly delegatedRoutingApi: string
private fs!: UnixFS
private readonly delegatedRoutingApi: string
private readonly log: debug.Debugger
private readonly rootFilePatterns: string[]
public node!: Helia
public ready: Promise<void>
Expand All @@ -28,17 +30,31 @@ export class HeliaFetch {
ttl: 1000 * 60 * 60 * 24
})

constructor (
node?: Helia,
rootFilePatterns: string[] = ROOT_FILE_PATTERNS,
delegatedRoutingApi: string = DELEGATED_ROUTING_API
) {
constructor ({
node,
rootFilePatterns = ROOT_FILE_PATTERNS,
delegatedRoutingApi = DELEGATED_ROUTING_API,
logger
}: {
node?: Helia
rootFilePatterns?: string[]
delegatedRoutingApi?: string
logger?: debug.Debugger
} = {}) {
// setup a logger
if (logger !== undefined) {
this.log = logger.extend('helia-fetch')
} else {
this.log = debug('helia-fetch')
}
// a node can be provided otherwise a new one will be created.
if (node !== undefined) {
this.node = node
}
this.rootFilePatterns = rootFilePatterns
this.delegatedRoutingApi = delegatedRoutingApi
this.ready = this.init()
this.log('Initialized')
}

/**
Expand All @@ -50,6 +66,7 @@ export class HeliaFetch {
datastore: new MemoryDatastore()
})
this.fs = unixfs(this.node)
this.log('Helia Setup Complete!')
}

/**
Expand All @@ -59,11 +76,14 @@ export class HeliaFetch {
if (path === undefined) {
throw new Error('Path is empty')
}
this.log(`Parsing path: ${path}`)
const regex = /^\/(?<namespace>ip[fn]s)\/(?<address>[^/$]+)(?<relativePath>[^$]*)/
const result = path.match(regex)
if (result == null) {
throw new Error('Path is not valid, provide path as /ipfs/<cid> or /ipns/<path>')
if (result == null || result?.groups == null) {
this.log(`Error parsing path: ${path}:`, result)
throw new Error(`Path: ${path} is not valid, provide path as /ipfs/<cid> or /ipns/<path>`)
}
this.log('Parsed path:', result?.groups)
return result.groups as { namespace: string, address: string, relativePath: string }
}

Expand All @@ -73,7 +93,9 @@ export class HeliaFetch {
public async fetch (path: string): Promise<AsyncIterable<Uint8Array>> {
try {
await this.ready
this.log('Fetching:', path)
const { namespace, address, relativePath } = this.parsePath(path)
this.log('Processing Fetch:', { namespace, address, relativePath })
switch (namespace) {
case 'ipfs':
return await this.fetchIpfs(CID.parse(address), { path: relativePath })
Expand All @@ -84,7 +106,7 @@ export class HeliaFetch {
}
} catch (error) {
// eslint-disable-next-line no-console
console.error(error)
this.log(`Error fetching: ${path}`, error)
throw error
}
}
Expand All @@ -93,7 +115,8 @@ export class HeliaFetch {
* Fetch from IPFS
*/
private async fetchIpfs (...[cid, options]: Parameters<UnixFS['cat']>): Promise<AsyncIterable<Uint8Array>> {
const { type } = await this.fs.stat(cid)
const { type } = await this.fs.stat(cid, options)
this.log('Fetching from IPFS:', { cid, type, options })
switch (type) {
case 'directory':
return this.getDirectoryResponse(cid, options)
Expand All @@ -110,30 +133,40 @@ export class HeliaFetch {
*/
private async fetchIpns (address: string, options?: Parameters<UnixFS['cat']>[1]): Promise<AsyncIterable<Uint8Array>> {
if (!this.ipnsResolutionCache.has(address)) {
this.log('Fetching from Delegate Routing:', address)
const { Path } = await (await fetch(this.delegatedRoutingApi + address)).json()
this.ipnsResolutionCache.set(address, Path)
this.ipnsResolutionCache.set(address, Path ?? 'not-found')
}
if (this.ipnsResolutionCache.get(address) === 'not-found') {
this.log('No Path found:', address)
throw new Error(`Could not resolve IPNS address: ${address}`)
}
const finalPath = `${this.ipnsResolutionCache.get(address)}${options?.path ?? ''}`
this.log('Final IPFS path:', finalPath)
return this.fetch(finalPath)
}

/**
* Get the response for a file.
*/
private async getFileResponse (...[cid, options]: Parameters<UnixFS['cat']>): Promise<AsyncIterable<Uint8Array>> {
this.log('Getting file response:', { cid, options })
return this.fs.cat(cid, options)
}

/**
* Gets the response for a directory.
*/
private async getDirectoryResponse (...[cid, options]: Parameters<UnixFS['cat']>): Promise<AsyncIterable<Uint8Array>> {
this.log('Getting directory response:', { cid, options })
const rootFile = await pTryEach(this.rootFilePatterns.map(file => {
const directoryPath = options?.path ?? ''
return async (): Promise<{ name: string, cid: CID }> => {
try {
const path = `${directoryPath}/${file}`.replace(/\/\//g, '/')
this.log('Trying to get root file:', { file, directoryPath })
const stats = await this.fs.stat(cid, { path })
this.log('Got root file:', { file, directoryPath, stats })
return {
name: file,
cid: stats.cid
Expand Down
48 changes: 32 additions & 16 deletions src/heliaServer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable no-console */
import { type Request, type Response } from 'express'
import { DEFAULT_MIME_TYPE, parseContentType } from './contentType.js'
import { HeliaFetch } from './heliaFetch.js'
import type debug from 'debug'

const HELIA_RELEASE_INFO_API = (version: string): string => `https://github.com/gitapi/repos/ipfs/helia/git/ref/tags/helia-v${version}`

Expand All @@ -16,22 +16,25 @@ interface IRouteHandler {
response: Response
}

class HeliaServer {
export class HeliaServer {
private heliaFetch!: HeliaFetch
private heliaVersionInfo!: { Version: string, Commit: string }
private readonly log: debug.Debugger
public isReady: Promise<void>
public routes: IRouteEntry[]

constructor () {
constructor (logger: debug.Debugger) {
this.log = logger.extend('express')
this.isReady = this.init()
this.routes = []
this.log('Initialized')
}

/**
* Initialize the HeliaServer instance
*/
async init (): Promise<void> {
this.heliaFetch = new HeliaFetch()
this.heliaFetch = new HeliaFetch({ logger: this.log })
await this.heliaFetch.ready
// eslint-disable-next-line no-console
console.log('Helia Started!')
Expand Down Expand Up @@ -64,15 +67,22 @@ class HeliaServer {
* Handles redirecting to the relative path
*/
private async redirectRelative ({ request, response }: IRouteHandler): Promise<void> {
this.log('Redirecting to relative path:', request.path, request.headers)
const referrerPath = new URL(request.headers.referer ?? '').pathname
if (referrerPath !== undefined) {
let relativeRedirectPath = `${referrerPath}${request.path}`
const { namespace, address } = this.heliaFetch.parsePath(referrerPath)
if (namespace === 'ipns') {
relativeRedirectPath = `/${namespace}/${address}${request.path}`
try {
let relativeRedirectPath = `${referrerPath}${request.path}`
const { namespace, address } = this.heliaFetch.parsePath(referrerPath)
if (namespace === 'ipns') {
relativeRedirectPath = `/${namespace}/${address}${request.path}`
}
// absolute redirect
this.log('Redirecting to relative path:', referrerPath)
response.redirect(301, relativeRedirectPath)
} catch (error) {
this.log('Error redirecting to relative path:', error)
response.status(500).end()
}
// absolute redirect
response.redirect(301, relativeRedirectPath)
}
}

Expand All @@ -87,6 +97,7 @@ class HeliaServer {
}): Promise<void> {
await this.isReady
let type: string | undefined
this.log('Fetching from Helia:', routePath)
for await (const chunk of await this.heliaFetch.fetch(routePath)) {
if (type === undefined) {
const { relativePath: path } = this.heliaFetch.parsePath(routePath)
Expand All @@ -108,9 +119,10 @@ class HeliaServer {
response
}: IRouteHandler): Promise<void> {
try {
this.log('Fetching from IPFS:', request.path)
await this.fetchFromHeliaAndWriteToResponse({ response, request, routePath: request.path })
} catch (error) {
console.debug(error)
this.log('Error fetching from IPFS:', error)
response.status(500).end()
}
}
Expand All @@ -121,6 +133,7 @@ class HeliaServer {
async fetchIpns ({ request, response }: IRouteHandler): Promise<void> {
try {
await this.isReady
this.log('Requesting content from IPNS:', request.path)

const {
namespace: reqNamespace,
Expand All @@ -129,6 +142,7 @@ class HeliaServer {
} = this.heliaFetch.parsePath(request.path)

if (request.headers.referer !== undefined) {
this.log('Referer found:', request.headers.referer)
const refererPath = new URL(request.headers.referer).pathname
const {
namespace: refNamespace,
Expand All @@ -137,15 +151,17 @@ class HeliaServer {
if (reqNamespace !== refNamespace || reqDomain !== refDomain) {
if (!request.originalUrl.startsWith(refererPath) && refNamespace === 'ipns') {
const finalUrl = `${request.headers.referer}/${reqDomain}/${relativePath}`.replace(/([^:]\/)\/+/g, '$1')
response.redirect(finalUrl)
this.log('Redirecting to final URL:', finalUrl)
response.redirect(301, finalUrl)
return
}
}
}

this.log('Requesting content from IPNS:', request.path)
await this.fetchFromHeliaAndWriteToResponse({ response, request, routePath: request.path })
} catch (error) {
console.debug(error)
this.log('Error requesting content from IPNS:', error)
response.status(500).end()
}
}
Expand All @@ -158,6 +174,7 @@ class HeliaServer {

try {
if (this.heliaVersionInfo === undefined) {
this.log('Fetching Helia version info')
const { default: packageJson } = await import('../../node_modules/helia/package.json', {
assert: { type: 'json' }
})
Expand All @@ -170,6 +187,7 @@ class HeliaServer {
}
}

this.log('Helia version info:', this.heliaVersionInfo)
response.json(this.heliaVersionInfo)
} catch (error) {
response.status(500).end()
Expand All @@ -181,10 +199,8 @@ class HeliaServer {
*/
async gc ({ response }: IRouteHandler): Promise<void> {
await this.isReady
this.log('GCing node')
await this.heliaFetch.node?.gc()
response.status(200).end()
}
}

const heliaServer = new HeliaServer()
export default heliaServer
Loading

0 comments on commit 6bbed9a

Please sign in to comment.