API Download File from BIM360 Doc Plans folder - autodesk-forge

I am trying to download file from Autodesk BIM360 Doc (https://docs.b360.autodesk.com) with the Forge API so the files can be then afterward archieved to our local storage.
I have managed to download any files from "Project Files" folder using the data management API https://forge.autodesk.com/en/docs/data/v2/reference/http/projects-project_id-versions-version_id-GET/, with which i can get the storage id under data.relationships.storage.data.id.
however with the same API i cannot get the storage Id when querying files under "Plan" folder,
So is there any way with Forge API we can download a file from Plan folder? any help is appreciated.

The item listed in the Plan folder is a type of items:autodesk.bim360:Document, this type item won't have storage attribute shown in its responses of GET versions/:version_id and GET items/:item_id directly.
To obtain the physical file location, you should call GET versions/:version_id/relationships/refs instead, see here for the similar thread: Download a Document with Autodesk API
Update for copied item
While accessing the relationship data of version of the copied item via GET versions/:version_id/relationships/refs, you would see a data attribute telling the relationship between the copied item and the source item with my experience:
"data": [
{
"type": "versions",
"id": "urn:adsk.wipprod:fs.file:vf.34Xvlw1jTcSQ_XkIVh07cg?version=2",
"meta": {
"refType": "derived",
"fromId": "urn:adsk.wipprod:fs.file:vf.34Xvlw1jTcSQ_XkIVh07cg?version=2",
"fromType": "versions",
"toId": "urn:adsk.wipprod:fs.file:vf.y3L7YbfAQJWwumMgqjJUxg?version=1",
"toType": "versions",
"direction": "to",
"extension": {
"type": "derived:autodesk.bim360:CopyDocument",
"version": "1.0",
"schema": {
"href": "https://developer.api.autodesk.com/schema/v1/versions/derived:autodesk.bim360:CopyDocument-1.0"
},
"data": {}
}
}
}
],
Afterward, you have to access the version relationship dat of the fromId via calling GET versions/:version_id/relationships/refs.
In this case, it's {PROJ_ID}/versions/urn:adsk.wipprod:fs.file:vf.34Xvlw1jTcSQ_XkIVh07cg%3Fversion=2/relationships/refs, then you will see the storage attribute inside the response with my investigation.

Just in case anyone else run into the same issue, i post my code with which i finally managed to get the file storage information. However please feel free to suggest other approaches than iteration to the full relationship trees.
internal static ForgeFileInfo getItemVersion(string token, string projectID, string versionID)
{
ForgeFileInfo forgeFileInfo = new ForgeFileInfo();
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
versionApi.Configuration.AccessToken = token;
var version = versionApi.GetVersion(projectID, versionID);
string fileType = version.data.attributes.extension.type;
switch (fileType) {
case "versions:autodesk.bim360:File":
//File from Project File library or is regual file
forgeFileInfo.FileName = version.data.attributes.displayName;
forgeFileInfo.FileLocation = version.data.relationships.storage.meta.link.href;
forgeFileInfo.StorageId = version.data.relationships.storage.data.id;
return forgeFileInfo;
case "versions:autodesk.bim360:Document":
//File from Plan Library
var versionRelationship=versionApi.GetVersionRelationshipsRefs(projectID, versionID);
// the GET Relationship has data node where we can get the related document
var relationshipData = new DynamicDictionaryItems(versionRelationship.data);
// let's start iterating the relationship DATA
foreach (KeyValuePair<string, dynamic> relationshipItem in relationshipData)
{
//Have to loop until we found "derived:autodesk.bim360:FileToDocument"
var relationType = relationshipItem.Value.meta.extension.type;
var relation = relationshipItem.Value.meta.direction;
if ("derived:autodesk.bim360:FileToDocument".Equals(relationType))
{
if ("to".Equals(relation))
{
//Go up stream
return getItemVersion(token, projectID, relationshipItem.Value.id);
}
}
else if ("derived:autodesk.bim360:CopyDocument".Equals(relationType))
{
if ("to".Equals(relation))
{
//Go up stream
return getItemVersion(token, projectID, relationshipItem.Value.id);
}
continue;
}
}
break;
}
return forgeFileInfo;
}

Related

How can I find Revit document derivatives URNs in BIM360 Docs using the Forge Data Management API

I have some Revit files stored in a BIM360 project. I am trying to visualize those files inside Forge Viewer. Now Forge Viewer won't work directly with Revit file/documents, but require the 'urn' of a translated file, in 'svf' format.
I could transform my Revit file into the 'svf' file using the Forge Model Derivative API, but that consume some credits, and I shouldn't be able to do this because when uploading a Revit file into BIM360, the translation is happening already there.
I was wondering then, how do I find out the 'urn' of the underlying 'svf' file for my Revit document ?
I found few resources helping there, when browsing the content of my BIM360 folder, or checking the versions of my Revit document using Forge Data Management API, I should be able to access a derivative object in the response which represent the derived model that can be used by the Forge viewer.
https://forums.autodesk.com/t5/bim-360-api-forum/connecting-forge-viewer-with-bim-360/td-p/6742779
However for me, there are no derivatives object in the API response, see below a sample of the API response (I have obfuscated some data for security purpose):
{
"type": "versions",
"id": "urn:adsk.wipprod:fs.file:vf.XXXXXXXXXXXXXXXXXXXX?version=1",
"attributes": "#{name=139200.33_Amenities Building_R21.rvt; displayName=139200.33_Amenities Building_R21.rvt; createTime=2021-09-03T04:24:18.0000000Z; createUserId=XXXXXXXXXX; createUserName=Holmes Consulting; lastModifiedTime=2021-09-03T04:28:02.0000000Z; lastModifiedUserId=XXXXXXXXXXXX; lastModifiedUserName=XXXXXXXXXX; versionNumber=1; storageSize=19808256; fileType=rvt; extension=}",
"links": "#{self=; webView=}",
"relationships": "#{item=; links=; refs=; downloadFormats=; derivatives=; thumbnails=; storage=}"
},
I am using the API call as used in the link I provided above:https://developer.api.autodesk.com/data/v1/projects/:project_id/folders/:folder_id/contents
Why is it that my response contains so little data ?
First thank Eason for contributing.
Since my derivatives object was empty I tried to directly use the 'urn' of my object version.
When listing all my folder documents using the folder get content API method mentioned in my issue, I get all the documents in the 'data' item array and all their versions in the 'included' version array. We need to use the document version id to build the urn. See my sample below:
"included": [
{
"type": "versions",
"id": "urn:adsk.wipprod:fs.file:vf.l9pc9re6QOmeEVHvTCTlIQ?version=1",
"attributes": "#{name=139200.33_Amenities Building_R21.rvt; displayName=139200.33_Amenities Building_R21.rvt; createTime=2021-09-03T04:24:18.0000000Z; createUserId=XXXXXX; createUserName=XXXXXXXX; lastModifiedTime=2021-09-03T04:28:02.0000000Z; lastModifiedUserId=XXXXXXXXXXXX; lastModifiedUserName=XXXXXXXXXXXX; versionNumber=1; storageSize=19808256; fileType=rvt; extension=}",
"links": "#{self=; webView=}",
"relationships": "#{item=; links=; refs=; downloadFormats=; derivatives=; thumbnails=; storage=}"
},
Now the id has to be base64 encoded. I am using https://www.freeformatter.com/base64-encoder.html to encode the id urn:adsk.wipprod:fs.file:vf.l9pc9re6QOmeEVHvTCTlIQ?version=1. Beware the result will be dXJuOmFkc2sud2lwcHJvZDpmcy5maWxlOnZmLmw5cGM5cmU2UU9tZUVWSHZUQ1RsSVE/dmVyc2lvbj0 which is not valid in my JS code to load the document in the Forge Viewer, because of the /. It needs to be replaced with a _. So eventually the bit of JS that load my document into the Forge Viewer looks like this:
var documentId = 'urn:dXJuOmFkc2sud2lwcHJvZDpmcy5maWxlOnZmLmw5cGM5cmU2UU9tZUVWSHZUQ1RsSVE_dmVyc2lvbj0x'; //139200.33_Amenities Building_R21.rvt
Autodesk.Viewing.Initializer(options, function() {
var htmlDiv = document.getElementById('forgeViewer');
viewer = new Autodesk.Viewing.GuiViewer3D(htmlDiv);
viewer.start();
Autodesk.Viewing.Document.load(documentId, onDocumentLoadSuccess, onDocumentLoadFailure);
function onDocumentLoadSuccess(viewerDocument) {
// Choose the default viewable - most likely a 3D model, rather than a 2D sheet.
var defaultModel = viewerDocument.getRoot().getDefaultGeometry();
viewer.loadDocumentNode(viewerDocument, defaultModel);
}
function onDocumentLoadFailure() {
console.error('Failed fetching Forge manifest');
}
});
Please find it in the id value of relationships.data.derivatives. For example,
"derivatives": {
"data": {
"type": "derivatives",
"id": "dXJuOmFkc2sud2lwcHJvZDpmcy5maWxlOnZmLkVueWtrU3FjU0lPVTVYMGhRdy1mQUM_dmVyc2lvbj0x"
},
// ...
},
Or check this line: https://github.com/Autodesk-Forge/learn.forge.viewhubmodels/blob/nodejs/routes/datamanagement.js#L155
const viewerUrn = (version.relationships != null && version.relationships.derivatives != null ? version.relationships.derivatives.data.id : null);

googleapis "appProperties" field not being returned for files in node

I'm trying to get the appProperties field to be returned with my files on gdrive, but currently unable to get it working.
"googleapis": "^29.0.0"
Here's my scopes and fields:
scopes: [
"https://www.googleapis.com/auth/drive",
"https://www.googleapis.com/auth/drive.file",
"https://www.googleapis.com/auth/drive.metadata.readonly"
]
fields = ["id", "name", "mimeType", "parents", "description", "modifiedTime", "appProperties"]
All of the other properties come back using drive.files.list without issue, but it won't return the appProperties field.
getFilesByQuery: function( queryString , extraFields ){
var fields = ["id", "name", "mimeType", "parents", "description", "modifiedTime", "appProperties"];
if( extraFields && extraFields.length )
fields = fields.concat( extraFields );
return drive.files.list({
'pageSize': 200,
'fields': `nextPageToken, files(${ fields.join(', ') })`,
'q': queryString
});
}
When I query directly through files/get on dev.google API, this is what I get back for that file:
{
"name": "US",
"appProperties": {
"order": "1"
}
}
Any ideas?
Thanks!
In my environment, I confirmed that appProperties can be retrieved using files.list and files.get of googleapis with v29.0.0. And I thought about the possibility of the reason for your situation. So can you confirm the following point?
When I read the document of Custom File Properties, it says as follows.
Properties are accessed using the properties (visible to all apps) and appProperties (restricted to single apps) fields on files
I investigated about this. As a sample, it supposes that {"key1": "value1"} was written to appProperties and properties by client_id_A.
For appProperties, when the appProperties is read, only the client ID which is the same with the client ID used when appProperties was written can read it.
Namely, when the access token retrieved from client_id_B is used, it cannot read appProperties written by client_id_A.
For properties, when the properties is read, it can be read by various client IDs.
Namely, even if the access token retrieved from client_id_B is used, it can read properties written by client_id_A.
From these results, appProperties and properties can be used as "Private" and "Public", respectively.
Using this, can you confirm your situation again? If you will write appProperties using node.js, you can use the following script. By this, you can confirm that you can write and read appProperties using the same client ID.
drive.files.update({
fileId: "### file ID ###",
resource: {"appProperties": {"key": "value"}},
fields: 'id,appProperties',
});
If this was not useful for your situation, I'm sorry.

Access object property while working with another object in ng-repeat

In my project I got a JSON response via GET request. The subTopics will be selected by the user and stored. Afterwards I send a POST request to the server with the selected ids.
Example JSON1: (from GET request)
{
"TopicList" :
[{
"id": "1234",
"name": "topic1",
"number": "1",
"subTopics": [
{
"id": "4567",
"name": "subTopic1.1",
"number": "1.1"
},
{
"id": "9876",
"name": "subTopic1.2",
"number": :1.2"
}
]
}]
}
In the POST response I get another JSON object from the server, which I have to show in my HTML view as a table. In the response JSON I have the subTopics id (selected by the user) but I do not have the subTopic name associated with the id.
I have to show the subTopic name in my table which is available in a separate object(see above JSON file). I don't know how to access the first JSON object while working with another.
My table view looks like this,
<tr ng-repeat-start="tableRow in searchCtrl.tableViewData" ng-click="tableRow.expanded = !tableRow.expanded">
<td>{{tableRow.project.name}}</td>
<td>{{tableRow.project.number}}</td>
<td>{{tableRow.project.endDate | date}}</td>
<td>{{tableRow.topicIds[0]}}</td>
<td>{{tableRow.matching.score}}</td>
</tr>
As you can see the 4th row: <td>{{tableRow.topicIds[0]}}</td> shows the id. How can I show the topicName?
Any help would be appreciable.
EDIT
In my controller this variable contains the above JSON object.
if (!self.topic) {
searchService.getTopic().then(
function (response) {
self.topic = response.data;
},
function (error) {
alert("Server is not found");
}
);
}
So, the topic variable contains the response JSON object. Maybe it will help.
You can create a function that takes an id and returns the subTopic.
$scope.getSubTopic = function(id) {
var selectedSubTopic = {};
angular.forEach(subTopics, function(subTopic) {
// loop through subTopics until a matching id is found
if (subTopic.id === id) {
selectedSubTopic = subTopic;
return;
}
});
return selectedSubTopic;
};
then you can update your fourth row to:
<td>{{getSubTopic(tableRow.topicIds[0]).name}}</td>
This assumes you have an array named subTopics.
Edit
As mentioned in my comment this will end up performing pretty slow for heavy pages and/or large datasets. You will likely want to generate a map object for the subTopics for quick access. The downside being you have to generate this each time the TopicList is modified.
function generateSubTopicMap(topics) {
var map = {};
angular.forEach(topics, function(topic) {
angular.forEach(topic.subTopics, function(subTopic) {
// use this if you want the map to reference the same data
// (i.e. updating subTopic.name will update the map at the same time)
map[subTopic.id] = subTopic;
// use this if you don't want the map to reference the same data
// map[subTopic.id] = {};
// angular.copy(subTopic, map[subTopic.id]);
// you can also add the parent id here if you need access to it
// this will modify the original object if you use the first method!
// map[subTopic.id].parentId = topic.id
});
});
return map;
}
The output looks like:
{
"4567": {
"id": "4567",
"name": "subTopic1.1",
"number": "1.1"
},
"9876": {
"id": "9876",
"name": "subTopic1.2",
"number": :1.2"
}
}
With this you would call it after every GET request and pass it the array of topics.
// where topics is the response from the GET request
$scope.subTopics = generateSubTopicMap(topics);
And finally to display you just need:
<td>{{subTopics[tableRow.topicIds[0])].name}}</td>
Edit 2
Here is a jsfiddle showing how to use the second method. All you have to do is pass the array containing your TopicList to generateSubTopicMap and it returns an object with the keys as subTopic ids and the value as the subTopic itself.
I wouldn't worry about my first solution. It isn't going to be performant inside an ng-repeat or grabbing 2nd level objects.

Can I store a node process variable in JSON?

I am currently in the process of migrating an Express app to Heroku.
To keep sensitive information out of source, Heroku uses config vars which are assigned by to process variables of the same name.
Currently, I am loading my keys using .json, such as:
{
"key": "thisismykey",
"secret": "thisismysecret"
}
However, if I try to load the variables in via Heroku's format:
{
"key": process.env.KEY
"secret": process.env.SECRET
}
Obviously, I get an error here. I would assume that it is possible to load these values into JSON, but I'm not sure. How could I do this?
To generate JSON with these values, you would first create a JavaScript object and then use JSON.stringify to turn it into JSON:
var obj = { "key": process.env.KEY
"secret": process.env.SECRET };
var json = JSON.stringify(obj);
// => '{"key":"ABCDEFGH...","secret":"MNOPQRST..."}'

Facebook Graph API not returning standard JSON

I am working on an iOS app using the MonoTouch framework. I am using Visual Studio 2010 Professional SP1 with the Xamarin.iOS (v1.3.250) extension. I have been able to open a valid FacebookConnect.FBSession by using the FacebookConnect.FBLoginView with no issues but when I try to make a Graph API request using FacebookConnect.FBRequest I recieve a non-standard JSON style string. When I run following request through the Graph API Explorer:
me?fields=albums.fields(id,name,cover_photo)
I receive the following response:
{
"id": "111111111111111111",
"albums": {
"data": [
{
"id": "111111111111111111",
"name": "Some Album (#1)",
"cover_photo": "111111111111111111",
"created_time": "000-00-00T00:00:00+0000"
},
{
"id": "111111111111111111",
"name": "Some Album (#2)",
"cover_photo": "111111111111111111",
"created_time": "000-00-00T00:00:00+0000"
},
],
"paging": {
"cursors": {
"after": "xxxxxxxx=",
"before": "xxxxxxxx="
}
}
}
}
Now all of this is just fine and is what I expect to receive but when I make the same Graph API request from my app like this:
public static void GetPhotoAlbums(string _userID)
{
// _userID = "me"
mFBRequest = new FBRequest(FBSession.ActiveSession, _userID + "?fields=albums.fields(id,name,cover_photo)");
FBRequestConnection fbRequestConnection = new FBRequestConnection();
fbRequestConnection.AddRequest(mFBRequest, OnPhotoAlbumsReceived);
fbRequestConnection.Start();
}
static void OnPhotoAlbumsReceived(FBRequestConnection _connection, NSObject _result, NSError _error)
{
if (_error == null)
{
Console.WriteLine("FacebookManager.OnPhotoAlbumsReceived() - JSON: " + _result.Description);
object o = JsonConvert.DeserializeObject(_result.Description);
// ...
}
}
I receive this JSON 'like' response:
{
albums = {
data = (
{
"cover_photo" = 111111111111111111;
"created_time" = "000-00-00T00:00:00+0000";
id = 111111111111111111;
name = "Some Album (#1)";
},
{
"cover_photo" = 111111111111111111;
"created_time" = "000-00-00T00:00:00+0000";
id = 111111111111111111;
name = "Some Album (#2)";
},
);
paging = {
cursors = {
after = "xxxxxxxx=";
before = "xxxxxxxx=";
};
};
};
"id": "111111111111111111";
}
I'm not really sure how/why I'm getting a response formatted in a non-standard way but needless to say, I get Newtonsoft.Json.JsonReaderException when attempting to deserialize the data because it does not follow the standard formatting rules (ie, = instead of : to separate key/value pairs, ; instead of , to separate elements of a container, some keys having quotes while others do not, etc...)
I'm pretty new to Facebook and JSON stuff in general and am really at a loss for what is happening to the response string I receive. Any help, feedback, ideas are greatly appreciated. Thanks.
* Solution *
After a bunch of digging around it seems to be that the Graph API response is indeed JSON but it gets converted to an FBGraphObject which holds a NSMutableArray as it the data makes its way through the MonoTouch->.NET bindings so when I pulled _result.Description (equivalent to _result.ToString() it returned me the string representation of that object which happens to look a lot like JSON but is not. After finding all this out (and a lot of runtime experimentation), I was finally able to extract the data into a usable state by doing this:
static void OnPhotoAlbumsReceived(FBRequestConnection _connection, NSObject _result, NSError _error)
{
if (_error == null)
{
NSArray fieldData = (NSArray) _result.ValueForKeyPath(new NSString("albums.data.name"))
string[] names = NSArray.StringArrayFromHandle(fieldData.Handle);
// ...
}
}
Although this works for me, I have a feeling that there is a better or more robust way to get the data I requested, so if any developers out there can offer any additional tips for improving this solution, I would love to hear them.
As stated in Facebook SDK documentation Graph API:
When a request returns a non-JSON response (such as a "true" literal),
that response will be wrapped into a dictionary using this const as
the key. This only applies for very few Graph API prior to v2.1.
So you can check first if result is an NSDictionary, otherwise you can deserialize the JSON data as usual.
Below some obj-c code you can translate into C#/MonoTouch (I don't know the framework, I hope it is helpful).
NSDictionary *dict;
if ([graphResult isKindOfClass:[NSDictionary class]]) {
dict = (NSDictionary *)graphResult;
} else {
NSError *JSONError;
dict = [NSJSONSerialization JSONObjectWithData:graphResult options:NSJSONReadingAllowFragments error:&JSONError];
if (JSONError) {
NSLog(#"Facebook: JSON parse error: %#", JSONError);
// Handle error
}
}