diff --git a/src/controllers/api/media.ts b/src/controllers/api/media.ts index 3df4f98..c28d1d3 100644 --- a/src/controllers/api/media.ts +++ b/src/controllers/api/media.ts @@ -20,7 +20,7 @@ const mediaController: AppController = async (c) => { try { const { file } = result.data; - const { cid } = await s3Uploader(file); + const { cid } = await s3Uploader.upload(file); return c.json({ id: cid, diff --git a/src/uploaders/ipfs.ts b/src/uploaders/ipfs.ts index d31a36f..e6a33cd 100644 --- a/src/uploaders/ipfs.ts +++ b/src/uploaders/ipfs.ts @@ -3,28 +3,48 @@ import { z } from '@/deps.ts'; import type { Uploader } from './types.ts'; -const ipfsAddResultSchema = z.object({ +/** Response schema for POST `/api/v0/add`. */ +const ipfsAddResponseSchema = z.object({ Name: z.string(), Hash: z.string(), Size: z.string(), }); -const ipfsUploader: Uploader = async (file) => { - const url = new URL('/api/v0/add', Conf.ipfs.apiUrl); +/** + * IPFS uploader. It expects an IPFS node up and running. + * It will try to connect to `http://localhost:5001` by default, + * and upload the file using the REST API. + */ +const ipfsUploader: Uploader = { + async upload(file) { + const url = new URL('/api/v0/add', Conf.ipfs.apiUrl); - const formData = new FormData(); - formData.append('file', file); + const formData = new FormData(); + formData.append('file', file); - const response = await fetch(url, { - method: 'POST', - body: formData, - }); + const response = await fetch(url, { + method: 'POST', + body: formData, + }); - const { Hash } = ipfsAddResultSchema.parse(await response.json()); + const { Hash } = ipfsAddResponseSchema.parse(await response.json()); - return { - cid: Hash, - }; + return { + cid: Hash, + }; + }, + async delete(cid) { + const url = new URL('/api/v0/pin/rm', Conf.ipfs.apiUrl); + + const query = new URLSearchParams(); + query.set('arg', cid); + + url.search = query.toString(); + + await fetch(url, { + method: 'POST', + }); + }, }; export { ipfsUploader }; diff --git a/src/uploaders/s3.ts b/src/uploaders/s3.ts index b96712e..9242bee 100644 --- a/src/uploaders/s3.ts +++ b/src/uploaders/s3.ts @@ -5,19 +5,29 @@ import type { Uploader } from './types.ts'; const s3 = new S3Client({ ...Conf.s3 }); -const s3Uploader: Uploader = async (file) => { - const cid = await IpfsHash.of(file.stream()) as string; +/** + * S3-compatible uploader for AWS, Wasabi, DigitalOcean Spaces, and more. + * Files are named by their IPFS CID and exposed at `/ipfs/`, letting it + * take advantage of IPFS features while not really using IPFS. + */ +const s3Uploader: Uploader = { + async upload(file) { + const cid = await IpfsHash.of(file.stream()) as string; - await s3.putObject(`ipfs/${cid}`, file.stream(), { - metadata: { - 'Content-Type': file.type, - 'x-amz-acl': 'public-read', - }, - }); + await s3.putObject(`ipfs/${cid}`, file.stream(), { + metadata: { + 'Content-Type': file.type, + 'x-amz-acl': 'public-read', + }, + }); - return { - cid, - }; + return { + cid, + }; + }, + async delete(cid) { + await s3.deleteObject(`ipfs/${cid}`); + }, }; export { s3Uploader }; diff --git a/src/uploaders/types.ts b/src/uploaders/types.ts index a60968f..80bf431 100644 --- a/src/uploaders/types.ts +++ b/src/uploaders/types.ts @@ -1,7 +1,15 @@ +/** Modular uploader interface, to support uploading to different backends. */ +interface Uploader { + /** Upload the file to the backend. */ + upload(file: File): Promise; + /** Delete the file from the backend. */ + delete(cid: string): Promise; +} + +/** Return value from the uploader after uploading a file. */ interface UploadResult { + /** IPFS CID for the file. */ cid: string; } -type Uploader = (file: File) => Promise; - -export type { Uploader, UploadResult }; +export type { Uploader };