mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 11:29:46 +00:00
split off nip94 metadata stuff into its own file, port changes to s3uploader
This commit is contained in:
parent
ff8374103e
commit
713260e110
4 changed files with 97 additions and 62 deletions
31
src/interfaces/Nip94Metadata.ts
Normal file
31
src/interfaces/Nip94Metadata.ts
Normal file
|
|
@ -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<Record<'x' | 'ox' | 'size' | 'dim' | 'blurhash' | 'cid', string>>;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
|
@ -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<Uint8Array> {
|
||||
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<Record<'x' | 'ox' | 'size' | 'dim' | 'blurhash' | 'cid', string>>;
|
||||
|
||||
/**
|
||||
* 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<void> {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
54
src/utils/image-metadata.ts
Normal file
54
src/utils/image-metadata.ts
Normal file
|
|
@ -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<Uint8Array> {
|
||||
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<Nip94MetadataOptional> {
|
||||
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;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue