Ceetron Cloud: Add a Send-To-Cloud Button to Your App

../../_images/cloud.png

With CEETRON Envision it is easy to add cloud export support to your application. With a single click of a button the users can share the 3D model they are currently showing in your app with the rest of the world. The model can be shown in any modern web browser without installing any software or plug-in.

To add a Send-To-Cloud button to your app, please follow the description below:

1: Get the upload ID from the user

The user needs to provide his/her unique upload ID to upload the file. The user will need an account on https://cloud.ceetron.com, so you need to send the user either to the https://cloud.ceetron.com/signup page or https://cloud.ceetron.com/login page. From the “My Account” page the user can press the button “Show and copy NN’s Upload ID” to get the upload ID. You should only ask for this once, and then store it somewhere safe in your app.

The Upload ID is a GUID, e.g.: 765ba418-58ea-4c0a-afd1-fb4ff8f410ee

2: Export the model to a VTFx file

Next, we need to create the VTFx file for upload. See: Share Everywhere with VTFx Files and Ceetron Viewers for documentation on how to do that. You should export to a temporary file, which should be deleted after upload.

3: Upload the file to Ceetron Cloud

Use the following REST API call to upload the file to Ceetron:

API: https://cloud.ceetron.com/api/v1/models

Params:

  • uploadId: The users’ Upload ID. See 1.

  • uploadApp: The name of your app.

  • name: The name of the model. This will become the default name in the My Models page of the user on Ceetron Cloud.

Example:

https://cloud.ceetron.com/api/v1/models?uploadId=765ba418-58ea-4c0a-afd1-fb4ff8f410ee&uploadApp=My%20App&name=Demo%20Model

Note

The uploadApp and name parameters needs to be percent encoded to form a valid URI

The upload must be done with a “multipart/form-data” HTTPS POST, where the VTFx is sent as a file with the name “vtfx”.

The header should be:

Content-Disposition: form-data; name=\"vtfx\"; filename=\"upload.vtfx\"
Content-Type: application/octet-stream

See the Demo App (Qt and .NET) for an implementation of this upload protocol.

4: Present the link to the user

../../_images/cloud_viewer.png

Finally, you should present the unique URL to the user so he/she can share it. You might also present a link to https://cloud.ceetron.com/myAccount so the user can see the newly uploaded file.

Security note

1: SSL
You should use SSL when uploading if possible. This will ensure that the communication between your app and the server is encrypted and that all the data is protected.

2: Upload ID
If you choose to store the upload ID in your app (in the registry/settings file), you should encrypt it and not store it in clear-text.

Examples

Both the Qt Demo App and .NET Demo App that comes with CEETRON Envision contain the new “Send-To-Cloud” feature. We encourage you to have a look there to see how to do the upload in Qt and .NET.

There is also a separate example provided in Qt (Examples/Qt/QtSendToCloud) that takes any VTFx file and uploads it to Ceetron Cloud. This small example takes arguments on the command line.

\Examples\Qt\QtSendToCloud\debug>QtSendToCloud.exe -uploadId MY_UPLOAD_ID -uploadApp "My cool app" -caseName "Nice model" filename.vtfx

Note

For the Qt version you will need a version of Qt compiled with SSL support and the OpenSSL DLLs available on the system for this to work. The .NET version should work without any problems.

Qt (4.x and Later) Example

QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
multiPart->setBoundary("boundary_Ceetron89906a4aJn1kvlAG52SFG2AD");

QHttpPart vtfxPart;
vtfxPart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/octet-stream"));
vtfxPart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"vtfx\" filename=\"upload.vtfx\""));

QFile *file = new QFile(tempFilename);
if (!file->open(QIODevice::ReadOnly))
{
    QMessageBox::critical(this, "Upload error", QString("Error opening temporary file %1").arg(tempFilename), QMessageBox::Ok);
    return;
}


vtfxPart.setBodyDevice(file);

file->setParent(multiPart); // we cannot delete the file now, so delete it with the multiPart
multiPart->append(vtfxPart);

QString uploadApp = QUrl::toPercentEncoding("Envision Demo App Qt");

