diff --git a/.gitignore b/.gitignore index 7338427..4f882a1 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,5 @@ crashlytics.properties crashlytics-build.properties fabric.properties .idea/ + +.vscode/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 079446a..0000000 --- a/.travis.yml +++ /dev/null @@ -1,51 +0,0 @@ -# This Travis config file -# was taken from https://gist.github.com/y0ssar1an/df2dab474520c4086926f672c52db139 -language: go - - -# Only the last two Go releases are supported by the Go team with security -# updates. Any versions older than that should be considered deprecated. -# Don't bother testing with them. tip builds your code with the latest -# development version of Go. This can warn you that your code will break -# in the next version of Go. Don't worry! Later we declare that test runs -# are allowed to fail on Go tip. -go: - - 1.9.2 -# - master - -# Skip the install step. Don't `go get` dependencies. Only build with the -# code in vendor/ -install: true - -#matrix: -# # It's ok if our code fails on unstable development versions of Go. -# allow_failures: -# - go: master -# # Don't wait for tip tests to finish. Mark the test run green if the -# # tests pass on the stable versions of Go. -# fast_finish: true - -# Don't email me the results of the test runs. -notifications: - email: false - -# Anything in before_script that returns a nonzero exit code will -# flunk the build and immediately stop. It's sorta like having -# set -e enabled in bash. -before_script: - - GO_FILES=$(find . -iname '*.go' -type f | grep -v /vendor/) # All the .go files, excluding vendor/ - - go get github.com/golang/lint/golint # Linter -# - go get honnef.co/go/tools/cmd/megacheck # Badass static analyzer/linter - - go get github.com/fzipp/gocyclo - -# script always run to completion (set +e). All of these code checks are must haves -# in a modern Go project. -script: -# - test -z $(gofmt -s -l $GO_FILES) # Fail if a .go file hasn't been formatted with gofmt - - go test -v -race ./... # Run all the tests with the race detector enabled - - go vet ./... # go vet is the official Go static analyzer -# - megacheck ./... # "go vet on steroids" + linter - - gocyclo -over 19 $GO_FILES # forbid code with huge functions -# - golint -set_exit_status $(go list ./oauth) # oauth lint - - golint -set_exit_status $(go list ./recap) # recap lint -# - golint -set_exit_status $(go list ./...) # lint them all diff --git a/LICENSE b/LICENSE index 806ee19..540d9f3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License Copyright (c) 2017 Denis Grigor +Portions Copyright (c) 2022-2023 Wolfgang Weh Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index ad7c43d..2e13de5 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,90 @@ -[![Build Status](https://travis-ci.org/apprentice3d/forge-api-go-client.svg?branch=master)](https://travis-ci.org/apprentice3d/forge-api-go-client) -[![GoDoc](https://godoc.org/github.com/apprentice3d/forge-api-go-client?status.svg)](https://godoc.org/github.com/apprentice3d/forge-api-go-client) -[![Go Report Card](https://goreportcard.com/badge/github.com/apprentice3d/forge-api-go-client)](https://goreportcard.com/report/github.com/apprentice3d/forge-api-go-client) +[![GoDoc](https://godoc.org/github.com/woweh/forge-api-go-client?status.svg)](https://godoc.org/github.com/woweh/forge-api-go-client) +[![Go Report Card](https://goreportcard.com/badge/github.com/woweh/forge-api-go-client)](https://goreportcard.com/report/github.com/woweh/forge-api-go-client) # forge-api-go-client -**Forge API:** [![oAuth2](https://img.shields.io/badge/oAuth2-v2-green.svg)](http://developer-autodesk.github.io/) -[![Data-Management](https://img.shields.io/badge/Data%20Management-v1-green.svg)](http://autodesk-forge.github.io/) -[![OSS](https://img.shields.io/badge/OSS-v2-green.svg)](http://autodesk-forge.github.io/) -[![Model-Derivative](https://img.shields.io/badge/Model%20Derivative-v2-green.svg)](http://autodesk-forge.github.io/) -[![Reality-Capture](https://img.shields.io/badge/Reality%20Capture-v1-green.svg)](http://developer-autodesk.github.io/) +**Autodesk Platform Services APIs:** +[![Authentication](https://img.shields.io/badge/Authentication-v2-green.svg)](https://aps.autodesk.com/en/docs/oauth/v2/developers_guide/overview/) +[![Data-Management](https://img.shields.io/badge/Data%20Management-v2-green.svg)](https://aps.autodesk.com/en/docs/data/v2/developers_guide/) +[![Model-Derivative](https://img.shields.io/badge/Model%20Derivative-v2-green.svg)](https://aps.autodesk.com/en/docs/model-derivative/v2/developers_guide/) -Golang SDK for building Forge based applications +Golang API client for building APS based applications ([Autodesk Platform Services], formerly *"Forge"*). +This is a fork of the [forge-api-go-client by Denis Grigor](https://github.com/apprentice3d/forge-api-go-client). +The original client is no longer maintained. +This forks has been updated and extended with new features. +--- +## Supported and maintained APIs +Note that this client only covers a subset of the APS APIs. + +At the time of writing (2023/06/15), only the following APIs are maintained: +1. Authentication (oauth) +2. Data Management (dm) +3. Model Derivative (md) + +The following APIs are not maintained: +1. Reality Capture (rc) +2. Design Automation (da) + +Autodesk is constantly adding new APIs and changing existing APIs. +A lot of the new APIs are not covered by this client. + +You are invited to contribute 🧑🏽‍💻! +Please fork and add missing APIs. + +--- +## Updates from the original client + +The client has been extended with the following features. +Note that there are a number of breaking changes. + +### Authentication (oauth): +- Update to OAuth V2. + See: https://aps.autodesk.com/blog/migration-guide-oauth2-v1-v2 + + +### Data Management API (dm): +- Update `upload object` and `download object` to use the direct-to-s3 approach (breaking change). + See: + - https://forge.autodesk.com/blog/data-management-oss-object-storage-service-migrating-direct-s3-approach + - https://forge.autodesk.com/en/docs/data/v2/reference/http/buckets-:bucketKey-objects-:objectKey-signeds3upload-GET/ + - https://forge.autodesk.com/en/docs/data/v2/reference/http/buckets-:bucketKey-objects-:objectKey-signeds3download-GET/ +- Add support for regions (US <> EMEA): + See: https://aps.autodesk.com/blog/data-management-and-model-derivative-regions +- dm Initialization now requires a region (breaking change). +- BucketAPI is renamed to OssAPI (breaking change). +- The API is changed to use pointer receivers. +- Update ListBuckets to list all buckets. + See: https://aps.autodesk.com/en/docs/data/v2/reference/http/buckets-GET/ +- Fix and update unit tests. You need a valid APS account (client ID and secret) to run the tests. + => Best run the tests locally, or on a private CI server. + + +### Model Derivative API (md): +- Add support for x-ads-headers and advanced translation options (input > formats > advanced). + See: https://forge.autodesk.com/en/docs/model-derivative/v2/reference/http/jobs/job-POST/ +- Update GET derivatives to use the `signedcookies` endpoint. + See: + - https://aps.autodesk.com/blog/data-management-oss-object-storage-service-migrating-direct-s3-approach + - https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/urn-manifest-derivativeUrn-signedcookies-GET/ +- Add support for regions (US <> EMEA): + See: https://aps.autodesk.com/blog/data-management-and-model-derivative-regions +- md Initialization now requires a region (breaking change). +- Add support for downloading all properties. + See: https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/metadata/urn-metadata-guid-properties-GET/ +- Add support for fetching the object tree. + See: https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/metadata/urn-metadata-guid-GET/ +- The API is changed to use pointer receivers. +- Fix and update unit tests. You need a valid APS account (client ID and secret) to run the tests. + => Best run the tests locally, or on a private CI server. + +--- +## TODO: +- Create proper changelog. +- Add support for more APIs. + +--- +[Autodesk Platform Services]: https://aps.autodesk.com/ \ No newline at end of file diff --git a/api_types.go b/api_types.go new file mode 100644 index 0000000..fa2aadd --- /dev/null +++ b/api_types.go @@ -0,0 +1,35 @@ +package forge + +// This file contains types and constants for the entire APS (aka Forge) API. + +import "strings" + +// HostName / domain of the Autodesk Forge API. +const HostName = "https://developer.api.autodesk.com" + +// Region is the region where the data resides. +type Region string + +const ( + US Region = "us" // US Region - us in lowercase! + EU Region = "emea" // EU (== EMEA) Region - emea in lowercase! + EMEA Region = "emea" // EMEA (== EU) Region - emea in lowercase! +) + +// IsUS returns true if the region is US. +func (r Region) IsUS() bool { + // case insensitive comparison! + return strings.EqualFold(string(r), string(US)) +} + +// IsEMEA returns true if the region is EMEA (== EU). +func (r Region) IsEMEA() bool { + // case insensitive comparison! + return strings.EqualFold(string(r), string(EMEA)) +} + +// IsEU returns true if the region is EU (== EMEA). +func (r Region) IsEU() bool { + // case insensitive comparison! + return strings.EqualFold(string(r), string(EU)) +} diff --git a/da/activity.go b/da/activity.go index 239b26a..79efb27 100644 --- a/da/activity.go +++ b/da/activity.go @@ -4,10 +4,11 @@ import ( "bytes" "encoding/json" "errors" - "github.com/apprentice3d/forge-api-go-client/oauth" - "io/ioutil" + "io" "net/http" "strconv" + + "github.com/woweh/forge-api-go-client/oauth" ) type Param struct { @@ -29,7 +30,7 @@ type ActivityConfig struct { Description string `json:"description"` AppBundles []string `json:"appbundles"` Engine string `json:"engine"` - Parameters map[string]Param `json:"paramaters"` + Parameters map[string]Param `json:"parameters"` Settings Setting `json:"settings"` } @@ -39,7 +40,6 @@ type Activity struct { authenticator oauth.ForgeAuthenticator path string name string - } func (activity *Activity) Delete() (err error) { @@ -52,9 +52,9 @@ func (activity *Activity) Delete() (err error) { activity.Parameters = make(map[string]Param) activity.ID = "" - activity.CommandLine = make([]string,0) + activity.CommandLine = make([]string, 0) activity.Description = "" - activity.AppBundles = make([]string,0) + activity.AppBundles = make([]string, 0) activity.Engine = "" activity.Settings = Setting{} activity.authenticator = nil @@ -64,8 +64,7 @@ func (activity *Activity) Delete() (err error) { return } - -//CreateAlias creates a new alias for this Activity. +// CreateAlias creates a new alias for this Activity. func (activity Activity) CreateAlias(alias string, version uint) (result Alias, err error) { bearer, err := activity.authenticator.GetToken("code:all") if err != nil { @@ -76,12 +75,10 @@ func (activity Activity) CreateAlias(alias string, version uint) (result Alias, return } - /* * SUPPORT FUNCTIONS */ - /* ACTIVITY */ @@ -91,12 +88,14 @@ func createActivity(path string, activity ActivityConfig, token string) (result task := http.Client{} body, err := json.Marshal( - activity) + activity, + ) if err != nil { return } - req, err := http.NewRequest("POST", + req, err := http.NewRequest( + "POST", path+"/activities", bytes.NewReader(body), ) @@ -113,7 +112,7 @@ func createActivity(path string, activity ActivityConfig, token string) (result defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -128,7 +127,8 @@ func createActivity(path string, activity ActivityConfig, token string) (result func deleteActivity(path string, activityId string, token string) (err error) { task := http.Client{} - req, err := http.NewRequest("DELETE", + req, err := http.NewRequest( + "DELETE", path+"/activities/"+activityId, nil, ) @@ -141,7 +141,7 @@ func deleteActivity(path string, activityId string, token string) (err error) { defer response.Body.Close() if response.StatusCode != http.StatusNoContent { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -149,8 +149,6 @@ func deleteActivity(path string, activityId string, token string) (err error) { return } - - /* ALIASES */ @@ -158,7 +156,8 @@ func deleteActivity(path string, activityId string, token string) (err error) { func listActivityAliases(path string, activityId, token string) (list AliasesList, err error) { task := http.Client{} - req, err := http.NewRequest("GET", + req, err := http.NewRequest( + "GET", path+"/activities/"+activityId+"/aliases", nil, ) @@ -171,7 +170,7 @@ func listActivityAliases(path string, activityId, token string) (list AliasesLis defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -190,12 +189,14 @@ func createActivityAlias(path, activityId, alias string, version uint, token str Alias{ alias, version, - }) + }, + ) if err != nil { return } - req, err := http.NewRequest("POST", + req, err := http.NewRequest( + "POST", path+"/activities/"+activityId+"/aliases", bytes.NewReader(body), ) @@ -212,7 +213,7 @@ func createActivityAlias(path, activityId, alias string, version uint, token str defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -231,12 +232,14 @@ func modifyActivityAlias(path, activityId, alias string, version uint, token str body, err := json.Marshal( struct { Version uint `json:"version"` - }{version}) + }{version}, + ) if err != nil { return } - req, err := http.NewRequest("PATCH", + req, err := http.NewRequest( + "PATCH", path+"/activities/"+activityId+"/aliases/"+alias, bytes.NewReader(body), ) @@ -253,7 +256,7 @@ func modifyActivityAlias(path, activityId, alias string, version uint, token str defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -269,7 +272,8 @@ func getActivityAliasDetails(path, activityId, alias, token string) (result Alia task := http.Client{} - req, err := http.NewRequest("GET", + req, err := http.NewRequest( + "GET", path+"/activities/"+activityId+"/aliases/"+alias, nil, ) @@ -286,7 +290,7 @@ func getActivityAliasDetails(path, activityId, alias, token string) (result Alia defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -298,12 +302,11 @@ func getActivityAliasDetails(path, activityId, alias, token string) (result Alia return } - - func deleteActivityAlias(path string, activityId, alias, token string) (err error) { task := http.Client{} - req, err := http.NewRequest("DELETE", + req, err := http.NewRequest( + "DELETE", path+"/activities/"+activityId+"/aliases/"+alias, nil, ) @@ -316,7 +319,7 @@ func deleteActivityAlias(path string, activityId, alias, token string) (err erro defer response.Body.Close() if response.StatusCode != http.StatusNoContent { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -324,7 +327,6 @@ func deleteActivityAlias(path string, activityId, alias, token string) (err erro return } - /* VERSIONS */ @@ -332,7 +334,8 @@ func deleteActivityAlias(path string, activityId, alias, token string) (err erro func listActivityVersions(path string, activityId, token string) (list VersionList, err error) { task := http.Client{} - req, err := http.NewRequest("GET", + req, err := http.NewRequest( + "GET", path+"/activities/"+activityId+"/versions", nil, ) @@ -345,7 +348,7 @@ func listActivityVersions(path string, activityId, token string) (list VersionLi defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -361,14 +364,16 @@ func createActivityVersion(path, activityId, engine string, token string) (resul task := http.Client{} body, err := json.Marshal( - struct{ + struct { Engine string `json:"engine"` - }{engine}) + }{engine}, + ) if err != nil { return } - req, err := http.NewRequest("POST", + req, err := http.NewRequest( + "POST", path+"/activities/"+activityId+"/versions", bytes.NewReader(body), ) @@ -385,7 +390,7 @@ func createActivityVersion(path, activityId, engine string, token string) (resul defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -397,13 +402,12 @@ func createActivityVersion(path, activityId, engine string, token string) (resul return } - - func getActivityVersionDetails(path, activityId string, version uint, token string) (result ActivityConfig, err error) { task := http.Client{} - req, err := http.NewRequest("GET", + req, err := http.NewRequest( + "GET", path+"/activities/"+activityId+"/versions/"+strconv.Itoa(int(version)), nil, ) @@ -420,7 +424,7 @@ func getActivityVersionDetails(path, activityId string, version uint, token stri defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -432,12 +436,11 @@ func getActivityVersionDetails(path, activityId string, version uint, token stri return } - - func deleteActivityVersion(path, activityId string, version uint, token string) (err error) { task := http.Client{} - req, err := http.NewRequest("DELETE", + req, err := http.NewRequest( + "DELETE", path+"/activities/"+activityId+"/versions/"+strconv.Itoa(int(version)), nil, ) @@ -450,10 +453,10 @@ func deleteActivityVersion(path, activityId string, version uint, token string) defer response.Body.Close() if response.StatusCode != http.StatusNoContent { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } return -} \ No newline at end of file +} diff --git a/da/api.go b/da/api.go index 1587150..90c4f36 100644 --- a/da/api.go +++ b/da/api.go @@ -1,12 +1,12 @@ package da -import "github.com/apprentice3d/forge-api-go-client/oauth" +import "github.com/woweh/forge-api-go-client/oauth" // API struct holds all paths necessary to access Design Automation API type API struct { - Authenticator oauth.ForgeAuthenticator + Authenticator oauth.ForgeAuthenticator DesignAutomationPath string - UploadAppURL string + UploadAppURL string } // NewAPI returns a DesignAutomation API client with default configurations @@ -18,21 +18,18 @@ func NewAPI(authenticator oauth.ForgeAuthenticator) API { } } - // UserId gives you the id used to identify the user func (api API) UserId() (nickname string, err error) { bearer, err := api.Authenticator.GetToken("code:all") if err != nil { return } - path := api.Authenticator.GetHostPath() + api.DesignAutomationPath + path := api.Authenticator.HostPath() + api.DesignAutomationPath nickname, err = getUserID(path, bearer.AccessToken) return } - - // EngineList lists all available Engines. func (api API) EngineList() (list EngineList, err error) { @@ -40,7 +37,7 @@ func (api API) EngineList() (list EngineList, err error) { if err != nil { return } - path := api.Authenticator.GetHostPath() + api.DesignAutomationPath + path := api.Authenticator.HostPath() + api.DesignAutomationPath list, err = listEngines(path, bearer.AccessToken) return @@ -53,23 +50,23 @@ func (api API) EngineDetails(id string) (list EngineDetails, err error) { if err != nil { return } - path := api.Authenticator.GetHostPath() + api.DesignAutomationPath + path := api.Authenticator.HostPath() + api.DesignAutomationPath list, err = getEngineDetails(path, id, bearer.AccessToken) return } - // CreateApp creates an app with given name and using specified engine -// name - should be unique and will be the appID -// engine - engineId to be used by this app (check EngineList) +// +// name - should be unique and will be the appID +// engine - engineId to be used by this app (check EngineList) func (api API) CreateApp(name, engine string) (app AppBundle, err error) { bearer, err := api.Authenticator.GetToken("code:all") if err != nil { return } - path := api.Authenticator.GetHostPath() + api.DesignAutomationPath + path := api.Authenticator.HostPath() + api.DesignAutomationPath app, err = createApp(path, name, engine, bearer.AccessToken) app.authenticator = api.Authenticator @@ -86,9 +83,6 @@ func (api API) CreateApp(name, engine string) (app AppBundle, err error) { return } - - - // AppList lists all available appbundles. func (api API) AppList() (list AppList, err error) { @@ -96,22 +90,23 @@ func (api API) AppList() (list AppList, err error) { if err != nil { return } - path := api.Authenticator.GetHostPath() + api.DesignAutomationPath + path := api.Authenticator.HostPath() + api.DesignAutomationPath list, err = listApps(path, bearer.AccessToken) return } // CreateActivity creates an activity given an app -// name - should be unique and will be the appID -// engine - engineId to be used by this app (check EngineList) +// +// name - should be unique and will be the appID +// engine - engineId to be used by this app (check EngineList) func (api API) CreateActivity(config ActivityConfig) (activity Activity, err error) { bearer, err := api.Authenticator.GetToken("code:all") if err != nil { return } - path := api.Authenticator.GetHostPath() + api.DesignAutomationPath + path := api.Authenticator.HostPath() + api.DesignAutomationPath activity, err = createActivity(path, config, bearer.AccessToken) activity.authenticator = api.Authenticator @@ -127,25 +122,6 @@ func (api API) CreateActivity(config ActivityConfig) (activity Activity, err err return } - - - - - - - - - - - - - - - - - - - // AppDelete will delete the app with specified id //func (api API) AppDelete(id string) (err error) { // @@ -158,7 +134,3 @@ func (api API) CreateActivity(config ActivityConfig) (activity Activity, err err // // return //} - - - - diff --git a/da/app.go b/da/app.go index 80375fb..8a98e07 100644 --- a/da/app.go +++ b/da/app.go @@ -6,15 +6,14 @@ import ( "encoding/xml" "errors" "fmt" - "github.com/apprentice3d/forge-api-go-client/oauth" - "io/ioutil" + "io" "log" "mime/multipart" "net/http" "strconv" -) - + "github.com/woweh/forge-api-go-client/oauth" +) type AppList struct { InfoList @@ -35,7 +34,7 @@ type FormData struct { } type AppParameters struct { - URL string `json:"endpointURL"` + URL string `json:"endpointURL"` Data FormData `json:"formData"` } @@ -52,7 +51,7 @@ type AppBundle struct { authenticator oauth.ForgeAuthenticator path string name string - uploadURL string + uploadURL string } type AppDetails struct { @@ -65,20 +64,16 @@ type CreateAppRequest struct { Engine string `json:"engine"` } - - - type AppUploadError struct { - Code string `xml:"Code"` - Message string `xml:"Message"` - Argument string `xml:"Argument"` + Code string `xml:"Code"` + Message string `xml:"Message"` + Argument string `xml:"Argument"` ArgumentValue string `xml:"ArgumentValue"` - Condition string `xml:"Condition"` - RequestID string `xml:"RequestId"` - HostID string `xml:"HostId"` + Condition string `xml:"Condition"` + RequestID string `xml:"RequestId"` + HostID string `xml:"HostId"` } - // Delete removes the AppBundle, including all versions and aliases. func (app *AppBundle) Delete() (err error) { @@ -102,18 +97,18 @@ func (app *AppBundle) Delete() (err error) { return } -//Details gets the details of the specified AppBundle, providing an alias +// Details gets the details of the specified AppBundle, providing an alias func (app *AppBundle) Details(alias string) (details AppDetails, err error) { bearer, err := app.authenticator.GetToken("code:all") if err != nil { return } - details, err = getAppDetails(app.path, app.ID + "+" + alias, bearer.AccessToken) + details, err = getAppDetails(app.path, app.ID+"+"+alias, bearer.AccessToken) return } -//Aliases lists all aliases for the specified AppBundle. +// Aliases lists all aliases for the specified AppBundle. func (app AppBundle) Aliases() (list AliasesList, err error) { bearer, err := app.authenticator.GetToken("code:all") if err != nil { @@ -124,7 +119,8 @@ func (app AppBundle) Aliases() (list AliasesList, err error) { return } -//CreateAlias creates a new alias for this AppBundle. +// CreateAlias creates a new alias for this AppBundle. +// // Limit: 1. Number of aliases (LimitAliases). func (app AppBundle) CreateAlias(alias string, version uint) (result Alias, err error) { bearer, err := app.authenticator.GetToken("code:all") @@ -136,7 +132,7 @@ func (app AppBundle) CreateAlias(alias string, version uint) (result Alias, err return } -//ModifyAlias will switch the given alias to another existing version +// ModifyAlias will switch the given alias to another existing version func (app AppBundle) ModifyAlias(alias string, version uint) (result Alias, err error) { bearer, err := app.authenticator.GetToken("code:all") if err != nil { @@ -147,7 +143,7 @@ func (app AppBundle) ModifyAlias(alias string, version uint) (result Alias, err return } -//AliasDetail gets the details on given alias +// AliasDetail gets the details on given alias func (app *AppBundle) AliasDetail(alias string) (details Alias, err error) { bearer, err := app.authenticator.GetToken("code:all") if err != nil { @@ -158,7 +154,7 @@ func (app *AppBundle) AliasDetail(alias string) (details Alias, err error) { return } -//DeleteAlias the alias for this AppBundle. +// DeleteAlias the alias for this AppBundle. func (app AppBundle) DeleteAlias(alias string) (err error) { bearer, err := app.authenticator.GetToken("code:all") if err != nil { @@ -169,7 +165,7 @@ func (app AppBundle) DeleteAlias(alias string) (err error) { return } -//Versions lists all aliases for the specified AppBundle. +// Versions lists all aliases for the specified AppBundle. func (app AppBundle) Versions() (list VersionList, err error) { bearer, err := app.authenticator.GetToken("code:all") if err != nil { @@ -193,7 +189,6 @@ func (app AppBundle) CreateVersion(engine string) (result AppBundle, err error) return } - func (app *AppBundle) VersionDetails(version uint) (details AppData, err error) { bearer, err := app.authenticator.GetToken("code:all") if err != nil { @@ -204,7 +199,6 @@ func (app *AppBundle) VersionDetails(version uint) (details AppData, err error) return } - func (app AppBundle) DeleteVersion(version uint) (err error) { bearer, err := app.authenticator.GetToken("code:all") if err != nil { @@ -215,23 +209,17 @@ func (app AppBundle) DeleteVersion(version uint) (err error) { return } - - -func (app AppBundle) Upload(data []byte) (err error){ +func (app AppBundle) Upload(data []byte) (err error) { err = uploadApp(app.uploadURL, app.Parameters.Data, data) return } - - /* * SUPPORT FUNCTIONS */ - - /* APPBUNDLE */ @@ -252,7 +240,7 @@ func listApps(path string, token string) (list AppList, err error) { defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -293,7 +281,7 @@ func createApp(path, name, engine, token string) (result AppBundle, err error) { defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -326,7 +314,7 @@ func getAppDetails(path, appID, token string) (result AppDetails, err error) { defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -354,7 +342,7 @@ func deleteApp(path string, id string, token string) (err error) { defer response.Body.Close() if response.StatusCode != http.StatusNoContent { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -362,7 +350,6 @@ func deleteApp(path string, id string, token string) (err error) { return } - /* ALIASES */ @@ -383,7 +370,7 @@ func listAppAliases(path string, appName, token string) (list AliasesList, err e defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -424,7 +411,7 @@ func createAppAlias(path, appName, alias string, version uint, token string) (re defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -465,7 +452,7 @@ func modifyAppAlias(path, appName, alias string, version uint, token string) (re defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -498,7 +485,7 @@ func getAliasDetails(path, appName, alias, token string) (result Alias, err erro defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -510,8 +497,6 @@ func getAliasDetails(path, appName, alias, token string) (result Alias, err erro return } - - func deleteAppAlias(path string, appName, alias, token string) (err error) { task := http.Client{} @@ -528,7 +513,7 @@ func deleteAppAlias(path string, appName, alias, token string) (err error) { defer response.Body.Close() if response.StatusCode != http.StatusNoContent { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -536,7 +521,6 @@ func deleteAppAlias(path string, appName, alias, token string) (err error) { return } - /* VERSIONS */ @@ -557,7 +541,7 @@ func listAppVersions(path string, appName, token string) (list VersionList, err defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -573,7 +557,7 @@ func createAppVersion(path, appName, engine string, token string) (result AppBun task := http.Client{} body, err := json.Marshal( - struct{ + struct { Engine string `json:"engine"` }{engine}) if err != nil { @@ -597,7 +581,7 @@ func createAppVersion(path, appName, engine string, token string) (result AppBun defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -609,8 +593,6 @@ func createAppVersion(path, appName, engine string, token string) (result AppBun return } - - func getVersionDetails(path, appName string, version uint, token string) (result AppData, err error) { task := http.Client{} @@ -632,7 +614,7 @@ func getVersionDetails(path, appName string, version uint, token string) (result defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -644,8 +626,6 @@ func getVersionDetails(path, appName string, version uint, token string) (result return } - - func deleteAppVersion(path string, appName string, version uint, token string) (err error) { task := http.Client{} @@ -662,7 +642,7 @@ func deleteAppVersion(path string, appName string, version uint, token string) ( defer response.Body.Close() if response.StatusCode != http.StatusNoContent { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -670,7 +650,6 @@ func deleteAppVersion(path string, appName string, version uint, token string) ( return } - func uploadApp(path string, formData FormData, data []byte) (err error) { body := &bytes.Buffer{} @@ -722,7 +701,6 @@ func uploadApp(path string, formData FormData, data []byte) (err error) { return } - err = errors.New(fmt.Sprintf("[%d][%s] - %s {%s}", response.StatusCode, errorDetails.Code, @@ -733,4 +711,4 @@ func uploadApp(path string, formData FormData, data []byte) (err error) { } return -} \ No newline at end of file +} diff --git a/da/engine.go b/da/engine.go index 0bc670f..5b8168e 100644 --- a/da/engine.go +++ b/da/engine.go @@ -3,7 +3,7 @@ package da import ( "encoding/json" "errors" - "io/ioutil" + "io" "net/http" "strconv" ) @@ -14,13 +14,11 @@ type EngineList struct { type EngineDetails struct { ProductVersion string `json:"productVersion"` - Description string `json:"description"` - Version uint `json:"version"` - Id string `json:"id"` + Description string `json:"description"` + Version uint `json:"version"` + Id string `json:"id"` } - - func listEngines(path string, token string) (list EngineList, err error) { task := http.Client{} @@ -37,7 +35,7 @@ func listEngines(path string, token string) (list EngineList, err error) { defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -48,7 +46,6 @@ func listEngines(path string, token string) (list EngineList, err error) { return } - func getEngineDetails(path string, engineID string, token string) (details EngineDetails, err error) { task := http.Client{} @@ -65,7 +62,7 @@ func getEngineDetails(path string, engineID string, token string) (details Engin defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -74,4 +71,4 @@ func getEngineDetails(path string, engineID string, token string) (details Engin err = decoder.Decode(&details) return -} \ No newline at end of file +} diff --git a/da/test/da_test.go b/da/test/da_test.go index af3ed82..055ce91 100644 --- a/da/test/da_test.go +++ b/da/test/da_test.go @@ -4,11 +4,12 @@ import ( "bytes" "encoding/json" "encoding/xml" - "github.com/apprentice3d/forge-api-go-client/da" - "github.com/apprentice3d/forge-api-go-client/oauth" "os" "reflect" "testing" + + "github.com/woweh/forge-api-go-client/da" + "github.com/woweh/forge-api-go-client/oauth" ) func TestAPI_UserId(t *testing.T) { @@ -83,7 +84,6 @@ func TestAPI_AppBundle(t *testing.T) { clientID := os.Getenv("FORGE_CLIENT_ID") clientSecret := os.Getenv("FORGE_CLIENT_SECRET") - authenticator := oauth.NewTwoLegged(clientID, clientSecret) daApi := da.NewAPI(authenticator) @@ -487,8 +487,6 @@ func TestAppBundle_Upload(t *testing.T) { }) } - - func TestAPI_Activity(t *testing.T) { // prepare the credentials clientID := os.Getenv("FORGE_CLIENT_ID") @@ -511,10 +509,9 @@ func TestAPI_Activity(t *testing.T) { t.Run("Create an activity", func(t *testing.T) { config := da.ActivityConfig{ - ID: testActivityName, - Engine:testEngine, - CommandLine:[]string{"dir"}, - + ID: testActivityName, + Engine: testEngine, + CommandLine: []string{"dir"}, } testActivity, err = daApi.CreateActivity(config) @@ -601,19 +598,6 @@ func TestAPI_Activity(t *testing.T) { }) } - - - - - - - - - - - - - func TestAPI_DataParsing(t *testing.T) { t.Run("Check EngineList struct parsing", func(t *testing.T) { jsonString := ` @@ -808,16 +792,10 @@ func TestAPI_DataParsing(t *testing.T) { t.Fatal(err.Error()) } - if !reflect.DeepEqual(result, response) { t.Fatal("failed to properly parse the Activity JSON") } - - - - - }) } diff --git a/da/types.go b/da/types.go index 0d774ac..4a3e12a 100644 --- a/da/types.go +++ b/da/types.go @@ -5,18 +5,17 @@ type InfoList struct { Data []string `json:"data"` } - type AliasesList struct { Pagination string `json:"paginationToken"` Data []Alias `json:"data"` } type VersionList struct { - Pagination string `json:"paginationToken"` + Pagination string `json:"paginationToken"` Data []uint `json:"data"` } type Alias struct { ID string `json:"id"` Version uint `json:"version"` -} \ No newline at end of file +} diff --git a/da/user.go b/da/user.go index 2564b41..caa7e49 100644 --- a/da/user.go +++ b/da/user.go @@ -2,7 +2,7 @@ package da import ( "errors" - "io/ioutil" + "io" "net/http" "strconv" "strings" @@ -23,20 +23,19 @@ func getUserID(path string, token string) (nickname string, err error) { defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } - data, err := ioutil.ReadAll(response.Body) + data, err := io.ReadAll(response.Body) if err != nil { return } //TODO: Review why the data has quotes in its content and find a more elegant way to remove them - nickname = strings.Replace(string(data), "\"", "", -1 ) - + nickname = strings.Replace(string(data), "\"", "", -1) return } diff --git a/dm/assets/rst_basic_sample_project.rvt b/dm/assets/rst_basic_sample_project.rvt new file mode 100644 index 0000000..113c73b Binary files /dev/null and b/dm/assets/rst_basic_sample_project.rvt differ diff --git a/dm/bucket.go b/dm/bucket.go index 08e836c..c15ae9d 100644 --- a/dm/bucket.go +++ b/dm/bucket.go @@ -4,82 +4,173 @@ import ( "bytes" "encoding/json" "errors" - "github.com/apprentice3d/forge-api-go-client/oauth" - "io/ioutil" + "io" "net/http" + "net/url" "strconv" + + "github.com/woweh/forge-api-go-client" + "github.com/woweh/forge-api-go-client/oauth" ) +// NewOssApi returns an OSS API client with default configurations and populates the RelativePath. +func NewOssApi(authenticator oauth.ForgeAuthenticator, region forge.Region) OssAPI { + return OssAPI{ + Authenticator: authenticator, + relativePath: "/oss/v2/buckets", + region: region, + } +} +// Region of the OSS API. +func (a *OssAPI) Region() forge.Region { + return a.region +} -// NewBucketAPIWithCredentials returns a Bucket API client with default configurations -func NewBucketAPI(authenticator oauth.ForgeAuthenticator) BucketAPI { - return BucketAPI{ - authenticator, - "/oss/v2/buckets", - } +// SetRegion sets the Region of the OSS API. +func (a *OssAPI) SetRegion(region forge.Region) { + a.region = region } +// RelativePath of the OSS API. +func (a *OssAPI) RelativePath() string { + return a.relativePath +} -// CreateBucket creates and returns details of created bucket, or an error on failure -func (api BucketAPI) CreateBucket(bucketKey, policyKey string) (result BucketDetails, err error) { +// BaseUrl of the OSS API. +func (a *OssAPI) BaseUrl() string { + return a.Authenticator.HostPath() + a.relativePath +} - bearer, err := api.Authenticator.GetToken("bucket:create") +// RetentionPolicy applies to all objects that are stored in a bucket. +// - This cannot be changed at a later time! +// +// When creating a bucket, specifically set the policyKey to transient, temporary, or persistent. +type RetentionPolicy string + +const ( + // PolicyTransient - Think of this type of storage as a cache. Use it for ephemeral results. + // For example, you might use this for objects that are part of producing other persistent artifacts, but otherwise are not required to be available later. + // Objects older than 24 hours are removed automatically. + // Each upload of an object is considered unique, so, for example, if the same rendering is uploaded multiple times, each of them will have its own retention period of 24 hours. + PolicyTransient RetentionPolicy = "transient" + + // PolicyTemporary - This type of storage is suitable for artifacts produced for user-uploaded content where after some period of activity, the user may rarely access the artifacts. + // When an object has reached 30 days of age, it is deleted. + PolicyTemporary RetentionPolicy = "temporary" + + // PolicyPersistent - Persistent storage is intended for user data. + // When a file is uploaded, the owner should expect this item to be available for as long as the owner account is active, or until he or she deletes the item. + PolicyPersistent RetentionPolicy = "persistent" +) + +// CreateBucket creates and returns details of created bucket, or an error on failure. +// The region is taken from the OssAPI instance. +// - bucketKey: A unique name you assign to a bucket. It must be globally unique across all applications and regions, otherwise the call will fail. +// Possible values: -_.a-z0-9 (between 3-128 characters in length). +// Note that you cannot change a bucket key. +// - policyKey: Data retention policy. Acceptable values: transient, temporary, persistent. +// This cannot be changed at a later time. The retention policy on the bucket applies to all objects stored within. +// +// References: +// - https://aps.autodesk.com/en/docs/data/v2/reference/http/buckets-POST/ +func (a *OssAPI) CreateBucket(bucketKey string, policyKey RetentionPolicy) (result BucketDetails, err error) { + + bearer, err := a.Authenticator.GetToken("bucket:create") if err != nil { return } - path := api.Authenticator.GetHostPath() + api.BucketAPIPath - result, err = createBucket(path, bucketKey, policyKey, bearer.AccessToken) + + result, err = createBucket(a.BaseUrl(), bucketKey, policyKey, bearer.AccessToken, a.region) return } // DeleteBucket deletes bucket given its key. -// WARNING: The bucket delete call is undocumented. -func (api BucketAPI) DeleteBucket(bucketKey string) error { - bearer, err := api.Authenticator.GetToken("bucket:delete") +// - The bucket must be owned by the application. +// - We recommend only deleting small buckets used for acceptance testing or prototyping, since it can take a long time for a bucket to be deleted. +// - Note that the bucket name will not be immediately available for reuse. +// +// References: +// - https://aps.autodesk.com/en/docs/data/v2/reference/http/buckets-:bucketKey-DELETE/ +func (a *OssAPI) DeleteBucket(bucketKey string) error { + bearer, err := a.Authenticator.GetToken("bucket:delete") if err != nil { return err } - path := api.Authenticator.GetHostPath() + api.BucketAPIPath - return deleteBucket(path, bucketKey, bearer.AccessToken) + return deleteBucket(a.BaseUrl(), bucketKey, bearer.AccessToken) } // ListBuckets returns a list of all buckets created or associated with Forge secrets used for token creation -func (api BucketAPI) ListBuckets(region, limit, startAt string) (result ListedBuckets, err error) { - bearer, err := api.Authenticator.GetToken("bucket:read") +// +// References: +// - https://aps.autodesk.com/en/docs/data/v2/reference/http/buckets-GET/ +func (a *OssAPI) ListBuckets(region forge.Region, limit, startAt string) (result BucketList, err error) { + + // init the result + result = BucketList{} + +loop: + bearer, err := a.Authenticator.GetToken("bucket:read") if err != nil { return } - path := api.Authenticator.GetHostPath()+ api.BucketAPIPath - return listBuckets(path, region, limit, startAt, bearer.AccessToken) + tmpResult, err := listBuckets(a.BaseUrl(), region, limit, startAt, bearer.AccessToken) + if err != nil { + return + } + + // append the result + result = append(result, tmpResult.Items...) + + // if there are more items, get them + for tmpResult.Next != "" { + // extract the startAt from the next link + startAt, err = extractStartAt(tmpResult.Next) + if err != nil { + return + } + goto loop + } + + return +} + +func extractStartAt(nextUrl string) (startAt string, err error) { + parsedUrl, err := url.Parse(nextUrl) + if err != nil { + return "", err + } + startAt = parsedUrl.Query().Get("startAt") + if startAt == "" { + return "", errors.New("startAt not found in next url") + } + return startAt, nil } // GetBucketDetails returns information associated to a bucket. See BucketDetails struct. -func (api BucketAPI) GetBucketDetails(bucketKey string) (result BucketDetails, err error) { - bearer, err := api.Authenticator.GetToken("bucket:read") +// +// References: +// - https://aps.autodesk.com/en/docs/data/v2/reference/http/buckets-:bucketKey-details-GET/ +func (a *OssAPI) GetBucketDetails(bucketKey string) (result BucketDetails, err error) { + bearer, err := a.Authenticator.GetToken("bucket:read") if err != nil { return } - path := api.Authenticator.GetHostPath() + api.BucketAPIPath - return getBucketDetails(path, bucketKey, bearer.AccessToken) + return getBucketDetails(a.BaseUrl(), bucketKey, bearer.AccessToken) } - - /* * SUPPORT FUNCTIONS */ + func getBucketDetails(path, bucketKey, token string) (result BucketDetails, err error) { task := http.Client{} - req, err := http.NewRequest("GET", - path+"/"+bucketKey+"/details", - nil, - ) + req, err := http.NewRequest("GET", path+"/"+bucketKey+"/details", nil) if err != nil { return @@ -93,7 +184,7 @@ func getBucketDetails(path, bucketKey, token string) (result BucketDetails, err defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -104,13 +195,10 @@ func getBucketDetails(path, bucketKey, token string) (result BucketDetails, err return } -func listBuckets(path, region, limit, startAt, token string) (result ListedBuckets, err error) { +func listBuckets(path string, region forge.Region, limit, startAt, token string) (result ListedBuckets, err error) { task := http.Client{} - req, err := http.NewRequest("GET", - path, - nil, - ) + req, err := http.NewRequest("GET", path, nil) if err != nil { return @@ -118,7 +206,7 @@ func listBuckets(path, region, limit, startAt, token string) (result ListedBucke params := req.URL.Query() if len(region) != 0 { - params.Add("region", region) + params.Add("region", string(region)) } if len(limit) != 0 { params.Add("limit", limit) @@ -137,19 +225,19 @@ func listBuckets(path, region, limit, startAt, token string) (result ListedBucke defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } - decoder := json.NewDecoder(response.Body) - err = decoder.Decode(&result) + err = json.NewDecoder(response.Body).Decode(&result) - //TODO: address the pagination of buckets return } -func createBucket(path, bucketKey, policyKey, token string) (result BucketDetails, err error) { +func createBucket( + path, bucketKey string, policyKey RetentionPolicy, token string, region forge.Region, +) (result BucketDetails, err error) { task := http.Client{} @@ -157,21 +245,20 @@ func createBucket(path, bucketKey, policyKey, token string) (result BucketDetail CreateBucketRequest{ bucketKey, policyKey, - }) + }, + ) if err != nil { return } - req, err := http.NewRequest("POST", - path, - bytes.NewReader(body), - ) - + req, err := http.NewRequest("POST", path, bytes.NewReader(body)) if err != nil { return } + req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("x-ads-region", string(region)) response, err := task.Do(req) if err != nil { return @@ -179,7 +266,7 @@ func createBucket(path, bucketKey, policyKey, token string) (result BucketDetail defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -194,11 +281,7 @@ func createBucket(path, bucketKey, policyKey, token string) (result BucketDetail func deleteBucket(path, bucketKey, token string) (err error) { task := http.Client{} - req, err := http.NewRequest("DELETE", - path+"/"+bucketKey, - nil, - ) - + req, err := http.NewRequest("DELETE", path+"/"+bucketKey, nil) if err != nil { return } @@ -211,7 +294,7 @@ func deleteBucket(path, bucketKey, token string) (err error) { defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } diff --git a/dm/doc.go b/dm/doc.go index 81a6941..d144b65 100644 --- a/dm/doc.go +++ b/dm/doc.go @@ -1,8 +1,12 @@ -// Package md contains the Go wrappers for calls to Data Management API -// https://developer.autodesk.com/en/docs/data/v2/overview/ -// -// The API offers the following features: -// -// - Access data from Autodesk SaaS applications; -// - Manage and store files from your app on the Forge platform, independent of any Autodesk SaaS application; +/* +Package dm provides wrappers for the Data Management V2 REST API. +https://aps.autodesk.com/data-management-api +https://aps.autodesk.com/en/docs/data/v2/developers_guide/ + +At the time of writing (2023/14/06), only the Object Storage Service (OSS) is supported. + +The API offers the following features: +- Create, list, read, update and delete buckets +- Upload, list and download objects (=> CAD files) +*/ package dm diff --git a/dm/hubs.go b/dm/hubs.go index 97b5cb7..2e3c85e 100644 --- a/dm/hubs.go +++ b/dm/hubs.go @@ -1,4 +1,5 @@ package dm + // //type Hubs struct { // Data []Content `json:"data,omitempty"` diff --git a/dm/object.go b/dm/object.go index b3e2109..8a7876b 100644 --- a/dm/object.go +++ b/dm/object.go @@ -1,63 +1,68 @@ package dm import ( - "bytes" "encoding/json" "errors" - "io/ioutil" + "fmt" + "io" "net/http" "strconv" ) +// ListObjects returns the bucket contains along with details on each item. +func (a *OssAPI) ListObjects(bucketKey, limit, beginsWith, startAt string) (result BucketContent, err error) { - -// UploadObject adds to specified bucket the given data (can originate from a multipart-form or direct file read). -// Return details on uploaded object, including the object URN. Check ObjectDetails struct. -func (api BucketAPI) UploadObject(bucketKey string, objectName string, data []byte) (result ObjectDetails, err error) { - bearer, err := api.Authenticator.GetToken("data:write") + bearer, err := a.Authenticator.GetToken("data:read") if err != nil { return } - path := api.Authenticator.GetHostPath() + api.BucketAPIPath - return uploadObject(path, bucketKey, objectName, data, bearer.AccessToken) + result, err = listObjects(a.BaseUrl(), bucketKey, limit, beginsWith, startAt, bearer.AccessToken) + + return } -// ListObjects returns the bucket contains along with details on each item. -func (api BucketAPI) ListObjects(bucketKey, limit, beginsWith, startAt string) (result BucketContent, err error) { - bearer, err := api.Authenticator.GetToken("data:read") +// DownloadObject downloads an on object, given the URL-encoded object name. +func (a *OssAPI) DownloadObject(bucketKey string, objectName string) (result []byte, err error) { + + bearer, err := a.Authenticator.GetToken("data:read") + if err != nil { + return + } + + downloadUrl, err := getSignedDownloadUrl(a.BaseUrl(), bucketKey, objectName, bearer.AccessToken) if err != nil { return } - path := api.Authenticator.GetHostPath() + api.BucketAPIPath - return listObjects(path, bucketKey, limit, beginsWith, startAt, bearer.AccessToken) + result, err = downloadObjectUsingSignedUrl(&downloadUrl) + + return } +// UploadObject adds to specified bucket the given data (can originate from a multipart-form or direct file read). +// Return details on uploaded object, including the object URN (> ObjectId). Check uploadOkResult struct. +func (a *OssAPI) UploadObject(bucketKey, objectName, fileToUpload string) (result UploadResult, err error) { -// DownloadObject downloads an on object, given the URL-encoded object name. -func (api BucketAPI) DownloadObject(bucketKey string, objectName string) (result []byte, err error) { - bearer, err := api.Authenticator.GetToken("data:read") + job, err := newUploadJob(a, bucketKey, objectName, fileToUpload) if err != nil { return } - path := api.Authenticator.GetHostPath() + api.BucketAPIPath - return downloadObject(path, bucketKey, objectName, bearer.AccessToken) -} + result, err = job.uploadFile() + return +} /* * SUPPORT FUNCTIONS */ func listObjects(path, bucketKey, limit, beginsWith, startAt, token string) (result BucketContent, err error) { + task := http.Client{} - req, err := http.NewRequest("GET", - path + "/" + bucketKey + "/objects", - nil, - ) + req, err := http.NewRequest("GET", path+"/"+bucketKey+"/objects", nil) if err != nil { return @@ -84,79 +89,82 @@ func listObjects(path, bucketKey, limit, beginsWith, startAt, token string) (res defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } - decoder := json.NewDecoder(response.Body) - err = decoder.Decode(&result) + err = json.NewDecoder(response.Body).Decode(&result) return } -func uploadObject(path, bucketKey, objectName string, data []byte, token string) (result ObjectDetails, err error) { - - task := http.Client{} +// signedDownloadUrl reflects the response from the "signeds3download" endpoint. +type signedDownloadUrl struct { + Status string `json:"status"` + Url string `json:"url"` + Params struct { + ContentType string `json:"content-type"` + ContentDisposition string `json:"content-disposition"` + } `json:"params"` + Size int `json:"size"` + Sha1 string `json:"sha1"` +} - dataContent := bytes.NewReader(data) - req, err := http.NewRequest("PUT", - path+"/"+ bucketKey + "/objects/" + objectName, - dataContent) +func getSignedDownloadUrl(path, bucketKey, objectName string, token string) (result signedDownloadUrl, err error) { + req, err := http.NewRequest("GET", path+"/"+bucketKey+"/objects/"+objectName+"/signeds3download", nil) if err != nil { return } - req.Header.Set("Authorization", "Bearer "+token) - response, err := task.Do(req) + task := http.Client{} + response, err := task.Do(req) if err != nil { return } defer response.Body.Close() - if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + if response.StatusCode == http.StatusOK { + err = json.NewDecoder(response.Body).Decode(&result) + } else { + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) - return } - decoder := json.NewDecoder(response.Body) - err = decoder.Decode(&result) - return - } -func downloadObject(path, bucketKey, objectName string, token string) (result []byte, err error) { - - task := http.Client{} - - req, err := http.NewRequest("GET", - path+"/"+ bucketKey + "/objects/" + objectName, - nil) +func downloadObjectUsingSignedUrl(s *signedDownloadUrl) (result []byte, err error) { + req, err := http.NewRequest("GET", s.Url, nil) if err != nil { return } - req.Header.Set("Authorization", "Bearer "+token) + task := http.Client{} response, err := task.Do(req) - if err != nil { return } defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } - result,err = ioutil.ReadAll(response.Body) + result, err = io.ReadAll(response.Body) + if err != nil { + return + } - return + receivedSize := len(result) + if receivedSize != s.Size { + err = fmt.Errorf("the file size doesn't match, expected %v, but received %v", s.Size, receivedSize) + } -} \ No newline at end of file + return +} diff --git a/dm/s3Upload.go b/dm/s3Upload.go new file mode 100644 index 0000000..ae0b41e --- /dev/null +++ b/dm/s3Upload.go @@ -0,0 +1,494 @@ +package dm + +// Instructions for the S3 update: +// https://forge.autodesk.com/blog/data-management-oss-object-storage-service-migrating-direct-s3-approach +/* + Direct-to-S3 approach for Data Management OSS + To upload and download files, applications must generate a signed URL, then upload or download the binary. Here are the steps (pseudo code): + + Upload + ======== + + 1. Calculate the number of parts of the file to upload + Note: Each uploaded part except for the last one must be at least 5MB (1024 * 5) + + 2. Generate up to 25 URLs for uploading specific parts of the file using the + GET buckets/:bucketKey/objects/:objectKey/signeds3upload?firstPart=&parts= + endpoint. + a) The part numbers start with 1 + b) For example, to generate upload URLs for parts 10 through 15, set firstPart to 10 and parts to 6 + c) This endpoint also returns an uploadKey that is used later to request additional URLs or to finalize the upload + + 3. Upload remaining parts of the file to their corresponding upload URLs + a) Consider retrying (for example, with an exponential backoff) individual uploads when the response code is 100-199, 429, or 500-599 + b) If the response code is 403, the upload URLs have expired; go back to step #2 + c) If you have used up all the upload URLs and there are still parts that must be uploaded, go back to step #2 + + 4. Finalize the upload using the POST buckets/:bucketKey/objects/:objectKey/signeds3upload endpoint, using the uploadKey value from step #2 +*/ + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net/http" + "os" + "path" + "strconv" + "time" +) + +const ( + // megaByte is 1048576 byte + megaByte = 1 << 20 + + // maxParts is the maximum number of parts returned by the "signeds3upload" endpoint. + maxParts = 25 + + // signedS3UploadEndpoint is the name of the signeds3upload endpoint. + signedS3UploadEndpoint = "signeds3upload" + + // minutesExpiration is the expiration period of the signed upload URLs. + // Autodesk default is 2 minutes (1 to 60 minutes). + minutesExpiration = 60 +) + +var ( + // defaultChunkSize is the default size of download/upload chunks. + // NOTE: + // The minimum size seems to be 5 MB. + // Using a value < 5 MB causes errors when completing the upload [400, TooSmall]. + defaultChunkSize = int64(100 * megaByte) +) + +// uploadJob provides information for uploading a file +type uploadJob struct { + // api is a pointer to an instance of the OssAPI. + api *OssAPI + // bucketKey is the key (= name) of the bucket where the file shall be stored. + bucketKey string + // objectKey is the key (= name) of the file in the Autodesk cloud (OSS). + objectKey string + // fileToUpload is the path of the file to upload. + fileToUpload string + // minutesExpiration is the custom expiration time within a 1 to 60 minutes range. + minutesExpiration int + // fileSize is the size of the file to upload. + fileSize int64 + // totalParts is the total number of parts to process. + totalParts int + // numberOfBatches indicates the number of 'batches' that must be processed, how often signed URLs must be requested. + // If totalParts > maxParts, then we need to request signedUploadUrls multiple times. + numberOfBatches int + // uploadKey is the identifier of the upload session, to differentiate multiple attempts to upload data for the same object. + // This must be provided when re-requesting chunk URLs for the same blob if they expire, and when calling the Complete Upload endpoint. + uploadKey string +} + +// signedUploadUrls reflects the response from the signedS3UploadEndpoint. +type signedUploadUrls struct { + UploadKey string `json:"uploadKey"` + UploadExpiration time.Time `json:"uploadExpiration"` + UrlExpiration time.Time `json:"urlExpiration"` + Urls []string `json:"urls"` +} + +func newUploadJob(api *OssAPI, bucketKey, objectName, fileToUpload string) (job uploadJob, err error) { + + fileInfo, err := os.Stat(fileToUpload) + if err != nil { + return + } + + // Determine the required number of parts + // - In the examples, typically a chunk size of 5 or 10 MB is used. + // - In the old API, the boundary for multipart uploads was 100 MB. + // => See const defaultChunkSize + totalParts := ceilingOfIntDivision(int(fileInfo.Size()), int(defaultChunkSize)) + numberOfBatches := ceilingOfIntDivision(totalParts, maxParts) + + job = uploadJob{ + api: api, + bucketKey: bucketKey, + objectKey: objectName, + fileToUpload: fileToUpload, + minutesExpiration: minutesExpiration, + fileSize: fileInfo.Size(), + totalParts: totalParts, + numberOfBatches: numberOfBatches, + uploadKey: "", + } + + log.Println("New upload job:") + log.Println("- bucketKey:", bucketKey) + log.Println("- objectName:", objectName) + log.Println("- fileToUpload:", fileToUpload) + log.Println("- fileSize:", job.fileSize) + log.Println("- totalParts:", job.totalParts) + log.Println("- numberOfBatches:", job.numberOfBatches) + + return +} + +func ceilingOfIntDivision(x, y int) int { + // https://stackoverflow.com/a/54006084 + // https://stackoverflow.com/a/2745086 + return 1 + (x-1)/y +} + +func (job *uploadJob) uploadFile() (result UploadResult, err error) { + + file, err := os.Open(job.fileToUpload) + if err != nil { + return + } + defer file.Close() + + log.Println("Start uploading file...") + + partsCounter := 0 + for i := 0; i < job.numberOfBatches; i++ { + + firstPart := (i * maxParts) + 1 + + parts := job.getParts(partsCounter) + + // generate signed S3 upload url(s) + log.Println("- getting signed URLs...") + uploadUrls, err2 := job.getSignedUploadUrlsWithRetries(firstPart, parts) + if err2 != nil { + err2 = fmt.Errorf("error getting signed URLs for parts %v-%v :\n%w", firstPart, parts, err2) + return result, err2 + } + + log.Println("- UploadKey: ", uploadUrls.UploadKey) + log.Println("- number of signed URLs: ", len(uploadUrls.Urls)) + + if i == 0 { + // remember the uploadKey when requesting signed URLs for the first time + job.uploadKey = uploadUrls.UploadKey + } + + // upload the file in chunks to the signed url(s) + for _, url := range uploadUrls.Urls { + + // read a chunk of the file + bytesSlice := make([]byte, defaultChunkSize) + + bytesRead, err3 := file.Read(bytesSlice) + if err3 != nil { + if err3 != io.EOF { + err3 = fmt.Errorf("error reading the file to upload:\n%w", err3) + return result, err3 + } + // EOF reached + } + + // upload the chunk to the signed URL + if bytesRead > 0 { + buffer := bytes.NewBuffer(bytesSlice[:bytesRead]) + err3 = uploadChunkWithRetries(url, buffer) + if err3 != nil { + err3 = fmt.Errorf("error uploading a chunk to URL:\n- %v\n%w", url, err3) + return result, err3 + } + log.Println("- number of bytes sent: ", bytesRead) + } + } + + partsCounter += parts + } + + // complete the upload + log.Println("- completing upload...") + result, err = job.completeUploadWithRetries() + if err != nil { + err = fmt.Errorf("error completing the upload:\n%w", err) + return result, err + } + log.Println("Finished uploading the file:") + log.Println("- ObjectId: ", result.ObjectId) + log.Println("- Location: ", result.Location) + log.Println("- Size: ", result.Size) + + return result, err +} + +// getParts gets the number of parts that must be processed in this batch. +func (job *uploadJob) getParts(partsCounter int) int { + + parts := maxParts + + if job.totalParts < (partsCounter + maxParts) { + // Say totalParts = 20: part[0]=20, firstPart[0]=1 + // Say totalParts = 30: part[0]=25, firstPart[0]=1, part[1]= 5, firstPart[1]=26 + // Say totalParts = 40: part[0]=25, firstPart[0]=1, part[1]=15, firstPart[1]=26 + // Say totalParts = 50: part[0]=25, firstPart[0]=1, part[1]=25, firstPart[1]=26 + parts = job.totalParts - partsCounter + } + + return parts +} + +// getSignedUploadUrlsWithRetries calls the signedS3UploadEndpoint to get parts signed URLs, retrying max 3 times. +// https://forge.autodesk.com/en/docs/data/v2/reference/http/buckets-:bucketKey-objects-:objectKey-signeds3upload-GET/ +func (job *uploadJob) getSignedUploadUrlsWithRetries(firstPart, parts int) (result signedUploadUrls, err error) { + + var statusCode int + + for i := 0; i < 3; i++ { + + statusCode, result, err = job.getSignedUploadUrls(firstPart, parts) + + // 429 - RATE-LIMIT EXCEEDED + // The maximum number of API calls that a Forge application can make _PER MINUTE_ was exceeded. + // 500 - INTERNAL SERVER ERROR + if statusCode == 429 || statusCode == 500 { + // retry in 1 minute + time.Sleep(1 * time.Minute) + } else { + // done + break + } + } + + return result, err +} + +// getSignedUploadUrls calls the signedS3UploadEndpoint to get parts signed URLs. +func (job *uploadJob) getSignedUploadUrls(firstPart, parts int) (statusCode int, result signedUploadUrls, err error) { + + // - https://forge.autodesk.com/en/docs/data/v2/reference/http/buckets-:bucketKey-objects-:objectKey-signeds3upload-GET/ + + // - https://forge.autodesk.com/en/docs/data/v2/tutorials/upload-file/#step-4-generate-a-signed-s3-url + // - https://forge.autodesk.com/en/docs/data/v2/tutorials/app-managed-bucket/#step-2-initiate-a-direct-to-s3-multipart-upload + + accessToken, err := job.authenticate() + if err != nil { + return + } + + // request the signed urls + req, err := http.NewRequest("GET", job.getSignedS3UploadPath(), nil) + if err != nil { + return + } + + addOrSetHeader(req, "Authorization", "Bearer "+accessToken) + + // appending to existing query args + q := req.URL.Query() + if job.uploadKey != "" { + q.Add("uploadKey", job.uploadKey) + } + q.Add("firstPart", strconv.Itoa(firstPart)) + q.Add("parts", strconv.Itoa(parts)) + q.Add("minutesExpiration", strconv.Itoa(job.minutesExpiration)) + // assign encoded query string to http request + req.URL.RawQuery = q.Encode() + + task := http.Client{} + + response, err := task.Do(req) + if err != nil { + return + } + defer response.Body.Close() + + statusCode = response.StatusCode + + if response.StatusCode != http.StatusOK { + content, _ := io.ReadAll(response.Body) + err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) + return + } + + err = json.NewDecoder(response.Body).Decode(&result) + + return +} + +// uploadChunkWithRetries uploads a chunk of bytes to a given signedUrl, retrying max 3 times. +func uploadChunkWithRetries(signedUrl string, buffer *bytes.Buffer) (err error) { + + var ( + statusCode int + + // A backoff schedule for when and how often to retry failed HTTP + // requests. The first element is the time to wait after the + // first failure, the second the time to wait after the second + // failure, etc. After reaching the last element, retries stop + // and the request is considered failed. + // https://brandur.org/fragments/go-http-retry + backoffSchedule = []time.Duration{ + 1 * time.Second, + 3 * time.Second, + 10 * time.Second, + } + ) + + for _, backoff := range backoffSchedule { + + statusCode, err = uploadChunk(signedUrl, buffer) + + // Consider retrying (for example, with an exponential backoff) individual uploads when the + // response code is 100-199, 429, or 500-599 + if (statusCode >= 100 && statusCode <= 199) || + statusCode == 429 || + (statusCode >= 500 && statusCode <= 599) { + // retry + time.Sleep(backoff) + } else { + // done + break + } + } + + return err +} + +// uploadChunk uploads a chunk of bytes to a given signedUrl. +func uploadChunk(signedUrl string, buffer *bytes.Buffer) (statusCode int, err error) { + + req, err := http.NewRequest("PUT", signedUrl, buffer) + if err != nil { + return + } + + l := buffer.Len() + + req.ContentLength = int64(l) + addOrSetHeader(req, "Content-Type", "application/octet-stream") + addOrSetHeader(req, "Content-Length", strconv.Itoa(l)) + + task := http.Client{} + response, err := task.Do(req) + if err != nil { + return + } + defer response.Body.Close() + + statusCode = response.StatusCode + + if response.StatusCode == http.StatusOK { + return + } + + content, _ := io.ReadAll(response.Body) + err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) + + return +} + +// completeUploadWithRetries instructs OSS to complete the object creation process after the bytes have been uploaded directly to S3, retrying max 3 times. +// https://forge.autodesk.com/en/docs/data/v2/reference/http/buckets-:bucketKey-objects-:objectKey-signeds3upload-POST/ +func (job *uploadJob) completeUploadWithRetries() (result UploadResult, err error) { + + var statusCode int + + for i := 0; i < 3; i++ { + + statusCode, result, err = job.completeUpload() + + // 429 - RATE-LIMIT EXCEEDED + // The maximum number of API calls that a Forge application can make _PER MINUTE_ was exceeded. + // 500 - INTERNAL SERVER ERROR + if statusCode == 429 || statusCode == 500 { + // retry in 1 minute + time.Sleep(1 * time.Minute) + } else { + // done + break + } + } + + return result, err +} + +// completeUpload instructs OSS to complete the object creation process after the bytes have been uploaded directly to S3. +// An object will not be accessible until this endpoint is called. +// This endpoint must be called within 24 hours of the upload beginning, otherwise the object will be discarded, +// and the upload must begin again from scratch. +func (job *uploadJob) completeUpload() (statusCode int, result UploadResult, err error) { + + // - https://forge.autodesk.com/en/docs/data/v2/reference/http/buckets-:bucketKey-objects-:objectKey-signeds3upload-POST/ + + // - https://forge.autodesk.com/en/docs/data/v2/tutorials/upload-file/#step-6-complete-the-upload + // - https://forge.autodesk.com/en/docs/data/v2/tutorials/app-managed-bucket/#step-4-complete-the-upload + + accessToken, err := job.authenticate() + if err != nil { + return + } + + // size integer: The expected size of the uploaded object. + // If provided, OSS will check this against the blob in S3 and return an error if the size does not match. + bodyData := struct { + UploadKey string `json:"uploadKey"` + Size int `json:"size"` + }{ + UploadKey: job.uploadKey, + Size: int(job.fileSize), + } + + bodyJson, err := json.Marshal(bodyData) + if err != nil { + return + } + + req, err := http.NewRequest("POST", job.getSignedS3UploadPath(), bytes.NewBuffer(bodyJson)) + if err != nil { + return + } + + addOrSetHeader(req, "Authorization", "Bearer "+accessToken) + addOrSetHeader(req, "Content-Type", "application/json") + addOrSetHeader(req, "x-ads-meta-Content-Type", "application/octet-stream") + + task := http.Client{} + response, err := task.Do(req) + if err != nil { + return + } + defer response.Body.Close() + + statusCode = response.StatusCode + + if response.StatusCode != http.StatusOK { + content, _ := io.ReadAll(response.Body) + err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) + return + } + + err = json.NewDecoder(response.Body).Decode(&result) + + return +} + +func addOrSetHeader(req *http.Request, key, value string) { + if req.Header.Get(key) == "" { + req.Header.Add(key, value) + } else { + req.Header.Set(key, value) + } +} + +func (job *uploadJob) getSignedS3UploadPath() string { + // https://developer.api.autodesk.com/oss/v2/buckets/:bucketKey/objects/:objectKey/signeds3upload + // :bucketKey/objects/:objectKey/signeds3upload + return job.api.Authenticator.HostPath() + path.Join( + job.api.relativePath, job.bucketKey, "objects", job.objectKey, signedS3UploadEndpoint, + ) +} + +func (job *uploadJob) authenticate() (accessToken string, err error) { + bearer, err := job.api.Authenticator.GetToken("data:write data:read") + if err != nil { + return + } + accessToken = bearer.AccessToken + return +} diff --git a/dm/test/bucket_test.go b/dm/test/bucket_test.go index 2371382..481806b 100644 --- a/dm/test/bucket_test.go +++ b/dm/test/bucket_test.go @@ -1,168 +1,179 @@ package dm_test import ( - "fmt" - "github.com/apprentice3d/forge-api-go-client/dm" - "github.com/apprentice3d/forge-api-go-client/oauth" - "log" - "os" "testing" + + "github.com/woweh/forge-api-go-client/dm" ) +/* +NOTE: +- You can only run these tests when you have a valid client ID and secret. + => You probably want to run the tests locally, with your own credentials. +- A bucketKey (= bucket name) must be globally unique across all applications and regions +- Rules for bucketKey names: -_.a-z0-9 (between 3-128 characters in length) +- Buckets can only be deleted by the user who created them. + => You might want to change the bucketKey if the bucket already exists. +- A bucket name will not be immediately available for reuse after deletion. + => Best use a unique bucket name for each subtest. + => You can also use a timestamp to make sure the bucket name is unique. +*/ + func TestBucketAPI_CreateBucket(t *testing.T) { - // prepare the credentials - clientID := os.Getenv("FORGE_CLIENT_ID") - clientSecret := os.Getenv("FORGE_CLIENT_SECRET") + bucketAPI := getBucketAPI(t) - authenticator := oauth.NewTwoLegged(clientID, clientSecret) - bucketAPI := dm.NewBucketAPI(authenticator) + bucketKey := "forge_unit_testing_create_bucket" - t.Run("Create a bucket", func(t *testing.T) { - _, err := bucketAPI.CreateBucket("go_testing_bucket", "transient") + t.Run( + "Create a bucket", func(t *testing.T) { + _, err := bucketAPI.GetBucketDetails(bucketKey) + if err == nil { + t.Skip("The temp bucket already exists.") + } - if err != nil { - t.Fatalf("Failed to create a bucket: %s\n", err.Error()) - } - }) + _, err = bucketAPI.CreateBucket(bucketKey, dm.PolicyTransient) + if err != nil { + t.Fatalf("Failed to create a bucket: %s\n", err.Error()) + } + }, + ) - t.Run("Delete created bucket", func(t *testing.T) { - err := bucketAPI.DeleteBucket("go_testing_bucket") + t.Run( + "Delete created bucket", func(t *testing.T) { + err := bucketAPI.DeleteBucket(bucketKey) - if err != nil { - t.Fatalf("Failed to delete bucket: %s\n", err.Error()) - } - }) + if err != nil { + t.Fatalf("Failed to delete bucket: %s\n", err.Error()) + } + }, + ) - t.Run("Create a bucket with invalid name", func(t *testing.T) { - _, err := bucketAPI.CreateBucket("goTestingBucket", "transient") + t.Run( + "Create a bucket with invalid name", func(t *testing.T) { + invalidBucketKey := "$Invalid@Bucket%Key!" + _, err := bucketAPI.CreateBucket(invalidBucketKey, dm.PolicyTransient) - if err == nil { - t.Fatalf("Should fail creating a bucket with invalid name\n") - } - }) + if err == nil { + t.Fatal("Should fail creating a bucket with invalid name: ", invalidBucketKey) + } + }, + ) - t.Run("Create a bucket with invalid policyKey", func(t *testing.T) { - _, err := bucketAPI.CreateBucket("goTestingBucket", "democracy") + t.Run( + "Create a bucket with invalid policyKey", func(t *testing.T) { + _, err := bucketAPI.CreateBucket("all_lower_case_bucket_key", "invalidPolicy") - if err == nil { - t.Fatalf("Should fail creating a bucket with invalid name\n") - } - }) + if err == nil { + t.Fatalf("Should fail creating a bucket with invalid name\n") + } + }, + ) } func TestBucketAPI_GetBucketDetails(t *testing.T) { - // prepare the credentials - clientID := os.Getenv("FORGE_CLIENT_ID") - clientSecret := os.Getenv("FORGE_CLIENT_SECRET") - - authenticator := oauth.NewTwoLegged(clientID, clientSecret) - bucketAPI := dm.NewBucketAPI(authenticator) + bucketAPI := getBucketAPI(t) - testBucketKey := "my_test_bucket_key_for_go" + bucketKey := "forge_unit_testing_get_bucket_details" - t.Run("Create a bucket", func(t *testing.T) { - _, err := bucketAPI.CreateBucket(testBucketKey, "transient") - - if err != nil { - t.Fatalf("Failed to create a bucket: %s\n", err.Error()) - } - }) - - t.Run("Get bucket details", func(t *testing.T) { - _, err := bucketAPI.GetBucketDetails(testBucketKey) - - if err != nil { - t.Fatalf("Failed to get bucket details: %s\n", err.Error()) - } - }) - - t.Run("Delete created bucket", func(t *testing.T) { - err := bucketAPI.DeleteBucket(testBucketKey) + t.Run( + "Create a bucket", func(t *testing.T) { + _, err := bucketAPI.GetBucketDetails(bucketKey) + if err == nil { + t.Skip("The temp bucket already exists.") + } - if err != nil { - t.Fatalf("Failed to delete bucket: %s\n", err.Error()) - } - }) + _, err = bucketAPI.CreateBucket(bucketKey, dm.PolicyTransient) + if err != nil { + t.Fatalf("Failed to create a bucket: %s\n", err.Error()) + } + }, + ) - t.Run("Get nonexistent bucket", func(t *testing.T) { - _, err := bucketAPI.GetBucketDetails(testBucketKey + "30091981") + t.Run( + "Get bucket details", func(t *testing.T) { + _, err := bucketAPI.GetBucketDetails(bucketKey) - if err == nil { - t.Fatalf("Should fail getting getting details for non-existing bucket\n") - } - }) + if err != nil { + t.Fatalf("Failed to get bucket details: %s\n", err.Error()) + } + }, + ) + + t.Cleanup( + func() { + t.Log("Cleaning up the temp bucket") + err := bucketAPI.DeleteBucket(bucketKey) + if err != nil { + t.Error("Could not delete temp bucket, got: ", err.Error()) + } + }, + ) } func TestBucketAPI_ListBuckets(t *testing.T) { - // prepare the credentials - clientID := os.Getenv("FORGE_CLIENT_ID") - clientSecret := os.Getenv("FORGE_CLIENT_SECRET") - - authenticator := oauth.NewTwoLegged(clientID, clientSecret) - bucketAPI := dm.NewBucketAPI(authenticator) - - t.Run("List available buckets", func(t *testing.T) { - _, err := bucketAPI.ListBuckets("", "", "") - - if err != nil { - t.Fatalf("Failed to list buckets: %s\n", err.Error()) - } - }) - - t.Run("Create a bucket and find it among listed", func(t *testing.T) { - testBucketKey := "just_for_testing" - _, err := bucketAPI.CreateBucket(testBucketKey, "transient") - - if err != nil { - t.Errorf("Failed to create a bucket: %s\n", err.Error()) - } - - list, err := bucketAPI.ListBuckets("", "", "") - - if err != nil { - t.Errorf("Failed to list buckets: %s\n", err.Error()) - } + bucketAPI := getBucketAPI(t) - found := false + bucketKey := "forge_unit_testing_list_buckets" - for _, bucket := range list.Items { - if bucket.BucketKey == testBucketKey { - found = true - break + t.Run( + "List available buckets", func(t *testing.T) { + _, err := bucketAPI.ListBuckets("", "", "") + if err != nil { + t.Fatalf("Failed to list buckets: %s\n", err.Error()) } - } + }, + ) - if !found { - t.Errorf("Could not find the %s bucket\n", testBucketKey) - } + t.Run( + "Create a bucket and find it among listed", func(t *testing.T) { - if err = bucketAPI.DeleteBucket(testBucketKey); err != nil { - t.Errorf("Failed to delete bucket: %s\n", err.Error()) - } - }) + _, err := bucketAPI.GetBucketDetails(bucketKey) + if err == nil { + t.Log("The temp bucket already exists, try to delete it.") -} + err = bucketAPI.DeleteBucket(bucketKey) + if err != nil { + t.Error("Could not delete temp bucket, got: ", err.Error()) + } + } -func ExampleBucketAPI_CreateBucket() { + _, err = bucketAPI.CreateBucket(bucketKey, dm.PolicyTransient) + if err != nil { + t.Errorf("Failed to create a bucket: %s\n", err.Error()) + } - // prepare the credentials - clientID := os.Getenv("FORGE_CLIENT_ID") - clientSecret := os.Getenv("FORGE_CLIENT_SECRET") + list, err := bucketAPI.ListBuckets("", "", "") - authenticator := oauth.NewTwoLegged(clientID, clientSecret) - bucketAPI := dm.NewBucketAPI(authenticator) + if err != nil { + t.Errorf("Failed to list buckets: %s\n", err.Error()) + } - bucket, err := bucketAPI.CreateBucket("some_unique_name", "transient") + found := false - if err != nil { - log.Fatalf("Failed to create a bucket: %s\n", err.Error()) - } + for _, bucket := range list { + if bucket.BucketKey == bucketKey { + found = true + break + } + } - fmt.Printf("Bucket %s was created with policy %s\n", - bucket.BucketKey, - bucket.PolicyKey) + if !found { + t.Errorf("Could not find the %s bucket\n", bucketKey) + } + }, + ) + + t.Cleanup( + func() { + t.Log("Cleaning up the temp bucket") + err := bucketAPI.DeleteBucket(bucketKey) + if err != nil { + t.Error("Could not delete temp bucket, got: ", err.Error()) + } + }, + ) } diff --git a/dm/test/object_test.go b/dm/test/object_test.go index affe54d..9d1feed 100644 --- a/dm/test/object_test.go +++ b/dm/test/object_test.go @@ -1,171 +1,233 @@ package dm_test import ( - "github.com/apprentice3d/forge-api-go-client/oauth" - "io/ioutil" + "fmt" "os" "testing" + "time" - "github.com/apprentice3d/forge-api-go-client/dm" + "github.com/woweh/forge-api-go-client" + "github.com/woweh/forge-api-go-client/oauth" + + "github.com/woweh/forge-api-go-client/dm" ) -func TestBucketAPI_ListObjects(t *testing.T) { +/* +NOTE: +- You can only run these tests when you have a valid client ID and secret. + => You probably want to run the tests locally, with your own credentials. +- A bucketKey (= bucket name) must be globally unique across all applications and regions +- Rules for bucketKey names: -_.a-z0-9 (between 3-128 characters in length) +- Buckets can only be deleted by the user who created them. + => You might want to change the bucketKey if the bucket already exists. +- A bucket name will not be immediately available for reuse after deletion. + => Best use a unique bucket name for each subtest. + => You can also use a timestamp to make sure the bucket name is unique. +*/ + +const ( + objectKey string = "rst_basic_sample_project.rvt" + testFilePath = "../assets/" + objectKey +) + +func getBucketAPI(t *testing.T) dm.OssAPI { + // prepare the credentials clientID := os.Getenv("FORGE_CLIENT_ID") + if clientID == "" { + t.Fatal("clientID is empty") + } + clientSecret := os.Getenv("FORGE_CLIENT_SECRET") + if clientSecret == "" { + t.Fatal("clientSecret is empty") + } authenticator := oauth.NewTwoLegged(clientID, clientSecret) - bucketAPI := dm.NewBucketAPI(authenticator) + if authenticator == nil { + t.Fatal("Error authenticating, authenticator is nil.") + } - testBucketName := "just_a_test_bucket" - - t.Run("List bucket content", func(t *testing.T) { - content, err := bucketAPI.ListObjects(testBucketName, "", "", "") - if err != nil { - t.Fatalf("Failed to list bucket content: %s\n", err.Error()) - } - - t.Logf("%#v", content) - }) + return dm.NewOssApi(authenticator, forge.US) +} - t.Run("List bucket content of non-existing bucket", func(t *testing.T) { - content, err := bucketAPI.ListObjects(testBucketName+"hz", "", "", "") - if err == nil { - t.Fatalf("Expected to fail upon listing a non-existing bucket, but it didn't, got %#v", content) - } - }) +func TestBucketAPI_ListObjects(t *testing.T) { + bucketAPI := getBucketAPI(t) + + bucketKey := "forge_unit_testing_list_objects" + + t.Run( + "Create a temp bucket to store an object", func(t *testing.T) { + + _, err := bucketAPI.GetBucketDetails(bucketKey) + if err == nil { + t.Skip("The temp bucket already exists.") + } + + _, err = bucketAPI.CreateBucket(bucketKey, dm.PolicyTransient) + if err != nil { + t.Error("Could not create temp bucket, got: ", err.Error()) + } + }, + ) + + t.Run( + "List bucket content", func(t *testing.T) { + content, err := bucketAPI.ListObjects(bucketKey, "", "", "") + if err != nil { + t.Fatalf("Failed to list bucket content: %s\n", err.Error()) + } + + t.Logf("%#v", content) + }, + ) + + t.Run( + "List bucket content of non-existing bucket", func(t *testing.T) { + tmpBucketKey := fmt.Sprintf("%v", time.Now().UnixNano()) + content, err := bucketAPI.ListObjects(tmpBucketKey, "", "", "") + if err == nil { + t.Fatalf("Expected to fail upon listing a non-existing bucket, but it didn't, got %#v", content) + } + }, + ) + + t.Cleanup( + func() { + t.Log("Cleaning up the temp bucket") + err := bucketAPI.DeleteBucket(bucketKey) + if err != nil { + t.Error("Could not delete temp bucket, got: ", err.Error()) + } + }, + ) } func TestBucketAPI_UploadObject(t *testing.T) { - // prepare the credentials - clientID := os.Getenv("FORGE_CLIENT_ID") - clientSecret := os.Getenv("FORGE_CLIENT_SECRET") - - authenticator := oauth.NewTwoLegged(clientID, clientSecret) - bucketAPI := dm.NewBucketAPI(authenticator) - - tempBucket := "some_temp_bucket_for_testings" - testFilePath := "../assets/HelloWorld.rvt" - - t.Run("Create a temp bucket to store an object", func(t *testing.T) { - _, err := bucketAPI.CreateBucket(tempBucket, "transient") - if err != nil { - t.Error("Could not create temp bucket, got: ", err.Error()) - } - }) - - t.Run("List objects in temp bucket, to make sure it is empty", func(t *testing.T) { - content, err := bucketAPI.ListObjects(tempBucket, "", "", "") - if err != nil { - t.Fatalf("Failed to list bucket content: %s\n", err.Error()) - } - if len(content.Items) != 0 { - t.Fatalf("temp bucket supposed to be empty, got %#v", content) - } - }) - - t.Run("Upload an object into temp bucket", func(t *testing.T) { - file, err := os.Open(testFilePath) - if err != nil { - t.Fatal("Cannot open testfile for reading") - } - defer file.Close() - data, err := ioutil.ReadAll(file) - if err != nil { - t.Fatal("Cannot read the testfile") - } - - result, err := bucketAPI.UploadObject(tempBucket, "temp_file.rvt", data) - - if err != nil { - t.Fatal("Could not upload the test object, got: ", err.Error()) - } - - if result.Size == 0 { - t.Fatal("The test object was uploaded but it is zero-sized") - } - }) - - t.Run("Delete the temp bucket", func(t *testing.T) { - err := bucketAPI.DeleteBucket(tempBucket) - if err != nil { - t.Error("Could not delete temp bucket, got: ", err.Error()) - } - }) + bucketAPI := getBucketAPI(t) + + bucketKey := "forge_unit_testing_upload_object" + + t.Run( + "Create a temp bucket to store an object", func(t *testing.T) { + _, err := bucketAPI.GetBucketDetails(bucketKey) + if err == nil { + t.Skip("The temp bucket already exists, try to delete it.") + } + + _, err = bucketAPI.CreateBucket(bucketKey, dm.PolicyTransient) + if err != nil { + t.Error("Could not create temp bucket, got: ", err.Error()) + } + }, + ) + + t.Run( + "List objects in temp bucket, to make sure it is empty", func(t *testing.T) { + content, err := bucketAPI.ListObjects(bucketKey, "", "", "") + if err != nil { + t.Fatalf("Failed to list bucket content: %s\n", err.Error()) + } + if len(content.Items) != 0 { + t.Fatalf("temp bucket supposed to be empty, got %#v", content) + } + }, + ) + + t.Run( + "Upload an object into temp bucket", func(t *testing.T) { + result, err := bucketAPI.UploadObject(bucketKey, objectKey, testFilePath) + + if err != nil { + t.Fatal("Could not upload the test object, got: ", err.Error()) + } + + if result.Size == 0 { + t.Fatal("The test object was uploaded but it is zero-sized") + } + }, + ) + + t.Cleanup( + func() { + t.Log("Cleaning up the temp bucket") + err := bucketAPI.DeleteBucket(bucketKey) + if err != nil { + t.Error("Could not delete temp bucket, got: ", err.Error()) + } + }, + ) } - func TestBucketAPI_DownloadObject(t *testing.T) { - // prepare the credentials - clientID := os.Getenv("FORGE_CLIENT_ID") - clientSecret := os.Getenv("FORGE_CLIENT_SECRET") - - authenticator := oauth.NewTwoLegged(clientID, clientSecret) - bucketAPI := dm.NewBucketAPI(authenticator) - - tempBucket := "some_temp_bucket_for_testings" - testFilePath := "../assets/HelloWorld.rvt" - const object_name = "temp_file.rvt" - - t.Run("Create a temp bucket to store an object", func(t *testing.T) { - _, err := bucketAPI.CreateBucket(tempBucket, "transient") - if err != nil { - t.Error("Could not create temp bucket, got: ", err.Error()) - } - }) - - t.Run("List objects in temp bucket, to make sure it is empty", func(t *testing.T) { - content, err := bucketAPI.ListObjects(tempBucket, "", "", "") - if err != nil { - t.Fatalf("Failed to list bucket content: %s\n", err.Error()) - } - if len(content.Items) != 0 { - t.Fatalf("temp bucket supposed to be empty, got %#v", content) - } - }) - - t.Run("Upload an object into temp bucket", func(t *testing.T) { - file, err := os.Open(testFilePath) - if err != nil { - t.Fatal("Cannot open testfile for reading") - } - defer file.Close() - data, err := ioutil.ReadAll(file) - if err != nil { - t.Fatal("Cannot read the testfile") - } - - - result, err := bucketAPI.UploadObject(tempBucket, object_name, data) - - if err != nil { - t.Fatal("Could not upload the test object, got: ", err.Error()) - } - - if result.Size == 0 { - t.Fatal("The test object was uploaded but it is zero-sized") - } - }) - - t.Run("Download an object from the temp bucket", func(t *testing.T) { - result, err := bucketAPI.DownloadObject(tempBucket, object_name) - if err != nil { - t.Errorf("Problems getting the object %s: %s", object_name, err.Error()) - } - - if len(result) == 0 { - t.Errorf("The object %s was downloaded sucessfully, but it is empty.", object_name) - } - - }) - - t.Run("Delete the temp bucket", func(t *testing.T) { - err := bucketAPI.DeleteBucket(tempBucket) - if err != nil { - t.Error("Could not delete temp bucket, got: ", err.Error()) - } - }) -} \ No newline at end of file + bucketAPI := getBucketAPI(t) + + bucketKey := "forge_unit_testing_upload_and_download_object" + + t.Run( + "Create a temp bucket to store an object", func(t *testing.T) { + _, err := bucketAPI.GetBucketDetails(bucketKey) + if err == nil { + t.Skip("The temp bucket already exists.") + } + + _, err = bucketAPI.CreateBucket(bucketKey, dm.PolicyTransient) + if err != nil { + t.Error("Could not create temp bucket, got: ", err.Error()) + } + }, + ) + + t.Run( + "List objects in temp bucket, to make sure it is empty", func(t *testing.T) { + content, err := bucketAPI.ListObjects(bucketKey, "", "", "") + if err != nil { + t.Fatalf("Failed to list bucket content: %s\n", err.Error()) + } + if len(content.Items) != 0 { + t.Fatalf("temp bucket supposed to be empty, got %#v", content) + } + }, + ) + + t.Run( + "Upload an object into temp bucket", func(t *testing.T) { + result, err := bucketAPI.UploadObject(bucketKey, objectKey, testFilePath) + + if err != nil { + t.Fatal("Could not upload the test object, got: ", err.Error()) + } + + if result.Size == 0 { + t.Fatal("The test object was uploaded but it is zero-sized") + } + }, + ) + + t.Run( + "Download an object from the temp bucket", func(t *testing.T) { + result, err := bucketAPI.DownloadObject(bucketKey, objectKey) + if err != nil { + t.Errorf("Problems getting the object %s: %s", objectKey, err.Error()) + } + + if len(result) == 0 { + t.Errorf("The object %s was downloaded successfully, but it is empty.", objectKey) + } + + }, + ) + + t.Cleanup( + func() { + t.Log("Cleaning up the temp bucket") + err := bucketAPI.DeleteBucket(bucketKey) + if err != nil { + t.Error("Could not delete temp bucket, got: ", err.Error()) + } + }, + ) +} diff --git a/dm/types.go b/dm/types.go index c102c40..d46acf0 100644 --- a/dm/types.go +++ b/dm/types.go @@ -1,33 +1,35 @@ package dm -import "github.com/apprentice3d/forge-api-go-client/oauth" - +import ( + "github.com/woweh/forge-api-go-client" + "github.com/woweh/forge-api-go-client/oauth" +) /* BUCKET API TYPES */ - -// BucketAPI holds the necessary data for making Bucket related calls to Forge Data Management service -type BucketAPI struct { +// OssAPI holds the necessary data for making Object Storage Service (OSS) related calls to the Forge Data Management API. +type OssAPI struct { Authenticator oauth.ForgeAuthenticator - BucketAPIPath string + relativePath string // = "/oss/v2/buckets", populate in NewOssApi + region forge.Region } // CreateBucketRequest contains the data necessary to be passed upon bucket creation type CreateBucketRequest struct { - BucketKey string `json:"bucketKey"` - PolicyKey string `json:"policyKey"` + BucketKey string `json:"bucketKey"` + PolicyKey RetentionPolicy `json:"policyKey"` } // BucketDetails reflects the body content received upon creation of a bucket type BucketDetails struct { BucketKey string `json:"bucketKey"` BucketOwner string `json:"bucketOwner"` - CreateDate int64 `json:"createDate"` + CreateDate int64 `json:"createDate"` Permissions []struct { AuthID string `json:"authId"` Access string `json:"access"` } `json:"permissions"` - PolicyKey string `json:"policyKey"` + PolicyKey RetentionPolicy `json:"policyKey"` } // ErrorResult reflects the body content when a request failed (g.e. Bad request or key conflict) @@ -35,32 +37,45 @@ type ErrorResult struct { Reason string `json:"reason"` } +type BucketList []BucketInfo + +type BucketInfo struct { + BucketKey string `json:"bucketKey"` + CreatedDate int64 `json:"createdDate"` + PolicyKey RetentionPolicy `json:"policyKey"` +} + // ListedBuckets reflects the response when query Data Management API for buckets associated with current Forge secrets. type ListedBuckets struct { - Items []struct { - BucketKey string `json:"bucketKey"` - CreatedDate int64 `json:"createdDate"` - PolicyKey string `json:"policyKey"` - } `json:"items"` - Next string `json:"next"` + Items BucketList `json:"items"` + Next string `json:"next"` } - // ObjectDetails reflects the data presented when uploading an object to a bucket or requesting details on object. type ObjectDetails struct { BucketKey string `json:"bucketKey"` - ObjectID string `json:"objectID"` + ObjectID string `json:"objectID"` // => urn = base64.RawStdEncoding.EncodeToString([]byte(ObjectID)) ObjectKey string `json:"objectKey"` SHA1 string `json:"sha1"` Size uint64 `json:"size"` - ContentType string `json:"contentType, omitempty"` + ContentType string `json:"contentType,omitempty"` Location string `json:"location"` - BlockSizes []int64 `json:"blockSizes, omitempty"` - Deltas map[string]string `json:"deltas, omitempty"` + BlockSizes []int64 `json:"blockSizes,omitempty"` + Deltas map[string]string `json:"deltas,omitempty"` +} + +// UploadResult provides the OK/200 result of the completeUpload POST. +type UploadResult struct { + BucketKey string `json:"bucketKey"` + ObjectId string `json:"objectId"` // => urn = base64.RawStdEncoding.EncodeToString([]byte(ObjectID)) + ObjectKey string `json:"objectKey"` + Size int `json:"size"` + ContentType string `json:"content-type"` + Location string `json:"location"` } // BucketContent reflects the response when query Data Management API for bucket content. type BucketContent struct { Items []ObjectDetails `json:"items"` Next string `json:"next"` -} \ No newline at end of file +} diff --git a/doc.go b/doc.go index 43ca656..4d33fdc 100644 --- a/doc.go +++ b/doc.go @@ -1,26 +1,25 @@ -// Package forge is an opinionated Autodesk Forge SDK for the Go programming language. +// Package forge is an opinionated APS (formerly `Forge`) API client. // -// The Forge SDK for Go provides APIs that developers can use to build Go applications -// that use Autodesk Forge Services such as -// Data Management, Model Derivative, Reality Capture and others. +// The forge package provides APIs that developers can use to build Go applications +// that use Autodesk Platform Services such as Data Management and Model Derivative. // -// The SDK removes the complexity of coding directly against a web service -// interface and it hides a lot of the lower-level plumbing, such as authentication. +// The API removes the complexity of coding directly against a web service +// interface, and it hides a lot of the lower-level plumbing, such as authentication. // -// Getting More Information +// # Getting More Information // -// Checkout the https://developer.autodesk.com/ portal for overviews, tutorials and -// detailed documentation for each Autodesk Forge Service. +// Checkout the https://aps.autodesk.com/ portal for overviews, tutorials and +// detailed documentation for each Autodesk Platform Service. // -// Checkout LearnForge http://learnforge.autodesk.io for a step-by-step tutorial on -// building a Forge powered web application in different language, including Go using this library. +// Checkout https://tutorials.autodesk.io/ for step-by-step tutorials on +// building APS powered web application in different language. // -// Overview of SDK's Packages +// # Overview of the forge APIs Packages // -// The SDK is composed of several parts, corresponding to each Forge Service, but all of them -// are relying on OAuth service for 2-legged and 3-legged authentication necessary to access -// Forge Services. -// * oauth - provides common shared types such as Config, Logger, -// and utilities to make working with API parameters easier. +// The API is composed of several parts, corresponding to each Autodesk Platform Service, +// but all of them are relying on OAuth service for 2-legged and 3-legged authentication +// necessary to access Autodesk Platform Services. +// - oauth - provides 2-legged and 3-legged authentication +// - dm - provides access to Data Management service +// - md - provides access to Model Derivative service package forge - diff --git a/go.mod b/go.mod index b3ca76a..8137e90 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module github.com/apprentice3d/forge-api-go-client +module github.com/woweh/forge-api-go-client -go 1.12 +go 1.20 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/md/advanced.go b/md/advanced.go new file mode 100644 index 0000000..c57180a --- /dev/null +++ b/md/advanced.go @@ -0,0 +1,161 @@ +package md + +// This file provides "enums" (= types and consts) and factory functions for advanced translation options. + +type ( + // RvtMaterialMode is a Revit specific option that specifies the materials to apply to the generated SVF/SVF2 derivatives. + RvtMaterialMode string + + // Rvt2dViews defines the format that 2D views must be rendered to. + // Available options are: + // - legacy - (Default) Render to a model derivative specific file format. + // - pdf - Render to the PDF file format. This option applies only to Revit 2022 files and newer. + Rvt2dViews string + + // RvtExtractorVersion specifies what version of the Revit translator/extractor to use. + // NOTE: If this attribute is not specified, the system uses the current official release version. + // Possible values: + // - next - Makes the translation job use the newest available version of the translator/extractor. + // This option is meant to be used by beta testers who wish to test a pre-release version of the translator. + // If no pre-release version is available, this option makes the translation job use the current official release version. + // - previous - Makes the translation job use the previous official release version of the translator/extractor. + // This option is meant to be used as a workaround in case of regressions caused by a new official release of the translator/extractor. + RvtExtractorVersion string + + // ObjUnit is an OBJ specific option for translating models into different units. + ObjUnit string + + // ObjExportFileStructure is a OBJ specific option for creating a single or multiple OBJ files. + ObjExportFileStructure string + + // IfcConversionMethod is an IFC specific option that specifies what IFC loader to use during translation. + IfcConversionMethod string // An + + // IfcOption are IFC specific options that specify how elements (BuildingStoreys, Spaces or OpeningElements) are translated. + // NOTE: These options are applicable only when conversionMethod is set to modern or v3. + IfcOption string +) + +/* +Revit specific options +*/ + +const ( + RvtAuto RvtMaterialMode = "auto" // (Default) Use the current setting of the default view of the input file. + RvtBasic RvtMaterialMode = "basic" // Use basic materials. + RvtAutoDesk RvtMaterialMode = "autodesk" // Use Autodesk materials. + RvtLegacy Rvt2dViews = "legacy" // (Default) Render to a model derivative specific file format. + RvtPdf Rvt2dViews = "pdf" // Render to the PDF file format. This option applies only to Revit 2022 files and newer. + RvtNext RvtExtractorVersion = "next" // Makes the translation job use the newest available version of the translator/extractor. + RvtPrevious RvtExtractorVersion = "previous" // Makes the translation job use the previous official release version of the translator/extractor. +) + +/* +OBJ specific options +*/ + +const ( + ObjSingle ObjExportFileStructure = "single" // (default): creates one OBJ file for all the input files (assembly file). + ObjMultiple ObjExportFileStructure = "multiple" // creates a separate OBJ file for each object +) + +const ( + ObjMeter ObjUnit = "meter" + ObjDecimeter ObjUnit = "decimeter" + ObjCentimeter ObjUnit = "centimeter" + ObjMillimeter ObjUnit = "millimeter" + ObjMicrometer ObjUnit = "micrometer" + ObjNanometer ObjUnit = "nanometer" + ObjYard ObjUnit = "yard" + ObjFoot ObjUnit = "foot" + ObjInch ObjUnit = "inch" + ObjMil ObjUnit = "mil" + ObjMicroInch ObjUnit = "microinch" + ObjNone ObjUnit = "" +) + +/* +IFC specific options +*/ + +const ( + IfcLegacy IfcConversionMethod = "legacy" // Use the old Navisworks IFC loader + IfcModern IfcConversionMethod = "modern" // Use the revit IFC loader (recommended over the legacy option). + IfcV3 IfcConversionMethod = "v3" // Use the newer revit IFC loader (recommended over the older modern option) +) + +const ( + IfcHide IfcOption = "hide" // (default) elements are extracted but not visible by default. + IfcShow IfcOption = "show" // elements are extracted and are visible by default. + IfcSkip IfcOption = "skip" // elements are not translated. +) + +// IfcAdvancedSpec returns an IFC specific AdvancedSpec. +// +// NOTE: +// - The storeys, spaces, and openings options are applicable only when conversionMethod is set to `modern` or `v3`. +// - If the conversionMethod is set to `legacy`, the storeys, spaces, and openings options are ignored. +// - Use empty strings for the IfcOptions to use the default values. +// +// Reference: +// - https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/jobs/job-POST/#body-structure +// -> Case 4 - Input file type is IFC: +func IfcAdvancedSpec(conversionMethod IfcConversionMethod, storeys, spaces, openings IfcOption) *AdvancedSpec { + if conversionMethod == IfcLegacy { + return &AdvancedSpec{ConversionMethod: conversionMethod} + } + return &AdvancedSpec{ + ConversionMethod: conversionMethod, + BuildingStoreys: storeys, + Spaces: spaces, + OpeningElements: openings, + } +} + +// RevitAdvancedSpec returns a Revit specific AdvancedSpec. +// - Use empty strings for materialMode, twoDView and version to use the default values. +// +// Reference: +// - https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/jobs/job-POST/#body-structure +// -> Case 6 - Input file type is RVT: +func RevitAdvancedSpec( + generateMasterViews bool, materialMode RvtMaterialMode, twoDView Rvt2dViews, version RvtExtractorVersion, +) *AdvancedSpec { + return &AdvancedSpec{ + GenerateMasterViews: &generateMasterViews, + MaterialMode: materialMode, + TwoDViews: twoDView, + ExtractorVersion: version, + } +} + +// NavisworksAdvancedSpec returns a Navisworks specific AdvancedSpec. +// - The Autodesk defaults for the 4 options are all `false`. +// +// Reference: +// - https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/jobs/job-POST/#body-structure +// -> Case 5 - Input file type is NWD: +func NavisworksAdvancedSpec(hiddenObjects, basicMaterialProperties, autodeskMaterialProperties, timeLinerProperties bool) *AdvancedSpec { + return &AdvancedSpec{ + HiddenObjects: &hiddenObjects, + BasicMaterialProperties: &basicMaterialProperties, + AutodeskMaterialProperties: &autodeskMaterialProperties, + TimeLinerProperties: &timeLinerProperties, + } +} + +// ObjAdvancedSpec returns a OBJ specific AdvancedSpec. +// +// Reference: +// - https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/jobs/job-POST/#body-structure +// -> Attributes that Apply to OBJ Outputs +func ObjAdvancedSpec( + exportFileStructure ObjExportFileStructure, unit ObjUnit, modelGuid string, objectIds *[]int, +) *AdvancedSpec { + return &AdvancedSpec{ + ExportFileStructure: exportFileStructure, + Unit: unit, + ModelGuid: modelGuid, + ObjectIds: objectIds, + } +} diff --git a/md/api.go b/md/api.go index 3ca0dd1..11ef0ba 100644 --- a/md/api.go +++ b/md/api.go @@ -1,90 +1,310 @@ package md import ( - "github.com/apprentice3d/forge-api-go-client/oauth" "encoding/base64" + "io" + "strings" + + "github.com/woweh/forge-api-go-client" + "github.com/woweh/forge-api-go-client/oauth" ) -// API struct holds all paths necessary to access Model Derivative API +// ModelDerivativeAPI struct holds all paths necessary to access Model Derivative API type ModelDerivativeAPI struct { + // Forge authenticator, used to get access token, either 2-legged or 3-legged Authenticator oauth.ForgeAuthenticator - ModelDerivativePath string + // The relativePath depends on the region => either usPath or euPath + relativePath string + // The region where data resides, either US or EU (EMEA) + region forge.Region } -// NewMDAPI returns a Model Derivative API client with default configurations -func NewMDAPI(authenticator oauth.ForgeAuthenticator) ModelDerivativeAPI { +const ( + usPath = "/modelderivative/v2/designdata" + euPath = "/modelderivative/v2/regions/eu/designdata" +) + +// NewMdApi returns a Model Derivative API client for a specific region. +func NewMdApi(authenticator oauth.ForgeAuthenticator, region forge.Region) ModelDerivativeAPI { + // default to US region + path := usPath + if region.IsEU() { + path = euPath + } + return ModelDerivativeAPI{ - authenticator, - "/modelderivative/v2/designdata", + Authenticator: authenticator, + relativePath: path, + region: region, + } +} + +// Region of the ModelDerivativeAPI. +func (a *ModelDerivativeAPI) Region() forge.Region { + return a.region +} + +// SetRegion sets the Region _AND_ RelativePath of the ModelDerivativeAPI. +// - If the region is US, the relativePath will be usPath (/modelderivative/v2/designdata) +// - If the region is EU (== EMEA), the relativePath will be euPath (/modelderivative/v2/regions/eu/designdata) +// +// References: +// - https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/ +func (a *ModelDerivativeAPI) SetRegion(region forge.Region) { + a.region = region + if region.IsUS() { + a.relativePath = usPath + } else if region.IsEU() { + a.relativePath = euPath } } -// TranslateWithParams triggers translation job with settings specified in given TranslationParams -func (a ModelDerivativeAPI) TranslateWithParams(params TranslationParams) (result TranslationResult, err error) { +// RelativePath of the ModelDerivativeAPI. +// Please note that the relativePath depends on the region. +// +// References: +// - https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/ +func (a *ModelDerivativeAPI) RelativePath() string { + return a.relativePath +} + +// BaseUrl of the ModelDerivativeAPI. +func (a *ModelDerivativeAPI) BaseUrl() string { + return a.Authenticator.HostPath() + a.relativePath +} + +// StartTranslation starts a translation job with the given TranslationParams and XAdsHeaders. +// +// References: +// - https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/jobs/job-POST/ +func (a *ModelDerivativeAPI) StartTranslation(params TranslationParams, xHeaders XAdsHeaders) ( + result TranslationJob, err error, +) { bearer, err := a.Authenticator.GetToken("data:write data:read") if err != nil { return } - path := a.Authenticator.GetHostPath() + a.ModelDerivativePath - result, err = translate(path, params, bearer.AccessToken) - return + return startTranslation(a.BaseUrl(), params, &xHeaders, bearer.AccessToken) } -// TranslationSVFPreset specifies the minimum necessary for translating a generic (single file, uncompressed) -// model into svf. -var TranslationSVFPreset = TranslationParams{ - Output: OutputSpec{ - Destination: DestSpec{"us"}, - Formats: []FormatSpec{ - FormatSpec{ - "svf", - []string{"2d", "3d"}, +// NewTranslationParams creates a TranslationParams struct with the given urn, outputType, views, and advanced options. +// - The region will be taken from the ModelDerivativeAPI. +// - The advanced options can be nil. +// +// Make sure to use the correct combination of views and advanced options for the given outputType. +// There are no checks for this. +func (a *ModelDerivativeAPI) NewTranslationParams( + urn string, outputType OutputType, views []ViewType, advanced *AdvancedSpec, +) TranslationParams { + return TranslationParams{ + Input: InputSpec{ + URN: urn, + }, + Output: OutputSpec{ + Destination: DestSpec{a.region}, + Formats: []FormatSpec{ + { + Type: outputType, + Views: views, + Advanced: advanced, + }, }, }, - }, + } } -// TranslateToSVF is a helper function that will use the TranslationSVFPreset for translating into svf a given ObjectID. -// It will also take care of converting objectID into Base64 (URL Safe) encoded URN. -func (a ModelDerivativeAPI) TranslateToSVF(objectID string) (result TranslationResult, err error) { - bearer, err := a.Authenticator.GetToken("data:write data:read") +// DefaultTranslationParams creates a TranslationParams struct with the given urn. +// - The region will be taken from the ModelDerivativeAPI. +// - The outputType will be SVF. +// - The views will be 2D and 3D. +// - The advanced options will be nil. +func (a *ModelDerivativeAPI) DefaultTranslationParams(urn string) TranslationParams { + return a.NewTranslationParams(urn, SVF, ViewTypes2DAnd3D(), nil) +} + +// UrnFromObjectId creates a Base64 (URL Safe) encoded URN from the given objectID. +// +// OssApi.UploadObject will return an objectID that can be used here. +// +// The URN is required as input for translating the object (CAD file), see: +// - NewTranslationParams +// - DefaultTranslationParams +func UrnFromObjectId(objectID string) string { + return base64.RawStdEncoding.EncodeToString([]byte(objectID)) +} + +// GetManifest returns information about derivatives that correspond to a specific source file, including derivative URNs and translation statuses. +// +// References: +// - https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/manifest/urn-manifest-GET/ +func (a *ModelDerivativeAPI) GetManifest(urn string) (result Manifest, err error) { + bearer, err := a.Authenticator.GetToken("data:read") if err != nil { return } - path := a.Authenticator.GetHostPath() + a.ModelDerivativePath - params := TranslationSVFPreset - params.Input.URN = base64.RawStdEncoding.EncodeToString([]byte(objectID)) - result, err = translate(path, params, bearer.AccessToken) + return getManifest(a.BaseUrl(), urn, bearer.AccessToken) +} + +// GetPropertiesDatabaseUrn returns the URN of the SQLite properties database from the manifest. +// If the database URN is not found, an empty string is returned. +// The database URN is used to download the SQLite properties database using the GetDerivative function. +func (m *Manifest) GetPropertiesDatabaseUrn() string { + for _, derivative := range m.Derivatives { + for _, child := range derivative.Children { + if child.Role == "Autodesk.CloudPlatform.PropertyDatabase" { + return child.URN + } + } + } + return "" +} + +// GetProgressReport returns the ProgressReport (status and progress) of a translation. +func (m *Manifest) GetProgressReport() ProgressReport { + return m.ProgressReport +} + +// GetProgressReportOfChild returns the ProgressReport of a translation of a given outputType for a specific child, +// identified by its model/view GUID string. +// If the child is not found, an empty ProgressReport is returned. +func (m *Manifest) GetProgressReportOfChild(derivativeOutputType, modelViewGuid string) ProgressReport { + for _, derivative := range m.Derivatives { + // strings.EqualFold => ignore casing + if strings.EqualFold(derivative.OutputType, derivativeOutputType) { + for _, child := range derivative.Children { + if child.ModelGUID != nil && *child.ModelGUID == modelViewGuid { + return child.ProgressReport + } + } + } + } + return ProgressReport{} +} - return +// GetSourceFileName returns the source file name of the translation. +// If the source file name is not found, an empty string is returned. +func (m *Manifest) GetSourceFileName() string { + // Is this always the name of the first derivative? + for _, derivative := range m.Derivatives { + if len(derivative.Name) > 0 { + return derivative.Name + } + } + return "" } +// GetDerivative downloads a selected derivative. +// To download the file, you need to specify the file’s URN, which you retrieve from the manifest. +// You can fetch the manifest using the GetManifest function. +// +// References: +// - https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/urn-manifest-derivativeUrn-signedcookies-GET/ +// +// Example: +// +// urn := "..." +// derivativeUrn := "..." +// writer, err := os.Create("propertiesDb.sqlite") +// +// if err != nil { +// log.Fatal(err) +// } +// +// defer writer.Close() +// written, err := client.GetDerivative(urn, derivativeUrn, writer) +// +// if err != nil { +// log.Fatal(err) +// } +// +// log.Printf("Downloaded %d bytes", written) +func (a *ModelDerivativeAPI) GetDerivative(urn, derivativeUrn string, writer io.Writer) (written int64, err error) { + bearer, err := a.Authenticator.GetToken("data:read") + if err != nil { + return + } + + return getDerivative(a.BaseUrl(), urn, derivativeUrn, bearer.AccessToken, writer) +} -// GetManifest returns information about derivatives that correspond to a specific source file, -// including derivative URNs and statuses. -func (a ModelDerivativeAPI) GetManifest(urn string) (result Manifest, err error) { +// GetMetadata returns a list of model views (Viewables) in the source design specified by the `urn` URI parameter. +// It also returns the ID that uniquely identifies the model view. +// You can use this ID with other metadata endpoints to obtain information about the objects within model view. +// +// NOTE: You can retrieve metadata only from an input file that has been translated to SVF or SVF2. +// +// Reference: +// - https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/metadata/urn-metadata-GET/ +func (a *ModelDerivativeAPI) GetMetadata(urn string) (result MetaData, err error) { bearer, err := a.Authenticator.GetToken("data:read") if err != nil { return } - path := a.Authenticator.GetHostPath() + a.ModelDerivativePath - result, err = getManifest(path, urn, bearer.AccessToken) - return + return getMetadata(a.BaseUrl(), urn, bearer.AccessToken) +} + +// GetMasterModelViewGuid returns the GUID of the master view. +// If no master view is found, the GUID of the first 3D view is returned. +// If no 3D view is found, an empty string is returned. +func (m *MetaData) GetMasterModelViewGuid() string { + // 1st look for the master view + for _, view := range m.Data.Views { + if view.IsMasterView { + return view.Guid + } + } + // else return the first 3d view + for _, view := range m.Data.Views { + if view.Role == View3D { + return view.Guid + } + } + return "" } +// GetModelViewProperties returns a list of all properties of all objects that are displayed in the model view specified by the modelGuid URI parameter. +// +// Properties are returned as a flat list ordered, by their `objectId`. +// The `objectId` is a non-persistent ID assigned to an object when a design file is translated to the SVF or SVF2 format. +// This means that: +// - A design file must be translated to SVF or SVF2 before you can retrieve properties. +// - The `objectId` of an object can change if the design is translated to SVF or SVF2 again. If you require a persistent ID to reference an object, use externalId. +// +// Note: Before you call this endpoint: +// - Get the modelGuid (unique model view ID) by using the GetModelViewProperties function. +// +// Reference: +// - https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/metadata/urn-metadata-guid-properties-GET/ +func (a *ModelDerivativeAPI) GetModelViewProperties(urn, modelGuid string, xHeaders XAdsHeaders) ( + jsonData []byte, err error, +) { + bearer, err := a.Authenticator.GetToken("data:read") + if err != nil { + return + } + + return getModelViewProperties(a.BaseUrl(), urn, modelGuid, bearer.AccessToken, xHeaders) +} -// GetDerivative downloads a selected derivative. To download the file, you need to specify the file’s URN, -// which you retrieve by calling the GET :urn/manifest endpoint. -func (a ModelDerivativeAPI) GetDerivative(urn, derivativeUrn string) (data []byte, err error) { +// GetObjectTree returns a hierarchical list of objects (object tree) in the model view specified by the modelGuid URI parameter. +// +// Note: Before you call this endpoint: +// - Get the modelGuid (unique model view ID) by using the GetModelViewProperties function. +// +// Use forceGet = `true` to retrieve the object tree even if it exceeds the recommended maximum size (20 MB). The default for forceGet is `false`. +// +// Reference: +// - https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/metadata/urn-metadata-guid-GET/ +func (a *ModelDerivativeAPI) GetObjectTree(urn, modelGuid string, forceGet bool, xHeaders XAdsHeaders) ( + result ObjectTree, err error, +) { bearer, err := a.Authenticator.GetToken("data:read") if err != nil { return } - path := a.Authenticator.GetHostPath() + a.ModelDerivativePath - data, err = getDerivative(path, urn, derivativeUrn, bearer.AccessToken) - return + return getObjectTree(a.BaseUrl(), urn, modelGuid, bearer.AccessToken, forceGet, xHeaders) } diff --git a/md/api_test.go b/md/api_test.go new file mode 100644 index 0000000..e615f53 --- /dev/null +++ b/md/api_test.go @@ -0,0 +1,97 @@ +package md + +import ( + "testing" + + "github.com/woweh/forge-api-go-client" +) + +func TestNewMdApi(t *testing.T) { + tests := []struct { + name string + region forge.Region + want string + }{ + { + name: "Test US Region", + region: forge.US, + want: usPath, + }, + { + name: "Test EMEA Region", + region: forge.EMEA, + want: euPath, + }, + { + name: "Test EU Region", + region: forge.EU, + want: euPath, + }, + } + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + mdApi := NewMdApi(nil, tt.region) + if mdApi.RelativePath() != tt.want { + t.Errorf("got RelativePath = %s, want %s", mdApi.RelativePath(), tt.want) + } + if mdApi.Region() != tt.region { + t.Errorf("got Region = %s, want %s", mdApi.Region(), tt.region) + } + }, + ) + } +} + +func TestModelDerivativeAPI_SetRegion(t *testing.T) { + tests := []struct { + name string + initialRegion forge.Region + initialPath string + newRegion forge.Region + newPath string + }{ + { + name: "Test US Region", + initialRegion: forge.US, + initialPath: usPath, + newRegion: forge.EMEA, + newPath: euPath, + }, + { + name: "Test EMEA Region", + initialRegion: forge.EMEA, + initialPath: euPath, + newRegion: forge.US, + newPath: usPath, + }, + { + name: "Test EU Region", + initialRegion: forge.EU, + initialPath: euPath, + newRegion: forge.US, + newPath: usPath, + }, + } + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + mdApi := NewMdApi(nil, tt.initialRegion) + if mdApi.RelativePath() != tt.initialPath { + t.Errorf("got RelativePath = %s, initialPath %s", mdApi.RelativePath(), tt.initialPath) + } + if mdApi.Region() != tt.initialRegion { + t.Errorf("got Region = %s, initialPath %s", mdApi.Region(), tt.initialRegion) + } + t.Log("Set new region: ", tt.newRegion) + mdApi.SetRegion(tt.newRegion) + if mdApi.RelativePath() != tt.newPath { + t.Errorf("got RelativePath = %s, newPath %s", mdApi.RelativePath(), tt.newPath) + } + if mdApi.Region() != tt.newRegion { + t.Errorf("got Region = %s, newPath %s", mdApi.Region(), tt.newRegion) + } + }, + ) + } +} diff --git a/md/assets/failed_manifest.json b/md/assets/failed_manifest.json new file mode 100644 index 0000000..5dfb491 --- /dev/null +++ b/md/assets/failed_manifest.json @@ -0,0 +1,70 @@ +{ + "type": "manifest", + "hasThumbnail": "false", + "status": "failed", + "progress": "complete", + "region": "US", + "urn": "dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA", + "derivatives": [ + { + "name": "A5.iam", + "hasThumbnail": "false", + "status": "failed", + "progress": "complete", + "messages": [ + { + "type": "warning", + "message": "The drawing's thumbnails were not properly created.", + "code": "TranslationWorker-ThumbnailGenerationFailed" + } + ], + "outputType": "svf", + "children": [ + { + "guid": "d998268f-eeb4-da87-0db4-c5dbbc4926d0", + "type": "geometry", + "role": "3d", + "name": "Scene", + "status": "success", + "messages": [ + { + "type": "warning", + "code": "ATF-1023", + "message": [ + "The file: {0} does not exist.", + "C:\\Users\\ADSK\\Documents\\A5\\Top.ipt" + ] + }, + { + "type": "warning", + "code": "ATF-1023", + "message": [ + "The file: {0} does not exist.", + "C:\\Users\\ADSK\\Documents\\A5\\Bottom.ipt" + ] + }, + { + "type": "error", + "code": "ATF-1026", + "message": [ + "The file: {0} is empty.", + "C:/worker/viewing-inventor-lmv/tmp/job-1/5/output/1/A5.svf" + ] + } + ], + "progress": "complete", + "hasThumbnail": "false", + "children": [ + { + "guid": "4f981e94-8241-4eaf-b08b-cd337c6b8b1f", + "type": "resource", + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/1/A5.svf", + "role": "graphics", + "mime": "application/autodesk-svf" + } + ] + } + ] + } + ] +} diff --git a/md/assets/in_progress_manifest.json b/md/assets/in_progress_manifest.json new file mode 100644 index 0000000..2eeb08c --- /dev/null +++ b/md/assets/in_progress_manifest.json @@ -0,0 +1,70 @@ +{ + "type": "manifest", + "hasThumbnail": "true", + "status": "inprogress", + "progress": "99% complete", + "region": "US", + "urn": "dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA", + "derivatives": [ + { + "name": "A5.iam", + "hasThumbnail": "true", + "status": "success", + "progress": "99% complete", + "outputType": "svf", + "children": [ + { + "guid": "d998268f-eeb4-da87-0db4-c5dbbc4926d0", + "type": "geometry", + "role": "3d", + "name": "Scene", + "status": "success", + "progress": "99% complete", + "hasThumbnail": "true", + "children": [ + { + "guid": "4f981e94-8241-4eaf-b08b-cd337c6b8b1f", + "type": "resource", + "progress": "99% complete", + "role": "graphics", + "mime": "application/autodesk-svf" + }, + { + "guid": "d718eb7e-fa8a-42f9-8b32-e323c0fbea0c", + "type": "resource", + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/1/A5.svf.png01_thumb_400x400.png", + "resolution": [ + 400.0, + 400.0 + ], + "mime": "image/png", + "role": "thumbnail" + }, + { + "guid": "34dc340b-835f-47f7-9da5-b8219aefe741", + "type": "resource", + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/1/A5.svf.png01_thumb_200x200.png", + "resolution": [ + 200.0, + 200.0 + ], + "mime": "image/png", + "role": "thumbnail" + }, + { + "guid": "299c6ba6-650e-423e-bbd6-3aaff44ee104", + "type": "resource", + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/1/A5.svf.png01_thumb_100x100.png", + "resolution": [ + 100.0, + 100.0 + ], + "mime": "image/png", + "role": "thumbnail" + } + ] + } + ] + } + ] +} diff --git a/md/assets/pending_manifest.json b/md/assets/pending_manifest.json new file mode 100644 index 0000000..bb27e59 --- /dev/null +++ b/md/assets/pending_manifest.json @@ -0,0 +1,10 @@ +{ + "type": "manifest", + "hasThumbnail": "false", + "status": "pending", + "progress": "0% complete", + "region": "US", + "urn": "dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA", + "derivatives": [ + ] +} \ No newline at end of file diff --git a/md/assets/revit_manifest.json b/md/assets/revit_manifest.json new file mode 100644 index 0000000..135ae85 --- /dev/null +++ b/md/assets/revit_manifest.json @@ -0,0 +1,160 @@ +{ + "type": "manifest", + "hasThumbnail": "true", + "status": "success", + "progress": "complete", + "region": "US", + "urn": "dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6dGVzdC1maWxlcy8yMDE3MDcyNF9BaXJwb3J0JTIwTW9kZWwucnZ0", + "version": "1.0", + "derivatives": [ + { + "name": "20170724_Airport Model.rvt", + "hasThumbnail": "true", + "status": "success", + "progress": "complete", + "messages": [ + { + "type": "warning", + "code": "Revit-MissingLink", + "message": [ + "Missing link files:
    {0}
", + "S-FIDS-Wx-Video.jpg, solutions-airport-bcic2.jpg, zone.png" + ] + } + ], + "outputType": "svf", + "children": [ + { + "guid": "6fac95cb-af5d-3e4f-b943-8a7f55847ff1", + "type": "resource", + "role": "Autodesk.CloudPlatform.PropertyDatabase", + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6dGVzdC1maWxlcy8yMDE3MDcyNF9BaXJwb3J0JTIwTW9kZWwucnZ0/output/Resource/model.sdb", + "mime": "application/autodesk-db", + "status": "success" + }, + { + "guid": "e7adaa0b-8274-132e-cdc1-65f55eb8b096", + "type": "geometry", + "role": "3d", + "name": "{3D}", + "viewableID": "a4646655-27fa-4fcc-b2cb-1c97f89f1e9b-00031929", + "phaseNames": "NewMdApi Construction", + "status": "success", + "hasThumbnail": "true", + "progress": "complete", + "children": [ + { + "guid": "a4646655-27fa-4fcc-b2cb-1c97f89f1e9b-00031929", + "type": "view", + "role": "3d", + "name": "{3D}", + "status": "success", + "progress": "complete", + "camera": [ + 68.769485, + -500.972656, + 82.696663, + -117.377716, + 29.634874, + 27.679764, + -0.032235, + 0.091885, + 0.995248, + 4.974191, + 0, + 1, + 1 + ] + }, + { + "guid": "59a17c17-6249-81c1-5ef0-504a39b3e54f", + "type": "resource", + "role": "graphics", + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6dGVzdC1maWxlcy8yMDE3MDcyNF9BaXJwb3J0JTIwTW9kZWwucnZ0/output/Resource/3D View/{3D} 203049/{3D}.svf", + "mime": "application/autodesk-svf" + }, + { + "guid": "daefa290-464d-9879-eefb-b60ee4549be1", + "type": "resource", + "role": "thumbnail", + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6dGVzdC1maWxlcy8yMDE3MDcyNF9BaXJwb3J0JTIwTW9kZWwucnZ0/output/Resource/3D View/{3D} 203049/{3D}1.png", + "resolution": [ + 100, + 100 + ], + "mime": "image/png", + "status": "success" + }, + { + "guid": "7241ccc4-2ed1-b357-abd3-f9acac457769", + "type": "resource", + "role": "thumbnail", + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6dGVzdC1maWxlcy8yMDE3MDcyNF9BaXJwb3J0JTIwTW9kZWwucnZ0/output/Resource/3D View/{3D} 203049/{3D}2.png", + "resolution": [ + 200, + 200 + ], + "mime": "image/png", + "status": "success" + }, + { + "guid": "71c50af6-78c8-3668-7aa6-62433efc5394", + "type": "resource", + "role": "thumbnail", + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6dGVzdC1maWxlcy8yMDE3MDcyNF9BaXJwb3J0JTIwTW9kZWwucnZ0/output/Resource/3D View/{3D} 203049/{3D}4.png", + "resolution": [ + 400, + 400 + ], + "mime": "image/png", + "status": "success" + } + ] + } + ] + }, + { + "status": "success", + "progress": "complete", + "outputType": "thumbnail", + "children": [ + { + "guid": "db899ab5-939f-e250-d79d-2d1637ce4565", + "type": "resource", + "role": "thumbnail", + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6dGVzdC1maWxlcy8yMDE3MDcyNF9BaXJwb3J0JTIwTW9kZWwucnZ0/output/preview1.png", + "resolution": [ + 100, + 100 + ], + "mime": "image/png", + "status": "success" + }, + { + "guid": "3f6c118d-f551-7bf0-03c9-8548d26c9772", + "type": "resource", + "role": "thumbnail", + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6dGVzdC1maWxlcy8yMDE3MDcyNF9BaXJwb3J0JTIwTW9kZWwucnZ0/output/preview2.png", + "resolution": [ + 200, + 200 + ], + "mime": "image/png", + "status": "success" + }, + { + "guid": "4e751806-0920-ce32-e9fd-47c3cec21536", + "type": "resource", + "role": "thumbnail", + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6dGVzdC1maWxlcy8yMDE3MDcyNF9BaXJwb3J0JTIwTW9kZWwucnZ0/output/preview4.png", + "resolution": [ + 400, + 400 + ], + "mime": "image/png", + "status": "success" + } + ] + } + ] +} \ No newline at end of file diff --git a/md/assets/revit_manifest_multiple_phases.json b/md/assets/revit_manifest_multiple_phases.json new file mode 100644 index 0000000..c5d2261 --- /dev/null +++ b/md/assets/revit_manifest_multiple_phases.json @@ -0,0 +1,5500 @@ +{ + "urn": "dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x", + "derivatives": [ + { + "extractorVersion": "2025.3.0.1260", + "hasThumbnail": "true", + "overrideOutputType": "svf2", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/model.sdb", + "role": "Autodesk.CloudPlatform.PropertyDatabase", + "mime": "application/autodesk-db", + "guid": "6fac95cb-af5d-3e4f-b943-8a7f55847ff1", + "type": "resource", + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/AECModelData.json", + "role": "Autodesk.AEC.ModelData", + "mime": "application/json", + "guid": "a4aac952-a3f4-031c-4113-b2d9ac2d0de6", + "type": "resource", + "status": "success" + }, + { + "guid": "08203144-4bc1-6e3f-52e3-bbf7852af4e1", + "hasThumbnail": "true", + "role": "3d", + "type": "geometry", + "phaseNames": "New Construction", + "ViewSets": "Coordination", + "name": "Coord - Arch Walls", + "progress": "complete", + "viewableID": "29948af7-066b-44c9-b460-4e3a3567113b-001921a9", + "status": "success", + "children": [ + { + "guid": "29948af7-066b-44c9-b460-4e3a3567113b-001921a9", + "role": "3d", + "name": "Coord - Arch Walls", + "progress": "complete", + "camera": [ + 32.84653091430664, + -167.76358032226562, + 241.2377471923828, + -19.750507354736328, + -6.9051971435546875, + 24.166667938232422, + -0.2450968325138092, + 0.7495836615562439, + 0.6148593425750732, + 1.1487019062042236, + 0, + 1, + 1 + ], + "type": "view", + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/3D Coordination/Coord - Arch Walls 1647017/Coord - Arch Walls.svf", + "role": "graphics", + "mime": "application/autodesk-svf", + "guid": "7738212f-d458-662f-cb9c-cf7c761af18d", + "type": "resource" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/3D Coordination/Coord - Arch Walls 1647017/Coord - Arch Walls1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "be680a8e-fea8-8a59-550a-6e4f81b84a82", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/3D Coordination/Coord - Arch Walls 1647017/Coord - Arch Walls2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "8876a470-8840-fea1-606a-321cf980c372", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/3D Coordination/Coord - Arch Walls 1647017/Coord - Arch Walls4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "0dfbc53d-1bf7-402d-d872-3ce261506d20", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + } + ] + }, + { + "guid": "34c2be7b-0480-0a14-d463-e3fbc469756e", + "hasThumbnail": "true", + "role": "3d", + "type": "geometry", + "phaseNames": "New Construction", + "ViewSets": "Coordination", + "name": "Coord - Arch Floors", + "progress": "complete", + "viewableID": "29948af7-066b-44c9-b460-4e3a3567113b-001921b5", + "status": "success", + "children": [ + { + "guid": "29948af7-066b-44c9-b460-4e3a3567113b-001921b5", + "role": "3d", + "name": "Coord - Arch Floors", + "progress": "complete", + "camera": [ + 124.78312683105469, + -202.7232208251953, + 144.117919921875, + -20.172168731689453, + -4.9051971435546875, + 21.958332061767578, + -0.263536661863327, + 0.35964399576187134, + 0.8951004147529602, + 1.3271679878234863, + 0, + 1, + 1 + ], + "type": "view", + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/3D Coordination/Coord - Arch Floors 1647029/Coord - Arch Floors.svf", + "role": "graphics", + "mime": "application/autodesk-svf", + "guid": "30e767fc-6fca-8b91-60c8-2cd50af08294", + "type": "resource" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/3D Coordination/Coord - Arch Floors 1647029/Coord - Arch Floors1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "0aa45fce-a077-8357-1d28-373040ecdc14", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/3D Coordination/Coord - Arch Floors 1647029/Coord - Arch Floors2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "3219003a-639b-59c3-f002-3684a362f38f", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/3D Coordination/Coord - Arch Floors 1647029/Coord - Arch Floors4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "07bee530-b082-9265-6ff5-2441fec45ff4", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + } + ] + }, + { + "guid": "eedf3877-170a-e92e-3292-cf53ce0ef9af", + "hasThumbnail": "true", + "role": "3d", + "type": "geometry", + "phaseNames": "New Construction", + "ViewSets": "Coordination", + "name": "Coord - Arch Stairs", + "progress": "complete", + "viewableID": "29948af7-066b-44c9-b460-4e3a3567113b-001921cb", + "status": "success", + "children": [ + { + "guid": "29948af7-066b-44c9-b460-4e3a3567113b-001921cb", + "role": "3d", + "name": "Coord - Arch Stairs", + "progress": "complete", + "camera": [ + 32.10084915161133, + -231.29534912109375, + 100.64582061767578, + -27.104167938232422, + -5.4051971435546875, + 23.1875, + -0.07982008904218674, + 0.3045446574687958, + 0.9491477012634277, + 1.5289415121078491, + 0, + 1, + 1 + ], + "type": "view", + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/3D Coordination/Coord - Arch Stairs 1647051/Coord - Arch Stairs.svf", + "role": "graphics", + "mime": "application/autodesk-svf", + "guid": "95acfaa9-0937-2560-ab46-86b6e30ee42e", + "type": "resource" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/3D Coordination/Coord - Arch Stairs 1647051/Coord - Arch Stairs1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "0b0c294a-516f-d8b9-1ba0-0659e8edff92", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/3D Coordination/Coord - Arch Stairs 1647051/Coord - Arch Stairs2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "4d054f4d-7010-c845-898e-696fe249c191", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/3D Coordination/Coord - Arch Stairs 1647051/Coord - Arch Stairs4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "059f6794-7fc5-1a01-8c93-2a6a90ca7658", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + } + ] + }, + { + "guid": "7a35b605-1e34-8c4b-fb3b-0242bbe40360", + "hasThumbnail": "true", + "role": "3d", + "type": "geometry", + "phaseNames": "New Construction", + "ViewSets": "Coordination", + "name": "Coord - Arch Lighting", + "progress": "complete", + "viewableID": "e04e969e-4680-4a0f-b085-592fbbbb7b9f-001967fe", + "status": "success", + "children": [ + { + "guid": "e04e969e-4680-4a0f-b085-592fbbbb7b9f-001967fe", + "role": "3d", + "name": "Coord - Arch Lighting", + "progress": "complete", + "camera": [ + 58.04084777832031, + -209.5172576904297, + 156.20687866210938, + -21.480728149414062, + 6.552305221557617, + 30.005207061767578, + -0.16601501405239105, + 0.4510824680328369, + 0.8769057393074036, + 1.4477330446243286, + 0, + 1, + 1 + ], + "type": "view", + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/3D Coordination/Coord - Arch Lighting 1665022/Coord - Arch Lighting.svf", + "role": "graphics", + "mime": "application/autodesk-svf", + "guid": "0551915b-128a-ab63-ee4d-18825d101da1", + "type": "resource" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/3D Coordination/Coord - Arch Lighting 1665022/Coord - Arch Lighting1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "377c1ddb-59c8-73a5-d9e2-22d48ca047e2", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/3D Coordination/Coord - Arch Lighting 1665022/Coord - Arch Lighting2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "a8682ed8-7ebe-078d-6339-ba33e76092b7", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/3D Coordination/Coord - Arch Lighting 1665022/Coord - Arch Lighting4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "fc9c7e62-4cd9-5655-9c1e-a0759e5f3147", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + } + ] + }, + { + "guid": "952ddec5-04a1-1a3c-2abd-4c8094d518d0", + "hasThumbnail": "true", + "role": "3d", + "type": "geometry", + "phaseNames": "New Construction", + "ViewSets": "Coordination", + "name": "Coord - Arch Roofs", + "progress": "complete", + "viewableID": "afdfbf8b-89a6-499e-944e-8f13c5a4bf8a-001df270", + "status": "success", + "children": [ + { + "guid": "afdfbf8b-89a6-499e-944e-8f13c5a4bf8a-001df270", + "role": "3d", + "name": "Coord - Arch Roofs", + "progress": "complete", + "camera": [ + 124.14277648925781, + -190.36642456054688, + 179.11643981933594, + -19.927600860595703, + 6.2439775466918945, + 57.70261001586914, + -0.263536661863327, + 0.35964399576187134, + 0.8951004147529602, + 1.9577382802963257, + 0, + 1, + 1 + ], + "type": "view", + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/3D Coordination/Coord - Arch Roofs 1962608/Coord - Arch Roofs.svf", + "role": "graphics", + "mime": "application/autodesk-svf", + "guid": "416e193c-65fc-a9e7-c0f1-e10440940028", + "type": "resource" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/3D Coordination/Coord - Arch Roofs 1962608/Coord - Arch Roofs1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "16ff4f3e-ce05-84d0-9187-2273f481bd5c", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/3D Coordination/Coord - Arch Roofs 1962608/Coord - Arch Roofs2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "fd080750-58a9-252a-1b83-b6e385b7d78b", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/3D Coordination/Coord - Arch Roofs 1962608/Coord - Arch Roofs4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "cc0417da-e1b0-12ef-7e20-ed9b06c5cc6b", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + } + ] + }, + { + "isMasterView": true, + "phaseNames": "Legends", + "hasThumbnail": "true", + "role": "3d", + "children": [ + { + "guid": "c884ae1b-61e7-4f9d-0001-719e20b22d0b-0025e35d", + "role": "3d", + "name": "Legends", + "progress": "complete", + "camera": [ + 124.2824478149414, + -128.91371154785156, + 161.73326110839844, + 0, + 0, + 0, + -0.40824830532073975, + 0.40824830532073975, + 0.8164966106414795, + 1.7320507764816284, + 0, + 1, + 1 + ], + "type": "view", + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/3D View/08f99ae5-b8be-4f8d-881b-128675723c10/Legends/Legends.svf", + "role": "graphics", + "mime": "application/autodesk-svf", + "guid": "9a756313-1b44-7f4b-349b-dcaab1a5dd0e", + "type": "resource" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/3D View/08f99ae5-b8be-4f8d-881b-128675723c10/Legends/Legends1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "42392836-1938-477e-37fb-5aa6807bd8a7", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/3D View/08f99ae5-b8be-4f8d-881b-128675723c10/Legends/Legends2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "7c7b6547-c374-02fd-e8d7-d7b084b0f122", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/3D View/08f99ae5-b8be-4f8d-881b-128675723c10/Legends/Legends4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "bc837e72-9b11-aec3-0395-f8e6b16e8616", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + } + ], + "name": "Legends", + "guid": "f166b07e-d36f-c9c4-19f1-08753bbeb665", + "progress": "complete", + "type": "geometry", + "viewableID": "c884ae1b-61e7-4f9d-0001-719e20b22d0b-0025e35d", + "status": "success" + }, + { + "isMasterView": true, + "phaseNames": "New Construction", + "hasThumbnail": "true", + "role": "3d", + "children": [ + { + "guid": "c884ae1b-61e7-4f9d-0002-719e20b22d0b-0025e374", + "role": "3d", + "name": "New Construction", + "progress": "complete", + "camera": [ + 139.60829162597656, + -165.37049865722656, + 185.67364501953125, + -20.357013702392578, + -5.4051971435546875, + 25.708335876464844, + -0.40824830532073975, + 0.40824830532073975, + 0.8164966106414795, + 1.03303861618042, + 0, + 1, + 1 + ], + "type": "view", + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/3D View/08f99ae5-b8be-4f8d-881b-128675723c10/New Construction/New Construction.svf", + "role": "graphics", + "mime": "application/autodesk-svf", + "guid": "eaba814b-3fb1-601f-cfe3-70669e896ac2", + "type": "resource" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/3D View/08f99ae5-b8be-4f8d-881b-128675723c10/New Construction/New Construction1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "a3c19573-8948-7ae0-fb5c-75cab1d0e87a", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/3D View/08f99ae5-b8be-4f8d-881b-128675723c10/New Construction/New Construction2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "86d53dd2-82c8-1948-5997-00ebf6078ed7", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/3D View/08f99ae5-b8be-4f8d-881b-128675723c10/New Construction/New Construction4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "7e36c074-8997-d941-4dc3-6a72fcdf3b9b", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + } + ], + "name": "New Construction", + "guid": "fc95d55f-ed74-a914-5736-e1be75e7e4fc", + "progress": "complete", + "type": "geometry", + "viewableID": "c884ae1b-61e7-4f9d-0002-719e20b22d0b-0025e374", + "status": "success" + }, + { + "guid": "cf35d5f5-7d68-4531-b60a-90e05555885e-0012fc1d", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Coordination", + "name": "WV_L1 - Dimensions Large Scale", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "cf35d5f5-7d68-4531-b60a-90e05555885e-0012fc1d", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Working Dimensions/WV_L1 - Dimensions Large Scale 1244189/pdf/WV_L1 - Dimensions Large Scale1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "287e3e15-1ada-c34f-c601-5bd15a87bfc3", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Working Dimensions/WV_L1 - Dimensions Large Scale 1244189/pdf/WV_L1 - Dimensions Large Scale2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "b3058f56-7408-e573-147d-481327c5ebd3", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Working Dimensions/WV_L1 - Dimensions Large Scale 1244189/pdf/WV_L1 - Dimensions Large Scale4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "d728dba7-2082-841a-0007-14acd3381c7e", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Working Dimensions/WV_L1 - Dimensions Large Scale 1244189/pdf/WV_L1 - Dimensions Large Scale.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "222fec72-9f07-449a-a946-428edd9c69a9", + "type": "resource", + "status": "success" + }, + { + "guid": "d325413a-39dc-4f8e-9795-5c117d633745", + "role": "2d", + "viewbox": [ + 0, + 0, + 11, + 8.5 + ], + "name": "WV_L1 - Dimensions Large Scale", + "type": "view" + } + ] + }, + { + "guid": "18c37a10-539c-471f-a5c1-eba7078189ec-0013f851", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Coordination", + "name": "WV_L2 - Dimensions Large Scale", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "18c37a10-539c-471f-a5c1-eba7078189ec-0013f851", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Working Dimensions/WV_L2 - Dimensions Large Scale 1308753/pdf/WV_L2 - Dimensions Large Scale1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "ee55b4e8-e8d1-d6c9-958d-9e6dc0318fcc", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Working Dimensions/WV_L2 - Dimensions Large Scale 1308753/pdf/WV_L2 - Dimensions Large Scale2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "81b411c7-f31b-c0cd-da48-36ff86d6c38e", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Working Dimensions/WV_L2 - Dimensions Large Scale 1308753/pdf/WV_L2 - Dimensions Large Scale4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "650d787b-23f7-9437-5516-8669df2b1560", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Working Dimensions/WV_L2 - Dimensions Large Scale 1308753/pdf/WV_L2 - Dimensions Large Scale.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "60d1220e-b2ee-4cc7-9ea6-1fcd45a20bc5", + "type": "resource", + "status": "success" + }, + { + "guid": "0cc01895-f846-4afb-a9da-f5473847d767", + "role": "2d", + "viewbox": [ + 0, + 0, + 11, + 8.5 + ], + "name": "WV_L2 - Dimensions Large Scale", + "type": "view" + } + ] + }, + { + "guid": "18c37a10-539c-471f-a5c1-eba7078189ec-0013f86a", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Coordination", + "name": "WV_L3 - Dimensions Large Scale", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "18c37a10-539c-471f-a5c1-eba7078189ec-0013f86a", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Working Dimensions/WV_L3 - Dimensions Large Scale 1308778/pdf/WV_L3 - Dimensions Large Scale1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "7cc77bf6-246e-5c3d-bebc-15ce1d6fdc08", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Working Dimensions/WV_L3 - Dimensions Large Scale 1308778/pdf/WV_L3 - Dimensions Large Scale2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "5f967018-6055-3fa9-b257-28efe7b01118", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Working Dimensions/WV_L3 - Dimensions Large Scale 1308778/pdf/WV_L3 - Dimensions Large Scale4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "4031d8ed-ffcd-a2aa-b5a1-3e6e5c91dd81", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Working Dimensions/WV_L3 - Dimensions Large Scale 1308778/pdf/WV_L3 - Dimensions Large Scale.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "3865c775-04ff-4040-95e4-3d40b68cd2d2", + "type": "resource", + "status": "success" + }, + { + "guid": "0037afbd-2a7e-4429-b0af-b97abef459a9", + "role": "2d", + "viewbox": [ + 0, + 0, + 11, + 8.5 + ], + "name": "WV_L3 - Dimensions Large Scale", + "type": "view" + } + ] + }, + { + "guid": "18c37a10-539c-471f-a5c1-eba7078189ec-0013f875", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Coordination", + "name": "WV_L4 - Dimensions Large Scale", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "18c37a10-539c-471f-a5c1-eba7078189ec-0013f875", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Working Dimensions/WV_L4 - Dimensions Large Scale 1308789/pdf/WV_L4 - Dimensions Large Scale1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "87c62274-11e0-8d6b-6e62-3db285f030b9", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Working Dimensions/WV_L4 - Dimensions Large Scale 1308789/pdf/WV_L4 - Dimensions Large Scale2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "132a6026-6687-7756-b604-5988afa94326", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Working Dimensions/WV_L4 - Dimensions Large Scale 1308789/pdf/WV_L4 - Dimensions Large Scale4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "02564b69-800a-33cd-2363-c79b986862f0", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Working Dimensions/WV_L4 - Dimensions Large Scale 1308789/pdf/WV_L4 - Dimensions Large Scale.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "42c1e037-dc7b-4ad4-a4c5-ab3ce2514e57", + "type": "resource", + "status": "success" + }, + { + "guid": "e94609fb-caba-471b-8350-484eb37fba71", + "role": "2d", + "viewbox": [ + 0, + 0, + 11, + 8.5 + ], + "name": "WV_L4 - Dimensions Large Scale", + "type": "view" + } + ] + }, + { + "guid": "18c37a10-539c-471f-a5c1-eba7078189ec-0013f881", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Coordination", + "name": "WV_L5 - Dimensions Large Scale", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "18c37a10-539c-471f-a5c1-eba7078189ec-0013f881", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Working Dimensions/WV_L5 - Dimensions Large Scale 1308801/pdf/WV_L5 - Dimensions Large Scale1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "83b2a8ec-37ee-e1c9-53ef-c7a45148632b", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Working Dimensions/WV_L5 - Dimensions Large Scale 1308801/pdf/WV_L5 - Dimensions Large Scale2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "b7f2c0fc-1894-2252-d2ee-418ed86f21ed", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Working Dimensions/WV_L5 - Dimensions Large Scale 1308801/pdf/WV_L5 - Dimensions Large Scale4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "6f3ddbf4-50f5-da62-e467-9d8cfb1bb062", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Working Dimensions/WV_L5 - Dimensions Large Scale 1308801/pdf/WV_L5 - Dimensions Large Scale.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "45befe5d-e1cc-402b-b297-43e4f0eaa46c", + "type": "resource", + "status": "success" + }, + { + "guid": "842b72e8-e051-4a6c-ae3d-12efa88e470d", + "role": "2d", + "viewbox": [ + 0, + 0, + 11, + 8.5 + ], + "name": "WV_L5 - Dimensions Large Scale", + "type": "view" + } + ] + }, + { + "guid": "18c37a10-539c-471f-a5c1-eba7078189ec-0013f88c", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Coordination", + "name": "WV_R2 - Dimensions Large Scale", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "18c37a10-539c-471f-a5c1-eba7078189ec-0013f88c", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Working Dimensions/WV_R2 - Dimensions Large Scale 1308812/pdf/WV_R2 - Dimensions Large Scale1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "1d8bb95a-a4f6-6f71-d417-33493d7e55e8", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Working Dimensions/WV_R2 - Dimensions Large Scale 1308812/pdf/WV_R2 - Dimensions Large Scale2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "822ab5dd-2db8-6d32-b84d-a47cc10ddfa0", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Working Dimensions/WV_R2 - Dimensions Large Scale 1308812/pdf/WV_R2 - Dimensions Large Scale4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "91c5f2f9-64c9-695e-43d4-0065f9ddb8e7", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Working Dimensions/WV_R2 - Dimensions Large Scale 1308812/pdf/WV_R2 - Dimensions Large Scale.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "c59f78f6-44eb-40f9-8fff-90497ef63b0f", + "type": "resource", + "status": "success" + }, + { + "guid": "564e6fe7-b6fd-412d-ae03-49f994f63f64", + "role": "2d", + "viewbox": [ + 0, + 0, + 11, + 8.5 + ], + "name": "WV_R2 - Dimensions Large Scale", + "type": "view" + } + ] + }, + { + "guid": "bae0f12a-1a2c-492c-807a-23c8aed45019-00143e99", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Coordination", + "name": "WV_Parking - Dimensions Large Scale", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "bae0f12a-1a2c-492c-807a-23c8aed45019-00143e99", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Working Dimensions/WV_Parking - Dimensions Large Scale 1326745/pdf/WV_Parking - Dimensions Large Scale1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "2a6640da-0de9-3d34-a749-985b878b69e7", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Working Dimensions/WV_Parking - Dimensions Large Scale 1326745/pdf/WV_Parking - Dimensions Large Scale2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "f9795f72-aa5d-b10b-e971-e47f1344bb96", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Working Dimensions/WV_Parking - Dimensions Large Scale 1326745/pdf/WV_Parking - Dimensions Large Scale4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "c7a0f1a8-dc0a-adab-5f39-08d8a9aa940c", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Working Dimensions/WV_Parking - Dimensions Large Scale 1326745/pdf/WV_Parking - Dimensions Large Scale.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "f775d2cd-859e-4f0b-ba28-86b2023030f4", + "type": "resource", + "status": "success" + }, + { + "guid": "13cfb42c-bb7b-4819-99e3-6733c256c377", + "role": "2d", + "viewbox": [ + 0, + 0, + 11, + 8.5 + ], + "name": "WV_Parking - Dimensions Large Scale", + "type": "view" + } + ] + }, + { + "guid": "6b9d0ab2-a90b-4bab-9042-0933c471b66b-00057c4e", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "C101 - Site Plan", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "6b9d0ab2-a90b-4bab-9042-0933c471b66b-00057c4e", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/C101 - Site Plan 359502/pdf/C101 - Site Plan1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "dfd9f962-9798-d635-3bfe-0987275aa6c1", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/C101 - Site Plan 359502/pdf/C101 - Site Plan2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "2a9150da-79f7-bcff-1320-f5230ca7c0d4", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/C101 - Site Plan 359502/pdf/C101 - Site Plan4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "c548f3ff-6759-0677-9d76-63c7ca8d8b22", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/C101 - Site Plan 359502/pdf/C101 - Site Plan.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "d7dc7eb9-578f-4dac-a2b1-c16374ab144c", + "type": "resource", + "status": "success" + }, + { + "guid": "2dd723a0-a868-4a4b-959b-966458063b1d", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "C101 - Site Plan", + "type": "view" + } + ] + }, + { + "guid": "03269b5d-9b4d-4275-8dad-0654230d2414-000cdc50", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "SD100 - Parking Deck Floor Plan", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "03269b5d-9b4d-4275-8dad-0654230d2414-000cdc50", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/SD100 - Parking Deck Floor Plan 842832/pdf/SD100 - Parking Deck Floor Plan1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "f253ffe8-5f0f-8390-15da-aa5794b3aa59", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/SD100 - Parking Deck Floor Plan 842832/pdf/SD100 - Parking Deck Floor Plan2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "2792c7ed-a926-6ffe-8d81-978f3dab81c6", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/SD100 - Parking Deck Floor Plan 842832/pdf/SD100 - Parking Deck Floor Plan4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "28148ca8-650a-6637-18c6-5db810fac719", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/SD100 - Parking Deck Floor Plan 842832/pdf/SD100 - Parking Deck Floor Plan.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "505fcb32-db1d-4c83-b8ac-eb4424afe1d0", + "type": "resource", + "status": "success" + }, + { + "guid": "7a91787a-1cc2-4323-b4fa-d18d783f84bb", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "SD100 - Parking Deck Floor Plan", + "type": "view" + } + ] + }, + { + "guid": "03269b5d-9b4d-4275-8dad-0654230d2414-000cdc55", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "SD101 - First Floor Plan", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "03269b5d-9b4d-4275-8dad-0654230d2414-000cdc55", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/SD101 - First Floor Plan 842837/pdf/SD101 - First Floor Plan1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "fd31aad8-ed18-fc24-46b2-fac01c78837f", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/SD101 - First Floor Plan 842837/pdf/SD101 - First Floor Plan2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "7097a377-b3d6-bb8f-9ebe-64b7e0ccf846", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/SD101 - First Floor Plan 842837/pdf/SD101 - First Floor Plan4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "b24c8c49-991d-bb5f-f911-941bd4264bae", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/SD101 - First Floor Plan 842837/pdf/SD101 - First Floor Plan.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "24b9670c-c585-4166-b745-bb515a68f15c", + "type": "resource", + "status": "success" + }, + { + "guid": "cde0324e-d4e4-4ac8-accf-098495b809f0", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "SD101 - First Floor Plan", + "type": "view" + } + ] + }, + { + "guid": "03269b5d-9b4d-4275-8dad-0654230d2414-000cdc5a", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "SD102 - Second Floor Plan", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "03269b5d-9b4d-4275-8dad-0654230d2414-000cdc5a", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/SD102 - Second Floor Plan 842842/pdf/SD102 - Second Floor Plan1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "3b69b33c-c3d1-694f-4490-a7b0b1c50002", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/SD102 - Second Floor Plan 842842/pdf/SD102 - Second Floor Plan2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "c7c66efe-ceef-eb8f-4d11-b67eb97be16b", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/SD102 - Second Floor Plan 842842/pdf/SD102 - Second Floor Plan4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "70efaf39-6e31-99f6-58ae-8f649f16aadf", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/SD102 - Second Floor Plan 842842/pdf/SD102 - Second Floor Plan.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "2d3cc67d-5591-4d87-8ff6-2ded4ca93a5b", + "type": "resource", + "status": "success" + }, + { + "guid": "91bb5f16-676a-4930-9383-92d032493948", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "SD102 - Second Floor Plan", + "type": "view" + } + ] + }, + { + "guid": "03269b5d-9b4d-4275-8dad-0654230d2414-000cdc5f", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "SD103 - Third Floor Plan", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "03269b5d-9b4d-4275-8dad-0654230d2414-000cdc5f", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/SD103 - Third Floor Plan 842847/pdf/SD103 - Third Floor Plan1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "90de56a6-3e3d-5ff1-c89d-ff73cc09b942", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/SD103 - Third Floor Plan 842847/pdf/SD103 - Third Floor Plan2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "2a5dbebf-03c9-e5ff-3f31-acaaa8058b66", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/SD103 - Third Floor Plan 842847/pdf/SD103 - Third Floor Plan4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "d0e97858-5c42-7eb2-f4fc-683ea4f8528e", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/SD103 - Third Floor Plan 842847/pdf/SD103 - Third Floor Plan.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "d449e173-b20b-4c70-859b-121f5885b7c6", + "type": "resource", + "status": "success" + }, + { + "guid": "b9ab6bcb-3cbb-4fb6-ade3-2e314a567889", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "SD103 - Third Floor Plan", + "type": "view" + } + ] + }, + { + "guid": "03269b5d-9b4d-4275-8dad-0654230d2414-000cdc64", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "SD104 - Fourth Floor Plan", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "03269b5d-9b4d-4275-8dad-0654230d2414-000cdc64", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/SD104 - Fourth Floor Plan 842852/pdf/SD104 - Fourth Floor Plan1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "2492eb4a-26da-a5b3-8082-20b8607c3ec3", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/SD104 - Fourth Floor Plan 842852/pdf/SD104 - Fourth Floor Plan2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "acbee4e0-cdcb-a8f1-5527-8d592b4043b6", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/SD104 - Fourth Floor Plan 842852/pdf/SD104 - Fourth Floor Plan4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "eefcdf3c-4ea9-956d-ac9c-7b210387147c", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/SD104 - Fourth Floor Plan 842852/pdf/SD104 - Fourth Floor Plan.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "67d0c5a5-71f5-4f90-9412-125d15ca5e4e", + "type": "resource", + "status": "success" + }, + { + "guid": "f47f81e6-7240-40e1-a898-2cd7dafb0858", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "SD104 - Fourth Floor Plan", + "type": "view" + } + ] + }, + { + "guid": "03269b5d-9b4d-4275-8dad-0654230d2414-000cdc69", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "SD105 - Fifth Floor Plan", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "03269b5d-9b4d-4275-8dad-0654230d2414-000cdc69", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/SD105 - Fifth Floor Plan 842857/pdf/SD105 - Fifth Floor Plan1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "e703f838-7463-006a-ae26-2eb628972e90", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/SD105 - Fifth Floor Plan 842857/pdf/SD105 - Fifth Floor Plan2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "79b3289b-e506-d085-cf51-2f8940d0186e", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/SD105 - Fifth Floor Plan 842857/pdf/SD105 - Fifth Floor Plan4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "f95fe6d0-70d1-931a-d08a-d2aa41978301", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/SD105 - Fifth Floor Plan 842857/pdf/SD105 - Fifth Floor Plan.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "9ea29e9f-ed2a-4a3d-90f9-3825d51b811d", + "type": "resource", + "status": "success" + }, + { + "guid": "62e8c1c5-6181-4c75-ae96-29fd693cf50d", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "SD105 - Fifth Floor Plan", + "type": "view" + } + ] + }, + { + "guid": "03269b5d-9b4d-4275-8dad-0654230d2414-000cdc6e", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "SD106 - Roof Plan", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "03269b5d-9b4d-4275-8dad-0654230d2414-000cdc6e", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/SD106 - Roof Plan 842862/pdf/SD106 - Roof Plan1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "19e4d70a-dc83-c648-bc7a-06328220d235", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/SD106 - Roof Plan 842862/pdf/SD106 - Roof Plan2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "4f2102e7-36c1-24e3-867a-f374209068d3", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/SD106 - Roof Plan 842862/pdf/SD106 - Roof Plan4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "9221420c-a552-5377-8291-1ecf8be7e5a1", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/SD106 - Roof Plan 842862/pdf/SD106 - Roof Plan.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "dd9d23f6-af32-4a0e-8ba6-39554629de70", + "type": "resource", + "status": "success" + }, + { + "guid": "bbad47cf-1cd4-4795-a1c9-06976219965c", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "SD106 - Roof Plan", + "type": "view" + } + ] + }, + { + "guid": "604fef57-cfb5-4505-9f38-b5e1854bad64-000cf3d4", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "A401 - Typical Public Restroom", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "604fef57-cfb5-4505-9f38-b5e1854bad64-000cf3d4", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A401 - Typical Public Restroom 848852/pdf/A401 - Typical Public Restroom1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "00b3b879-e544-b26c-29fb-c0db6eb46ef8", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A401 - Typical Public Restroom 848852/pdf/A401 - Typical Public Restroom2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "926448b7-607a-0956-67ed-ecccd8d7b5d0", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A401 - Typical Public Restroom 848852/pdf/A401 - Typical Public Restroom4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "4921a393-5ec0-f110-44ed-cf662006b802", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A401 - Typical Public Restroom 848852/pdf/A401 - Typical Public Restroom.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "4610d24c-77ec-4e3a-aeac-396cb9220320", + "type": "resource", + "status": "success" + }, + { + "guid": "56c3cf8a-9398-495a-8104-4e739396c9c4", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A401 - Typical Public Restroom", + "type": "view" + } + ] + }, + { + "guid": "604fef57-cfb5-4505-9f38-b5e1854bad64-000cf3e3", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "A902 - Stair Towers - Cutaway Views", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "604fef57-cfb5-4505-9f38-b5e1854bad64-000cf3e3", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A902 - Stair Towers - Cutaway Views 848867/pdf/A902 - Stair Towers - Cutaway Views1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "d634a618-1880-7d6f-671d-3ebff9455f4d", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A902 - Stair Towers - Cutaway Views 848867/pdf/A902 - Stair Towers - Cutaway Views2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "ce89af22-5a8f-97e2-f3e7-dde3080f3ff6", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A902 - Stair Towers - Cutaway Views 848867/pdf/A902 - Stair Towers - Cutaway Views4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "bed3db47-7816-6d36-2051-7bbcc258d789", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A902 - Stair Towers - Cutaway Views 848867/pdf/A902 - Stair Towers - Cutaway Views.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "e463706c-e3d5-46d4-8d1d-73d8c11fd56f", + "type": "resource", + "status": "success" + }, + { + "guid": "5a7d6b36-36f9-4835-9760-ed6eb0be4a02", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A902 - Stair Towers - Cutaway Views", + "type": "view" + } + ] + }, + { + "guid": "604fef57-cfb5-4505-9f38-b5e1854bad64-000cf3f2", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "A901 - Perspective From Above", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "604fef57-cfb5-4505-9f38-b5e1854bad64-000cf3f2", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A901 - Perspective From Above 848882/pdf/A901 - Perspective From Above1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "2622d2f3-0841-b88d-e45a-d6f600eeefaf", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A901 - Perspective From Above 848882/pdf/A901 - Perspective From Above2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "48b8b697-fa71-0ba7-eeca-50f373ef25b7", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A901 - Perspective From Above 848882/pdf/A901 - Perspective From Above4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "6aa0245c-a730-3084-e401-19786bd562f0", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A901 - Perspective From Above 848882/pdf/A901 - Perspective From Above.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "44784510-a6ce-4de6-880d-98baa1af1919", + "type": "resource", + "status": "success" + }, + { + "guid": "b04beb78-c333-40e6-8178-4a2a73e96224", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A901 - Perspective From Above", + "type": "view" + } + ] + }, + { + "guid": "487309ac-3af1-4fab-a7bb-9b30a3910b36-000cfaaf", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "ViewSets": "Arch - All Sheets", + "name": "A601 - Door Schedule", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "487309ac-3af1-4fab-a7bb-9b30a3910b36-000cfaaf", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A601 - Door Schedule 850607/pdf/A601 - Door Schedule1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "5cf19c7f-ef20-8f63-1894-d5c80d6d5123", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A601 - Door Schedule 850607/pdf/A601 - Door Schedule2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "72d20ce8-a3d4-9b9e-5a4e-b70514333d8d", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A601 - Door Schedule 850607/pdf/A601 - Door Schedule4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "c88f98e3-7636-a4d4-58ee-48eb9499a0d3", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A601 - Door Schedule 850607/pdf/A601 - Door Schedule.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "3fed2198-e843-48b6-83e0-f60c3178b7fd", + "type": "resource", + "status": "success" + }, + { + "guid": "034bb8d5-2936-4bf4-bd86-2f9e789b7952", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A601 - Door Schedule", + "type": "view" + } + ] + }, + { + "guid": "487309ac-3af1-4fab-a7bb-9b30a3910b36-000cfabf", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "ViewSets": "Arch - All Sheets", + "name": "G000 - Cover", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "487309ac-3af1-4fab-a7bb-9b30a3910b36-000cfabf", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G000 - Cover 850623/pdf/G000 - Cover1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "4eb6c0d3-46ae-7e9c-76da-719fc7b53748", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G000 - Cover 850623/pdf/G000 - Cover2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "a6b65900-8444-049a-0aa1-6f8ef3caa473", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G000 - Cover 850623/pdf/G000 - Cover4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "cbc048f9-c873-5b57-42e5-c2a6ba35828f", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G000 - Cover 850623/pdf/G000 - Cover.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "43cb6407-9d45-4e0c-93f3-b7a22ffa0f37", + "type": "resource", + "status": "success" + }, + { + "guid": "3d378f64-22e7-462d-a081-2c80f8d0c0ac", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "G000 - Cover", + "type": "view" + } + ] + }, + { + "guid": "487309ac-3af1-4fab-a7bb-9b30a3910b36-000cfef3", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "A903 - 3D Views", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "487309ac-3af1-4fab-a7bb-9b30a3910b36-000cfef3", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A903 - 3D Views 851699/pdf/A903 - 3D Views1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "0f4aaae4-455d-94ce-b881-06a8a648eaca", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A903 - 3D Views 851699/pdf/A903 - 3D Views2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "fa9767ed-0555-62ff-93c6-f37b354818e6", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A903 - 3D Views 851699/pdf/A903 - 3D Views4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "f2fa2545-57e8-141b-850e-dd9fad928e8d", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A903 - 3D Views 851699/pdf/A903 - 3D Views.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "d5fb8e3e-706b-4b71-adf6-ef1bde4eca3d", + "type": "resource", + "status": "success" + }, + { + "guid": "9f49bdfc-29a3-4c97-b6d8-6660610583e1", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A903 - 3D Views", + "type": "view" + } + ] + }, + { + "guid": "e42f54e4-4240-43da-a817-c69e489e25b7-000d03e4", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "K101 - Café Kitchen", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "e42f54e4-4240-43da-a817-c69e489e25b7-000d03e4", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/K101 - Café Kitchen 852964/pdf/K101 - Café Kitchen1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "e5ea3f8d-2750-c588-2842-faec06ecc27e", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/K101 - Café Kitchen 852964/pdf/K101 - Café Kitchen2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "99fd889a-84c7-758f-d0ee-f4cb5481375d", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/K101 - Café Kitchen 852964/pdf/K101 - Café Kitchen4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "71e4cfe5-9f24-c5f5-0dbc-ad8b82d2c8a4", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/K101 - Café Kitchen 852964/pdf/K101 - Café Kitchen.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "75ace5e1-3055-4224-951b-f3079dcc034a", + "type": "resource", + "status": "success" + }, + { + "guid": "6113ac2b-9ce8-47b8-b18d-ee3190903b65", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "K101 - Café Kitchen", + "type": "view" + } + ] + }, + { + "guid": "abf6d7c8-77b7-4005-a8b7-5dd2b1220a3c-00149b95", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "A100 - Parking Deck Floor Plan", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "abf6d7c8-77b7-4005-a8b7-5dd2b1220a3c-00149b95", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A100 - Parking Deck Floor Plan 1350549/pdf/A100 - Parking Deck Floor Plan1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "e55167bb-81dc-bd98-bc1e-8e971eb73091", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A100 - Parking Deck Floor Plan 1350549/pdf/A100 - Parking Deck Floor Plan2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "e1b7dfdb-9458-11b9-ee4e-5daac8030bc5", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A100 - Parking Deck Floor Plan 1350549/pdf/A100 - Parking Deck Floor Plan4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "313ced38-d627-7c54-2ea1-e5d8b6d6a403", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A100 - Parking Deck Floor Plan 1350549/pdf/A100 - Parking Deck Floor Plan.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "615845b9-0f70-44eb-adcd-39b7435af3b8", + "type": "resource", + "status": "success" + }, + { + "guid": "5e94f318-703a-4090-9d83-c299c2186b12", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A100 - Parking Deck Floor Plan", + "type": "view" + } + ] + }, + { + "guid": "abf6d7c8-77b7-4005-a8b7-5dd2b1220a3c-00149baf", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "A101 - First Floor Plan", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "abf6d7c8-77b7-4005-a8b7-5dd2b1220a3c-00149baf", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A101 - First Floor Plan 1350575/pdf/A101 - First Floor Plan1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "aa208438-5bbd-2153-058a-695a875f0dba", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A101 - First Floor Plan 1350575/pdf/A101 - First Floor Plan2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "2dede054-e114-a5e9-3653-449ea69658fc", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A101 - First Floor Plan 1350575/pdf/A101 - First Floor Plan4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "d90c4305-717f-131f-479a-5427cc852e87", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A101 - First Floor Plan 1350575/pdf/A101 - First Floor Plan.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "95a36017-fc02-4d08-b994-e1322a3a2d81", + "type": "resource", + "status": "success" + }, + { + "guid": "081b6ca4-8465-46b3-bd3d-06e775148299", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A101 - First Floor Plan", + "type": "view" + } + ] + }, + { + "guid": "abf6d7c8-77b7-4005-a8b7-5dd2b1220a3c-00149be1", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "A102 - Second Floor Plan", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "abf6d7c8-77b7-4005-a8b7-5dd2b1220a3c-00149be1", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A102 - Second Floor Plan 1350625/pdf/A102 - Second Floor Plan1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "d083f5fe-72db-9a27-3de6-66c53255814f", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A102 - Second Floor Plan 1350625/pdf/A102 - Second Floor Plan2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "56e3b0a6-d46f-b4df-5bc0-4b7b207f8218", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A102 - Second Floor Plan 1350625/pdf/A102 - Second Floor Plan4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "00ec35c7-764a-0248-2124-a3b905a36076", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A102 - Second Floor Plan 1350625/pdf/A102 - Second Floor Plan.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "83d0c4af-50d6-43a6-a869-1f1cb4e4a464", + "type": "resource", + "status": "success" + }, + { + "guid": "0c164769-110b-40f4-9a5c-c7a0e6fa3fe8", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A102 - Second Floor Plan", + "type": "view" + } + ] + }, + { + "guid": "abf6d7c8-77b7-4005-a8b7-5dd2b1220a3c-00149c0d", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "A103 - Third Floor Plan", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "abf6d7c8-77b7-4005-a8b7-5dd2b1220a3c-00149c0d", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A103 - Third Floor Plan 1350669/pdf/A103 - Third Floor Plan1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "b0493abd-15d4-0342-2029-b8c7804b5094", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A103 - Third Floor Plan 1350669/pdf/A103 - Third Floor Plan2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "5ff534de-3283-d1ce-8225-87356b63ac94", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A103 - Third Floor Plan 1350669/pdf/A103 - Third Floor Plan4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "1c982d78-3c74-a481-ab1c-61f51493dcdf", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A103 - Third Floor Plan 1350669/pdf/A103 - Third Floor Plan.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "a960328c-ce08-458a-99d0-1bb7544852e3", + "type": "resource", + "status": "success" + }, + { + "guid": "6949f987-69ec-4854-935b-f7681e2fd384", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A103 - Third Floor Plan", + "type": "view" + } + ] + }, + { + "guid": "abf6d7c8-77b7-4005-a8b7-5dd2b1220a3c-00149c27", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "A104 - Fourth Floor Plan", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "abf6d7c8-77b7-4005-a8b7-5dd2b1220a3c-00149c27", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A104 - Fourth Floor Plan 1350695/pdf/A104 - Fourth Floor Plan1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "7ce08af5-4ae7-f5a5-5313-20a5eddf68cd", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A104 - Fourth Floor Plan 1350695/pdf/A104 - Fourth Floor Plan2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "606b7df9-e3c4-06f0-8f24-43004657de00", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A104 - Fourth Floor Plan 1350695/pdf/A104 - Fourth Floor Plan4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "7442fc7a-6c20-d8af-76ad-147b8b4bb094", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A104 - Fourth Floor Plan 1350695/pdf/A104 - Fourth Floor Plan.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "3a8ab4fc-0434-4a62-bcc9-eaf3c0b98cab", + "type": "resource", + "status": "success" + }, + { + "guid": "da631942-c1e8-4c68-81a0-6aba5fd2edb2", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A104 - Fourth Floor Plan", + "type": "view" + } + ] + }, + { + "guid": "abf6d7c8-77b7-4005-a8b7-5dd2b1220a3c-00149c41", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "A105 - Fifth Floor Plan", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "abf6d7c8-77b7-4005-a8b7-5dd2b1220a3c-00149c41", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A105 - Fifth Floor Plan 1350721/pdf/A105 - Fifth Floor Plan1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "133d696b-d7bc-76fa-9510-c44e182df604", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A105 - Fifth Floor Plan 1350721/pdf/A105 - Fifth Floor Plan2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "31c4f45c-a1fc-8be0-0710-0596a379e827", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A105 - Fifth Floor Plan 1350721/pdf/A105 - Fifth Floor Plan4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "bba758b5-d1fc-1351-f474-c4a05c1698e1", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A105 - Fifth Floor Plan 1350721/pdf/A105 - Fifth Floor Plan.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "7549e894-46bb-4a3b-88f9-dd31451f4d4f", + "type": "resource", + "status": "success" + }, + { + "guid": "bb90230a-9cb2-417f-b3e7-e41b6b0a8ef1", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A105 - Fifth Floor Plan", + "type": "view" + } + ] + }, + { + "guid": "abf6d7c8-77b7-4005-a8b7-5dd2b1220a3c-00149c5b", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "A107 - Roof Plan", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "abf6d7c8-77b7-4005-a8b7-5dd2b1220a3c-00149c5b", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A107 - Roof Plan 1350747/pdf/A107 - Roof Plan1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "0bb4b9cf-66d7-f7eb-0cec-46854c998777", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A107 - Roof Plan 1350747/pdf/A107 - Roof Plan2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "53b7a225-d5d8-ecf5-e8d0-06635fa3da5d", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A107 - Roof Plan 1350747/pdf/A107 - Roof Plan4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "ef3ef027-f6a0-0b12-680e-ec428ca5eb18", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A107 - Roof Plan 1350747/pdf/A107 - Roof Plan.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "3c8e8f12-61e9-4930-b011-6a89b3fe6774", + "type": "resource", + "status": "success" + }, + { + "guid": "1865a6cb-7488-42ad-a840-095705d08831", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A107 - Roof Plan", + "type": "view" + } + ] + }, + { + "guid": "7587b0c1-db93-4407-be70-bee49ca3f935-00164922", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "A501 - Details", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "7587b0c1-db93-4407-be70-bee49ca3f935-00164922", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A501 - Details 1460514/pdf/A501 - Details1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "263a6c6d-6b80-3ef9-7d01-8648517af8a6", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A501 - Details 1460514/pdf/A501 - Details2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "e58fdd8b-6a1b-f9c1-fb6d-7208f4ee1fbe", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A501 - Details 1460514/pdf/A501 - Details4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "4c3a92c3-041b-8e1a-37f9-853d76b5b426", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A501 - Details 1460514/pdf/A501 - Details.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "9571d3ed-f4a1-4ae1-af4a-885cfb54a8b3", + "type": "resource", + "status": "success" + }, + { + "guid": "1c9e968e-67a1-4add-a073-ac2937297593", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A501 - Details", + "type": "view" + } + ] + }, + { + "guid": "0a7af33c-3cf6-493f-9ee6-3da1bbc36e88-001f0ed4", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "A405 - Wall Sections", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "0a7af33c-3cf6-493f-9ee6-3da1bbc36e88-001f0ed4", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A405 - Wall Sections 2035412/pdf/A405 - Wall Sections1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "558436d2-89e5-5f76-6436-bdf63ca2be7a", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A405 - Wall Sections 2035412/pdf/A405 - Wall Sections2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "e72a94ed-d7d7-c38f-8f36-29810222ef5f", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A405 - Wall Sections 2035412/pdf/A405 - Wall Sections4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "e1b3f66f-8a56-ebaf-a161-2b791dce115c", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A405 - Wall Sections 2035412/pdf/A405 - Wall Sections.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "87fbee42-4d29-453e-a679-83292abe960f", + "type": "resource", + "status": "success" + }, + { + "guid": "b93a25db-7549-4ff1-9739-1ad17579971b", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A405 - Wall Sections", + "type": "view" + } + ] + }, + { + "guid": "06267d70-f4a6-48ea-8cc9-5f52ccc923be-001f2886", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "A406 - Wall Sections", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "06267d70-f4a6-48ea-8cc9-5f52ccc923be-001f2886", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A406 - Wall Sections 2041990/pdf/A406 - Wall Sections1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "0a7902c5-2503-8c9f-6f92-6529f5d6962e", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A406 - Wall Sections 2041990/pdf/A406 - Wall Sections2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "5f4c2c5b-8f30-0154-b7b3-ac36057898b6", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A406 - Wall Sections 2041990/pdf/A406 - Wall Sections4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "0fdf176d-1e66-7745-9ff7-e2999897b099", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A406 - Wall Sections 2041990/pdf/A406 - Wall Sections.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "50d2f304-dd02-4987-bdfb-50d5cca42dd7", + "type": "resource", + "status": "success" + }, + { + "guid": "e70b438e-c8e4-4fac-949d-74f2fd1042d2", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A406 - Wall Sections", + "type": "view" + } + ] + }, + { + "guid": "d8663925-b904-4207-954f-f6acb859abfc-002060e4", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "A302 - Building Sections", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "d8663925-b904-4207-954f-f6acb859abfc-002060e4", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A302 - Building Sections 2121956/pdf/A302 - Building Sections1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "94c72ca2-41a8-a012-5149-311a5eef1cd3", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A302 - Building Sections 2121956/pdf/A302 - Building Sections2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "f7685d33-29c0-acc5-16df-1c4a7e3e99c4", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A302 - Building Sections 2121956/pdf/A302 - Building Sections4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "13a9194d-432d-677f-7a19-f1c03f3f2be1", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A302 - Building Sections 2121956/pdf/A302 - Building Sections.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "f06f116e-9090-468c-9f02-9a737062a1bd", + "type": "resource", + "status": "success" + }, + { + "guid": "ea174a83-fea2-47a6-bcd5-452b9b95b47e", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A302 - Building Sections", + "type": "view" + } + ] + }, + { + "guid": "f4c501e5-cacc-488d-b04a-cdf717c3b819-00206655", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "A303 - Building Sections", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "f4c501e5-cacc-488d-b04a-cdf717c3b819-00206655", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A303 - Building Sections 2123349/pdf/A303 - Building Sections1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "2aab7b02-6782-deb0-9d74-389b4919b63c", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A303 - Building Sections 2123349/pdf/A303 - Building Sections2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "705445f9-c947-a7ee-ea3b-763da97d9827", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A303 - Building Sections 2123349/pdf/A303 - Building Sections4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "4b38c029-7c02-8ea6-f2f7-7e6b9a16c625", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A303 - Building Sections 2123349/pdf/A303 - Building Sections.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "351ee291-721c-47f8-bb24-b6598a4b934b", + "type": "resource", + "status": "success" + }, + { + "guid": "9b13d5f0-b35f-4cc4-a2ad-45a837a6b949", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A303 - Building Sections", + "type": "view" + } + ] + }, + { + "guid": "f4c501e5-cacc-488d-b04a-cdf717c3b819-00206681", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "A304 - Building Sections", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "f4c501e5-cacc-488d-b04a-cdf717c3b819-00206681", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A304 - Building Sections 2123393/pdf/A304 - Building Sections1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "46c55cd0-bcd7-dbbd-1c72-6dd67ae1c8aa", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A304 - Building Sections 2123393/pdf/A304 - Building Sections2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "cb274cb2-e81b-1185-38a6-ed03517c68a2", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A304 - Building Sections 2123393/pdf/A304 - Building Sections4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "5bed733a-99ae-8031-279d-333cb640c374", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A304 - Building Sections 2123393/pdf/A304 - Building Sections.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "ee048c22-ddba-44f4-8f62-05ea093ad25c", + "type": "resource", + "status": "success" + }, + { + "guid": "f964f283-f15b-45d6-93dd-3daaedba37df", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A304 - Building Sections", + "type": "view" + } + ] + }, + { + "guid": "f4c501e5-cacc-488d-b04a-cdf717c3b819-002066ad", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "A305 - Building Sections", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "f4c501e5-cacc-488d-b04a-cdf717c3b819-002066ad", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A305 - Building Sections 2123437/pdf/A305 - Building Sections1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "e87a0d35-1ffe-012a-a5b0-2dbf5b0c4494", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A305 - Building Sections 2123437/pdf/A305 - Building Sections2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "26750d19-6fb9-0a4a-43b5-96d77a35d939", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A305 - Building Sections 2123437/pdf/A305 - Building Sections4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "99405e5d-c834-f8aa-9aeb-915b650203ae", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A305 - Building Sections 2123437/pdf/A305 - Building Sections.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "d5f48d67-b378-4002-97ac-a12b40754f18", + "type": "resource", + "status": "success" + }, + { + "guid": "ac3d4684-9322-4ae9-8704-3c15233168eb", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A305 - Building Sections", + "type": "view" + } + ] + }, + { + "guid": "f4c501e5-cacc-488d-b04a-cdf717c3b819-00206705", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "A307 - Building Sections", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "f4c501e5-cacc-488d-b04a-cdf717c3b819-00206705", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A307 - Building Sections 2123525/pdf/A307 - Building Sections1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "802235a2-fcb7-051e-bac3-45254ae13433", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A307 - Building Sections 2123525/pdf/A307 - Building Sections2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "3c8d243f-6363-f9c0-d11a-7fad6e275f4c", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A307 - Building Sections 2123525/pdf/A307 - Building Sections4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "bc6e7444-d445-1ec9-4124-1dfe367979e2", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A307 - Building Sections 2123525/pdf/A307 - Building Sections.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "54605dcc-996e-4626-bcd7-baf76721c90d", + "type": "resource", + "status": "success" + }, + { + "guid": "92e4582c-7cab-4d0a-aeea-027e0fd28db2", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A307 - Building Sections", + "type": "view" + } + ] + }, + { + "guid": "f4c501e5-cacc-488d-b04a-cdf717c3b819-0020676a", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "A301 - Building Sections", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "f4c501e5-cacc-488d-b04a-cdf717c3b819-0020676a", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A301 - Building Sections 2123626/pdf/A301 - Building Sections1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "6a422f3b-909c-906a-0011-1da2ef4414f0", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A301 - Building Sections 2123626/pdf/A301 - Building Sections2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "6299c064-0b39-a47d-1266-30c3fef5e527", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A301 - Building Sections 2123626/pdf/A301 - Building Sections4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "85c0c9c3-46e4-99f7-0c94-d89d0cf17411", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A301 - Building Sections 2123626/pdf/A301 - Building Sections.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "f6a7fadc-10dd-44e6-9937-8012ea930000", + "type": "resource", + "status": "success" + }, + { + "guid": "f5605114-c614-4de1-b7a1-a70c7d6c35ed", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A301 - Building Sections", + "type": "view" + } + ] + }, + { + "guid": "f4c501e5-cacc-488d-b04a-cdf717c3b819-00206812", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "A306 - Building Sections", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "f4c501e5-cacc-488d-b04a-cdf717c3b819-00206812", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A306 - Building Sections 2123794/pdf/A306 - Building Sections1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "2b11db72-b061-03aa-3543-443d1b536377", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A306 - Building Sections 2123794/pdf/A306 - Building Sections2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "15a74296-9d4b-d0cf-8f14-b3a42a14b877", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A306 - Building Sections 2123794/pdf/A306 - Building Sections4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "cb9ffecb-98f8-9633-fabd-ee6571907055", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A306 - Building Sections 2123794/pdf/A306 - Building Sections.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "4c654a6c-cd4b-4717-9db3-cfbdbb6adb32", + "type": "resource", + "status": "success" + }, + { + "guid": "2e36e685-9d92-4eb4-ac73-48013b445256", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A306 - Building Sections", + "type": "view" + } + ] + }, + { + "guid": "d51f4fca-0196-4b94-ada3-8d176e8be166-00206830", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "A201 - Building Elevations", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "d51f4fca-0196-4b94-ada3-8d176e8be166-00206830", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A201 - Building Elevations 2123824/pdf/A201 - Building Elevations1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "24049595-66c5-3d54-63ec-825fb75b0d8c", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A201 - Building Elevations 2123824/pdf/A201 - Building Elevations2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "1d54af5a-613c-da85-cd9e-a53f10020e71", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A201 - Building Elevations 2123824/pdf/A201 - Building Elevations4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "3b1c1c21-f2d3-638a-6531-273a05d380a5", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A201 - Building Elevations 2123824/pdf/A201 - Building Elevations.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "a7d5863f-e6d3-47df-8c4d-2511b74f4af7", + "type": "resource", + "status": "success" + }, + { + "guid": "7d93b866-57b7-4b30-b930-11476c103c2b", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A201 - Building Elevations", + "type": "view" + } + ] + }, + { + "guid": "fe1acf37-538e-4877-b6fe-c56e696b4c79-00206c16", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "A202 - Building Elevations", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "fe1acf37-538e-4877-b6fe-c56e696b4c79-00206c16", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A202 - Building Elevations 2124822/pdf/A202 - Building Elevations1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "1f71b7d6-2c86-9f40-86f1-825be776fc67", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A202 - Building Elevations 2124822/pdf/A202 - Building Elevations2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "dde62937-f6a5-5755-1de9-9663d0c3f3e9", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A202 - Building Elevations 2124822/pdf/A202 - Building Elevations4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "8c295603-9ee2-3246-8b26-b618b915d210", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A202 - Building Elevations 2124822/pdf/A202 - Building Elevations.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "79c2d4c2-342d-415d-aefd-e41f2a2aaf19", + "type": "resource", + "status": "success" + }, + { + "guid": "d1ba5c45-2d35-4385-8e7f-85e059d3d891", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A202 - Building Elevations", + "type": "view" + } + ] + }, + { + "guid": "836eb15c-b5d2-4a94-a74f-3d51032616d2-0020e7a0", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "A108 - First Floor Ceiling Plan", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "836eb15c-b5d2-4a94-a74f-3d51032616d2-0020e7a0", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A108 - First Floor Ceiling Plan 2156448/pdf/A108 - First Floor Ceiling Plan1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "eee48b2f-55b1-aae7-b087-74be5e596be3", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A108 - First Floor Ceiling Plan 2156448/pdf/A108 - First Floor Ceiling Plan2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "99602790-6bab-c6c7-aa53-db55a910d0a6", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A108 - First Floor Ceiling Plan 2156448/pdf/A108 - First Floor Ceiling Plan4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "6315d178-e424-9290-b76f-804c5822e0d4", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A108 - First Floor Ceiling Plan 2156448/pdf/A108 - First Floor Ceiling Plan.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "11580f29-b147-46e5-a72d-bbc9166f3b1c", + "type": "resource", + "status": "success" + }, + { + "guid": "8c31f547-0df2-4d49-b0fe-b2fb11cc5c9f", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A108 - First Floor Ceiling Plan", + "type": "view" + } + ] + }, + { + "guid": "836eb15c-b5d2-4a94-a74f-3d51032616d2-0020e7df", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "A109 - Second Floor Ceiling Plan", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "836eb15c-b5d2-4a94-a74f-3d51032616d2-0020e7df", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A109 - Second Floor Ceiling Plan 2156511/pdf/A109 - Second Floor Ceiling Plan1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "77e08353-4ced-834c-2f34-ea00d314a263", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A109 - Second Floor Ceiling Plan 2156511/pdf/A109 - Second Floor Ceiling Plan2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "9b88a183-79f8-61d7-6467-8bf5e3f6dacb", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A109 - Second Floor Ceiling Plan 2156511/pdf/A109 - Second Floor Ceiling Plan4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "793e4e7e-d83e-5cef-c199-e527f93e9ac3", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A109 - Second Floor Ceiling Plan 2156511/pdf/A109 - Second Floor Ceiling Plan.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "bc5884fe-2085-4196-b732-7d5a25ad0674", + "type": "resource", + "status": "success" + }, + { + "guid": "91d12916-8d70-4b51-8063-bd523464bc8e", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A109 - Second Floor Ceiling Plan", + "type": "view" + } + ] + }, + { + "guid": "836eb15c-b5d2-4a94-a74f-3d51032616d2-0020e802", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "A110 - Third Floor Ceiling Plan", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "836eb15c-b5d2-4a94-a74f-3d51032616d2-0020e802", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A110 - Third Floor Ceiling Plan 2156546/pdf/A110 - Third Floor Ceiling Plan1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "296e325f-b6d7-08fd-b62e-536f29b40238", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A110 - Third Floor Ceiling Plan 2156546/pdf/A110 - Third Floor Ceiling Plan2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "30c27bcb-5fee-89e1-2746-27e194682b2f", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A110 - Third Floor Ceiling Plan 2156546/pdf/A110 - Third Floor Ceiling Plan4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "771ced2c-7d77-5bc7-1de2-7feb95ef0990", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A110 - Third Floor Ceiling Plan 2156546/pdf/A110 - Third Floor Ceiling Plan.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "58aa1c03-0fd3-407b-92bd-8e9685841fd8", + "type": "resource", + "status": "success" + }, + { + "guid": "3385a608-b344-430b-a935-67ab9d0be801", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A110 - Third Floor Ceiling Plan", + "type": "view" + } + ] + }, + { + "guid": "836eb15c-b5d2-4a94-a74f-3d51032616d2-0020e825", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "A111 - Fourth Floor Ceiling Plan", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "836eb15c-b5d2-4a94-a74f-3d51032616d2-0020e825", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A111 - Fourth Floor Ceiling Plan 2156581/pdf/A111 - Fourth Floor Ceiling Plan1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "b3fd077e-64be-bf77-8990-dbda872bed2f", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A111 - Fourth Floor Ceiling Plan 2156581/pdf/A111 - Fourth Floor Ceiling Plan2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "3825c78a-7b71-1877-c379-fddfc5469ddd", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A111 - Fourth Floor Ceiling Plan 2156581/pdf/A111 - Fourth Floor Ceiling Plan4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "404e1d89-466c-88a6-adbb-66e47b47d602", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A111 - Fourth Floor Ceiling Plan 2156581/pdf/A111 - Fourth Floor Ceiling Plan.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "6c0c7c5c-7b62-4ef9-974a-9e8f84c26734", + "type": "resource", + "status": "success" + }, + { + "guid": "750a9d49-0b55-4a50-8854-800585fc48d8", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A111 - Fourth Floor Ceiling Plan", + "type": "view" + } + ] + }, + { + "guid": "836eb15c-b5d2-4a94-a74f-3d51032616d2-0020e848", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "A112 - Fifth Floor Ceiling Plan", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "836eb15c-b5d2-4a94-a74f-3d51032616d2-0020e848", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A112 - Fifth Floor Ceiling Plan 2156616/pdf/A112 - Fifth Floor Ceiling Plan1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "452376f2-2e6c-1b03-1112-3f161c223cce", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A112 - Fifth Floor Ceiling Plan 2156616/pdf/A112 - Fifth Floor Ceiling Plan2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "acf749e3-040d-3bf2-7f08-d5da0d90cbce", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A112 - Fifth Floor Ceiling Plan 2156616/pdf/A112 - Fifth Floor Ceiling Plan4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "efe42d24-f8ff-c23e-95f3-9161b6284ac1", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A112 - Fifth Floor Ceiling Plan 2156616/pdf/A112 - Fifth Floor Ceiling Plan.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "6ef991ef-2f83-4780-b2c4-1206f48b434d", + "type": "resource", + "status": "success" + }, + { + "guid": "adef3ed3-85ca-4fed-ac53-37784202c914", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A112 - Fifth Floor Ceiling Plan", + "type": "view" + } + ] + }, + { + "guid": "836eb15c-b5d2-4a94-a74f-3d51032616d2-0020e86b", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "G100 - Parking Deck Life Safety Plan", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "836eb15c-b5d2-4a94-a74f-3d51032616d2-0020e86b", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G100 - Parking Deck Life Safety Plan 2156651/pdf/G100 - Parking Deck Life Safety Plan1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "33cd58db-3e3d-a3ac-fa7d-fe247e768503", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G100 - Parking Deck Life Safety Plan 2156651/pdf/G100 - Parking Deck Life Safety Plan2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "f5d6da49-5986-5426-7487-3cd5dfb515d8", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G100 - Parking Deck Life Safety Plan 2156651/pdf/G100 - Parking Deck Life Safety Plan4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "5f3fa8f5-3487-78ed-a3d1-56c0137d01af", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G100 - Parking Deck Life Safety Plan 2156651/pdf/G100 - Parking Deck Life Safety Plan.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "230b8eda-e90a-4a19-8ec0-88ae1af02c96", + "type": "resource", + "status": "success" + }, + { + "guid": "3c9c81c2-0c2a-49d6-92cf-5ac4a160274d", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "G100 - Parking Deck Life Safety Plan", + "type": "view" + } + ] + }, + { + "guid": "836eb15c-b5d2-4a94-a74f-3d51032616d2-0020e8c2", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "G101 - First Floor Life Safety Plan", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "836eb15c-b5d2-4a94-a74f-3d51032616d2-0020e8c2", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G101 - First Floor Life Safety Plan 2156738/pdf/G101 - First Floor Life Safety Plan1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "4b2dab5d-f94a-78f3-484f-e8d142c19499", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G101 - First Floor Life Safety Plan 2156738/pdf/G101 - First Floor Life Safety Plan2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "ff8cdd68-3029-1d29-2afe-a9ff55f2c4ac", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G101 - First Floor Life Safety Plan 2156738/pdf/G101 - First Floor Life Safety Plan4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "bfc5a01e-fe9d-d326-8ea9-4d89183e3c73", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G101 - First Floor Life Safety Plan 2156738/pdf/G101 - First Floor Life Safety Plan.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "df184fb7-c8c1-4e45-b3ee-a816b15310fb", + "type": "resource", + "status": "success" + }, + { + "guid": "21df7b2f-d5b9-4231-8478-cdfc74a265a5", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "G101 - First Floor Life Safety Plan", + "type": "view" + } + ] + }, + { + "guid": "836eb15c-b5d2-4a94-a74f-3d51032616d2-0020e8e5", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "G103 - Third Floor Life Safety Plan", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "836eb15c-b5d2-4a94-a74f-3d51032616d2-0020e8e5", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G103 - Third Floor Life Safety Plan 2156773/pdf/G103 - Third Floor Life Safety Plan1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "0cd5de8e-1079-17cc-3148-35c685444bcd", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G103 - Third Floor Life Safety Plan 2156773/pdf/G103 - Third Floor Life Safety Plan2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "b08f294a-436a-2313-123d-9faf63797923", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G103 - Third Floor Life Safety Plan 2156773/pdf/G103 - Third Floor Life Safety Plan4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "989b78c7-4ecf-da05-be2e-446149c27bce", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G103 - Third Floor Life Safety Plan 2156773/pdf/G103 - Third Floor Life Safety Plan.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "63dcbfc9-cc6f-4684-9dec-df7f993cb7f9", + "type": "resource", + "status": "success" + }, + { + "guid": "8c50ccae-cc6b-4247-a730-86550ee8e41b", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "G103 - Third Floor Life Safety Plan", + "type": "view" + } + ] + }, + { + "guid": "836eb15c-b5d2-4a94-a74f-3d51032616d2-0020e92a", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "G104 - Fourth Floor Life Safety Plan", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "836eb15c-b5d2-4a94-a74f-3d51032616d2-0020e92a", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G104 - Fourth Floor Life Safety Plan 2156842/pdf/G104 - Fourth Floor Life Safety Plan1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "f3fe0ff6-5600-cbb3-fd09-e183f8d19115", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G104 - Fourth Floor Life Safety Plan 2156842/pdf/G104 - Fourth Floor Life Safety Plan2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "17ad6b9d-83cf-e8d2-48fb-e29a2ccb97c5", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G104 - Fourth Floor Life Safety Plan 2156842/pdf/G104 - Fourth Floor Life Safety Plan4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "a2ed2dd0-159c-60bf-b02a-d3795f5b0204", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G104 - Fourth Floor Life Safety Plan 2156842/pdf/G104 - Fourth Floor Life Safety Plan.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "ed6754c5-d138-46a5-bfbb-134beb554769", + "type": "resource", + "status": "success" + }, + { + "guid": "1a68ed7f-5a02-44c9-8e22-2ac49685efee", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "G104 - Fourth Floor Life Safety Plan", + "type": "view" + } + ] + }, + { + "guid": "836eb15c-b5d2-4a94-a74f-3d51032616d2-0020e94d", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "G105 - Fifth Floor Life Safety Plan", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "836eb15c-b5d2-4a94-a74f-3d51032616d2-0020e94d", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G105 - Fifth Floor Life Safety Plan 2156877/pdf/G105 - Fifth Floor Life Safety Plan1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "272ba518-2784-d9a9-aa1f-57938e50b11d", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G105 - Fifth Floor Life Safety Plan 2156877/pdf/G105 - Fifth Floor Life Safety Plan2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "bac1e28d-d5b0-3515-39e5-9feaf2af558e", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G105 - Fifth Floor Life Safety Plan 2156877/pdf/G105 - Fifth Floor Life Safety Plan4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "f3b0004e-c6de-7fe2-b26d-9f8a881ca928", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G105 - Fifth Floor Life Safety Plan 2156877/pdf/G105 - Fifth Floor Life Safety Plan.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "a5a96883-f6ec-4f85-b4b9-80a91f26a110", + "type": "resource", + "status": "success" + }, + { + "guid": "61c27d57-c545-44c6-a026-32b379a44da5", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "G105 - Fifth Floor Life Safety Plan", + "type": "view" + } + ] + }, + { + "guid": "836eb15c-b5d2-4a94-a74f-3d51032616d2-0020e970", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "G106 - Roof Plan Life Safety Plan", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "836eb15c-b5d2-4a94-a74f-3d51032616d2-0020e970", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G106 - Roof Plan Life Safety Plan 2156912/pdf/G106 - Roof Plan Life Safety Plan1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "6a655197-8506-5908-c9b4-a3c0aa2c1bdb", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G106 - Roof Plan Life Safety Plan 2156912/pdf/G106 - Roof Plan Life Safety Plan2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "a0f60210-06b5-bb65-3a1f-9dd797fa7ab3", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G106 - Roof Plan Life Safety Plan 2156912/pdf/G106 - Roof Plan Life Safety Plan4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "73522dd3-eafb-eb53-5f88-116420a8d7bd", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G106 - Roof Plan Life Safety Plan 2156912/pdf/G106 - Roof Plan Life Safety Plan.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "4a4c2ca3-b741-4b42-9828-264ad5409603", + "type": "resource", + "status": "success" + }, + { + "guid": "bbd09d61-0a3c-4cdb-98d1-f5584150c03e", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "G106 - Roof Plan Life Safety Plan", + "type": "view" + } + ] + }, + { + "guid": "836eb15c-b5d2-4a94-a74f-3d51032616d2-0020e9af", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "G102 - Second Floor Life Safety Plan", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "836eb15c-b5d2-4a94-a74f-3d51032616d2-0020e9af", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G102 - Second Floor Life Safety Plan 2156975/pdf/G102 - Second Floor Life Safety Plan1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "f304e6de-ada4-181d-0c0e-f98414ead6d7", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G102 - Second Floor Life Safety Plan 2156975/pdf/G102 - Second Floor Life Safety Plan2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "55786c11-e7d2-8cd4-9ec1-92c7359995c7", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G102 - Second Floor Life Safety Plan 2156975/pdf/G102 - Second Floor Life Safety Plan4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "f1080f59-a6a6-b91c-e196-c110625bd61f", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G102 - Second Floor Life Safety Plan 2156975/pdf/G102 - Second Floor Life Safety Plan.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "4db64892-68f2-4f5d-9a23-b7d9fb7a6da9", + "type": "resource", + "status": "success" + }, + { + "guid": "094820a1-534b-41c6-b187-dfa92a98513b", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "G102 - Second Floor Life Safety Plan", + "type": "view" + } + ] + }, + { + "guid": "79d5c389-014d-44a1-9488-8dd012ffaa5c-00226c50", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "A402 - Enlarged Live/Work Cores", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "79d5c389-014d-44a1-9488-8dd012ffaa5c-00226c50", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A402 - Enlarged Live_Work Cores 2255952/pdf/A402 - Enlarged Live_Work Cores1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "f71c4e9f-2efa-b296-2871-223ce5a16ffb", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A402 - Enlarged Live_Work Cores 2255952/pdf/A402 - Enlarged Live_Work Cores2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "24667115-8738-b913-c140-32d081033cfd", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A402 - Enlarged Live_Work Cores 2255952/pdf/A402 - Enlarged Live_Work Cores4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "029a2291-cf3a-a0d4-0800-3605cb4e8c73", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A402 - Enlarged Live_Work Cores 2255952/pdf/A402 - Enlarged Live_Work Cores.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "0c91ee07-2296-421e-8322-7955f152adac", + "type": "resource", + "status": "success" + }, + { + "guid": "33ce450c-5531-4c7e-882c-cc1b026ac95d", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A402 - Enlarged Live/Work Cores", + "type": "view" + } + ] + }, + { + "guid": "0a41a6b5-ee33-411d-9d8d-1d39b117f240-0022c939", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "A403 - Enlarged Live/Work Cores", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "0a41a6b5-ee33-411d-9d8d-1d39b117f240-0022c939", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A403 - Enlarged Live_Work Cores 2279737/pdf/A403 - Enlarged Live_Work Cores1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "b348d237-0f6b-b57d-5a13-491d58e8f1eb", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A403 - Enlarged Live_Work Cores 2279737/pdf/A403 - Enlarged Live_Work Cores2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "bc46cdbb-c6c2-5fb6-bded-f3539fa04de9", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A403 - Enlarged Live_Work Cores 2279737/pdf/A403 - Enlarged Live_Work Cores4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "23d31780-9620-1107-d60d-0195978b33ed", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A403 - Enlarged Live_Work Cores 2279737/pdf/A403 - Enlarged Live_Work Cores.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "febac6ca-70a0-4648-8c55-00af0682f537", + "type": "resource", + "status": "success" + }, + { + "guid": "645f831b-b644-4e4f-aa9c-d5c51bf91093", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A403 - Enlarged Live/Work Cores", + "type": "view" + } + ] + }, + { + "guid": "160c32d0-bcd0-407e-bcd1-aa0f87ca9cb3-0022d27d", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": [ + "New Construction", + "Legends" + ], + "ViewSets": "Arch - All Sheets", + "name": "A106 - Green Roof", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "160c32d0-bcd0-407e-bcd1-aa0f87ca9cb3-0022d27d", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A106 - Green Roof 2282109/pdf/A106 - Green Roof1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "01304faa-bd98-e5d1-3c54-7cec6a66e56c", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A106 - Green Roof 2282109/pdf/A106 - Green Roof2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "2ed4493b-9f36-253c-a0c3-4bf6278ecca7", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A106 - Green Roof 2282109/pdf/A106 - Green Roof4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "1a198d91-a791-55b9-b6e5-9bb307d94286", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A106 - Green Roof 2282109/pdf/A106 - Green Roof.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "5175b74a-9288-4073-9da4-1e15300f8edf", + "type": "resource", + "status": "success" + }, + { + "guid": "a19a2753-b170-41ed-957f-7847c9672fb4", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A106 - Green Roof", + "type": "view" + } + ] + }, + { + "guid": "166bc4cc-be47-45e3-95a0-c5dfe01877c0-00234eac", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "ViewSets": "Arch - All Sheets", + "name": "A502 - Partition Types", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "166bc4cc-be47-45e3-95a0-c5dfe01877c0-00234eac", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A502 - Partition Types 2313900/pdf/A502 - Partition Types1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "1c599451-c9eb-0184-a3cb-f264b324369e", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A502 - Partition Types 2313900/pdf/A502 - Partition Types2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "0530c49b-92d2-b771-fd74-fa9226802cab", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A502 - Partition Types 2313900/pdf/A502 - Partition Types4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "f2f4419f-04c3-5de2-0ad4-4264475d314e", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A502 - Partition Types 2313900/pdf/A502 - Partition Types.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "5e1ac92a-fa17-45a7-b608-cc6d4bb62085", + "type": "resource", + "status": "success" + }, + { + "guid": "f1f7ce2e-9443-49b6-85f1-96cda0e038b2", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A502 - Partition Types", + "type": "view" + } + ] + }, + { + "guid": "511e78b4-2b52-4e4c-b58e-6016a3532234-002392a0", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "A404 - Residential Lobby", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "511e78b4-2b52-4e4c-b58e-6016a3532234-002392a0", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A404 - Residential Lobby 2331296/pdf/A404 - Residential Lobby1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "006194e4-be1b-52a5-c141-7978be280eac", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A404 - Residential Lobby 2331296/pdf/A404 - Residential Lobby2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "1539615d-8b58-8f41-9d2f-47e0530178d7", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A404 - Residential Lobby 2331296/pdf/A404 - Residential Lobby4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "0605c32f-6d76-0fa5-c248-1dd8f98173ab", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A404 - Residential Lobby 2331296/pdf/A404 - Residential Lobby.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "9f8c2398-78b5-40f6-99b2-055fe30fb11a", + "type": "resource", + "status": "success" + }, + { + "guid": "ccc8cfa3-da79-409b-a603-ff64d7566ebe", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A404 - Residential Lobby", + "type": "view" + } + ] + }, + { + "guid": "a13d8a30-b7e6-46fa-a729-164b0b856849-0023a2b9", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "ViewSets": "Arch - All Sheets", + "name": "A602 - Schedules", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "a13d8a30-b7e6-46fa-a729-164b0b856849-0023a2b9", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A602 - Schedules 2335417/pdf/A602 - Schedules1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "54d0baa2-e18c-eb87-2e28-82da98195ca3", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A602 - Schedules 2335417/pdf/A602 - Schedules2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "f753a553-a009-2cf4-92ce-04ce4e1cf947", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A602 - Schedules 2335417/pdf/A602 - Schedules4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "4a35ad59-f70b-f0f6-5a09-ef9bdd351c3d", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A602 - Schedules 2335417/pdf/A602 - Schedules.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "a4fb8b8a-ae36-4b79-bd53-ad40de521168", + "type": "resource", + "status": "success" + }, + { + "guid": "f414c411-75b6-4f24-8cc1-17bb01e338ea", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A602 - Schedules", + "type": "view" + } + ] + }, + { + "guid": "6516f048-a290-4290-8aaa-d2bd6c5fec41-0023e26b", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "A200 - Existing Conditions Elevations", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "6516f048-a290-4290-8aaa-d2bd6c5fec41-0023e26b", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A200 - Existing Conditions Elevations 2351723/pdf/A200 - Existing Conditions Elevations1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "0aa103e9-efb1-a631-7346-2d637ae63605", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A200 - Existing Conditions Elevations 2351723/pdf/A200 - Existing Conditions Elevations2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "b30ab690-b338-cf69-3b82-249579efe5aa", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A200 - Existing Conditions Elevations 2351723/pdf/A200 - Existing Conditions Elevations4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "b5cb95d1-378c-8511-0706-4b8378fc4734", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A200 - Existing Conditions Elevations 2351723/pdf/A200 - Existing Conditions Elevations.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "e558dba6-ade9-4655-b284-51932f61f12a", + "type": "resource", + "status": "success" + }, + { + "guid": "a21b9c0d-583c-4c36-9e3b-f838e93a0c28", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A200 - Existing Conditions Elevations", + "type": "view" + } + ] + }, + { + "guid": "7e0a8f06-23a2-4e6d-960a-af2e68fbb44b-0023e29b", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "phaseNames": "New Construction", + "ViewSets": "Arch - All Sheets", + "name": "A904 - Solar Study", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "7e0a8f06-23a2-4e6d-960a-af2e68fbb44b-0023e29b", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A904 - Solar Study 2351771/pdf/A904 - Solar Study1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "961626e2-957f-fd1b-f2e7-81441a93b315", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A904 - Solar Study 2351771/pdf/A904 - Solar Study2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "185a264e-7240-4556-be0a-631907588551", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A904 - Solar Study 2351771/pdf/A904 - Solar Study4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "f7bfa272-15eb-159c-7c11-4d04e3f17ff0", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/A904 - Solar Study 2351771/pdf/A904 - Solar Study.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "5b37e604-2d1e-4ce1-a992-662578dad5f3", + "type": "resource", + "status": "success" + }, + { + "guid": "2fdfc66d-5486-4e01-a3b6-d4422995ba14", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "A904 - Solar Study", + "type": "view" + } + ] + }, + { + "guid": "53fc028f-104a-4646-b870-da4e9d031d6c-002459d3", + "hasThumbnail": "true", + "role": "2d", + "units": "inch", + "type": "geometry", + "isVectorPDF": true, + "ViewSets": "Arch - All Sheets", + "name": "G001 - Learn about this project", + "progress": "complete", + "properties": { + "Print Setting": { + "Layout": "Landscape", + "Paper size": "ISO A4, 210 x 297 mm" + } + }, + "viewableID": "53fc028f-104a-4646-b870-da4e9d031d6c-002459d3", + "status": "success", + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G001 - Learn about this project 2382291/pdf/G001 - Learn about this project1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "b52b6adf-73b1-3ed9-6f23-6bce0cb44b67", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G001 - Learn about this project 2382291/pdf/G001 - Learn about this project2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "035f4ccf-d377-861d-0da8-c2ceb48181c0", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G001 - Learn about this project 2382291/pdf/G001 - Learn about this project4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "180e6096-82ee-f5d3-5aa0-e913ebea778b", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/Sheet/G001 - Learn about this project 2382291/pdf/G001 - Learn about this project.pdf", + "role": "pdf-page", + "mime": "application/pdf", + "guid": "eb19f486-cdb9-4c12-bc4a-7485be49300c", + "type": "resource", + "status": "success" + }, + { + "guid": "6da3238a-2144-474c-b2a9-c4795ce31213", + "role": "2d", + "viewbox": [ + 0, + 0, + 42.00333333333333, + 30 + ], + "name": "G001 - Learn about this project", + "type": "view" + } + ] + } + ], + "name": "Snowdon_Towers_Sample_Architectural_24.rvt", + "progress": "complete", + "messages": [ + { + "type": "warning", + "code": "Revit-MissingLink", + "message": [ + "Missing link files:
    {0}
", + "Snowdon Towers Sample Electrical.rvt, Snowdon Towers Sample Facades.rvt, Snowdon Towers Sample HVAC.rvt, Snowdon Towers Sample Plumbing.rvt, Snowdon Towers Sample Site.rvt, Snowdon Towers Sample Structural.rvt" + ] + } + ], + "outputType": "svf", + "properties": { + "Document Information": { + "RVTVersion": "2024", + "Project Name": "Snowdon Towers", + "Project Number": "7765328-33-A", + "Author": "Autodesk", + "Project Address": "43 Market ST\r\nBrownsville, PA 15417", + "Project Issue Date": "6/1/2023", + "Project Status": "Construction Documents", + "Building Name": "Snowdon Towers", + "Client Name": "Autodesk", + "Organization Name": "Brownsville, PA", + "Organization Description": "" + } + }, + "status": "success" + }, + { + "children": [ + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/preview1.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "db899ab5-939f-e250-d79d-2d1637ce4565", + "type": "resource", + "resolution": [ + 100, + 100 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/preview2.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "3f6c118d-f551-7bf0-03c9-8548d26c9772", + "type": "resource", + "resolution": [ + 200, + 200 + ], + "status": "success" + }, + { + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/preview4.png", + "role": "thumbnail", + "mime": "image/png", + "guid": "4e751806-0920-ce32-e9fd-47c3cec21536", + "type": "resource", + "resolution": [ + 400, + 400 + ], + "status": "success" + } + ], + "progress": "complete", + "outputType": "thumbnail", + "status": "success" + } + ], + "hasThumbnail": "true", + "progress": "complete", + "type": "manifest", + "region": "EMEA", + "version": "1.0", + "status": "success" +} \ No newline at end of file diff --git a/md/assets/success_manifest.json b/md/assets/success_manifest.json new file mode 100644 index 0000000..012e731 --- /dev/null +++ b/md/assets/success_manifest.json @@ -0,0 +1,217 @@ +{ + "type": "manifest", + "hasThumbnail": "true", + "status": "success", + "progress": "complete", + "region": "US", + "urn": "dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA", + "derivatives": [ + { + "name": "A5.iam", + "hasThumbnail": "true", + "status": "success", + "progress": "complete", + "outputType": "svf", + "children": [ + { + "guid": "d998268f-eeb4-da87-0db4-c5dbbc4926d0", + "type": "geometry", + "role": "3d", + "name": "Scene", + "status": "success", + "progress": "complete", + "hasThumbnail": "true", + "children": [ + { + "guid": "4f981e94-8241-4eaf-b08b-cd337c6b8b1f", + "type": "resource", + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/1/A5.svf", + "role": "graphics", + "mime": "application/autodesk-svf" + }, + { + "guid": "d718eb7e-fa8a-42f9-8b32-e323c0fbea0c", + "type": "resource", + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/1/A5.svf.png01_thumb_400x400.png", + "resolution": [ + 400.0, + 400.0 + ], + "mime": "image/png", + "role": "thumbnail" + }, + { + "guid": "34dc340b-835f-47f7-9da5-b8219aefe741", + "type": "resource", + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/1/A5.svf.png01_thumb_200x200.png", + "resolution": [ + 200.0, + 200.0 + ], + "mime": "image/png", + "role": "thumbnail" + }, + { + "guid": "299c6ba6-650e-423e-bbd6-3aaff44ee104", + "type": "resource", + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/1/A5.svf.png01_thumb_100x100.png", + "resolution": [ + 100.0, + 100.0 + ], + "mime": "image/png", + "role": "thumbnail" + } + ] + }, + { + "guid": "b86dcf4d-dd4e-561a-1b52-50ee01f7af4f", + "hasThumbnail": "true", + "progress": "complete", + "role": "2d", + "status": "success", + "type": "geometry", + "children": [ + { + "guid": "cfe81eb4-fbc6-17c0-beba-3ab845d228f0", + "mime": "image/png", + "resolution": [ + 100, + 100 + ], + "role": "thumbnail", + "status": "success", + "type": "resource", + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/661c6096-056d-e58c-6c87-38769662932f_f2d/02___Floor1.png" + }, + { + "guid": "03c34714-36c7-b2bf-eb19-245f26c15e50", + "mime": "image/png", + "resolution": [ + 200, + 200 + ], + "role": "thumbnail", + "status": "success", + "type": "resource", + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/661c6096-056d-e58c-6c87-38769662932f_f2d/02___Floor2.png" + }, + { + "guid": "b680b9ec-5240-6858-b7ef-7e9adafd9d9a", + "mime": "image/png", + "resolution": [ + 400, + 400 + ], + "role": "thumbnail", + "status": "success", + "type": "resource", + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/661c6096-056d-e58c-6c87-38769662932f_f2d/02___Floor4.png" + }, + { + "guid": "a81433d1-e3e7-97f8-17f2-e85c1bbc1f66", + "mime": "application/autodesk-f2d", + "role": "graphics", + "status": "success", + "type": "resource", + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/661c6096-056d-e58c-6c87-38769662932f_f2d/primaryGraphics.f2d" + }, + { + "guid": "5d2d63c3-943e-4111-b0fe-75abfeb85cb8", + "name": "Floor Plan: 02 - Floor", + "role": "2d", + "type": "view", + "viewbox": [ + 0, + 0, + 279.4, + 215.9 + ] + } + ] + } + ] + }, + { + "status": "success", + "progress": "complete", + "outputType": "step", + "children": [ + { + "guid": "a6128518-dcf0-967b-31a1-3439a375daeb", + "role": "STEP", + "mime": "application/octet-stream", + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/1/A5.stp", + "status": "success", + "type": "resource" + } + ] + }, + { + "name": "A5.iam", + "hasThumbnail": "true", + "status": "success", + "progress": "complete", + "outputType": "thumbnail", + "children": [ + { + "guid": "63c50197-c285-411b-bcfd-b3f19b1d37ef", + "mime": "image/png", + "resolution": [ + 256, + 256 + ], + "role": "thumbnail", + "type": "resource", + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/256x256.png" + } + ] + }, + { + "status": "success", + "progress": "complete", + "outputType": "obj", + "children": [ + { + "guid": "1122e136-ea24-31ee-a7ef-ad065fafad42", + "type": "resource", + "role": "obj", + "modelGUID": "4f981e94-8241-4eaf-b08b-cd337c6b8b1f", + "objectIds": [ + 2, + 3, + 4 + ], + "status": "success", + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/geometry/bc3339b2-73cd-4fba-9cb3-15363703a354.obj" + }, + { + "guid": "29c1c0d4-7a35-350a-b3e5-fb221b054e29", + "type": "resource", + "role": "obj", + "modelGUID": "4f981e94-8241-4eaf-b08b-cd337c6b8b1f", + "objectIds": [ + 2, + 3, + 4 + ], + "status": "success", + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/geometry/bc3339b2-73cd-4fba-9cb3-15363703a354.mtl" + }, + { + "guid": "3e9752f1-5989-38b1-bff1-1f2d81841c8a", + "type": "resource", + "role": "obj", + "modelGUID": "4f981e94-8241-4eaf-b08b-cd337c6b8b1f", + "objectIds": [ + 2, + 3, + 4 + ], + "status": "success", + "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/geometry/bc3339b2-73cd-4fba-9cb3-15363703a354.zip" + } + ] + } + ] +} diff --git a/md/derivative.go b/md/derivative.go new file mode 100644 index 0000000..5831ad6 --- /dev/null +++ b/md/derivative.go @@ -0,0 +1,99 @@ +package md + +import ( + "encoding/json" + "errors" + "io" + "net/http" + "strconv" + "strings" +) + +type derivativeDownloadUrl struct { + Etag string `json:"etag"` + Size int `json:"size"` + Url string `json:"url"` + ContentType string `json:"content-type"` + Expiration int64 `json:"expiration"` +} + +func getDerivative(path, urn, derivativeUrn, token string, writer io.Writer) (written int64, err error) { + + task := http.Client{} + + req, err := http.NewRequest("GET", path+"/"+urn+"/manifest/"+derivativeUrn+"/signedcookies", nil) + if err != nil { + return 0, err + } + + req.Header.Set("Authorization", "Bearer "+token) + response, err := task.Do(req) + if err != nil { + return 0, err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + content, _ := io.ReadAll(response.Body) + err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) + return 0, err + } + + var getUrlResult derivativeDownloadUrl + + // deserialize the response + err = json.NewDecoder(response.Body).Decode(&getUrlResult) + if err != nil { + return 0, err + } + // https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/urn-manifest-derivativeUrn-signedcookies-GET/#http-headers + // Signed cookie to use with download URL. + // There will be three headers in the response named Set-Cookie + if len(response.Header.Values("Set-Cookie")) != 3 { + err = errors.New("invalid number of Set-Cookie headers in the response") + return 0, err + } + + signedCookieValue := strings.Join(response.Header.Values("Set-Cookie"), ";") + + return downloadDerivative(getUrlResult, signedCookieValue, writer) +} + +func downloadDerivative(downloadUrl derivativeDownloadUrl, cookieValue string, writer io.Writer) ( + written int64, err error, +) { + + task := http.Client{} + + req, err := http.NewRequest("GET", downloadUrl.Url, nil) + if err != nil { + return + } + + req.Header.Set("Cookie", cookieValue) + req.Header.Set("Content-Type", downloadUrl.ContentType) + req.Header.Set("Content-Length", strconv.Itoa(downloadUrl.Size)) + response, err := task.Do(req) + if err != nil { + return 0, err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + content, _ := io.ReadAll(response.Body) + err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) + return 0, err + } + + written, err = io.Copy(writer, response.Body) + if err != nil { + return 0, err + } + + if written != int64(downloadUrl.Size) { + err = errors.New("downloaded file size is different than the expected size") + return + } + + return written, nil +} diff --git a/md/doc.go b/md/doc.go index 6215d93..ce99be8 100644 --- a/md/doc.go +++ b/md/doc.go @@ -1,11 +1,14 @@ -// Package md contains the Go wrappers for calls to Model Derivative API -// https://developer.autodesk.com/en/docs/model-derivative/v2/overview/ -// -// The API offers the following features: -// -// - Translate designs into SVF format for rendering in the Forge Viewer. -// - Extract design metadata and integrate it into your app. Including object hierarchy trees, model views, and object properties, - such as materials, density and volume, etc. -// - Extract selected parts of a design and export the set of geometries in OBJ format. -// - Create different-sized thumbnails from design files. -// - Translate designs into different formats, such as STL and OBJ. +/* +Package md provides wrappers for the Model Derivative V2 REST API. +- https://aps.autodesk.com/model-derivative-api-2d-3d-conversions +- https://aps.autodesk.com/en/docs/model-derivative/v2/developers_guide/ + +The API offers the following features: +- Translate CAD file into viewables for rendering in the Viewer (svf/svf2 format). +- Export selected sets of geometries to OBJ format. +- Extract design metadata (properties in json or sqlite format, view IDs, object tree). + +To-Do: +- Implement all endpoints +*/ package md diff --git a/md/getters.go b/md/getters.go deleted file mode 100644 index e5864ff..0000000 --- a/md/getters.go +++ /dev/null @@ -1,156 +0,0 @@ -package md - -import ( - "encoding/json" - "errors" - "io/ioutil" - "net/http" - "strconv" -) - -func getDerivative(path string, urn, derivativeUrn, token string) (result []byte, err error) { - task := http.Client{} - - req, err := http.NewRequest("GET", - path+"/"+urn+"/manifest/"+derivativeUrn, - nil, - ) - - if err != nil { - return - } - - req.Header.Set("Authorization", "Bearer "+token) - response, err := task.Do(req) - if err != nil { - return - } - defer response.Body.Close() - - if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) - err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) - return - } - - result, err = ioutil.ReadAll(response.Body) - - return -} - -type Manifest struct { - Type string `json:"type"` - HasThumbnail string `json:"hasThumbnail"` - Status string `json:"status"` - Progress string `json:"progress"` - Region string `json:"region"` - URN string `json:"urn"` - Derivatives []Derivative `json:"derivatives"` -} - -type Derivative struct { - Name string `json:"name"` - HasThumbnail string `json:"hasThumbnail"` - Status string `json:"status"` - Progress string `json:"progress"` - Messages []struct { - Type string `json:"type"` - //Message string `json:"message"` - Code string `json:"code"` - } `json:"messages,omitempty"` - OutputType string `json:"outputType"` - Children []Child `json:"children"` -} - -//BUG: When translating a non-Revit model, the -// Manifest will contain an array of strings as message, -// while in case of others it is just a string - - -type Child struct { - GUID string `json:"guid"` - Type string `json:"type"` - Role string `json:"role"` - Name string `json:"name,omitempty"` - Status string `json:"status,omitempty"` - Progress string `json:"progress,omitempty"` - Mime string `json:"mime,omitempty"` - HasThumbnail string `json:"hasThumbnail,omitempty"` - URN string `json:"urn,omitempty"` - ViewableID string `json:"viewableID,omitempty"` - PhaseNames string `json:"phaseNames,omitempty"` - Resolution []float32 `json:"resolution,omitempty"` - Children []Child `json:"children,omitempty"` - Camera []float32 `json:"camera,omitempty"` - Messages []struct { - Type string `json:"type"` - Message []string `json:"message"` - Code string `json:"code"` - } `json:"messages,omitempty"` -} - -func getManifest(path string, urn, token string) (result Manifest, err error) { - task := http.Client{} - - req, err := http.NewRequest("GET", - path+"/"+urn+"/manifest", - nil, - ) - - if err != nil { - return - } - - req.Header.Set("Authorization", "Bearer "+token) - response, err := task.Do(req) - if err != nil { - return - } - defer response.Body.Close() - - if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) - err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) - return - } - - decoder := json.NewDecoder(response.Body) - err = decoder.Decode(&result) - - return -} - -/**************************** **********************/ - - -type LMVManifest struct { - Name string `json:"name"` - ToolkitVersion string `json:"toolkitversion"` - ADSKID struct { - SourceSystem string `json:"sourcesystem"` - Type string `json:"type"` - ID string `json:"id"` - Version string `json:"version"` - } `json:"adskID"` - Assets []Asset `json:"assets"` - Typesets []Typeset `json:"typesets"` -} - -type Asset struct { - ID string `json:"id"` - Type string `json:"type"` - URI string `json:"URI"` - Size uint64 `json:"size"` - USize uint64 `json:"usize"` -} - -type Typeset struct { - ID string `json:"id"` - Types []Type `json:"types"` -} - -type Type struct { - Class string `json:"class"` - Type string `json:"type"` - Version uint `json:"version"` -} \ No newline at end of file diff --git a/md/manifest.go b/md/manifest.go new file mode 100644 index 0000000..469b961 --- /dev/null +++ b/md/manifest.go @@ -0,0 +1,153 @@ +package md + +import ( + "encoding/json" + "errors" + "io" + "net/http" + "strconv" + "strings" + + "github.com/woweh/forge-api-go-client" +) + +// BUG: When translating a non-Revit model, the +// Manifest will contain an array of strings as message, +// while in case of others it is just a string + +// Status is the status of the translation +type Status string + +const ( + StatusPending Status = "pending" + StatusInProgress Status = "inprogress" + StatusSuccess Status = "success" + StatusFailed Status = "failed" + StatusTimeout Status = "timeout" +) + +// IsSuccess returns true if the status is success. +func (s *Status) IsSuccess() bool { + // case insensitive comparison! + return strings.EqualFold(string(*s), string(StatusSuccess)) +} + +// IsFailed returns true if the status is failed. +func (s *Status) IsFailed() bool { + // case insensitive comparison! + return strings.EqualFold(string(*s), string(StatusFailed)) +} + +// IsPending returns true if the status is pending. +func (s *Status) IsPending() bool { + // case insensitive comparison! + return strings.EqualFold(string(*s), string(StatusPending)) +} + +// IsInProgress returns true if the status is in progress. +func (s *Status) IsInProgress() bool { + // case insensitive comparison! + return strings.EqualFold(string(*s), string(StatusInProgress)) +} + +// IsTimeout returns true if the status is timeout. +func (s *Status) IsTimeout() bool { + // case insensitive comparison! + return strings.EqualFold(string(*s), string(StatusTimeout)) +} + +// IsEmpty returns true if this ProgressReport is empty. +func (pr *ProgressReport) IsEmpty() bool { + return pr.Status == "" && pr.Progress == "" +} + +type ProgressReport struct { + Status Status `json:"status"` + Progress string `json:"progress"` +} + +type Manifest struct { + ProgressReport + Type string `json:"type"` + HasThumbnail string `json:"hasThumbnail"` + Region forge.Region `json:"region"` + URN string `json:"urn"` + Version string `json:"version"` + Derivatives []Derivative `json:"derivatives"` +} + +type Derivative struct { + ProgressReport + Name string `json:"name"` + HasThumbnail string `json:"hasThumbnail"` + Messages []Message `json:"messages,omitempty"` + OutputType string `json:"outputType"` + Properties *Properties `json:"properties,omitempty"` + Children []Child `json:"children"` +} + +type Message struct { + Type string `json:"type"` + Code string `json:"code"` + // Message can either be a string, or an array of strings. + Message any `json:"message,omitempty"` +} + +type Properties struct { + DocumentInformation DocumentInformation `json:"Document Information"` +} + +type DocumentInformation struct { + NavisworksFileCreator string `json:"Navisworks File Creator"` + IFCApplicationName string `json:"IFC Application Name"` + IFCApplicationVersion string `json:"IFC Application Version"` + IFCSchema string `json:"IFC Schema"` + IFCLoader string `json:"IFC Loader"` +} + +type Child struct { + ProgressReport + GUID string `json:"guid"` + Type string `json:"type"` + Role string `json:"role"` + Name string `json:"name,omitempty"` + Mime string `json:"mime,omitempty"` + UseAsDefault *bool `json:"useAsDefault,omitempty"` + HasThumbnail string `json:"hasThumbnail,omitempty"` + URN string `json:"urn,omitempty"` + ViewableID string `json:"viewableID,omitempty"` + // PhaseNames can either be a string, or an array of strings. + PhaseNames any `json:"phaseNames,omitempty"` + Resolution []float32 `json:"resolution,omitempty"` + Children []Child `json:"children,omitempty"` + Camera []float32 `json:"camera,omitempty"` + ModelGUID *string `json:"modelGuid,omitempty"` + ObjectIDs []int `json:"objectIds,omitempty"` + Messages []Message `json:"messages,omitempty"` +} + +func getManifest(path, urn, token string) (result Manifest, err error) { + task := http.Client{} + + req, err := http.NewRequest("GET", path+"/"+urn+"/manifest", nil) + if err != nil { + return + } + + req.Header.Set("Authorization", "Bearer "+token) + response, err := task.Do(req) + if err != nil { + return + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + content, _ := io.ReadAll(response.Body) + err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) + return + } + + err = json.NewDecoder(response.Body).Decode(&result) + + return +} diff --git a/md/metadata.go b/md/metadata.go new file mode 100644 index 0000000..7f7cabc --- /dev/null +++ b/md/metadata.go @@ -0,0 +1,155 @@ +package md + +import ( + "encoding/json" + "errors" + "io" + "log" + "net/http" + "strconv" + "time" +) + +type MetaData struct { + Data struct { + Type string `json:"type,omitempty"` + Views []View `json:"metadata,omitempty"` + } `json:"data,omitempty"` +} + +type View struct { + Name string `json:"name,omitempty"` + Role ViewType `json:"role,omitempty"` + Guid string `json:"guid,omitempty"` + IsMasterView bool `json:"isMasterView,omitempty"` +} + +type ObjectTree struct { + Data struct { + Type string `json:"type"` + Objects []ObjectTreeNode `json:"objects"` + } `json:"data"` +} + +type ObjectTreeNode struct { + ObjectId int `json:"objectid"` + Name string `json:"name"` + Objects []ObjectTreeNode `json:"objects"` +} + +const ( + timeToWait = time.Duration(5) * time.Second + maxRetries = 12 * 5 // => 5 minutes max +) + +func getMetadata(path, urn, token string) (result MetaData, err error) { + task := http.Client{} + + req, err := http.NewRequest("GET", path+"/"+urn+"/metadata", nil) + if err != nil { + return + } + + req.Header.Set("Authorization", "Bearer "+token) + + response, err := task.Do(req) + if err != nil { + return + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + content, _ := io.ReadAll(response.Body) + err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) + return + } + + err = json.NewDecoder(response.Body).Decode(&result) + + return +} + +func getObjectTree(path, urn, modelGuid, token string, forceGet bool, xHeaders XAdsHeaders) ( + result ObjectTree, err error, +) { + // retry logic, not very elegant but it works + tries := 0 +retry: + tries++ + task := http.Client{} + + url := path + "/" + urn + "/metadata/" + modelGuid + "?forceget=" + strconv.FormatBool(forceGet) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return + } + + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Add("x-ads-force", strconv.FormatBool(xHeaders.Overwrite)) + req.Header.Add("x-ads-derivative-format", string(xHeaders.Format)) + + response, err := task.Do(req) + if err != nil { + return + } + defer response.Body.Close() + + if response.StatusCode == http.StatusAccepted { + // 202 Accepted => the request has been accepted for processing, but the processing has not been completed. + if tries < maxRetries { + log.Println("=> retry number: ", tries) + time.Sleep(timeToWait) + goto retry + } + } else if response.StatusCode != http.StatusOK { + content, _ := io.ReadAll(response.Body) + err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) + return + } + + err = json.NewDecoder(response.Body).Decode(&result) + + return +} + +func getModelViewProperties(path, urn, modelGuid, token string, xHeaders XAdsHeaders) ( + jsonData []byte, err error, +) { + + // retry logic, not very elegant but it works + tries := 0 +retry: + tries++ + task := http.Client{} + + req, err := http.NewRequest("GET", path+"/"+urn+"/metadata/"+modelGuid+"/properties", nil) + if err != nil { + return + } + + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Add("x-ads-force", strconv.FormatBool(xHeaders.Overwrite)) + req.Header.Add("x-ads-derivative-format", string(xHeaders.Format)) + + response, err := task.Do(req) + if err != nil { + return + } + defer response.Body.Close() + + if response.StatusCode == http.StatusAccepted { + // 202 Accepted => the request has been accepted for processing, but the processing has not been completed. + if tries < maxRetries { + log.Println("=> retry number: ", tries) + time.Sleep(timeToWait) + goto retry + } + } else if response.StatusCode != http.StatusOK { + content, _ := io.ReadAll(response.Body) + err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) + return + } + + return io.ReadAll(response.Body) +} diff --git a/md/test/advanced_spec_test.go b/md/test/advanced_spec_test.go new file mode 100644 index 0000000..1e170b9 --- /dev/null +++ b/md/test/advanced_spec_test.go @@ -0,0 +1,190 @@ +package md + +import ( + "encoding/json" + "testing" + + "github.com/woweh/forge-api-go-client/md" +) + +func Test_IfcAdvancedSpec_Json(t *testing.T) { + type args struct { + conversionMethod md.IfcConversionMethod + storeys md.IfcOption + spaces md.IfcOption + openings md.IfcOption + } + tests := []struct { + name string + args args + want string + }{ + { + name: "All params are filled in", + args: args{conversionMethod: md.IfcV3, storeys: md.IfcHide, spaces: md.IfcShow, openings: md.IfcSkip}, + want: "{\"conversionMethod\":\"v3\",\"buildingStoreys\":\"hide\",\"spaces\":\"show\",\"openingElements\":\"skip\"}", + }, + { + name: "IfcLegacy method - no additional parameters", + args: args{conversionMethod: md.IfcLegacy}, + want: "{\"conversionMethod\":\"legacy\"}", + }, + } + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + advancedSpec := md.IfcAdvancedSpec( + tt.args.conversionMethod, tt.args.storeys, tt.args.spaces, tt.args.openings, + ) + bytes, _ := json.Marshal(advancedSpec) + gotJson := string(bytes) + if gotJson != tt.want { + t.Errorf("IfcAdvancedSpec() json = %v, want %v", gotJson, tt.want) + } + }, + ) + } +} + +func Test_RevitAdvancedSpec_Json(t *testing.T) { + type args struct { + generateMasterViews bool + materialMode md.RvtMaterialMode + twoDViews md.Rvt2dViews + version md.RvtExtractorVersion + } + tests := []struct { + name string + args args + want string + }{ + { + name: "All params are filled in", + args: args{true, md.RvtAuto, md.RvtPdf, md.RvtNext}, + want: "{\"2dviews\":\"pdf\",\"extractorVersion\":\"next\",\"generateMasterViews\":true,\"materialMode\":\"auto\"}", + }, + { + name: "Only generateMasterViews", + args: args{false, "", "", ""}, + want: "{\"generateMasterViews\":false}", + }, + } + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + advancedSpec := md.RevitAdvancedSpec( + tt.args.generateMasterViews, tt.args.materialMode, tt.args.twoDViews, tt.args.version, + ) + bytes, _ := json.Marshal(advancedSpec) + gotJson := string(bytes) + if gotJson != tt.want { + t.Errorf("RevitAdvancedSpec() json = %v, want %v", gotJson, tt.want) + } + }, + ) + } +} + +func TestNavisworksAdvancedSpec(t *testing.T) { + type args struct { + hiddenObjects bool + basicMaterialProperties bool + autodeskMaterialProperties bool + timeLinerProperties bool + } + tests := []struct { + name string + args args + want string + }{ + { + name: "True False True False", + args: args{true, false, true, false}, + want: "{\"hiddenObjects\":true,\"basicMaterialProperties\":false,\"autodeskMaterialProperties\":true,\"timelinerProperties\":false}", + }, + { + name: "All True", + args: args{true, true, true, true}, + want: "{\"hiddenObjects\":true,\"basicMaterialProperties\":true,\"autodeskMaterialProperties\":true,\"timelinerProperties\":true}", + }, + { + name: "All False", + args: args{false, false, false, false}, + want: "{\"hiddenObjects\":false,\"basicMaterialProperties\":false,\"autodeskMaterialProperties\":false,\"timelinerProperties\":false}", + }, + } + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + advancedSpec := md.NavisworksAdvancedSpec( + tt.args.hiddenObjects, tt.args.basicMaterialProperties, tt.args.autodeskMaterialProperties, + tt.args.timeLinerProperties, + ) + bytes, _ := json.Marshal(advancedSpec) + gotJson := string(bytes) + if gotJson != tt.want { + t.Errorf("NavisworksAdvancedSpec() json = %v, want %v", gotJson, tt.want) + } + }, + ) + } +} + +func Test_ObjAdvancedSpec_Json(t *testing.T) { + type args struct { + exportFileStructure md.ObjExportFileStructure + unit md.ObjUnit + modelGuid string + objectIds []int + } + tests := []struct { + name string + args args + want string + }{ + { + name: "All params are filled in", + args: args{ + exportFileStructure: md.ObjSingle, unit: md.ObjMeter, modelGuid: "justSomeGuid", + objectIds: []int{1, 2, 3}, + }, + want: "{\"exportFileStructure\":\"single\",\"unit\":\"meter\",\"modelGuid\":\"justSomeGuid\",\"objectIds\":[1,2,3]}", + }, + { + name: "Minimum params are filled in", + args: args{exportFileStructure: "", unit: md.ObjNone, modelGuid: "justSomeGuid", objectIds: []int{1, 2, 3}}, + want: "{\"modelGuid\":\"justSomeGuid\",\"objectIds\":[1,2,3]}", + }, + } + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + advancedSpec := md.ObjAdvancedSpec( + tt.args.exportFileStructure, tt.args.unit, tt.args.modelGuid, &tt.args.objectIds, + ) + bytes, _ := json.Marshal(advancedSpec) + gotJson := string(bytes) + if gotJson != tt.want { + t.Errorf("ObjAdvancedSpec() json = %v, want %v", gotJson, tt.want) + } + }, + ) + } +} + +func Test_AdvancedSpec_IsEmpty(t *testing.T) { + + // if no AdvancedSpec is defined, it shouldn't be in the json + expectedJson := "{\"type\":\"svf\",\"views\":[\"3d\"]}" + + formatSpec := md.FormatSpec{ + Type: md.SVF, + Views: md.ViewType3D(), + } + bytes, _ := json.Marshal(formatSpec) + gotJson := string(bytes) + + if gotJson != expectedJson { + t.Errorf("formatSpec() json = %v, want %v", gotJson, expectedJson) + } +} diff --git a/md/test/md_test.go b/md/test/md_test.go index f6d7648..a77711a 100644 --- a/md/test/md_test.go +++ b/md/test/md_test.go @@ -1,929 +1,368 @@ package md_test +/* +package md_test provides "blackbox" tests for the md package. +These tests are meant to test the public API of the md package. +*/ + import ( - "bytes" - "encoding/base64" "encoding/json" - "github.com/apprentice3d/forge-api-go-client/dm" - "github.com/apprentice3d/forge-api-go-client/md" - "github.com/apprentice3d/forge-api-go-client/oauth" - "io/ioutil" + "io" + "log" "os" "testing" + "time" + + "github.com/woweh/forge-api-go-client" + "github.com/woweh/forge-api-go-client/dm" + "github.com/woweh/forge-api-go-client/md" + "github.com/woweh/forge-api-go-client/oauth" ) -func TestAPI_TranslateToSVF(t *testing.T) { - // prepare the credentials - clientID := os.Getenv("FORGE_CLIENT_ID") - clientSecret := os.Getenv("FORGE_CLIENT_SECRET") - authenticator := oauth.NewTwoLegged(clientID, clientSecret) - bucketAPI := dm.NewBucketAPI(authenticator) - mdAPI := md.NewMDAPI(authenticator) - - tempBucketName := "go_testing_md_bucket" - testFilePath := "../../assets/HelloWorld.rvt" - - var testObject dm.ObjectDetails - - t.Run("Create a temporary bucket", func(t *testing.T) { - _, err := bucketAPI.CreateBucket(tempBucketName, "transient") - - if err != nil { - t.Errorf("Failed to create a bucket: %s\n", err.Error()) - } - }) - - t.Run("Get bucket details", func(t *testing.T) { - _, err := bucketAPI.GetBucketDetails(tempBucketName) - - if err != nil { - t.Fatalf("Failed to get bucket details: %s\n", err.Error()) - } - }) - - t.Run("Upload an object into temp bucket", func(t *testing.T) { - file, err := os.Open(testFilePath) - if err != nil { - t.Fatal("Cannot open testfile for reading") - } - defer file.Close() - data, err := ioutil.ReadAll(file) - if err != nil { - t.Fatal("Cannot read the testfile") - } - - testObject, err = bucketAPI.UploadObject(tempBucketName, "temp_file.rvt", data) - - if err != nil { - t.Fatal("Could not upload the test object, got: ", err.Error()) - } - - if testObject.Size == 0 { - t.Fatal("The test object was uploaded but it is zero-sized") - } - }) - - t.Run("Translate object into SVF", func(t *testing.T) { - - result, err := mdAPI.TranslateToSVF(testObject.ObjectID) - - if err != nil { - t.Error("Could not translate the test object, got: ", err.Error()) - } - - if result.Result != "created" { - t.Error("The test object was uploaded, but failed to create the translation job") - } - }) - - t.Run("Delete the temporary bucket", func(t *testing.T) { - err := bucketAPI.DeleteBucket(tempBucketName) - - if err != nil { - t.Fatalf("Failed to delete bucket: %s\n", err.Error()) - } - }) -} +/* +NOTE: +- You can only run these tests when you have a valid client ID and secret. + => You probably want to run the tests locally, with your own credentials. +- A bucketKey (= bucket name) must be globally unique across all applications and regions +- Rules for bucketKey names: -_.a-z0-9 (between 3-128 characters in length) +- Buckets can only be deleted by the user who created them. + => You might want to change the bucketKey if the bucket already exists. +- A bucket name will not be immediately available for reuse after deletion. + => Best use a unique bucket name for each subtest. +*/ + +const ( + testFilePath = "../../dm/assets/rst_basic_sample_project.rvt" +) + +var ( + backoffSchedule = []time.Duration{ + 1 * time.Second, + 3 * time.Second, + 7 * time.Second, + 15 * time.Second, + 31 * time.Second, + } + usTestsFailed bool + emeaTestsFailed bool +) + +func TestMain(m *testing.M) { -func TestAPI_TranslateToSVF2_JSON_Creation(t *testing.T) { + log.Println("In TestMain()...") - params := md.TranslationSVFPreset - params.Input.URN = base64.RawStdEncoding.EncodeToString([]byte("just a test urn")) + usTestsFailed = false + emeaTestsFailed = false - output, err := json.Marshal(¶ms) - if err != nil { - t.Fatal("Could not marshal the preset into JSON: ", err.Error()) + log.Println("Running tests...") + exitCode := m.Run() + + log.Println("Tests finished, determining exit code") + log.Println("- usTestsFailed: ", usTestsFailed) + log.Println("- emeaTestsFailed: ", emeaTestsFailed) + if usTestsFailed || emeaTestsFailed { + exitCode = 1 + } else { + exitCode = 0 } - referenceExample := ` -{ - "input": { - "urn": "anVzdCBhIHRlc3QgdXJu" - }, - "output": { - "destination": { - "region": "us" - }, - "formats": [ - { - "type": "svf", - "views": [ - "2d", - "3d" - ] - } - ] - } - } -` - - var example md.TranslationParams - err = json.Unmarshal([]byte(referenceExample), &example) - if err != nil { - t.Fatal("Could not unmarshal the reference example: ", err.Error()) + log.Println("Exiting with code: ", exitCode) + os.Exit(exitCode) +} + +func TestModelDerivativeAPI_HappyPath_AllFunctions(t *testing.T) { + + type args struct { + region forge.Region + tempBucketName string + objectName string + error *bool } - expected, err := json.Marshal(example) - if err != nil { - t.Fatal("Could not marshal the reference example into JSON: ", err.Error()) + tests := []struct { + name string + args args + }{ + { + name: "Default-US", + args: args{ + region: forge.US, + tempBucketName: "forge_api_go_client_unit_testing_happy_path_default_us", + objectName: "rst_basic_sample_project_us.rvt", + error: &usTestsFailed, + }, + }, + { + name: "EMEA", + args: args{ + region: forge.EMEA, + tempBucketName: "forge_api_go_client_unit_testing_happy_path_default_emea", + objectName: "rst_basic_sample_project_emea.rvt", + error: &emeaTestsFailed, + }, + }, } - if bytes.Compare(expected, output) != 0 { - t.Fatalf("The translation params are not correct:\nexpected: %s\n created: %s", - string(expected), - string(output)) + // run the tests + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { - } + trueVal := true -} + // prepare the credentials + clientID := os.Getenv("FORGE_CLIENT_ID") + clientSecret := os.Getenv("FORGE_CLIENT_SECRET") + // check client ID and secret + if clientID == "" || clientSecret == "" { + t.Skip("Skipping tests because FORGE_CLIENT_ID and/or FORGE_CLIENT_SECRET env variables are not set") + } -func TestModelDerivativeAPI_GetManifest(t *testing.T) { - // prepare the credentials - clientID := os.Getenv("FORGE_CLIENT_ID") - clientSecret := os.Getenv("FORGE_CLIENT_SECRET") - authenticator := oauth.NewTwoLegged(clientID, clientSecret) - bucketAPI := dm.NewBucketAPI(authenticator) - mdAPI := md.NewMDAPI(authenticator) - - tempBucketName := "go_testing_md_bucket" - testFilePath := "../../assets/HelloWorld.rvt" - - var testObject dm.ObjectDetails - var translationResult md.TranslationResult - - t.Run("Create a temporary bucket", func(t *testing.T) { - _, err := bucketAPI.CreateBucket(tempBucketName, "transient") - - if err != nil { - t.Errorf("Failed to create a bucket: %s\n", err.Error()) - } - }) - - t.Run("Get bucket details", func(t *testing.T) { - _, err := bucketAPI.GetBucketDetails(tempBucketName) - - if err != nil { - t.Fatalf("Failed to get bucket details: %s\n", err.Error()) - } - }) - - t.Run("Upload an object into temp bucket", func(t *testing.T) { - file, err := os.Open(testFilePath) - if err != nil { - t.Fatal("Cannot open testfile for reading") - } - defer file.Close() - data, err := ioutil.ReadAll(file) - if err != nil { - t.Fatal("Cannot read the testfile") - } - - testObject, err = bucketAPI.UploadObject(tempBucketName, "temp_file.rvt", data) - - if err != nil { - t.Fatal("Could not upload the test object, got: ", err.Error()) - } - - if testObject.Size == 0 { - t.Fatal("The test object was uploaded but it is zero-sized") - } - }) - - t.Run("Translate object into SVF", func(t *testing.T) { - var err error - translationResult, err = mdAPI.TranslateToSVF(testObject.ObjectID) - - if err != nil { - t.Error("Could not translate the test object, got: ", err.Error()) - } - - if translationResult.Result != "created" { - t.Error("The test object was uploaded, but failed to create the translation job") - } - }) - - t.Run("Get manifest of the object", func(t *testing.T) { - manifest, err := mdAPI.GetManifest(translationResult.URN) - if err != nil { - t.Errorf("Problems getting the manifest for %s: %s", translationResult.URN, err.Error()) - } - - if manifest.Type != "manifest" { - t.Error("Expecting 'manifest' type, got ", manifest.Type) - } - - if manifest.URN != translationResult.URN { - t.Errorf("URN not matching: translation=%s\tmanifest=%s", translationResult.URN, manifest.URN) - } - - status := manifest.Status - if status != "failed" && status != "success" && status != "inprogress" && status != "pending" { - t.Errorf("Got unexpected status: %s", status) - } - - - if status == "success" && len(manifest.Derivatives) != 2 { - t.Errorf("Expecting to have 2 derivative, got %d", len(manifest.Derivatives)) - } - - outputType := manifest.Derivatives[0].OutputType - if status == "success" && outputType != "svf" { - t.Errorf("Expecting first derivative to be 'svf', got %s", outputType) - } - - }) - - t.Run("Delete the temporary bucket", func(t *testing.T) { - err := bucketAPI.DeleteBucket(tempBucketName) - - if err != nil { - t.Fatalf("Failed to delete bucket: %s\n", err.Error()) - } - }) -} + authenticator := oauth.NewTwoLegged(clientID, clientSecret) + _, err := authenticator.GetToken("bucket:create bucket:read data:read data:create data:write") + if err != nil { + // can't continue if we can't get a token + tt.args.error = &trueVal + t.Fatalf("Failed to get token: %s\n", err.Error()) + } + ossAPI := dm.NewOssApi(authenticator, tt.args.region) + mdAPI := md.NewMdApi(authenticator, tt.args.region) + t.Log("Checking if bucket already exists...") + bucketDetails, err := ossAPI.GetBucketDetails(tt.args.tempBucketName) + if err == nil { -func TestParseManifest(t *testing.T) { - t.Run("Parse pending manifest", func(t *testing.T) { - manifest := ` - { - "type": "manifest", - "hasThumbnail": "false", - "status": "pending", - "progress": "0% complete", - "region": "US", - "urn": "dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA", - "derivatives": [ - ] - } - ` - var decodedManifest md.Manifest - err := json.Unmarshal([]byte(manifest), &decodedManifest) - if err != nil { - t.Error(err.Error()) - } - - if len(decodedManifest.Derivatives) != 0 { - t.Error("There should not be derivatives") - } - - }) - - t.Run("Parse in progress manifest", func(t *testing.T) { - manifest := ` - { - "type": "manifest", - "hasThumbnail": "true", - "status": "inprogress", - "progress": "99% complete", - "region": "US", - "urn": "dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA", - "derivatives": [ - { - "name": "A5.iam", - "hasThumbnail": "true", - "status": "success", - "progress": "99% complete", - "outputType": "svf", - "children": [ - { - "guid": "d998268f-eeb4-da87-0db4-c5dbbc4926d0", - "type": "geometry", - "role": "3d", - "name": "Scene", - "status": "success", - "progress": "99% complete", - "hasThumbnail": "true", - "children": [ - { - "guid": "4f981e94-8241-4eaf-b08b-cd337c6b8b1f", - "type": "resource", - "progress": "99% complete", - "role": "graphics", - "mime": "application/autodesk-svf" - }, - { - "guid": "d718eb7e-fa8a-42f9-8b32-e323c0fbea0c", - "type": "resource", - "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/1/A5.svf.png01_thumb_400x400.png", - "resolution": [ - 400.0, - 400.0 - ], - "mime": "image/png", - "role": "thumbnail" - }, - { - "guid": "34dc340b-835f-47f7-9da5-b8219aefe741", - "type": "resource", - "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/1/A5.svf.png01_thumb_200x200.png", - "resolution": [ - 200.0, - 200.0 - ], - "mime": "image/png", - "role": "thumbnail" - }, - { - "guid": "299c6ba6-650e-423e-bbd6-3aaff44ee104", - "type": "resource", - "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/1/A5.svf.png01_thumb_100x100.png", - "resolution": [ - 100.0, - 100.0 - ], - "mime": "image/png", - "role": "thumbnail" - } - ] - } - ] - } - ] - } - ` - var decodedManifest md.Manifest - err := json.Unmarshal([]byte(manifest), &decodedManifest) - if err != nil { - t.Error(err.Error()) - } - - if len(decodedManifest.Derivatives) != 1 { - t.Error("Failed to parse derivatives") - } - - if len(decodedManifest.Derivatives[0].Children) != 1 { - t.Error("Failed to parse childern derivatives") - } - - if len(decodedManifest.Derivatives[0].Children[0].Children) != 4 { - t.Error("Failed to parse childern of derivative's children [funny]") - } - - if decodedManifest.Derivatives[0].Children[0].Children[0].URN != "" { - child := decodedManifest.Derivatives[0].Children[0].Children[0] - t.Errorf("URN should be empty: %s => %s", child.Name, child.URN) - } - - }) - - t.Run("Parse complete failed manifest", func(t *testing.T) { - manifest := ` - { - "type": "manifest", - "hasThumbnail": "false", - "status": "failed", - "progress": "complete", - "region": "US", - "urn": "dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA", - "derivatives": [ - { - "name": "A5.iam", - "hasThumbnail": "false", - "status": "failed", - "progress": "complete", - "messages": [ - { - "type": "warning", - "message": "The drawing's thumbnails were not properly created.", - "code": "TranslationWorker-ThumbnailGenerationFailed" + t.Log("Bucket exists, no need to create it...") + t.Log(bucketDetails) + + } else { + + t.Log("Bucket does not exist, try creating it...") + _, err = ossAPI.CreateBucket(tt.args.tempBucketName, dm.PolicyTransient) + if err != nil { + // can't continue if bucket creation fails + tt.args.error = &trueVal + t.Fatalf("Failed to create a bucket: %s\n", err.Error()) } - ], - "outputType": "svf", - "children": [ - { - "guid": "d998268f-eeb4-da87-0db4-c5dbbc4926d0", - "type": "geometry", - "role": "3d", - "name": "Scene", - "status": "success", - "messages": [ - { - "type": "warning", - "code": "ATF-1023", - "message": [ - "The file: {0} does not exist.", - "C:\\Users\\ADSK\\Documents\\A5\\Top.ipt" - ] - }, - { - "type": "warning", - "code": "ATF-1023", - "message": [ - "The file: {0} does not exist.", - "C:\\Users\\ADSK\\Documents\\A5\\Bottom.ipt" - ] - }, - { - "type": "error", - "code": "ATF-1026", - "message": [ - "The file: {0} is empty.", - "C:/worker/viewing-inventor-lmv/tmp/job-1/5/output/1/A5.svf" - ] - } - ], - "progress": "complete", - "hasThumbnail": "false", - "children": [ - { - "guid": "4f981e94-8241-4eaf-b08b-cd337c6b8b1f", - "type": "resource", - "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/1/A5.svf", - "role": "graphics", - "mime": "application/autodesk-svf" + + t.Log("Verify that bucket exists...") + for _, backoff := range backoffSchedule { + bucketDetails, err = ossAPI.GetBucketDetails(tt.args.tempBucketName) + if err != nil { + t.Logf("Failed to get bucket details: %s\n", err.Error()) + t.Log("Trying again...") + time.Sleep(backoff) + } else { + t.Log(bucketDetails) + break } - ] } - ] + if err != nil { + // can't continue if bucket creation failed + t.Log("Bucket does not exist, even after waiting for it to be created") + tt.args.error = &trueVal + t.Fatalf("Failed to get bucket details: %s\n", err.Error()) + } } - ] - } - ` - var decodedManifest md.Manifest - err := json.Unmarshal([]byte(manifest), &decodedManifest) - if err != nil { - t.Error(err.Error()) - } - - if len(decodedManifest.Derivatives) != 1 { - t.Error("Failed to parse derivatives") - } - - if len(decodedManifest.Derivatives[0].Children) != 1 { - t.Error("Failed to parse childern derivatives") - } - - if len(decodedManifest.Derivatives[0].Children[0].Children) != 1 { - t.Error("Failed to parse childern of derivative's children [funny]") - } - - if decodedManifest.Derivatives[0].Children[0].Children[0].URN == "" { - t.Error("URN should not be empty") - } - - if decodedManifest.Derivatives[0].Messages[0].Type != "warning" { - t.Error("Chould contain a warning message") - } - - if len(decodedManifest.Derivatives[0].Children[0].Messages) != 3 { - t.Error("Derivative child should contain 3 error message") - } - - if decodedManifest.Derivatives[0].Children[0].Messages[0].Type != "warning" { - t.Error("Derivative child message should be a warning message") - } - if decodedManifest.Derivatives[0].Children[0].Messages[2].Type != "error" { - t.Error("Derivative child message should be an error message") - } - - if len(decodedManifest.Derivatives[0].Children[0].Messages[2].Message) != 2 { - t.Error("Derivative child message should contain 2 message descriptions") - } - - if decodedManifest.Derivatives[0].Children[0].Children[0].Role != "graphics" { - t.Error("Failed to parse childern of derivative's children [funny]") - } - - }) - - t.Run("Parse complete success manifest", func(t *testing.T) { - manifest := ` - { - "type": "manifest", - "hasThumbnail": "true", - "status": "success", - "progress": "complete", - "region": "US", - "urn": "dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA", - "derivatives": [ - { - "name": "A5.iam", - "hasThumbnail": "true", - "status": "success", - "progress": "complete", - "outputType": "svf", - "children": [ - { - "guid": "d998268f-eeb4-da87-0db4-c5dbbc4926d0", - "type": "geometry", - "role": "3d", - "name": "Scene", - "status": "success", - "progress": "complete", - "hasThumbnail": "true", - "children": [ - { - "guid": "4f981e94-8241-4eaf-b08b-cd337c6b8b1f", - "type": "resource", - "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/1/A5.svf", - "role": "graphics", - "mime": "application/autodesk-svf" - }, - { - "guid": "d718eb7e-fa8a-42f9-8b32-e323c0fbea0c", - "type": "resource", - "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/1/A5.svf.png01_thumb_400x400.png", - "resolution": [ - 400.0, - 400.0 - ], - "mime": "image/png", - "role": "thumbnail" - }, - { - "guid": "34dc340b-835f-47f7-9da5-b8219aefe741", - "type": "resource", - "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/1/A5.svf.png01_thumb_200x200.png", - "resolution": [ - 200.0, - 200.0 - ], - "mime": "image/png", - "role": "thumbnail" - }, - { - "guid": "299c6ba6-650e-423e-bbd6-3aaff44ee104", - "type": "resource", - "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/1/A5.svf.png01_thumb_100x100.png", - "resolution": [ - 100.0, - 100.0 - ], - "mime": "image/png", - "role": "thumbnail" - } - ] - }, - { - "guid": "b86dcf4d-dd4e-561a-1b52-50ee01f7af4f", - "hasThumbnail": "true", - "progress": "complete", - "role": "2d", - "status": "success", - "type": "geometry", - "children": [ - { - "guid": "cfe81eb4-fbc6-17c0-beba-3ab845d228f0", - "mime": "image/png", - "resolution": [ - 100, - 100 - ], - "role": "thumbnail", - "status": "success", - "type": "resource", - "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/661c6096-056d-e58c-6c87-38769662932f_f2d/02___Floor1.png" - }, - { - "guid": "03c34714-36c7-b2bf-eb19-245f26c15e50", - "mime": "image/png", - "resolution": [ - 200, - 200 - ], - "role": "thumbnail", - "status": "success", - "type": "resource", - "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/661c6096-056d-e58c-6c87-38769662932f_f2d/02___Floor2.png" - }, - { - "guid": "b680b9ec-5240-6858-b7ef-7e9adafd9d9a", - "mime": "image/png", - "resolution": [ - 400, - 400 - ], - "role": "thumbnail", - "status": "success", - "type": "resource", - "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/661c6096-056d-e58c-6c87-38769662932f_f2d/02___Floor4.png" - }, - { - "guid": "a81433d1-e3e7-97f8-17f2-e85c1bbc1f66", - "mime": "application/autodesk-f2d", - "role": "graphics", - "status": "success", - "type": "resource", - "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/661c6096-056d-e58c-6c87-38769662932f_f2d/primaryGraphics.f2d" - }, - { - "guid": "5d2d63c3-943e-4111-b0fe-75abfeb85cb8", - "name": "Floor Plan: 02 - Floor", - "role": "2d", - "type": "view", - "viewbox": [ - 0, - 0, - 279.4, - 215.9 - ] + + t.Log("Checking if test file exists...") + file, err := os.Open(testFilePath) + if err != nil { + // can't continue if file cannot be opened/found + tt.args.error = &trueVal + t.Fatal("Cannot open test file for reading") + } + defer file.Close() + + t.Log("Uploading test object...") + uploadResult, err := ossAPI.UploadObject(tt.args.tempBucketName, tt.args.objectName, testFilePath) + if err != nil { + // can't continue if upload fails + tt.args.error = &trueVal + t.Fatal("Could not upload the test object, got: ", err.Error()) + } + if uploadResult.Size == 0 { + // can't continue if upload fails + tt.args.error = &trueVal + t.Fatal("The test object was uploaded but it is zero-sized") + } + t.Log("Uploaded object details: ", uploadResult) + + t.Log("Creating translation job...") + params := mdAPI.DefaultTranslationParams(md.UrnFromObjectId(uploadResult.ObjectId)) + translationJob, err := mdAPI.StartTranslation(params, md.DefaultXAdsHeaders()) + if err != nil { + // can't continue if translation job creation fails + tt.args.error = &trueVal + t.Fatal("Could not create the translation job, got: ", err.Error()) + } + t.Log("Translation job: ", translationJob) + if translationJob.Result != "created" && translationJob.Result != "success" { + // can't continue if translation job creation fails + tt.args.error = &trueVal + t.Fatal( + "The the translation job result is neither \"created\" nor \"success\": ", + translationJob.Result, + ) + } + + // make this a fixed value for now, to avoid golang test timeouts + timeToWait := time.Duration(5) * time.Second + + t.Log("Initial wait for the translation to get started...") + time.Sleep(timeToWait) + + var manifest md.Manifest + + seconds := 0 + timeout := float64(60 * 60) // 1 hour + startTime := time.Now() + errorCount := 0 + + loopUntilTranslationIsFinished: + for time.Since(startTime).Seconds() < timeout && manifest.Status != md.StatusSuccess { + seconds++ + + t.Log("Getting manifest...") + manifest, err = mdAPI.GetManifest(translationJob.URN) + if err != nil { + errorCount++ + if errorCount > 10 { + t.Errorf("Too many errors getting the manifest for %s: %s", translationJob.URN, err.Error()) + } else { + t.Logf("Problems getting the manifest for %s: %s", translationJob.URN, err.Error()) + t.Log("Waiting a bit and trying again...") + time.Sleep(timeToWait) + continue loopUntilTranslationIsFinished } - ] - } - ] - }, - { - "status": "success", - "progress": "complete", - "outputType": "step", - "children": [ - { - "guid": "a6128518-dcf0-967b-31a1-3439a375daeb", - "role": "STEP", - "mime": "application/octet-stream", - "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/1/A5.stp", - "status": "success", - "type": "resource" } - ] - }, - { - "name": "A5.iam", - "hasThumbnail": "true", - "status": "success", - "progress": "complete", - "outputType": "thumbnail", - "children": [ - { - "guid": "63c50197-c285-411b-bcfd-b3f19b1d37ef", - "mime": "image/png", - "resolution": [ - 256, - 256 - ], - "role": "thumbnail", - "type": "resource", - "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/256x256.png" - } - ] - }, - { - "status": "success", - "progress": "complete", - "outputType": "obj", - "children": [ - { - "guid": "1122e136-ea24-31ee-a7ef-ad065fafad42", - "type": "resource", - "role": "obj", - "modelGUID": "4f981e94-8241-4eaf-b08b-cd337c6b8b1f", - "objectIds": [ - 2, - 3, - 4 - ], - "status": "success", - "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/geometry/bc3339b2-73cd-4fba-9cb3-15363703a354.obj" - }, - { - "guid": "29c1c0d4-7a35-350a-b3e5-fb221b054e29", - "type": "resource", - "role": "obj", - "modelGUID": "4f981e94-8241-4eaf-b08b-cd337c6b8b1f", - "objectIds": [ - 2, - 3, - 4 - ], - "status": "success", - "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/geometry/bc3339b2-73cd-4fba-9cb3-15363703a354.mtl" - }, - { - "guid": "3e9752f1-5989-38b1-bff1-1f2d81841c8a", - "type": "resource", - "role": "obj", - "modelGUID": "4f981e94-8241-4eaf-b08b-cd337c6b8b1f", - "objectIds": [ - 2, - 3, - 4 - ], - "status": "success", - "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bW9kZWxkZXJpdmF0aXZlL0E1LnppcA/output/geometry/bc3339b2-73cd-4fba-9cb3-15363703a354.zip" + + switch manifest.Status { + case md.StatusPending: + t.Log("Translation pending...") + time.Sleep(timeToWait) + continue loopUntilTranslationIsFinished + + case md.StatusInProgress: + t.Logf("Translation in progress: %s", manifest.Progress) + time.Sleep(timeToWait) + + case md.StatusSuccess: + t.Log("Translation completed") + // break out of the loop + break loopUntilTranslationIsFinished + + case md.StatusFailed: + // can't continue if translation failed + tt.args.error = &trueVal + t.Fatal("Translation failed") + + case md.StatusTimeout: + // can't continue if translation timed out + tt.args.error = &trueVal + t.Fatal("Translation timed out") + + default: + t.Errorf("Got unexpected status: %s", manifest.Status) } - ] } - ] - } - ` - var decodedManifest md.Manifest - err := json.Unmarshal([]byte(manifest), &decodedManifest) - if err != nil { - t.Error(err.Error()) - } - - if len(decodedManifest.Derivatives) != 4 { - t.Error("Failed to parse derivatives") - } - - if len(decodedManifest.Derivatives[0].Children) == 1 { - t.Errorf("Failed to parse childern derivatives, expecting 1, got %d", - len(decodedManifest.Derivatives[0].Children)) - } - - if len(decodedManifest.Derivatives[0].Children[0].Children) != 4 { - t.Errorf("Failed to parse childern of derivative's children [funny], expecting 4, got %d", - len(decodedManifest.Derivatives[0].Children[0].Children)) - } - - if decodedManifest.Derivatives[0].Children[0].Children[0].URN == "" { - t.Error("URN should not be empty") - } - - if len(decodedManifest.Derivatives[0].Messages) != 0 { - t.Error("Derivative should not contain any error messages") - } - - expectedOutputTypes := []string{"svf", "step", "thumbnail", "obj"} - - for idx := range decodedManifest.Derivatives { - if decodedManifest.Derivatives[idx].OutputType != expectedOutputTypes[idx] { - t.Errorf("Wrong derivative type parsing: expectd %s, got %s", - decodedManifest.Derivatives[idx].OutputType, - expectedOutputTypes[idx], - ) - } - } - - }) - - t.Run("Parse Revit manifest", func(t *testing.T) { - manifestExample := ` -{ - "type": "manifest", - "hasThumbnail": "true", - "status": "success", - "progress": "complete", - "region": "US", - "urn": "dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6dGVzdC1maWxlcy8yMDE3MDcyNF9BaXJwb3J0JTIwTW9kZWwucnZ0", - "version": "1.0", - "derivatives": [ - { - "name": "20170724_Airport Model.rvt", - "hasThumbnail": "true", - "status": "success", - "progress": "complete", - "messages": [ - { - "type": "warning", - "code": "Revit-MissingLink", - "message": [ - "Missing link files:
    {0}
", - "S-FIDS-Wx-Video.jpg, solutions-airport-bcic2.jpg, zone.png" - ] - } - ], - "outputType": "svf", - "children": [ - { - "guid": "6fac95cb-af5d-3e4f-b943-8a7f55847ff1", - "type": "resource", - "role": "Autodesk.CloudPlatform.PropertyDatabase", - "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6dGVzdC1maWxlcy8yMDE3MDcyNF9BaXJwb3J0JTIwTW9kZWwucnZ0/output/Resource/model.sdb", - "mime": "application/autodesk-db", - "status": "success" - }, - { - "guid": "e7adaa0b-8274-132e-cdc1-65f55eb8b096", - "type": "geometry", - "role": "3d", - "name": "{3D}", - "viewableID": "a4646655-27fa-4fcc-b2cb-1c97f89f1e9b-00031929", - "phaseNames": "New Construction", - "status": "success", - "hasThumbnail": "true", - "progress": "complete", - "children": [ - { - "guid": "a4646655-27fa-4fcc-b2cb-1c97f89f1e9b-00031929", - "type": "view", - "role": "3d", - "name": "{3D}", - "status": "success", - "progress": "complete", - "camera": [ - 68.769485, - -500.972656, - 82.696663, - -117.377716, - 29.634874, - 27.679764, - -0.032235, - 0.091885, - 0.995248, - 4.974191, - 0, - 1, - 1 - ] - }, - { - "guid": "59a17c17-6249-81c1-5ef0-504a39b3e54f", - "type": "resource", - "role": "graphics", - "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6dGVzdC1maWxlcy8yMDE3MDcyNF9BaXJwb3J0JTIwTW9kZWwucnZ0/output/Resource/3D View/{3D} 203049/{3D}.svf", - "mime": "application/autodesk-svf" - }, - { - "guid": "daefa290-464d-9879-eefb-b60ee4549be1", - "type": "resource", - "role": "thumbnail", - "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6dGVzdC1maWxlcy8yMDE3MDcyNF9BaXJwb3J0JTIwTW9kZWwucnZ0/output/Resource/3D View/{3D} 203049/{3D}1.png", - "resolution": [ - 100, - 100 - ], - "mime": "image/png", - "status": "success" - }, - { - "guid": "7241ccc4-2ed1-b357-abd3-f9acac457769", - "type": "resource", - "role": "thumbnail", - "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6dGVzdC1maWxlcy8yMDE3MDcyNF9BaXJwb3J0JTIwTW9kZWwucnZ0/output/Resource/3D View/{3D} 203049/{3D}2.png", - "resolution": [ - 200, - 200 - ], - "mime": "image/png", - "status": "success" - }, - { - "guid": "71c50af6-78c8-3668-7aa6-62433efc5394", - "type": "resource", - "role": "thumbnail", - "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6dGVzdC1maWxlcy8yMDE3MDcyNF9BaXJwb3J0JTIwTW9kZWwucnZ0/output/Resource/3D View/{3D} 203049/{3D}4.png", - "resolution": [ - 400, - 400 - ], - "mime": "image/png", - "status": "success" - } - ] - } - ] - }, - { - "status": "success", - "progress": "complete", - "outputType": "thumbnail", - "children": [ - { - "guid": "db899ab5-939f-e250-d79d-2d1637ce4565", - "type": "resource", - "role": "thumbnail", - "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6dGVzdC1maWxlcy8yMDE3MDcyNF9BaXJwb3J0JTIwTW9kZWwucnZ0/output/preview1.png", - "resolution": [ - 100, - 100 - ], - "mime": "image/png", - "status": "success" - }, - { - "guid": "3f6c118d-f551-7bf0-03c9-8548d26c9772", - "type": "resource", - "role": "thumbnail", - "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6dGVzdC1maWxlcy8yMDE3MDcyNF9BaXJwb3J0JTIwTW9kZWwucnZ0/output/preview2.png", - "resolution": [ - 200, - 200 - ], - "mime": "image/png", - "status": "success" - }, - { - "guid": "4e751806-0920-ce32-e9fd-47c3cec21536", - "type": "resource", - "role": "thumbnail", - "urn": "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6dGVzdC1maWxlcy8yMDE3MDcyNF9BaXJwb3J0JTIwTW9kZWwucnZ0/output/preview4.png", - "resolution": [ - 400, - 400 - ], - "mime": "image/png", - "status": "success" - } - ] - } - ] -}` - - result := md.Manifest{} - - buffer := bytes.NewBufferString(manifestExample) - decoder := json.NewDecoder(buffer) - err := decoder.Decode(&result) - - if err != nil { - t.Fatal(err.Error()) - } - - }) -} + if manifest.Type != "manifest" { + t.Error("Expecting 'manifest' type, got ", manifest.Type) + } + + if manifest.URN != translationJob.URN { + // can't continue if URN doesn't match + tt.args.error = &trueVal + t.Fatalf("URN not matching: translation=%s\tmanifest=%s", translationJob.URN, manifest.URN) + } + + if len(manifest.Derivatives) != 2 { + t.Errorf("Expecting to have 2 derivative, got %d", len(manifest.Derivatives)) + } + + outputType := manifest.Derivatives[0].OutputType + if outputType != "svf" { + t.Errorf("Expecting first derivative to be 'svf', got %s", outputType) + } + + t.Log("Getting properties database URN...") + propertiesDatabaseUrn := manifest.GetPropertiesDatabaseUrn() + if propertiesDatabaseUrn == "" { + t.Error("Expecting a non-empty URN") + } + + t.Log("Downloading properties database...") + _, err = mdAPI.GetDerivative(manifest.URN, propertiesDatabaseUrn, io.Writer(nil)) + if err != nil { + t.Error("Failed to download the properties database, got: ", err.Error()) + } + + t.Log("Downloading metadata...") + metaData, err := mdAPI.GetMetadata(manifest.URN) + if err != nil { + // can't continue if metadata download fails + tt.args.error = &trueVal + t.Fatal("Failed to download the metadata, got: ", err.Error()) + } + + if metaData.Data.Type != "metadata" { + t.Error("Expecting 'metadata' result type, got ", metaData.Data.Type) + } + + masterViewGuid := metaData.GetMasterModelViewGuid() + if masterViewGuid == "" { + // can't continue if master view GUID is empty + tt.args.error = &trueVal + t.Fatal("Expecting a non-empty master view GUID") + } + t.Log("Downloading all properties for master view: ", masterViewGuid) + bytes, err := mdAPI.GetModelViewProperties(manifest.URN, masterViewGuid, md.DefaultXAdsHeaders()) + if err != nil { + t.Error("Failed to download the properties, got: ", err.Error()) + } + + if len(bytes) == 0 { + t.Error("Properties data (byte array) empty") + } + + // convert the bytes to JSON + jsonProperties, err := json.Marshal(string(bytes)) + if err != nil { + t.Error("Failed to convert the properties to JSON, got: ", err.Error()) + } + + if len(jsonProperties) == 0 { + t.Error("Properties data (JSON) empty") + } + + t.Log("Downloading object tree for master view: ", masterViewGuid) + tree, err := mdAPI.GetObjectTree(manifest.URN, masterViewGuid, true, md.DefaultXAdsHeaders()) + if err != nil { + t.Error("Failed to download the object tree, got: ", err.Error()) + } + + if tree.Data.Type != "objects" { + t.Error("Expecting 'objects' result type, got ", tree.Data.Type) + } + + if len(tree.Data.Objects) == 0 { + t.Error("Object tree is empty") + } + + t.Cleanup( + func() { + t.Log("Try to delete the temporary bucket...") + err = ossAPI.DeleteBucket(tt.args.tempBucketName) + if err != nil { + t.Logf("Failed to delete bucket: %s\n", err.Error()) + } + }, + ) + }, + ) + } +} diff --git a/md/test/parsing_test.go b/md/test/parsing_test.go new file mode 100644 index 0000000..29cc6bd --- /dev/null +++ b/md/test/parsing_test.go @@ -0,0 +1,414 @@ +package md_test + +/* +package md_test provides "blackbox" tests for the md package. +These tests are meant to test the public API of the md package. + +TODO: add tests to check region, ProgressReport and Status +*/ + +import ( + "bytes" + "encoding/json" + "os" + "testing" + + "github.com/woweh/forge-api-go-client" + "github.com/woweh/forge-api-go-client/md" +) + +func TestAPI_DefaultTranslationParams_JSON_Creation(t *testing.T) { + + mdApi := md.NewMdApi(nil, forge.US) + params := mdApi.DefaultTranslationParams("just a test urn") + + output, err := json.Marshal(¶ms) + if err != nil { + t.Fatal("Could not marshal the preset into JSON: ", err.Error()) + } + + referenceExample := ` +{ + "input": { + "urn": "just a test urn" + }, + "output": { + "destination": { + "region": "us" + }, + "formats": [ + { + "type": "svf", + "views": [ + "2d", + "3d" + ] + } + ] + } + } +` + + var example md.TranslationParams + err = json.Unmarshal([]byte(referenceExample), &example) + if err != nil { + t.Fatal("Could not unmarshal the reference example: ", err.Error()) + } + + expected, err := json.Marshal(example) + if err != nil { + t.Fatal("Could not marshal the reference example into JSON: ", err.Error()) + } + + if !bytes.Equal(expected, output) { + t.Fatalf( + "The translation params are not correct:\nexpected: %s\n created: %s", + string(expected), + string(output), + ) + + } +} + +func TestParseManifest(t *testing.T) { + t.Run( + "Parse pending manifest", func(t *testing.T) { + manifestJson, err := os.ReadFile("../assets/pending_manifest.json") + if err != nil { + t.Fatal(err.Error()) + } + + var manifest md.Manifest + err = json.Unmarshal(manifestJson, &manifest) + if err != nil { + t.Error(err.Error()) + } + + if !manifest.Status.IsPending() { + t.Error("Status should be pending") + } + + if manifest.Status.IsTimeout() { + t.Error("Status should not be timeout") + } + + if len(manifest.Derivatives) != 0 { + t.Error("There should not be derivatives") + } + + pr := manifest.GetProgressReportOfChild("abc", "def") + if !pr.IsEmpty() { + t.Error("Progress report should be empty") + } + }, + ) + + t.Run( + "Parse in progress manifest", func(t *testing.T) { + manifestJson, err := os.ReadFile("../assets/in_progress_manifest.json") + if err != nil { + t.Fatal(err.Error()) + } + + var manifest md.Manifest + err = json.Unmarshal(manifestJson, &manifest) + if err != nil { + t.Error(err.Error()) + } + + if !manifest.Status.IsInProgress() { + t.Error("Status should be in progress") + } + + if len(manifest.Derivatives) != 1 { + t.Error("Failed to parse derivatives") + } + + if len(manifest.Derivatives[0].Children) != 1 { + t.Error("Failed to parse children derivatives") + } + + if len(manifest.Derivatives[0].Children[0].Children) != 4 { + t.Error("Failed to parse children of derivative's children [funny]") + } + + if manifest.Derivatives[0].Children[0].Children[0].URN != "" { + child := manifest.Derivatives[0].Children[0].Children[0] + t.Errorf("URN should be empty: %s => %s", child.Name, child.URN) + } + }, + ) + + t.Run( + "Parse complete failed manifest", func(t *testing.T) { + manifestJson, err := os.ReadFile("../assets/failed_manifest.json") + if err != nil { + t.Fatal(err.Error()) + } + + var manifest md.Manifest + err = json.Unmarshal(manifestJson, &manifest) + if err != nil { + t.Error(err.Error()) + } + + if !manifest.Status.IsFailed() { + t.Error("Status should be failed") + } + + if !manifest.Region.IsUS() { + t.Error("region should be US") + } + + if manifest.Region.IsEMEA() { + t.Error("region should not be EMEA") + } + + if len(manifest.Derivatives) != 1 { + t.Error("Failed to parse derivatives") + } + + if len(manifest.Derivatives[0].Children) != 1 { + t.Error("Failed to parse children derivatives") + } + + if len(manifest.Derivatives[0].Children[0].Children) != 1 { + t.Error("Failed to parse children of derivative's children [funny]") + } + + if manifest.Derivatives[0].Children[0].Children[0].URN == "" { + t.Error("URN should not be empty") + } + + if manifest.Derivatives[0].Messages[0].Type != "warning" { + t.Error("Should contain a warning message") + } + + if len(manifest.Derivatives[0].Children[0].Messages) != 3 { + t.Error("Derivative child should contain 3 error message") + } + + if manifest.Derivatives[0].Children[0].Messages[0].Type != "warning" { + t.Error("Derivative child message should be a warning message") + } + if manifest.Derivatives[0].Children[0].Messages[2].Type != "error" { + t.Error("Derivative child message should be an error message") + } + + // use type assertion to check if the message is an array and assign it to a variable + if messages, ok := manifest.Derivatives[0].Children[0].Messages[2].Message.([]interface{}); !ok { + t.Error("Derivative child message should be an array") + } else { + // check if the message is an array of strings + if _, okay := messages[0].(string); !okay { + t.Error("Derivative child message should be an array of strings") + } + + if len(messages) != 2 { + t.Error("Derivative child message should contain 2 message descriptions") + } + } + + if manifest.Derivatives[0].Children[0].Children[0].Role != "graphics" { + t.Error("Failed to parse children of derivative's children [funny]") + } + }, + ) + + t.Run( + "Parse complete success manifest", func(t *testing.T) { + manifestJson, err := os.ReadFile("../assets/success_manifest.json") + if err != nil { + t.Fatal(err.Error()) + } + + var manifest md.Manifest + err = json.Unmarshal(manifestJson, &manifest) + if err != nil { + t.Error(err.Error()) + } + + if !manifest.Status.IsSuccess() { + t.Error("Status should be success") + } + + if !manifest.Region.IsUS() { + t.Error("region should be US") + } + + if manifest.Region.IsEMEA() { + t.Error("region should not be EMEA") + } + + if len(manifest.Derivatives) != 4 { + t.Error("Failed to parse derivatives") + } + + if len(manifest.Derivatives[0].Children) == 1 { + t.Errorf( + "Failed to parse childern derivatives, expecting 1, got %d", + len(manifest.Derivatives[0].Children), + ) + } + + if len(manifest.Derivatives[0].Children[0].Children) != 4 { + t.Errorf( + "Failed to parse childern of derivative's children [funny], expecting 4, got %d", + len(manifest.Derivatives[0].Children[0].Children), + ) + } + + if manifest.Derivatives[0].Children[0].Children[0].URN == "" { + t.Error("URN should not be empty") + } + + if len(manifest.Derivatives[0].Messages) != 0 { + t.Error("Derivative should not contain any error messages") + } + + expectedOutputTypes := []string{"svf", "step", "thumbnail", "obj"} + + for idx := range manifest.Derivatives { + if manifest.Derivatives[idx].OutputType != expectedOutputTypes[idx] { + t.Errorf( + "Wrong derivative type parsing: expectd %s, got %s", + manifest.Derivatives[idx].OutputType, + expectedOutputTypes[idx], + ) + } + } + + objPr := manifest.GetProgressReportOfChild("obj", "4f981e94-8241-4eaf-b08b-cd337c6b8b1f") + if objPr.Status != md.StatusSuccess { + t.Error("Wrong status") + } + if objPr.Progress != "" { + t.Error("Wrong progress") + } + }, + ) + + t.Run( + "Parse Revit manifest", func(t *testing.T) { + manifestJson, err := os.ReadFile("../assets/revit_manifest.json") + if err != nil { + t.Fatal(err.Error()) + } + + manifest := md.Manifest{} + + buffer := bytes.NewBuffer(manifestJson) + decoder := json.NewDecoder(buffer) + err = decoder.Decode(&manifest) + if err != nil { + t.Fatal(err.Error()) + } + + revitFileName := manifest.GetSourceFileName() + if revitFileName != "20170724_Airport Model.rvt" { + t.Error("Wrong source file name") + } + + sp := manifest.GetProgressReport() + if sp.Status != md.StatusSuccess { + t.Error("Wrong status") + } + if sp.Progress != "complete" { + t.Error("Wrong progress") + } + + propDbUrn := manifest.GetPropertiesDatabaseUrn() + if propDbUrn != "urn:adsk.viewing:fs.file:dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6dGVzdC1maWxlcy8yMDE3MDcyNF9BaXJwb3J0JTIwTW9kZWwucnZ0/output/Resource/model.sdb" { + t.Error("Wrong properties database urn") + } + }, + ) + + t.Run( + "Parse Revit manifest with multiple phases", func(t *testing.T) { + manifestJson, err := os.ReadFile("../assets/revit_manifest_multiple_phases.json") + if err != nil { + t.Fatal(err.Error()) + } + + manifest := md.Manifest{} + + buffer := bytes.NewBuffer(manifestJson) + decoder := json.NewDecoder(buffer) + err = decoder.Decode(&manifest) + if err != nil { + t.Fatal(err.Error()) + } + + revitFileName := manifest.GetSourceFileName() + if revitFileName != "Snowdon_Towers_Sample_Architectural_24.rvt" { + t.Error("Wrong source file name") + } + + sp := manifest.GetProgressReport() + if sp.Status != md.StatusSuccess { + t.Error("Wrong status") + } + if sp.Progress != "complete" { + t.Error("Wrong progress") + } + + propDbUrn := manifest.GetPropertiesDatabaseUrn() + if propDbUrn != "urn:adsk.viewing:fs.file:dXJuOmFkc2sud2lwZW1lYTpmcy5maWxlOnZmLllWbTRTWnpMUXlpdnFzQUhRXzlSbFE_dmVyc2lvbj0x/output/Resource/model.sdb" { + t.Error("Wrong properties database urn") + } + + // check if the manifest contains correct phase names + for _, derivative := range manifest.Derivatives { + childrenHaveValidPhaseNames(t, derivative.Children) + } + }, + ) +} + +func childrenHaveValidPhaseNames(t *testing.T, children []md.Child) { + for _, child := range children { + if !hasValidPhaseNames(child) { + t.Errorf("Child %s (%s, %s) has invalid phase names", child.Name, child.Role, child.Type) + } + + if len(child.Children) > 0 { + childrenHaveValidPhaseNames(t, child.Children) + } + } +} + +func hasValidPhaseNames(c md.Child) bool { + + // PhaseNames can be nil, a string or an array of strings + + if c.PhaseNames == nil { + return true + } + + // check if phaseName is a string + if phaseName, ok := c.PhaseNames.(string); ok { + return isValidPhaseName(phaseName) + } + + // check if phaseName is an array of strings + if phaseNames, ok := c.PhaseNames.([]any); ok { + for _, phaseName := range phaseNames { + if name, o := phaseName.(string); o { + if !isValidPhaseName(name) { + return false + } + } + } + } + + return true +} + +func isValidPhaseName(s string) bool { + // phaseNames in "../assets/revit_manifest_multiple_phases.json" + // - Legends + // - New Construction + // - NewMdApi Construction + return s == "New Construction" || s == "NewMdApi Construction" || s == "Legends" +} diff --git a/md/translation.go b/md/translation.go index 5cfcd69..11c9b5b 100644 --- a/md/translation.go +++ b/md/translation.go @@ -1,29 +1,46 @@ package md import ( - "log" - "net/http" "bytes" - "io/ioutil" - "errors" "encoding/json" + "errors" + "io" + "log" + "net/http" "strconv" + + "github.com/woweh/forge-api-go-client" ) -//TranslationParams is used when specifying the translation jobs +// TranslationParams are used when specifying translation jobs. +// See: https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/jobs/job-POST/#body-structure type TranslationParams struct { - Input struct { - URN string `json:"urn"` - CompressedURN *bool `json:"compressedUrn,omitempty"` - RootFileName *string `json:"rootFileName,omitempty"` - } `json:"input"` - Output OutputSpec `json:"output"` + Input InputSpec `json:"input"` // InputSpec is used when specifying the source design + Output OutputSpec `json:"output"` // OutputSpec is used when specifying the expected format and views (2d and/or 3d) + // TODO: Add misc option } -// TranslationResult reflects data received upon successful creation of translation job -type TranslationResult struct { - Result string `json:"result"` - URN string `json:"urn"` +type InputSpec struct { + // The URN of the source design. This is typically returned as `ObjectId` when you upload the source design to APS. + // The URN needs to be Base64 (URL Safe) encoded. + URN string `json:"urn"` + // Set this to `true` if the source design is compressed as a zip file. + // The design can consist of a single file or as in the case of Autodesk Inventor, multiple files. + // If set to `true`, you must specify the rootFilename attribute. + CompressedURN *bool `json:"compressedUrn,omitempty"` + // The name of the top-level design file in the compressed file. + // Mandatory if the compressedUrn is set to true. + RootFileName *string `json:"rootFileName,omitempty"` + // - true - Instructs the server to check for references and download all referenced files. + // If the design consists of multiple files (as with Autodesk Inventor assemblies) the translation fails if this attribute is not set to true. + // - false - (Default) Does not check for any references. + CheckReferences *bool `json:"checkReferences,omitempty"` +} + +// TranslationJob reflects data received upon successful creation of translation job +type TranslationJob struct { + Result string `json:"result"` + URN string `json:"urn"` AcceptedJobs struct { Output OutputSpec `json:"output"` } @@ -37,17 +54,128 @@ type OutputSpec struct { // DestSpec is used within OutputSpecs and is useful when specifying the region for translation results type DestSpec struct { - Region string `json:"region"` + Region forge.Region `json:"region"` // Region in which to store outputs. Possible values: US, EMEA. By default, it is set to US. +} + +// OutputType is the requested output type. +// For a list of supported types, call the [GET formats endpoint]. +// Note that Advanced Options are not supported for all output types. +// Make sure you specify the correct options for the requested output type. +// The API has only been tested with the following output types: svf, svf2 and obj +// +// [GET formats endpoint]: https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/informational/formats-GET/ +type OutputType string + +const ( + DWG OutputType = "dwg" + FBX OutputType = "fbx" + IFC OutputType = "ifc" + IGES OutputType = "iges" + OBJ OutputType = "obj" + STEP OutputType = "step" + STL OutputType = "stl" + SVF OutputType = "svf" + SVF2 OutputType = "svf2" + Thumbnail OutputType = "thumbnail" +) + +// ViewType - possible values: 2d, 3d +// Note that some output types have only one possible view. +// Make sure you specify the correct view for the requested output type. +type ViewType string + +const ( + View2D ViewType = "2d" + View3D ViewType = "3d" +) + +// ViewTypes2DAnd3D returns a slice of ViewTypes containing View2D and View3D. +func ViewTypes2DAnd3D() []ViewType { + return []ViewType{View2D, View3D} +} + +// ViewType2D returns a slice of ViewTypes containing only View2D. +func ViewType2D() []ViewType { + return []ViewType{View2D} +} + +// ViewType3D returns a slice of ViewTypes containing only View3D. +func ViewType3D() []ViewType { + return []ViewType{View3D} } // FormatSpec is used within OutputSpecs and should be used when specifying the expected format and views (2d or/and 3d) type FormatSpec struct { - Type string `json:"type"` - Views []string `json:"views"` + Type OutputType `json:"type"` // The requested output types. + Views []ViewType `json:"views"` // An Array of the requested views. + Advanced *AdvancedSpec `json:"advanced,omitempty"` // A set of special options, which you must specify only if the input file type is IFC, Revit, or Navisworks. } +// AdvancedSpec is a set of extra translation options. +// - You *can* specify them if the input file type is IFC, Revit, or Navisworks and the output is SVF/SVF2. +// - You *must* specify them if the output is OBJ. +type AdvancedSpec struct { + // ConversionMethod specifies what _IFC_ loader to use during translation (_IFC_ => SVF/SVF2). + ConversionMethod IfcConversionMethod `json:"conversionMethod,omitempty"` + + // BuildingStoreys specifies how storeys are translated (_IFC_ => SVF/SVF2). + // NOTE: These options are applicable **only** when conversionMethod is set to modern or v3. + BuildingStoreys IfcOption `json:"buildingStoreys,omitempty"` + + // Spaces specifies how spaces are translated (_IFC_ => SVF/SVF2). + // NOTE: These options are applicable **only** when conversionMethod is set to modern or v3. + Spaces IfcOption `json:"spaces,omitempty"` + + // OpeningElements specifies how openings are translated (_IFC_ => SVF/SVF2). + // NOTE: These options are applicable **only** when conversionMethod is set to modern or v3. + OpeningElements IfcOption `json:"openingElements,omitempty"` + + // TwoDViews specifies the format that 2D views must be rendered to (_Revit_ => SVF/SVF2). + TwoDViews Rvt2dViews `json:"2dviews,omitempty"` + + // ExtractorVersion specifies what version of the Revit translator/extractor to use (_Revit_ => SVF/SVF2). + ExtractorVersion RvtExtractorVersion `json:"extractorVersion,omitempty"` + + // GenerateMasterViews specifies if master views shall be created (_Revit_ => SVF/SVF2). + // This attribute defaults to false. + GenerateMasterViews *bool `json:"generateMasterViews,omitempty"` + + // MaterialMode specifies the materials to apply to the generated SVF/SVF2 derivatives (_Revit_ => SVF/SVF2). + MaterialMode RvtMaterialMode `json:"materialMode,omitempty"` + + // HiddenObjects specifies whether hidden objects must be extracted or not (_Navisworks_ => SVF/SVF2). + HiddenObjects *bool `json:"hiddenObjects,omitempty"` + + // BasicMaterialProperties specifies whether basic material properties must be extracted or not (_Navisworks_ => SVF/SVF2). + BasicMaterialProperties *bool `json:"basicMaterialProperties,omitempty"` -func translate(path string, params TranslationParams, token string) (result TranslationResult, err error) { + // AutodeskMaterialProperties specifies how to handle Autodesk material properties (_Navisworks_ => SVF/SVF2). + AutodeskMaterialProperties *bool `json:"autodeskMaterialProperties,omitempty"` + + // TimeLinerProperties specifies whether timeliner properties must be extracted or not (_Navisworks_ => SVF/SVF2). + TimeLinerProperties *bool `json:"timelinerProperties,omitempty"` + + // ExportFileStructure specifies if a single or multiple OBJ files shall be generated (SVF/SVF2 => _OBJ_). + ExportFileStructure ObjExportFileStructure `json:"exportFileStructure,omitempty"` + + /* Unit specifies the unit for translating models (SVF/SVF2 => _OBJ_). + This causes the values to change. For example, from millimeters (10, 123, 31) to centimeters (1.0, 12.3, 3.1). + If the source unit or the unit you are translating into is not supported, the values remain unchanged. */ + Unit ObjUnit `json:"unit,omitempty"` + + // ModelGuid specifies the model view ID (guid) required for geometry extraction (SVF/SVF2 => _OBJ_). + // Currently, only valid for 3d views. + ModelGuid string `json:"modelGuid,omitempty"` + + // ObjectIds are required for geometry extraction (SVF/SVF2 => _OBJ_). List object ids to be translated. + // NOTE: -1 will extract the entire model. Currently, only valid for 3d views. + ObjectIds *[]int `json:"objectIds,omitempty"` +} + +// startTranslation triggers a translation job with the given TranslationParams and xAdsHeaders.XAdsHeaders. +func startTranslation(path string, params TranslationParams, xAdsHeaders *XAdsHeaders, token string) ( + result TranslationJob, err error, +) { byteParams, err := json.Marshal(params) if err != nil { @@ -55,16 +183,17 @@ func translate(path string, params TranslationParams, token string) (result Tran return } - req, err := http.NewRequest("POST", - path+"/job", - bytes.NewBuffer(byteParams)) - + req, err := http.NewRequest("POST", path+"/job", bytes.NewBuffer(byteParams)) if err != nil { return } req.Header.Add("Content-Type", "application/json") req.Header.Add("Authorization", "Bearer "+token) + if xAdsHeaders != nil { + req.Header.Add("x-ads-derivative-format", string(xAdsHeaders.Format)) + req.Header.Add("x-ads-force", strconv.FormatBool(xAdsHeaders.Overwrite)) + } response, err := http.DefaultClient.Do(req) if err != nil { @@ -73,7 +202,7 @@ func translate(path string, params TranslationParams, token string) (result Tran defer response.Body.Close() if response.StatusCode != http.StatusCreated && response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } diff --git a/md/x_ads_headers.go b/md/x_ads_headers.go new file mode 100644 index 0000000..9048841 --- /dev/null +++ b/md/x_ads_headers.go @@ -0,0 +1,44 @@ +package md + +// XAdsHeaders are used when specifying translation jobs. +type XAdsHeaders struct { + // Format (x-ads-derivative-format) header: + // - "latest" (Default) or + // - "fallback" + // Specifies how to interpret the formats.advanced.objectIds request body parameter for OBJ output. + // If you use this header with one derivative (URN), you must use it consistently across the following endpoints, whenever you reference the same derivative. + // - POST job (for OBJ output) + // - GET {urn}/metadata/{guid} + // - GET {urn}/metadata/{guid}/properties + Format DerivativeFormat + // Overwrite (x-ads-force) header: false (default) or true + Overwrite bool +} + +// NewXAdsHeaders gets XAdsHeaders with the given values. +// - format => x-ads-derivative-format header: +// Possible values are: "latest" or "fallback" +// - overwrite => x-ads-force header; +// Possible values are: false or true +func NewXAdsHeaders(format DerivativeFormat, overwrite bool) XAdsHeaders { + return XAdsHeaders{ + Format: format, + Overwrite: overwrite, + } +} + +// DefaultXAdsHeaders gets XAdsHeaders with default values (Format: Latest, Overwrite: false). +func DefaultXAdsHeaders() XAdsHeaders { + return XAdsHeaders{ + Format: Latest, + Overwrite: false, + } +} + +// DerivativeFormat indicates the value for the xAdsHeaders.Format +type DerivativeFormat string + +const ( + Latest DerivativeFormat = "latest" // (Default) Consider formats.advanced.objectIds to be SVF2 Object IDs. + FallBack DerivativeFormat = "fallback" // Consider formats.advanced.objectIds to be SVF Object IDs. +) diff --git a/oauth/basic_authorization.go b/oauth/basic_authorization.go new file mode 100644 index 0000000..ad2e189 --- /dev/null +++ b/oauth/basic_authorization.go @@ -0,0 +1,12 @@ +package oauth + +import ( + "encoding/base64" + "net/http" +) + +// setBasicAuthHeader sets the Basic Authorization header for a given request +func setBasicAuthHeader(r *http.Request, a AuthData) { + base64encodedClientIdAndSecret := base64.StdEncoding.EncodeToString([]byte(a.ClientID + ":" + a.ClientSecret)) + r.Header.Set("Authorization", "Basic "+base64encodedClientIdAndSecret) +} diff --git a/oauth/doc.go b/oauth/doc.go new file mode 100644 index 0000000..1f2aa90 --- /dev/null +++ b/oauth/doc.go @@ -0,0 +1,57 @@ +/* +Package oauth provides wrappers for the Authentication (OAuth) V2 REST API. +https://aps.autodesk.com/en/docs/oauth/v2/developers_guide/overview/ + +The API supports the following features: +- Two-legged authentication +- Three-legged authentication +- Refreshing tokens (only for three-legged) + +To-do: +- Update APIs: + - get user profile (replaces informational) + +- Add missing APIs: + - get OIDC specs + - get JWKS + - logout + - introspect token + - revoke token + +Example code: +```go` + + func ExampleTwoLeggedAuth_Authenticate() { + + // acquire Forge secrets from environment + clientID := os.Getenv("FORGE_CLIENT_ID") + clientSecret := os.Getenv("FORGE_CLIENT_SECRET") + + if len(clientID) == 0 || len(clientSecret) == 0 { + log.Fatalf("Could not get from env the Forge secrets") + } + + // create oauth client + authenticator := oauth.NewTwoLegged(clientID, clientSecret) + + // request a token with needed scopes, separated by spaces + bearer, err := authenticator.GetToken("data:read data:write") + + if err != nil || len(bearer.AccessToken) == 0 { + log.Fatalf("Could not get from env the Forge secrets") + } + + // at this point, the bearer should contain the needed data. Check Bearer struct for more info + fmt.Printf("Bearer now contains:\n"+ + "AccessToken: %s\n"+ + "TokenType: %s\n"+ + "Expires in: %d\n", + bearer.AccessToken, + bearer.TokenType, + bearer.ExpiresIn) + + } + +``` +*/ +package oauth diff --git a/oauth/informational.go b/oauth/informational.go index 9320d50..086c139 100644 --- a/oauth/informational.go +++ b/oauth/informational.go @@ -3,7 +3,7 @@ package oauth import ( "encoding/json" "errors" - "io/ioutil" + "io" "net/http" "strconv" ) @@ -51,13 +51,14 @@ func NewInformationQuerier(authenticator ForgeAuthenticator) Information { } } -//AboutMe is used to get the profile of an authorizing end user +// AboutMe is used to get the profile of an authorizing end user func (i Information) AboutMe() (profile UserProfile, err error) { - requestPath := i.Authenticator.GetHostPath() + i.InformationalAPIPath + requestPath := i.Authenticator.HostPath() + i.InformationalAPIPath task := http.Client{} - req, err := http.NewRequest("GET", + req, err := http.NewRequest( + "GET", requestPath, nil, ) @@ -80,7 +81,7 @@ func (i Information) AboutMe() (profile UserProfile, err error) { defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } diff --git a/oauth/test/informational_test.go b/oauth/test/informational_test.go index 0ea0f04..6362eec 100644 --- a/oauth/test/informational_test.go +++ b/oauth/test/informational_test.go @@ -2,12 +2,13 @@ package oauth_test import ( "fmt" - "github.com/apprentice3d/forge-api-go-client/oauth" "os" "testing" + + "github.com/woweh/forge-api-go-client/oauth" ) -//TODO: set up a pipeline for auto-creating a 3-legged oauth token +// TODO: set up a pipeline for auto-creating a 3-legged oauth token func TestInformation_AboutMe(t *testing.T) { //prepare the credentials @@ -54,7 +55,7 @@ func ExampleInformation_AboutMe() { clientID := os.Getenv("FORGE_CLIENT_ID") clientSecret := os.Getenv("FORGE_CLIENT_SECRET") - authenticator := oauth.NewTwoLegged(clientID,clientSecret) + authenticator := oauth.NewTwoLegged(clientID, clientSecret) //authenticator := oauth.NewThreeLegged("","", "") info := oauth.NewInformationQuerier(authenticator) diff --git a/oauth/test/three_legged_test.go b/oauth/test/three_legged_test.go index 5c318a6..7db839c 100644 --- a/oauth/test/three_legged_test.go +++ b/oauth/test/three_legged_test.go @@ -1,9 +1,10 @@ package oauth_test import ( - "github.com/apprentice3d/forge-api-go-client/oauth" "os" "testing" + + "github.com/woweh/forge-api-go-client/oauth" ) func TestThreeLeggedAuthentication(t *testing.T) { @@ -61,8 +62,6 @@ func TestThreeLeggedAuthentication(t *testing.T) { }) } - - } func TestThreeLeggedAuthWithRefreshToken(t *testing.T) { @@ -114,6 +113,4 @@ func TestThreeLeggedAuthWithRefreshToken(t *testing.T) { }) } - - } diff --git a/oauth/test/two_legged_test.go b/oauth/test/two_legged_test.go index 87e8fb3..eb64668 100644 --- a/oauth/test/two_legged_test.go +++ b/oauth/test/two_legged_test.go @@ -1,11 +1,10 @@ package oauth_test import ( - "fmt" - "github.com/apprentice3d/forge-api-go-client/oauth" - "log" "os" "testing" + + "github.com/woweh/forge-api-go-client/oauth" ) func TestTwoLeggedAuthentication(t *testing.T) { @@ -17,91 +16,100 @@ func TestTwoLeggedAuthentication(t *testing.T) { t.Fatalf("Could not get from env the Forge secrets") } - t.Run("Valid Forge Secrets", func(t *testing.T) { - authenticator := oauth.NewTwoLegged(clientID, clientSecret) + t.Run( + "Valid Forge Secrets", func(t *testing.T) { + authenticator := oauth.NewTwoLegged(clientID, clientSecret) - bearer, err := authenticator.GetToken("data:read") + bearer, err := authenticator.GetToken("data:read") - if err != nil { - t.Error(err.Error()) - } + if err != nil { + t.Error(err.Error()) + } - if len(bearer.AccessToken) == 0 { - t.Errorf("Wrong bearer content: %v", bearer) - } - }) + if len(bearer.AccessToken) == 0 { + t.Errorf("Wrong bearer content: %v", bearer) + } + }, + ) - t.Run("Invalid Forge Secrets", func(t *testing.T) { - authenticator := oauth.NewTwoLegged("", clientSecret) + t.Run( + "Multiple scopes", func(t *testing.T) { + authenticator := oauth.NewTwoLegged(clientID, clientSecret) - bearer, err := authenticator.GetToken("data:read") + bearer, err := authenticator.GetToken("data:read data:search viewables:read") - if err == nil { - t.Errorf("Expected to fail due to wrong credentials, but got %v", bearer) - } + if err != nil { + t.Error(err.Error()) + } - if len(bearer.AccessToken) != 0 { - t.Errorf("expected to not receive a token, but received: %s", bearer.AccessToken) - } - }) + if len(bearer.AccessToken) == 0 { + t.Errorf("Wrong bearer content: %v", bearer) + } + }, + ) - t.Run("Invalid scope", func(t *testing.T) { - authenticator := oauth.NewTwoLegged(clientID, clientSecret) + t.Run( + "Invalid Forge Secrets", func(t *testing.T) { + authenticator := oauth.NewTwoLegged("", clientSecret) - bearer, err := authenticator.GetToken("data:improvise") + bearer, err := authenticator.GetToken("data:read") - if err == nil { - t.Errorf("Expected to fail due to wrong scope, but got %v\n", bearer) - } + if err == nil { + t.Errorf("Expected to fail due to wrong credentials, but got %v", bearer) + } - if len(bearer.AccessToken) != 0 { - t.Errorf("expected to not receive a token, but received: %s", bearer.AccessToken) - } - }) + if len(bearer.AccessToken) != 0 { + t.Errorf("expected to not receive a token, but received: %s", bearer.AccessToken) + } + }, + ) - t.Run("Invalid or unreachable host", func(t *testing.T) { - authenticator := oauth.NewTwoLegged(clientID, clientSecret) - authenticator.Host = "http://localhost" + t.Run( + "Invalid scope", func(t *testing.T) { + authenticator := oauth.NewTwoLegged(clientID, clientSecret) - bearer, err := authenticator.GetToken("data:read") + bearer, err := authenticator.GetToken("data:invalidScopeValue") - if err == nil { - t.Errorf("Expected to fail due to wrong host, but got %v\n", bearer) - } + if err == nil { + t.Errorf("Expected to fail due to wrong scope, but got %v\n", bearer) + } - if len(bearer.AccessToken) != 0 { - t.Errorf("expected to not receive a token, but received: %s", bearer.AccessToken) - } - }) -} + if len(bearer.AccessToken) != 0 { + t.Errorf("expected to not receive a token, but received: %s", bearer.AccessToken) + } + }, + ) -func ExampleTwoLeggedAuth_Authenticate() { + t.Run( + "Invalid multiple scopes, wrong separators", func(t *testing.T) { + authenticator := oauth.NewTwoLegged(clientID, clientSecret) - // aquire Forge secrets from environment - clientID := os.Getenv("FORGE_CLIENT_ID") - clientSecret := os.Getenv("FORGE_CLIENT_SECRET") + bearer, err := authenticator.GetToken("data:read;data:search,viewables:read") - if len(clientID) == 0 || len(clientSecret) == 0 { - log.Fatalf("Could not get from env the Forge secrets") - } + if err == nil { + t.Errorf("Expected to fail due to wrong scope, but got %v\n", bearer) + } - // create oauth client - authenticator := oauth.NewTwoLegged(clientID, clientSecret) + if len(bearer.AccessToken) != 0 { + t.Errorf("expected to not receive a token, but received: %s", bearer.AccessToken) + } + }, + ) - // request a token with needed scopes, separated by spaces - bearer, err := authenticator.GetToken("data:read data:write") + t.Run( + "Invalid or unreachable host", func(t *testing.T) { + authenticator := oauth.NewTwoLegged(clientID, clientSecret) + authenticator.Host = "http://localhost" - if err != nil || len(bearer.AccessToken) == 0 { - log.Fatalf("Could not get from env the Forge secrets") - } + bearer, err := authenticator.GetToken("data:read") - // at this point, the bearer should contain the needed data. Check Bearer struct for more info - fmt.Printf("Bearer now contains:\n"+ - "AccessToken: %s\n"+ - "TokenType: %s\n"+ - "Expires in: %d\n", - bearer.AccessToken, - bearer.TokenType, - bearer.ExpiresIn) + if err == nil { + t.Errorf("Expected to fail due to wrong host, but got %v\n", bearer) + } + if len(bearer.AccessToken) != 0 { + t.Errorf("expected to not receive a token, but received: %s", bearer.AccessToken) + } + }, + ) } diff --git a/oauth/three_legged.go b/oauth/three_legged.go index 67a6e2c..a78f5bc 100644 --- a/oauth/three_legged.go +++ b/oauth/three_legged.go @@ -4,12 +4,15 @@ import ( "bytes" "encoding/json" "errors" - "io/ioutil" + "io" "net/http" "net/url" "strconv" + + "github.com/woweh/forge-api-go-client" ) +// TODO: Review and test 3-legged authentication // NewThreeLegged returns a 3-legged authenticator with default host and authPath, // giving client secrets, redirectURI and optionally with a starting refresh token (useful for CLI apps) @@ -18,8 +21,8 @@ func NewThreeLegged(clientID, clientSecret, redirectURI, refreshToken string) *T AuthData{ clientID, clientSecret, - "https://developer.api.autodesk.com", - "/authentication/v1", + forge.HostName, + "/authentication/v2", }, redirectURI, refreshToken, @@ -27,19 +30,18 @@ func NewThreeLegged(clientID, clientSecret, redirectURI, refreshToken string) *T } // Authorize method returns an URL to redirect an end user, where it will be asked to give his consent for app to -//access the specified resources. +// access the specified resources. +// +// Parameter: +// - scope: a space separated list, like "data:read viewables:read". // -// The resources for which the permission is asked are specified as a space-separated list of required scopes. -// State can be used to specify, as URL-encoded payload, some arbitrary data that the authentication flow will pass back -// verbatim in a state query parameter to the callback URL. -// Note: You do not call this URL directly in your server code. -// See the Get a 3-Legged Token tutorial for more information on how to use this endpoint. -func (a ThreeLeggedAuth) Authorize(scope string, state string) (string, error) { - - request, err := http.NewRequest("GET", - a.Host+a.authPath+"/authorize", - nil, - ) +// References: +// - https://aps.autodesk.com/en/docs/oauth/v2/tutorials/get-3-legged-token/ +// - https://aps.autodesk.com/en/docs/oauth/v2/reference/http/authorize-GET/ +// - https://aps.autodesk.com/en/docs/oauth/v2/developers_guide/scopes/ +func (a *ThreeLeggedAuth) Authorize(scope, state string) (string, error) { + + request, err := http.NewRequest("GET", a.Host+a.authPath+"/authorize", nil) if err != nil { return "", err @@ -57,41 +59,42 @@ func (a ThreeLeggedAuth) Authorize(scope string, state string) (string, error) { return request.URL.String(), nil } +// SetRefreshToken sets the refresh token for the ThreeLeggedAuth instance. +// Parameter: +// - refreshtoken: a string representing the refresh token to be set. func (a *ThreeLeggedAuth) SetRefreshToken(refreshtoken string) { a.RefreshToken = refreshtoken } -//ExchangeCode is used to exchange the authorization code for a token and an exchange token +// ExchangeCode is used to exchange the authorization code for an access token (and refresh token). +// References: +// - https://aps.autodesk.com/en/docs/oauth/v2/tutorials/get-3-legged-token/ +// - https://aps.autodesk.com/en/docs/oauth/v2/reference/http/gettoken-POST/ func (a *ThreeLeggedAuth) ExchangeCode(code string) (bearer Bearer, err error) { task := http.Client{} body := url.Values{} - body.Add("client_id", a.ClientID) - body.Add("client_secret", a.ClientSecret) body.Add("grant_type", "authorization_code") body.Add("code", code) body.Add("redirect_uri", a.RedirectURI) - req, err := http.NewRequest("POST", - a.Host+a.authPath+"/gettoken", - bytes.NewBufferString(body.Encode()), - ) - + req, err := http.NewRequest("POST", a.Host+a.authPath+"/token", bytes.NewBufferString(body.Encode())) if err != nil { return } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + setBasicAuthHeader(req, a.AuthData) + req.Header.Set("Accept", "application/json") response, err := task.Do(req) - if err != nil { return } - defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -106,41 +109,39 @@ func (a *ThreeLeggedAuth) ExchangeCode(code string) (bearer Bearer, err error) { func (a *ThreeLeggedAuth) GetToken(scope string) (token Bearer, err error) { token, err = a.GetNewRefreshToken(a.RefreshToken, scope) + if err != nil { + return + } a.RefreshToken = token.RefreshToken return } // GetNewRefreshToken is used to get a new access token by using the refresh token provided by ExchangeCode -func (a ThreeLeggedAuth) GetNewRefreshToken(refreshToken string, scope string) (bearer Bearer, err error) { +func (a *ThreeLeggedAuth) GetNewRefreshToken(refreshToken string, scope string) (bearer Bearer, err error) { task := http.Client{} body := url.Values{} - body.Add("client_id", a.ClientID) - body.Add("client_secret", a.ClientSecret) body.Add("grant_type", "refresh_token") body.Add("refresh_token", refreshToken) body.Add("scope", scope) - req, err := http.NewRequest("POST", - a.Host+a.authPath+"/refreshtoken", - bytes.NewBufferString(body.Encode()), - ) - + req, err := http.NewRequest("POST", a.Host+a.authPath+"/token", bytes.NewBufferString(body.Encode())) if err != nil { return } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + setBasicAuthHeader(req, a.AuthData) + req.Header.Set("Accept", "application/json") response, err := task.Do(req) - if err != nil { return } - defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -150,7 +151,6 @@ func (a ThreeLeggedAuth) GetNewRefreshToken(refreshToken string, scope string) ( return } - -func (a ThreeLeggedAuth) GetRefreshToken() string { +func (a *ThreeLeggedAuth) GetRefreshToken() string { return a.RefreshToken -} \ No newline at end of file +} diff --git a/oauth/two_legged.go b/oauth/two_legged.go index e24d5e2..abcdfa6 100644 --- a/oauth/two_legged.go +++ b/oauth/two_legged.go @@ -4,45 +4,54 @@ import ( "bytes" "encoding/json" "errors" - "io/ioutil" + "io" "net/http" "net/url" "strconv" + + "github.com/woweh/forge-api-go-client" ) // NewTwoLegged returns a 2-legged authenticator with default host and authPath func NewTwoLegged(clientID, clientSecret string) *TwoLeggedAuth { - return &TwoLeggedAuth { + return &TwoLeggedAuth{ AuthData{ clientID, clientSecret, - "https://developer.api.autodesk.com", - "/authentication/v1", + forge.HostName, + "/authentication/v2", }, - } } // GetToken allows getting a token with a given scope -func (a TwoLeggedAuth) GetToken(scope string) (bearer Bearer, err error) { +// +// Parameter: +// - scope: a space separated list, like "data:read data:search viewables:read". +// +// References: +// - https://aps.autodesk.com/en/docs/oauth/v2/reference/http/gettoken-POST/ - +// - https://aps.autodesk.com/en/docs/oauth/v2/tutorials/get-2-legged-token/ +// - https://aps.autodesk.com/en/docs/oauth/v2/developers_guide/scopes/ +func (a *TwoLeggedAuth) GetToken(scope string) (bearer Bearer, err error) { task := http.Client{} body := url.Values{} - body.Add("client_id", a.ClientID) - body.Add("client_secret", a.ClientSecret) body.Add("grant_type", "client_credentials") body.Add("scope", scope) - req, err := http.NewRequest("POST", - a.Host+a.authPath+"/authenticate", + req, err := http.NewRequest( + "POST", + a.Host+a.authPath+"/token", bytes.NewBufferString(body.Encode()), ) - if err != nil { return } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + setBasicAuthHeader(req, a.AuthData) response, err := task.Do(req) if err != nil { @@ -51,7 +60,7 @@ func (a TwoLeggedAuth) GetToken(scope string) (bearer Bearer, err error) { defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -62,18 +71,20 @@ func (a TwoLeggedAuth) GetToken(scope string) (bearer Bearer, err error) { return } - -func (a TwoLeggedAuth) GetRefreshToken() string { +func (a *TwoLeggedAuth) GetRefreshToken() string { return "" } -// GetHostPath returns host path, usually different in case of prd stg and dev environments -func (a AuthData) GetHostPath() string { +// HostPath returns host path, usually different in case of prd stg and dev environments +// Note: +// - This might be useful for Autodesk internal use, but not for external developers. +func (a *AuthData) HostPath() string { return a.Host } // SetHostPath allows changing the host, usually useful for switching between prd stg and dev environments +// Note: +// - This might be useful for Autodesk internal use, but not for external developers. func (a *AuthData) SetHostPath(host string) { a.Host = host } - diff --git a/oauth/types.go b/oauth/types.go index ae746aa..9caf21c 100644 --- a/oauth/types.go +++ b/oauth/types.go @@ -1,12 +1,12 @@ package oauth // ForgeAuthenticator defines an interface that allows abstraction of 2-legged and a 3-legged context. -// This provides useful when an API accepts both 2-legged and 3-legged context tokens +// +// This provides useful when an API accepts both 2-legged and 3-legged context tokens type ForgeAuthenticator interface { GetToken(scope string) (Bearer, error) - GetHostPath() string + HostPath() string GetRefreshToken() string - //SetHostPath(path string) } // AuthData reflects the data common to 2-legged and 3-legged api calls @@ -17,13 +17,11 @@ type AuthData struct { authPath string } - // TwoLeggedAuth struct holds data necessary for making requests in 2-legged context type TwoLeggedAuth struct { AuthData } - // ThreeLeggedAuth struct holds data necessary for making requests in 3-legged context type ThreeLeggedAuth struct { AuthData @@ -31,7 +29,6 @@ type ThreeLeggedAuth struct { RefreshToken string } - // Bearer reflects the response when acquiring a 2-legged token or in 3-legged context for exchanging the authorization // code for a token + refresh token and when exchanging the refresh token for a new token type Bearer struct { @@ -40,11 +37,3 @@ type Bearer struct { AccessToken string `json:"access_token"` // The access token RefreshToken string `json:"refresh_token,omitempty"` // The refresh token used in 3-legged oauth } - - -// ThreeLeggedAuthenticator interface defines the method necessary to qualify as 3-legged authenticator -//type ThreeLeggedAuthenticator interface { -// Authorize(scope string, state string) (string, error) -// GetToken(code string) (Bearer, error) -// RefreshToken(refreshToken string, scope string) (Bearer, error) -//} \ No newline at end of file diff --git a/recap/call.go b/recap/call.go index a74aa59..6981ace 100644 --- a/recap/call.go +++ b/recap/call.go @@ -4,15 +4,15 @@ import ( "bytes" "encoding/json" "errors" - "io/ioutil" + "io" "log" + "math/rand" "mime/multipart" "net/http" "net/url" "strconv" "strings" "time" - "math/rand" ) func createPhotoScene(path string, name string, formats []string, sceneType string, token string) (scene PhotoScene, err error) { @@ -45,7 +45,7 @@ func createPhotoScene(path string, name string, formats []string, sceneType stri defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -105,7 +105,7 @@ func addFileToSceneUsingLink(path string, photoSceneID string, link string, toke defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -135,7 +135,7 @@ func addFileToSceneUsingFileData(path string, photoSceneID string, data []byte, writer := multipart.NewWriter(body) writer.WriteField("photosceneid", photoSceneID) writer.WriteField("type", "image") - formFile, err := writer.CreateFormFile("file[0]", "data" + strconv.Itoa(rand.Int())) + formFile, err := writer.CreateFormFile("file[0]", "data"+strconv.Itoa(rand.Int())) if err != nil { log.Println(err.Error()) return @@ -163,7 +163,7 @@ func addFileToSceneUsingFileData(path string, photoSceneID string, data []byte, defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -205,7 +205,7 @@ func startSceneProcessing(path string, photoSceneID string, token string) (resul defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -247,7 +247,7 @@ func getSceneProgress(path string, photoSceneID string, token string) (result Sc defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -291,7 +291,7 @@ func getSceneResult(path string, photoSceneID string, token string, format strin defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -333,7 +333,7 @@ func cancelSceneProcessing(path string, photoSceneID string, token string) (resu defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } @@ -377,7 +377,7 @@ func deleteScene(path string, photoSceneID string, token string) (result SceneDe defer response.Body.Close() if response.StatusCode != http.StatusOK { - content, _ := ioutil.ReadAll(response.Body) + content, _ := io.ReadAll(response.Body) err = errors.New("[" + strconv.Itoa(response.StatusCode) + "] " + string(content)) return } diff --git a/recap/recap.go b/recap/recap.go index 68c1e0e..e413db2 100644 --- a/recap/recap.go +++ b/recap/recap.go @@ -1,21 +1,21 @@ // Package recap contains the Go wrappers for calls to Forge Reality Capture API // https://developer.autodesk.com/api/reality-capture-cover-page/ // -// The workflow is the following: -// - create a photoScene +// The workflow is the following: +// - create a photoScene // - upload images to photoScene // - start photoScene processing // - get the result package recap import ( - "github.com/apprentice3d/forge-api-go-client/oauth" + "github.com/woweh/forge-api-go-client/oauth" ) // API struct holds all paths necessary to access ReCap API type ReCapAPI struct { Authenticator oauth.ForgeAuthenticator - ReCapPath string + ReCapPath string } // NewAPI returns a ReCap API client with default configurations @@ -27,16 +27,17 @@ func NewAPI(authenticator oauth.ForgeAuthenticator) ReCapAPI { } // CreatePhotoScene prepares a scene with a given name, expected output formats and sceneType -// name - should not be empty -// formats - should be of type rcm, rcs, obj, ortho or report -// sceneType - should be either "aerial" or "object" +// +// name - should not be empty +// formats - should be of type rcm, rcs, obj, ortho or report +// sceneType - should be either "aerial" or "object" func (api ReCapAPI) CreatePhotoScene(name string, formats []string, sceneType string) (scene PhotoScene, err error) { bearer, err := api.Authenticator.GetToken("data:write") if err != nil { return } - path := api.Authenticator.GetHostPath() + api.ReCapPath + path := api.Authenticator.HostPath() + api.ReCapPath scene, err = createPhotoScene(path, name, formats, sceneType, bearer.AccessToken) return @@ -50,7 +51,7 @@ func (api ReCapAPI) AddFileToSceneUsingLink(sceneID string, link string) (upload if err != nil { return } - path := api.Authenticator.GetHostPath() + api.ReCapPath + path := api.Authenticator.HostPath() + api.ReCapPath uploads, err = addFileToSceneUsingLink(path, sceneID, link, bearer.AccessToken) return @@ -64,7 +65,7 @@ func (api ReCapAPI) AddFileToSceneUsingData(sceneID string, data []byte) (upload if err != nil { return } - path := api.Authenticator.GetHostPath() + api.ReCapPath + path := api.Authenticator.HostPath() + api.ReCapPath uploads, err = addFileToSceneUsingFileData(path, sceneID, data, bearer.AccessToken) @@ -77,24 +78,26 @@ func (api ReCapAPI) StartSceneProcessing(sceneID string) (result SceneStartProce if err != nil { return } - path := api.Authenticator.GetHostPath() + api.ReCapPath + path := api.Authenticator.HostPath() + api.ReCapPath result, err = startSceneProcessing(path, sceneID, bearer.AccessToken) return } // GetSceneProgress polls the scene processing status and progress +// // Note: instead of polling, consider using the callback parameter that can be specified upon scene creation func (api ReCapAPI) GetSceneProgress(sceneID string) (progress SceneProgressReply, err error) { bearer, err := api.Authenticator.GetToken("data:read") if err != nil { return } - path := api.Authenticator.GetHostPath() + api.ReCapPath + path := api.Authenticator.HostPath() + api.ReCapPath progress, err = getSceneProgress(path, sceneID, bearer.AccessToken) return } // GetSceneResults requests result in a specified format +// // Note: The link specified in SceneResultReplies will be available for the time specified in reply, // even if the scene is deleted func (api ReCapAPI) GetSceneResults(sceneID string, format string) (result SceneResultReply, err error) { @@ -102,7 +105,7 @@ func (api ReCapAPI) GetSceneResults(sceneID string, format string) (result Scene if err != nil { return } - path := api.Authenticator.GetHostPath() + api.ReCapPath + path := api.Authenticator.HostPath() + api.ReCapPath result, err = getSceneResult(path, sceneID, bearer.AccessToken, format) return } @@ -113,7 +116,7 @@ func (api ReCapAPI) CancelSceneProcessing(sceneID string) (ID string, err error) if err != nil { return } - path := api.Authenticator.GetHostPath() + api.ReCapPath + path := api.Authenticator.HostPath() + api.ReCapPath _, err = cancelSceneProcessing(path, sceneID, bearer.AccessToken) return sceneID, err @@ -125,7 +128,7 @@ func (api ReCapAPI) DeleteScene(sceneID string) (ID string, err error) { if err != nil { return } - path := api.Authenticator.GetHostPath() + api.ReCapPath + path := api.Authenticator.HostPath() + api.ReCapPath _, err = deleteScene(path, sceneID, bearer.AccessToken) ID = sceneID return diff --git a/recap/test/recap_test.go b/recap/test/recap_test.go index e76b9fc..5d6a38a 100644 --- a/recap/test/recap_test.go +++ b/recap/test/recap_test.go @@ -2,15 +2,16 @@ package recap_test import ( "fmt" - "github.com/apprentice3d/forge-api-go-client/oauth" - "github.com/apprentice3d/forge-api-go-client/recap" - "io/ioutil" + "io" "log" "net/http" "os" "strconv" "testing" "time" + + "github.com/woweh/forge-api-go-client/oauth" + "github.com/woweh/forge-api-go-client/recap" ) func TestReCapAPIWorkflowUsingRemoteLinks(t *testing.T) { @@ -33,7 +34,6 @@ func TestReCapAPIWorkflowUsingRemoteLinks(t *testing.T) { clientID := os.Getenv("FORGE_CLIENT_ID") clientSecret := os.Getenv("FORGE_CLIENT_SECRET") - authenticator := oauth.NewTwoLegged(clientID, clientSecret) recapAPI := recap.NewAPI(authenticator) @@ -91,7 +91,6 @@ func TestReCapAPIWorkflowUsingRemoteLinks(t *testing.T) { } }) - t.Run("Check the result file size for normal size", func(t *testing.T) { response, err := recapAPI.GetSceneResults(scene.ID, testingFormat) if err != nil { @@ -130,7 +129,6 @@ func TestReCapAPIWorkflowUsingRemoteLinks(t *testing.T) { }) - t.Run("Delete the scene", func(t *testing.T) { _, err := recapAPI.DeleteScene(scene.ID) if err != nil { @@ -183,7 +181,7 @@ func TestReCapAPIWorkflowUsingLocalFiles(t *testing.T) { t.Fatal(err.Error()) } - data, err := ioutil.ReadAll(response.Body) + data, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { t.Fatal(err.Error()) diff --git a/recap/types.go b/recap/types.go index 1542c4a..a6d4ee5 100644 --- a/recap/types.go +++ b/recap/types.go @@ -2,10 +2,10 @@ package recap // PhotoScene holds data encountered in replies like creation of photoScene type PhotoScene struct { - ID string `json:"photosceneid"` - Name string `json:"name,omitempty"` - Files []string `json:",omitempty"` - Formats []string `json:",omitempty"` + ID string `json:"photosceneid"` + Name string `json:"name,omitempty"` + Files []string `json:",omitempty"` + Formats []string `json:",omitempty"` Metadata []struct { Name string Values string @@ -41,7 +41,7 @@ type SceneCancelReply struct { type FileUploadingReply struct { Usage string `json:",omitempty"` Resource string `json:",omitempty"` - Files *struct { + Files *struct { File struct { FileName string `json:"filename"` FileID string `json:"fileid"` @@ -79,8 +79,8 @@ type SceneStartProcessingReply struct { // SceneProgressReply reflects the response content upon polling for scene status type SceneProgressReply struct { - Usage string `json:",omitempty"` - Resource string `json:",omitempty"` + Usage string `json:",omitempty"` + Resource string `json:",omitempty"` PhotoScene struct { ID string `json:"photosceneid"` Message string `json:"progressmsg"` @@ -90,8 +90,6 @@ type SceneProgressReply struct { Error *Error `json:"Error,omitempty"` } - - // SceneResultReply reflects the response content upon requesting the scene results in a certain format type SceneResultReply struct { PhotoScene struct { @@ -124,7 +122,8 @@ type ErrorMessage struct { // Error is inner struct encountered in cases when the server reported status OK, but still contains details // on encountered errors. Check the bug section of this documentation for more info. -// This bug was reported to the engineering team +// +// This bug was reported to the engineering team type Error struct { Code string `json:"code"` Message string `json:"msg"`