0

I have an Azure hosted web app based on this sample: https://github.com/Autodesk-Forge/data.management-csharp-desktop.sample

This sample demonstrates a desktop application that shows BIM 360 Team, BIM 360 Docs and Fusion Team hubs, which respective Projects, Folders, Items and Versions. For each version it is possible to view it using Viewer.

(The hosted app is used as an intermediary for security purposes.)

I have extended this sample so that the item and version objects can be downloaded to the desktop. This works.

The object/file to download is retrieved from the Autodesk server as an HttpResponseMessage.

using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
using Autodesk.Forge;

namespace FPD.Sample.Cloud.Controllers
{
    public class DownloadController : ApiController
    {
        private const string FORGE_BASE_URL = "https://developer.api.autodesk.com";
        private const string PROXY_ROUTE = "api/forge/download/";

        // HttpClient has been designed to be re-used for multiple calls. 
        // Even across multiple threads. 
        // https://stackoverflow.com/a/22561368/4838205
        private static HttpClient _httpClient;

        [HttpGet]
        [Route("api/forge/download/{bucketKey}/{objectName}/bucket")]
        public async Task<HttpResponseMessage> Get(string bucketKey, string objectName)
        {
            string sessionId, localId;
            if (!HeaderUtils.GetSessionLocalIDs(out sessionId, out localId)) return null;
            if (!await OAuthDB.IsSessionIdValid(sessionId, localId)) return null;
            string userAccessToken =  await OAuthDB.GetAccessToken(sessionId, localId);


            if (_httpClient == null)
            {
                _httpClient = new HttpClient(
                  // this should avoid HttpClient searching for proxy settings
                  new HttpClientHandler()
                  {
                      UseProxy = false,
                      Proxy = null
                  }, true);
                _httpClient.BaseAddress = new Uri(FORGE_BASE_URL);
                ServicePointManager.DefaultConnectionLimit = int.MaxValue;
            }

            string resourceUrl = $"/oss/v2/buckets/{bucketKey}/objects/{objectName}";
            try
            {
                HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, resourceUrl);

                // add our Access Token
                request.Headers.Add("Authorization", "Bearer " + userAccessToken);

                HttpResponseMessage response = await _httpClient.SendAsync(request,
                  // this ResponseHeadersRead force the SendAsync to return
                  // as soon as the header is ready, faster
                HttpCompletionOption.ResponseHeadersRead);

                return response;
            }
            catch
            {
                return new HttpResponseMessage(System.Net.HttpStatusCode.InternalServerError);
            }
        }
    }
}

This response is then passed to the desktop application, then converted to bytes:

        public async static Task<byte[]> GetDownload(string  bucketKey, string objectName)
        {
            byte[] bytes = await RestAPI<byte[]>.RequestAsyncBytes("api/forge/download/" + bucketKey + "/" + objectName +  "/bucket", true);
            return bytes;
        }
        public static async Task<byte[]> RequestAsyncBytes(string endpoint, IDictionary<string, string> headers, bool includeIdHeaders)
        {
            var client = new RestClient(EndPoints.BaseURL);
            var request = new RestRequest(endpoint, Method.GET);

            if (includeIdHeaders)
            {
                if (headers == null) headers = new Dictionary<string, string>();
                headers.Add("FPDSampleSessionId", SessionManager.SessionId);
                headers.Add("FPDSampleLocalId", SessionManager.MachineId);
            }

            if (headers != null)
                foreach (KeyValuePair<string, string> header in headers)
                    request.AddHeader(header.Key, header.Value);

            IRestResponse response = await client.ExecuteTaskAsync(request);
            if (response.StatusCode == System.Net.HttpStatusCode.OK)
            {
                return response.RawBytes; 
            }
            else if (response.StatusCode == System.Net.HttpStatusCode.NoContent)
            {
                throw new Exception("No Content: " + endpoint);

            }
            else if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
            {
                throw new Exception("Not found: " + endpoint);

            }
            else
            {
                throw new Exception($"Error {response.StatusCode}. Cannot call {endpoint}");
            }

        }

Then saved to the hard drive as a file, using something like this:

byte [] bytes = await Forge.DataManagement.GetDownload(bucketKey, objectName);
System.IO.File.WriteAllBytes(pathAndFilename, bytes);

Is it possible to pass the majority of the payload around the Azure app so that the object/file arrives intact, the security is maintained, but the data allowance of the app isn't blown out?

  • You can try to return from rest API not a downloaded content from bucket, but a tuple (bucketUri, userAccessToken) . Then perform download of bucket on desktop client using same code from rest API for download – dvitel Sep 12 '19 at 06:05
  • @dvitel That would involve passing the accessToken to the desktop client, wouldn't it? I'm trying to avoid that. – Matt Taylor Sep 13 '19 at 01:32

1 Answers1

0

You can create a Signed URL for the object and pass that around your backend and basically a signed URL is a link to the object allowing temporary anonymous access - see here for details.

Alternatively you can download the object to a temporary location (see here and here) and pass the path info around which saves other parts of your code the time to download it again.

Bryan Huang
  • 5,247
  • 2
  • 15
  • 20
  • Thanks for that, @bryan. I tried the Signed URL approach, but got a 403 forbidden response. (I changed the scope to include data:write, used a POST method, and used the signed endpoint.) Maybe it's because of this: "A successful call to this endpoint requires bucket owner access." I'm trying to download models, derivatives etc. The alternative you mention doesn't differ much from my current workflow; it still involves taking the data to a temp spot in memory or on virtual disk. – Matt Taylor Sep 13 '19 at 02:42
  • Make sure the Forge app with the client id/secret used owns the target bucket when creating signed URLs for objects - just tried on an object in my bucket and it worked: `{ "signedUrl": "https://developer.api.autodesk.com/oss/v2/signedresources/7012e598-dc2b-4aee-b8df-8ed968021170?region=US", "expiration": 1568603960947, "singleUse": false }` – Bryan Huang Sep 16 '19 at 02:19
  • 1
    I'm wanting to use this for BIM360 models and derivatives, so I'll definitely not be the owner of the bucket. Any other ideas? – Matt Taylor Sep 17 '19 at 03:51
  • Any further ideas? – Matt Taylor Sep 19 '19 at 20:43
  • Lemme speak with Engineering and see if there's a way to work around the permission issue. – Bryan Huang Sep 20 '19 at 04:20