@xyo-network/image-thumbnail-plugin 2.70.10 → 2.70.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,14 @@
1
+ import { promises as dnsPromises } from 'node:dns'
2
+
1
3
  import { axios, AxiosError, AxiosResponse } from '@xyo-network/axios'
2
4
  import { PayloadHasher } from '@xyo-network/core'
3
- import { ImageThumbnailErrorPayload, ImageThumbnailPayload, ImageThumbnailSchema } from '@xyo-network/image-thumbnail-payload-plugin'
4
- import { isPayload, ModuleError, ModuleErrorSchema } from '@xyo-network/payload-model'
5
+ import { ImageThumbnail, ImageThumbnailSchema } from '@xyo-network/image-thumbnail-payload-plugin'
5
6
  import { UrlPayload } from '@xyo-network/url-payload-plugin'
6
7
  import { AbstractWitness } from '@xyo-network/witness'
7
8
  import { subClass } from 'gm'
9
+ import { sync as hasbin } from 'hasbin'
8
10
  import { sha256 } from 'hash-wasm'
9
11
  import compact from 'lodash/compact'
10
- import isBuffer from 'lodash/isBuffer'
11
12
  import { LRUCache } from 'lru-cache'
12
13
  import shajs from 'sha.js'
13
14
  import Url from 'url-parse'
@@ -19,63 +20,22 @@ import { ImageThumbnailWitnessParams } from './Params'
19
20
 
20
21
  const gm = subClass({ imageMagick: '7+' })
21
22
 
