start the feature of replacing an existing image
This commit is contained in:
@@ -37,6 +37,9 @@ JWT=`node -e "$CODE"`; curl -X POST -H "Authorization: Bearer $JWT" -F "image=@.
|
|||||||
JWT=`node -e "$CODE"`; curl -X DELETE -H "Authorization: Bearer $JWT" http://localhost:3001/image/https%3A%2F%2Fgifts-image-test.s3.amazonaws.com%2F4599145c3a8792a678f458747f2d8512c680e8680bf5563c35b06cd770051ed2.png
|
JWT=`node -e "$CODE"`; curl -X DELETE -H "Authorization: Bearer $JWT" http://localhost:3001/image/https%3A%2F%2Fgifts-image-test.s3.amazonaws.com%2F4599145c3a8792a678f458747f2d8512c680e8680bf5563c35b06cd770051ed2.png
|
||||||
```
|
```
|
||||||
|
|
||||||
|
https://github.com/box/Makefile.test
|
||||||
|
`make -C test -j`
|
||||||
|
|
||||||
## deploy to prod first time
|
## deploy to prod first time
|
||||||
|
|
||||||
* Do the necessary steps from "setup" above, or `docker build` it.
|
* Do the necessary steps from "setup" above, or `docker build` it.
|
||||||
@@ -51,4 +54,4 @@ JWT=`node -e "$CODE"`; curl -X DELETE -H "Authorization: Bearer $JWT" http://loc
|
|||||||
|
|
||||||
## deploy to prod subsequent times
|
## deploy to prod subsequent times
|
||||||
|
|
||||||
* Update version in server.js file. Add CHANGELOG.md entry.
|
* Update version in server.js 'ping' endpoint. Add CHANGELOG.md entry.
|
||||||
|
|||||||
@@ -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 > 10485760) { // 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');
|
|
||||||
hashSum.update(data);
|
|
||||||
const hashHex = hashSum.digest('hex');
|
|
||||||
|
|
||||||
const fileName = hashHex + path.extname(reqFile.originalname);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
let finalFileName;
|
||||||
|
if (req.body.fileName) {
|
||||||
|
finalFileName = req.body.fileName;
|
||||||
|
|
||||||
// look to see if this image already exists
|
// check if the file to replace was sent by this user earlier
|
||||||
const imageUrl = await new Promise((resolve, reject) => {
|
const didForOriginal = await new Promise((resolve, reject) => {
|
||||||
db.get(
|
db.get(
|
||||||
'SELECT url FROM image WHERE final_file = ? and did = ?',
|
'SELECT did FROM image WHERE did = ? and final_file = ?'
|
||||||
[ fileName, issuerDid ],
|
[ finalFileName, issuerDid ],
|
||||||
(dbErr, row) => {
|
(dbErr, row) => {
|
||||||
if (dbErr) {
|
if (dbErr) {
|
||||||
console.error(currentDate, 'Error getting image for user from database:', dbErr)
|
console.error(currentDate, 'Error getting image for user from database:', dbErr)
|
||||||
// continue anyway
|
reject(dbErr);
|
||||||
|
}
|
||||||
|
resolve(row?.did);
|
||||||
}
|
}
|
||||||
resolve(row?.url);
|
);
|
||||||
}
|
});
|
||||||
);
|
if (!didForOriginal) {
|
||||||
});
|
return res.status(404).send(JSON.stringify({ success: false, message: 'No image entry found for user ' + issuerDid + ' for file ' + finalFileName }));
|
||||||
if (imageUrl) {
|
}
|
||||||
return res.status(201).send(JSON.stringify({ success: true, url: imageUrl, message: 'This image already existed.' }));
|
|
||||||
|
// 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.' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// might as well remove from DB and add it all back again later
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
db.run(
|
||||||
|
'DELETE FROM image where did = ? and final_file = ?',
|
||||||
|
[ issuerDid, finalFileName ],
|
||||||
|
(dbErr) => {
|
||||||
|
if (dbErr) {
|
||||||
|
const currentDate = new Date().toISOString();
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const hashSum = crypto.createHash('sha256');
|
||||||
|
hashSum.update(data);
|
||||||
|
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}/${finalFileName}`;
|
||||||
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,
|
finalFileName,
|
||||||
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: finalFileName, // 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 otherUserImage = await new Promise((resolve, reject) => {
|
const othersWhoSentImage = 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 (!otherUserImage) {
|
if (!othersWhoSentImage) {
|
||||||
// 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();
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
ALTER TABLE image ADD COLUMN is_replacement BOOLEAN NOT NULL DEFAULT FALSE;
|
||||||
Executable
+18
@@ -0,0 +1,18 @@
|
|||||||
|
# requires node & curl & jq
|
||||||
|
|
||||||
|
HOST=http://localhost:3002
|
||||||
|
|
||||||
|
CODE='OWNER_DID="did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F"; OWNER_PRIVATE_KEY_HEX="2b6472c026ec2aa2c4235c994a63868fc9212d18b58f6cbfe861b52e71330f5b"; didJwt = require("did-jwt"); didJwt.createJWT({ exp: Math.floor(Date.now() / 1000) + 60, iat: Math.floor(Date.now() / 1000), iss: OWNER_DID }, { issuer: OWNER_DID, signer: didJwt.SimpleSigner(OWNER_PRIVATE_KEY_HEX) }).then(console.log)'
|
||||||
|
|
||||||
|
# exit as soon as anything fails
|
||||||
|
set -e
|
||||||
|
|
||||||
|
JWT=$(node -e "$CODE")
|
||||||
|
RESULT=$(curl -X POST -H "Authorization: Bearer $JWT" -F "image=@test1.png" $HOST/image)
|
||||||
|
echo $RESULT
|
||||||
|
URL=$(echo $RESULT | jq -r '.url')
|
||||||
|
|
||||||
|
STATUS_CODE=$(curl -o out-test1.png -w "%{http_code}" $URL);
|
||||||
|
if [ $STATUS_CODE -ne 200 ]; then
|
||||||
|
echo "File is not accessible, received status code: $STATUS_CODE";
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user