@@ -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
@@ -20,15 +21,23 @@ export default class UploadPlugin extends AdminForthPlugin {
2021
2122 rateLimiter : RateLimiter ;
2223
24+ getFileDownloadUrl : ( ( path : string ) => Promise < string > ) ;
25+
2326 constructor ( options : PluginOptions ) {
2427 super ( options , import . meta. url ) ;
2528 this . options = options ;
2629
2730 // for calcualting average time
2831 this . totalCalls = 0 ;
2932 this . totalDuration = 0 ;
33+ this . getFileDownloadUrl = async ( path : string , expiresInSeconds : number = 1800 ) : Promise < string > => {
34+ if ( ! path ) {
35+ return '' ;
36+ }
37+ return this . options . storageAdapter . getDownloadUrl ( path , expiresInSeconds ) ;
38+ }
3039 if ( this . options . generation ?. rateLimit ?. limit ) {
31- this . rateLimiter = new RateLimiter ( this . options . generation . rateLimit ?. limit )
40+ this . rateLimiter = new RateLimiter ( this . options . generation . rateLimit ?. limit )
3241 }
3342 }
3443
@@ -55,6 +64,83 @@ export default class UploadPlugin extends AdminForthPlugin {
5564 return this . callStorageAdapter ( 'markKeyForDeletion' , 'markKeyForDeletation' , filePath ) ;
5665 }
5766
67+ private async generateImages ( jobId : string , prompt : string , recordId : any , adminUser : any , headers : any ) {
68+ if ( this . options . generation . rateLimit ?. limit ) {
69+ // rate limit
70+ // const { error } = RateLimiter.checkRateLimit(
71+ // this.pluginInstanceId,
72+ // this.options.generation.rateLimit?.limit,
73+ // this.adminforth.auth.getClientIp(headers),
74+ // );
75+ if ( ! await this . rateLimiter . consume ( `${ this . pluginInstanceId } -${ this . adminforth . auth . getClientIp ( headers ) } ` ) ) {
76+ jobs . set ( jobId , { status : "failed" , error : this . options . generation . rateLimit . errorMessage } ) ;
77+ return { error : this . options . generation . rateLimit . errorMessage } ;
78+ }
79+ }
80+ let attachmentFiles = [ ] ;
81+ if ( this . options . generation . attachFiles ) {
82+ // TODO - does it require additional allowed action to check this record id has access to get the image?
83+ // or should we mention in docs that user should do validation in method itself
84+ const record = await this . adminforth . resource ( this . resourceConfig . resourceId ) . get (
85+ [ Filters . EQ ( this . resourceConfig . columns . find ( c => c . primaryKey ) ?. name , recordId ) ]
86+ ) ;
87+
88+
89+ if ( ! record ) {
90+ return { error : `Record with id ${ recordId } not found` } ;
91+ }
92+
93+ attachmentFiles = await this . options . generation . attachFiles ( { record, adminUser } ) ;
94+ // if files is not array, make it array
95+ if ( ! Array . isArray ( attachmentFiles ) ) {
96+ attachmentFiles = [ attachmentFiles ] ;
97+ }
98+
99+ }
100+
101+ let error : string | undefined = undefined ;
102+
103+ const STUB_MODE = false ;
104+
105+ const images = await Promise . all (
106+ ( new Array ( this . options . generation . countToGenerate ) ) . fill ( 0 ) . map ( async ( ) => {
107+ if ( STUB_MODE ) {
108+ await new Promise ( ( resolve ) => setTimeout ( resolve , 2000 ) ) ;
109+ return `https://picsum.photos/200/300?random=${ Math . floor ( Math . random ( ) * 1000 ) } ` ;
110+ }
111+ const start = + new Date ( ) ;
112+ let resp ;
113+ try {
114+ resp = await this . options . generation . adapter . generate (
115+ {
116+ prompt,
117+ inputFiles : attachmentFiles ,
118+ n : 1 ,
119+ size : this . options . generation . outputSize ,
120+ }
121+ )
122+ } catch ( e : any ) {
123+ error = `No response from image generation provider: ${ e . message } . Please check your prompt or try again later.` ;
124+ return ;
125+ }
126+
127+ if ( resp . error ) {
128+ console . error ( 'Error generating image' , resp . error ) ;
129+ error = resp . error ;
130+ return ;
131+ }
132+
133+ this . totalCalls ++ ;
134+ this . totalDuration += ( + new Date ( ) - start ) / 1000 ;
135+
136+ return resp . imageURLs [ 0 ]
137+
138+ } )
139+ ) ;
140+ jobs . set ( jobId , { status : "completed" , images, error } ) ;
141+ return { ok : true } ;
142+ } ;
143+
58144 instanceUniqueRepresentation ( pluginOptions : any ) : string {
59145 return `${ pluginOptions . pathColumnName } ` ;
60146 }
@@ -343,81 +429,34 @@ export default class UploadPlugin extends AdminForthPlugin {
343429
344430 server . endpoint ( {
345431 method : 'POST' ,
346- path : `/plugin/${ this . pluginInstanceId } /generate_images ` ,
432+ path : `/plugin/${ this . pluginInstanceId } /create-image-generation-job ` ,
347433 handler : async ( { body, adminUser, headers } ) => {
348434 const { prompt, recordId } = body ;
349- if ( this . rateLimiter ) {
350- // rate limit
351- // const { error } = RateLimiter.checkRateLimit(
352- // this.pluginInstanceId,
353- // this.options.generation.rateLimit?.limit,
354- // this.adminforth.auth.getClientIp(headers),
355- // );
356- if ( ! await this . rateLimiter . consume ( `${ this . pluginInstanceId } -${ this . adminforth . auth . getClientIp ( headers ) } ` ) ) {
357- return { error : this . options . generation . rateLimit . errorMessage } ;
358- }
359- }
360- let attachmentFiles = [ ] ;
361- if ( this . options . generation . attachFiles ) {
362- // TODO - does it require additional allowed action to check this record id has access to get the image?
363- // or should we mention in docs that user should do validation in method itself
364- const record = await this . adminforth . resource ( this . resourceConfig . resourceId ) . get (
365- [ Filters . EQ ( this . resourceConfig . columns . find ( ( column : any ) => column . primaryKey ) ?. name , recordId ) ]
366- ) ;
367-
368- if ( ! record ) {
369- return { error : `Record with id ${ recordId } not found` } ;
370- }
371-
372- attachmentFiles = await this . options . generation . attachFiles ( { record, adminUser } ) ;
373- // if files is not array, make it array
374- if ( ! Array . isArray ( attachmentFiles ) ) {
375- attachmentFiles = [ attachmentFiles ] ;
376- }
377-
378- }
379-
380- let error : string | undefined = undefined ;
381-
382- const STUB_MODE = false ;
383435
384- const images = await Promise . all (
385- ( new Array ( this . options . generation . countToGenerate ) ) . fill ( 0 ) . map ( async ( ) => {
386- if ( STUB_MODE ) {
387- await new Promise ( ( resolve ) => setTimeout ( resolve , 2000 ) ) ;
388- return `https://picsum.photos/200/300?random=${ Math . floor ( Math . random ( ) * 1000 ) } ` ;
389- }
390- const start = + new Date ( ) ;
391- let resp ;
392- try {
393- resp = await this . options . generation . adapter . generate (
394- {
395- prompt,
396- inputFiles : attachmentFiles ,
397- n : 1 ,
398- size : this . options . generation . outputSize ,
399- }
400- )
401- } catch ( e : any ) {
402- error = `No response from image generation provider: ${ e . message } . Please check your prompt or try again later.` ;
403- return ;
404- }
405-
406- if ( resp . error ) {
407- console . error ( 'Error generating image' , resp . error ) ;
408- error = resp . error ;
409- return ;
410- }
436+ const jobId = randomUUID ( ) ;
437+ jobs . set ( jobId , { status : "in_progress" } ) ;
411438
412- this . totalCalls ++ ;
413- this . totalDuration += ( + new Date ( ) - start ) / 1000 ;
414-
415- return resp . imageURLs [ 0 ]
439+ this . generateImages ( jobId , prompt , recordId , adminUser , headers ) ;
440+ setTimeout ( ( ) => jobs . delete ( jobId ) , 1_800_000 ) ;
441+ setTimeout ( ( ) => { jobs . set ( jobId , { status : "timeout" } ) ; } , 300_000 ) ;
416442
417- } )
418- ) ;
443+ return { ok : true , jobId } ;
444+ }
445+ } ) ;
419446
420- return { error, images } ;
447+ server . endpoint ( {
448+ method : 'POST' ,
449+ path : `/plugin/${ this . pluginInstanceId } /get-image-generation-job-status` ,
450+ handler : async ( { body, adminUser, headers } ) => {
451+ const jobId = body . jobId ;
452+ if ( ! jobId ) {
453+ return { error : "Can't find job id" } ;
454+ }
455+ const job = jobs . get ( jobId ) ;
456+ if ( ! job ) {
457+ return { error : "Job not found" } ;
458+ }
459+ return { ok : true , job } ;
421460 }
422461 } ) ;
423462
@@ -479,5 +518,6 @@ export default class UploadPlugin extends AdminForthPlugin {
479518 } ) ;
480519
481520 }
521+
482522
483523}
0 commit comments