22
- export const binaryToSha256 = async (data: Uint8Array) => {
23
- await PayloadHasher.wasmInitialized
24
- if (PayloadHasher.wasmSupport.canUseWasm) {
25
- try {
26
- return await sha256(data)
27
- } catch (ex) {
28
- PayloadHasher.wasmSupport.allowWasm = false
29
- }
30
- }
31
- // eslint-disable-next-line deprecation/deprecation
32
- return shajs('sha256').update(data).digest().toString()
23
+ export interface ImageThumbnailWitnessError extends Error {
24
+ name: 'ImageThumbnailWitnessError'
25
+ url: string
33
26
  }
34
27
 
35
- const checkIpfsUrl = (urlToCheck: string) => {
36
- const url = new Url(urlToCheck)
37
- let protocol = url.protocol
38
- let host = url.host
39
- let path = url.pathname
40
- const query = url.query
41
- if (protocol === 'ipfs:') {
42
- protocol = 'https:'
43
- host = 'cloudflare-ipfs.com'
44
- path = url.host === 'ipfs' ? `ipfs${path}` : `ipfs/${url.host}${path}`
45
- const root = `${protocol}//${host}/${path}`
46
- return query?.length > 0 ? `root?${query}` : root
47
- } else {
48
- return urlToCheck
49
- }
50
- }
51
-
52
- const checkDataUrl = (url: string): [Buffer | undefined, ImageThumbnailErrorPayload | undefined] => {
53
- if (url.startsWith('data:image')) {
54
- const data = url.split(',')[1]
55
- if (data) {
56
- const buffer = Buffer.from(Uint8Array.from(atob(data), (c) => c.charCodeAt(0)))
57
- console.log(`data buffer: ${buffer.length}`)
58
- return [buffer, undefined]
59
- } else {
60
- const error: ImageThumbnailErrorPayload = {
61
- message: 'Invalid data Url',
62
- schema: ModuleErrorSchema,
63
- url,
64
- }
65
- return [undefined, error]
66
- }
67
- } else {
68
- return [undefined, undefined]
69
- }
28
+ export interface DnsError extends Error {
29
+ code: string
70
30
  }
71
31
 
72
32
  export class ImageThumbnailWitness<TParams extends ImageThumbnailWitnessParams = ImageThumbnailWitnessParams> extends AbstractWitness<TParams> {
73
33
  static override configSchemas = [ImageThumbnailWitnessConfigSchema]
74
34
 
75
- private _cache?: LRUCache<string, ImageThumbnailPayload | ImageThumbnailErrorPayload>
35
+ private _cache?: LRUCache<string, ImageThumbnail>
76
36
 
77
37
  get cache() {
78
- this._cache = this._cache ?? new LRUCache<string, ImageThumbnailPayload | ImageThumbnailErrorPayload>({ max: this.maxCacheEntries })
38
+ this._cache = this._cache ?? new LRUCache<string, ImageThumbnail>({ max: this.maxCacheEntries })
79
39
  return this._cache
80
40
  }
81
41
 
@@ -99,124 +59,165 @@ export class ImageThumbnailWitness<TParams extends ImageThumbnailWitnessParams =
99
59
  return this.config.width ?? 128
100
60
  }
101
61
 
102
- protected override async observeHandler(payloads: UrlPayload[] = []): Promise<(ImageThumbnailPayload | ImageThumbnailErrorPayload)[]> {
103
- const responsePairs = compact(
104
- await Promise.all(
105
- payloads.map<Promise<[string, ImageThumbnailPayload | ImageThumbnailErrorPayload | AxiosResponse | Buffer]>>(async ({ url }) => {
106
- const cachedResult = this.cache.get(url)
107
- if (cachedResult) {
108
- return [url, cachedResult]
109
- }
110
- //if it is a data URL, return a Buffer
111
- const [dataBuffer, dataError] = checkDataUrl(url)
112
-
113
- if (dataBuffer) {
114
- return [url, dataBuffer]
115
- }
62
+ private static async binaryToSha256(data: Uint8Array) {
63
+ await PayloadHasher.wasmInitialized
64
+ if (PayloadHasher.wasmSupport.canUseWasm) {
65
+ try {
66
+ return await sha256(data)
67
+ } catch (ex) {
68
+ PayloadHasher.wasmSupport.allowWasm = false
69
+ }
70
+ }
71
+ // eslint-disable-next-line deprecation/deprecation
72
+ return shajs('sha256').update(data).digest().toString()
73
+ }
116
74
 
117
- if (dataError) {
118
- return [url, dataError]
119
- }
75
+ private static bufferFromDataUrl(url: string): Buffer | undefined {
76
+ if (url.startsWith('data:image')) {
77
+ const data = url.split(',')[1]
78
+ if (data) {
79
+ const buffer = Buffer.from(Uint8Array.from(atob(data), (c) => c.charCodeAt(0)))
80
+ console.log(`data buffer: ${buffer.length}`)
81
+ return buffer
82
+ } else {
83
+ const error: ImageThumbnailWitnessError = {
84
+ message: 'Invalid data Url',
85
+ name: 'ImageThumbnailWitnessError',
86
+ url,
87
+ }
88
+ throw error
89
+ }
90
+ }
91
+ }
120
92
 
121
- //if it is ipfs, go through cloud flair
122
- const mutatedUrl = checkIpfsUrl(url)
93
+ private static checkIpfsUrl(urlToCheck: string) {
94
+ const url = new Url(urlToCheck)
95
+ let protocol = url.protocol
96
+ let host = url.host
97
+ let path = url.pathname
98
+ const query = url.query
99
+ if (protocol === 'ipfs:') {
100
+ protocol = 'https:'
101
+ host = 'cloudflare-ipfs.com'
102
+ path = url.host === 'ipfs' ? `ipfs${path}` : `ipfs/${url.host}${path}`
103
+ const root = `${protocol}//${host}/${path}`
104
+ return query?.length > 0 ? `root?${query}` : root
105
+ } else {
106
+ return urlToCheck
107
+ }
108
+ }
123
109
 
124
- try {
125
- return [
126
- url,
127
- await axios.get(mutatedUrl, {
128
- responseType: 'arraybuffer',
129
- }),
130
- ]
131
- } catch (ex) {
132
- const axiosError = ex as AxiosError
133
- if (axiosError.isAxiosError) {
134
- //selectively pick fields from AxiosError
135
- const errorPayload: ImageThumbnailErrorPayload = {
136
- code: axiosError.code,
137
- message: axiosError.message,
138
- schema: ModuleErrorSchema,
139
- status: axiosError.status,
140
- url,
141
- }
142
- return [url, errorPayload]
143
- } else {
144
- throw ex
145
- }
146
- }
147
- }),
148
- ),
149
- )
110
+ protected override async observeHandler(payloads: UrlPayload[] = []): Promise<ImageThumbnail[]> {
111
+ if (!hasbin('magick')) {
112
+ throw Error('ImageMagick is required for this witness')
113
+ }
150
114
  return compact(
151
115
  await Promise.all(
152
- responsePairs.map(async ([url, urlResult]) => {
153
- if (isPayload(urlResult)) {
154
- this.cache.set(url, urlResult)
155
- return urlResult
116
+ payloads.map<Promise<ImageThumbnail>>(async ({ url }) => {
117
+ const cachedResult = this.cache.get(url)
118
+ if (cachedResult) {
119
+ return cachedResult
156
120
  }
121
+ let result: ImageThumbnail
157
122
 
158
- let sourceBuffer: Buffer
123
+ //if it is a data URL, return a Buffer
124
+ const dataBuffer = ImageThumbnailWitness.bufferFromDataUrl(url)
159
125
 
160
- if (isBuffer(urlResult)) {
161
- sourceBuffer = urlResult as Buffer
162
- } else {
163
- const response = urlResult as AxiosResponse
164
-
165
- if (response.status >= 200 && response.status < 300) {
166
- const contentType = response.headers['content-type']?.toString()
167
- if (contentType.split('/')[0] !== 'image') {
168
- const error: ImageThumbnailErrorPayload = {
169
- message: `Invalid file type: ${contentType}`,
170
- schema: ModuleErrorSchema,
171
- url,
172
- }
173
- this.cache.set(url, error)
174
- return error
175
- }
176
- sourceBuffer = Buffer.from(response.data, 'binary')
177
- } else {
178
- const error: ImageThumbnailErrorPayload = {
179
- schema: ModuleErrorSchema,
180
- status: response.status,
181
- url,
182
- }
183
- this.cache.set(url, error)
184
- return error
185
- }
186
- }
187
- try {
188
- const thumb = await new Promise<Buffer>((resolve, reject) => {
189
- gm(sourceBuffer)
190
- .quality(this.quality)
191
- .resize(this.width, this.height)
192
- .flatten()
193
- .toBuffer(this.encoding, (error, buffer) => {
194
- if (error) {
195
- reject(error)
196
- } else {
197
- resolve(buffer)
198
- }
199
- })
200
- })
201
- const result: ImageThumbnailPayload = {
126
+ if (dataBuffer) {
127
+ result = {
202
128
  schema: ImageThumbnailSchema,
203
- sourceHash: await binaryToSha256(sourceBuffer),
129
+ sourceHash: await ImageThumbnailWitness.binaryToSha256(dataBuffer),
204
130
  sourceUrl: url,
205
- url: `data:image/png;base64,${thumb.toString('base64')}`,
206
- }
207
- this.cache.set(url, result)
208
- return result
209
- } catch (ex) {
210
- const error: ImageThumbnailErrorPayload = {
211
- message: 'Failed to resize image',
212
- schema: ModuleErrorSchema,
213
131
  url,
214
132
  }
215
- this.cache.set(url, error)
216
- return error
133
+ } else {
134
+ //if it is ipfs, go through cloud flair
135
+ const mutatedUrl = ImageThumbnailWitness.checkIpfsUrl(url)
136
+ result = await this.fromHttp(mutatedUrl)
217
137
  }
138
+ this.cache.set(url, result)
139
+ return result
218
140
  }),
219
141
  ),
220
142
  )
221
143
  }
144
+
145
+ private async createThumbnail(sourceBuffer: Buffer) {
146
+ const thumb = await new Promise<Buffer>((resolve, reject) => {
147
+ gm(sourceBuffer)
148
+ .quality(this.quality)
149
+ .resize(this.width, this.height)
150
+ .flatten()
151
+ .toBuffer(this.encoding, (error, buffer) => {
152
+ if (error) {
153
+ reject(error)
154
+ } else {
155
+ resolve(buffer)
156
+ }
157
+ })
158
+ })
159
+ return `data:image/png;base64,${thumb.toString('base64')}`
160
+ }
161
+
162
+ private async fromHttp(url: string): Promise<ImageThumbnail> {
163
+ let response: AxiosResponse
164
+ let dnsResult: string[]
165
+ try {
166
+ const urlObj = new Url(url)
167
+ dnsResult = await dnsPromises.resolve(urlObj.host)
168
+ console.log(`dnsResult: ${JSON.stringify(dnsResult, null, 2)}`)
169
+ } catch (ex) {
170
+ const error = ex as DnsError
171
+ const result: ImageThumbnail = {
172
+ http: {
173
+ dnsError: error.code,
174
+ },
175
+ schema: ImageThumbnailSchema,
176
+ sourceUrl: url,
177
+ }
178
+ return result
179
+ }
180
+ try {
181
+ response = await axios.get(url, {
182
+ responseType: 'arraybuffer',
183
+ })
184
+ } catch (ex) {
185
+ const axiosError = ex as AxiosError
186
+ if (axiosError.isAxiosError) {
187
+ //selectively pick fields from AxiosError
188
+ const result: ImageThumbnail = {
189
+ http: {
190
+ ipAddress: dnsResult[0],
191
+ status: axiosError.status,
192
+ },
193
+ schema: ImageThumbnailSchema,
194
+ sourceUrl: url,
195
+ }
196
+ return result
197
+ } else {
198
+ throw ex
199
+ }
200
+ }
201
+
202
+ const result: ImageThumbnail = {
203
+ http: {
204
+ status: response.status,
205
+ },
206
+ schema: ImageThumbnailSchema,
207
+ sourceUrl: url,
208
+ }
209
+
210
+ if (response.status >= 200 && response.status < 300) {
211
+ const contentType = response.headers['content-type']?.toString()
212
+ if (contentType.split('/')[0] !== 'image') {
213
+ result.mime = result.mime ?? {}
214
+ result.mime.invalid = true
215
+ } else {
216
+ const sourceBuffer = Buffer.from(response.data, 'binary')
217
+ result.sourceHash = await ImageThumbnailWitness.binaryToSha256(sourceBuffer)
218
+ result.url = await this.createThumbnail(sourceBuffer)
219
+ }
220
+ }
221
+ return result
222
+ }
222
223
  }