Map Image Share Prototype
Created this map image sharing prototype source here using various technologies
Components used
- Nodejs (Hapijs framework): To handle the uploads and reading of data
- DataStore (AirTable spreadsheet): Picked this option because AirTable has numerous options with images, graphics, and sharing
- Map (Mapbox)
- Digital Ocean file storage (S3 compatible library): This is to do the temporary upload of the image file location that AirTable requires a link to. Using presigned expiring links and code has full compatibility with S3
General approach
- Create the AirTable with appropriate columns, sharing and the API key
- Setup Digital Ocean spaces to work with the temporary image files for linking into AirTable
- Merge all this processing code together with Nodejs (Hapijs framework) running on a boosted glitch instance. This all is to create a testable API that runs OpenAPI for validating methods prior to work on the map interface.
- Adjust some of the Mapbox code for clicks, clustering, and preview and upload rendering
Image upload details
- Create the Document Object Model element as an img tag with the source content being the file encoded as base64 image
- Upload this received content from hapijs to S3 and generate a presigned link to send to AirTable
Relevant code
const s3 = require("@aws-sdk/client-s3");
const s3Link = require("@aws-sdk/s3-request-presigner");
const crypto = require("crypto");
const s3Client = new s3.S3Client({
endpoint: process.env.spaces_endpoint,
forcePathStyle: false,
region: process.env.spaces_region,
credentials: {
accessKeyId: process.env.spaces_access_id,
secretAccessKey: process.env.spaces_secret,
},
});
function getUploadParameters(inputData) {
const base64Data = new Buffer.from(
inputData.replace(/^data:image\/\w+;base64,/, ""),
"base64"
);
const type = inputData.split(";")[0].split("/")[1];
const params = {
Bucket: process.env.spaces_bucket,
Key: `${crypto.randomUUID()}.${type}`,
Body: base64Data,
ACL: "private",
ContentEncoding: "base64",
ContentType: `image/${type}`,
Metadata: {
"x-amz-meta-my-key": "Upload File",
},
};
return params;
}
module.exports = {
uploadImage: async function (fileData) {
const params = getUploadParameters(fileData);
const cmd = new s3.PutObjectCommand(params);
const data = await s3Client.send(cmd);
console.log(`Successfully uploaded object: ${params.Bucket}/${params.Key}`);
const getObjectParams = {
Bucket: params.Bucket,
Key: params.Key,
};
const command = new s3.GetObjectCommand(getObjectParams);
const shareUrl = await s3Link.getSignedUrl(s3Client, command, {
expiresIn: process.env.spaces_expires_in,
});
console.log(shareUrl);
return shareUrl;
},
};
and the hapijs intermediate handler code
const Joi = require("joi");
const appHelper = require("../src/helpers");
const uploadHelper = require("../src/s3uploader");
module.exports = [
{
method: "POST",
path: "/upload",
options: {
description: "Upload a file",
notes: "Uploads a file",
tags: ["api", "Upload"],
validate: {
failAction: async (request, h, err) => {
// During development, log and respond with the full error.
console.log(err);
throw err;
},
payload: Joi.object({
fileData: Joi.string()
.required()
.default(
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAD0lEQVR42mNkwAIYh7IgAAVVAAuInjI5AAAAAElFTkSuQmCC"
)
.description("Base 64 Image bytes"),
}),
},
},
handler: async (request, h) => {
const input = request.payload;
return await appHelper.GeneralErrorHandlerFn(async () => {
return {
shareUrl: await uploadHelper.uploadImage(input.fileData),
};
});
},
},
];
relevant upload handling code in the html map page
// Because the FileReader looks for an onload event async it is problematic
const toBase64 = (file) => new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
});
document
.getElementById("savePhoto")
.addEventListener("click", async (e) => {
const currentText = document.getElementById("photoText").value || "";
if (currentText == "") {
alert("Photo description required");
return;
}
const position = photoMarker.getLngLat() || null;
if (position == null) {
alert(
"You must place a marker that corresponds to the photo location"
);
return;
}
const base64Data = document.getElementById(currentFiles[0].name).src;
const photoData = { fileData: base64Data };
const savePhotoUrl = `${window.location.href}upload`;
const savePhotoResponse = await fetch(savePhotoUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(photoData),
});
const shareablePhoto = await savePhotoResponse.json();
const shareUrl = shareablePhoto.shareUrl || "";
if (shareUrl == "") {
return;
}
const newRecord = {
photo: shareUrl,
publicText: currentText,
latitude: position.lat.toFixed(6),
longitude: position.lng.toFixed(6),
};
const saveNewRecord = await fetch(
`${window.location.href}locations`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(newRecord),
}
);
const saveNewRecordResponse = await saveNewRecord.json();
console.log(saveNewRecordResponse);
location.reload();
});