Skip to content

Commit 8aa6e5e

Browse files
committed
add(ipos): car file support
1 parent 1754cfa commit 8aa6e5e

File tree

14 files changed

+1218
-2679
lines changed

14 files changed

+1218
-2679
lines changed

pnpm-lock.yaml

Lines changed: 965 additions & 2449 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

services/ipos/biome.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
3+
"organizeImports": {
4+
"enabled": true
5+
},
6+
"files": {
7+
"ignore": ["node_modules", ".wrangler", ".papi"]
8+
},
9+
"linter": {
10+
"enabled": true,
11+
"rules": {
12+
"recommended": true
13+
}
14+
},
15+
"javascript": {
16+
"formatter": {
17+
"semicolons": "asNeeded",
18+
"quoteStyle": "single"
19+
}
20+
}
21+
}

services/ipos/package.json

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,31 @@
11
{
2-
"name": "ipos",
3-
"version": "0.0.0",
4-
"private": true,
5-
"scripts": {
6-
"deploy": "wrangler deploy",
7-
"dev": "wrangler dev",
8-
"start": "wrangler dev",
9-
"test": "vitest",
10-
"cf-typegen": "wrangler types"
11-
},
12-
"devDependencies": {
13-
"@cloudflare/vitest-pool-workers": "^0.4.5",
14-
"@cloudflare/workers-types": "^4.20240620.0",
15-
"typescript": "^5.4.5",
16-
"vitest": "1.5.0",
17-
"wrangler": "^3.60.3"
18-
},
19-
"dependencies": {
20-
"@aws-sdk/client-s3": "^3.609.0",
21-
"@helia/unixfs": "^3.0.6",
22-
"@hono/valibot-validator": "^0.2.5",
23-
"blockstore-core": "^4.4.1",
24-
"helia": "^4.2.4",
25-
"hono": "^4.4.12",
26-
"ipfs-only-hash": "^4.0.0",
27-
"ipfs-unixfs-importer": "^15.2.5",
28-
"multiformats": "^13.1.3",
29-
"valibot": "^0.30.0"
30-
}
2+
"name": "ipos",
3+
"version": "0.0.0",
4+
"private": true,
5+
"scripts": {
6+
"deploy": "wrangler deploy",
7+
"dev": "wrangler dev",
8+
"start": "wrangler dev",
9+
"test": "vitest",
10+
"cf-typegen": "wrangler types",
11+
"lint": "biome check --write ."
12+
},
13+
"devDependencies": {
14+
"@biomejs/biome": "^1.9.2",
15+
"@cloudflare/vitest-pool-workers": "^0.4.5",
16+
"@cloudflare/workers-types": "^4.20240620.0",
17+
"typescript": "^5.4.5",
18+
"vitest": "1.5.0",
19+
"wrangler": "^3.60.3"
20+
},
21+
"dependencies": {
22+
"@aws-sdk/client-s3": "^3.609.0",
23+
"@hono/valibot-validator": "^0.2.5",
24+
"hono": "^4.4.12",
25+
"ipfs-car": "^0.9.0",
26+
"ipfs-only-hash": "^4.0.0",
27+
"ipfs-unixfs-importer": "^15.2.5",
28+
"multiformats": "^13.1.3",
29+
"valibot": "^0.30.0"
30+
}
3131
}

services/ipos/src/index.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
1-
import { Hono } from 'hono';
2-
import { cors } from 'hono/cors';
1+
import { Hono } from 'hono'
2+
import { cors } from 'hono/cors'
33

4-
import { HonoEnv } from './utils/constants';
5-
import { pinning } from './routes/pinning';
4+
import { pinning } from './routes/pinning'
5+
import type { HonoEnv } from './utils/constants'
66

7-
const app = new Hono<HonoEnv>();
7+
const app = new Hono<HonoEnv>()
88

9-
app.get('/', (c) => c.text('Hello <<Artists>>!'));
10-
app.use('*', cors());
9+
app.get('/', (c) => c.text('Hello <<Artists>>!'))
10+
app.use('*', cors())
1111

12-
app.route('/', pinning);
12+
app.route('/', pinning)
1313

1414
app.onError((err, c) => {
15-
console.error(`${err}`);
16-
return c.json({ error: err.message, path: c.req.url }, 400);
17-
});
15+
console.error(`${err}`)
16+
return c.json({ error: err.message, path: c.req.url }, 400)
17+
})
1818