QFileInfo fi(m_fileName);
QString caseName = QUrl::toPercentEncoding(fi.fileName());

QString urlString = QString("https://cloud.ceetron.com/api/v1/models?uploadId=%1&uploadApp=%2&name=%3").arg(uploadId).arg(uploadApp).arg(caseName);

QUrl url(urlString);
QNetworkRequest request(url);

QNetworkAccessManager* manager = new QNetworkAccessManager(this);
QNetworkReply* reply = manager->post(request, multiPart);
multiPart->setParent(reply);

C#/.NET Example

string uploadApp = Uri.EscapeDataString("Envision Demo App .NET");
string caseName = Uri.EscapeDataString("Demo app case");
string urlString = String.Format("https://cloud.ceetron.com/api/v1/models?uploadId={0}&uploadApp={1}&name={2}", uploadId, uploadApp, caseName);

string boundary = "boundary_Ceetron89906a4aJn1kvlAG52SFG2AD";
byte[] boundarybytes = System.Text.Encoding.ASCII.GetBytes("\r\n--" + boundary + "\r\n");

HttpWebRequest wr = (HttpWebRequest)WebRequest.Create(urlString);
wr.ContentType = "multipart/form-data; boundary=" + boundary;
wr.Method = "POST";
wr.KeepAlive = true;
wr.Credentials = System.Net.CredentialCache.DefaultCredentials;

Stream rs = wr.GetRequestStream();
rs.Write(boundarybytes, 0, boundarybytes.Length);

string header = "Content-Disposition: form-data; name=\"vtfx\"; filename=\"upload.vtfx\"\r\nContent-Type: application/octet-stream\r\n\r\n";
byte[] headerbytes = System.Text.Encoding.UTF8.GetBytes(header);
rs.Write(headerbytes, 0, headerbytes.Length);

FileStream fileStream = new FileStream(filename, FileMode.Open, FileAccess.Read);
byte[] buffer = new byte[4096];
int bytesRead = 0;
while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0)
{
    rs.Write(buffer, 0, bytesRead);
}
fileStream.Close();
File.Delete(filename);

byte[] trailer = System.Text.Encoding.ASCII.GetBytes("\r\n--" + boundary + "--\r\n");
rs.Write(trailer, 0, trailer.Length);
rs.Close();

