@@ -3,11 +3,12 @@ import { PluginOptions } from './types.js';
33import { AdminForthPlugin , AdminForthResourceColumn , AdminForthResource , Filters , IAdminForth , IHttpServer , suggestIfTypo } from "adminforth" ;
44import { Readable } from "stream" ;
55import { RateLimiter } from "adminforth" ;
6+ import { randomUUID } from "crypto" ;
67import { interpretResource } from 'adminforth' ;
78import { ActionCheckSource } from 'adminforth' ;
89
910const ADMINFORTH_NOT_YET_USED_TAG = 'adminforth-candidate-for-cleanup' ;
10-
11+ const jobs = new Map ( ) ;
1112export default class UploadPlugin extends AdminForthPlugin {
1213 options : PluginOptions ;
1314
@@ -36,10 +37,87 @@ export default class UploadPlugin extends AdminForthPlugin {
3637 return this . options . storageAdapter . getDownloadUrl ( path , expiresInSeconds ) ;
3738 }
3839 if ( this . options . generation ?. rateLimit ?. limit ) {
39- this . rateLimiter = new RateLimiter ( this . options . generation . rateLimit ?. limit )
40+ this . rateLimiter = new RateLimiter ( this . options . generation . rateLimit ?. limit )
4041 }
4142 }
4243
44+ private async generateImages ( jobId : string , prompt : string , recordId : any , adminUser : any , headers : any ) {
45+ if ( this . options . generation . rateLimit ?. limit ) {
46+ // rate limit
47+ // const { error } = RateLimiter.checkRateLimit(
48+ // this.pluginInstanceId,
49+ // this.options.generation.rateLimit?.limit,
50+ // this.adminforth.auth.getClientIp(headers),
51+ // );
52+ if ( ! await this . rateLimiter . consume ( `${ this . pluginInstanceId } -${ this . adminforth . auth . getClientIp ( headers ) } ` ) ) {
53+ jobs . set ( jobId , { status : "failed" , error : this . options . generation . rateLimit . errorMessage } ) ;
54+ return { error : this . options . generation . rateLimit . errorMessage } ;
55+ }
56+ }
57+ let attachmentFiles = [ ] ;
58+ if ( this . options . generation . attachFiles ) {
59+ // TODO - does it require additional allowed action to check this record id has access to get the image?
60+ // or should we mention in docs that user should do validation in method itself
61+ const record = await this . adminforth . resource ( this . resourceConfig . resourceId ) . get (
62+ [ Filters . EQ ( this . resourceConfig . columns . find ( c => c . primaryKey ) ?. name , recordId ) ]
63+ ) ;
64+
65+
66+ if ( ! record ) {
67+ return { error : `Record with id ${ recordId } not found` } ;
68+ }
69+
70+ attachmentFiles = await this . options . generation . attachFiles ( { record, adminUser } ) ;
71+ // if files is not array, make it array
72+ if ( ! Array . isArray ( attachmentFiles ) ) {
73+ attachmentFiles = [ attachmentFiles ] ;
74+ }
75+
76+ }
77+
78+ let error : string | undefined = undefined ;
79+
80+ const STUB_MODE = false ;
81+
82+ const images = await Promise . all (
83+ ( new Array ( this . options . generation . countToGenerate ) ) . fill ( 0 ) . map ( async ( ) => {
84+ if ( STUB_MODE ) {
85+ await new Promise ( ( resolve ) => setTimeout ( resolve , 2000 ) ) ;
86+ return `https://picsum.photos/200/300?random=${ Math . floor ( Math . random ( ) * 1000 ) } ` ;
87+ }
88+ const start = + new Date ( ) ;
89+ let resp ;
90+ try {
91+ resp = await this . options . generation . adapter . generate (
92+ {
93+ prompt,
94+ inputFiles : attachmentFiles ,
95+ n : 1 ,
96+ size : this . options . generation . outputSize ,
97+ }
98+ )
99+ } catch ( e : any ) {
100+ error = `No response from image generation provider: ${ e . message } . Please check your prompt or try again later.` ;
101+ return ;
102+ }
103+
104+ if ( resp . error ) {
105+ console . error ( 'Error generating image' , resp . error ) ;
106+ error = resp . error ;
107+ return ;
108+ }
109+
110+ this . totalCalls ++ ;
111+ this . totalDuration += ( + new Date ( ) - start ) / 1000 ;
112+
113+ return resp . imageURLs [ 0 ]
114+
115+ } )
116+ ) ;
117+ jobs . set ( jobId , { status : "completed" , images, error } ) ;
118+ return { ok : true } ;
119+ } ;
120+
43121 instanceUniqueRepresentation ( pluginOptions : any ) : string {
44122 return `${ pluginOptions . pathColumnName } ` ;
45123 }
@@ -323,81 +401,34 @@ export default class UploadPlugin extends AdminForthPlugin {
323401
324402 server . endpoint ( {
325403 method : 'POST' ,
326- path : `/plugin/${ this . pluginInstanceId } /generate_images ` ,
404+ path : `/plugin/${ this . pluginInstanceId } /create-image-generation-job ` ,
327405 handler : async ( { body, adminUser, headers } ) => {
328406 const { prompt, recordId } = body ;
329- if ( this . rateLimiter ) {
330- // rate limit
331- // const { error } = RateLimiter.checkRateLimit(
332- // this.pluginInstanceId,
333- // this.options.generation.rateLimit?.limit,
334- // this.adminforth.auth.getClientIp(headers),
335- // );
336- if ( ! await this . rateLimiter . consume ( `${ this . pluginInstanceId } -${ this . adminforth . auth . getClientIp ( headers ) } ` ) ) {
337- return { error : this . options . generation . rateLimit . errorMessage } ;
338- }
339- }
340- let attachmentFiles = [ ] ;
341- if ( this . options . generation . attachFiles ) {
342- // TODO - does it require additional allowed action to check this record id has access to get the image?
343- // or should we mention in docs that user should do validation in method itself
344- const record = await this . adminforth . resource ( this . resourceConfig . resourceId ) . get (
345- [ Filters . EQ ( this . resourceConfig . columns . find ( ( column : any ) => column . primaryKey ) ?. name , recordId ) ]
346- ) ;
347-
348- if ( ! record ) {
349- return { error : `Record with id ${ recordId } not found` } ;
350- }
351-
352- attachmentFiles = await this . options . generation . attachFiles ( { record, adminUser } ) ;
353- // if files is not array, make it array
354- if ( ! Array . isArray ( attachmentFiles ) ) {
355- attachmentFiles = [ attachmentFiles ] ;
356- }
357-
358- }
359-
360- let error : string | undefined = undefined ;
361-
362- const STUB_MODE = false ;
363-
364- const images = await Promise . all (
365- ( new Array ( this . options . generation . countToGenerate ) ) . fill ( 0 ) . map ( async ( ) => {
366- if ( STUB_MODE ) {
367- await new Promise ( ( resolve ) => setTimeout ( resolve , 2000 ) ) ;
368- return `https://picsum.photos/200/300?random=${ Math . floor ( Math . random ( ) * 1000 ) } ` ;
369- }
370- const start = + new Date ( ) ;
371- let resp ;
372- try {
373- resp = await this . options . generation . adapter . generate (
374- {
375- prompt,
376- inputFiles : attachmentFiles ,
377- n : 1 ,
378- size : this . options . generation . outputSize ,
379- }
380- )
381- } catch ( e : any ) {
382- error = `No response from image generation provider: ${ e . message } . Please check your prompt or try again later.` ;
383- return ;
384- }
385407
386- if ( resp . error ) {
387- console . error ( 'Error generating image' , resp . error ) ;
388- error = resp . error ;
389- return ;
390- }
408+ const jobId = randomUUID ( ) ;
409+ jobs . set ( jobId , { status : "in_progress" } ) ;
391410
392- this . totalCalls ++ ;
393- this . totalDuration += ( + new Date ( ) - start ) / 1000 ;
394-
395- return resp . imageURLs [ 0 ]
411+ this . generateImages ( jobId , prompt , recordId , adminUser , headers ) ;
412+ setTimeout ( ( ) => jobs . delete ( jobId ) , 1_800_000 ) ;
413+ setTimeout ( ( ) => { jobs . set ( jobId , { status : "timeout" } ) ; } , 300_000 ) ;
396414
397- } )
398- ) ;
415+ return { ok : true , jobId } ;
416+ }
417+ } ) ;
399418
400- return { error, images } ;
419+ server . endpoint ( {
420+ method : 'POST' ,
421+ path : `/plugin/${ this . pluginInstanceId } /get-image-generation-job-status` ,
422+ handler : async ( { body, adminUser, headers } ) => {
423+ const jobId = body . jobId ;
424+ if ( ! jobId ) {
425+ return { error : "Can't find job id" } ;
426+ }
427+ const job = jobs . get ( jobId ) ;
428+ if ( ! job ) {
429+ return { error : "Job not found" } ;
430+ }
431+ return { ok : true , job } ;
401432 }
402433 } ) ;
403434
@@ -459,5 +490,6 @@ export default class UploadPlugin extends AdminForthPlugin {
459490 } ) ;
460491
461492 }
493+
462494
463495}
0 commit comments