19-
export default app;
19+
export default app

services/ipos/src/routes/pinning.ts

Lines changed: 107 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,44 @@
1-
import { Hono } from 'hono'
2-
import { HonoEnv } from '../utils/constants'
31
import { vValidator } from '@hono/valibot-validator'
4-
import { blob, object, union, array } from 'valibot'
5-
import { getUint8ArrayFromFile, getObjectSize, hashOf, keyOf } from '../utils/format'
2+
import { Hono } from 'hono'
3+
import { array, blob, object, union } from 'valibot'
4+
import type { HonoEnv } from '../utils/constants'
5+
import {
6+
getObjectSize,
7+
getUint8ArrayFromFile,
8+
hashOf,
9+
keyOf,
10+
} from '../utils/format'
11+
import toCar from '../utils/ipfs'
612
import { getS3 } from '../utils/s3'
7-
import { getDirectoryCID } from '../utils/helia'
813

914
const app = new Hono<HonoEnv>()
1015

1116
app.post('/pinJson', vValidator('json', object({})), async (c) => {
12-
const body = await c.req.json()
13-
const type = 'application/json'
14-
const s3 = getS3(c)
15-
16-
const content = JSON.stringify(body)
17-
const cid = (await hashOf(content)).toV0().toString()
18-
19-
await s3.putObject({
20-
Body: content,
21-
Bucket: c.env.FILEBASE_BUCKET_NAME,
22-
Key: cid,
23-
ContentType: type,
24-
})
25-
26-
c.executionCtx.waitUntil(c.env.BUCKET.put(keyOf(cid), new Blob([content], { type })))
27-
28-
return c.json(
29-
getPinResponse({
30-
cid: cid,
31-
type: type,
32-
size: getObjectSize(body),
33-
}),
34-
)
17+
const body = await c.req.json()
18+
const type = 'application/json'
19+
const s3 = getS3(c)
20+
21+
const content = JSON.stringify(body)
22+
const cid = (await hashOf(content)).toV0().toString()
23+
24+
await s3.putObject({
25+
Body: content,
26+
Bucket: c.env.FILEBASE_BUCKET_NAME,
27+
Key: cid,
28+
ContentType: type,
29+
})
30+
31+
c.executionCtx.waitUntil(
32+
c.env.BUCKET.put(keyOf(cid), new Blob([content], { type })),
33+
)
34+
35+
return c.json(
36+
getPinResponse({
37+
cid: cid,
38+
type: type,
39+
size: getObjectSize(body),
40+
}),
41+
)
3542
})
3643

3744
const fileRequiredMessage = 'File is required'
@@ -40,85 +47,85 @@ const fileKey = 'file'
4047
type PinFile = { [fileKey]: File } | { [fileKey]: File[] }
4148

4249
const pinFileRequestSchema = object({
43-
[fileKey]: union([
44-
blob(fileRequiredMessage),
45-
array(blob(fileRequiredMessage)),
46-
]),
50+
[fileKey]: union([
51+
blob(fileRequiredMessage),
52+
array(blob(fileRequiredMessage)),
53+
]),
4754
})
4855

