Cloud Code to Upload File (GeoTiff with properties) to add to Parent Object

What is the best practice for Cloud Functions for this scenario, I have two possible scenarios (or maybe you can see another)

I’d like to make an API that receives/upload a file (.tif with geodata) and extract properties from the file and add the URL returned and the extracted properties to a Parent Parse Object.

Potential Solution 1: Upload the tiff file directly from the Client App (without Cloud Code) and get the extents of the GeoTiff in FileTrigger’s afterSaveFile

Parent Class:

  • pointer to uploadedTif: String(URL)
  • tif_properties: Object (JSON data)

An issue with this, is that User Upload other types of files, so I assume I can check if (file extension == ‘tif’) prior to extracting the GeoTiff extents.

I am not sure where to put this in Cloud Code, since I cannot call the existing hello function which is in cloud/functions.js:

Parse.Cloud.define('hello', (req) => {
    return "Hello from Simple Cloud Code :)";
});

Making an API call to hello:

curl "http://localhost:1337/1/functions/hello" \    // also tried http://localhost:1337/1/hello & http://localhost:1337/hello as unauthorized error.
     -H 'X-Parse-Application-Id: XYZ' \
     -H 'X-Parse-Rest-Api-Key: ABC' \
     -H 'Content-Type: application/json'

I get back: {"error":"unauthorized"}

Potential Solution 2: POST a JSON object (with the fileData as one of the JSON properties)

  • Users need to Upload a File (GeoTiff file).
  • GeoTiff files have special properties called extents within it (that are extracted with an npm called geotiff-extents and other packages),
  • When the File finishes uploading on Cloud Code, the library extracts these properties (code below)
  • The URL of the recently uploaded .tif file is then a Pointer (1 to 1) to a Parent Object (a class object on Parse)
  • The extracted properties of the tiff file are then added as a property object of the Parent class obj

The issue I am facing is that I cannot seem to Post to my cloud function a JSON and one of the properties is a File data. I’m fairly confident that the issue stems from the header Content-Type
If I set Content-Type as application/json the req.body only shows the properties without the fileData
If I set Content-Type as multipart/form-data the req.body is {}

Cloud Code:
app.js

var express = require('express'),
fs = require('fs'),
geotiff = require('geotiff'),
epsg = require('epsg-to-proj'),
extents = require('geotiff-extents');
 
var app = express();

app.use(express.json({limit: '20mb'}))
app.use(express.urlencoded({limit: '20mb', extended: true }))

app.get('/hello-advanced', (req, res) => {
    res.send("Hello from Advanced Cloud Code");
});

app.post('/uploadExpandTiff', (req, res) => {
    var result = {};
    console.info("-- Start --");
    console.info("req.body " + JSON.stringify(req.body)); 
    if (req.body.file) {
        // 1- Get raw file binary data
        var fileData = fs.createReadStream(req.body.file);
        result.fileAdded = true;
        // 2- Parse tif & get GeoBounds
        fs.readFile(req.body.file, function (err, data) {
            if (err) throw err;
            var dataArray = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
            var im = geotiff.parse(dataArray).getImage();
            var fd = im.getFileDirectory();
            var gk = im.getGeoKeys();
            // extract .tif Exif info & generate coordinate bounds
            var boundsCoords = extents({
                tiePoint: fd.ModelTiepoint,
                pixelScale: fd.ModelPixelScale,
                width: fd.ImageWidth,
                height: fd.ImageLength,
                proj: require('proj4'),
                from: epsg[gk.ProjectedCSTypeGeoKey || gk.GeographicTypeGeoKey],
                to: epsg[4326]
            });
            result.tifBounds = true;
            console.info(".tif Properties:\n" + JSON.stringify(boundsCoords));

            // return results 
            res.send(result);

            // 3- TODO Upload .tif to parse
            // 4- TODO Update Parent classname to have a Pointer to geotiff file & add properties to Parent classname with the extents properties
            
        });
    } else {
        result.fileAdded = false;
        result.error = "Error: No File data Found";
        console.info("Error: No req.body.file Found " + JSON.stringify(req.body));
        res.send(result);
    }
});

So I’m blocked on both paths. Using afterSaveFile looks more promising & a cleaner solution.

A third solution could be sending the .tif file content to a cloud code function. For the first solutions problem, what is the endpoint that your parse server is mounted on? would you mind to share your parse server options? For the second solution, what is the error that you have?

Sure - here is my server settings:

