From 713260e110c472c0cafc0a69a4f8eca7c708e9a6 Mon Sep 17 00:00:00 2001 From: Siddharth Singh Date: Sun, 27 Oct 2024 10:16:00 +0530 Subject: [PATCH] split off nip94 metadata stuff into its own file, port changes to s3uploader --- src/interfaces/Nip94Metadata.ts | 31 +++++++++++++++++ src/uploaders/IPFSUploader.ts | 61 +++------------------------------ src/uploaders/S3Uploader.ts | 13 +++---- src/utils/image-metadata.ts | 54 +++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 62 deletions(-) create mode 100644 src/interfaces/Nip94Metadata.ts create mode 100644 src/utils/image-metadata.ts diff --git a/src/interfaces/Nip94Metadata.ts b/src/interfaces/Nip94Metadata.ts new file mode 100644 index 00000000..6069ac31 --- /dev/null +++ b/src/interfaces/Nip94Metadata.ts @@ -0,0 +1,31 @@ +/** + * Required fields of NIP-94 metadata for images. + * Contains the following fields: + * * `url` - required, the URL to of the file + * * `m` - required, the file mimetype. + */ +export type Nip94MetadataRequired = Record<'url' | 'm', string>; + +/** + * Optional fields of NIP-94 metadata for images. + * Contains the following fields: + * * `x` - sha-256 hash + * * `ox` - sha-256 hash + * * `dim` - image dimensions in ${w}x${h} format + * * `blurhash` - the blurhash for the image. useful for image previews etc + * * `cid` - the ipfs cid of the image. + */ +export type Nip94MetadataOptional = Partial>; + +/** + * NIP-94 metadata for images. + * Contains the following fields: + * * `url` - required, the URL to of the file + * * `m` - required, the file mimetype. + * * `x` - sha-256 hash + * * `ox` - sha-256 hash + * * `dim` - image dimensions in ${w}x${h} format + * * `blurhash` - the blurhash for the image. useful for image previews etc + * * `cid` - the ipfs cid of the image. + */ +export type Nip94Metadata = Nip94MetadataOptional & Nip94MetadataRequired; diff --git a/src/uploaders/IPFSUploader.ts b/src/uploaders/IPFSUploader.ts index 1065ea0c..07184016 100644 --- a/src/uploaders/IPFSUploader.ts +++ b/src/uploaders/IPFSUploader.ts @@ -1,11 +1,6 @@ import { NUploader } from '@nostrify/nostrify'; import { z } from 'zod'; -import sharp from 'sharp'; -import { Stickynotes } from '@soapbox/stickynotes'; -import { encode } from 'blurhash'; -import { encodeHex } from '@std/encoding/hex'; - -const console = new Stickynotes('ditto:uploader:ipfs'); +import { getOptionalNip94Metadata } from '@/utils/image-metadata.ts'; export interface IPFSUploaderOpts { baseUrl: string; @@ -13,22 +8,6 @@ export interface IPFSUploaderOpts { fetch?: typeof fetch; } -function toByteArray(f: File): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.addEventListener('loadend', (m) => { - if (m?.target?.result instanceof ArrayBuffer) { - resolve(new Uint8Array(m.target.result)); - } else reject('Error loading file: readAsArrayBufferFailed'); - }); - reader.addEventListener('error', (e) => reject(e)); - reader.readAsArrayBuffer(f); - }); -} -type Nip94Metadata = - & Record<'url' | 'm', string> - & Partial>; - /** * IPFS uploader. It expects an IPFS node up and running. * It will try to connect to `http://localhost:5001` by default, @@ -58,44 +37,14 @@ export class IPFSUploader implements NUploader { }); const { Hash: cid } = IPFSUploader.schema().parse(await response.json()); - const tags: Nip94Metadata = { + + return Object.entries({ url: new URL(`/ipfs/${cid}`, this.baseUrl).toString(), m: file.type, cid, size: file.size.toString(), - }; - - try { - const buffer = await toByteArray(file); - const hash = await crypto.subtle.digest('SHA-256', buffer).then(encodeHex); - tags.x = tags.ox = hash; - const img = sharp(buffer); - const metadata = await img.metadata(); - - if (metadata.width && metadata.height) { - tags.dim = `${metadata.width}x${metadata.height}`; - const pixels = await img - .raw() - .ensureAlpha() - .toBuffer({ resolveWithObject: true }) - .then((buf) => { - return new Uint8ClampedArray(buf.data); - }); - tags.blurhash = encode( - pixels, - metadata.width, - metadata.height, - // sane default from https://github.com/woltapp/blurhash readme - 4, - 4, - ); - } - } catch (e) { - console.error(`Error parsing ipfs metadata: ${e}`); - } - - console.debug(tags); - return Object.entries(tags) as [['url', string], ...string[][]]; + ...await getOptionalNip94Metadata(file), + }) as [['url', string], ...string[][]]; } async delete(cid: string, opts?: { signal?: AbortSignal }): Promise { diff --git a/src/uploaders/S3Uploader.ts b/src/uploaders/S3Uploader.ts index b74796ab..2059077d 100644 --- a/src/uploaders/S3Uploader.ts +++ b/src/uploaders/S3Uploader.ts @@ -7,6 +7,7 @@ import { encodeHex } from '@std/encoding/hex'; import { extensionsByType } from '@std/media-types'; import { Conf } from '@/config.ts'; +import { getOptionalNip94Metadata } from '@/utils/image-metadata.ts'; export interface S3UploaderOpts { endPoint: string; @@ -45,12 +46,12 @@ export class S3Uploader implements NUploader { const path = (pathStyle && bucket) ? join(bucket, filename) : filename; const url = new URL(path, Conf.mediaDomain).toString(); - return [ - ['url', url], - ['m', file.type], - ['x', sha256], - ['size', file.size.toString()], - ]; + return Object.entries({ + url: url, + m: file.type, + size: file.size.toString(), + ...await getOptionalNip94Metadata(file), + }) as [['url', string], ...string[][]]; } async delete(objectName: string) { diff --git a/src/utils/image-metadata.ts b/src/utils/image-metadata.ts new file mode 100644 index 00000000..2d5e861f --- /dev/null +++ b/src/utils/image-metadata.ts @@ -0,0 +1,54 @@ +import sharp from 'sharp'; +import { encode } from 'blurhash'; +import { encodeHex } from '@std/encoding/hex'; +import type { Nip94MetadataOptional } from '@/interfaces/Nip94Metadata.ts'; +import { Stickynotes } from '@soapbox/stickynotes'; + +const console = new Stickynotes('ditto:uploaders'); + +function toByteArray(f: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.addEventListener('loadend', (m) => { + if (m?.target?.result instanceof ArrayBuffer) { + resolve(new Uint8Array(m.target.result)); + } else reject('Error loading file: readAsArrayBufferFailed'); + }); + reader.addEventListener('error', (e) => reject(e)); + reader.readAsArrayBuffer(f); + }); +} + +export async function getOptionalNip94Metadata(f: File): Promise { + const tags: Nip94MetadataOptional = {}; + try { + const buffer = await toByteArray(f); + const hash = await crypto.subtle.digest('SHA-256', buffer).then(encodeHex); + tags.x = tags.ox = hash; + const img = sharp(buffer); + const metadata = await img.metadata(); + + if (metadata.width && metadata.height) { + tags.dim = `${metadata.width}x${metadata.height}`; + const pixels = await img + .raw() + .ensureAlpha() + .toBuffer({ resolveWithObject: true }) + .then((buf) => { + return new Uint8ClampedArray(buf.data); + }); + tags.blurhash = encode( + pixels, + metadata.width, + metadata.height, + // sane default from https://github.com/woltapp/blurhash readme + 4, + 4, + ); + } + } catch (e) { + console.error(`Error parsing ipfs metadata: ${e}`); + } + + return tags; +}