4956
app.post('/pinFile', vValidator('form', pinFileRequestSchema), async (c) => {
50-
const body = (await c.req.parseBody({ all: true })) as PinFile
51-
52-
const files = await Promise.all(
53-
([[body[fileKey]]].flat(2).filter(Boolean) as File[]).map(async (file) => ({
54-
file,
55-
content: await getUint8ArrayFromFile(file),
56-
})),
57-
)
58-
59-
const hasMultipleFiles = files.length > 1
60-
const s3 = getS3(c)
61-
62-
let directoryCId: string | undefined
63-
64-
if (hasMultipleFiles) {
65-
directoryCId = (await getDirectoryCID({ files, c })).toV0().toString()
66-
}
67-
68-
const addedFiles: { file: File; cid: string; content: Uint8Array }[] =
69-
await Promise.all(
70-
files.map(async ({ file, content }) => {
71-
try {
72-
const cid = (await hashOf(content)).toV0().toString()
73-
const prefix = directoryCId ? `${directoryCId}/` : ''
74-
75-
await s3.putObject({
76-
Body: content,
77-
Bucket: c.env.FILEBASE_BUCKET_NAME,
78-
Key: `${prefix}${cid}`,
79-
ContentType: file.type,
80-
})
81-
82-
console.log('File added', cid)
83-
return { file, cid, content }
84-
} catch (error) {
85-
throw new Error(`Failed to add file ${file.name}: ${error?.message}`)
86-
}
87-
}),
88-
)
89-
90-
c.executionCtx.waitUntil(
91-
Promise.all(
92-
addedFiles.map(({ content, cid }) => c.env.BUCKET.put(keyOf(cid), content)),
93-
),
94-
)
95-
96-
const size = files.reduce((reducer, file) => reducer + file.file.size, 0)
97-
const { cid: addedFileCid, file } = addedFiles[0]
98-
let cid = addedFileCid
99-
let type = file.type
100-
101-
if (hasMultipleFiles) {
102-
cid = directoryCId as string
103-
type = 'directory'
104-
}
105-
106-
return c.json(
107-
getPinResponse({
108-
cid: cid,
109-
type: type,
110-
size: size,
111-
}),
112-
)
57+
const body = (await c.req.parseBody({ all: true })) as PinFile
58+
59+
const files = await Promise.all(
60+
([[body[fileKey]]].flat(2).filter(Boolean) as File[]).map(async (file) => ({
61+
file,
62+
content: await getUint8ArrayFromFile(file),
63+
})),
64+
)
65+
66+
const s3 = getS3(c)
67+
68+
let cid: string
69+
let file: Uint8Array | File
70+
let size: number
71+
let type: string
72+
73+
if (files.length > 1) {
74+
const { root, car } = await toCar(
75+
files.map(({ file, content }) => ({
76+
path: file.name,
77+
content: content,
78+
})),
79+
)
80+
81+
cid = root.toString() as string
82+
file = car
83+
size = file.byteLength
84+
type = 'directory'
85+
86+
await s3.putObject({
87+
Body: car,
88+
Bucket: c.env.FILEBASE_BUCKET_NAME,
89+
Key: cid,
90+
ContentType: 'application/vnd.ipld.car',
91+
Metadata: {
92+
import: 'car',
93+
},
94+
})
95+
} else {
96+
const { content, file: f } = files[0]
97+
cid = (await hashOf(content)).toV0().toString()
98+
99+
file = f
100+
size = f.size
101+
type = f.type
102+
103+
await s3.putObject({
104+
Body: content,
105+
Bucket: c.env.FILEBASE_BUCKET_NAME,
106+
Key: cid,
107+
ContentType: f.type,
108+
})
109+
}
110+
111+
c.executionCtx.waitUntil(c.env.BUCKET.put(keyOf(cid), file))
112+
113+
return c.json(
114+
getPinResponse({
115+
cid: cid,
116+
type: type,
117+
size: size,
118+
}),
119+
)
113120
})
114121

115122
const getPinResponse = (value: {
116-
cid: string
117-
type: string
118-
size: number
123+
cid: string
124+
type: string
125+
size: number
119126
}) => ({
120-
ok: true,
121-
value,
127+
ok: true,
128+
value,
122129
})
123130

124131
export { app as pinning }

services/ipos/src/utils/constants.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
1-
import { Env } from 'hono'
1+
import type { Env } from 'hono'
22

3-
export interface CloudflareEnv extends Record<string, any> {
4-
BUCKET: R2Bucket
3+
export interface CloudflareEnv extends Record<string, unknown> {
4+
BUCKET: R2Bucket
55

6-
// wrangler secret
7-
// S3
8-
S3_ACCESS_KEY_ID: string
9-
S3_SECRET_ACCESS_KEY: string
10-
11-
// Filebase
12-
FILEBASE_BUCKET_NAME: string
6+
// wrangler secret
7+
// S3
8+
S3_ACCESS_KEY_ID: string
9+
S3_SECRET_ACCESS_KEY: string
10+
11+
// Filebase
12+
FILEBASE_BUCKET_NAME: string
1313
}
1414

1515
export interface HonoEnv extends Env {
16-
Bindings: CloudflareEnv
16+
Bindings: CloudflareEnv
1717
}
1818

1919
export const ORIGIN = 'https://kodadot.xyz'

0 commit comments

Comments
 (0)