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 { NUploader } from '@nostrify/nostrify';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import sharp from 'sharp';
|
import { getOptionalNip94Metadata } from '@/utils/image-metadata.ts';
|
||||||
import { Stickynotes } from '@soapbox/stickynotes';
|
|
||||||
import { encode } from 'blurhash';
|
|
||||||
import { encodeHex } from '@std/encoding/hex';
|
|
||||||
|
|
||||||
const console = new Stickynotes('ditto:uploader:ipfs');
|
|
||||||
|
|
||||||
export interface IPFSUploaderOpts {
|
export interface IPFSUploaderOpts {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
|
|
@ -13,22 +8,6 @@ export interface IPFSUploaderOpts {
|
||||||
fetch?: typeof fetch;
|
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.
|
* IPFS uploader. It expects an IPFS node up and running.
|
||||||
* It will try to connect to `http://localhost:5001` by default,
|
* 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 { Hash: cid } = IPFSUploader.schema().parse(await response.json());
|
||||||
const tags: Nip94Metadata = {
|
|
||||||
|
return Object.entries({
|
||||||
url: new URL(`/ipfs/${cid}`, this.baseUrl).toString(),
|
url: new URL(`/ipfs/${cid}`, this.baseUrl).toString(),
|
||||||
m: file.type,
|
m: file.type,
|
||||||
cid,
|
cid,
|
||||||
size: file.size.toString(),
|
size: file.size.toString(),
|
||||||
};
|
...await getOptionalNip94Metadata(file),
|
||||||
|
}) as [['url', string], ...string[][]];
|
||||||
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[][]];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(cid: string, opts?: { signal?: AbortSignal }): Promise<void> {
|
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 { extensionsByType } from '@std/media-types';
|
||||||
|
|
||||||
import { Conf } from '@/config.ts';
|
import { Conf } from '@/config.ts';
|
||||||
|
import { getOptionalNip94Metadata } from '@/utils/image-metadata.ts';
|
||||||
|
|
||||||
export interface S3UploaderOpts {
|
export interface S3UploaderOpts {
|
||||||
endPoint: string;
|
endPoint: string;
|
||||||
|
|
@ -45,12 +46,12 @@ export class S3Uploader implements NUploader {
|
||||||
const path = (pathStyle && bucket) ? join(bucket, filename) : filename;
|
const path = (pathStyle && bucket) ? join(bucket, filename) : filename;
|
||||||
const url = new URL(path, Conf.mediaDomain).toString();
|
const url = new URL(path, Conf.mediaDomain).toString();
|
||||||
|
|
||||||
return [
|
return Object.entries({
|
||||||
['url', url],
|
url: url,
|
||||||
['m', file.type],
|
m: file.type,
|
||||||
['x', sha256],
|
size: file.size.toString(),
|
||||||
['size', file.size.toString()],
|
...await getOptionalNip94Metadata(file),
|
||||||
];
|
}) as [['url', string], ...string[][]];
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(objectName: 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