Compare commits
107 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d10b77ee4 | |||
| 82dca56fbd | |||
| 80d1df8eca | |||
| b5796245aa | |||
| 639c774f80 | |||
| 3bb5d1e73a | |||
| e01360ad03 | |||
| 2494b160e8 | |||
| b0da4510bf | |||
| 96392d0d2f | |||
| 0aacc3fe89 | |||
| 39f5257f85 | |||
| 2ea6348689 | |||
| 42c26e38bf | |||
| cb31fd1a08 | |||
| 67236abafe | |||
| 59a4f8f864 | |||
| 0e98e1cccd | |||
| 79c6f02544 | |||
| 07713b41e1 | |||
| c3f944dcf1 | |||
| 3c0b9e8b1c | |||
| 8890f8cb36 | |||
| a0eae88479 | |||
| 8785e49f73 | |||
| 6e35d6af6d | |||
| b67eb8d8af | |||
| 63f06c8541 | |||
| 3f49bb05d1 | |||
| 9a879fd1ac | |||
| c3230d5d91 | |||
| f1a64590d7 | |||
| c2735f0a32 | |||
| 0b01091209 | |||
| 6568781275 | |||
| 98223e251c | |||
| 6740659af4 | |||
| 08f702794a | |||
| 879ebf7e3c | |||
| 1046e2aafc | |||
| 5cb0eddf4e | |||
| e97ce83c6b | |||
| ea23e72c77 | |||
| 701013ad46 | |||
| 37358570ec | |||
| 02b9a73164 | |||
| fdc0842b03 | |||
| 530387b87c | |||
| b16e32d1ff | |||
| d91fccbb10 | |||
| 62f687b589 | |||
| c0d4d951df | |||
| 4e1753e1fd | |||
| 23d5dd44bc | |||
| 4c615ae8c6 | |||
| d5ee9fb76c | |||
| ea118bcdbb | |||
| ba456ee3eb | |||
| 10d283a9f7 | |||
| 647c4733cb | |||
| 30c9b17dab | |||
| f7ddc19086 | |||
| 6d06901b4f | |||
| 4b588fc287 | |||
| 786e683d89 | |||
| 8d999f4f9c | |||
| bb7542e254 | |||
| d6f6254a92 | |||
| f60f85b639 | |||
| bcdf73cc70 | |||
| 47e72ee1a7 | |||
| f3de5324db | |||
| 4dd6db886f | |||
| 4b82db8ea2 | |||
| 9e7f26f7a6 | |||
| b19f8c4219 | |||
| c517e61517 | |||
| b3e0623856 | |||
| e5d1ef2448 | |||
| 83c3de05fa | |||
| 507ded7d4a | |||
| e15029bab3 | |||
| a43fd44206 | |||
| 1bcd8ac3a4 | |||
| a8dc93e22b | |||
| 5a0f883b98 | |||
| a5d035671a | |||
| cd6ebad619 | |||
| 33c2e6e1a4 | |||
| b97702adb1 | |||
| 80c4f694ec | |||
| fb5042004f | |||
| c0a9291632 | |||
| b783d2acb6 | |||
| 93539adc1e | |||
| 98005933de | |||
| 50906b172a | |||
| 05f7353925 | |||
| 8328498553 | |||
| 59019bf846 | |||
| 3afaf61a1a | |||
| 424609fad0 | |||
| 46c067308e | |||
| 0e33e8df8f | |||
| bc81c21e9d | |||
| 7f8b59d348 | |||
| 44ba61e4a5 |
@@ -3,18 +3,11 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"csharpier": {
|
||||
"version": "1.0.2",
|
||||
"version": "1.1.2",
|
||||
"commands": [
|
||||
"csharpier"
|
||||
],
|
||||
"rollForward": false
|
||||
},
|
||||
"gitversion.tool": {
|
||||
"version": "6.1.0",
|
||||
"commands": [
|
||||
"dotnet-gitversion"
|
||||
],
|
||||
"rollForward": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ To ensure high-quality and consistent commits, please follow these guidelines:
|
||||
3. **Test your changes**
|
||||
- Run all unit tests before committing.
|
||||
- Add or update xUnit tests as needed.
|
||||
- Use FluentAssertions for assertions and Moq for mocking in tests.
|
||||
- Use AwesomeAssertions for assertions and Moq for mocking in tests.
|
||||
|
||||
4. **Review your changes**
|
||||
- Double-check for accidental debug code or commented-out code.
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
name: Integration Test
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
docker-compose-file:
|
||||
required: true
|
||||
type: string
|
||||
use-internal-image:
|
||||
default: false
|
||||
type: boolean
|
||||
secrets:
|
||||
CODECOV_TOKEN:
|
||||
required: true
|
||||
jobs:
|
||||
integration-test:
|
||||
env:
|
||||
Solution: "Speckle.Sdk.sln"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: 8.x.x
|
||||
cache: true
|
||||
cache-dependency-path: "**/packages.lock.json"
|
||||
|
||||
- name: 🔐 Login to Github Container Registry
|
||||
if: ${{ inputs.use-internal-image }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: "ghcr.io"
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ github.token }}
|
||||
|
||||
- name: ⚙️ Spin up Server
|
||||
run: docker compose -f ${{ inputs.docker-compose-file }} up --wait
|
||||
|
||||
- name: 📦 Restore
|
||||
run: dotnet restore ${{ env.Solution }} --locked-mode
|
||||
|
||||
- name: 🏗️ Build
|
||||
run: dotnet build ${{ env.Solution }} --configuration Release --no-restore -warnaserror
|
||||
|
||||
- name: 🔨 Integration Tests against Public Server
|
||||
if: ${{ !inputs.use-internal-image }}
|
||||
run: dotnet test ${{ env.Solution }} --filter "(Category=Integration)&(Server!=Internal)" --configuration Release --no-build --no-restore --verbosity=normal /p:AltCover=true /p:AltCoverAttributeFilter=ExcludeFromCodeCoverage
|
||||
|
||||
- name: 🔨 Integration Tests against Internal Server
|
||||
if: ${{ inputs.use-internal-image }}
|
||||
run: dotnet test ${{ env.Solution }} --filter "Category=Integration" --configuration Release --no-build --no-restore --verbosity=normal /p:AltCover=true /p:AltCoverAttributeFilter=ExcludeFromCodeCoverage
|
||||
|
||||
- name: Upload coverage reports to Codecov with GitHub Action
|
||||
uses: codecov/codecov-action@v5
|
||||
continue-on-error: true
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
files: tests/**/coverage.xml
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
+50
-21
@@ -1,32 +1,61 @@
|
||||
name: .NET CI Build
|
||||
name: PR Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
env:
|
||||
Solution: "Speckle.Sdk.sln"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.x.x
|
||||
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.nuget/packages
|
||||
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: 8.x.x
|
||||
cache: true
|
||||
cache-dependency-path: "**/packages.lock.json"
|
||||
|
||||
- name: 🔫 Build All
|
||||
run: ./build.sh
|
||||
- name: 📦 Tool Restore
|
||||
run: dotnet tool restore
|
||||
|
||||
- name: 📄 Format
|
||||
run: dotnet csharpier check .
|
||||
|
||||
- name: 📦 Restore
|
||||
run: dotnet restore ${{ env.Solution }} --locked-mode
|
||||
|
||||
- name: 🏗️ Build
|
||||
run: dotnet build ${{ env.Solution }} --configuration Release --no-restore -warnaserror
|
||||
|
||||
- name: 🔨 Unit Tests
|
||||
run: dotnet test ${{ env.Solution }} --configuration Release --filter "Category!=Integration" --no-build --no-restore --verbosity=normal /p:AltCover=true /p:AltCoverAttributeFilter=ExcludeFromCodeCoverage
|
||||
|
||||
- name: 🎁 Pack
|
||||
run: dotnet pack ${{ env.Solution }} --configuration Release --no-build
|
||||
|
||||
- name: Upload coverage reports to Codecov with GitHub Action
|
||||
uses: codecov/codecov-action@v5
|
||||
continue-on-error: true
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
files: tests/**/coverage.xml
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
integration-test-internal:
|
||||
uses: "./.github/workflows/integration-test.yml"
|
||||
with:
|
||||
docker-compose-file: "docker-compose-internal.yml"
|
||||
use-internal-image: true
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
- name: Upload coverage reports to Codecov with GitHub Action
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
files: tests/**/coverage.xml
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
integration-test-public:
|
||||
uses: "./.github/workflows/integration-test.yml"
|
||||
with:
|
||||
docker-compose-file: "docker-compose.yml"
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -2,36 +2,63 @@ name: .NET Build and Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main", "dev"]
|
||||
tags: ["3.*"]
|
||||
tags: ["3.*.*"]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: "nuget.org"
|
||||
permissions:
|
||||
id-token: write # enable GitHub OIDC token issuance for this job
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: 8.x.x
|
||||
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.nuget/packages
|
||||
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
|
||||
|
||||
cache: true
|
||||
cache-dependency-path: "**/packages.lock.json"
|
||||
|
||||
- id: set-version
|
||||
name: Set version to output
|
||||
run: |
|
||||
TAG=${{ github.ref_name }}
|
||||
if [[ "${{ github.ref }}" != refs/tags/* ]]; then
|
||||
TAG="3.0.99.${{ github.run_number }}"
|
||||
fi
|
||||
SEMVER="${TAG}"
|
||||
FILE_VERSION=$(echo "$TAG" | sed -E 's/^([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
|
||||
FILE_VERSION="$FILE_VERSION.${{ github.run_number }}"
|
||||
|
||||
echo "semver=$SEMVER" >> "$GITHUB_OUTPUT"
|
||||
echo "fileVersion=$FILE_VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo $SEMVER
|
||||
echo $FILE_VERSION
|
||||
|
||||
- name: 🔫 Build and Pack
|
||||
run: ./build.sh pack
|
||||
|
||||
env:
|
||||
SEMVER: ${{ steps.set-version.outputs.SEMVER }}
|
||||
FILE_VERSION: ${{ steps.set-version.outputs.FILE_VERSION }}
|
||||
|
||||
- name: Upload coverage reports to Codecov with GitHub Action
|
||||
uses: codecov/codecov-action@v5
|
||||
continue-on-error: true
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
files: tests/**/coverage.xml
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
- name: NuGet login (OIDC → temp API key)
|
||||
uses: NuGet/login@v1
|
||||
id: login
|
||||
with:
|
||||
user: ${{ secrets.NUGET_USER }}
|
||||
|
||||
- name: Push to nuget.org
|
||||
run: dotnet nuget push output/*.nupkg --source "https://api.nuget.org/v3/index.json" --api-key ${{secrets.CONNECTORS_NUGET_TOKEN }} --skip-duplicate
|
||||
run: dotnet nuget push output/*.nupkg --source "https://api.nuget.org/v3/index.json" --api-key ${{steps.login.outputs.NUGET_API_KEY}}
|
||||
|
||||
@@ -15,6 +15,7 @@ tests/TestArchives/Scratch
|
||||
tools
|
||||
.vscode
|
||||
.idea/
|
||||
.volumes/
|
||||
|
||||
.DS_Store
|
||||
*.snupkg
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
dotnet 8.0.400
|
||||
@@ -1,8 +1,8 @@
|
||||
<Project>
|
||||
<PropertyGroup Condition="'$(IsTestProject)' == 'true'">
|
||||
<PropertyGroup Condition="'$(IsTestProject)' == 'true' or '$(TestProjectAnalyserRules)' == 'true' ">
|
||||
<NoWarn>
|
||||
<!-- Things we need to test -->
|
||||
CS0618;CA1034;CA2201;CA1051;CA1040;CA1724;
|
||||
CS0618;CA1034;CA2201;CA1051;CA1040;CA1724;CA1065;
|
||||
IDE0044;IDE0130;CA1508;
|
||||
<!-- Analysers that provide no tangeable value to a test project -->
|
||||
CA5394;CA2007;CA1852;CA1819;CA1711;CA1063;CA1816;CA2234;CS8618;CA1054;CA1810;CA2208;CA1019;CA1831;
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
workflow: GitFlow/v1
|
||||
next-version: 3.0.0
|
||||
branches:
|
||||
main:
|
||||
prevent-increment:
|
||||
when-current-commit-tagged: true
|
||||
@@ -9,9 +9,12 @@ Speckle | Sharp | SDK
|
||||
### .NET SDK, Tests, and Objects
|
||||
|
||||
[](https://codecov.io/gh/specklesystems/speckle-sharp-sdk)
|
||||
<a href="https://www.nuget.org/packages/Speckle.Sdk/"><img alt="NuGet Version" src="https://img.shields.io/nuget/v/Speckle.Sdk?label=Speckle.Sdk"></a>
|
||||
<a href="https://www.nuget.org/packages/Speckle.Objects/"><img alt="NuGet Version" src="https://img.shields.io/nuget/v/Speckle.Sdk?label=Speckle.Objects"></a>
|
||||
<a href="https://www.nuget.org/packages/Speckle.Automate.Sdk/"><img alt="NuGet Version" src="https://img.shields.io/nuget/v/Speckle.Sdk?label=Speckle.Automate.Sdk"></a>
|
||||
|
||||
> [!WARNING]
|
||||
> This is an early beta release, not meant for use in production! We're working to stabilise the 3.0 API, and until then there will be breaking changes. You have been warned!
|
||||
> Releases Speckle.Sdk and Speckle.Objects are reliable for production use, but the APIs may not be wholly stable, and there may be breaking changes between releases, with little documentation.
|
||||
|
||||
# Repo structure
|
||||
|
||||
@@ -28,11 +31,13 @@ This repo is the home of our next-generation Speckle .NET SDK. It uses .NET Stan
|
||||
|
||||
### Other repos
|
||||
|
||||
Make sure to also check and ⭐️ these other Speckle next generation repositories:
|
||||
Make sure to also check and ⭐️ these other repositories:
|
||||
|
||||
- [`speckle-sharp-connectors`](https://github.com/specklesystems/speckle-sharp-connectors): our csharp repo of next gen connectors
|
||||
- [`speckle-sketchup`](https://github.com/specklesystems/speckle-sketchup): Sketchup connector
|
||||
- [`speckle-powerbi`](https://github.com/specklesystems/speckle-powerbi): PowerBi connector
|
||||
- [`speckle-sharp-connectors`](https://github.com/specklesystems/speckle-sharp-connectors): our csharp repo of next gen connectors.
|
||||
- [`speckle-server`](https://github.com/specklesystems/speckle-server): the speckle server.
|
||||
- [`speckle-sketchup`](https://github.com/specklesystems/speckle-blender): Blender connector.
|
||||
- [`speckle-sketchup`](https://github.com/specklesystems/speckle-sketchup): Sketchup connector.
|
||||
- [`speckle-powerbi`](https://github.com/specklesystems/speckle-powerbi): PowerBi connector.
|
||||
- and more [connectors & tooling](https://github.com/specklesystems/)!
|
||||
|
||||
## Documentation
|
||||
@@ -45,19 +50,27 @@ Comprehensive developer and user documentation can be found in our:
|
||||
|
||||
### Building
|
||||
|
||||
Make sure you clone this repository together with its submodules: `git clone https://github.com/specklesystems/speckle-sharp-sdk.git -recursive`.
|
||||
Afterwards, just restore all the NuGet packages and hit Build!
|
||||
Ensure you're using a [8.0.4xx](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) .NET SDK.
|
||||
After cloning this repository, just restore all the NuGet packages and hit Build!
|
||||
|
||||
### Developing
|
||||
|
||||
This project is evolving fast, to better understand how to use Core we suggest checking out the Unit and Integration tests. Running the integration tests locally requires a local server running on your computer.
|
||||
It is highly recommended you use
|
||||
- Either Jetbrains Rider or Visual Studio 2022
|
||||
- Ensure your IDE is set to use [the correct .NET SDK version](https://github.com/specklesystems/speckle-sharp-sdk/blob/main/global.json) (newer major versions may work, but may incorrectly run analysers we haven't configured)
|
||||
- You should install the cshapier plugin ([Rider](https://plugins.jetbrains.com/plugin/18243-csharpier), [VS](https://marketplace.visualstudio.com/items?itemName=csharpier.CSharpier)) and configure it to run on save
|
||||
|
||||
We'll be also adding [preliminary documentation on our forum](https://discourse.speckle.works/c/speckle-insider/10).
|
||||
Docs are a bit patchy [https://docs.speckle.systems/developers/looking-for-developer-docs](https://docs.speckle.systems/developers/looking-for-developer-docs)
|
||||
|
||||
### Tests
|
||||
|
||||
There are two test projects, one for unit tests and one for integration tests. The latter needs a server running locally in order to run.
|
||||
There are several test projects. It is a requirement that all tests pass for PRs to be merged.
|
||||
|
||||
The Integration test projects require a local server to be running.
|
||||
You must have docker installed. Then you can run `docker compose up` from the root of the repo to start the required containers.
|
||||
|
||||
In CI, they will be run against both the public and private versions of the server.
|
||||
It is important that we remain compatible with both server versions.
|
||||
## Contributing
|
||||
|
||||
Before embarking on submitting a patch, please make sure you read:
|
||||
|
||||
+2
-1
@@ -23,11 +23,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "config", "config", "{DA2AED
|
||||
Directory.Packages.props = Directory.Packages.props
|
||||
global.json = global.json
|
||||
README.md = README.md
|
||||
GitVersion.yml = GitVersion.yml
|
||||
docker-compose.yml = docker-compose.yml
|
||||
CodeMetricsConfig.txt = CodeMetricsConfig.txt
|
||||
Directory.Build.Targets = Directory.Build.Targets
|
||||
.config\dotnet-tools.json = .config\dotnet-tools.json
|
||||
docker-compose-internal.yml = docker-compose-internal.yml
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{58D37DA9-F948-48CA-9A73-F5BBBD533DBF}"
|
||||
@@ -42,6 +42,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.github\workflows\pr.yml = .github\workflows\pr.yml
|
||||
.github\workflows\release.yml = .github\workflows\release.yml
|
||||
.github\workflows\integration-test.yml = .github\workflows\integration-test.yml
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Speckle.Sdk.Tests.Performance", "tests\Speckle.Sdk.Tests.Performance\Speckle.Sdk.Tests.Performance.csproj", "{870E3396-E6F7-43AE-B120-E651FA4F46BD}"
|
||||
|
||||
+2
-2
@@ -11,13 +11,13 @@
|
||||
<File Path="Directory.Build.Targets" />
|
||||
<File Path="Directory.Packages.props" />
|
||||
<File Path="docker-compose.yml" />
|
||||
<File Path="GitVersion.yml" />
|
||||
<File Path="global.json" />
|
||||
<File Path="README.md" />
|
||||
<File Path=".github\copilot-instructions.md" />
|
||||
<File Path=".github\git-commit-instructions.md" />
|
||||
</Folder>
|
||||
<Folder Name="/config/workflows/">
|
||||
<File Path=".github/workflows/integration-test.yml" />
|
||||
<File Path=".github/workflows/pr.yml" />
|
||||
<File Path=".github/workflows/release.yml" />
|
||||
</Folder>
|
||||
@@ -43,4 +43,4 @@
|
||||
<Project Path="tests/Speckle.Sdk.Serialization.Tests/Speckle.Sdk.Serialization.Tests.csproj" />
|
||||
<Project Path="tests/Speckle.Sdk.Tests.Unit/Speckle.Sdk.Tests.Unit.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
</Solution>
|
||||
|
||||
+8
-10
@@ -1,4 +1,3 @@
|
||||
using System.Text.Json;
|
||||
using GlobExpressions;
|
||||
using static Bullseye.Targets;
|
||||
using static SimpleExec.Command;
|
||||
@@ -16,14 +15,13 @@ const string CLEAN_LOCKS = "clean-locks";
|
||||
const string PERF = "perf";
|
||||
const string DEEP_CLEAN = "deep-clean";
|
||||
|
||||
static async Task<(string, string)> GetVersions()
|
||||
static (string semver, string fileVerison) GetVersions()
|
||||
{
|
||||
var (output, _) = await ReadAsync("dotnet", "dotnet-gitversion /output json").ConfigureAwait(false);
|
||||
output = output.Trim();
|
||||
var jDoc = JsonDocument.Parse(output);
|
||||
var version = jDoc.RootElement.GetProperty("FullSemVer").GetString() ?? "3.0.0-localBuild";
|
||||
var fileVersion = jDoc.RootElement.GetProperty("AssemblySemFileVer").GetString() ?? "3.0.0.0";
|
||||
return (version, fileVersion);
|
||||
string semver =
|
||||
Environment.GetEnvironmentVariable("SEMVER") ?? throw new ArgumentException("Expected SEMVER env var");
|
||||
string fileVersion =
|
||||
Environment.GetEnvironmentVariable("FILE_VERSION") ?? throw new ArgumentException("Expected FILE_VERSION env var");
|
||||
return (semver, fileVersion);
|
||||
}
|
||||
|
||||
Target(
|
||||
@@ -77,7 +75,7 @@ Target(
|
||||
dependsOn: [RESTORE],
|
||||
async () =>
|
||||
{
|
||||
var (version, fileVersion) = await GetVersions().ConfigureAwait(false);
|
||||
var (version, fileVersion) = GetVersions();
|
||||
Console.WriteLine($"Version: {version} & {fileVersion}");
|
||||
await RunAsync(
|
||||
"dotnet",
|
||||
@@ -174,7 +172,7 @@ Target(
|
||||
async () =>
|
||||
{
|
||||
{
|
||||
var (version, fileVersion) = await GetVersions().ConfigureAwait(false);
|
||||
var (version, fileVersion) = GetVersions();
|
||||
Console.WriteLine($"Version: {version} & {fileVersion}");
|
||||
await RunAsync("dotnet", $"pack Speckle.Sdk.sln -c Release -o output --no-build -p:Version={version}")
|
||||
.ConfigureAwait(false);
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
name: "speckle-server"
|
||||
|
||||
services:
|
||||
####
|
||||
# Speckle Server dependencies
|
||||
#######
|
||||
postgres:
|
||||
image: "postgres:16.4-alpine3.20@sha256:d898b0b78a2627cb4ee63464a14efc9d296884f1b28c841b0ab7d7c42f1fffdf"
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_DB: speckle
|
||||
POSTGRES_USER: speckle
|
||||
POSTGRES_PASSWORD: speckle
|
||||
volumes:
|
||||
- ./.volumes/postgres-data:/var/lib/postgresql/data/
|
||||
healthcheck:
|
||||
# the -U user has to match the POSTGRES_USER value
|
||||
test: ["CMD-SHELL", "pg_isready -U speckle"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
|
||||
redis:
|
||||
image: "valkey/valkey:8.1-alpine@sha256:0d27f0bca0249f61d060029a6aaf2e16b2c417d68d02a508e1dfb763fa2948b4"
|
||||
restart: always
|
||||
volumes:
|
||||
- ./.volumes/redis-data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
|
||||
minio:
|
||||
image: "minio/minio:RELEASE.2023-10-25T06-33-25Z"
|
||||
command: server /data --console-address ":9001"
|
||||
restart: always
|
||||
volumes:
|
||||
- ./.volumes/minio-data:/data
|
||||
ports:
|
||||
- '127.0.0.1:9000:9000'
|
||||
- '127.0.0.1:9001:9001'
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"curl -s -o /dev/null http://127.0.0.1:9000/minio/index.html",
|
||||
]
|
||||
interval: 5s
|
||||
timeout: 30s
|
||||
retries: 30
|
||||
start_period: 10s
|
||||
|
||||
speckle-server:
|
||||
image: ghcr.io/specklesystems/speckle-server:latest
|
||||
restart: always
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- /nodejs/bin/node
|
||||
- -e
|
||||
- "try { require('node:http').request({headers: {'Content-Type': 'application/json'}, port:3000, hostname:'127.0.0.1', path:'/readiness', method: 'GET', timeout: 2000 }, (res) => { body = ''; res.on('data', (chunk) => {body += chunk;}); res.on('end', () => {process.exit(Number(res.statusCode != 200 || body.toLowerCase().includes('error')));}); }).end(); } catch { process.exit(1); }"
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 90s
|
||||
ports:
|
||||
- "0.0.0.0:3000:3000"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
minio:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
# TODO: Change this to the URL of the speckle server, as accessed from the network
|
||||
CANONICAL_URL: "http://127.0.0.1:8080"
|
||||
SPECKLE_AUTOMATE_URL: "http://127.0.0.1:3030"
|
||||
FRONTEND_ORIGIN: "http://127.0.0.1:8081"
|
||||
|
||||
# TODO: Change thvolumes:
|
||||
REDIS_URL: "redis://redis"
|
||||
|
||||
S3_ENDPOINT: "http://minio:9000"
|
||||
S3_PUBLIC_ENDPOINT: "http://127.0.0.1:9000"
|
||||
S3_ACCESS_KEY: "minioadmin"
|
||||
S3_SECRET_KEY: "minioadmin"
|
||||
S3_BUCKET: "speckle-server"
|
||||
S3_CREATE_BUCKET: "true"
|
||||
|
||||
FILE_SIZE_LIMIT_MB: 100
|
||||
MAX_PROJECT_MODELS_PER_PAGE: 500
|
||||
|
||||
# TODO: Change this to a unique secret for this server
|
||||
SESSION_SECRET: "TODO:ReplaceWithLongString"
|
||||
|
||||
STRATEGY_LOCAL: "true"
|
||||
|
||||
POSTGRES_URL: "postgres"
|
||||
POSTGRES_USER: "speckle"
|
||||
POSTGRES_PASSWORD: "speckle"
|
||||
POSTGRES_DB: "speckle"
|
||||
ENABLE_MP: "false"
|
||||
|
||||
LOG_PRETTY: "true"
|
||||
|
||||
FF_NEXT_GEN_FILE_IMPORTER_ENABLED: "true"
|
||||
FF_LARGE_FILE_IMPORTS_ENABLED: "true"
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: speckle-server
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
redis-data:
|
||||
minio-data:
|
||||
+14
-9
@@ -1,4 +1,3 @@
|
||||
version: "3.9"
|
||||
name: "speckle-server"
|
||||
|
||||
services:
|
||||
@@ -13,7 +12,7 @@ services:
|
||||
POSTGRES_USER: speckle
|
||||
POSTGRES_PASSWORD: speckle
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data/
|
||||
- ./.volumes/postgres-data:/var/lib/postgresql/data/
|
||||
healthcheck:
|
||||
# the -U user has to match the POSTGRES_USER value
|
||||
test: ["CMD-SHELL", "pg_isready -U speckle"]
|
||||
@@ -22,10 +21,10 @@ services:
|
||||
retries: 30
|
||||
|
||||
redis:
|
||||
image: "redis:6.0-alpine"
|
||||
image: "valkey/valkey:8.1-alpine@sha256:0d27f0bca0249f61d060029a6aaf2e16b2c417d68d02a508e1dfb763fa2948b4"
|
||||
restart: always
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
- ./.volumes/redis-data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
|
||||
interval: 5s
|
||||
@@ -37,7 +36,10 @@ services:
|
||||
command: server /data --console-address ":9001"
|
||||
restart: always
|
||||
volumes:
|
||||
- minio-data:/data
|
||||
- ./.volumes/minio-data:/data
|
||||
ports:
|
||||
- '127.0.0.1:9000:9000'
|
||||
- '127.0.0.1:9001:9001'
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
@@ -57,7 +59,7 @@ services:
|
||||
- CMD
|
||||
- /nodejs/bin/node
|
||||
- -e
|
||||
- "try { require('node:http').request({headers: {'Content-Type': 'application/json'}, port:3000, hostname:'127.0.0.1', path:'/readiness', method: 'GET', timeout: 2000 }, (res) => { body = ''; res.on('data', (chunk) => {body += chunk;}); res.on('end', () => {process.exit(res.statusCode != 200 || body.toLowerCase().includes('error'));}); }).end(); } catch { process.exit(1); }"
|
||||
- "try { require('node:http').request({headers: {'Content-Type': 'application/json'}, port:3000, hostname:'127.0.0.1', path:'/readiness', method: 'GET', timeout: 2000 }, (res) => { body = ''; res.on('data', (chunk) => {body += chunk;}); res.on('end', () => {process.exit(Number(res.statusCode != 200 || body.toLowerCase().includes('error')));}); }).end(); } catch { process.exit(1); }"
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
@@ -81,6 +83,7 @@ services:
|
||||
REDIS_URL: "redis://redis"
|
||||
|
||||
S3_ENDPOINT: "http://minio:9000"
|
||||
S3_PUBLIC_ENDPOINT: "http://127.0.0.1:9000"
|
||||
S3_ACCESS_KEY: "minioadmin"
|
||||
S3_SECRET_KEY: "minioadmin"
|
||||
S3_BUCKET: "speckle-server"
|
||||
@@ -93,16 +96,18 @@ services:
|
||||
SESSION_SECRET: "TODO:ReplaceWithLongString"
|
||||
|
||||
STRATEGY_LOCAL: "true"
|
||||
DEBUG: "speckle:*"
|
||||
|
||||
POSTGRES_URL: "postgres"
|
||||
POSTGRES_USER: "speckle"
|
||||
POSTGRES_PASSWORD: "speckle"
|
||||
POSTGRES_DB: "speckle"
|
||||
ENABLE_MP: "false"
|
||||
|
||||
|
||||
LOG_PRETTY: "true"
|
||||
|
||||
FF_NEXT_GEN_FILE_IMPORTER_ENABLED: "true"
|
||||
FF_LARGE_FILE_IMPORTS_ENABLED: "true"
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: speckle-server
|
||||
@@ -110,4 +115,4 @@ networks:
|
||||
volumes:
|
||||
postgres-data:
|
||||
redis-data:
|
||||
minio-data:
|
||||
minio-data:
|
||||
|
||||
@@ -63,7 +63,7 @@ internal sealed class AutomationContext(IOperations operations) : IAutomationCon
|
||||
);
|
||||
}
|
||||
|
||||
Base? rootObject = await operations
|
||||
Base rootObject = await operations
|
||||
.Receive2(
|
||||
SpeckleClient.ServerUrl,
|
||||
AutomationRunData.ProjectId,
|
||||
@@ -74,6 +74,10 @@ internal sealed class AutomationContext(IOperations operations) : IAutomationCon
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await SpeckleClient
|
||||
.Version.Received(new(version.id, AutomationRunData.ProjectId, "automate_function"), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
Console.WriteLine($"It took {Elapsed.TotalSeconds} seconds to receive the speckle version {versionId}");
|
||||
return rootObject;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
using Speckle.Sdk.Models;
|
||||
|
||||
namespace Speckle.Objects.Geometry;
|
||||
|
||||
[SpeckleType("Objects.Geometry.SolidX")]
|
||||
public class SolidX : RawEncodedObject;
|
||||
@@ -2,6 +2,7 @@ using Speckle.Objects.Geometry;
|
||||
using Speckle.Objects.Other;
|
||||
using Speckle.Objects.Primitive;
|
||||
using Speckle.Sdk.Models;
|
||||
using Speckle.Sdk.Models.Data;
|
||||
|
||||
namespace Speckle.Objects;
|
||||
|
||||
@@ -110,15 +111,7 @@ public interface IDisplayValue<out T> : ISpeckleObject
|
||||
|
||||
#region Data objects
|
||||
|
||||
/// <summary>
|
||||
/// Specifies properties on objects to be used for data-based workflows
|
||||
/// </summary>
|
||||
public interface IProperties : ISpeckleObject
|
||||
{
|
||||
Dictionary<string, object?> properties { get; }
|
||||
}
|
||||
|
||||
public interface IDataObject : IProperties, IDisplayValue<IReadOnlyList<Base>>
|
||||
public interface IDataObject : IProperties, IDisplayValue<IReadOnlyList<Base>>, ISpeckleObject
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the object, primarily used to decorate the object for consumption in frontend and other apps
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
using Speckle.Objects.Geometry;
|
||||
using Speckle.Sdk.Models;
|
||||
|
||||
namespace Speckle.Objects.Other;
|
||||
|
||||
/// <summary>
|
||||
/// Camera class to represent a perspective camera for a 3D view.
|
||||
/// </summary>
|
||||
/// <remarks>Assumes a Z-up, right-handed convention for orientation vectors</remarks>
|
||||
[SpeckleType("Objects.Other.Camera")]
|
||||
public class Camera : Base
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the view that is created by this camera
|
||||
/// </summary>
|
||||
public required string name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The location of the camera
|
||||
/// </summary>
|
||||
public required Point position { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The unit up vector of the camera
|
||||
/// </summary>
|
||||
public required Vector up { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The unit forward vector of the camera
|
||||
/// </summary>
|
||||
public required Vector forward { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Speckle.Objects.Data;
|
||||
using Speckle.Sdk.Models;
|
||||
using Speckle.Sdk.Models.Proxies;
|
||||
|
||||
namespace Speckle.Objects.Other;
|
||||
|
||||
/// <summary>
|
||||
/// Proxy for levels as DataObject value.
|
||||
/// <remarks> These proxy lives in Objects library because it depends on DataObject</remarks>
|
||||
/// </summary>
|
||||
[SpeckleType("Objects.Other.LevelProxy")]
|
||||
public class LevelProxy : Base, IProxyCollection
|
||||
{
|
||||
/// <summary>
|
||||
/// The list of application ids of objects that use this level
|
||||
/// </summary>
|
||||
public required List<string> objects { get; set; }
|
||||
|
||||
public required DataObject value { get; set; }
|
||||
}
|
||||
@@ -20,4 +20,6 @@ public class RawEncoding : Base // note: at this stage, since we're using this f
|
||||
public static class RawEncodingFormats
|
||||
{
|
||||
public const string RHINO_3DM = "3dm";
|
||||
public const string ACAD_DWG = "dwg";
|
||||
public const string ACAD_SAT = "sat";
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System.Drawing;
|
||||
using Speckle.Newtonsoft.Json;
|
||||
using Speckle.Sdk.Models;
|
||||
using Speckle.Sdk.Models.Proxies;
|
||||
|
||||
namespace Speckle.Objects.Other;
|
||||
|
||||
@@ -36,23 +35,6 @@ public class RenderMaterial : Base
|
||||
public Color emissiveColor
|
||||
{
|
||||
get => Color.FromArgb(emissive);
|
||||
set => diffuse = value.ToArgb();
|
||||
set => emissive = value.ToArgb();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to store render material to object relationships in root collections
|
||||
/// </summary>
|
||||
[SpeckleType("Objects.Other.RenderMaterialProxy")]
|
||||
public class RenderMaterialProxy : Base, IProxyCollection
|
||||
{
|
||||
/// <summary>
|
||||
/// The list of application ids of objects that use this render material
|
||||
/// </summary>
|
||||
public required List<string> objects { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The render material used by <see cref="objects"/>
|
||||
/// </summary>
|
||||
public required RenderMaterial value { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
using Speckle.Sdk.Models;
|
||||
using Speckle.Sdk.Models.Proxies;
|
||||
|
||||
namespace Speckle.Objects.Other;
|
||||
|
||||
/// <summary>
|
||||
/// Used to store render material to object relationships in root collections
|
||||
/// <remarks> These proxy lives in Objects library because it depends on RenderMaterial</remarks>
|
||||
/// </summary>
|
||||
[SpeckleType("Objects.Other.RenderMaterialProxy")]
|
||||
public class RenderMaterialProxy : Base, IProxyCollection
|
||||
{
|
||||
/// <summary>
|
||||
/// The list of application ids of objects that use this render material
|
||||
/// </summary>
|
||||
public required List<string> objects { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The render material used by <see cref="objects"/>
|
||||
/// </summary>
|
||||
public required RenderMaterial value { get; set; }
|
||||
}
|
||||
@@ -22,23 +22,4 @@ public static class Collections
|
||||
public static class EnumerableExtensions
|
||||
{
|
||||
public static IEnumerable<int> RangeFrom(int from, int to) => Enumerable.Range(from, to - from + 1);
|
||||
|
||||
#if NETSTANDARD2_0
|
||||
public static IEnumerable<TSource> DistinctBy<TSource, TKey>(
|
||||
this IEnumerable<TSource> source,
|
||||
Func<TSource, TKey> keySelector
|
||||
)
|
||||
{
|
||||
var keys = new HashSet<TKey>();
|
||||
foreach (var element in source)
|
||||
{
|
||||
if (keys.Contains(keySelector(element)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
keys.Add(keySelector(element));
|
||||
yield return element;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Speckle.Sdk.Dependencies.Serialization;
|
||||
|
||||
public abstract class ChannelLoader<T>(CancellationToken cancellationToken)
|
||||
{
|
||||
private const int RECEIVE_CAPACITY = 5000;
|
||||
private const int RECEIVE_CAPACITY = 10000;
|
||||
|
||||
private const int HTTP_GET_CHUNK_SIZE = 500;
|
||||
private const int MAX_PARALLELISM_HTTP = 4;
|
||||
@@ -109,6 +109,9 @@ public abstract class ChannelLoader<T>(CancellationToken cancellationToken)
|
||||
Exception = ex;
|
||||
_channel.Writer.TryComplete(ex);
|
||||
//cancel everything!
|
||||
_cts.Cancel();
|
||||
if (!_cts.IsCancellationRequested)
|
||||
{
|
||||
_cts.Cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,13 @@ namespace Speckle.Sdk.Dependencies.Serialization;
|
||||
public abstract class ChannelSaver<T>
|
||||
where T : IHasByteSize
|
||||
{
|
||||
private const int SEND_CAPACITY = 1000;
|
||||
private const int SEND_CAPACITY = 10000;
|
||||
private const int HTTP_SEND_CHUNK_SIZE = 25_000_000; //bytes
|
||||
private static readonly TimeSpan HTTP_BATCH_TIMEOUT = TimeSpan.FromSeconds(2);
|
||||
private const int MAX_PARALLELISM_HTTP = 4;
|
||||
private const int HTTP_CAPACITY = 500;
|
||||
private const int MAX_CACHE_WRITE_PARALLELISM = 4;
|
||||
private const int MAX_CACHE_BATCH = 500;
|
||||
private const int MAX_CACHE_WRITE_PARALLELISM = 1;
|
||||
private const int MAX_CACHE_BATCH = 1000;
|
||||
|
||||
private readonly Channel<T> _checkCacheChannel = Channel.CreateBounded<T>(
|
||||
new BoundedChannelOptions(SEND_CAPACITY)
|
||||
@@ -45,9 +45,9 @@ public abstract class ChannelSaver<T>
|
||||
cancellationToken
|
||||
)
|
||||
.Join()
|
||||
.Batch(cacheBatchSize ?? MAX_CACHE_BATCH)
|
||||
.Batch(cacheBatchSize ?? MAX_CACHE_BATCH, singleReader: true)
|
||||
.WithTimeout(HTTP_BATCH_TIMEOUT)
|
||||
.ReadAllConcurrently(maxParallelism ?? MAX_CACHE_WRITE_PARALLELISM, SaveToCache, cancellationToken)
|
||||
.ReadAllConcurrently(MAX_CACHE_WRITE_PARALLELISM, SaveToCache, cancellationToken)
|
||||
.ContinueWith(
|
||||
t =>
|
||||
{
|
||||
|
||||
@@ -41,13 +41,7 @@ internal sealed class SpeckleHttpClientHandler : DelegatingHandler
|
||||
activity?.InjectHeaders((k, v) => request.Headers.TryAddWithoutValidation(k, v));
|
||||
|
||||
var policyResult = await _resiliencePolicy
|
||||
.ExecuteAndCaptureAsync(
|
||||
ctx =>
|
||||
{
|
||||
return base.SendAsync(request, cancellationToken);
|
||||
},
|
||||
context
|
||||
)
|
||||
.ExecuteAndCaptureAsync(ctx => base.SendAsync(request, cancellationToken), context)
|
||||
.ConfigureAwait(false);
|
||||
context.TryGetValue("retryCount", out var retryCount);
|
||||
activity?.SetTag("retryCount", retryCount);
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using Speckle.InterfaceGenerator;
|
||||
using Speckle.Newtonsoft.Json;
|
||||
using Speckle.Sdk.Api.GraphQL.Resources;
|
||||
using Speckle.Sdk.Common;
|
||||
using Speckle.Sdk.Credentials;
|
||||
using Speckle.Sdk.Helpers;
|
||||
using Speckle.Sdk.Logging;
|
||||
using Speckle.Sdk.Transports;
|
||||
using Speckle.Sdk.Transports.ServerUtils;
|
||||
|
||||
namespace Speckle.Sdk.Api.Blob;
|
||||
|
||||
public partial interface IBlobApi : IDisposable;
|
||||
|
||||
/// <summary>
|
||||
/// Low level access to the blob API
|
||||
/// </summary>
|
||||
/// <seealso cref="FileImportResource"/>
|
||||
/// <seealso cref="ServerApi"/>
|
||||
[GenerateAutoInterface]
|
||||
public sealed class BlobApi : IBlobApi
|
||||
{
|
||||
public const int DEFAULT_TIMEOUT_SECONDS = SpeckleHttp.DEFAULT_TIMEOUT_SECONDS;
|
||||
private static readonly string[] s_filenameSeparator = ["filename="];
|
||||
|
||||
private readonly ISdkActivityFactory _activityFactory;
|
||||
|
||||
/// <summary>
|
||||
/// HTTP client for communicating with Speckle Server with auth token header
|
||||
/// </summary>
|
||||
private readonly HttpClient _authedClient;
|
||||
|
||||
/// <summary>
|
||||
/// HTTP client for communicating with pre-signed s3 url
|
||||
/// </summary>
|
||||
private readonly HttpClient _unauthedClient;
|
||||
|
||||
public BlobApi(
|
||||
ISpeckleHttp speckleHttp,
|
||||
ISdkActivityFactory activityFactory,
|
||||
Account account,
|
||||
int timeoutSeconds = DEFAULT_TIMEOUT_SECONDS
|
||||
)
|
||||
{
|
||||
_activityFactory = activityFactory;
|
||||
_authedClient = speckleHttp.CreateHttpClient(
|
||||
new HttpClientHandler { AutomaticDecompression = DecompressionMethods.GZip },
|
||||
timeoutSeconds: timeoutSeconds,
|
||||
authorizationToken: account.token
|
||||
);
|
||||
_authedClient.BaseAddress = new(account.serverInfo.url);
|
||||
|
||||
_unauthedClient = speckleHttp.CreateHttpClient(
|
||||
new HttpClientHandler { AutomaticDecompression = DecompressionMethods.GZip },
|
||||
timeoutSeconds: timeoutSeconds
|
||||
);
|
||||
}
|
||||
|
||||
private static string GetBlobDownloadPath(string blobId, HttpResponseMessage response)
|
||||
{
|
||||
response.Content.Headers.TryGetValues("Content-Disposition", out IEnumerable<string>? cdHeaderValues);
|
||||
var cdHeader = (cdHeaderValues?.FirstOrDefault()).NotNull(
|
||||
"Expected response from server to contain attachment header"
|
||||
);
|
||||
string fileName = cdHeader.Split(s_filenameSeparator, StringSplitOptions.None)[1].TrimStart('"').TrimEnd('"');
|
||||
return Path.Combine(
|
||||
SpecklePathProvider.BlobStoragePath(),
|
||||
$"{blobId[..Models.Blob.LocalHashPrefixLength]}-{fileName}"
|
||||
);
|
||||
}
|
||||
|
||||
/// <param name="blobId">The ID of the blob to download</param>
|
||||
/// <param name="progress"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <exception cref="HttpRequestException">Request for the blob fails</exception>
|
||||
/// <exception cref="OperationCanceledException"></exception>
|
||||
/// <returns>File Path of the downloaded file</returns>
|
||||
public async Task<string> DownloadBlob(
|
||||
string projectId,
|
||||
string blobId,
|
||||
string? pathOverride = null,
|
||||
IProgress<ProgressArgs>? progress = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
using var _ = _activityFactory.Start();
|
||||
|
||||
var url = new Uri($"api/stream/{projectId}/blob/{blobId}", UriKind.Relative);
|
||||
|
||||
using var response = await _authedClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
string fileLocation = pathOverride ?? GetBlobDownloadPath(blobId, response);
|
||||
using var source = new ProgressStream(
|
||||
#if NET5_0_OR_GREATER
|
||||
await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false),
|
||||
#else
|
||||
await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
|
||||
#endif
|
||||
response.Content.Headers.ContentLength,
|
||||
progress,
|
||||
true
|
||||
);
|
||||
|
||||
using var fs = new FileStream(fileLocation, FileMode.OpenOrCreate);
|
||||
#if NET5_0_OR_GREATER
|
||||
await source.CopyToAsync(fs, cancellationToken).ConfigureAwait(false);
|
||||
#else
|
||||
await source.CopyToAsync(fs).ConfigureAwait(false);
|
||||
#endif
|
||||
return fileLocation;
|
||||
}
|
||||
|
||||
/// <summary>Queries the server for diff of the given <paramref name="blobIds"/></summary>
|
||||
/// <param name="blobIds"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns>A list of blob ids that the server doesn't have</returns>
|
||||
/// <exception cref="HttpRequestException">Request for the blob fails</exception>
|
||||
/// <exception cref="OperationCanceledException"></exception>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
public async Task<List<string>> HasBlobs(
|
||||
string projectId,
|
||||
IReadOnlyCollection<string> blobIds,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
using var _ = _activityFactory.Start();
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var payload = JsonConvert.SerializeObject(blobIds);
|
||||
|
||||
var url = new Uri($"/api/stream/{projectId}/blob/diff", UriKind.Relative);
|
||||
|
||||
using StringContent stringContent = new(payload, Encoding.UTF8, "application/json");
|
||||
|
||||
using var response = await _authedClient.PostAsync(url, stringContent, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
#if NET5_0_OR_GREATER
|
||||
var responseString = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
#else
|
||||
var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
#endif
|
||||
var parsed = JsonConvert
|
||||
.DeserializeObject<List<string>>(responseString)
|
||||
.NotNull($"Failed to deserialize successful response {response.Content}");
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uploads a single file to the given S3 url.
|
||||
/// This method should be used together with the <see cref="FileImportResource"/> <see cref="FileImportResource.GenerateUploadUrl"/> method,
|
||||
/// which generates a pre-signed S3 url, that can be used to upload the file to.
|
||||
/// </summary>
|
||||
/// <param name="filePath"></param>
|
||||
/// <param name="url"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns>etag header</returns>
|
||||
/// <seealso cref="FileImportResource"/>
|
||||
/// <exception cref="HttpRequestException"></exception>
|
||||
/// <exception cref="ArgumentException">Unexpected response header the server</exception>
|
||||
/// <exception cref="FileNotFoundException"><paramref name="filePath"/> does not point to a file</exception>
|
||||
/// <exception cref="OperationCanceledException"></exception>
|
||||
public async Task<string> UploadFile(
|
||||
string filePath,
|
||||
Uri url,
|
||||
IProgress<ProgressArgs>? progress = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
using var _ = _activityFactory.Start();
|
||||
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
throw new FileNotFoundException("File not found.", filePath);
|
||||
}
|
||||
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
|
||||
using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
|
||||
using var requestMessage = new HttpRequestMessage(HttpMethod.Put, url);
|
||||
requestMessage.Content = progress is null
|
||||
? new StreamContent(fileStream)
|
||||
: new ProgressContent(new StreamContent(fileStream), progress);
|
||||
|
||||
requestMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
|
||||
requestMessage.Content.Headers.ContentLength = fileInfo.Length;
|
||||
|
||||
using var response = await _unauthedClient.SendAsync(requestMessage, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
return ParseEtagHeader(response.Headers);
|
||||
}
|
||||
|
||||
private static string ParseEtagHeader(HttpResponseHeaders headers)
|
||||
{
|
||||
if (!headers.TryGetValues("ETag", out var etagValues))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Response does not have an ETag attached to it, cannot use this as an upload",
|
||||
nameof(headers)
|
||||
);
|
||||
}
|
||||
|
||||
var etagValuesArray = etagValues.ToArray();
|
||||
|
||||
if (etagValuesArray.Length != 1)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Expected Etag header to have a single value but got {etagValuesArray.Length}",
|
||||
nameof(headers)
|
||||
);
|
||||
}
|
||||
|
||||
return etagValuesArray[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uploads blobs via the <c>"/api/stream/:streamId/blob"</c> endpoint
|
||||
/// </summary>
|
||||
/// <param name="projectId"></param>
|
||||
/// <param name="blobPaths"></param>
|
||||
/// <param name="progress"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
public async Task UploadBlobs(
|
||||
string projectId,
|
||||
IReadOnlyCollection<(string id, string filePath)> blobPaths,
|
||||
IProgress<ProgressArgs>? progress,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
using var _ = _activityFactory.Start();
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (blobPaths.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var multipartFormDataContent = new MultipartFormDataContent();
|
||||
foreach (var (id, filePath) in blobPaths)
|
||||
{
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
|
||||
var stream = File.OpenRead(filePath);
|
||||
var fsc = new StreamContent(stream);
|
||||
multipartFormDataContent.Add(fsc, $"hash:{id}", fileName);
|
||||
}
|
||||
|
||||
using HttpContent content = progress is null
|
||||
? multipartFormDataContent
|
||||
: new ProgressContent(multipartFormDataContent, progress);
|
||||
|
||||
var url = new Uri($"/api/stream/{projectId}/blob", UriKind.Relative);
|
||||
|
||||
using var response = await _authedClient.PostAsync(url, content, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
[AutoInterfaceIgnore]
|
||||
public void Dispose()
|
||||
{
|
||||
_authedClient.Dispose();
|
||||
_unauthedClient.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Speckle.InterfaceGenerator;
|
||||
using Speckle.Sdk.Credentials;
|
||||
using Speckle.Sdk.Helpers;
|
||||
using Speckle.Sdk.Logging;
|
||||
|
||||
namespace Speckle.Sdk.Api.Blob;
|
||||
|
||||
[GenerateAutoInterface]
|
||||
public sealed class BlobApiFactory(ISpeckleHttp speckleHttp, ISdkActivityFactory activityFactory) : IBlobApiFactory
|
||||
{
|
||||
public IBlobApi Create(Account account, int timeoutSeconds = BlobApi.DEFAULT_TIMEOUT_SECONDS) =>
|
||||
new BlobApi(speckleHttp, activityFactory, account, timeoutSeconds);
|
||||
}
|
||||
@@ -23,10 +23,15 @@ public class SpeckleGraphQLException : SpeckleException
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a "FORBIDDEN" or "UNAUTHORIZED" GraphQL error as an exception.
|
||||
/// Represents a "FORBIDDEN" or "UNAUTHENTICATED" or "UNAUTHORIZED" or "UNAUTHORIZED_ACCESS_ERROR" GraphQL error as an exception.
|
||||
/// https://www.apollographql.com/docs/apollo-server/v2/data/errors/#unauthenticated
|
||||
/// https://www.apollographql.com/docs/apollo-server/v2/data/errors/#forbidden
|
||||
/// https://github.com/specklesystems/speckle-server/blob/v2.23.18/packages/server/modules/shared/errors/index.ts#L34
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Server is a bit inconsistent with these error codes, hence there's 4 different codes that mean "auth no work"
|
||||
/// Apollo no longer considers "FORBIDDEN" or "UNAUTHENTICATED" as built in error codes, so everything is custom anyway.
|
||||
/// </remarks>
|
||||
public sealed class SpeckleGraphQLForbiddenException : SpeckleGraphQLException
|
||||
{
|
||||
public SpeckleGraphQLForbiddenException() { }
|
||||
@@ -126,3 +131,14 @@ public sealed class WorkspacePermissionException : SpeckleGraphQLException
|
||||
public WorkspacePermissionException(string? message, Exception? innerException)
|
||||
: base(message, innerException) { }
|
||||
}
|
||||
|
||||
public sealed class CannotCreateCommitException : SpeckleGraphQLException
|
||||
{
|
||||
public CannotCreateCommitException() { }
|
||||
|
||||
public CannotCreateCommitException(string? message)
|
||||
: base(message) { }
|
||||
|
||||
public CannotCreateCommitException(string? message, Exception? innerException)
|
||||
: base(message, innerException) { }
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using GraphQL.Client.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.InterfaceGenerator;
|
||||
using Speckle.Newtonsoft.Json;
|
||||
using Speckle.Sdk.Api.Blob;
|
||||
using Speckle.Sdk.Api.GraphQL;
|
||||
using Speckle.Sdk.Api.GraphQL.Resources;
|
||||
using Speckle.Sdk.Credentials;
|
||||
@@ -33,6 +34,8 @@ public sealed class Client : ISpeckleGraphQLClient, IClient
|
||||
public SubscriptionResource Subscription { get; }
|
||||
public WorkspaceResource Workspace { get; }
|
||||
public ServerResource Server { get; }
|
||||
public FileImportResource FileImport { get; }
|
||||
public ModelIngestionResource Ingestion { get; }
|
||||
|
||||
public Uri ServerUrl => new(Account.serverInfo.url);
|
||||
|
||||
@@ -48,12 +51,15 @@ public sealed class Client : ISpeckleGraphQLClient, IClient
|
||||
ILogger<Client> logger,
|
||||
ISdkActivityFactory activityFactory,
|
||||
IGraphQLClientFactory graphqlClientFactory,
|
||||
Account account
|
||||
IBlobApiFactory blobApiFactory,
|
||||
[NotNull] Account? account
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_activityFactory = activityFactory;
|
||||
|
||||
Account = account ?? throw new ArgumentException("Provided account is null.");
|
||||
GQLClient = graphqlClientFactory.CreateGraphQLClient(account);
|
||||
|
||||
Project = new(this);
|
||||
Model = new(this);
|
||||
@@ -65,8 +71,8 @@ public sealed class Client : ISpeckleGraphQLClient, IClient
|
||||
Subscription = new(this);
|
||||
Workspace = new(this);
|
||||
Server = new(this);
|
||||
|
||||
GQLClient = graphqlClientFactory.CreateGraphQLClient(account);
|
||||
FileImport = new(this, blobApiFactory.Create(account));
|
||||
Ingestion = new(this);
|
||||
}
|
||||
|
||||
[AutoInterfaceIgnore]
|
||||
@@ -74,6 +80,7 @@ public sealed class Client : ISpeckleGraphQLClient, IClient
|
||||
{
|
||||
try
|
||||
{
|
||||
FileImport.Dispose();
|
||||
Subscription.Dispose();
|
||||
GQLClient.Dispose();
|
||||
}
|
||||
@@ -127,10 +134,10 @@ public sealed class Client : ISpeckleGraphQLClient, IClient
|
||||
activity?.SetStatus(SdkActivityStatusCode.Ok);
|
||||
return ret;
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
activity?.SetStatus(SdkActivityStatusCode.Error);
|
||||
activity?.RecordException(ex);
|
||||
// Don't record exception as it's rethrown.
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.InterfaceGenerator;
|
||||
using Speckle.Sdk.Api.Blob;
|
||||
using Speckle.Sdk.Credentials;
|
||||
using Speckle.Sdk.Logging;
|
||||
|
||||
@@ -9,9 +10,10 @@ namespace Speckle.Sdk.Api;
|
||||
public class ClientFactory(
|
||||
ILoggerFactory loggerFactory,
|
||||
ISdkActivityFactory activityFactory,
|
||||
IGraphQLClientFactory graphQLClientFactory
|
||||
IGraphQLClientFactory graphQLClientFactory,
|
||||
IBlobApiFactory blobApiFactory
|
||||
) : IClientFactory
|
||||
{
|
||||
public IClient Create(Account account) =>
|
||||
new Client(loggerFactory.CreateLogger<Client>(), activityFactory, graphQLClientFactory, account);
|
||||
new Client(loggerFactory.CreateLogger<Client>(), activityFactory, graphQLClientFactory, blobApiFactory, account);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||
// ReSharper disable InconsistentNaming
|
||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||
|
||||
//This enum isn't explicitly defined in the schema, instead its usages are int typed (But represent an enum)
|
||||
/// <remarks>
|
||||
/// This enum isn't explicitly defined in the schema, instead its usages are int typed (But represent an enum)
|
||||
/// </remarks>
|
||||
public enum FileUploadConversionStatus
|
||||
{
|
||||
Queued = 0,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// ReSharper disable InconsistentNaming
|
||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||
|
||||
/// <remarks>
|
||||
/// string based enum
|
||||
/// </remarks>
|
||||
public enum ModelIngestionStatus
|
||||
{
|
||||
cancelled,
|
||||
failed,
|
||||
processing,
|
||||
queued,
|
||||
success,
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||
|
||||
/// <remarks>
|
||||
/// string based enum
|
||||
/// </remarks>
|
||||
public enum ProjectCommentsUpdatedMessageType
|
||||
{
|
||||
ARCHIVED,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||
|
||||
/// <remarks>
|
||||
/// string based enum
|
||||
/// </remarks>
|
||||
public enum ProjectFileImportUpdatedMessageType
|
||||
{
|
||||
CREATED,
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// ReSharper disable InconsistentNaming
|
||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||
|
||||
/// <remarks>
|
||||
/// string based enum
|
||||
/// </remarks>
|
||||
public enum ProjectModelIngestionUpdatedMessageType
|
||||
{
|
||||
cancellationRequested,
|
||||
created,
|
||||
deleted,
|
||||
updated,
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||
|
||||
/// <remarks>
|
||||
/// string based enum
|
||||
/// </remarks>
|
||||
public enum ProjectModelsUpdatedMessageType
|
||||
{
|
||||
CREATED,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||
|
||||
/// <remarks>
|
||||
/// string based enum
|
||||
/// </remarks>
|
||||
public enum ProjectPendingModelsUpdatedMessageType
|
||||
{
|
||||
CREATED,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||
|
||||
/// <remarks>
|
||||
/// string based enum
|
||||
/// </remarks>
|
||||
public enum ProjectUpdatedMessageType
|
||||
{
|
||||
DELETED,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||
|
||||
/// <remarks>
|
||||
/// string based enum
|
||||
/// </remarks>
|
||||
public enum ProjectVersionsUpdatedMessageType
|
||||
{
|
||||
CREATED,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
// ReSharper disable InconsistentNaming
|
||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||
|
||||
/// <remarks>
|
||||
/// string based enum
|
||||
/// </remarks>
|
||||
public enum ProjectVisibility
|
||||
{
|
||||
Private,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||
// ReSharper disable InconsistentNaming
|
||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||
|
||||
/// <remarks>
|
||||
/// string based enum
|
||||
/// </remarks>
|
||||
public enum ResourceType
|
||||
{
|
||||
commit,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Enums;
|
||||
|
||||
/// <remarks>
|
||||
/// string based enum
|
||||
/// </remarks>
|
||||
public enum UserProjectsUpdatedMessageType
|
||||
{
|
||||
ADDED,
|
||||
|
||||
@@ -28,11 +28,13 @@ internal static class GraphQLErrorHandler
|
||||
var ex = code switch
|
||||
{
|
||||
"GRAPHQL_PARSE_FAILED" or "GRAPHQL_VALIDATION_FAILED" => new SpeckleGraphQLInvalidQueryException(message),
|
||||
"FORBIDDEN" or "UNAUTHENTICATED" => new SpeckleGraphQLForbiddenException(message),
|
||||
"FORBIDDEN" or "UNAUTHENTICATED" or "UNAUTHORIZED" or "UNAUTHORIZED_ACCESS_ERROR" =>
|
||||
new SpeckleGraphQLForbiddenException(message),
|
||||
"STREAM_NOT_FOUND" => new SpeckleGraphQLStreamNotFoundException(message),
|
||||
"BAD_USER_INPUT" => new SpeckleGraphQLBadInputException(message),
|
||||
"INTERNAL_SERVER_ERROR" => new SpeckleGraphQLInternalErrorException(message),
|
||||
"WORKSPACES_MODULE_DISABLED_ERROR" => new SpeckleGraphQLWorkspaceNotEnabledException(message),
|
||||
"COMMIT_CREATE_ERROR" => new CannotCreateCommitException(message),
|
||||
_ => new SpeckleGraphQLException(message),
|
||||
};
|
||||
exceptions.Add(ex);
|
||||
|
||||
@@ -40,7 +40,8 @@ public static class GraphQLHttpClientExtensions
|
||||
response.EnsureGraphQLSuccess();
|
||||
|
||||
string versionString = response.Data.data.data;
|
||||
if (versionString == "dev")
|
||||
//Local server builds will have a non-numerical version string
|
||||
if (versionString == "dev" || versionString == "custom")
|
||||
{
|
||||
return new Version(999, 999, 999);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Inputs;
|
||||
|
||||
public record GenerateFileUploadUrlInput(string projectId, string fileName);
|
||||
|
||||
public record StartFileImportInput(string projectId, string modelId, string fileId, string etag);
|
||||
|
||||
public record FileImportResult(
|
||||
double durationSeconds,
|
||||
double downloadDurationSeconds,
|
||||
double parseDurationSeconds,
|
||||
string parser,
|
||||
string? versionId
|
||||
);
|
||||
|
||||
public abstract class FileImportInputBase
|
||||
{
|
||||
public required string projectId { get; init; }
|
||||
public required string jobId { get; init; }
|
||||
public required IReadOnlyCollection<string> warnings { get; init; }
|
||||
public required FileImportResult result { get; init; }
|
||||
}
|
||||
|
||||
#pragma warning disable CA1822 //Mark members as static
|
||||
|
||||
public sealed class FileImportSuccessInput() : FileImportInputBase()
|
||||
{
|
||||
public const string TYPE_STATUS = "success";
|
||||
|
||||
public string status => TYPE_STATUS;
|
||||
}
|
||||
|
||||
public sealed class FileImportErrorInput() : FileImportInputBase()
|
||||
{
|
||||
public const string TYPE_STATUS = "error";
|
||||
|
||||
public string status => TYPE_STATUS;
|
||||
public required string reason { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using Speckle.Newtonsoft.Json;
|
||||
using Speckle.Sdk.Api.GraphQL.Enums;
|
||||
|
||||
namespace Speckle.Sdk.Api.GraphQL.Inputs;
|
||||
|
||||
public record SourceDataInput(
|
||||
string sourceApplicationSlug,
|
||||
string sourceApplicationVersion,
|
||||
string? fileName,
|
||||
long? fileSizeBytes
|
||||
);
|
||||
|
||||
public record ModelIngestionCreateInput(
|
||||
string modelId,
|
||||
string projectId,
|
||||
string progressMessage,
|
||||
SourceDataInput sourceData
|
||||
);
|
||||
|
||||
public record ModelIngestionUpdateInput(string ingestionId, string projectId, string progressMessage, double? progress);
|
||||
|
||||
public record ModelIngestionSuccessInput(string ingestionId, string projectId, string rootObjectId);
|
||||
|
||||
public record ModelIngestionFailedInput(
|
||||
string ingestionId,
|
||||
string projectId,
|
||||
string errorReason,
|
||||
string? errorStacktrace
|
||||
)
|
||||
{
|
||||
public static ModelIngestionFailedInput FromException(string ingestionId, string projectId, Exception ex)
|
||||
{
|
||||
return new ModelIngestionFailedInput(ingestionId, projectId, ex.Message, ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
public record ModelIngestionCancelledInput(string ingestionId, string projectId, string cancellationMessage);
|
||||
|
||||
public record ModelIngestionStartProcessingInput(
|
||||
string ingestionId,
|
||||
string projectId,
|
||||
string progressMessage,
|
||||
SourceDataInput sourceData
|
||||
);
|
||||
|
||||
public record ModelIngestionRequeueInput(string ingestionId, string projectId, string progressMessage);
|
||||
|
||||
public record ProjectModelIngestionSubscriptionInput(
|
||||
string projectId,
|
||||
ModelIngestionReference ingestionReference,
|
||||
[property: JsonIgnore] ProjectModelIngestionUpdatedMessageType messageType
|
||||
)
|
||||
{
|
||||
// The Newtonsoft serializer is setup to handle SCREAMING_CASE enums.
|
||||
// But the API requires the enum to look exactly like they are
|
||||
[JsonProperty(nameof(messageType))]
|
||||
public string serializedType => messageType.ToString();
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// <c>@oneOf</c> i.e. server expects <b>either</b> <paramref name="ingestionId"/> or <paramref name="modelId"/>, but not both.
|
||||
/// </remarks>
|
||||
/// <param name="ingestionId"></param>
|
||||
/// <param name="modelId"></param>
|
||||
public record ModelIngestionReference(string? ingestionId, string? modelId);
|
||||
@@ -14,7 +14,7 @@ public sealed class Comment
|
||||
public string rawText { get; init; }
|
||||
public ResourceCollection<Comment> replies { get; init; }
|
||||
public CommentReplyAuthorCollection replyAuthors { get; init; }
|
||||
public List<ResourceIdentifier> resources { get; init; }
|
||||
public List<ResourceIdentifier> resources { get; init; } //todo: add resourceIds/baseResourceIds
|
||||
public string? screenshot { get; init; }
|
||||
public DateTime updatedAt { get; init; }
|
||||
public DateTime? viewedAt { get; init; }
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Models;
|
||||
|
||||
public sealed class FileImport
|
||||
{
|
||||
public string id { get; init; }
|
||||
public string projectId { get; init; }
|
||||
public string? convertedVersionId { get; init; }
|
||||
public string userId { get; init; }
|
||||
public int convertedStatus { get; init; }
|
||||
public string? convertedMessage { get; init; }
|
||||
public string? modelId { get; init; }
|
||||
public DateTime updatedAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Models;
|
||||
|
||||
public sealed class FileUploadUrl
|
||||
{
|
||||
public Uri url { get; init; }
|
||||
public string fileId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Models;
|
||||
|
||||
public sealed class ModelIngestion
|
||||
{
|
||||
public required string id { get; init; }
|
||||
public required DateTime createdAt { get; init; }
|
||||
public required DateTime updatedAt { get; init; }
|
||||
public required string modelId { get; init; }
|
||||
public required bool cancellationRequested { get; init; }
|
||||
public required ModelIngestionStatusData statusData { get; init; }
|
||||
// public required LimitedUser user { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Speckle.Sdk.Api.GraphQL.Enums;
|
||||
|
||||
namespace Speckle.Sdk.Api.GraphQL.Models;
|
||||
|
||||
public sealed class ModelIngestionStatusData
|
||||
{
|
||||
public required ModelIngestionStatus status { get; init; }
|
||||
public required string? progressMessage { get; init; }
|
||||
}
|
||||
@@ -19,6 +19,9 @@ public sealed class ServerInfo
|
||||
[Obsolete("Don't use")]
|
||||
public bool frontend2 { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// The URL that should be used to talk with the server
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This field is not returned from the GQL API,
|
||||
/// it should be populated after construction.
|
||||
|
||||
@@ -6,10 +6,10 @@ namespace Speckle.Sdk.Api.GraphQL.Models;
|
||||
public sealed class UserProjectsUpdatedMessage : EventArgs
|
||||
{
|
||||
[JsonRequired]
|
||||
public string id { get; init; }
|
||||
public required string id { get; init; }
|
||||
|
||||
[JsonRequired]
|
||||
public UserProjectsUpdatedMessageType type { get; init; }
|
||||
public required UserProjectsUpdatedMessageType type { get; init; }
|
||||
|
||||
public Project? project { get; init; }
|
||||
}
|
||||
@@ -17,10 +17,10 @@ public sealed class UserProjectsUpdatedMessage : EventArgs
|
||||
public sealed class ProjectCommentsUpdatedMessage : EventArgs
|
||||
{
|
||||
[JsonRequired]
|
||||
public string id { get; init; }
|
||||
public required string id { get; init; }
|
||||
|
||||
[JsonRequired]
|
||||
public ProjectCommentsUpdatedMessageType type { get; init; }
|
||||
public required ProjectCommentsUpdatedMessageType type { get; init; }
|
||||
|
||||
public Comment? comment { get; init; }
|
||||
}
|
||||
@@ -28,10 +28,10 @@ public sealed class ProjectCommentsUpdatedMessage : EventArgs
|
||||
public sealed class ProjectFileImportUpdatedMessage : EventArgs
|
||||
{
|
||||
[JsonRequired]
|
||||
public string id { get; init; }
|
||||
public required string id { get; init; }
|
||||
|
||||
[JsonRequired]
|
||||
public ProjectFileImportUpdatedMessageType type { get; init; }
|
||||
public required ProjectFileImportUpdatedMessageType type { get; init; }
|
||||
|
||||
public FileUpload? upload { get; init; }
|
||||
}
|
||||
@@ -39,10 +39,10 @@ public sealed class ProjectFileImportUpdatedMessage : EventArgs
|
||||
public sealed class ProjectModelsUpdatedMessage : EventArgs
|
||||
{
|
||||
[JsonRequired]
|
||||
public string id { get; init; }
|
||||
public required string id { get; init; }
|
||||
|
||||
[JsonRequired]
|
||||
public ProjectModelsUpdatedMessageType type { get; init; }
|
||||
public required ProjectModelsUpdatedMessageType type { get; init; }
|
||||
|
||||
public Model? model { get; init; }
|
||||
}
|
||||
@@ -50,10 +50,10 @@ public sealed class ProjectModelsUpdatedMessage : EventArgs
|
||||
public sealed class ProjectPendingModelsUpdatedMessage : EventArgs
|
||||
{
|
||||
[JsonRequired]
|
||||
public string id { get; init; }
|
||||
public required string id { get; init; }
|
||||
|
||||
[JsonRequired]
|
||||
public ProjectPendingModelsUpdatedMessageType type { get; init; }
|
||||
public required ProjectPendingModelsUpdatedMessageType type { get; init; }
|
||||
|
||||
public FileUpload? model { get; init; }
|
||||
}
|
||||
@@ -61,10 +61,10 @@ public sealed class ProjectPendingModelsUpdatedMessage : EventArgs
|
||||
public sealed class ProjectUpdatedMessage : EventArgs
|
||||
{
|
||||
[JsonRequired]
|
||||
public string id { get; init; }
|
||||
public required string id { get; init; }
|
||||
|
||||
[JsonRequired]
|
||||
public ProjectUpdatedMessageType type { get; init; }
|
||||
public required ProjectUpdatedMessageType type { get; init; }
|
||||
|
||||
public Project? project { get; init; }
|
||||
}
|
||||
@@ -72,13 +72,22 @@ public sealed class ProjectUpdatedMessage : EventArgs
|
||||
public sealed class ProjectVersionsUpdatedMessage : EventArgs
|
||||
{
|
||||
[JsonRequired]
|
||||
public string id { get; init; }
|
||||
public required string id { get; init; }
|
||||
|
||||
[JsonRequired]
|
||||
public ProjectVersionsUpdatedMessageType type { get; init; }
|
||||
public required ProjectVersionsUpdatedMessageType type { get; init; }
|
||||
|
||||
[JsonRequired]
|
||||
public string modelId { get; init; }
|
||||
public required string modelId { get; init; }
|
||||
|
||||
public Version? version { get; init; }
|
||||
}
|
||||
|
||||
public sealed class ProjectModelIngestionUpdatedMessage : EventArgs
|
||||
{
|
||||
[JsonRequired]
|
||||
public required ModelIngestion modelIngestion { get; init; }
|
||||
|
||||
[JsonRequired]
|
||||
public required ProjectModelIngestionUpdatedMessageType type { get; init; }
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
namespace Speckle.Sdk.Api.GraphQL.Models;
|
||||
|
||||
public sealed class Workspace
|
||||
public class LimitedWorkspace
|
||||
{
|
||||
public string id { get; init; }
|
||||
public string name { get; init; }
|
||||
public string role { get; init; }
|
||||
public string? role { get; init; }
|
||||
public string slug { get; init; }
|
||||
public string? description { get; init; }
|
||||
public string? logo { get; init; }
|
||||
public DateTime? createdAt { get; init; }
|
||||
public DateTime? updatedAt { get; init; }
|
||||
public bool? readOnly { get; init; }
|
||||
public string? description { get; init; }
|
||||
}
|
||||
|
||||
public class Workspace : LimitedWorkspace
|
||||
{
|
||||
public DateTime createdAt { get; init; }
|
||||
public DateTime updatedAt { get; init; }
|
||||
public bool readOnly { get; init; }
|
||||
public WorkspacePermissionChecks permissions { get; init; }
|
||||
public WorkspaceCreationState? creationState { get; init; }
|
||||
}
|
||||
|
||||
@@ -313,10 +313,11 @@ public sealed class ActiveUserResource
|
||||
}
|
||||
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <returns>The active (last selected) workspace</returns>
|
||||
/// <remarks>note this returns a <see cref="LimitedWorkspace"/>, because it may be a workspace the user is not a member of</remarks>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
/// <exception cref="SpeckleException">The ActiveUser could not be found (e.g. the client is not authenticated)</exception>
|
||||
public async Task<Workspace?> GetActiveWorkspace(CancellationToken cancellationToken = default)
|
||||
public async Task<LimitedWorkspace?> GetActiveWorkspace(CancellationToken cancellationToken = default)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
@@ -328,21 +329,7 @@ public sealed class ActiveUserResource
|
||||
role
|
||||
slug
|
||||
logo
|
||||
createdAt
|
||||
updatedAt
|
||||
readOnly
|
||||
description
|
||||
creationState
|
||||
{
|
||||
completed
|
||||
}
|
||||
permissions {
|
||||
canCreateProject {
|
||||
authorized
|
||||
code
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -351,7 +338,7 @@ public sealed class ActiveUserResource
|
||||
var request = new GraphQLRequest { Query = QUERY };
|
||||
|
||||
var response = await _client
|
||||
.ExecuteGraphQLRequest<NullableResponse<NullableResponse<Workspace?>?>>(request, cancellationToken)
|
||||
.ExecuteGraphQLRequest<NullableResponse<NullableResponse<LimitedWorkspace?>?>>(request, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (response.data is null)
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
using System.Diagnostics;
|
||||
using GraphQL;
|
||||
using Speckle.Sdk.Api.Blob;
|
||||
using Speckle.Sdk.Api.GraphQL.Inputs;
|
||||
using Speckle.Sdk.Api.GraphQL.Models;
|
||||
using Speckle.Sdk.Api.GraphQL.Models.Responses;
|
||||
using Speckle.Sdk.Transports;
|
||||
|
||||
namespace Speckle.Sdk.Api.GraphQL.Resources;
|
||||
|
||||
public sealed class FileImportResource : IDisposable
|
||||
{
|
||||
private readonly ISpeckleGraphQLClient _client;
|
||||
private readonly IBlobApi _blobApi;
|
||||
|
||||
internal FileImportResource(ISpeckleGraphQLClient client, IBlobApi blobApi)
|
||||
{
|
||||
_client = client;
|
||||
_blobApi = blobApi;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is mostly an internal api, that marks a file import job finished.
|
||||
/// </summary>
|
||||
/// <param name="input">Either <see cref="FileImportSuccessInput"/> or <see cref="FileImportErrorInput"/></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
/// <remarks>
|
||||
/// Only use this if you are writing a file importer, that is responsible for
|
||||
/// processing file import jobs.
|
||||
/// Only works on servers version >=2.25.8
|
||||
/// </remarks>
|
||||
public async Task<bool> FinishFileImportJob(FileImportInputBase input, CancellationToken cancellationToken)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
mutation FinishFileImport($input: FinishFileImportInput!) {
|
||||
data:fileUploadMutations {
|
||||
data:finishFileImport(input: $input)
|
||||
}
|
||||
}
|
||||
""";
|
||||
var request = new GraphQLRequest { Query = QUERY, Variables = new { input } };
|
||||
|
||||
var response = await _client
|
||||
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<bool>>>(request, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
/// <remarks>Only works on servers version >=2.25.8</remarks>
|
||||
public async Task<FileImport> StartFileImportJob(
|
||||
StartFileImportInput input,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
mutation StartFileImport($input: StartFileImportInput!) {
|
||||
data:fileUploadMutations {
|
||||
data:startFileImport(input: $input) {
|
||||
id
|
||||
projectId
|
||||
convertedVersionId
|
||||
userId
|
||||
convertedStatus
|
||||
convertedMessage
|
||||
modelId
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
var request = new GraphQLRequest { Query = QUERY, Variables = new { input } };
|
||||
|
||||
var response = await _client
|
||||
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<FileImport>>>(request, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a file upload url from the Speckle server.
|
||||
/// This method asks the server to create a pre-signed S3 url,
|
||||
/// which can be used as a short term authenticated route, to put a file to the server.
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
/// <remarks>Only works on servers version >=2.25.8</remarks>
|
||||
public async Task<FileUploadUrl> GenerateUploadUrl(
|
||||
GenerateFileUploadUrlInput input,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
mutation GenerateUploadUrl($input: GenerateFileUploadUrlInput!) {
|
||||
data:fileUploadMutations {
|
||||
data:generateUploadUrl(input: $input) {
|
||||
fileId
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
var request = new GraphQLRequest { Query = QUERY, Variables = new { input } };
|
||||
|
||||
var response = await _client
|
||||
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<FileUploadUrl>>>(request, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Blob.BlobApi.UploadFile"/>
|
||||
[DebuggerStepThrough]
|
||||
public Task<string> UploadFile(
|
||||
string filePath,
|
||||
Uri url,
|
||||
IProgress<ProgressArgs>? progress = null,
|
||||
CancellationToken cancellationToken = default
|
||||
) => _blobApi.UploadFile(filePath, url, progress, cancellationToken);
|
||||
|
||||
/// <inheritdoc cref="Blob.BlobApi.DownloadBlob"/>
|
||||
[DebuggerStepThrough]
|
||||
public Task DownloadFile(
|
||||
string projectId,
|
||||
string fileId,
|
||||
string targetFile,
|
||||
IProgress<ProgressArgs>? progress = null,
|
||||
CancellationToken cancellationToken = default
|
||||
) => _blobApi.DownloadBlob(projectId, fileId, targetFile, progress, cancellationToken);
|
||||
|
||||
/// <param name="projectId"></param>
|
||||
/// <param name="modelId"></param>
|
||||
/// <param name="limit"></param>
|
||||
/// <param name="cursor"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
/// <remarks>Only works on servers version >=2.25.8</remarks>
|
||||
public async Task<ResourceCollection<FileImport>> GetModelFileImportJobs(
|
||||
string projectId,
|
||||
string modelId,
|
||||
int limit = ServerLimits.DEFAULT_PAGINATION_REQUEST,
|
||||
string? cursor = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
query ModelFileImportJobs(
|
||||
$projectId: String!,
|
||||
$modelId: String!,
|
||||
$input: GetModelUploadsInput
|
||||
) {
|
||||
data:project(id: $projectId) {
|
||||
data:model(id: $modelId) {
|
||||
data:uploads(input: $input) {
|
||||
totalCount
|
||||
cursor
|
||||
items {
|
||||
id
|
||||
projectId
|
||||
convertedVersionId
|
||||
userId
|
||||
convertedStatus
|
||||
convertedMessage
|
||||
modelId
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
var request = new GraphQLRequest
|
||||
{
|
||||
Query = QUERY,
|
||||
Variables = new
|
||||
{
|
||||
projectId,
|
||||
modelId,
|
||||
input = new { limit, cursor },
|
||||
},
|
||||
};
|
||||
|
||||
var response = await _client
|
||||
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<ResourceCollection<FileImport>>>>>(
|
||||
request,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return response.data.data.data;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_blobApi.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
using GraphQL;
|
||||
using Speckle.Sdk.Api.GraphQL.Inputs;
|
||||
using Speckle.Sdk.Api.GraphQL.Models;
|
||||
using Speckle.Sdk.Api.GraphQL.Models.Responses;
|
||||
|
||||
namespace Speckle.Sdk.Api.GraphQL.Resources;
|
||||
|
||||
/// <remarks>
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3-alpha.583</c> and above
|
||||
/// </remarks>
|
||||
public sealed class ModelIngestionResource
|
||||
{
|
||||
private readonly ISpeckleGraphQLClient _client;
|
||||
|
||||
internal ModelIngestionResource(ISpeckleGraphQLClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new model ingestion
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The model ingestion created will have a <c>processing</c> state (not <c>queued</c>). This mutation is designed to be used
|
||||
/// by client/connectors that are immediately processing
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3-alpha.583</c> and above
|
||||
/// </remarks>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
public async Task<ModelIngestion> Create(
|
||||
ModelIngestionCreateInput input,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
mutation IngestionCreate($input: ModelIngestionCreateInput!) {
|
||||
data: projectMutations {
|
||||
data: modelIngestionMutations {
|
||||
data: create(input: $input) {
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
modelId
|
||||
cancellationRequested
|
||||
statusData {
|
||||
... on HasModelIngestionStatus {
|
||||
status
|
||||
}
|
||||
... on HasProgressMessage {
|
||||
progressMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
|
||||
|
||||
var res = await _client
|
||||
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<ModelIngestion>>>>(
|
||||
request,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return res.data.data.data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For File Import / Cloud integrations only
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3-alpha.583</c> and above
|
||||
/// </remarks>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
public async Task<ModelIngestion> StartProcessing(
|
||||
ModelIngestionStartProcessingInput input,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
mutation IngestionStartProcessing($input: ModelIngestionStartProcessingInput!) {
|
||||
data: projectMutations {
|
||||
data: modelIngestionMutations {
|
||||
data: startProcessing(input: $input) {
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
modelId
|
||||
cancellationRequested
|
||||
statusData {
|
||||
... on HasModelIngestionStatus {
|
||||
status
|
||||
}
|
||||
... on HasProgressMessage {
|
||||
progressMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
|
||||
|
||||
var res = await _client
|
||||
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<ModelIngestion>>>>(
|
||||
request,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return res.data.data.data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For File Import / Cloud integrations only
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3-alpha.583</c> and above
|
||||
/// </remarks>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
public async Task<ModelIngestion> Requeue(
|
||||
ModelIngestionRequeueInput input,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
mutation IngestionStartProcessing($input: ModelIngestionRequeueInput!) {
|
||||
data: projectMutations {
|
||||
data: modelIngestionMutations {
|
||||
data: requeue(input: $input) {
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
modelId
|
||||
cancellationRequested
|
||||
statusData {
|
||||
... on HasModelIngestionStatus {
|
||||
status
|
||||
}
|
||||
... on HasProgressMessage {
|
||||
progressMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
|
||||
|
||||
var res = await _client
|
||||
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<ModelIngestion>>>>(
|
||||
request,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return res.data.data.data;
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3-alpha.583</c> and above
|
||||
/// </remarks>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
public async Task<ModelIngestion> UpdateProgress(
|
||||
ModelIngestionUpdateInput input,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
mutation IngestionUpdateProgress(
|
||||
$input: ModelIngestionUpdateInput!
|
||||
) {
|
||||
data: projectMutations {
|
||||
data: modelIngestionMutations {
|
||||
data: updateProgress(input: $input) {
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
modelId
|
||||
cancellationRequested
|
||||
statusData {
|
||||
... on HasModelIngestionStatus {
|
||||
status
|
||||
}
|
||||
... on HasProgressMessage {
|
||||
progressMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
|
||||
|
||||
var res = await _client
|
||||
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<ModelIngestion>>>>(
|
||||
request,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return res.data.data.data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request that the server completes the ingestion by creating a version
|
||||
/// If successful, the job will be in a terminal "successful" state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3-alpha.583</c> and above
|
||||
/// </remarks>
|
||||
/// <seealso cref="FailWithError"/>
|
||||
/// <seealso cref="FailWithCancel"/>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns>The version id</returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
public async Task<string> Complete(ModelIngestionSuccessInput input, CancellationToken cancellationToken = default)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
mutation IngestionComplete($input: ModelIngestionSuccessInput!) {
|
||||
data: projectMutations {
|
||||
data: modelIngestionMutations {
|
||||
data: completeWithVersion(input: $input) {
|
||||
data:statusData {
|
||||
... on ModelIngestionSuccessStatus {
|
||||
data:versionId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
|
||||
|
||||
var res = await _client
|
||||
.ExecuteGraphQLRequest<
|
||||
RequiredResponse<RequiredResponse<RequiredResponse<RequiredResponse<RequiredResponse<string>>>>>
|
||||
>(request, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return res.data.data.data.data.data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fail the job with an error.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For requested user cancellation, use <see cref="FailWithCancel"/> instead<br/>
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3-alpha.583</c> and above
|
||||
/// </remarks>
|
||||
/// <seealso cref="FailWithCancel"/>
|
||||
/// <seealso cref="Complete"/>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
public async Task<ModelIngestion> FailWithError(
|
||||
ModelIngestionFailedInput input,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
mutation IngestionFailWithError($input: ModelIngestionFailedInput!) {
|
||||
data: projectMutations {
|
||||
data: modelIngestionMutations {
|
||||
data: failWithError(input: $input) {
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
modelId
|
||||
cancellationRequested
|
||||
statusData {
|
||||
... on HasModelIngestionStatus {
|
||||
status
|
||||
}
|
||||
... on HasProgressMessage {
|
||||
progressMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
|
||||
|
||||
var res = await _client
|
||||
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<ModelIngestion>>>>(
|
||||
request,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return res.data.data.data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fail the ingestion with a <c>canceled</c> status.
|
||||
/// This should only be done if the user has explicitly requested cancellation
|
||||
/// Other forms of cancellation use <see cref="FailWithError"/>.
|
||||
/// The ingestion should then enter a terminal "canceled" state.<br/>
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3-alpha.583</c> and above
|
||||
/// </summary>
|
||||
/// <seealso cref="FailWithError"/>
|
||||
/// <seealso cref="Complete"/>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
public async Task<ModelIngestion> FailWithCancel(
|
||||
ModelIngestionCancelledInput input,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
mutation IngestionFailWithCancel($input: ModelIngestionCancelledInput!) {
|
||||
data: projectMutations {
|
||||
data: modelIngestionMutations {
|
||||
data: failWithCancel(input: $input) {
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
modelId
|
||||
cancellationRequested
|
||||
statusData {
|
||||
... on HasModelIngestionStatus {
|
||||
status
|
||||
}
|
||||
... on HasProgressMessage {
|
||||
progressMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
|
||||
|
||||
var res = await _client
|
||||
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<ModelIngestion>>>>(
|
||||
request,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return res.data.data.data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request that the <see cref="ModelIngestion"/> is canceled.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note simply calling this mutation does not imediatly cancel, it doesn't even guarantee it will be canceled at all.
|
||||
/// It's up to the client to observe this cancellation request
|
||||
/// via <see cref="SubscriptionResource.CreateProjectModelIngestionCancellationRequestedSubscription"/>
|
||||
/// and report it as canceled via <see cref="ModelIngestionResource.FailWithCancel"/>
|
||||
/// See "cooperative cancellation pattern"<br/>
|
||||
/// Model Ingestion API is available for server versions <c>3.0.3-alpha.583</c> and above
|
||||
/// </remarks>
|
||||
/// <seealso cref="FailWithError"/>
|
||||
/// <seealso cref="Complete"/>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
public async Task<ModelIngestion> RequestCancellation(
|
||||
ModelIngestionCancelledInput input,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
mutation IngestionRequestCancellation($input: ModelIngestionRequestCancellationInput!) {
|
||||
data: projectMutations {
|
||||
data: modelIngestionMutations {
|
||||
data: requestCancellation (input: $input) {
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
modelId
|
||||
cancellationRequested
|
||||
statusData {
|
||||
... on HasModelIngestionStatus {
|
||||
status
|
||||
}
|
||||
... on HasProgressMessage {
|
||||
progressMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
|
||||
|
||||
var res = await _client
|
||||
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<ModelIngestion>>>>(
|
||||
request,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return res.data.data.data;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using GraphQL;
|
||||
using Speckle.Sdk.Api.GraphQL.Enums;
|
||||
using Speckle.Sdk.Api.GraphQL.Inputs;
|
||||
using Speckle.Sdk.Api.GraphQL.Models;
|
||||
using Speckle.Sdk.Api.GraphQL.Models.Responses;
|
||||
@@ -212,6 +213,60 @@ public sealed class SubscriptionResource : IDisposable
|
||||
return subscription;
|
||||
}
|
||||
|
||||
/// <summary>Subscribe to a cancellation request being made for a Model Ingestion</summary>
|
||||
/// <remarks><inheritdoc cref="CreateUserProjectsUpdatedSubscription"/></remarks>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.SubscribeTo{T}"/>
|
||||
public Subscription<ProjectModelIngestionUpdatedMessage> CreateProjectModelIngestionUpdatedSubscription(
|
||||
ProjectModelIngestionSubscriptionInput input
|
||||
)
|
||||
{
|
||||
//language=graphql
|
||||
const string QUERY = """
|
||||
subscription IngestionUpdated($input: ProjectModelIngestionSubscriptionInput!) {
|
||||
data: projectModelIngestionUpdated(input: $input) {
|
||||
modelIngestion {
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
modelId
|
||||
cancellationRequested
|
||||
statusData {
|
||||
... on HasModelIngestionStatus {
|
||||
status
|
||||
}
|
||||
... on HasProgressMessage {
|
||||
progressMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
type
|
||||
}
|
||||
}
|
||||
""";
|
||||
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
|
||||
|
||||
Subscription<ProjectModelIngestionUpdatedMessage> subscription = new(_client, request);
|
||||
_subscriptions.Add(subscription);
|
||||
return subscription;
|
||||
}
|
||||
|
||||
/// <summary>Subscribe to a cancellation request being made for a Model Ingestion</summary>
|
||||
/// <remarks><inheritdoc cref="CreateUserProjectsUpdatedSubscription"/></remarks>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.SubscribeTo{T}"/>
|
||||
public Subscription<ProjectModelIngestionUpdatedMessage> CreateProjectModelIngestionCancellationRequestedSubscription(
|
||||
string ingestionId,
|
||||
string projectId
|
||||
)
|
||||
{
|
||||
return CreateProjectModelIngestionUpdatedSubscription(
|
||||
new ProjectModelIngestionSubscriptionInput(
|
||||
projectId,
|
||||
new(ingestionId, null),
|
||||
ProjectModelIngestionUpdatedMessageType.cancellationRequested
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var subscription in _subscriptions)
|
||||
|
||||
@@ -2,6 +2,7 @@ using Microsoft.Extensions.Logging;
|
||||
using Speckle.Sdk.Logging;
|
||||
using Speckle.Sdk.Models;
|
||||
using Speckle.Sdk.Serialisation;
|
||||
using Speckle.Sdk.Serialisation.V2.Receive;
|
||||
using Speckle.Sdk.Transports;
|
||||
|
||||
namespace Speckle.Sdk.Api;
|
||||
@@ -15,6 +16,7 @@ public partial class Operations
|
||||
/// <exception cref="ArgumentException">No transports were specified</exception>
|
||||
/// <exception cref="ArgumentNullException">The <paramref name="objectId"/> was <see langword="null"/></exception>
|
||||
/// <exception cref="SpeckleException">Serialization or Send operation was unsuccessful</exception>
|
||||
/// <exception cref="HttpRequestException">HTTP layer errors</exception>
|
||||
/// <exception cref="OperationCanceledException">The <paramref name="cancellationToken"/> requested cancellation</exception>
|
||||
public async Task<Base> Receive2(
|
||||
Uri url,
|
||||
@@ -22,7 +24,8 @@ public partial class Operations
|
||||
string objectId,
|
||||
string? authorizationToken,
|
||||
IProgress<ProgressArgs>? onProgressAction,
|
||||
CancellationToken cancellationToken
|
||||
CancellationToken cancellationToken,
|
||||
DeserializeProcessOptions? options = null
|
||||
)
|
||||
{
|
||||
using var receiveActivity = activityFactory.Start("Operations.Receive");
|
||||
@@ -36,7 +39,8 @@ public partial class Operations
|
||||
streamId,
|
||||
authorizationToken,
|
||||
onProgressAction,
|
||||
cancellationToken
|
||||
cancellationToken,
|
||||
options
|
||||
);
|
||||
try
|
||||
{
|
||||
@@ -44,6 +48,11 @@ public partial class Operations
|
||||
receiveActivity?.SetStatus(SdkActivityStatusCode.Ok);
|
||||
return result;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
//this is handled by the caller
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
receiveActivity?.SetStatus(SdkActivityStatusCode.Error);
|
||||
|
||||
@@ -25,7 +25,8 @@ public partial class Operations
|
||||
string? authorizationToken,
|
||||
Base value,
|
||||
IProgress<ProgressArgs>? onProgressAction,
|
||||
CancellationToken cancellationToken
|
||||
CancellationToken cancellationToken,
|
||||
SerializeProcessOptions? options = null
|
||||
)
|
||||
{
|
||||
using var receiveActivity = activityFactory.Start("Operations.Send");
|
||||
@@ -38,7 +39,8 @@ public partial class Operations
|
||||
streamId,
|
||||
authorizationToken,
|
||||
onProgressAction,
|
||||
cancellationToken
|
||||
cancellationToken,
|
||||
options
|
||||
);
|
||||
try
|
||||
{
|
||||
@@ -47,6 +49,11 @@ public partial class Operations
|
||||
receiveActivity?.SetStatus(SdkActivityStatusCode.Ok);
|
||||
return results;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
//this is handled by the caller
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
receiveActivity?.SetStatus(SdkActivityStatusCode.Error);
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
using Speckle.InterfaceGenerator;
|
||||
|
||||
namespace Speckle.Sdk.Caching;
|
||||
|
||||
/// <summary>
|
||||
/// This mocks away the file system operations for testing purposes.
|
||||
/// </summary>
|
||||
[GenerateAutoInterface]
|
||||
public class FileSystem : IFileSystem
|
||||
{
|
||||
public bool DirectoryExists(string path) => Directory.Exists(path);
|
||||
|
||||
public void CreateDirectory(string path) => Directory.CreateDirectory(path);
|
||||
|
||||
public IEnumerable<string> EnumerateFiles(string path) => Directory.EnumerateFiles(path);
|
||||
|
||||
public void DeleteFile(string path) => File.Delete(path);
|
||||
|
||||
public long GetFileSize(string path) => new FileInfo(path).Length;
|
||||
|
||||
public string Combine(params string[] paths) => Path.Combine(paths);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.InterfaceGenerator;
|
||||
using Speckle.Sdk.Logging;
|
||||
using Speckle.Sdk.Transports;
|
||||
|
||||
namespace Speckle.Sdk.Caching;
|
||||
|
||||
/// <summary>
|
||||
/// This class manages the cache for model data, providing methods to get stream paths, clear the cache, and calculate cache size.
|
||||
/// </summary>
|
||||
[GenerateAutoInterface]
|
||||
public class ModelCacheManager(ILogger<ModelCacheManager> logger, IFileSystem fileSystem) : IModelCacheManager
|
||||
{
|
||||
private const string DATA_FOLDER = "Projects";
|
||||
private static readonly string s_basePath = SpecklePathProvider.UserSpeckleFolderPath;
|
||||
|
||||
private static string CacheFolder => Path.Combine(s_basePath, DATA_FOLDER);
|
||||
|
||||
public string GetStreamPath(string streamId) => GetDbPath(streamId);
|
||||
|
||||
public static string GetDbPath(string streamId)
|
||||
{
|
||||
var db = Path.Combine(CacheFolder, $"{streamId}.db");
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(CacheFolder); //ensure dir is there
|
||||
return db;
|
||||
}
|
||||
catch (Exception ex)
|
||||
when (ex is ArgumentException or IOException or UnauthorizedAccessException or NotSupportedException)
|
||||
{
|
||||
throw new TransportException($"Path was invalid or could not be created {db}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearCache()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!fileSystem.DirectoryExists(CacheFolder))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var db in fileSystem.EnumerateFiles(CacheFolder))
|
||||
{
|
||||
try
|
||||
{
|
||||
fileSystem.DeleteFile(db);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or NotSupportedException)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to delete cache file {filePath}", db);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
when (ex is ArgumentException or IOException or UnauthorizedAccessException or NotSupportedException)
|
||||
{
|
||||
throw new TransportException($"Cache folder could not be cleared: {CacheFolder}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public long GetCacheSize()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!fileSystem.DirectoryExists(CacheFolder))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
long size = 0;
|
||||
foreach (var file in fileSystem.EnumerateFiles(CacheFolder))
|
||||
{
|
||||
try
|
||||
{
|
||||
size += fileSystem.GetFileSize(file);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or NotSupportedException)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to get size for cache file {a}", file);
|
||||
}
|
||||
}
|
||||
return size;
|
||||
}
|
||||
catch (Exception ex)
|
||||
when (ex is ArgumentException or IOException or UnauthorizedAccessException or NotSupportedException)
|
||||
{
|
||||
throw new TransportException($"Cache folder size could not be determined: {CacheFolder}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,15 +59,20 @@ public class Account : IEquatable<Account>
|
||||
|
||||
#region public methods
|
||||
|
||||
|
||||
/// <remarks>The logic is aligned with <c>distinct_id</c> mixpanel property</remarks>
|
||||
/// <exception cref="ArgumentNullException">Thrown if <see name="userInfo.email"/> was <see langword="null"/></exception>
|
||||
public string GetHashedEmail()
|
||||
{
|
||||
string email = userInfo?.email ?? "unknown";
|
||||
string email = userInfo.email.NotNull();
|
||||
return "@" + Md5.GetString(email).ToUpperInvariant();
|
||||
}
|
||||
|
||||
/// <remarks>The logic is aligned with <c>server</c> mixpanel property</remarks>
|
||||
/// <exception cref="ArgumentNullException">Thrown if <see name="serverInfo.url"/> was <see langword="null"/></exception>
|
||||
public string GetHashedServer()
|
||||
{
|
||||
string url = serverInfo?.url ?? AccountManager.DEFAULT_SERVER_URL;
|
||||
string url = serverInfo.url.NotNull();
|
||||
return Md5.GetString(CleanURL(url)).ToUpperInvariant();
|
||||
}
|
||||
|
||||
@@ -97,6 +102,8 @@ public class Account : IEquatable<Account>
|
||||
|
||||
#endregion
|
||||
|
||||
internal const string LOCAL_IDENTIFIER_DEPRECATION_MESSAGE = "Local identifiers no longer nesseary";
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the local identifier for the current user.
|
||||
/// </summary>
|
||||
@@ -121,5 +128,6 @@ public class Account : IEquatable<Account>
|
||||
/// https://speckle.xyz?id=123
|
||||
/// </code>
|
||||
/// </example>
|
||||
[Obsolete(LOCAL_IDENTIFIER_DEPRECATION_MESSAGE)]
|
||||
internal Uri GetLocalIdentifier() => new($"{serverInfo.url}?id={userInfo.id}");
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ public sealed class AccountManager(
|
||||
account.id = null!; //TODO this is gross so remove when id is nullable
|
||||
|
||||
RemoveAccount(id);
|
||||
_accountStorage.SaveObject(account.id.NotNull(), JsonConvert.SerializeObject(account));
|
||||
_accountStorage.UpdateObject(account.id.NotNull(), JsonConvert.SerializeObject(account));
|
||||
}
|
||||
|
||||
public IEnumerable<Account> GetAccounts(string serverUrl)
|
||||
@@ -407,7 +407,7 @@ public sealed class AccountManager(
|
||||
{
|
||||
account.isDefault = true;
|
||||
}
|
||||
_accountStorage.SaveObject(account.id, JsonConvert.SerializeObject(account));
|
||||
_accountStorage.UpdateObject(account.id, JsonConvert.SerializeObject(account));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,6 +419,7 @@ public sealed class AccountManager(
|
||||
/// <remarks>
|
||||
/// <inheritdoc cref="Account.GetLocalIdentifier"/>
|
||||
/// </remarks>
|
||||
[Obsolete(Account.LOCAL_IDENTIFIER_DEPRECATION_MESSAGE)]
|
||||
public Uri? GetLocalIdentifierForAccount(Account account)
|
||||
{
|
||||
var identifier = account.GetLocalIdentifier();
|
||||
@@ -440,6 +441,7 @@ public sealed class AccountManager(
|
||||
/// </summary>
|
||||
/// <param name="localIdentifier">The local identifier of the account.</param>
|
||||
/// <returns>The account that matches the local identifier, or null if no match is found.</returns>
|
||||
[Obsolete(Account.LOCAL_IDENTIFIER_DEPRECATION_MESSAGE)]
|
||||
public Account? GetAccountForLocalIdentifier(Uri localIdentifier)
|
||||
{
|
||||
var searchResult = GetAccounts()
|
||||
|
||||
@@ -15,10 +15,14 @@ internal static class TypeLoader
|
||||
private static ConcurrentDictionary<string, Type> s_cachedTypes = new();
|
||||
private static ConcurrentDictionary<Type, string> s_fullTypeStrings = new();
|
||||
private static ConcurrentDictionary<PropertyInfo, JsonPropertyAttribute?> s_jsonPropertyAttribute = new();
|
||||
private static readonly ConcurrentDictionary<PropertyInfo, bool> s_obsolete = new();
|
||||
private static ConcurrentDictionary<Type, IReadOnlyList<PropertyInfo>> s_propInfoCache = new();
|
||||
|
||||
public static IEnumerable<LoadedType> Types => s_availableTypes;
|
||||
|
||||
public static bool IsObsolete(PropertyInfo property) =>
|
||||
s_obsolete.GetOrAdd(property, p => p.IsDefined(typeof(ObsoleteAttribute), true));
|
||||
|
||||
public static JsonPropertyAttribute? GetJsonPropertyAttribute(PropertyInfo property) =>
|
||||
s_jsonPropertyAttribute.GetOrAdd(property, p => p.GetCustomAttribute<JsonPropertyAttribute>(true));
|
||||
|
||||
|
||||
@@ -9,12 +9,14 @@ public static class SpecklePathProvider
|
||||
{
|
||||
private const string APPLICATION_NAME = "Speckle";
|
||||
|
||||
private const string LOG_FOLDER_NAME = "Logs";
|
||||
|
||||
private const string BLOB_FOLDER_NAME = "Blobs";
|
||||
|
||||
private const string ACCOUNTS_FOLDER_NAME = "Accounts";
|
||||
|
||||
private static string UserDataPathEnvVar => "SPECKLE_USERDATA_PATH";
|
||||
private static string? Path => Environment.GetEnvironmentVariable(UserDataPathEnvVar);
|
||||
public const string USER_DATA_PATH_ENV_VAR = "SPECKLE_USERDATA_PATH";
|
||||
private static string? Path => Environment.GetEnvironmentVariable(USER_DATA_PATH_ENV_VAR);
|
||||
|
||||
/// <summary>
|
||||
/// Get the installation path.
|
||||
@@ -43,7 +45,7 @@ public static class SpecklePathProvider
|
||||
/// <see cref="Environment.SpecialFolder.ApplicationData"/> path usually maps to
|
||||
/// <ul>
|
||||
/// <li>win: <c>%appdata%/</c></li>
|
||||
/// <li>MacOS: <c>~/.config/</c></li>
|
||||
/// <li>MacOS: <c>~/Library/Application Support</c></li>
|
||||
/// <li>Linux: <c>~/.config/</c></li>
|
||||
/// </ul>
|
||||
/// </remarks>
|
||||
@@ -57,29 +59,18 @@ public static class SpecklePathProvider
|
||||
return pathOverride;
|
||||
}
|
||||
|
||||
// on desktop linux and macos we use the appdata.
|
||||
// but we might not have write access to the disk
|
||||
// so the catch falls back to the user profile
|
||||
try
|
||||
{
|
||||
return Environment.GetFolderPath(
|
||||
Environment.SpecialFolder.ApplicationData,
|
||||
// if the folder doesn't exist, we get back an empty string on OSX,
|
||||
// which in turn, breaks other stuff down the line.
|
||||
// passing in the Create option ensures that this directory exists,
|
||||
// which is not a given on all OS-es.
|
||||
// It's not a given that the folder is already there on all OS-es, so we'll create it
|
||||
Environment.SpecialFolderOption.Create
|
||||
);
|
||||
}
|
||||
catch (SystemException ex) when (ex is PlatformNotSupportedException or ArgumentException)
|
||||
catch (PlatformNotSupportedException)
|
||||
{
|
||||
//Adding this log just so we confidently know which Exception type to catch here.
|
||||
// TODO: Must re-add log call when (and if) this get's made as a service
|
||||
//SpeckleLog.Logger.Warning(ex, "Falling back to user profile path");
|
||||
|
||||
// on server linux, there might not be a user setup, things can run under root
|
||||
// in that case, the appdata variable is most probably not set up
|
||||
// we fall back to the value of the home folder
|
||||
// We might not have write access to the disk to create the folder,
|
||||
// so we'll fall back to the user profile
|
||||
return Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
}
|
||||
}
|
||||
@@ -96,4 +87,7 @@ public static class SpecklePathProvider
|
||||
Directory.CreateDirectory(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
public static string LogFolderPath(string applicationAndVersion) =>
|
||||
EnsureFolderExists(UserSpeckleFolderPath, LOG_FOLDER_NAME, applicationAndVersion);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using Speckle.Newtonsoft.Json;
|
||||
using Speckle.Newtonsoft.Json.Linq;
|
||||
using Speckle.Sdk.Common;
|
||||
using Speckle.Sdk.Helpers;
|
||||
using Speckle.Sdk.Host;
|
||||
using Speckle.Sdk.Serialisation;
|
||||
@@ -92,8 +91,7 @@ public class Base : DynamicBase, ISpeckleObject
|
||||
var typedProps = @base.GetInstanceMembers();
|
||||
foreach (var prop in typedProps.Where(p => p.CanRead))
|
||||
{
|
||||
bool isIgnored =
|
||||
prop.IsDefined(typeof(ObsoleteAttribute), true) || prop.IsDefined(typeof(JsonIgnoreAttribute), true);
|
||||
bool isIgnored = TypeLoader.IsObsolete(prop) || prop.IsDefined(typeof(JsonIgnoreAttribute), true);
|
||||
if (isIgnored)
|
||||
{
|
||||
continue;
|
||||
@@ -171,8 +169,7 @@ public class Base : DynamicBase, ISpeckleObject
|
||||
|
||||
return count;
|
||||
}
|
||||
case IEnumerable e
|
||||
and not string:
|
||||
case IEnumerable e and not string:
|
||||
{
|
||||
foreach (var arrValue in e)
|
||||
{
|
||||
@@ -193,30 +190,4 @@ public class Base : DynamicBase, ISpeckleObject
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a shallow copy of the current base object.
|
||||
/// This operation does NOT copy/duplicate the data inside each prop.
|
||||
/// The new object's property values will be pointers to the original object's property value.
|
||||
/// </summary>
|
||||
/// <returns>A shallow copy of the original object.</returns>
|
||||
public Base ShallowCopy()
|
||||
{
|
||||
Type type = GetType();
|
||||
Base myDuplicate = (Base)Activator.CreateInstance(type).NotNull();
|
||||
myDuplicate.id = id;
|
||||
myDuplicate.applicationId = applicationId;
|
||||
|
||||
foreach (var kvp in GetMembers())
|
||||
{
|
||||
var propertyInfo = type.GetProperty(kvp.Key);
|
||||
if (propertyInfo is not null && !propertyInfo.CanWrite)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
myDuplicate[kvp.Key] = kvp.Value;
|
||||
}
|
||||
|
||||
return myDuplicate;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using Speckle.Sdk.Models.Data;
|
||||
|
||||
namespace Speckle.Sdk.Models.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// Root collection that represents the top-level commit object.
|
||||
/// Extends Collection to include model-wide properties that apply to the entire model.
|
||||
/// </summary>
|
||||
[SpeckleType("Speckle.Core.Models.Collections.RootCollection")]
|
||||
public class RootCollection : Collection, IProperties
|
||||
{
|
||||
public RootCollection() { }
|
||||
|
||||
public RootCollection(string name)
|
||||
: base(name) { }
|
||||
|
||||
/// <summary>
|
||||
/// Model-wide properties that apply to the entire model.
|
||||
/// </summary>
|
||||
public Dictionary<string, object?> properties { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Speckle.Sdk.Models.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies properties on objects to be used for data-based workflows.
|
||||
/// Can be applied to both objects and collections.
|
||||
/// </summary>
|
||||
public interface IProperties
|
||||
{
|
||||
Dictionary<string, object?> properties { get; }
|
||||
}
|
||||
@@ -7,7 +7,7 @@ namespace Speckle.Sdk.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Base class implementing a bunch of nice dynamic object methods, like adding and removing props dynamically. Makes c# feel like json.
|
||||
/// <para>Orginally adapted from Rick Strahl 🤘</para>
|
||||
/// <para>Originally adapted from Rick Strahl 🤘</para>
|
||||
/// <para>https://weblog.west-wind.com/posts/2012/feb/08/creating-a-dynamic-extensible-c-expando-object</para>
|
||||
/// </summary>
|
||||
public class DynamicBase : DynamicObject, IDynamicMetaObjectProvider
|
||||
@@ -84,6 +84,44 @@ public class DynamicBase : DynamicObject, IDynamicMetaObjectProvider
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a shallow copy of the current base object.
|
||||
/// This operation does NOT copy/duplicate the data inside each prop.
|
||||
/// The new object's property values will be pointers to the original object's property value.
|
||||
/// </summary>
|
||||
/// <returns>A shallow copy of the original object.</returns>
|
||||
public DynamicBase ShallowCopy()
|
||||
{
|
||||
Type type = GetType();
|
||||
DynamicBase myDuplicate = (DynamicBase)(
|
||||
Activator.CreateInstance(type) ?? throw new SpeckleException($"Failed to create instance of {type.Name}")
|
||||
);
|
||||
|
||||
// Add dynamic members
|
||||
foreach (var kvp in _properties)
|
||||
{
|
||||
myDuplicate._properties[kvp.Key] = kvp.Value;
|
||||
}
|
||||
|
||||
var pinfos = TypeLoader.GetBaseProperties(type).Where(x => !TypeLoader.IsObsolete(x));
|
||||
foreach (var pi in pinfos)
|
||||
{
|
||||
if (pi.CanWrite)
|
||||
{
|
||||
try
|
||||
{
|
||||
pi.SetValue(myDuplicate, pi.GetValue(this));
|
||||
}
|
||||
catch (Exception ex) when (!ex.IsFatal())
|
||||
{
|
||||
throw new SpeckleException($"Failed to set value for {type.Name}.{pi.Name}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return myDuplicate;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Gets properties via the dot syntax.
|
||||
@@ -232,7 +270,7 @@ public class DynamicBase : DynamicObject, IDynamicMetaObjectProvider
|
||||
.GetBaseProperties(GetType())
|
||||
.Where(x =>
|
||||
{
|
||||
var hasObsolete = x.IsDefined(typeof(ObsoleteAttribute), true);
|
||||
var hasObsolete = TypeLoader.IsObsolete(x);
|
||||
|
||||
// If obsolete is false and prop has obsolete attr
|
||||
// OR
|
||||
|
||||
@@ -109,9 +109,9 @@ public abstract class GraphTraversal<T>
|
||||
break;
|
||||
case IList list:
|
||||
{
|
||||
foreach (object? obj in list)
|
||||
for (int i = list.Count - 1; i >= 0; i--)
|
||||
{
|
||||
TraverseMemberToStack(stack, obj, memberName, parent);
|
||||
TraverseMemberToStack(stack, list[i], memberName, parent);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
@@ -13,7 +13,26 @@ public sealed class SqLiteJsonCacheManager : ISqLiteJsonCacheManager
|
||||
{
|
||||
private readonly CacheDbCommandPool _pool;
|
||||
|
||||
public SqLiteJsonCacheManager(string path, int concurrency)
|
||||
public static ISqLiteJsonCacheManager FromMemory(int concurrency) => new SqLiteJsonCacheManager(concurrency);
|
||||
|
||||
private SqLiteJsonCacheManager(int concurrency)
|
||||
{
|
||||
//disable pooling as we pool ourselves
|
||||
var builder = new SqliteConnectionStringBuilder
|
||||
{
|
||||
Pooling = false,
|
||||
DataSource = ":memory:",
|
||||
Cache = SqliteCacheMode.Shared,
|
||||
Mode = SqliteOpenMode.Memory,
|
||||
};
|
||||
_pool = new CacheDbCommandPool(builder.ToString(), concurrency);
|
||||
Initialize();
|
||||
}
|
||||
|
||||
public static ISqLiteJsonCacheManager FromFilePath(string path, int concurrency) =>
|
||||
new SqLiteJsonCacheManager(path, concurrency);
|
||||
|
||||
private SqLiteJsonCacheManager(string path, int concurrency)
|
||||
{
|
||||
//disable pooling as we pool ourselves
|
||||
var builder = new SqliteConnectionStringBuilder { Pooling = false, DataSource = path };
|
||||
@@ -47,12 +66,6 @@ public sealed class SqLiteJsonCacheManager : ISqLiteJsonCacheManager
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
// Insert Optimisations
|
||||
|
||||
//Note / Hack: This setting has the potential to corrupt the db.
|
||||
//cmd = new SqliteCommand("PRAGMA synchronous=OFF;", Connection);
|
||||
//cmd.ExecuteNonQuery();
|
||||
|
||||
using (SqliteCommand cmd1 = new("PRAGMA count_changes=OFF;", c))
|
||||
{
|
||||
cmd1.ExecuteNonQuery();
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
using Speckle.InterfaceGenerator;
|
||||
using Speckle.Sdk.Caching;
|
||||
using Speckle.Sdk.Logging;
|
||||
using Speckle.Sdk.Serialisation.Utilities;
|
||||
|
||||
namespace Speckle.Sdk.SQLite;
|
||||
|
||||
[GenerateAutoInterface]
|
||||
public class SqLiteJsonCacheManagerFactory : ISqLiteJsonCacheManagerFactory
|
||||
public class SqLiteJsonCacheManagerFactory(IModelCacheManager modelCacheManager) : ISqLiteJsonCacheManagerFactory
|
||||
{
|
||||
public const int INITIAL_CONCURRENCY = 4;
|
||||
|
||||
private ISqLiteJsonCacheManager Create(string path, int concurrency) => new SqLiteJsonCacheManager(path, concurrency);
|
||||
private ISqLiteJsonCacheManager Create(string path, int concurrency) =>
|
||||
SqLiteJsonCacheManager.FromFilePath(path, concurrency);
|
||||
|
||||
public ISqLiteJsonCacheManager CreateForUser(string scope) =>
|
||||
Create(Path.Combine(SpecklePathProvider.UserApplicationDataPath(), "Speckle", $"{scope}.db"), 1);
|
||||
|
||||
public ISqLiteJsonCacheManager CreateFromStream(string streamId) =>
|
||||
Create(SqlitePaths.GetDBPath(streamId), INITIAL_CONCURRENCY);
|
||||
Create(modelCacheManager.GetStreamPath(streamId), INITIAL_CONCURRENCY);
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
using Speckle.Sdk.Logging;
|
||||
using Speckle.Sdk.Transports;
|
||||
|
||||
namespace Speckle.Sdk.Serialisation.Utilities;
|
||||
|
||||
public static class SqlitePaths
|
||||
{
|
||||
private const string APPLICATION_NAME = "Speckle";
|
||||
private const string DATA_FOLDER = "Projects";
|
||||
private static readonly string basePath = SpecklePathProvider.UserApplicationDataPath();
|
||||
|
||||
public static string BlobStorageFolder =>
|
||||
SpecklePathProvider.BlobStoragePath(Path.Combine(basePath, APPLICATION_NAME));
|
||||
|
||||
public static string GetDBPath(string streamId)
|
||||
{
|
||||
var dir = Path.Combine(basePath, APPLICATION_NAME, DATA_FOLDER);
|
||||
var db = Path.Combine(dir, $"{streamId}.db");
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(dir); //ensure dir is there
|
||||
return db;
|
||||
}
|
||||
catch (Exception ex)
|
||||
when (ex is ArgumentException or IOException or UnauthorizedAccessException or NotSupportedException)
|
||||
{
|
||||
throw new TransportException($"Path was invalid or could not be created {db}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ public class MemoryServerObjectManager(ConcurrentDictionary<string, string> obje
|
||||
{
|
||||
public virtual async IAsyncEnumerable<(string, string)> DownloadObjects(
|
||||
IReadOnlyCollection<string> objectIds,
|
||||
string? attributeMask,
|
||||
IProgress<ProgressArgs>? progress,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ using Speckle.Sdk.Transports;
|
||||
namespace Speckle.Sdk.Serialisation.V2.Receive;
|
||||
|
||||
public record DeserializeProcessOptions(
|
||||
bool SkipCache = false,
|
||||
bool SkipCache = false, //TODO: This appears to be bugged when set to `true`, `LoadId` depends on sqlite
|
||||
bool ThrowOnMissingReferences = true,
|
||||
bool SkipInvalidConverts = false,
|
||||
int? MaxParallelism = null,
|
||||
|
||||
@@ -19,9 +19,7 @@ public sealed class ObjectLoader(
|
||||
IProgress<ProgressArgs>? progress,
|
||||
ILogger<ObjectLoader> logger,
|
||||
CancellationToken cancellationToken
|
||||
#pragma warning disable CS9107 // Parameter is captured into the state of the enclosing type and its value is also passed to the base constructor. The value might be captured by the base class as well.
|
||||
) : ChannelLoader<BaseItem>(cancellationToken), IObjectLoader
|
||||
#pragma warning restore CS9107 // Parameter is captured into the state of the enclosing type and its value is also passed to the base constructor. The value might be captured by the base class as well.
|
||||
{
|
||||
private int? _allChildrenCount;
|
||||
private long _checkCache;
|
||||
@@ -29,6 +27,7 @@ public sealed class ObjectLoader(
|
||||
private long _downloaded;
|
||||
private long _totalToDownload;
|
||||
private DeserializeProcessOptions _options = new();
|
||||
private readonly CancellationToken _cancellationToken = cancellationToken;
|
||||
|
||||
[AutoInterfaceIgnore]
|
||||
public void Dispose() => sqLiteJsonCacheManager.Dispose();
|
||||
@@ -46,7 +45,7 @@ public sealed class ObjectLoader(
|
||||
{
|
||||
//assume everything exists as the root is there.
|
||||
var allChildren = ClosureParser
|
||||
.GetClosuresSorted(rootJson, cancellationToken)
|
||||
.GetClosuresSorted(rootJson, _cancellationToken)
|
||||
.Select(x => new Id(x.Item1))
|
||||
.ToList();
|
||||
//this probably yields away from the Main thread to let host apps update progress
|
||||
@@ -59,11 +58,11 @@ public sealed class ObjectLoader(
|
||||
if (!options.SkipServer)
|
||||
{
|
||||
rootJson = await serverObjectManager
|
||||
.DownloadSingleObject(rootId, progress, cancellationToken)
|
||||
.DownloadSingleObject(rootId, progress, _cancellationToken)
|
||||
.NotNull()
|
||||
.ConfigureAwait(false);
|
||||
IReadOnlyCollection<Id> allChildrenIds = ClosureParser
|
||||
.GetClosures(rootJson, cancellationToken)
|
||||
.GetClosures(rootJson, _cancellationToken)
|
||||
.OrderByDescending(x => x.Item2)
|
||||
.Select(x => new Id(x.Item1))
|
||||
.Where(x => !x.Value.StartsWith("blob", StringComparison.Ordinal))
|
||||
@@ -111,12 +110,13 @@ public sealed class ObjectLoader(
|
||||
await foreach (
|
||||
var (id, json) in serverObjectManager.DownloadObjects(
|
||||
ids.Select(x => x.NotNull()).ToList(),
|
||||
null, //TODO: Implement attribute masking in a safe way that will not poison SQLite DB.
|
||||
progress,
|
||||
cancellationToken
|
||||
_cancellationToken
|
||||
)
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
_cancellationToken.ThrowIfCancellationRequested();
|
||||
Interlocked.Increment(ref _downloaded);
|
||||
progress?.Report(new(ProgressEvent.DownloadObjects, _downloaded, _totalToDownload));
|
||||
toCache.Add(new(new(id), new(json), true, null));
|
||||
@@ -138,7 +138,7 @@ public sealed class ObjectLoader(
|
||||
{
|
||||
if (!_options.SkipCache)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
_cancellationToken.ThrowIfCancellationRequested();
|
||||
sqLiteJsonCacheManager.SaveObjects(batch.Select(x => (x.Id.Value, x.Json.Value)));
|
||||
Interlocked.Exchange(ref _cached, _cached + batch.Count);
|
||||
progress?.Report(new(ProgressEvent.CachedToLocal, _cached, _allChildrenCount));
|
||||
@@ -168,10 +168,10 @@ public sealed class ObjectLoader(
|
||||
private void ThrowIfFailed()
|
||||
{
|
||||
//always check for cancellation first
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
_cancellationToken.ThrowIfCancellationRequested();
|
||||
if (Exception is not null)
|
||||
{
|
||||
throw new SpeckleException("Error while loading", Exception);
|
||||
throw new SpeckleException($"Error while loading: {Exception.Message}", Exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,12 +21,7 @@ public class BaseSerializer(
|
||||
public IReadOnlyDictionary<Id, ObjectReference> ObjectReferences => _objectReferences;
|
||||
|
||||
//leave this sync
|
||||
public IEnumerable<BaseItem> Serialise(
|
||||
Base obj,
|
||||
IReadOnlyDictionary<Id, NodeInfo> childInfo,
|
||||
bool skipCacheRead,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
public IEnumerable<BaseItem> Serialise(Base obj, bool skipCacheRead, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!skipCacheRead && obj.id != null)
|
||||
{
|
||||
@@ -38,7 +33,7 @@ public class BaseSerializer(
|
||||
}
|
||||
}
|
||||
|
||||
using var serializer2 = objectSerializerFactory.Create(childInfo, cancellationToken);
|
||||
using var serializer2 = objectSerializerFactory.Create(cancellationToken);
|
||||
var items = _pool.Get();
|
||||
try
|
||||
{
|
||||
|
||||
@@ -20,10 +20,10 @@ public sealed class ObjectSaver(
|
||||
ISqLiteJsonCacheManager sqLiteJsonCacheManager,
|
||||
IServerObjectManager serverObjectManager,
|
||||
ILogger<ObjectSaver> logger,
|
||||
CancellationToken cancellationToken,
|
||||
SerializeProcessOptions options,
|
||||
CancellationToken cancellationToken
|
||||
#pragma warning disable CS9107
|
||||
#pragma warning disable CA2254
|
||||
SerializeProcessOptions? options = null
|
||||
) : ChannelSaver<BaseItem>, IObjectSaver
|
||||
#pragma warning restore CA2254
|
||||
#pragma warning restore CS9107
|
||||
@@ -38,10 +38,11 @@ public sealed class ObjectSaver(
|
||||
private long _cached;
|
||||
|
||||
private long _objectsSerialized;
|
||||
private bool _disposed;
|
||||
|
||||
protected override async Task SendToServerInternal(Batch<BaseItem> batch)
|
||||
{
|
||||
if (_cancellationTokenSource.IsCancellationRequested)
|
||||
if (IsCancelled())
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -66,7 +67,7 @@ public sealed class ObjectSaver(
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_cancellationTokenSource.Cancel();
|
||||
CancelSaving();
|
||||
}
|
||||
#pragma warning disable CA1031
|
||||
catch (Exception e)
|
||||
@@ -91,7 +92,7 @@ public sealed class ObjectSaver(
|
||||
|
||||
public override void SaveToCache(List<BaseItem> batch)
|
||||
{
|
||||
if (_cancellationTokenSource.IsCancellationRequested)
|
||||
if (IsCancelled())
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -99,14 +100,14 @@ public sealed class ObjectSaver(
|
||||
{
|
||||
if (!_options.SkipCacheWrite && batch.Count != 0)
|
||||
{
|
||||
sqLiteJsonCacheManager.SaveObjects(batch.Select(x => (x.Id.Value, x.Json.Value)));
|
||||
Interlocked.Add(ref _cached, batch.Count);
|
||||
progress?.Report(new(ProgressEvent.CachedToLocal, _cached, _objectsSerialized));
|
||||
sqLiteJsonCacheManager.SaveObjects(batch.Select(x => (x.Id.Value, x.Json.Value)));
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_cancellationTokenSource.Cancel();
|
||||
CancelSaving();
|
||||
}
|
||||
#pragma warning disable CA1031
|
||||
catch (Exception e)
|
||||
@@ -123,8 +124,23 @@ public sealed class ObjectSaver(
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsCancelled() => _disposed || _cancellationTokenSource.IsCancellationRequested;
|
||||
|
||||
private void CancelSaving()
|
||||
{
|
||||
if (IsCancelled())
|
||||
{
|
||||
return;
|
||||
}
|
||||
_cancellationTokenSource.Cancel();
|
||||
}
|
||||
|
||||
private void RecordException(Exception e)
|
||||
{
|
||||
if (IsCancelled())
|
||||
{
|
||||
return;
|
||||
}
|
||||
//order here matters
|
||||
logger.LogError(e, "Error in SDK: {message}", e.Message);
|
||||
Exception = e;
|
||||
@@ -133,6 +149,7 @@ public sealed class ObjectSaver(
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_disposed = true;
|
||||
_cancellationTokenSource.Dispose();
|
||||
sqLiteJsonCacheManager.Dispose();
|
||||
}
|
||||
|
||||
@@ -26,8 +26,6 @@ public sealed class ObjectSerializer : IObjectSerializer
|
||||
{
|
||||
private HashSet<object> _parentObjects = new();
|
||||
|
||||
private readonly IReadOnlyDictionary<Id, NodeInfo> _childCache;
|
||||
|
||||
private readonly IBasePropertyGatherer _propertyGatherer;
|
||||
private readonly CancellationToken _cancellationToken;
|
||||
|
||||
@@ -52,7 +50,6 @@ public sealed class ObjectSerializer : IObjectSerializer
|
||||
/// <param name="cancellationToken"></param>
|
||||
public ObjectSerializer(
|
||||
IBasePropertyGatherer propertyGatherer,
|
||||
IReadOnlyDictionary<Id, NodeInfo> childCache,
|
||||
Pool<List<(Id, Json, Closures)>> chunksPool,
|
||||
Pool<List<DataChunk>> chunks2Pool,
|
||||
Pool<List<object?>> chunks3Pool,
|
||||
@@ -60,7 +57,6 @@ public sealed class ObjectSerializer : IObjectSerializer
|
||||
)
|
||||
{
|
||||
_propertyGatherer = propertyGatherer;
|
||||
_childCache = childCache;
|
||||
_chunksPool = chunksPool;
|
||||
_chunks2Pool = chunks2Pool;
|
||||
_chunks3Pool = chunks3Pool;
|
||||
@@ -299,28 +295,14 @@ public sealed class ObjectSerializer : IObjectSerializer
|
||||
|
||||
private (Id, Json)? SerializeDetachedBase(Base baseObj, Closures closures)
|
||||
{
|
||||
Closures childClosures;
|
||||
Id id;
|
||||
Json json;
|
||||
//avoid multiple serialization to get closures
|
||||
if (baseObj.id != null && _childCache.TryGetValue(new(baseObj.id), out var info))
|
||||
{
|
||||
id = new Id(baseObj.id);
|
||||
childClosures = info.GetClosures(_cancellationToken);
|
||||
json = info.Json;
|
||||
closures.IncrementClosures(childClosures);
|
||||
}
|
||||
else
|
||||
{
|
||||
childClosures = [];
|
||||
var sb = Pools.StringBuilders.Get();
|
||||
using var writer = new StringWriter(sb);
|
||||
using var jsonWriter = SpeckleObjectSerializerPool.Instance.GetJsonTextWriter(writer);
|
||||
id = SerializeBaseWithClosures(baseObj, jsonWriter, childClosures, true);
|
||||
closures.IncrementClosures(childClosures);
|
||||
json = new Json(writer.ToString());
|
||||
Pools.StringBuilders.Return(sb);
|
||||
}
|
||||
Closures childClosures = [];
|
||||
var sb = Pools.StringBuilders.Get();
|
||||
using var writer = new StringWriter(sb);
|
||||
using var jsonWriter = SpeckleObjectSerializerPool.Instance.GetJsonTextWriter(writer);
|
||||
var id = SerializeBaseWithClosures(baseObj, jsonWriter, childClosures, true);
|
||||
var json = new Json(writer.ToString());
|
||||
Pools.StringBuilders.Return(sb);
|
||||
closures.IncrementClosures(childClosures);
|
||||
var json2 = ReferenceGenerator.CreateReference(id);
|
||||
closures.MergeClosure(id);
|
||||
// add to obj refs to return
|
||||
|
||||
@@ -12,6 +12,6 @@ public class ObjectSerializerFactory(IBasePropertyGatherer propertyGatherer) : I
|
||||
private readonly Pool<List<DataChunk>> _chunk2Pool = Pools.CreateListPool<DataChunk>();
|
||||
private readonly Pool<List<object?>> _chunk3Pool = Pools.CreateListPool<object?>();
|
||||
|
||||
public IObjectSerializer Create(IReadOnlyDictionary<Id, NodeInfo> baseCache, CancellationToken cancellationToken) =>
|
||||
new ObjectSerializer(propertyGatherer, baseCache, _chunkPool, _chunk2Pool, _chunk3Pool, cancellationToken);
|
||||
public IObjectSerializer Create(CancellationToken cancellationToken) =>
|
||||
new ObjectSerializer(propertyGatherer, _chunkPool, _chunk2Pool, _chunk3Pool, cancellationToken);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.InterfaceGenerator;
|
||||
@@ -16,8 +15,8 @@ public record SerializeProcessOptions(
|
||||
bool SkipFindTotalObjects = false
|
||||
)
|
||||
{
|
||||
public int? MaxHttpSendSize { get; set; }
|
||||
public int? MaxCacheSize { get; set; }
|
||||
public int? MaxHttpSendBatchSize { get; set; }
|
||||
public int? MaxCacheBatchSize { get; set; }
|
||||
public int? MaxParallelism { get; set; }
|
||||
}
|
||||
|
||||
@@ -37,8 +36,8 @@ public sealed class SerializeProcess(
|
||||
IBaseChildFinder baseChildFinder,
|
||||
IBaseSerializer baseSerializer,
|
||||
ILoggerFactory loggerFactory,
|
||||
CancellationToken cancellationToken,
|
||||
SerializeProcessOptions? options = null
|
||||
SerializeProcessOptions options,
|
||||
CancellationToken cancellationToken
|
||||
) : ISerializeProcess
|
||||
{
|
||||
private static readonly Dictionary<Id, NodeInfo> EMPTY_CLOSURES = new();
|
||||
@@ -47,6 +46,7 @@ public sealed class SerializeProcess(
|
||||
cancellationToken
|
||||
);
|
||||
private readonly ILogger<SerializeProcess> _logger = loggerFactory.CreateLogger<SerializeProcess>();
|
||||
private bool _disposed;
|
||||
|
||||
//async dispose
|
||||
[SuppressMessage("Usage", "CA2213:Disposable fields should be disposed")]
|
||||
@@ -63,13 +63,8 @@ public sealed class SerializeProcess(
|
||||
ThreadPriority.BelowNormal,
|
||||
Environment.ProcessorCount * 2
|
||||
);
|
||||
private readonly SerializeProcessOptions _options = options ?? new();
|
||||
|
||||
private readonly Pool<Dictionary<Id, NodeInfo>> _currentClosurePool = Pools.CreateDictionaryPool<Id, NodeInfo>();
|
||||
private readonly Pool<ConcurrentDictionary<Id, NodeInfo>> _childClosurePool = Pools.CreateConcurrentDictionaryPool<
|
||||
Id,
|
||||
NodeInfo
|
||||
>();
|
||||
|
||||
private readonly Pool<List<Task<Dictionary<Id, NodeInfo>>>> _taskResultPool = Pools.CreateListPool<
|
||||
Task<Dictionary<Id, NodeInfo>>
|
||||
@@ -83,6 +78,7 @@ public sealed class SerializeProcess(
|
||||
[AutoInterfaceIgnore]
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
_disposed = true;
|
||||
await WaitForSchedulerCompletion().ConfigureAwait(false);
|
||||
await _highest.DisposeAsync().ConfigureAwait(false);
|
||||
await _belowNormal.DisposeAsync().ConfigureAwait(false);
|
||||
@@ -95,7 +91,7 @@ public sealed class SerializeProcess(
|
||||
//order here matters...null with cancellation means a user did it, otherwise it's a real Exception
|
||||
if (objectSaver.Exception is not null)
|
||||
{
|
||||
throw new SpeckleException("Error while sending", objectSaver.Exception);
|
||||
throw new SpeckleException($"Error while sending: {objectSaver.Exception.Message}", objectSaver.Exception);
|
||||
}
|
||||
_processSource.Token.ThrowIfCancellationRequested();
|
||||
}
|
||||
@@ -111,13 +107,13 @@ public sealed class SerializeProcess(
|
||||
try
|
||||
{
|
||||
var channelTask = objectSaver.Start(
|
||||
options?.MaxParallelism,
|
||||
options?.MaxHttpSendSize,
|
||||
options?.MaxCacheSize,
|
||||
options.MaxParallelism,
|
||||
options.MaxHttpSendBatchSize,
|
||||
options.MaxCacheBatchSize,
|
||||
_processSource.Token
|
||||
);
|
||||
var findTotalObjectsTask = Task.CompletedTask;
|
||||
if (!_options.SkipFindTotalObjects)
|
||||
if (!options.SkipFindTotalObjects)
|
||||
{
|
||||
ThrowIfFailed();
|
||||
findTotalObjectsTask = Task.Factory.StartNew(
|
||||
@@ -148,7 +144,7 @@ public sealed class SerializeProcess(
|
||||
|
||||
private void TraverseTotal(Base obj)
|
||||
{
|
||||
if (_processSource.Token.IsCancellationRequested)
|
||||
if (IsCancelled())
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -162,7 +158,7 @@ public sealed class SerializeProcess(
|
||||
|
||||
private async Task<Dictionary<Id, NodeInfo>> Traverse(Base obj)
|
||||
{
|
||||
if (_processSource.Token.IsCancellationRequested)
|
||||
if (IsCancelled())
|
||||
{
|
||||
return EMPTY_CLOSURES;
|
||||
}
|
||||
@@ -174,7 +170,7 @@ public sealed class SerializeProcess(
|
||||
{
|
||||
// tmp is necessary because of the way closures close over loop variables
|
||||
var tmp = child;
|
||||
if (_processSource.Token.IsCancellationRequested)
|
||||
if (IsCancelled())
|
||||
{
|
||||
return EMPTY_CLOSURES;
|
||||
}
|
||||
@@ -191,7 +187,7 @@ public sealed class SerializeProcess(
|
||||
tasks.Add(t);
|
||||
}
|
||||
|
||||
if (_processSource.Token.IsCancellationRequested)
|
||||
if (IsCancelled())
|
||||
{
|
||||
return EMPTY_CLOSURES;
|
||||
}
|
||||
@@ -218,61 +214,61 @@ public sealed class SerializeProcess(
|
||||
}
|
||||
_taskResultPool.Return(tasks);
|
||||
|
||||
if (_processSource.Token.IsCancellationRequested)
|
||||
if (IsCancelled())
|
||||
{
|
||||
return EMPTY_CLOSURES;
|
||||
}
|
||||
|
||||
var childClosures = _childClosurePool.Get();
|
||||
foreach (var childClosure in taskClosures)
|
||||
{
|
||||
if (IsCancelled())
|
||||
{
|
||||
return EMPTY_CLOSURES;
|
||||
}
|
||||
foreach (var kvp in childClosure)
|
||||
{
|
||||
childClosures[kvp.Key] = kvp.Value;
|
||||
if (IsCancelled())
|
||||
{
|
||||
return EMPTY_CLOSURES;
|
||||
}
|
||||
}
|
||||
|
||||
_currentClosurePool.Return(childClosure);
|
||||
}
|
||||
|
||||
if (_processSource.Token.IsCancellationRequested)
|
||||
if (IsCancelled())
|
||||
{
|
||||
return EMPTY_CLOSURES;
|
||||
}
|
||||
|
||||
var items = baseSerializer.Serialise(obj, childClosures, _options.SkipCacheRead, _processSource.Token);
|
||||
var items = baseSerializer.Serialise(obj, options.SkipCacheRead, _processSource.Token);
|
||||
|
||||
if (_processSource.Token.IsCancellationRequested)
|
||||
if (IsCancelled())
|
||||
{
|
||||
return EMPTY_CLOSURES;
|
||||
}
|
||||
|
||||
var currentClosures = _currentClosurePool.Get();
|
||||
try
|
||||
|
||||
Interlocked.Increment(ref _objectCount);
|
||||
progress?.Report(new(ProgressEvent.FromCacheOrSerialized, _objectCount, Math.Max(_objectCount, _objectsFound)));
|
||||
foreach (var item in items)
|
||||
{
|
||||
Interlocked.Increment(ref _objectCount);
|
||||
progress?.Report(new(ProgressEvent.FromCacheOrSerialized, _objectCount, Math.Max(_objectCount, _objectsFound)));
|
||||
foreach (var item in items)
|
||||
if (IsCancelled())
|
||||
{
|
||||
if (item.NeedsStorage)
|
||||
{
|
||||
if (_processSource.Token.IsCancellationRequested)
|
||||
{
|
||||
return EMPTY_CLOSURES;
|
||||
}
|
||||
|
||||
Interlocked.Increment(ref _objectsSerialized);
|
||||
await objectSaver.SaveAsync(item).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!currentClosures.ContainsKey(item.Id))
|
||||
{
|
||||
currentClosures.Add(item.Id, new NodeInfo(item.Json, item.Closures));
|
||||
}
|
||||
return EMPTY_CLOSURES;
|
||||
}
|
||||
|
||||
if (item.NeedsStorage)
|
||||
{
|
||||
Interlocked.Increment(ref _objectsSerialized);
|
||||
await objectSaver.SaveAsync(item).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!currentClosures.ContainsKey(item.Id))
|
||||
{
|
||||
currentClosures.Add(item.Id, new NodeInfo(item.Json, item.Closures));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_childClosurePool.Return(childClosures);
|
||||
}
|
||||
|
||||
return currentClosures;
|
||||
@@ -290,6 +286,8 @@ public sealed class SerializeProcess(
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsCancelled() => _disposed || _processSource.IsCancellationRequested;
|
||||
|
||||
public void RecordException(Exception e)
|
||||
{
|
||||
if (e is OperationCanceledException)
|
||||
@@ -304,6 +302,11 @@ public sealed class SerializeProcess(
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (IsCancelled())
|
||||
{
|
||||
//if we are already cancelled, don't log or save the exceptions
|
||||
return;
|
||||
}
|
||||
//order here matters
|
||||
_logger.LogError(e, "Error in SDK: {message}", e.Message);
|
||||
objectSaver.Exception = e;
|
||||
|
||||
@@ -44,13 +44,14 @@ public class SerializeProcessFactory(
|
||||
sqLiteJsonCacheManager,
|
||||
serverObjectManager,
|
||||
loggerFactory.CreateLogger<ObjectSaver>(),
|
||||
options ?? new SerializeProcessOptions(),
|
||||
cancellationToken
|
||||
),
|
||||
baseChildFinder,
|
||||
new BaseSerializer(sqLiteJsonCacheManager, objectSerializerFactory),
|
||||
loggerFactory,
|
||||
cancellationToken,
|
||||
options
|
||||
options ?? new SerializeProcessOptions(),
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
public ISerializeProcess CreateSerializeProcess(
|
||||
@@ -64,18 +65,10 @@ public class SerializeProcessFactory(
|
||||
#pragma warning disable CA2000
|
||||
var memoryJsonCacheManager = new MemoryJsonCacheManager(jsonCache);
|
||||
#pragma warning restore CA2000
|
||||
return new SerializeProcess(
|
||||
return CreateSerializeProcess(
|
||||
memoryJsonCacheManager,
|
||||
new MemoryServerObjectManager(objects),
|
||||
progress,
|
||||
new ObjectSaver(
|
||||
progress,
|
||||
memoryJsonCacheManager,
|
||||
new MemoryServerObjectManager(objects),
|
||||
loggerFactory.CreateLogger<ObjectSaver>(),
|
||||
cancellationToken
|
||||
),
|
||||
baseChildFinder,
|
||||
new BaseSerializer(memoryJsonCacheManager, objectSerializerFactory),
|
||||
loggerFactory,
|
||||
cancellationToken,
|
||||
options
|
||||
);
|
||||
|
||||
@@ -51,6 +51,7 @@ public class ServerObjectManager : IServerObjectManager
|
||||
|
||||
public async IAsyncEnumerable<(string, string)> DownloadObjects(
|
||||
IReadOnlyCollection<string> objectIds,
|
||||
string? attributeMask,
|
||||
IProgress<ProgressArgs>? progress,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken
|
||||
)
|
||||
@@ -59,10 +60,14 @@ public class ServerObjectManager : IServerObjectManager
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
using var childrenHttpMessage = new HttpRequestMessage();
|
||||
childrenHttpMessage.RequestUri = new Uri($"/api/getobjects/{_streamId}", UriKind.Relative);
|
||||
childrenHttpMessage.RequestUri = new Uri($"/api/v2/projects/{_streamId}/object-stream/", UriKind.Relative);
|
||||
childrenHttpMessage.Method = HttpMethod.Post;
|
||||
|
||||
Dictionary<string, string> postParameters = new() { { "objects", JsonConvert.SerializeObject(objectIds) } };
|
||||
Dictionary<string, object> postParameters = new() { { "objectIds", objectIds } };
|
||||
if (!string.IsNullOrWhiteSpace(attributeMask))
|
||||
{
|
||||
postParameters.Add("attributeMask", attributeMask.NotNull());
|
||||
}
|
||||
string serializedPayload = JsonConvert.SerializeObject(postParameters);
|
||||
childrenHttpMessage.Content = new StringContent(serializedPayload, Encoding.UTF8, "application/json");
|
||||
childrenHttpMessage.Headers.Add("Accept", "text/plain");
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Speckle.Sdk.Api;
|
||||
using Speckle.Sdk.Api.Blob;
|
||||
using Speckle.Sdk.Credentials;
|
||||
using Speckle.Sdk.Dependencies;
|
||||
using Speckle.Sdk.Host;
|
||||
@@ -86,6 +87,7 @@ public static class ServiceRegistration
|
||||
typeof(ServerApi),
|
||||
typeof(SqLiteJsonCacheManager),
|
||||
typeof(ServerObjectManager),
|
||||
typeof(BlobApi),
|
||||
typeof(BaseSerializer),
|
||||
typeof(SerializeProcess),
|
||||
typeof(ObjectSaver),
|
||||
|
||||
@@ -3,9 +3,9 @@ using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Timers;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Speckle.Sdk.Caching;
|
||||
using Speckle.Sdk.Logging;
|
||||
using Speckle.Sdk.Models;
|
||||
using Speckle.Sdk.Serialisation.Utilities;
|
||||
using Timer = System.Timers.Timer;
|
||||
|
||||
namespace Speckle.Sdk.Transports;
|
||||
@@ -28,7 +28,7 @@ public sealed class SQLiteTransport2 : IDisposable, ICloneable, ITransport, IBlo
|
||||
{
|
||||
_streamId = streamId;
|
||||
|
||||
_rootPath = SqlitePaths.GetDBPath(streamId);
|
||||
_rootPath = ModelCacheManager.GetDbPath(streamId);
|
||||
|
||||
_connectionString = $"Data Source={_rootPath};";
|
||||
|
||||
@@ -50,7 +50,7 @@ public sealed class SQLiteTransport2 : IDisposable, ICloneable, ITransport, IBlo
|
||||
private SqliteConnection Connection { get; set; }
|
||||
private readonly SemaphoreSlim _connectionLock = new(1, 1);
|
||||
|
||||
public string BlobStorageFolder => SqlitePaths.BlobStorageFolder;
|
||||
public string BlobStorageFolder => SpecklePathProvider.UserSpeckleFolderPath;
|
||||
|
||||
public void SaveBlob(Blob obj)
|
||||
{
|
||||
|
||||
@@ -20,7 +20,7 @@ internal sealed class ProgressStream(
|
||||
{
|
||||
int n = _stream.Read(buffer, offset, count);
|
||||
_position += n;
|
||||
progress?.Report(new(ProgressEvent.DownloadBytes, _position, streamLength));
|
||||
progress?.Report(new(ProgressEvent.DownloadBytes, n, streamLength));
|
||||
return n;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ internal sealed class ProgressStream(
|
||||
{
|
||||
_stream.Write(buffer, offset, count);
|
||||
_position += count;
|
||||
progress?.Report(new(ProgressEvent.UploadBytes, _position, streamLength));
|
||||
progress?.Report(new(ProgressEvent.UploadBytes, count, streamLength));
|
||||
}
|
||||
|
||||
public override bool CanRead => true;
|
||||
|
||||
@@ -3,6 +3,13 @@
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="altcover" />
|
||||
<PackageReference Include="AwesomeAssertions" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit.assert" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Speckle.Sdk.Testing\Speckle.Sdk.Testing.csproj" />
|
||||
<ProjectReference Include="..\..\src\Speckle.Automate.Sdk\Speckle.Automate.Sdk.csproj" />
|
||||
|
||||
@@ -25,7 +25,9 @@ public sealed class AutomationContextTest : IAsyncLifetime
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
var serviceProvider = TestServiceSetup.GetServiceProvider();
|
||||
var serviceCollection = new ServiceCollection();
|
||||
serviceCollection.AddAutomateSdk();
|
||||
var serviceProvider = serviceCollection.BuildServiceProvider();
|
||||
_account = await Fixtures.SeedUser().ConfigureAwait(false);
|
||||
_client = serviceProvider.GetRequiredService<IClientFactory>().Create(_account);
|
||||
_runner = serviceProvider.GetRequiredService<IAutomationRunner>();
|
||||
@@ -42,7 +44,7 @@ public sealed class AutomationContextTest : IAsyncLifetime
|
||||
private async Task<AutomationRunData> AutomationRunData(Base testObject)
|
||||
{
|
||||
Project project = await _client.Project.Create(new("Automate function e2e test", null, ProjectVisibility.Public));
|
||||
const string BRANCH_NAME = "main";
|
||||
const string BRANCH_NAME = "Trigger";
|
||||
|
||||
var model = await _client.Model.Create(new(BRANCH_NAME, null, project.id));
|
||||
string modelId = model.id;
|
||||
|
||||
@@ -2,6 +2,28 @@
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net8.0": {
|
||||
"altcover": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.1, )",
|
||||
"resolved": "9.0.1",
|
||||
"contentHash": "aadciFNDT5bnylaYUkKal+s5hF7yU/lmZxImQWAlk1438iPqK1Uf79H5ylELpyLIU49HL5ql+tnWBihp3WVLCA=="
|
||||
},
|
||||
"AwesomeAssertions": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.1.0, )",
|
||||
"resolved": "8.1.0",
|
||||
"contentHash": "IfNC4cpXPi9tclWvuNO9lfkuIxJsUTLTS1NXto55jDrAUQJYl0zLI9ByISrfkbBE2Xtg+IWaAXQ6jnUx3anDuw=="
|
||||
},
|
||||
"Microsoft.NET.Test.Sdk": {
|
||||
"type": "Direct",
|
||||
"requested": "[17.13.0, )",
|
||||
"resolved": "17.13.0",
|
||||
"contentHash": "W19wCPizaIC9Zh47w8wWI/yxuqR7/dtABwOrc8r2jX/8mUNxM2vw4fXDh+DJTeogxV+KzKwg5jNNGQVwf3LXyA==",
|
||||
"dependencies": {
|
||||
"Microsoft.CodeCoverage": "17.13.0",
|
||||
"Microsoft.TestPlatform.TestHost": "17.13.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.0, )",
|
||||
@@ -24,6 +46,18 @@
|
||||
"resolved": "0.9.6",
|
||||
"contentHash": "HKH7tYrYYlCK1ct483hgxERAdVdMtl7gUKW9ijWXxA1UsYR4Z+TrRHYmzZ9qmpu1NnTycSrp005NYM78GDKV1w=="
|
||||
},
|
||||
"xunit.assert": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.9.3, )",
|
||||
"resolved": "2.9.3",
|
||||
"contentHash": "/Kq28fCE7MjOV42YLVRAJzRF0WmEqsmflm0cfpMjGtzQ2lR5mYVj1/i0Y8uDAOLczkL3/jArrwehfMD0YogMAA=="
|
||||
},
|
||||
"xunit.runner.visualstudio": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.0.2, )",
|
||||
"resolved": "3.0.2",
|
||||
"contentHash": "oXbusR6iPq0xlqoikjdLvzh+wQDkMv9If58myz9MEzldS4nIcp442Btgs2sWbYWV+caEluMe2pQCZ0hUZgPiow=="
|
||||
},
|
||||
"Argon": {
|
||||
"type": "Transitive",
|
||||
"resolved": "0.28.0",
|
||||
@@ -364,18 +398,6 @@
|
||||
"xunit.runner.visualstudio": "[3.0.2, )"
|
||||
}
|
||||
},
|
||||
"altcover": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[9.0.1, )",
|
||||
"resolved": "9.0.1",
|
||||
"contentHash": "aadciFNDT5bnylaYUkKal+s5hF7yU/lmZxImQWAlk1438iPqK1Uf79H5ylELpyLIU49HL5ql+tnWBihp3WVLCA=="
|
||||
},
|
||||
"AwesomeAssertions": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[8.1.0, )",
|
||||
"resolved": "8.1.0",
|
||||
"contentHash": "IfNC4cpXPi9tclWvuNO9lfkuIxJsUTLTS1NXto55jDrAUQJYl0zLI9ByISrfkbBE2Xtg+IWaAXQ6jnUx3anDuw=="
|
||||
},
|
||||
"GraphQL.Client": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[6.0.0, )",
|
||||
@@ -424,16 +446,6 @@
|
||||
"Microsoft.Extensions.Options": "2.2.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.NET.Test.Sdk": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[17.13.0, )",
|
||||
"resolved": "17.13.0",
|
||||
"contentHash": "W19wCPizaIC9Zh47w8wWI/yxuqR7/dtABwOrc8r2jX/8mUNxM2vw4fXDh+DJTeogxV+KzKwg5jNNGQVwf3LXyA==",
|
||||
"dependencies": {
|
||||
"Microsoft.CodeCoverage": "17.13.0",
|
||||
"Microsoft.TestPlatform.TestHost": "17.13.0"
|
||||
}
|
||||
},
|
||||
"Moq": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[4.20.72, )",
|
||||
@@ -515,18 +527,6 @@
|
||||
"xunit.assert": "2.9.3",
|
||||
"xunit.core": "[2.9.3]"
|
||||
}
|
||||
},
|
||||
"xunit.assert": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.9.3, )",
|
||||
"resolved": "2.9.3",
|
||||
"contentHash": "/Kq28fCE7MjOV42YLVRAJzRF0WmEqsmflm0cfpMjGtzQ2lR5mYVj1/i0Y8uDAOLczkL3/jArrwehfMD0YogMAA=="
|
||||
},
|
||||
"xunit.runner.visualstudio": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[3.0.2, )",
|
||||
"resolved": "3.0.2",
|
||||
"contentHash": "oXbusR6iPq0xlqoikjdLvzh+wQDkMv9If58myz9MEzldS4nIcp442Btgs2sWbYWV+caEluMe2pQCZ0hUZgPiow=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,7 @@ public sealed class ObjectsSerializationTest
|
||||
|
||||
private static IReadOnlyList<(Id, Json, Dictionary<Id, int>)> Serialize(Base data)
|
||||
{
|
||||
using var serializer = new ObjectSerializerFactory(new BasePropertyGatherer()).Create(
|
||||
new Dictionary<Id, NodeInfo>(),
|
||||
default
|
||||
);
|
||||
using var serializer = new ObjectSerializerFactory(new BasePropertyGatherer()).Create(default);
|
||||
return serializer.Serialize(data).ToList();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<TestProjectAnalyserRules>true</TestProjectAnalyserRules>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Speckle.Sdk\Speckle.Sdk.csproj" />
|
||||
|
||||
@@ -41,7 +41,7 @@ public class DataObjectTests
|
||||
new DummyServerObjectManager(),
|
||||
null,
|
||||
default,
|
||||
new SerializeProcessOptions(true, true, false, true)
|
||||
new SerializeProcessOptions(false, false, true, true)
|
||||
);
|
||||
await serializeProcess.Serialize(x);
|
||||
await VerifyJson(json.Single().Value.Value).UseParameters(type);
|
||||
|
||||
@@ -41,7 +41,7 @@ public class DetachedTests
|
||||
objects,
|
||||
null,
|
||||
default,
|
||||
new SerializeProcessOptions(false, false, true, true)
|
||||
new SerializeProcessOptions(true, true, false, true)
|
||||
);
|
||||
await serializeProcess.Serialize(@base);
|
||||
|
||||
@@ -123,7 +123,7 @@ public class DetachedTests
|
||||
objects,
|
||||
null,
|
||||
default,
|
||||
new SerializeProcessOptions(false, false, true, true) { MaxParallelism = 1, MaxHttpSendSize = 1 }
|
||||
new SerializeProcessOptions(true, true, false, true) { MaxParallelism = 1, MaxHttpSendBatchSize = 1 }
|
||||
);
|
||||
var results = await serializeProcess.Serialize(@base);
|
||||
|
||||
@@ -150,7 +150,7 @@ public class DetachedTests
|
||||
objects,
|
||||
null,
|
||||
default,
|
||||
new SerializeProcessOptions(false, false, true, true) { MaxParallelism = 1, MaxHttpSendSize = 1 }
|
||||
new SerializeProcessOptions(true, true, false, true) { MaxParallelism = 1, MaxHttpSendBatchSize = 1 }
|
||||
);
|
||||
var results = await serializeProcess.Serialize(@base);
|
||||
|
||||
@@ -172,7 +172,7 @@ public class DetachedTests
|
||||
objects,
|
||||
null,
|
||||
default,
|
||||
new SerializeProcessOptions(false, false, true, true) { MaxParallelism = 1, MaxHttpSendSize = 1 }
|
||||
new SerializeProcessOptions(true, true, false, true) { MaxParallelism = 1, MaxHttpSendBatchSize = 1 }
|
||||
);
|
||||
var results = await serializeProcess.Serialize(@base);
|
||||
|
||||
@@ -239,7 +239,7 @@ public class DetachedTests
|
||||
objects,
|
||||
null,
|
||||
default,
|
||||
new SerializeProcessOptions(false, false, true, true)
|
||||
new SerializeProcessOptions(true, true, false, true)
|
||||
);
|
||||
|
||||
var results = await serializeProcess.Serialize(@base);
|
||||
@@ -272,7 +272,7 @@ public class DetachedTests
|
||||
objects,
|
||||
null,
|
||||
default,
|
||||
new SerializeProcessOptions(false, false, true, true)
|
||||
new SerializeProcessOptions(true, true, false, true)
|
||||
);
|
||||
var results = await serializeProcess.Serialize(@base);
|
||||
await VerifyJsonDictionary(objects);
|
||||
@@ -338,6 +338,7 @@ public class DummyServerObjectManager : IServerObjectManager
|
||||
{
|
||||
public IAsyncEnumerable<(string, string)> DownloadObjects(
|
||||
IReadOnlyCollection<string> objectIds,
|
||||
string? attributeMask,
|
||||
IProgress<ProgressArgs>? progress,
|
||||
CancellationToken cancellationToken
|
||||
) => throw new NotImplementedException();
|
||||
|
||||
+1
-1
@@ -6,6 +6,6 @@
|
||||
"Message": "The method or operation is not implemented.",
|
||||
"Type": "NotImplementedException"
|
||||
},
|
||||
"Message": "Error while sending",
|
||||
"Message": "Error while sending: The method or operation is not implemented.",
|
||||
"Type": "SpeckleException"
|
||||
}
|
||||
|
||||
+1
-1
@@ -6,6 +6,6 @@
|
||||
"Message": "Count exceeded",
|
||||
"Type": "Exception"
|
||||
},
|
||||
"Message": "Error while sending",
|
||||
"Message": "Error while sending: Count exceeded",
|
||||
"Type": "SpeckleException"
|
||||
}
|
||||
|
||||
+1
-1
@@ -6,6 +6,6 @@
|
||||
"Message": "The method or operation is not implemented.",
|
||||
"Type": "NotImplementedException"
|
||||
},
|
||||
"Message": "Error while loading",
|
||||
"Message": "Error while loading: The method or operation is not implemented.",
|
||||
"Type": "SpeckleException"
|
||||
}
|
||||
|
||||
+1
-1
@@ -6,6 +6,6 @@
|
||||
"Message": "The method or operation is not implemented.",
|
||||
"Type": "NotImplementedException"
|
||||
},
|
||||
"Message": "Error while sending",
|
||||
"Message": "Error while sending: The method or operation is not implemented.",
|
||||
"Type": "SpeckleException"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user