HttpWebResponse wresp = null;
try
{
    wresp = (HttpWebResponse)wr.GetResponse();
    Stream stream = wresp.GetResponseStream();
    StreamReader reader = new StreamReader(stream);
    string responseText = reader.ReadToEnd();

    string modelKey = parseTopLevelJsonValue(responseText, "modelKey");

    string htmlResponse = "";
    HttpStatusCode httpStatusCode = wresp.StatusCode;

    if (httpStatusCode == HttpStatusCode.OK)
    {
        string viewerUrl = "https://cloud.ceetron.com/v/" + modelKey;
        htmlResponse = "<h1>Upload Successful</h1><p>Click <a href=\"" + viewerUrl + "\">here<a> to see your uploaded model.<p>The link to the model is:</p><pre>" + viewerUrl + "</pre>";
    }
    else
    {
        // Note: Will probably not get here, as GetResponse() seems to throw if status is not 200
        htmlResponse = string.Format("<h1>Upload Failed</h1><p>httpStatusCode:{0}", httpStatusCode);
    }

    UploadResultDialog dlg = new UploadResultDialog(htmlResponse);
    dlg.ShowDialog();
}
catch (Exception ex)
{
    string htmlResponse = "<h1>Error uploading to Ceetron Cloud</h1><p>" + ex.Message + "</p>";

    if (ex is WebException)
    {
        WebException webEx = (WebException)ex;

        wresp = (HttpWebResponse)webEx.Response;
        Stream stream = wresp.GetResponseStream();
        StreamReader reader = new StreamReader(stream);
        string responseText = reader.ReadToEnd();

        string failReason = parseTopLevelJsonValue(responseText, "message");
        string apiErrorCode = parseTopLevelJsonValue(responseText, "apiErrorCode");

        if (myKey != null && apiErrorCode == "ERR_ILLEGAL_UPLOAD_ID")
        {
            // Upload ID is invalid, delete from the registry
            myKey.Close();
            Registry.CurrentUser.DeleteSubKey(@"Software\Ceetron\dNetDemoApp\Settings\");
        }

        htmlResponse = string.Format("<h1>Upload Failed</h1><p>{0}</p><p>{1}</p><p>apiErrorCode: {2}</p>", failReason, ex.Message, apiErrorCode);
    }

    UploadResultDialog dlg = new UploadResultDialog(htmlResponse);
    dlg.ShowDialog();

    if (wresp != null)
    {
        wresp.Close();
        wresp = null;
    }
}
finally
{
    wr = null;
    Cursor.Current = Cursors.Default;
}

MFC (win32) Example

CString urlString;
urlString.Format("api/v1/models?uploadId=%s&uploadApp=%s&name=%s", ToPercentEncoding(sUploadID).ShortChar(), ToPercentEncoding(sUploadApp).ShortChar(), ToPercentEncoding(sCaseName).ShortChar());

CInternetSession session("sendFile");
CHttpConnection *connection = session.GetHttpConnection(ceetronCloudHost, INTERNET_FLAG_SECURE, ceetronCloudPort);
if (!connection)
{
    *psErrorMsg = "Error connecting to Ceetron cloud";
    return VT_FALSE;
}

CHttpFile* pHTTP = connection->OpenRequest(CHttpConnection::HTTP_VERB_POST, urlString, NULL, 1, NULL, NULL, INTERNET_FLAG_SECURE);
if (!pHTTP)
{
    *psErrorMsg = "Error OpenRequest to Ceetron cloud";
    return VT_FALSE;
}

CString boundary        = _T("boundary_GLview89906a4aJn1kvlAG52SFG2AD");
CString endline         = "\r\n";
CString start_delim     = "--" + boundary + endline;
CString cont_disp_str   = "Content-Disposition: form-data; ";
CString stop_delim      = endline + "--" + boundary + "--" + endline;

CString vtfx_str = start_delim + cont_disp_str + "name=" + "\"vtfx\""+"; filename=\"upload.vtfx\""+endline+"Content-Type: application/octet-stream"+endline+endline;
DWORD dwTotalRequestLength = vtfx_str.GetLength() + stop_delim.GetLength() + (DWORD)vtfxFileToUpload.GetLength();

CString strRequestHeaders = CString("Content-Type: multipart/form-data; boundary=") + boundary + endline;
pHTTP->AddRequestHeaders(strRequestHeaders);
pHTTP->SendRequestEx(dwTotalRequestLength, HSR_SYNC | HSR_INITIATE);

//Write out the headers and the form variables
pHTTP->Write((LPCSTR)vtfx_str, vtfx_str.GetLength());

//upload the file.

DWORD dwReadLength = -1;
DWORD dwChunkLength = 64 * 1024;
void* pBuffer = malloc(dwChunkLength);

//int length = vtfxFileToUpload.GetLength(); //used to calculate percentage complete.
while (0 != dwReadLength)
{
    dwReadLength = vtfxFileToUpload.Read(pBuffer, dwChunkLength);
    if (0 != dwReadLength)
    {
        pHTTP->Write(pBuffer, dwReadLength);
    }
}

free(pBuffer);

vtfxFileToUpload.Close();

//Finish the upload.
pHTTP->Write((LPCSTR)stop_delim, stop_delim.GetLength());
pHTTP->EndRequest(HSR_SYNC);

//get the response from the server.
LPSTR szResponse;
CString strResponse;
DWORD dwResponseLength = (DWORD)pHTTP->GetLength();
while (0 != dwResponseLength )
{
    szResponse = (LPSTR)malloc(dwResponseLength + 1);
    szResponse[dwResponseLength] = '\0';
    pHTTP->Read(szResponse, dwResponseLength);
    strResponse += szResponse;
    free(szResponse);
    dwResponseLength = (DWORD)pHTTP->GetLength();
}