@@ -586,18 +586,14 @@ export default class ProjectFactory {
586586 }
587587 }
588588
589- static async createManifestFromImage ( imageURL , projectLabel , creator ) {
590- if ( ! imageURL ) {
589+ static async createManifestFromImage ( imageURLs , projectLabel , creator ) {
590+ if ( ! imageURLs || imageURLs . length === 0 ) {
591591 throw {
592592 status : 404 ,
593593 message : "No image found. Cannot process further."
594594 }
595595 }
596596
597- let isIIFImage = false
598- let IIIFServiceParts = imageURL . split ( '/' ) . reverse ( )
599- let IIIFServiceJson = null
600-
601597 function isValidIIIFRegion ( region ) {
602598 return (
603599 region === "full" ||
@@ -627,7 +623,7 @@ export default class ProjectFactory {
627623 function isValidIIIFRotation ( rotation ) {
628624 return (
629625 / ^ \d + ( \. \d + ) ? $ / . test ( rotation ) ||
630- size . startsWith ( "!" ) && / ^ \d + ( \. \d + ) ? $ / . test ( rotation )
626+ rotation . startsWith ( "!" ) && / ^ \d + ( \. \d + ) ? $ / . test ( rotation )
631627 )
632628 }
633629
@@ -640,89 +636,97 @@ export default class ProjectFactory {
640636 )
641637 }
642638
643- let IIIFServiceURL = IIIFServiceParts . slice ( 4 ) . reverse ( ) . join ( "/" )
644-
645- if ( isValidIIIFQuality ( IIIFServiceParts [ 0 ] . split ( "." ) [ 0 ] ) && isValidIIIFRotation ( IIIFServiceParts [ 1 ] ) && isValidIIIFSize ( IIIFServiceParts [ 2 ] ) && isValidIIIFRegion ( IIIFServiceParts [ 3 ] ) ) {
646- await fetch ( `${ IIIFServiceURL } /info.json` )
647- . then ( response => {
648- if ( ! response . ok ) {
649- throw new Error ( `Failed to fetch IIIF info: ${ response . statusText } ` )
650- }
651- return response . json ( )
652- } )
653- . then ( info => {
654- if ( info ?. protocol === "http://iiif.io/api/image" ) {
655- isIIFImage = true
656- IIIFServiceJson = info
657- }
658- } )
659- . catch ( err => {
660- console . error ( "Error fetching IIIF info:" , err . message )
661- throw {
662- status : 500 ,
663- message : "Failed to fetch IIIF info"
664- }
665- } )
666- }
667-
668- const _id = database . reserveId ( )
669639 const now = Date . now ( ) . toString ( ) . slice ( - 6 )
670640 const label = projectLabel ?? now
671- const dimensions = await this . getImageDimensions ( imageURL )
672-
673- const canvasLayout = {
674- id : `${ process . env . TPENSTATIC } /${ _id } /canvas-1.json` ,
675- type : "Canvas" ,
676- label : { "none" : [ `${ label } Page 1` ] } ,
677- width : dimensions . width ,
678- height : dimensions . height ,
679- items : [
680- {
681- id : `${ process . env . TPENSTATIC } /${ _id } /contentPage.json` ,
682- type : "AnnotationPage" ,
683- items : [
684- {
685- id : `${ process . env . TPENSTATIC } /${ _id } /content.json` ,
686- type : "Annotation" ,
687- motivation : "painting" ,
688- body : {
689- id : imageURL ,
690- type : "Image" ,
691- format : mime . lookup ( imageURL ) || "image/jpeg" ,
692- width : dimensions . width ,
693- height : dimensions . height ,
694- ...( isIIFImage && {
695- service : [ {
696- id : IIIFServiceURL ,
697- type : IIIFServiceJson ?. type ,
698- profile : IIIFServiceJson ?. profile ,
699- } ]
700- } )
701- } ,
702- target : `${ process . env . TPENSTATIC } /${ _id } /canvas-1.json`
703- }
704- ]
705- }
706- ] ,
707- creator : await fetchUserAgent ( creator ) ,
708- }
709-
641+ const _id = database . reserveId ( )
642+
710643 const projectManifest = {
711644 "@context" : "http://iiif.io/api/presentation/3/context.json" ,
712645 id : `${ process . env . TPENSTATIC } /${ _id } /manifest.json` ,
713646 type : "Manifest" ,
714647 label : { "none" : [ label ] } ,
715- items : [ canvasLayout ] ,
648+ items : [ ] ,
716649 creator : await fetchUserAgent ( creator ) ,
717650 }
718651
719- const projectCanvas = {
720- "@context" : "http://iiif.io/api/presentation/3/context.json" ,
721- ...canvasLayout
652+ for ( let index = 0 ; index < imageURLs . length ; index ++ ) {
653+ const imageURL = imageURLs [ index ]
654+ let isIIFImage = false
655+ let IIIFServiceParts = imageURL . split ( '/' ) . reverse ( )
656+ let IIIFServiceJson = null
657+ let IIIFServiceURL = IIIFServiceParts . slice ( 4 ) . reverse ( ) . join ( "/" )
658+
659+ if (
660+ isValidIIIFQuality ( IIIFServiceParts [ 0 ] . split ( "." ) [ 0 ] ) &&
661+ isValidIIIFRotation ( IIIFServiceParts [ 1 ] ) &&
662+ isValidIIIFSize ( IIIFServiceParts [ 2 ] ) &&
663+ isValidIIIFRegion ( IIIFServiceParts [ 3 ] )
664+ ) {
665+ try {
666+ const response = await fetch ( `${ IIIFServiceURL } /info.json` )
667+ if ( response . ok ) {
668+ const info = await response . json ( )
669+ if ( info ?. protocol === "http://iiif.io/api/image" ) {
670+ isIIFImage = true
671+ IIIFServiceJson = info
672+ }
673+ } else {
674+ console . warn ( `Failed to fetch IIIF info for image ${ index + 1 } ` )
675+ }
676+ } catch ( err ) {
677+ console . error ( "Error fetching IIIF info:" , err . message )
678+ }
679+ }
680+
681+ const dimensions = await this . getImageDimensions ( imageURL )
682+
683+ const canvasLayout = {
684+ id : `${ process . env . TPENSTATIC } /${ _id } /canvas-${ index + 1 } .json` ,
685+ type : "Canvas" ,
686+ label : { "none" : [ `${ label } Page ${ index + 1 } ` ] } ,
687+ width : dimensions . width ,
688+ height : dimensions . height ,
689+ items : [
690+ {
691+ id : `${ process . env . TPENSTATIC } /${ _id } /contentPage.json` ,
692+ type : "AnnotationPage" ,
693+ items : [
694+ {
695+ id : `${ process . env . TPENSTATIC } /${ _id } /content.json` ,
696+ type : "Annotation" ,
697+ motivation : "painting" ,
698+ body : {
699+ id : imageURL ,
700+ type : "Image" ,
701+ format : mime . lookup ( imageURL ) || "image/jpeg" ,
702+ width : dimensions . width ,
703+ height : dimensions . height ,
704+ ...( isIIFImage && {
705+ service : [ {
706+ id : IIIFServiceURL ,
707+ type : IIIFServiceJson ?. type ,
708+ profile : IIIFServiceJson ?. profile ,
709+ } ]
710+ } )
711+ } ,
712+ target : `${ process . env . TPENSTATIC } /${ _id } /canvas-${ index + 1 } .json`
713+ }
714+ ]
715+ }
716+ ] ,
717+ creator : await fetchUserAgent ( creator ) ,
718+ }
719+
720+ projectManifest . items . push ( canvasLayout )
721+ const projectCanvas = {
722+ "@context" : "http://iiif.io/api/presentation/3/context.json" ,
723+ ...canvasLayout
724+ }
725+
726+ await this . uploadFileToGitHub ( projectCanvas , _id )
722727 }
723728
724729 await this . uploadFileToGitHub ( projectManifest , _id )
725- await this . uploadFileToGitHub ( projectCanvas , _id )
726730
727731 return await ProjectFactory . DBObjectFromImage ( projectManifest , creator )
728732 . then ( async ( project ) => {
@@ -937,22 +941,22 @@ export default class ProjectFactory {
937941 const manifestUrl = `https://api.github.com/repos/${ process . env . REPO_OWNER } /${ process . env . REPO_NAME } /contents/${ projectId } /${ fileName } `
938942 const token = process . env . GITHUB_TOKEN
939943
940- try {
941- let sha = null
942-
943- const getResponse = await fetch ( manifestUrl , {
944+ async function getFileSha ( ) {
945+ const res = await fetch ( manifestUrl , {
944946 headers : {
945947 'Authorization' : `token ${ token } ` ,
946948 'Accept' : 'application/vnd.github.v3+json' ,
947949 } ,
948950 } )
949-
950- if ( getResponse . ok ) {
951- const fileData = await getResponse . json ( )
952- sha = fileData . sha
951+ if ( res . ok ) {
952+ const data = await res . json ( )
953+ return data . sha
953954 }
955+ return null
956+ }
954957
955- const putResponse = await fetch ( manifestUrl , {
958+ async function uploadWithSha ( sha = null ) {
959+ const res = await fetch ( manifestUrl , {
956960 method : 'PUT' ,
957961 headers : {
958962 'Authorization' : `token ${ token } ` ,
@@ -961,19 +965,44 @@ export default class ProjectFactory {
961965 } ,
962966 body : JSON . stringify ( {
963967 message : sha ? `Updated ${ projectId } /${ fileName } ` : `Created ${ projectId } /${ fileName } ` ,
964- content : Buffer . from ( JSON . stringify ( manifest ) ) . toString ( 'base64' ) ,
968+ content : Buffer . from ( JSON . stringify ( manifest , null , 2 ) ) . toString ( 'base64' ) ,
965969 branch : process . env . BRANCH ,
966970 ...( sha && { sha } ) ,
967971 } )
968972 } )
973+ return res
974+ }
975+
976+ async function delay ( ms ) {
977+ return new Promise ( resolve => setTimeout ( resolve , ms ) )
978+ }
979+
980+ try {
981+ let sha = await getFileSha ( )
982+ let response
983+ const maxRetries = 3
969984
970- if ( ! putResponse . ok ) {
971- const errText = await putResponse . text ( )
972- throw new Error ( `GitHub upload failed: ${ putResponse . status } - ${ errText } ` )
985+ for ( let attempt = 1 ; attempt <= maxRetries ; attempt ++ ) {
986+ response = await uploadWithSha ( sha )
987+
988+ if ( response . ok ) break
989+
990+ if ( response . status === 409 ) {
991+ await delay ( 500 * attempt )
992+ sha = await getFileSha ( )
993+ continue
994+ }
995+
996+ const errText = await response . text ( )
997+ throw new Error ( `GitHub upload failed: ${ response . status } - ${ errText } ` )
973998 }
974999
975- return await putResponse . json ( )
1000+ if ( ! response . ok ) {
1001+ const errText = await response . text ( )
1002+ throw new Error ( `GitHub upload failed after ${ maxRetries } attempts: ${ errText } ` )
1003+ }
9761004
1005+ return await response . json ( )
9771006 } catch ( error ) {
9781007 console . error ( `Failed to upload ${ projectId } /${ fileName } :` , error )
9791008 }
0 commit comments