const api = new ParseServer({
    databaseURI: databaseUri || 'mongodb://localhost:27017/dev',
    appId: process.env.APP_ID || 'myAppId',
    masterKey: process.env.MASTER_KEY || 'masterKey', // Add your master key here. Keep it secret!
    serverURL: process.env.SERVER_URL || `http://localhost:${port}${mountPath}`,
    cloud: process.env.CLOUD_CODE_MAIN || 'cloud/main.js',
    liveQuery: {
        classNames: [] // List of classes to support for query subscriptions example: [ 'Posts', 'Comments' ]
    }
});

When I make a call like so:

curl "http://localhost:1337/1/functions/hello" \
         -H 'X-Parse-Application-Id: xyz' \
         -H 'X-Parse-Rest-Api-Key: abc' \
         -H 'Content-Type: application/json'

In functions.js:

Parse.Cloud.define('hello', (req) => {
    return "Hello from Simple Cloud Code :)";
});

Even changing the URL to /1/functions/hello or /functions/hello or /hello result in the this error:

{"error":"unauthorized"}

You mentioned a good solution would be to upload the file to the Cloud Code (I would need to pass with the fileData some other data, like the Parent Object Id, and the User SessionToken. Uploading the fileData and other properties, like so:

curl -X "POST" "http://localhost:1337/uploadExpandTiff" \
     -H 'X-Parse-Application-Id: abc' \
     -H 'X-Parse-Rest-Api-Key: xyz' \
     -H 'Content-Type: multipart/form-data' \
     -d $'{
  "sessionToken": "r:xyz",
  "fileName": "someName.tif",
  "file": _fileData_,
  "fieldObjectId": _someObjectId_
}'

Works making the call with this URL since the method is in app.js, however passing a JSON object with header: Content-Type: application/json passes all the properties, except for the fileData.
Using Content-Type: multipart/form-data the req.body is {}

It looks you are initializing Parse Server without rest api key but you are passing it in your call. Could you please remove it from your curl command and try again?

So I’ve decided to use multer which is very handy to handle multipart/form-data; content-type.

I seem to have solved most of my issues, however, this may be straight forward to some, I can’t seem to send the file data in the proper format (I assume Parse wants a base64 string). Its the way I’m using encode_image

The error occurs below in //3- Upload .tif to Parse!
I’m getting: TypeError: Cannot create a Parse.File with that data.

app.post('/uploadTif', upload.single('file'), function (req, res, next) {
    var result = {};
    if (req.file && req.file.mimetype == "image/tiff") {
        result.fileAdded = true;
        var img = fs.readFileSync(req.file.path);
        var encode_image = img.toString('base64'); // also tried: fs.readFileSync(req.file.path, {encoding: 'base64'});
        fs.readFile(req.file.path, function (err, data) {
            if (err) {
                result.readFileError = err;
                throw err;
            }
            var dataArray = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
            var im = geotiff.parse(dataArray).getImage();
            var fd = im.getFileDirectory();
            var gk = im.getGeoKeys();
            // extract .tif Exif info & generate coordinate bounds
            var boundsCoords = extents({
                tiePoint: fd.ModelTiepoint,
                pixelScale: fd.ModelPixelScale,
                width: fd.ImageWidth,
                height: fd.ImageLength,
                proj: require('proj4'),
                from: epsg[gk.ProjectedCSTypeGeoKey || gk.GeographicTypeGeoKey],
                to: epsg[4326]
            });
            console.info(".tif Properties:\n" + JSON.stringify(boundsCoords));

            //3- Upload .tif to Parse!
            const parseFile = new Parse.File(req.body.fileName, encode_image, "image/tiff");
            // Throws error: TypeError: Cannot create a Parse.File with that data.

            parseFile.save().then(function (_e) {
                // Is this how to get confirmation from Parse that the objectId has been created?
                console.info("Parse File has been saved " + JSON.stringify(_e));
                // The file has been saved to Parse.
            }, function (error) {
                console.info("Error Parse File has NOT been saved " + JSON.stringify(error));
                // The file either could not be read, or could not be saved to Parse.
            });
            res.send(result);
        });
    } else {
        result.fileAdded = false;
        result.fileMime = req.file.mimetype;
        result.error = "Error: No File data Found";
        console.info("Error: No req.file Found OR wrong file type (mimeType should be image/tiff): " + req.file.mimetype);
        res.send(result);
    }
});

SOLVED:
Converting the file data to a base64 String: const encodedTif = fs.readFileSync(req.file.path, {encoding: 'base64'});

const parseFile = new Parse.File(req.body.fileName, { base64: encodedTif}, "image/tiff");