@ -18,7 +18,7 @@ app.use(cors());
const port = process . env . PORT || 3002 ;
const port = process . env . PORT || 3002 ;
// file name also referenced in flyway.conf and potentially in .env files or in environment variables
// file name also referenced in flyway.conf and potentially in .env files or in environment variables
const dbFile = process . env . SQLITE_FILE || './image.sqlite' ;
const dbFile = process . env . SQLITE_FILE || './image-db .sqlite' ;
const bucketName = process . env . S3_BUCKET_NAME || 'gifts-image-test' ;
const bucketName = process . env . S3_BUCKET_NAME || 'gifts-image-test' ;
const imageServer = process . env . DOWNLOAD_IMAGE_SERVER || 'test-image.timesafari.app' ;
const imageServer = process . env . DOWNLOAD_IMAGE_SERVER || 'test-image.timesafari.app' ;
@ -54,7 +54,7 @@ const uploadDir = 'uploads';
const uploadMulter = multer ( { dest : uploadDir + '/' } ) ;
const uploadMulter = multer ( { dest : uploadDir + '/' } ) ;
app . get ( '/ping' , async ( req , res ) => {
app . get ( '/ping' , async ( req , res ) => {
res . send ( 'pong v1.0 .0' ) ;
res . send ( 'pong - v 1.1 .0' ) ; // version
} ) ;
} ) ;
app . get ( '/image-limits' , async ( req , res ) => {
app . get ( '/image-limits' , async ( req , res ) => {
@ -70,13 +70,21 @@ app.get('/image-limits', async (req, res) => {
} ) ) ;
} ) ) ;
} ) ;
} ) ;
// POST endpoint to upload an image
/ * *
* POST endpoint to upload an image
*
* Send as FormData , with :
* - "image" file Blob
* - "claimType" ( optional , eg . "GiveAction" , "PlanAction" , "profile" )
* - "handleId" ( optional )
* - "fileName" ( optional , if you want to replace an previous image )
* /
app . post ( '/image' , uploadMulter . single ( 'image' ) , async ( req , res ) => {
app . post ( '/image' , uploadMulter . single ( 'image' ) , async ( req , res ) => {
const reqFile = req . file ;
const reqFile = req . file ;
if ( reqFile == null ) {
if ( reqFile == null ) {
return res . status ( 400 ) . send ( JSON . stringify ( { success : false , message : 'No file uploaded.' } ) ) ;
return res . status ( 400 ) . send ( JSON . stringify ( { success : false , message : 'No file uploaded.' } ) ) ;
}
}
if ( reqFile . size > 10000000 ) {
if ( reqFile . size > 1048576 0 ) { // 10MB
fs . rm ( reqFile . path , ( err ) => {
fs . rm ( reqFile . path , ( err ) => {
if ( err ) {
if ( err ) {
console . error ( "Error deleting too-large temp file" , reqFile . path , "with error (but continuing):" , err ) ;
console . error ( "Error deleting too-large temp file" , reqFile . path , "with error (but continuing):" , err ) ;
@ -102,41 +110,115 @@ app.post('/image', uploadMulter.single('image'), async (req, res) => {
fs . readFile ( reqFile . path , async ( err , data ) => {
fs . readFile ( reqFile . path , async ( err , data ) => {
if ( err ) throw err ; // Handle error
if ( err ) throw err ; // Handle error
const hashSum = crypto . createHash ( 'sha256' ) ;
try {
hashSum . update ( data ) ;
let finalFileName ;
const hashHex = hashSum . digest ( 'hex' ) ;
if ( req . body . fileName ) {
finalFileName = req . body . fileName ;
// check if the file to replace was sent by this user earlier
const didForOriginal = await new Promise ( ( resolve , reject ) => {
db . get (
'SELECT did FROM image WHERE did = ? and final_file = ?'
[ finalFileName , issuerDid ] ,
( dbErr , row ) => {
if ( dbErr ) {
console . error ( currentDate , 'Error getting image for user from database:' , dbErr )
reject ( dbErr ) ;
}
resolve ( row ? . did ) ;
}
) ;
} ) ;
if ( ! didForOriginal ) {
return res . status ( 404 ) . send ( JSON . stringify ( { success : false , message : 'No image entry found for user ' + issuerDid + ' for file ' + finalFileName } ) ) ;
}
const fileName = hashHex + path . extname ( reqFile . originalname ) ;
// check if any other user recorded this image
const othersWhoSentImage = await new Promise ( ( resolve , reject ) => {
db . get (
'SELECT did FROM image WHERE final_file = ? and did != ?' ,
[ url , issuerDid ] ,
( dbErr , row ) => {
if ( dbErr ) {
console . error ( currentDate , 'Error getting image for other users from database:' , dbErr )
reject ( dbErr ) ;
}
resolve ( row ? . did ) ;
}
) ;
} ) ;
if ( othersWhoSentImage ) {
return res . status ( 400 ) . send ( JSON . stringify ( { success : false , message : 'Other users have also saved this image so it cannot be modified. You will have to replace your own references.' } ) ) ;
}
try {
// remove from S3
const params = {
Bucket : bucketName , // S3 Bucket name
Key : finalFileName , // File name to use in S3
} ;
const command = new DeleteObjectCommand ( params ) ;
const response = await s3Client . send ( command ) ;
if ( response . $metadata . httpStatusCode !== 200
&& response . $metadata . httpStatusCode !== 202
&& response . $metadata . httpStatusCode !== 204 ) {
const errorTime = new Date ( ) . toISOString ( ) ;
console . error ( errorTime , "Error deleting from S3 with bad HTTP status, with metadata:" , response . $metadata ) ;
return res . status ( 500 ) . send ( JSON . stringify ( {
success : false ,
message : "Got bad status of " + response . $metadata . httpStatusCode + " from S3. See server logs at " + errorTime
} ) ) ;
}
// look to see if this image already exists
// might as well remove from DB and add it all back again later
const imageUrl = await new Promise ( ( resolve , reject ) => {
await new Promise ( ( resolve , reject ) => {
db . get (
db . run (
'SELECT url FROM image WHERE final_file = ? and did = ?' ,
'DELETE FROM image where did = ? and final_file = ?' ,
[ fileName , issuerDid ] ,
[ issuerDid , finalFileName ] ,
( dbErr , row ) => {
( dbErr ) => {
if ( dbErr ) {
if ( dbErr ) {
console . error ( currentDate , 'Error getting image for user from database:' , dbErr )
const currentDate = new Date ( ) . toISOString ( ) ;
// continue anyway
console . error ( currentDate , "Error deleting record by" , issuerDid , "named" , finalFileName , "from database:" , dbErr ) ;
// don't continue because then we'll have storage we cannot track (and potentially limit)
reject ( dbErr ) ;
}
resolve ( ) ;
}
}
resolve ( row ? . url ) ;
) ;
}
} ) ;
) ;
} else {
} ) ;
const hashSum = crypto . createHash ( 'sha256' ) ;
if ( imageUrl ) {
hashSum . update ( data ) ;
return res . status ( 201 ) . send ( JSON . stringify ( { success : true , url : imageUrl , message : 'This image already existed.' } ) ) ;
const hashHex = hashSum . digest ( 'hex' ) ;
finalFileName = hashHex + path . extname ( reqFile . originalname ) ;
// look to see if this image already exists for this user
const imageUrl = await new Promise ( ( resolve , reject ) => {
db . get (
'SELECT url FROM image WHERE final_file = ? and did = ?' ,
[ finalFileName , issuerDid ] ,
( dbErr , row ) => {
if ( dbErr ) {
console . error ( currentDate , 'Error getting image for user from database:' , dbErr )
// continue anyway
}
resolve ( row ? . url ) ;
}
) ;
} ) ;
if ( imageUrl ) {
return res . status ( 201 ) . send ( JSON . stringify ( { success : true , url : imageUrl , message : 'This image already existed.' } ) ) ;
}
}
}
// record the upload in the database
// record the upload in the database
const currentDate = new Date ( ) . toISOString ( ) ;
const currentDate = new Date ( ) . toISOString ( ) ;
const localFile = reqFile . path . startsWith ( uploadDir + '/' ) ? reqFile . path . substring ( uploadDir . length + 1 ) : reqFile . path ;
const localFile = reqFile . path . startsWith ( uploadDir + '/' ) ? reqFile . path . substring ( uploadDir . length + 1 ) : reqFile . path ;
const finalUrl = ` https:// ${ imageServer } / ${ fileName } ` ;
const finalUrl = ` https:// ${ imageServer } / ${ finalFi leName } ` ;
const claimType = req . body . claimType ;
const claimType = req . body . claimType ;
const handleId = req . body . handleId ;
const handleId = req . body . handleId ;
await new Promise ( ( resolve , reject ) => {
await new Promise ( ( resolve , reject ) => {
db . run (
db . run (
'INSERT INTO image (time, did, claim_type, handle_id, local_file, size, final_file, mime_type, url) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)' ,
'INSERT INTO image (time, did, claim_type, handle_id, local_file, size, final_file, mime_type, url, is_replacement ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' ,
[
[
currentDate ,
currentDate ,
issuerDid ,
issuerDid ,
@ -144,9 +226,10 @@ app.post('/image', uploadMulter.single('image'), async (req, res) => {
handleId ,
handleId ,
localFile ,
localFile ,
reqFile . size ,
reqFile . size ,
fileName ,
finalFi leName ,
reqFile . mimetype ,
reqFile . mimetype ,
finalUrl
finalUrl ,
! ! req . body . fileName ,
] ,
] ,
( dbErr ) => {
( dbErr ) => {
if ( dbErr ) {
if ( dbErr ) {
@ -164,7 +247,7 @@ app.post('/image', uploadMulter.single('image'), async (req, res) => {
Body : data ,
Body : data ,
Bucket : bucketName , // S3 Bucket name
Bucket : bucketName , // S3 Bucket name
ContentType : reqFile . mimetype , // File content type
ContentType : reqFile . mimetype , // File content type
Key : fileName , // File name to use in S3
Key : finalFi leName , // File name to use in S3
} ;
} ;
if ( process . env . S3_SET_ACL === 'true' ) {
if ( process . env . S3_SET_ACL === 'true' ) {
params . ACL = 'public-read' ;
params . ACL = 'public-read' ;
@ -186,7 +269,7 @@ app.post('/image', uploadMulter.single('image'), async (req, res) => {
} ) ;
} ) ;
// AWS URL: https://gifts-image-test.s3.amazonaws.com/gifts-image-test/FILE
// AWS URL: https://gifts-image-test.s3.amazonaws.com/gifts-image-test/FILE
// American Cloud URL: https://a2-west.americancloud.com/TENANT:giftsimagetest/FILE
// American Cloud URL: https://a2-west.americancloud.com/TENANT:giftsimagetest/FILE
return res . status ( 200 ) . send ( JSON . stringify ( { success : true , url : finalUrl } ) ) ;
return res . status ( 201 ) . send ( JSON . stringify ( { success : true , url : finalUrl } ) ) ;
}
}
} catch ( uploadError ) {
} catch ( uploadError ) {
const errorTime = new Date ( ) . toISOString ( ) ;
const errorTime = new Date ( ) . toISOString ( ) ;
@ -244,7 +327,7 @@ app.delete('/image/:url', async (req, res) => {
}
}
// check if any other user recorded this image
// check if any other user recorded this image
const otherUser Image = await new Promise ( ( resolve , reject ) => {
const othersWhoSent Image = await new Promise ( ( resolve , reject ) => {
db . get (
db . get (
'SELECT did FROM image WHERE url = ? and did != ?' ,
'SELECT did FROM image WHERE url = ? and did != ?' ,
[ url , issuerDid ] ,
[ url , issuerDid ] ,
@ -258,7 +341,7 @@ app.delete('/image/:url', async (req, res) => {
) ;
) ;
} ) ;
} ) ;
if ( ! otherUser Image ) {
if ( ! othersWhoSent Image ) {
// remove from S3 since nobody else recorded it
// remove from S3 since nobody else recorded it
const params = {
const params = {
Bucket : bucketName , // S3 Bucket name
Bucket : bucketName , // S3 Bucket name
@ -286,8 +369,8 @@ app.delete('/image/:url', async (req, res) => {
( dbErr ) => {
( dbErr ) => {
if ( dbErr ) {
if ( dbErr ) {
const currentDate = new Date ( ) . toISOString ( ) ;
const currentDate = new Date ( ) . toISOString ( ) ;
console . error ( currentDate , "Error deleting record from " , issuerDid , "into database:" , dbErr ) ;
console . error ( currentDate , "Error deleting record by " , issuerDid , "with URL" , url , "from database:" , dbErr ) ;
// don't continue because then we'll have storage we cannot track (and potentially limit)
// we'll let them know that it's not all cleaned up so they can try again
reject ( dbErr ) ;
reject ( dbErr ) ;
}
}
resolve ( ) ;
resolve ( ) ;