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