diff --git a/.gitignore b/.gitignore
index bc898eaa..f51b449b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,7 +2,10 @@
.DS_Store
# Docs
-public
+__pycache__
+docs/build
+/public
+js/public
# JS
node_modules
@@ -11,17 +14,24 @@ tsconfig.tsbuildinfo
dist
# JS CLI
-js/cli/proton-drive
+js/cli/release
auth.txt
+auth-session.json
cache*.sqlite
+events.json
+events.lock
+clientUid.json
*.log
*.bun-build
+config.json
+*.map
-# VS Code
+# IDEs
.vs
+.vscode
+.idea
+*.swp
# Tests
tests/storage
-
-# Test reporter
tests/test-results
diff --git a/LICENSE.md b/LICENSE.md
index c9e43613..8f8e23f2 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,6 +1,6 @@
The MIT License
-Copyright (c) 2025 Proton AG
+Copyright (c) 2025-2026 Proton AG
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 596f51ef..6b5206a5 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,102 @@
# Proton Drive SDK
-Copyright (c) 2025 Proton AG
+The Proton Drive SDK provides a high-level interface for interacting with Proton Drive. It is available in the following languages:
-This repository contains the Proton Drive SDK for JavaScript and C# (coming soon).
+- **TypeScript** — native SDK in [`js/sdk/`](./js/sdk/), available on npm as [`@protontech/drive-sdk`](https://www.npmjs.com/package/@protontech/drive-sdk). See [changelog](./js/CHANGELOG.md) for changes.
+- **C#** — native SDK in [`cs/sdk/`](./cs/sdk/). See [changelog](./cs/CHANGELOG.md) for changes.
+- **Kotlin** — bindings that wrap the C# SDK in [`kt/`](./kt/). See [changelog](./cs/CHANGELOG.md) for changes to the C# SDK.
+- **Swift** - bindings that wrap the C# SDK in [`swift/ProtonDriveSDK/`](./swift/ProtonDriveSDK/), available on github as [`sdk-swift`](https://github.com/ProtonDriveApps/sdk-swift). See [changelog](./cs/CHANGELOG.md) for changes to the C# SDK.
-At this point, the SDK is not ready for use. It is a work in progress.
+### Who this is for
-## Contributions
+| Audience | Expectations |
+| --- | --- |
+| **Proton first-party clients** | Primary focus today: this codebase is built for and validated alongside official Proton Drive apps. |
+| **Personal, non-commercial projects** | Allowed under [Guidelines](#usage-guidelines-for-personal-projects) below. Expect interface changes and the upcoming cryptographic migration until general availability. |
+| **Commercial or production third-party apps** | The SDK is not yet ready for third-party production use. |
-Contributions are not accepted at the moment.
+Using the SDK directly is still recommended over raw Drive API calls for any experimentation, so correctness, safety and rate-limit expectations stay aligned with first-party behavior. The SDK handles encryption and metadata processing, protecting uploaded data from corruption due to incorrect encryption or invalid metadata.
+
+## Current Status
+
+The SDK is actively being integrated into official Proton Drive clients. During this phase, the architecture and public interface may still change.
+
+**Upcoming cryptographic model change**:
+
+- **What changes:** Proton Drive will move to a new cryptographic model that improves performance, simplifies the architecture, and strengthens security.
+- **When:** Currently targeted for the **end of 2026/early 2027**. This window is an estimate and may shift; final timing and migration steps will be documented in this README and in the changelogs when they are finalized.
+- **What breaks:** Once the service uses the new model, any client that only implements the previous cryptography including older SDK releases will **not** interoperate until upgraded to a release that implements the new model.
+- **How to stay informed:** Watch this repository and read changelogs and README for migration notes and definitive dates.
+
+Once these changes are complete and the integration is stable, the SDK will be officially released for third-party use.
+
+Despite not being officially supported for third-party use at present, Proton strongly recommends integrating through this SDK rather than calling the Drive API directly. It is the same implementation used in Proton's first-party clients and is maintained to the same quality standards, even while the public interface continues to evolve. If you integrate without the SDK, you must still follow those guidelines; non-compliant clients may be rate-limited or blocked to protect Proton Drive and other users.
+
+## Usage Guidelines for Personal Projects
+
+The SDK may be used for personal, non-commercial projects. If you choose to build an application using Proton Drive, you **must** adhere to the requirements below.
+
+### Operational requirements
+
+These rules protect service availability and honest identification of clients. Rate limits are per session and user, thus third-party applications use the **same rate-limiting policy** as Proton first-party Drive clients.
+
+| Requirement | Description |
+| --- | --- |
+| **Use the SDK** | You are strongly encouraged to interact with Proton Drive through the SDK. If you make direct API calls, your application **must** implement the same correctness and safety guarantees as the SDK. Failing to use appropriate caching, event-based sync, parallelism limits, and exponential backoff may cause your application to be rate-limited to protect service availability. |
+| **Use official endpoints** | All HTTP requests must go to the official Proton Drive domain. Do not modify or proxy API endpoints to different domains. |
+| **Identify your application** | Set the `x-pm-appversion` HTTP header so it identifies your build honestly. Use the shape described below (for example, `external-drive-myapp@1.2.3-stable`). The value must accurately represent your application. Do not spoof or falsify this header. Third-party clients that seek to masquerade as official Proton first-party clients are forbidden and may stop working at any time. Customer support and development use the reported app version to troubleshoot requests; a **specific version may be blocked** if it is known to ship a serious bug. |
+| **Use event-based sync** | Synchronize data using Drive events. Do not poll the API or perform frequent recursive traversals of the file tree. Excessive polling or recursion may cause your application and your account to be rate-limited to protect service availability. |
+
+Use this pattern for `x-pm-appversion`:
+
+`external-drive-{name}@{semver}-{channel}+{suffix}` with optional SemVer build metadata `+{suffix}` (for example a short commit hash).
+
+- **`{name}`** — your project identifier using lowercase letters and underscores (e.g. `my_app`).
+- **`{semver}`** — `major.minor.patch` (e.g. `1.2.3`).
+- (optional) **`{channel}`** — one of `stable`, `beta`, or `alpha`.
+- (optional) **`+{suffix}`** — build metadata, for example a short commit hash (e.g. `+abc123f`).
+
+Examples:
+
+- `external-drive-myapp@1.2.3-stable`
+- `external-drive-my_app@2.0.0-beta`
+- `external-drive-photo_backup@1.0.0-alpha+abc123f`
+
+### Product and legal requirements
+
+These rules keep third-party apps distinguishable from official Proton products and transparent to users.
+
+| Requirement | Description |
+| --- | --- |
+| **No Proton branding** | Your application must not use Proton logos, trademarks, or design elements. It must be clearly distinguishable as an unofficial, third-party product. |
+| **Credential handling disclosure** | When you prompt a user for account details (including but not limited to username and password) your application must clearly state that it is a third-party application not officially supported by Proton. Suggested text: _This is a third-party application not officially supported by Proton._ |
+
+To protect the availability of Proton Drive and to properly safeguard the Proton customer experience, failure to comply with these requirements may result in your third-party application being limited or blocked from accessing Proton services. If you believe your third-party application has been improperly limited and/or blocked, please contact customer support on [proton.me/support/contact](https://proton.me/support/contact).
+
+## Scope and Limitations
+
+The SDK provides functionality for Proton Drive business logic only. It does **not** include:
+
+- Authentication or login flows
+- Session management
+- User address provider
+
+**Where to look first:** Official Proton Drive clients wire these pieces into the SDK; treat them as the living reference until this repository publishes standalone sample apps. Standalone integration support will be documented once the SDK reaches general availability.
+
+## Documentation
+
+We are preparing the documentation for the SDK. It will be available in the future.
+
+Until then, you can generate the code reference for the TypeScript SDK using the following command:
+
+```bash
+cd js/sdk && OUTPUT_PATH=./doc npm run generate-doc:interface
+```
+
+## License
+
+This project is licensed under the MIT License. See [LICENSE.md](./LICENSE.md) for details.
+
+> **Using Proton’s hosted services:** The MIT license governs **use of the source code in this repository** only. Access to **Proton’s hosted services** (including Proton Drive) remains subject to separate terms of service and operational policies. Integration rules and enforcement described in this README apply regardless of the OSS license.
+
+Copyright (c) 2026 Proton AG
diff --git a/SECURITY.md b/SECURITY.md
index 7b5ad73d..46e68856 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,3 +1,7 @@
# Security
-TBD
+If you discover a security vulnerability in the Proton Drive SDK, please report it through our official bug bounty program:
+
+**[Proton Bug Bounty Program](https://proton.me/security/bug-bounty)**
+
+For general security inquiries, you can reach us at [security@proton.me](mailto:security@proton.me).
diff --git a/cs/.editorconfig b/cs/.editorconfig
index a9508694..70312fa4 100644
--- a/cs/.editorconfig
+++ b/cs/.editorconfig
@@ -15,7 +15,6 @@ indent_style = space
tab_width = 4
# New line preferences
-end_of_line = crlf
insert_final_newline = true
#### .NET Coding Conventions ####
@@ -130,6 +129,8 @@ csharp_new_line_before_members_in_object_initializers = false
csharp_new_line_before_open_brace = all
csharp_new_line_between_query_expression_clauses = true
+roslynator_max_line_length = 160
+
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
diff --git a/cs/.gitignore b/cs/.gitignore
index a1996f32..875100c3 100644
--- a/cs/.gitignore
+++ b/cs/.gitignore
@@ -351,6 +351,11 @@ vcpkg_installed/
build/output
.vscode
+# DocFX
+docfx/api/
+docfx/_site/
+.tools/
+
# macOS
*.DS_Store
.AppleDouble
diff --git a/cs/.globalconfig b/cs/.globalconfig
index 01e77d9b..cb95dd6e 100644
--- a/cs/.globalconfig
+++ b/cs/.globalconfig
@@ -3,10 +3,49 @@ is_global = true
stylecop.layout.allowConsecutiveUsings = true
stylecop.layout.allowDoWhileOnClosingBrace = true
+# IDE0001: Simplify Names
+dotnet_diagnostic.IDE0001.severity = warning
+
+# IDE0002: Simplify Member Access
+dotnet_diagnostic.IDE0002.severity = warning
+
+# IDE0003: Remove qualification
+dotnet_diagnostic.IDE0003.severity = warning
+
+# IDE0004: Remove Unnecessary Cast
+dotnet_diagnostic.IDE0004.severity = warning
+
+# IDE0005: Using directive is unnecessary.
+dotnet_diagnostic.IDE0005.severity = warning
+
+# IDE0007: Use var instead of explicit type
+dotnet_diagnostic.IDE0007.severity = warning
+
+# IDE0028: Use collection initializers or expressions
+dotnet_diagnostic.IDE0028.severity = warning
+
+# IDE0047: Remove unnecessary parentheses
+dotnet_diagnostic.IDE0047.severity = warning
+
+# CA1032: Implement standard exception constructors
dotnet_diagnostic.CA1032.severity = warning
-dotnet_diagnostic.CA1711.severity = suggestion
+
+# CS1591: Missing XML comment for publicly visible type or member
+dotnet_diagnostic.CS1591.severity = none
+
+# CA1711: Identifiers should not have incorrect suffix
+dotnet_diagnostic.CA1711.severity = warning
+
+# CA1849: Call async methods when in an async method
+dotnet_diagnostic.CA1849.severity = warning
+
+# CA2000: Dispose objects before losing scope
dotnet_diagnostic.CA2000.severity = suggestion
-dotnet_diagnostic.CA2201.severity = suggestion
+
+# CA2201: Do not raise reserved exception types
+dotnet_diagnostic.CA2201.severity = warning
+
+# CA2215: Dispose methods should call base class dispose
dotnet_diagnostic.CA2215.severity = warning
# StyleCop - Special
@@ -23,13 +62,13 @@ dotnet_diagnostic.SA1009.severity = suggestion
# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/ReadabilityRules.md
dotnet_diagnostic.SA1101.severity = none
-dotnet_diagnostic.SA1200.severity = none
-dotnet_diagnostic.SA1216.severity = none
-dotnet_diagnostic.SA1217.severity = none
-dotnet_diagnostic.SA1413.severity = none
-dotnet_diagnostic.SA1502.severity = none
dotnet_diagnostic.SA1516.severity = none
+# StyleCop - Naming
+# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/NamingRules.md
+
+dotnet_diagnostic.SA1309.severity = none
+
# StyleCop - Documentation
# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/DocumentationRules.md
@@ -77,70 +116,28 @@ dotnet_diagnostic.SA1648.severity = none
dotnet_diagnostic.SX1101.severity = warning
dotnet_diagnostic.SX1309.severity = warning
-dotnet_diagnostic.SA1309.severity = none
# Roslynator
-dotnet_diagnostic.RCS1037.severity = none # Redundant with SA1028
-dotnet_diagnostic.RCS1194.severity = none # Redundant with CA1032
-
-# To discuss, remove to enable:
-
-# IDE0058: Expression value is never used
-dotnet_diagnostic.IDE0058.severity = none
-
-# CA1806: Do not ignore method results
-dotnet_diagnostic.CA1806.severity = none
-
-# SA1005: Single line comments should begin with single space
-dotnet_diagnostic.SA1005.severity = suggestion
-
-# SA1108: Block statements should not contain embedded comments
-dotnet_diagnostic.SA1108.severity = suggestion
-
-# SA1111: Closing parenthesis should be on line of last parameter
-dotnet_diagnostic.SA1111.severity = suggestion
-
-# SA1122: Use string.Empty for empty strings
-dotnet_diagnostic.SA1122.severity = suggestion
-# SA1127: Generic type constraints should be on their own line
-dotnet_diagnostic.SA1127.severity = suggestion
+# RCS0056: A line is too long
+dotnet_diagnostic.RCS0056.severity = warning
-# SA1128: Put constructor initializers on their own line
-dotnet_diagnostic.SA1128.severity = suggestion
-
-# SA1201: Elements should appear in the correct order
-dotnet_diagnostic.SA1201.severity = suggestion
-
-# SA1202: Elements should be ordered by access
-dotnet_diagnostic.SA1202.severity = suggestion
-
-# SA1204: Static elements should appear before instance elements
-dotnet_diagnostic.SA1204.severity = suggestion
-
-# SA1214: Readonly fields should appear before non-readonly fields
-dotnet_diagnostic.SA1214.severity = suggestion
-
-# SA1408: Conditional expressions should declare precedence
-dotnet_diagnostic.SA1408.severity = suggestion
-
-# SA1503: Braces should not be omitted
-dotnet_diagnostic.SA1503.severity = suggestion
+# RCS1037: Remove trailing white-space
+dotnet_diagnostic.RCS1037.severity = none # Redundant with SA1028
-# SA1512: Single-line comments should not be followed by blank line
-dotnet_diagnostic.SA1512.severity = suggestion
+# RCS1047: Non-asynchronous method name should not end with 'Async'
+dotnet_diagnostic.RCS1047.severity = warning
-# SA1515: Single-line comment should be preceded by blank line
-dotnet_diagnostic.SA1515.severity = suggestion
+# RCS1118: Mark local variable as const
+dotnet_diagnostic.RCS1118.severity = warning
-# SA1519: Braces should not be omitted from multi-line child statement
-dotnet_diagnostic.SA1519.severity = suggestion
+# RCS1090: Add parameter to exception constructor
+dotnet_diagnostic.RCS1194.severity = none # Redundant with CA1032
-# RCS1139: Add summary element to documentation comment.
-dotnet_diagnostic.RCS1139.severity = suggestion
+# Sonar
-# RCS1181: Convert comment to documentation comment.
-dotnet_diagnostic.RCS1181.severity = none
+# S1134: Track uses of "FIXME" tags
+dotnet_diagnostic.S1134.severity = suggestion
# S1135: Complete the task associated to this 'TODO' comment
-dotnet_diagnostic.S1135.severity = suggestion
\ No newline at end of file
+dotnet_diagnostic.S1135.severity = suggestion
diff --git a/cs/CHANGELOG.md b/cs/CHANGELOG.md
new file mode 100644
index 00000000..7f9177e0
--- /dev/null
+++ b/cs/CHANGELOG.md
@@ -0,0 +1,577 @@
+# Changelog
+
+## cs/v0.16.0 (2026-05-29)
+
+* Add validation_error category for upload & download telemetry events
+* Support more modification time formats and report invalid ones as node errors
+
+## cs/v0.15.1 (2026-05-28)
+
+* Fix node secrets not being read from cache
+* Use photos API when fetching album and photo node details
+* Merge result error message with first error message
+* Fix E2E kotlin tests
+
+## cs/v0.15.0 (2026-05-27)
+
+* Report extended attributes size for download progress instead of revision size
+* Fix cache not evicting incompatible entries
+* Do not close the input stream in Swift's StreamForUpload
+* Fix interop account client requesting empty address instead of default address
+* Use single type hierarchy for nodes
+* Retry block encryption and report metric
+* Wrap node not found into a dedicated exception
+
+## cs/v0.14.6 (2026-05-22)
+
+* Make last modification time optional for file uploads
+* Show what was actually in the JSON when extended attributes cannot be parsed
+
+## cs/v0.14.5 (2026-05-18)
+
+* Fix missing disposal of reader in Sqlite cache repository
+* Fix error mapping for decryption
+
+## cs/v0.14.4 (2026-05-14)
+
+* Fix incorrect reporting of decryption errors
+
+## cs/v0.14.3 (2026-05-11)
+
+* Handle degraded folder secrets in upload and node operations
+* Classify HTTP response code 499 as server error
+* Flatten messages of decryption errors reported to telemetry
+* Reproduce content size mismatch
+* Add an E2E tests for conflict name with draft
+* Add info log for uploader and downloader
+* Fix upload failing to resume when blocks were uploaded out of order
+* Fix handling of mismatch between uploaded and intended sizes
+* Remove slash validation name after decryption
+
+## cs/v0.14.2 (2026-05-06)
+
+* Dispose upload controller in test to see events
+* Make cryptography time monotonic
+* Optional AccountClientProtocol + interop nil handling
+
+## cs/v0.14.1 (2026-05-01)
+
+* Refactor Proton API exception to consolidate constructor initialization
+* Add error for verification error event
+* Include error details in decryption telemetry events
+
+## cs/v0.14.0 (2026-04-28)
+
+* Fix name conflict handling regression
+* Reduce log level for draft deletion failure from error to warning
+* Evict non-deserializable entries from cache
+* Upgrade to .NET 10
+* Fix download queuing not blocking on full queue
+
+## cs/v0.13.8 (2026-04-27)
+
+* Fix nullable data in name conflict error
+* Add extension to aborted exception
+* Reduce log in controllers
+* Improve exception type names in error reports
+
+## cs/v0.13.7 (2026-04-23)
+
+* Remove unnecessary too many children exception
+* Log error when volume type is unknown
+
+## cs/v0.13.6 (2026-04-22)
+
+* Handle too many children exception when creating a new draft
+
+## cs/v0.13.5 (2026-04-22)
+
+* Add thumbnail error handling from API response
+* Ensure expected SHA1 provider is called only once during upload
+
+## cs/v0.13.4 (2026-04-20)
+
+* Improve download initialization speed by parallelizing some server round-trips
+* Add get node for Kotlin drive client
+* Fix memory leak on SHA1 provision through interop
+
+## cs/v0.13.3 (2026-04-16)
+
+* Fix failure to upload new revision on single file sharing
+
+## cs/v0.13.2 (2026-04-07)
+
+* Resume continuation only when active
+* Log network error and retries in kotlin
+
+## cs/v0.13.1 (2026-04-03)
+
+* Update logs from kotlin resume api
+* Fix feature flag parsing in kotlin
+
+## cs/v0.13.0 (2026-04-02)
+
+* Keep http request body in kotlin memory for retries
+* Fix illegal assignments of null values to Protobuf fields for authorship results
+* Enable streaming of results when enumerating folder children and Photos timeline
+* Fix function to get node from Photos client not using Photos API
+* Log network body for tests by chunk
+* Extract clients interfaces
+* Add trash management to Photos
+
+## cs/v0.12.0 (2026-03-30)
+
+* Remove get thumbnails in favor of enumerate thumbnails
+* Introduce uids in the kotlin bindings
+* Move native weak reference management to kotlin
+* Do not call interop functions if cancelled
+* Fix thumbnail enumeration to stay within API limits
+* Fix cancellation in download and upload
+* Log network calls with body size
+* Add streaming thumbnails enumeration to Swift bindings
+* Remove the need to dispose of Photos client
+
+## cs/v0.11.2 (2026-03-27)
+
+* Stream trash enumeration instead of loading all items at once
+* Fix regression in disposal of file transfer controllers
+* Update Swift binding to get trash error
+
+## cs/v0.11.1 (2026-03-25)
+
+* Wrap SDK exception into IO exception for android network library to handle it
+
+## cs/v0.11.0 (2026-03-24)
+
+* Surface non-resumable upload and download as typed exceptions
+
+## cs/v0.10.0 (2026-03-24)
+
+* Enable resuming of uploads from Swift bindings
+
+## cs/v0.9.4 (2026-03-23)
+
+* Fix wrong volume type for photo events
+* Expose structured data on upload integrity errors to Swift binding
+
+## cs/v0.9.3 (2026-03-23)
+
+* Mark checksum verified as optional in the api
+* Report checksum verification state to interop
+
+## cs/v0.9.2 (2026-03-20)
+
+* Report checksum verification state to back-end and client
+
+## cs/v0.9.1 (2026-03-20)
+
+* Report unmapped HTTP errors as Network errors instead of Unknown
+* Allow resuming download to non seekable data stream
+* Fix wrong link details endpoint being used for Photos
+* Improve error details for node decryption failures
+
+## cs/v0.9.0 (2026-03-20)
+
+* Fail node provision when parent key could not be obtained
+* Try all album inclusions to find the entry point key
+* Handle missing timestamps in photo upload metadata
+* Improve error details for drive errors
+* Remove failing test data
+* Fix telemetry causing deadlock on uploads and downloads
+* Expose structured data on upload integrity errors
+* Throw error if node is not found
+* Parse enumerate result synchronously
+* Clarify exception for missing node when looking up entry point
+* Fix setup for timeouts in test
+* Log number of ids when enumerate thumbnails
+
+## cs/v0.8.1 (2026-03-16)
+
+* Fix disposal of upload controller and update upload bindings api
+* Add streaming thumbnails enumeration for Drive and Photos clients
+* Update download event values for tests
+
+## cs/v0.8.0 (2026-03-12)
+
+* Implement upload to Photos
+* Set swift error message
+* Handle nullable OwnedBy fields when mapping to proto
+* Propagate individual thumbnail errors to callers instead of silently skipping them
+* Add owned by property
+
+## cs/v0.7.0-alpha.17 (2026-03-10)
+
+* Fix manifest verification errors due to wrong thumbnail order in manifest
+* Prevent resumed uploads from being paused by a stale previous attempt
+* Use java Instant instead for Long to describe time
+* Add interop and Kotlin bindings for trash management
+* Add context traversal for photo nodes and set telemetry volume type
+* Log failed attempts to report decryption errors to telemetry
+* Align telemetry with the web SDK
+
+## cs/v0.7.0-alpha.16 (2026-03-04)
+
+* Ensure cancelled uploads/downloads don't block queue
+
+## cs/v0.7.0-alpha.15 (2026-03-03)
+
+* Fix registry not removing objects when the removeAll call happens from the owner's deinit
+
+## cs/v0.7.0-alpha.14 (2026-03-02)
+
+* Improve the way drafts are considered non-resumable to pass through original exceptions
+
+## cs/v0.7.0-alpha.13 (2026-03-02)
+
+* Add Kotlin bindings for trash nodes
+* Test should not failed when SDK is aborted
+* Improve error reporting for trash and restore operations
+* Fix second-attempt file upload failing due to signature key disposal
+* Categorize upload integrity exception properly
+
+## cs/v0.7.0-alpha.12 (2026-02-25)
+
+* Transmit api codes through interop
+* Provide clearer context when canceling operations
+* Improve error reporting with full exception details
+* Clean native memory of weak references after release
+* Fix failures due to empty authorship results on degraded nodes
+* Clean native memory of global weak references
+* Upgrade android core to the last version (36.3.0)
+* Fix value type check
+* Set caller exception as cause to be reported in Sentry
+* Add context to timestamp conversion errors
+* Raise the timeout to 5min to upload 100MB file
+* Log progress as percentage
+* Accept null content key signatures
+
+## cs/v0.7.0-alpha.11 (2026-02-18)
+
+* Fix download of photos and their thumbnails from shared albums
+* Capture caller stack trace in ResponseCallback
+* Fix tranforming CompletedDownloadManifestVerificationException to...
+* Only set AEAD flag on file key creation
+
+## cs/v0.7.0-alpha.10 (2026-02-18)
+
+* Introduce callback handle registry, separate callback lifecycle from object lifecycle
+
+## cs/v0.7.0-alpha.9 (2026-02-17)
+
+* Expose errorToString
+* Fix deserialization of DegradedNode
+* Add E2E tests for photo thumbnails in albums
+
+## cs/v0.7.0-alpha.8 (2026-02-11)
+
+* Provide expected SHA1 for upload through callback
+* Refactor and fix support for Photos nodes
+
+## cs/v0.7.0-alpha.7 (2026-02-11)
+
+* Abort pause state on non-resumable upload errors
+* Exclude integrity errors from being resumable during upload
+
+## cs/v0.7.0-alpha.6 (2026-02-10)
+
+* Add SHA1 upload verification
+
+## cs/v0.7.0-alpha.5 (2026-02-05)
+
+* Log "is paused" state for download too
+* Check is controller is paused instead of looking at the domain error
+* Make author and signature verification error mutually exclusive in interop
+* Remove Photo from telemetry VolumeType
+* Add seek to photo download
+* Use SDK to get nodes in tests
+* Expose functions to get nodes and enumerate folder children through interop layer
+* Add photo upload and xAttr support to Swift bindings
+* Use unconfined dispatcher
+* Set coroutine context of operation and function to Dispatchers.IO
+* Rename Jni* methods to match proto requests
+
+## cs/v0.7.0-alpha.4 (2026-01-30)
+
+* Fix files being truncated when downloading to file path through interop
+* Follow up on download pausing to address issues with hanging, seeking with interop and telemetry
+* Fix timeout reported as cancellation through interop
+
+## cs/v0.7.0-alpha.3 (2026-01-27)
+
+* Transform progress callback to flow
+* Implement pausing and resuming of downloads
+* Add photos client kotlin bindings for upload
+* Handle and send decryption error telemetry to client
+* Enable request body streaming for upload
+
+## cs/v0.7.0-alpha.2 (2026-01-26)
+
+* Fix location of Photos project
+* Make cache optional
+* Log ignored errors
+* Add file upload methods to the Photos client
+* Replace stream with buffer for HTTP
+
+## cs/v0.7.0-alpha.1 (2026-01-23)
+
+* Enforce static code analysis warnings as errors on release builds
+* Replace stream by channel for thumbnails
+* Replace stream with channel
+* Add node metadata decryption error metrics
+* Fix native clients getting garbage collected during long request to the sdk
+* Add Kotlin tests for pausing and resuming downloads
+* Fix error not caught or returned to the sdk when scope was null
+* Add getThumbnails to DrivePhotosClient
+* Remove copyrights
+
+## cs/v0.6.1-alpha.17 (2026-01-20)
+
+* Fix errors not caught in Kotlin bindings and crashing client
+* Remove unnecessary parameter from .BeginTransaction calls
+
+## cs/v0.6.1-alpha.16 (2026-01-19)
+
+* Improve cache DB transaction locking behavior
+* Implement delayed cancellation for reading content during upload
+
+## cs/v0.6.1-alpha.15 (2026-01-16)
+
+* Adding Photos SDK bindings
+* Propagate encryption key via client configuration in swift bindings
+
+## cs/v0.6.1-alpha.14 (2026-01-16)
+
+* Improve on-disk cache handling
+* Update driveClientCreate to use ProtonDriveClientOptions and timeouts
+* Fix download photos from album
+* Add ability to override HTTP timeouts
+
+## cs/v0.6.1-alpha.13 (2026-01-15)
+
+* Fix build error due to missing brace in Protobuf definition
+* Implement support for protecting SDK databases
+* Expose functions to trash node through Swift package
+* refactor: consolidate PhotoDownloadOperation into DownloadOperation
+* Fix failure to resume upload that has gaps in block upload completions
+* Implement 429 handling for block downloads
+* Log paused status for each call
+* Expose folder creation in interop and Kotlin bindings
+* Update coroutine scope when resume
+* Introduce PhotoDownloadOperation
+* Simplify implementation for pausing uploads
+* Add Kotlin bindings for rename
+* Ignore cancellation error after cancelling in download test
+* Expose folder creation in interop and Swift bindings
+* Add support for photo decryption through album key packet
+
+## cs/v0.6.1-alpha.12 (2026-01-09)
+
+* Prevent download cancellation from blocking future downloads
+* Downloading empty file now report metric
+* Add Kotlin bindings for isPaused
+* Reduce network log level for tests from debug to verbose
+
+## cs/v0.6.1-alpha.11 (2026-01-08)
+
+* Fix builds for Kotlin and Swift bindings broken due to Experimental attribute
+* Handle 429 responses on block uploads
+
+## cs/v0.6.1-alpha.10 (2026-01-07)
+
+* Fix InteropStream length initialization for write streams
+* Implement initial photos client interop
+* Interop and bindings for DownloadController.GetIsDownloadCompleteWithVerificationIssue
+* Avoid logging storage body for test
+* Map download integrity exception to integrity domain for interop
+
+## cs/v0.6.1-alpha.9 (2026-01-06)
+
+* Pause upload on timeout
+* Fix progress logs in kotlin
+
+## cs/v0.6.1-alpha.8 (2026-01-04)
+
+* Switch to SQLite-free implementation for in-memory caching
+* Expose function to rename node through Swift package
+* Update download error handling
+* Limit GC pressure by creating less Channel instances
+* Add levels to logs
+
+## cs/v0.6.1-alpha.7 (2025-12-22)
+
+* Reapply removed upload controller dispose calls
+* Move incomplete draft deletion to upload controller disposal
+* Fix shares and share secrets not being cached
+* Expose download integrity errors and download status
+
+## cs/v0.6.1-alpha.6 (2025-12-19)
+
+* Fix download retrying on cancellation
+* Pass error when operation is paused to the client. Prevent crashes for calls after operation throws.
+
+## cs/v0.6.1-alpha.5 (2025-12-19)
+
+* Add cancellation message when CS cancels a job
+* Fix download failures due to missing keys for manifest check
+* Cancel CancellationTokenSource when coroutine scope is cancelled executing blocking function
+* Add photos thumbnail downloader
+* Update telemetry error mapping
+* Implement pausing and resuming of uploads
+* Fix exception on retrying thumbnail block upload
+* Add photo downloader
+* Add Photos client and Photos volume creation
+* Extract Job code from JniDriveClient
+* Test upload and download events
+* Convert stateless JNI methods to static
+* Log swallowed exceptions
+* Propagate exception to interop logger
+
+## cs/v0.6.1-alpha.4 (2025-12-15)
+
+* No changes
+
+## cs/v0.6.1-alpha.3 (2025-12-15)
+
+* Prefix the SDK static lib name for Swift with `lib`. Use non-macOS runner for SPM release.
+* Adds the pause, resume and isPaused calls to Swift bindings for upload and download
+
+## cs/v0.6.1-alpha.2 (2025-12-11)
+
+* No changes
+
+## cs/v0.6.1-alpha.1 (2025-12-11)
+
+* Fix build of Swift bindings on CI
+* Attach current thread only when detached
+* Reduce log level and normalize logs
+* Keep reference to logger provider in Kotlin test
+* Set error type to the name of the Kotlin exception
+* Improve error generation and parsing in Swift bindings
+* Check optional proto fields
+* Add properties to query paused state of upload and download
+* Prevent download from seeking back in output stream
+* Add error handling for writing to output stream
+* Add support to C# CLI for downloading by node UID
+* Increase number of attempts for block transfers
+* Remove debug log with fatal level
+
+## cs/v0.6.0-test.2 (2025-12-04)
+
+* No changes
+
+## cs/v0.6.0-alpha.7 (2025-12-10)
+
+* Set error type to the name of the Kotlin exception
+
+## cs/v0.6.0-alpha.6 (2025-12-10)
+
+* Improve error generation and parsing in Swift bindings
+
+## cs/v0.6.0-alpha.5 (2025-12-09)
+
+* Check optional proto fields
+* Add properties to query paused state of upload and download
+* Prevent download from seeking back in output stream
+* Add error handling for writing to output stream
+* Add support to C# CLI for downloading by node UID
+
+## cs/v0.6.0-alpha.4 (2025-12-05)
+
+* Increase number of attempts for block transfers
+* Remove debug log with fatal level
+
+## cs/v0.6.0-alpha.3 (2025-12-04)
+
+* Bump crypto lib to handle decrypted AEAD session key exports
+* Improve performance of iterating over URLSession.AsyncBytes during download
+* Handle degraded node
+
+## cs/v0.6.0-alpha.1 (2025-12-02)
+
+* Fix Kotlin build failure due to Protobuf changes
+* Implement telemetry for download
+* Fix crashes when download is interrupted
+* Add Kotlin bindings for feature flags
+* Remove unused parameter
+* Fix CLI resilience retrying even on successful round trips
+* Fix address verification happening too early
+* Include the Swift's error message in the SDK interop error
+* Add auto-retries into HTTP client bridge for certain HTTP errors: 401, 429, 5xx
+* Add HTTP timeouts and ability to cancel requests through interop
+* Handle diverging size on upload
+* Address security review of C# crypto
+* Preserve interop errors passing through SDK
+* Allow multiple calls to override native library name
+* Replace option to disable HTTP retries with a request type
+* Delay opening upload stream until necessery
+* Upgrade version from 0.4.0 to 0.5.0
+* Add hint to disable retries on HTTP requests
+* Close properly response body when read
+* Add more logging to transfer queues
+* Use streaming in HTTP client
+* Add AEAD support
+* Add approximate upload size to upload metric event in kt binding
+* Improve mapping of SDK exceptions to Kotlin errors
+* Add approximate upload size to upload metric event
+* Parse Protobuf request within the same JNI call
+* Support client-injected feature flags in Swift
+* Remove copyrights and optimize imports
+* Add filtering by type to thumbnail enumeration
+* Fix missing disposal of file uploader and file downloader through interop
+* Add pause and resume API
+* Add Kotlin bindings package for Android
+* Make feature flag provision asynchronous
+* Add feature flag support
+* Fix cancellation token source being double-freed in the Swift interop
+* Fix wrong additional metadata parameters in upload
+* Add possibility to provide additional metadata on file upload
+* Add method to download thumbnails
+* Pass node name conflict error data through interop
+* Fix blocks not being released during download
+* Expose cancellation support in SDK bindings
+* Add CI job to build and deploy Swift package
+* Update client creation through interop to be able to set client UID
+* Add telemetry for uploads
+* Expose function to get available node name through Swift package
+* Fix logger
+* Feat/parse error swift interop
+* Fix possibility of missing domain and type on interop errors
+* Fix missing SDK version header when injecting HTTP client without interop
+* Fix progress callback doesn't report issue
+* Fix thumbnails causing upload to hang
+* Fix deserialization error on getting available names
+* Add Swift SDK package for iOS & macOS
+* Fix download error due to misuse of new URL block fields
+* Fix error on HTTP response with Expires header when using interop
+* Fix deserialization error on download
+* Apply server time to PGP when injecting the HTTP client through interop
+* Improve logging and clean up some code
+* Fix SHA1 extended attribute
+* Align JSON output of the C# CLI with the JavaScript one
+* Fix conflicting draft deletion failure
+* Fix old revision UID being returned instead of new one after revision upload
+* Fix various interop issues found after enabling HTTP client injection
+
+## cs/v0.1.0-alpha.3 (2025-10-14)
+
+* Fix conflicting draft deletion failure
+* Fix old revision UID being returned instead of new one after revision upload
+* Fix thumbnail type enum
+* Allow logger provider handle for drive client creation
+* Add logging for upload and session
+* Make some naming clearer
+* Make thumbnail type strongly-typed in Protobufs
+* Fix exception when returning HTTP response through interop
+* Improve error message in case of invalid cast from interop handle
+
+## cs/0.6.0-alpha.3 (2025-12-04)
+
+* Bump crypto lib to handle decrypted AEAD session key exports
+* Improve performance of iterating over URLSession.AsyncBytes during download
+* Handle degraded node
+
+## cs/0.6.0-alpha.1 (2025-12-02)
+
+* Bump crypto lib to handle decrypted AEAD session key exports
+* Improve performance of iterating over URLSession.AsyncBytes during download
+* Handle degraded node
diff --git a/cs/Directory.Build.props b/cs/Directory.Build.props
index d1c74fbc..2b73b042 100644
--- a/cs/Directory.Build.props
+++ b/cs/Directory.Build.props
@@ -1,8 +1,14 @@
- net9.0
+ net10.0
true
+ true
+ false
+ true
+
+
+ true
Proton Drive
Proton AG
@@ -15,13 +21,26 @@
enable
en
true
-
+ true
+
+ lib
-
- false
+
+ embedded
+ true
+
+
+
+ false
+
- lib
+
+ Exe
+ true
+ true
+ true
+ true
@@ -30,6 +49,11 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -41,6 +65,12 @@
+
+
+
+
+
+
all
@@ -55,6 +85,8 @@
+
+
diff --git a/cs/Directory.Build.targets b/cs/Directory.Build.targets
index 64b81d91..875a2046 100644
--- a/cs/Directory.Build.targets
+++ b/cs/Directory.Build.targets
@@ -1,9 +1,9 @@
-
-
+
$(NETCoreSdkRuntimeIdentifier)
$(DefineConstants);WINDOWS
+ lib
diff --git a/cs/Directory.Packages.props b/cs/Directory.Packages.props
index 3a0c26f1..d04ff283 100644
--- a/cs/Directory.Packages.props
+++ b/cs/Directory.Packages.props
@@ -3,28 +3,29 @@
true
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
+
+
+
-
+
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
+
+
\ No newline at end of file
diff --git a/cs/Proton.Drive.Sdk.slnx b/cs/Proton.Drive.Sdk.slnx
new file mode 100644
index 00000000..dcdc2765
--- /dev/null
+++ b/cs/Proton.Drive.Sdk.slnx
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/cs/headers/module.modulemap b/cs/headers/module.modulemap
new file mode 100644
index 00000000..345cd13c
--- /dev/null
+++ b/cs/headers/module.modulemap
@@ -0,0 +1,4 @@
+module CProtonDriveSDK {
+ umbrella header "proton_drive_sdk.h"
+ export *
+}
diff --git a/cs/headers/proton_drive_sdk.h b/cs/headers/proton_drive_sdk.h
new file mode 100644
index 00000000..3af0c726
--- /dev/null
+++ b/cs/headers/proton_drive_sdk.h
@@ -0,0 +1,15 @@
+#ifndef PROTON_DRIVE_SDK_H
+#define PROTON_DRIVE_SDK_H
+
+#include
+#include
+
+#include "proton_sdk.h"
+
+void proton_drive_sdk_handle_request(
+ ByteArray request,
+ intptr_t bindings_handle,
+ array_action response_action
+);
+
+#endif // PROTON_DRIVE_SDK_H
diff --git a/cs/headers/proton_sdk.h b/cs/headers/proton_sdk.h
new file mode 100644
index 00000000..b2f55e7a
--- /dev/null
+++ b/cs/headers/proton_sdk.h
@@ -0,0 +1,30 @@
+#ifndef PROTON_SDK_H
+#define PROTON_SDK_H
+
+#include
+#include
+
+typedef struct {
+ const uint8_t* pointer;
+ size_t length;
+} ByteArray;
+
+typedef void array_action(intptr_t handle, ByteArray array);
+
+void override_native_library_name(
+ ByteArray library_name,
+ ByteArray overriding_library_name
+);
+
+void proton_sdk_handle_request(
+ ByteArray request,
+ intptr_t bindings_handle,
+ array_action response_action
+);
+
+void proton_sdk_handle_response(
+ intptr_t sdk_handle,
+ ByteArray response
+);
+
+#endif // PROTON_SDK_H
diff --git a/cs/nuget.config b/cs/nuget.config
new file mode 100644
index 00000000..12130aac
--- /dev/null
+++ b/cs/nuget.config
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/cs/sdk/src/Proton.Drive.Sdk.CExports/DriveInteropTelemetryDecorator.cs b/cs/sdk/src/Proton.Drive.Sdk.CExports/DriveInteropTelemetryDecorator.cs
new file mode 100644
index 00000000..3f322143
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk.CExports/DriveInteropTelemetryDecorator.cs
@@ -0,0 +1,186 @@
+using System.Net;
+using Google.Protobuf;
+using Microsoft.Extensions.Logging;
+using Proton.Drive.Sdk.Telemetry;
+using Proton.Sdk.CExports;
+using Proton.Sdk.Http;
+using Proton.Sdk.Telemetry;
+
+namespace Proton.Drive.Sdk.CExports;
+
+internal sealed class DriveInteropTelemetryDecorator(InteropTelemetry instanceToDecorate) : ITelemetry
+{
+ private readonly InteropTelemetry _instanceToDecorate = instanceToDecorate;
+
+ public ILogger GetLogger(string name)
+ {
+ return _instanceToDecorate.GetLogger(name);
+ }
+
+ public void RecordMetric(IMetricEvent metricEvent)
+ {
+ IMessage? payload = metricEvent switch
+ {
+ UploadEvent me => GetUploadEventPayload(me),
+ DownloadEvent me => GetDownloadEventPayload(me),
+ DecryptionErrorEvent me => GetDecryptionErrorPayload(me),
+ BlockVerificationErrorEvent me => GetBlockVerificationErrorPayload(me),
+ _ => null,
+ };
+
+ if (payload is null)
+ {
+ _instanceToDecorate.RecordMetric(metricEvent);
+ return;
+ }
+
+ _instanceToDecorate.RecordMetric(metricEvent.Name, payload);
+ }
+
+ private static UploadEventPayload GetUploadEventPayload(UploadEvent me)
+ {
+ var payload = new UploadEventPayload
+ {
+ VolumeType = (VolumeType)me.VolumeType,
+ UploadedSize = me.UploadedSize,
+ ApproximateUploadedSize = me.ApproximateUploadedSize,
+ ExpectedSize = me.ExpectedSize,
+ ApproximateExpectedSize = me.ApproximateExpectedSize,
+ };
+
+ // Check if we should translate InteropErrorException when error is Unknown
+ var error = me is { Error: Telemetry.UploadError.Unknown, OriginalError: InteropErrorException interopError }
+ ? TranslateToUploadError(interopError)
+ : me.Error;
+
+ if (error is not null)
+ {
+ payload.Error = (UploadError)error;
+ }
+
+ if (me.OriginalError is not null)
+ {
+ payload.OriginalError = me.OriginalError.GetBaseException().ToString();
+ }
+
+ return payload;
+ }
+
+ private static DownloadEventPayload GetDownloadEventPayload(DownloadEvent me)
+ {
+ var payload = new DownloadEventPayload
+ {
+ VolumeType = (VolumeType)me.VolumeType,
+ DownloadedSize = me.DownloadedSize,
+ ApproximateDownloadedSize = me.ApproximateDownloadedSize,
+ ClaimedFileSize = me.ClaimedFileSize ?? 0,
+ ApproximateClaimedFileSize = me.ApproximateClaimedFileSize ?? 0,
+ };
+
+ // Check if we should translate InteropErrorException when error is Unknown
+ var error = me is { Error: Telemetry.DownloadError.Unknown, OriginalError: InteropErrorException interopError }
+ ? TranslateToDownloadError(interopError)
+ : me.Error;
+
+ if (error is not null)
+ {
+ payload.Error = (DownloadError)error;
+ }
+
+ if (me.OriginalError is not null)
+ {
+ payload.OriginalError = me.OriginalError.GetBaseException().ToString();
+ }
+
+ return payload;
+ }
+
+ private static BlockVerificationErrorEventPayload GetBlockVerificationErrorPayload(BlockVerificationErrorEvent me)
+ {
+ return new BlockVerificationErrorEventPayload
+ {
+ VolumeType = (VolumeType)me.VolumeType,
+ RetryHelped = me.RetryHelped,
+ };
+ }
+
+ private static DecryptionErrorEventPayload GetDecryptionErrorPayload(DecryptionErrorEvent me)
+ {
+ var payload = new DecryptionErrorEventPayload
+ {
+ VolumeType = (VolumeType)me.VolumeType,
+ Field = (EncryptedField)me.Field,
+ Uid = me.Uid.ToString(),
+ };
+
+ if (me.FromBefore2024.HasValue)
+ {
+ payload.FromBefore2024 = me.FromBefore2024.Value;
+ }
+
+ if (me.Error is not null)
+ {
+ payload.Error = me.Error;
+ }
+
+ return payload;
+ }
+
+ private static Telemetry.UploadError? TranslateToUploadError(InteropErrorException exception)
+ {
+ if (exception.Error is null)
+ {
+ return Telemetry.UploadError.Unknown;
+ }
+
+ var error = exception.Error;
+ return exception.Error.Domain switch
+ {
+ ErrorDomain.Api => TranslateApiErrorToUploadError(error.SecondaryCode),
+ ErrorDomain.Network or ErrorDomain.Transport => Telemetry.UploadError.NetworkError,
+ ErrorDomain.Serialization => Telemetry.UploadError.HttpClientSideError,
+ ErrorDomain.Cryptography or ErrorDomain.DataIntegrity => Telemetry.UploadError.IntegrityError,
+ ErrorDomain.BusinessLogic => Telemetry.UploadError.ValidationError,
+ _ => Telemetry.UploadError.Unknown,
+ };
+ }
+
+ private static Telemetry.UploadError TranslateApiErrorToUploadError(long statusCode)
+ {
+ return statusCode switch
+ {
+ (int)HttpStatusCode.TooManyRequests => Telemetry.UploadError.RateLimited,
+ >= StatusCodes.MinClientErrorCode and <= StatusCodes.MaxClientErrorCode => Telemetry.UploadError.HttpClientSideError,
+ _ => Telemetry.UploadError.ServerError,
+ };
+ }
+
+ private static Telemetry.DownloadError? TranslateToDownloadError(InteropErrorException exception)
+ {
+ if (exception.Error is null)
+ {
+ return Telemetry.DownloadError.Unknown;
+ }
+
+ var error = exception.Error;
+ return exception.Error.Domain switch
+ {
+ ErrorDomain.Api => TranslateApiErrorToDownloadError(error.SecondaryCode),
+ ErrorDomain.Network or ErrorDomain.Transport => Telemetry.DownloadError.NetworkError,
+ ErrorDomain.Serialization => Telemetry.DownloadError.HttpClientSideError,
+ ErrorDomain.Cryptography or ErrorDomain.DataIntegrity => Telemetry.DownloadError.IntegrityError,
+ ErrorDomain.BusinessLogic => Telemetry.DownloadError.ValidationError,
+ _ => Telemetry.DownloadError.Unknown,
+ };
+ }
+
+ private static Telemetry.DownloadError TranslateApiErrorToDownloadError(long statusCode)
+ {
+ return statusCode switch
+ {
+ (int)HttpStatusCode.TooManyRequests => Telemetry.DownloadError.RateLimited,
+ >= StatusCodes.MinClientErrorCode and <= StatusCodes.MaxClientErrorCode => Telemetry.DownloadError.HttpClientSideError,
+ _ => Telemetry.DownloadError.ServerError,
+ };
+ }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropAccountClient.cs b/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropAccountClient.cs
new file mode 100644
index 00000000..3674e2c8
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropAccountClient.cs
@@ -0,0 +1,77 @@
+using Google.Protobuf.WellKnownTypes;
+using Proton.Cryptography.Pgp;
+using Proton.Sdk.Addresses;
+using Proton.Sdk.CExports;
+using Address = Proton.Sdk.Addresses.Address;
+using AddressKey = Proton.Sdk.Addresses.AddressKey;
+using AddressStatus = Proton.Sdk.Addresses.AddressStatus;
+
+namespace Proton.Drive.Sdk.CExports;
+
+internal sealed class InteropAccountClient(nint bindingsHandle, InteropAction, nint> requestAction) : IAccountClient
+{
+ private readonly nint _bindingsHandle = bindingsHandle;
+ private readonly InteropAction, nint> _requestAction = requestAction;
+
+ public async ValueTask GetAddressAsync(AddressId addressId, CancellationToken cancellationToken)
+ {
+ var request = new AccountRequest { GetAddress = new GetAddressRequest { AddressId = addressId.ToString() } };
+ var response = await _requestAction.SendRequestAsync(_bindingsHandle, request).ConfigureAwait(false);
+
+ return ConvertToAddress(response);
+ }
+
+ public async ValueTask GetDefaultAddressAsync(CancellationToken cancellationToken)
+ {
+ var response = await _requestAction.SendRequestAsync(
+ _bindingsHandle,
+ new AccountRequest { GetDefaultAddress = new GetDefaultAddressRequest() }).ConfigureAwait(false);
+
+ return ConvertToAddress(response);
+ }
+
+ public async ValueTask GetAddressPrimaryPrivateKeyAsync(AddressId addressId, CancellationToken cancellationToken)
+ {
+ var request = new AccountRequest { GetAddressPrimaryPrivateKey = new GetAddressPrimaryPrivateKeyRequest { AddressId = addressId.ToString() } };
+ var response = await _requestAction.SendRequestAsync(_bindingsHandle, request).ConfigureAwait(false);
+
+ return PgpPrivateKey.Import(response.Value.Span);
+ }
+
+ public async ValueTask> GetAddressPrivateKeysAsync(AddressId addressId, CancellationToken cancellationToken)
+ {
+ var request = new AccountRequest { GetAddressPrivateKeys = new GetAddressPrivateKeysRequest { AddressId = addressId.ToString() } };
+ var response = await _requestAction.SendRequestAsync(_bindingsHandle, request).ConfigureAwait(false);
+
+ return [.. response.Value.Select(keyData => PgpPrivateKey.Import(keyData.Span))];
+ }
+
+ public async ValueTask> GetAddressPublicKeysAsync(string emailAddress, CancellationToken cancellationToken)
+ {
+ var request = new AccountRequest { GetAddressPublicKeys = new GetAddressPublicKeysRequest { EmailAddress = emailAddress } };
+ var response = await _requestAction.SendRequestAsync(_bindingsHandle, request).ConfigureAwait(false);
+
+ return [.. response.Value.Select(keyData => PgpPublicKey.Import(keyData.Span))];
+ }
+
+ private static Address ConvertToAddress(Proton.Sdk.CExports.Address addressMessage)
+ {
+ var addressId = new AddressId(addressMessage.AddressId);
+
+ var keys = addressMessage.Keys.Select((key, index) => new AddressKey(
+ addressId,
+ new AddressKeyId(key.AddressKeyId),
+ index == addressMessage.PrimaryKeyIndex,
+ key.IsActive,
+ key.IsAllowedForEncryption,
+ key.IsAllowedForVerification)).ToList();
+
+ return new Address(
+ addressId,
+ addressMessage.Order,
+ addressMessage.EmailAddress,
+ (AddressStatus)addressMessage.Status,
+ keys,
+ addressMessage.PrimaryKeyIndex);
+ }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropActionExtensions.cs b/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropActionExtensions.cs
new file mode 100644
index 00000000..13df84dd
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropActionExtensions.cs
@@ -0,0 +1,27 @@
+using Google.Protobuf;
+using Proton.Sdk.CExports;
+
+namespace Proton.Drive.Sdk.CExports;
+
+internal static class InteropActionExtensions
+{
+ public static unsafe void InvokeProgressUpdate(this InteropAction> interopAction, nint bindingsHandle, long progress, long? total)
+ {
+ var progressUpdate = new ProgressUpdate
+ {
+ BytesCompleted = progress,
+ };
+
+ if (total is not null)
+ {
+ progressUpdate.BytesInTotal = total.Value;
+ }
+
+ var requestBytes = progressUpdate.ToByteArray();
+
+ fixed (byte* requestBytesPointer = requestBytes)
+ {
+ interopAction.Invoke(bindingsHandle, new InteropArray(requestBytesPointer, requestBytes.Length));
+ }
+ }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropConversionExtensions.cs b/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropConversionExtensions.cs
new file mode 100644
index 00000000..e6948777
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropConversionExtensions.cs
@@ -0,0 +1,261 @@
+using Google.Protobuf;
+using Google.Protobuf.WellKnownTypes;
+using Proton.Drive.Sdk.Nodes;
+using Proton.Sdk;
+using Proton.Sdk.CExports;
+
+namespace Proton.Drive.Sdk.CExports;
+
+internal static class InteropConversionExtensions
+{
+ extension(Nodes.Node node)
+ {
+ public Node ToInterop()
+ {
+ var result = new Node();
+
+ switch (node)
+ {
+ case Nodes.FolderNode folderNode:
+ result.Folder = Nodes.Node.ToInterop(folderNode);
+ break;
+ case Nodes.FileNode fileNode:
+ result.File = Nodes.Node.ToInterop(fileNode);
+ break;
+ }
+
+ return result;
+ }
+
+ private static FolderNode ToInterop(Nodes.FolderNode folderNode)
+ {
+ var folderNodeProto = new FolderNode
+ {
+ Uid = folderNode.Uid.ToString(),
+ TreeEventScopeId = folderNode.TreeEventScopeId.ToString(),
+ Name = folderNode.Name.ToInterop(),
+ CreationTime = folderNode.CreationTime.ToUniversalTime().ToTimestamp(),
+ TrashTime = folderNode.TrashTime?.ToUniversalTime().ToTimestamp(),
+ NameAuthor = folderNode.NameAuthor.ToInterop(),
+ Author = folderNode.Author.ToInterop(),
+ OwnedBy = folderNode.OwnedBy.ToInterop(),
+ };
+
+ if (folderNode.ParentUid != null)
+ {
+ folderNodeProto.ParentUid = folderNode.ParentUid.ToString();
+ }
+
+ folderNodeProto.Errors.AddRange(folderNode.Errors.Select(ToInterop));
+
+ return folderNodeProto;
+ }
+
+ private static FileNode ToInterop(Nodes.FileNode fileNode)
+ {
+ var fileNodeProto = new FileNode
+ {
+ Uid = fileNode.Uid.ToString(),
+ TreeEventScopeId = fileNode.TreeEventScopeId.ToString(),
+ Name = fileNode.Name.ToInterop(),
+ MediaType = fileNode.MediaType,
+ CreationTime = fileNode.CreationTime.ToUniversalTime().ToTimestamp(),
+ TrashTime = fileNode.TrashTime?.ToUniversalTime().ToTimestamp(),
+ NameAuthor = fileNode.NameAuthor.ToInterop(),
+ Author = fileNode.Author.ToInterop(),
+ TotalSizeOnCloudStorage = fileNode.TotalSizeOnCloudStorage,
+ OwnedBy = fileNode.OwnedBy.ToInterop(),
+ };
+
+ if (fileNode.ParentUid != null)
+ {
+ fileNodeProto.ParentUid = fileNode.ParentUid.ToString();
+ }
+
+ fileNodeProto.ActiveRevision = fileNode.ActiveRevision.ToInterop();
+
+ fileNodeProto.Errors.AddRange(fileNode.Errors.Select(ToInterop));
+
+ return fileNodeProto;
+ }
+ }
+
+ extension(ProtonDriveError error)
+ {
+ public DriveError ToInterop()
+ {
+ var driveError = new DriveError
+ {
+ InnerError = error.InnerError?.ToInterop(),
+ };
+
+ if (error.Message != null)
+ {
+ driveError.Message = error.Message;
+ }
+
+ return driveError;
+ }
+ }
+
+ extension(IReadOnlyDictionary> results)
+ {
+ public NodeResultListResponse ToInterop()
+ {
+ return new NodeResultListResponse
+ {
+ Results =
+ {
+ results.Select(pair =>
+ {
+ var result = new NodeResultPair
+ {
+ NodeUid = pair.Key.ToString(),
+ };
+
+ if (pair.Value.TryGetError(out var exception))
+ {
+ result.Error = exception.ToProtoError(InteropDriveErrorConverter.SetDomainAndCodes);
+ }
+
+ return result;
+ }),
+ },
+ };
+ }
+ }
+
+ extension(Revision revision)
+ {
+ public FileRevision ToInterop()
+ {
+ var protoRevision = new FileRevision
+ {
+ Uid = revision.Uid.ToString(),
+ CreationTime = revision.CreationTime.ToUniversalTime().ToTimestamp(),
+ SizeOnCloudStorage = revision.SizeOnCloudStorage,
+ ClaimedSize = revision.ClaimedSize ?? 0,
+ ClaimedModificationTime = revision.ClaimedModificationTime?.ToUniversalTime().ToTimestamp(),
+ };
+
+ if (revision.ClaimedDigests is { } claimedDigests)
+ {
+ protoRevision.ClaimedDigests = new FileContentDigests
+ {
+ Sha1Verified = claimedDigests.Sha1Verified,
+ };
+
+ if (claimedDigests.Sha1 is { } sha1)
+ {
+ protoRevision.ClaimedDigests.Sha1 = ByteString.CopyFrom(sha1.Span);
+ }
+ }
+
+ protoRevision.Thumbnails.AddRange(
+ revision.Thumbnails.Select(t => new ThumbnailHeader
+ {
+ Id = t.Id,
+ Type = (ThumbnailType)(int)t.Type,
+ }));
+
+ if (revision.AdditionalClaimedMetadata is not null)
+ {
+ protoRevision.AdditionalClaimedMetadata.AddRange(
+ revision.AdditionalClaimedMetadata.Select(m => new AdditionalMetadataProperty
+ {
+ Name = m.Name,
+ Utf8JsonValue = ByteString.CopyFromUtf8(m.Value.ToString()),
+ }));
+ }
+
+ if (revision.ContentAuthor.HasValue)
+ {
+ protoRevision.ContentAuthor = revision.ContentAuthor.Value.ToInterop();
+ }
+
+ return protoRevision;
+ }
+ }
+
+ extension(Result result)
+ {
+ public AuthorResult ToInterop()
+ {
+ var authorResult = new AuthorResult();
+
+ if (result.TryGetValueElseError(out var author, out var error))
+ {
+ var authorResultValue = new Author();
+ if (authorResultValue.EmailAddress != null)
+ {
+ authorResultValue.EmailAddress = author.EmailAddress;
+ }
+
+ authorResult.Value = authorResultValue;
+ }
+ else
+ {
+ var claimedAuthor = new Author();
+ if (error.ClaimedAuthor.EmailAddress != null)
+ {
+ claimedAuthor.EmailAddress = error.ClaimedAuthor.EmailAddress;
+ }
+
+ authorResult.Error = new SignatureVerificationError
+ {
+ ClaimedAuthor = claimedAuthor,
+ };
+
+ if (error.Message != null)
+ {
+ // TODO change message to be a DriveError
+ authorResult.Error.Message = error.FlattenMessage();
+ }
+ }
+
+ return authorResult;
+ }
+ }
+
+ extension(Nodes.OwnedBy? ownedBy)
+ {
+ public OwnedBy ToInterop()
+ {
+ if (ownedBy is null)
+ {
+ return new OwnedBy();
+ }
+
+ var result = new OwnedBy();
+ if (ownedBy.Email != null)
+ {
+ result.Email = ownedBy.Email;
+ }
+
+ if (ownedBy.Organization != null)
+ {
+ result.Organization = ownedBy.Organization;
+ }
+
+ return result;
+ }
+ }
+
+ extension(Result result)
+ {
+ public StringResult ToInterop()
+ {
+ var stringResult = new StringResult();
+ if (result.TryGetValueElseError(out var value, out var error))
+ {
+ stringResult.Value = value;
+ }
+ else
+ {
+ stringResult.Error = error.ToInterop();
+ }
+
+ return stringResult;
+ }
+ }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropDownloadController.cs b/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropDownloadController.cs
new file mode 100644
index 00000000..26894c73
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropDownloadController.cs
@@ -0,0 +1,59 @@
+using Google.Protobuf;
+using Google.Protobuf.WellKnownTypes;
+using Proton.Drive.Sdk.Nodes.Download;
+using Proton.Sdk.CExports;
+
+namespace Proton.Drive.Sdk.CExports;
+
+internal static class InteropDownloadController
+{
+ public static IMessage HandleIsPaused(DownloadControllerIsPausedRequest request)
+ {
+ var downloadController = Interop.GetFromHandle(request.DownloadControllerHandle);
+
+ return new BoolValue { Value = downloadController.IsPaused };
+ }
+
+ public static async ValueTask HandleAwaitCompletion(DownloadControllerAwaitCompletionRequest request)
+ {
+ var downloadController = Interop.GetFromHandle(request.DownloadControllerHandle);
+
+ await downloadController.Completion.ConfigureAwait(false);
+
+ return null;
+ }
+
+ public static IMessage? HandlePause(DownloadControllerPauseRequest request)
+ {
+ var downloadController = Interop.GetFromHandle(request.DownloadControllerHandle);
+
+ downloadController.Pause();
+
+ return null;
+ }
+
+ public static IMessage? HandleResume(DownloadControllerResumeRequest request)
+ {
+ var downloadController = Interop.GetFromHandle(request.DownloadControllerHandle);
+
+ downloadController.Resume();
+
+ return null;
+ }
+
+ public static IMessage? HandleIsDownloadCompleteWithVerificationIssue(DownloadControllerIsDownloadCompleteWithVerificationIssueRequest request)
+ {
+ var downloadController = Interop.GetFromHandle(request.DownloadControllerHandle);
+
+ return new BoolValue { Value = downloadController.GetIsDownloadCompleteWithVerificationIssue() };
+ }
+
+ public static async ValueTask HandleFree(DownloadControllerFreeRequest request)
+ {
+ var downloadController = Interop.FreeHandle(request.DownloadControllerHandle);
+
+ await downloadController.DisposeAsync().ConfigureAwait(false);
+
+ return null;
+ }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropDriveErrorConverter.cs b/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropDriveErrorConverter.cs
new file mode 100644
index 00000000..aa3bcd55
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropDriveErrorConverter.cs
@@ -0,0 +1,192 @@
+using Google.Protobuf;
+using Google.Protobuf.WellKnownTypes;
+using Proton.Drive.Sdk.Nodes;
+using Proton.Drive.Sdk.Nodes.Download;
+using Proton.Drive.Sdk.Nodes.Upload;
+using Proton.Drive.Sdk.Nodes.Upload.Verification;
+using Proton.Sdk.CExports;
+
+namespace Proton.Drive.Sdk.CExports;
+
+internal static class InteropDriveErrorConverter
+{
+ private const int UnknownDecryptionErrorPrimaryCode = 0;
+ private const int NodeMetadataDecryptionErrorPrimaryCode = 2;
+ private const int FileContentsDecryptionErrorPrimaryCode = 3;
+ private const int UploadKeyMismatchErrorPrimaryCode = 4;
+ private const int ManifestSignatureVerificationErrorPrimaryCode = 5;
+ private const int ContentUploadIntegrityErrorPrimaryCode = 6;
+
+ public static void SetDomainAndCodes(Error error, Exception exception)
+ {
+ switch (exception)
+ {
+ case NodeMetadataDecryptionException e:
+ error.Domain = ErrorDomain.DataIntegrity;
+ error.PrimaryCode = NodeMetadataDecryptionErrorPrimaryCode;
+ error.SecondaryCode = (long)e.Part;
+ break;
+
+ case FileContentsDecryptionException:
+ error.Domain = ErrorDomain.DataIntegrity;
+ error.PrimaryCode = FileContentsDecryptionErrorPrimaryCode;
+ break;
+
+ case NodeKeyAndSessionKeyMismatchException:
+ case SessionKeyAndDataPacketMismatchException:
+ error.Domain = ErrorDomain.DataIntegrity;
+ error.PrimaryCode = UploadKeyMismatchErrorPrimaryCode;
+ break;
+
+ case DataIntegrityException:
+ error.Domain = ErrorDomain.DataIntegrity;
+ error.PrimaryCode = ManifestSignatureVerificationErrorPrimaryCode;
+ break;
+
+ case MissingContentBlockIntegrityException e:
+ error.Domain = ErrorDomain.DataIntegrity;
+ error.PrimaryCode = ContentUploadIntegrityErrorPrimaryCode;
+ error.AdditionalData = Any.Pack(ToAdditionalData(e));
+ break;
+
+ case ContentSizeMismatchIntegrityException e:
+ error.Domain = ErrorDomain.DataIntegrity;
+ error.PrimaryCode = ContentUploadIntegrityErrorPrimaryCode;
+ error.AdditionalData = Any.Pack(ToAdditionalData(e));
+ break;
+
+ case ThumbnailCountMismatchIntegrityException e:
+ error.Domain = ErrorDomain.DataIntegrity;
+ error.PrimaryCode = ContentUploadIntegrityErrorPrimaryCode;
+ error.AdditionalData = Any.Pack(ToAdditionalData(e));
+ break;
+
+ case ChecksumMismatchIntegrityException e:
+ error.Domain = ErrorDomain.DataIntegrity;
+ error.PrimaryCode = ContentUploadIntegrityErrorPrimaryCode;
+ error.AdditionalData = Any.Pack(ToAdditionalData(e));
+ break;
+
+ case IntegrityException:
+ error.Domain = ErrorDomain.DataIntegrity;
+ error.PrimaryCode = ContentUploadIntegrityErrorPrimaryCode;
+ break;
+
+ case NodeWithSameNameExistsException e:
+ if (e.Code is not null)
+ {
+ error.PrimaryCode = (long)e.Code.Value;
+ }
+
+ error.Domain = ErrorDomain.BusinessLogic;
+ error.AdditionalData = Any.Pack(ToAdditionalData(e));
+ break;
+
+ case NodeNotFoundException e:
+ if (e.Code is not null)
+ {
+ error.PrimaryCode = (long)e.Code.Value;
+ }
+
+ error.Domain = ErrorDomain.BusinessLogic;
+ error.AdditionalData = Any.Pack(ToAdditionalData(e));
+ break;
+
+ default:
+ error.PrimaryCode = UnknownDecryptionErrorPrimaryCode;
+ InteropErrorConverter.SetDomainAndCodes(error, exception);
+ break;
+ }
+ }
+
+ private static MissingContentBlockErrorData ToAdditionalData(MissingContentBlockIntegrityException e)
+ {
+ var data = new MissingContentBlockErrorData();
+ if (e.BlockNumber is { } blockNumber)
+ {
+ data.BlockNumber = blockNumber;
+ }
+
+ return data;
+ }
+
+ private static ContentSizeMismatchErrorData ToAdditionalData(ContentSizeMismatchIntegrityException e)
+ {
+ var data = new ContentSizeMismatchErrorData();
+ if (e.UploadedSize is { } uploadedSize)
+ {
+ data.UploadedSize = uploadedSize;
+ }
+
+ if (e.ExpectedSize is { } expectedSize)
+ {
+ data.ExpectedSize = expectedSize;
+ }
+
+ return data;
+ }
+
+ private static ThumbnailCountMismatchErrorData ToAdditionalData(ThumbnailCountMismatchIntegrityException e)
+ {
+ var data = new ThumbnailCountMismatchErrorData();
+ if (e.UploadedBlockCount is { } uploadedBlockCount)
+ {
+ data.UploadedBlockCount = uploadedBlockCount;
+ }
+
+ if (e.ExpectedBlockCount is { } expectedBlockCount)
+ {
+ data.ExpectedBlockCount = expectedBlockCount;
+ }
+
+ return data;
+ }
+
+ private static ChecksumMismatchErrorData ToAdditionalData(ChecksumMismatchIntegrityException e)
+ {
+ var data = new ChecksumMismatchErrorData();
+ if (e.ActualChecksum is not null)
+ {
+ data.ActualChecksum = ByteString.CopyFrom(e.ActualChecksum);
+ }
+
+ if (e.ExpectedChecksum is not null)
+ {
+ data.ExpectedChecksum = ByteString.CopyFrom(e.ExpectedChecksum);
+ }
+
+ return data;
+ }
+
+ private static NodeNameConflictErrorData ToAdditionalData(NodeWithSameNameExistsException e)
+ {
+ var data = new NodeNameConflictErrorData();
+ if (e.ConflictingNodeIsFileDraft is { } conflictingNodeIsFileDraft)
+ {
+ data.ConflictingNodeIsFileDraft = conflictingNodeIsFileDraft;
+ }
+
+ if (e.ConflictingNodeUid is { } conflictingNodeUid)
+ {
+ data.ConflictingNodeUid = conflictingNodeUid.ToString();
+ }
+
+ if (e.ConflictingRevisionUid is { } conflictingRevisionUid)
+ {
+ data.ConflictingRevisionUid = conflictingRevisionUid.ToString();
+ }
+
+ return data;
+ }
+
+ private static NodeNotFoundErrorData ToAdditionalData(NodeNotFoundException e)
+ {
+ var data = new NodeNotFoundErrorData();
+ if (e.NodeUid is { } nodeUid)
+ {
+ data.NodeUid = nodeUid.ToString();
+ }
+
+ return data;
+ }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropDriveProtobufMetadata.cs b/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropDriveProtobufMetadata.cs
new file mode 100644
index 00000000..ce50b909
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropDriveProtobufMetadata.cs
@@ -0,0 +1,16 @@
+using System.Text.Json;
+using Google.Protobuf.Collections;
+
+namespace Proton.Drive.Sdk.CExports;
+
+internal static class InteropDriveProtobufMetadata
+{
+ internal static IEnumerable? ParseAdditionalMetadata(
+ RepeatedField additionalMetadata) =>
+ additionalMetadata.Count > 0
+ ? additionalMetadata.Select(x =>
+ new Proton.Drive.Sdk.Nodes.AdditionalMetadataProperty(
+ x.Name,
+ JsonDocument.Parse(x.Utf8JsonValue.Memory).RootElement))
+ : null;
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropFileDownloader.cs b/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropFileDownloader.cs
new file mode 100644
index 00000000..6f7bb904
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropFileDownloader.cs
@@ -0,0 +1,59 @@
+using Google.Protobuf;
+using Google.Protobuf.WellKnownTypes;
+using Proton.Drive.Sdk.Nodes.Download;
+using Proton.Sdk.CExports;
+
+namespace Proton.Drive.Sdk.CExports;
+
+internal static class InteropFileDownloader
+{
+ public static IMessage HandleDownloadToStream(DownloadToStreamRequest request, nint bindingsHandle)
+ {
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+
+ var downloader = Interop.GetFromHandle(request.DownloaderHandle);
+
+ var writeFunction = new InteropFunction, nint, nint>(request.WriteAction);
+
+ var seekAction = request.SeekAction != 0
+ ? new InteropAction, nint>(request.SeekAction)
+ : (InteropAction, nint>?)null;
+
+ var cancelAction = request.CancelAction != 0 ? new InteropAction(request.CancelAction) : (InteropAction?)null;
+ var stream = new InteropStream(bindingsHandle, writeFunction, seekAction, cancelAction);
+
+ var progressAction = new InteropAction>(request.ProgressAction);
+
+ var downloadController = downloader.DownloadToStream(
+ stream,
+ (bytesCompleted, bytesInTotal) => progressAction.InvokeProgressUpdate(bindingsHandle, bytesCompleted, bytesInTotal),
+ cancellationToken);
+
+ return new Int64Value { Value = Interop.AllocHandle(downloadController) };
+ }
+
+ public static IMessage HandleDownloadToFile(DownloadToFileRequest request, nint bindingsHandle)
+ {
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+
+ var downloader = Interop.GetFromHandle(request.DownloaderHandle);
+
+ var progressAction = new InteropAction>(request.ProgressAction);
+
+ var downloadController = downloader.DownloadToFile(
+ request.FilePath,
+ (bytesCompleted, bytesInTotal) => progressAction.InvokeProgressUpdate(bindingsHandle, bytesCompleted, bytesInTotal),
+ cancellationToken);
+
+ return new Int64Value { Value = Interop.AllocHandle(downloadController) };
+ }
+
+ public static IMessage? HandleFree(FileDownloaderFreeRequest request)
+ {
+ var fileDownloader = Interop.FreeHandle(request.FileDownloaderHandle);
+
+ fileDownloader.Dispose();
+
+ return null;
+ }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropFileUploader.cs b/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropFileUploader.cs
new file mode 100644
index 00000000..529a3b54
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropFileUploader.cs
@@ -0,0 +1,103 @@
+using System.Security.Cryptography;
+using Google.Protobuf;
+using Google.Protobuf.WellKnownTypes;
+using Proton.Drive.Sdk.Nodes.Upload;
+using Proton.Sdk.CExports;
+
+namespace Proton.Drive.Sdk.CExports;
+
+internal static class InteropFileUploader
+{
+ public static IMessage HandleUploadFromStream(UploadFromStreamRequest request, nint bindingsHandle)
+ {
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+
+ var uploader = Interop.GetFromHandle(request.UploaderHandle);
+
+ var readFunction = new InteropFunction, nint, nint>(request.ReadAction);
+ var cancelAction = request.CancelAction != 0 ? new InteropAction(request.CancelAction) : (InteropAction?)null;
+ var stream = new InteropStream(uploader.FileSize, bindingsHandle, readFunction, cancelAction);
+
+ var thumbnails = request.Thumbnails.Select(t =>
+ {
+ unsafe
+ {
+ var thumbnailType = (Nodes.ThumbnailType)t.Type;
+ return new Nodes.Thumbnail(thumbnailType, new InteropArray((byte*)t.DataPointer, (nint)t.DataLength).ToArray());
+ }
+ });
+
+ var progressAction = new InteropAction>(request.ProgressAction);
+
+ var expectedSha1Provider = request.HasSha1Function ? CreateSha1Provider(bindingsHandle, request.Sha1Function) : null;
+
+ var uploadController = uploader.UploadFromStream(
+ stream,
+ thumbnails,
+ (progress, total) => progressAction.InvokeProgressUpdate(bindingsHandle, progress, total),
+ expectedSha1Provider,
+ forPhotos: false,
+ cancellationToken);
+
+ return new Int64Value { Value = Interop.AllocHandle(uploadController) };
+ }
+
+ public static IMessage HandleUploadFromFile(UploadFromFileRequest request, nint bindingsHandle)
+ {
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+
+ var uploader = Interop.GetFromHandle(request.UploaderHandle);
+
+ var thumbnails = request.Thumbnails.Select(t =>
+ {
+ unsafe
+ {
+ var thumbnailType = (Nodes.ThumbnailType)t.Type;
+ return new Nodes.Thumbnail(thumbnailType, new InteropArray((byte*)t.DataPointer, (nint)t.DataLength).ToArray());
+ }
+ });
+
+ var progressAction = new InteropAction>(request.ProgressAction);
+
+ var expectedSha1Provider = request.HasSha1Function ? CreateSha1Provider(bindingsHandle, request.Sha1Function) : null;
+
+ var uploadController = uploader.UploadFromFile(
+ request.FilePath,
+ thumbnails,
+ (progress, total) => progressAction.InvokeProgressUpdate(bindingsHandle, progress, total),
+ expectedSha1Provider,
+ forPhotos: false,
+ cancellationToken);
+
+ return new Int64Value { Value = Interop.AllocHandle(uploadController) };
+ }
+
+ public static IMessage? HandleFree(FileUploaderFreeRequest request)
+ {
+ var fileUploader = Interop.FreeHandle(request.FileUploaderHandle);
+
+ fileUploader.Dispose();
+
+ return null;
+ }
+
+ internal static Func> CreateSha1Provider(nint bindingsHandle, long functionPointer)
+ {
+ return () =>
+ {
+ var sha1Buffer = new byte[SHA1.HashSizeInBytes];
+
+ unsafe
+ {
+ fixed (byte* sha1BufferPointer = sha1Buffer)
+ {
+ var function = new InteropAction>(functionPointer);
+
+ function.Invoke(bindingsHandle, new InteropArray(sha1BufferPointer, sha1Buffer.Length));
+ }
+ }
+
+ return sha1Buffer;
+ };
+ }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropMessageHandler.cs b/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropMessageHandler.cs
new file mode 100644
index 00000000..091b784b
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropMessageHandler.cs
@@ -0,0 +1,207 @@
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using Google.Protobuf.WellKnownTypes;
+using Proton.Sdk.CExports;
+
+namespace Proton.Drive.Sdk.CExports;
+
+internal static class InteropMessageHandler
+{
+ [UnmanagedCallersOnly(EntryPoint = "proton_drive_sdk_handle_request", CallConvs = [typeof(CallConvCdecl)])]
+ public static async void OnRequestReceived(InteropArray requestBytes, nint bindingsHandle, InteropAction> responseAction)
+ {
+ try
+ {
+ var request = Request.Parser.ParseFrom(requestBytes.AsReadOnlySpan());
+
+ var response = request.PayloadCase switch
+ {
+ Request.PayloadOneofCase.DriveClientCreate
+ => InteropProtonDriveClient.HandleCreate(request.DriveClientCreate, bindingsHandle),
+
+ Request.PayloadOneofCase.DriveClientCreateFromSession
+ => InteropProtonDriveClient.HandleCreate(request.DriveClientCreateFromSession),
+
+ Request.PayloadOneofCase.DriveClientFree
+ => InteropProtonDriveClient.HandleFree(request.DriveClientFree),
+
+ Request.PayloadOneofCase.DriveClientGetFileUploader
+ => await InteropProtonDriveClient.HandleGetFileUploaderAsync(request.DriveClientGetFileUploader).ConfigureAwait(false),
+
+ Request.PayloadOneofCase.DriveClientGetFileRevisionUploader
+ => await InteropProtonDriveClient.HandleGetFileRevisionUploaderAsync(request.DriveClientGetFileRevisionUploader).ConfigureAwait(false),
+
+ Request.PayloadOneofCase.DriveClientGetFileDownloader
+ => await InteropProtonDriveClient.HandleGetFileDownloaderAsync(request.DriveClientGetFileDownloader).ConfigureAwait(false),
+
+ Request.PayloadOneofCase.DriveClientGetAvailableName
+ => await InteropProtonDriveClient.HandleGetAvailableNameAsync(request.DriveClientGetAvailableName).ConfigureAwait(false),
+
+ Request.PayloadOneofCase.DriveClientTrashNodes
+ => await InteropProtonDriveClient.HandleTrashNodesAsync(request.DriveClientTrashNodes).ConfigureAwait(false),
+
+ Request.PayloadOneofCase.DriveClientDeleteNodes
+ => await InteropProtonDriveClient.HandleDeleteNodesAsync(request.DriveClientDeleteNodes).ConfigureAwait(false),
+
+ Request.PayloadOneofCase.DriveClientRestoreNodes
+ => await InteropProtonDriveClient.HandleRestoreNodesAsync(request.DriveClientRestoreNodes).ConfigureAwait(false),
+
+ Request.PayloadOneofCase.DriveClientEnumerateTrash
+ => await InteropProtonDriveClient.HandleEnumerateTrashAsync(request.DriveClientEnumerateTrash, bindingsHandle).ConfigureAwait(false),
+
+ Request.PayloadOneofCase.DriveClientEmptyTrash
+ => await InteropProtonDriveClient.HandleEmptyTrashAsync(request.DriveClientEmptyTrash).ConfigureAwait(false),
+
+ Request.PayloadOneofCase.DriveClientRename
+ => await InteropProtonDriveClient.HandleRenameAsync(request.DriveClientRename).ConfigureAwait(false),
+
+ Request.PayloadOneofCase.DriveClientCreateFolder
+ => await InteropProtonDriveClient.HandleCreateFolderAsync(request.DriveClientCreateFolder).ConfigureAwait(false),
+
+ Request.PayloadOneofCase.DriveClientEnumerateThumbnails
+ => await InteropProtonDriveClient.HandleEnumerateThumbnailsAsync(
+ request.DriveClientEnumerateThumbnails,
+ bindingsHandle).ConfigureAwait(false),
+
+ Request.PayloadOneofCase.DriveClientEnumerateFolderChildren
+ => await InteropProtonDriveClient.HandleEnumerateFolderChildrenAsync(
+ request.DriveClientEnumerateFolderChildren,
+ bindingsHandle).ConfigureAwait(false),
+
+ Request.PayloadOneofCase.DriveClientGetMyFilesFolder
+ => await InteropProtonDriveClient.HandleGetMyFilesFolderAsync(request.DriveClientGetMyFilesFolder).ConfigureAwait(false),
+
+ Request.PayloadOneofCase.DriveClientGetNode
+ => await InteropProtonDriveClient.HandleGetNodeAsync(request.DriveClientGetNode).ConfigureAwait(false),
+
+ Request.PayloadOneofCase.UploadFromStream
+ => InteropFileUploader.HandleUploadFromStream(request.UploadFromStream, bindingsHandle),
+
+ Request.PayloadOneofCase.UploadFromFile
+ => InteropFileUploader.HandleUploadFromFile(request.UploadFromFile, bindingsHandle),
+
+ Request.PayloadOneofCase.FileUploaderFree
+ => InteropFileUploader.HandleFree(request.FileUploaderFree),
+
+ Request.PayloadOneofCase.UploadControllerIsPaused
+ => InteropUploadController.HandleIsPaused(request.UploadControllerIsPaused),
+
+ Request.PayloadOneofCase.UploadControllerAwaitCompletion
+ => await InteropUploadController.HandleAwaitCompletion(request.UploadControllerAwaitCompletion).ConfigureAwait(false),
+
+ Request.PayloadOneofCase.UploadControllerPause
+ => InteropUploadController.HandlePause(request.UploadControllerPause),
+
+ Request.PayloadOneofCase.UploadControllerResume
+ => InteropUploadController.HandleResume(request.UploadControllerResume),
+
+ Request.PayloadOneofCase.UploadControllerDispose
+ => await InteropUploadController.HandleDisposeAsync(request.UploadControllerDispose).ConfigureAwait(false),
+
+ Request.PayloadOneofCase.UploadControllerFree
+ => InteropUploadController.HandleFree(request.UploadControllerFree),
+
+ Request.PayloadOneofCase.DownloadToStream
+ => InteropFileDownloader.HandleDownloadToStream(request.DownloadToStream, bindingsHandle),
+
+ Request.PayloadOneofCase.DownloadToFile
+ => InteropFileDownloader.HandleDownloadToFile(request.DownloadToFile, bindingsHandle),
+
+ Request.PayloadOneofCase.FileDownloaderFree
+ => InteropFileDownloader.HandleFree(request.FileDownloaderFree),
+
+ Request.PayloadOneofCase.DownloadControllerIsPaused
+ => InteropDownloadController.HandleIsPaused(request.DownloadControllerIsPaused),
+
+ Request.PayloadOneofCase.DownloadControllerIsDownloadCompleteWithVerificationIssue
+ => InteropDownloadController.HandleIsDownloadCompleteWithVerificationIssue(
+ request.DownloadControllerIsDownloadCompleteWithVerificationIssue),
+
+ Request.PayloadOneofCase.DownloadControllerAwaitCompletion
+ => await InteropDownloadController.HandleAwaitCompletion(request.DownloadControllerAwaitCompletion).ConfigureAwait(false),
+
+ Request.PayloadOneofCase.DownloadControllerPause
+ => InteropDownloadController.HandlePause(request.DownloadControllerPause),
+
+ Request.PayloadOneofCase.DownloadControllerResume
+ => InteropDownloadController.HandleResume(request.DownloadControllerResume),
+
+ Request.PayloadOneofCase.DownloadControllerFree
+ => await InteropDownloadController.HandleFree(request.DownloadControllerFree).ConfigureAwait(false),
+
+ Request.PayloadOneofCase.DrivePhotosClientCreate
+ => InteropProtonPhotosClient.HandleCreate(request.DrivePhotosClientCreate, bindingsHandle),
+
+ Request.PayloadOneofCase.DrivePhotosClientCreateFromSession
+ => InteropProtonPhotosClient.HandleCreate(request.DrivePhotosClientCreateFromSession),
+
+ Request.PayloadOneofCase.DrivePhotosClientFree
+ => InteropProtonPhotosClient.HandleFree(request.DrivePhotosClientFree),
+
+ Request.PayloadOneofCase.DrivePhotosClientEnumerateThumbnails
+ => await InteropProtonPhotosClient.HandleEnumerateThumbnailsAsync(
+ request.DrivePhotosClientEnumerateThumbnails, bindingsHandle).ConfigureAwait(false),
+
+ Request.PayloadOneofCase.DrivePhotosClientEnumerateTimeline
+ => await InteropProtonPhotosClient.HandleEnumeratePhotosTimelineAsync(
+ request.DrivePhotosClientEnumerateTimeline, bindingsHandle).ConfigureAwait(false),
+
+ Request.PayloadOneofCase.DrivePhotosClientGetNode
+ => await InteropProtonPhotosClient.HandleGetNodeAsync(request.DrivePhotosClientGetNode).ConfigureAwait(false),
+
+ Request.PayloadOneofCase.DrivePhotosClientGetPhotoDownloader
+ => await InteropProtonPhotosClient.HandleGetPhotosDownloaderAsync(request.DrivePhotosClientGetPhotoDownloader).ConfigureAwait(false),
+
+ Request.PayloadOneofCase.DrivePhotosClientDownloadToStream
+ => InteropPhotosDownloader.HandleDownloadToStream(request.DrivePhotosClientDownloadToStream, bindingsHandle),
+
+ Request.PayloadOneofCase.DrivePhotosClientDownloadToFile
+ => InteropPhotosDownloader.HandleDownloadToFile(request.DrivePhotosClientDownloadToFile, bindingsHandle),
+
+ Request.PayloadOneofCase.DrivePhotosClientDownloaderFree
+ => InteropPhotosDownloader.HandleFree(request.DrivePhotosClientDownloaderFree),
+
+ Request.PayloadOneofCase.DrivePhotosClientGetPhotoUploader
+ => await InteropProtonPhotosClient.HandleGetFileUploaderAsync(request.DrivePhotosClientGetPhotoUploader).ConfigureAwait(false),
+
+ Request.PayloadOneofCase.DrivePhotosClientFindDuplicates
+ => await InteropProtonPhotosClient.HandleFindDuplicatesAsync(request.DrivePhotosClientFindDuplicates, bindingsHandle).ConfigureAwait(false),
+
+ Request.PayloadOneofCase.DrivePhotosClientUploadFromStream
+ => InteropPhotosUploader.HandleUploadFromStream(request.DrivePhotosClientUploadFromStream, bindingsHandle),
+
+ Request.PayloadOneofCase.DrivePhotosClientUploadFromFile
+ => InteropPhotosUploader.HandleUploadFromFile(request.DrivePhotosClientUploadFromFile, bindingsHandle),
+
+ Request.PayloadOneofCase.DrivePhotosClientUploaderFree
+ => InteropPhotosUploader.HandleFree(request.DrivePhotosClientUploaderFree),
+
+ Request.PayloadOneofCase.DrivePhotosClientTrashNodes
+ => await InteropProtonPhotosClient.HandleTrashNodesAsync(request.DrivePhotosClientTrashNodes).ConfigureAwait(false),
+
+ Request.PayloadOneofCase.DrivePhotosClientDeleteNodes
+ => await InteropProtonPhotosClient.HandleDeleteNodesAsync(request.DrivePhotosClientDeleteNodes).ConfigureAwait(false),
+
+ Request.PayloadOneofCase.DrivePhotosClientRestoreNodes
+ => await InteropProtonPhotosClient.HandleRestoreNodesAsync(request.DrivePhotosClientRestoreNodes).ConfigureAwait(false),
+
+ Request.PayloadOneofCase.DrivePhotosClientEnumerateTrash
+ => await InteropProtonPhotosClient.HandleEnumerateTrashAsync(request.DrivePhotosClientEnumerateTrash, bindingsHandle).ConfigureAwait(false),
+
+ Request.PayloadOneofCase.DrivePhotosClientEmptyTrash
+ => await InteropProtonPhotosClient.HandleEmptyTrashAsync(request.DrivePhotosClientEmptyTrash).ConfigureAwait(false),
+
+ Request.PayloadOneofCase.None or _
+ => throw new ArgumentException($"Unknown request type: {request.PayloadCase}", nameof(requestBytes)),
+ };
+
+ responseAction.InvokeWithMessage(bindingsHandle, response is not null ? new Response { Value = Any.Pack(response) } : new Response());
+ }
+ catch (Exception e)
+ {
+ var error = e.ToProtoError(InteropDriveErrorConverter.SetDomainAndCodes);
+
+ responseAction.InvokeWithMessage(bindingsHandle, new Response { Error = error });
+ }
+ }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropPhotosDownloader.cs b/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropPhotosDownloader.cs
new file mode 100644
index 00000000..014a95e2
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropPhotosDownloader.cs
@@ -0,0 +1,59 @@
+using Google.Protobuf;
+using Google.Protobuf.WellKnownTypes;
+using Proton.Drive.Sdk.Nodes.Download;
+using Proton.Sdk.CExports;
+
+namespace Proton.Drive.Sdk.CExports;
+
+internal static class InteropPhotosDownloader
+{
+ public static IMessage HandleDownloadToStream(DrivePhotosClientDownloadToStreamRequest request, nint bindingsHandle)
+ {
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+
+ var downloader = Interop.GetFromHandle(request.DownloaderHandle);
+
+ var writeFunction = new InteropFunction, nint, nint>(request.WriteAction);
+
+ var seekAction = request.SeekAction != 0
+ ? new InteropAction, nint>(request.SeekAction)
+ : (InteropAction, nint>?)null;
+
+ var cancelAction = request.CancelAction != 0 ? new InteropAction(request.CancelAction) : (InteropAction?)null;
+ var stream = new InteropStream(bindingsHandle, writeFunction, seekAction, cancelAction);
+
+ var progressAction = new InteropAction>(request.ProgressAction);
+
+ var downloadController = downloader.DownloadToStream(
+ stream,
+ (bytesCompleted, bytesInTotal) => progressAction.InvokeProgressUpdate(bindingsHandle, bytesCompleted, bytesInTotal),
+ cancellationToken);
+
+ return new Int64Value { Value = Interop.AllocHandle(downloadController) };
+ }
+
+ public static IMessage HandleDownloadToFile(DrivePhotosClientDownloadToFileRequest request, nint bindingsHandle)
+ {
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+
+ var downloader = Interop.GetFromHandle(request.DownloaderHandle);
+
+ var progressAction = new InteropAction>(request.ProgressAction);
+
+ var downloadController = downloader.DownloadToFile(
+ request.FilePath,
+ (bytesCompleted, bytesInTotal) => progressAction.InvokeProgressUpdate(bindingsHandle, bytesCompleted, bytesInTotal),
+ cancellationToken);
+
+ return new Int64Value { Value = Interop.AllocHandle(downloadController) };
+ }
+
+ public static IMessage? HandleFree(DrivePhotosClientDownloaderFreeRequest request)
+ {
+ var fileDownloader = Interop.FreeHandle(request.FileDownloaderHandle);
+
+ fileDownloader.Dispose();
+
+ return null;
+ }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropPhotosUploader.cs b/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropPhotosUploader.cs
new file mode 100644
index 00000000..1a62f55a
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropPhotosUploader.cs
@@ -0,0 +1,81 @@
+using Google.Protobuf;
+using Google.Protobuf.WellKnownTypes;
+using Proton.Drive.Sdk.Nodes.Upload;
+using Proton.Sdk.CExports;
+
+namespace Proton.Drive.Sdk.CExports;
+
+internal static class InteropPhotosUploader
+{
+ public static IMessage HandleUploadFromStream(DrivePhotosClientUploadFromStreamRequest request, nint bindingsHandle)
+ {
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+
+ var uploader = Interop.GetFromHandle(request.UploaderHandle);
+
+ var stream = new InteropStream(uploader.FileSize, bindingsHandle, new InteropFunction, nint, nint>(request.ReadAction));
+
+ var thumbnails = request.Thumbnails.Select(t =>
+ {
+ unsafe
+ {
+ var thumbnailType = (Nodes.ThumbnailType)t.Type;
+ return new Nodes.Thumbnail(thumbnailType, new InteropArray((byte*)t.DataPointer, (nint)t.DataLength).ToArray());
+ }
+ });
+
+ var progressAction = new InteropAction>(request.ProgressAction);
+
+ var expectedSha1Provider = request.HasSha1Function ?
+ InteropFileUploader.CreateSha1Provider(bindingsHandle, request.Sha1Function) : null;
+
+ var uploadController = uploader.UploadFromStream(
+ stream,
+ thumbnails,
+ (progress, total) => progressAction.InvokeProgressUpdate(bindingsHandle, progress, total),
+ expectedSha1Provider,
+ forPhotos: true,
+ cancellationToken);
+
+ return new Int64Value { Value = Interop.AllocHandle(uploadController) };
+ }
+
+ public static IMessage HandleUploadFromFile(DrivePhotosClientUploadFromFileRequest request, nint bindingsHandle)
+ {
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+
+ var thumbnails = request.Thumbnails.Select(t =>
+ {
+ unsafe
+ {
+ var thumbnailType = (Nodes.ThumbnailType)t.Type;
+ return new Nodes.Thumbnail(thumbnailType, new InteropArray((byte*)t.DataPointer, (nint)t.DataLength).ToArray());
+ }
+ });
+
+ var progressAction = new InteropAction>(request.ProgressAction);
+ var expectedSha1Provider = request.HasSha1Function ?
+ InteropFileUploader.CreateSha1Provider(bindingsHandle, request.Sha1Function) : null;
+
+ var uploader = Interop.GetFromHandle(request.UploaderHandle);
+
+ var uploadController = uploader.UploadFromFile(
+ request.FilePath,
+ thumbnails,
+ (progress, total) => progressAction.InvokeProgressUpdate(bindingsHandle, progress, total),
+ expectedSha1Provider,
+ forPhotos: true,
+ cancellationToken);
+
+ return new Int64Value { Value = Interop.AllocHandle(uploadController) };
+ }
+
+ public static IMessage? HandleFree(DrivePhotosClientUploaderFreeRequest request)
+ {
+ var fileUploader = Interop.FreeHandle(request.FileUploaderHandle);
+
+ fileUploader.Dispose();
+
+ return null;
+ }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropProtonDriveClient.cs b/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropProtonDriveClient.cs
new file mode 100644
index 00000000..3e3d90c0
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropProtonDriveClient.cs
@@ -0,0 +1,373 @@
+using System.Text.Json;
+using Google.Protobuf;
+using Google.Protobuf.WellKnownTypes;
+using Proton.Drive.Sdk.Nodes;
+using Proton.Drive.Sdk.Nodes.Download;
+using Proton.Drive.Sdk.Nodes.Upload;
+using Proton.Sdk;
+using Proton.Sdk.Caching;
+using Proton.Sdk.CExports;
+using Proton.Sdk.Telemetry;
+
+namespace Proton.Drive.Sdk.CExports;
+
+internal static class InteropProtonDriveClient
+{
+ public static IMessage HandleCreate(DriveClientCreateRequest request, nint bindingsHandle)
+ {
+ if (!request.BaseUrl.EndsWith('/'))
+ {
+ throw new UriFormatException("Base URL must end with a '/'");
+ }
+
+ var protonDriveClientOptions = new Sdk.ProtonDriveClientOptions(
+ request.ClientOptions.HasUid ? request.ClientOptions.Uid : null,
+ request.ClientOptions.HasBindingsLanguage ? request.ClientOptions.BindingsLanguage : null,
+ request.ClientOptions.HasApiCallTimeout ? request.ClientOptions.ApiCallTimeout : null,
+ request.ClientOptions.HasStorageCallTimeout ? request.ClientOptions.StorageCallTimeout : null,
+ request.ClientOptions.HasBlockTransferParallelism ? request.ClientOptions.BlockTransferParallelism : null);
+
+ var httpClientFactory = new InteropHttpClientFactory(
+ bindingsHandle,
+ request.BaseUrl,
+ protonDriveClientOptions.BindingsLanguage,
+ new InteropFunction, nint, nint>(request.HttpClient.RequestFunction),
+ new InteropFunction, nint, nint>(request.HttpClient.ResponseContentReadAction),
+ new InteropAction(request.HttpClient.CancellationAction));
+
+ var accountClient = new InteropAccountClient(bindingsHandle, new InteropAction, nint>(request.AccountRequestAction));
+
+ ICacheRepository entityCacheRepository = request.HasEntityCachePath
+ ? SqliteCacheRepository.OpenFile(request.EntityCachePath)
+ : new InMemoryCacheRepository();
+
+ ICacheRepository secretCacheRepository = request.HasSecretCachePath
+ ? SqliteCacheRepository.OpenFile(request.SecretCachePath)
+ : new InMemoryCacheRepository();
+
+ if (request.HasSecretCacheEncryptionKey)
+ {
+ secretCacheRepository = new EncryptedCacheRepository(
+ secretCacheRepository,
+ request.SecretCacheEncryptionKey.ToByteArray());
+ }
+
+ ITelemetry telemetry = request.Telemetry.ToTelemetry(bindingsHandle) is { } interopTelemetry
+ ? new DriveInteropTelemetryDecorator(interopTelemetry)
+ : NullTelemetry.Instance;
+
+ var featureFlagProvider = request.HasFeatureEnabledFunction
+ ? new InteropFeatureFlagProvider(bindingsHandle, new InteropFunction, int>(request.FeatureEnabledFunction))
+ : AlwaysDisabledFeatureFlagProvider.Instance;
+
+ var client = new ProtonDriveClient(
+ httpClientFactory,
+ accountClient,
+ entityCacheRepository,
+ secretCacheRepository,
+ featureFlagProvider,
+ telemetry,
+ protonDriveClientOptions);
+
+ return new Int64Value
+ {
+ Value = Interop.AllocHandle(client),
+ };
+ }
+
+ public static IMessage HandleCreate(DriveClientCreateFromSessionRequest request)
+ {
+ var session = Interop.GetFromHandle(request.SessionHandle);
+
+ var client = new ProtonDriveClient(session);
+
+ return new Int64Value { Value = Interop.AllocHandle(client) };
+ }
+
+ public static async ValueTask HandleCreateFolderAsync(DriveClientCreateFolderRequest request)
+ {
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+
+ var client = Interop.GetFromHandle(request.ClientHandle);
+
+ var createdFolder = await client.CreateFolderAsync(
+ NodeUid.Parse(request.ParentFolderUid),
+ request.FolderName,
+ request.LastModificationTime?.ToDateTimeFixed(),
+ cancellationToken).ConfigureAwait(false);
+
+ return createdFolder.ToInterop();
+ }
+
+ public static async ValueTask HandleGetFileUploaderAsync(DriveClientGetFileUploaderRequest request)
+ {
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+ var additionalMetadata = request.AdditionalMetadata is { Count: > 0 }
+ ? request.AdditionalMetadata.Select(x =>
+ new Nodes.AdditionalMetadataProperty(x.Name, JsonDocument.Parse(x.Utf8JsonValue.Memory).RootElement))
+ : null;
+
+ var metadata = new FileUploadMetadata
+ {
+ LastModificationTime = request.LastModificationTime?.ToDateTimeFixed(),
+ AdditionalMetadata = additionalMetadata,
+ };
+
+ var client = Interop.GetFromHandle(request.ClientHandle);
+
+ FileUploader? fileUploader;
+ if (request is { HasNoWaiting: true, NoWaiting: true })
+ {
+#pragma warning disable TryTransferQueuing
+ fileUploader = client.TryGetFileUploader(
+ NodeUid.Parse(request.ParentFolderUid),
+ request.Name,
+ request.MediaType,
+ request.Size,
+ metadata,
+ request.OverrideExistingDraftByOtherClient);
+#pragma warning restore TryTransferQueuing
+ }
+ else
+ {
+ fileUploader = await client.GetFileUploaderAsync(
+ NodeUid.Parse(request.ParentFolderUid),
+ request.Name,
+ request.MediaType,
+ request.Size,
+ metadata,
+ request.OverrideExistingDraftByOtherClient,
+ cancellationToken).ConfigureAwait(false);
+ }
+
+ return new Int64Value { Value = fileUploader is null ? 0 : Interop.AllocHandle(fileUploader) };
+ }
+
+ public static async ValueTask HandleGetFileRevisionUploaderAsync(DriveClientGetFileRevisionUploaderRequest request)
+ {
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+
+ var client = Interop.GetFromHandle(request.ClientHandle);
+
+ var additionalMetadata = request.AdditionalMetadata.Count > 0
+ ? request.AdditionalMetadata.Select(x =>
+ new Nodes.AdditionalMetadataProperty(x.Name, JsonDocument.Parse(x.Utf8JsonValue.Memory).RootElement))
+ : null;
+
+ var metadata = new FileUploadMetadata
+ {
+ LastModificationTime = request.LastModificationTime?.ToDateTimeFixed(),
+ AdditionalMetadata = additionalMetadata,
+ };
+
+ FileUploader? fileUploader;
+ if (request is { HasNoWaiting: true, NoWaiting: true })
+ {
+#pragma warning disable TryTransferQueuing
+ fileUploader = client.TryGetFileRevisionUploader(
+ RevisionUid.Parse(request.CurrentActiveRevisionUid),
+ request.Size,
+ metadata);
+#pragma warning restore TryTransferQueuing
+ }
+ else
+ {
+ fileUploader = await client.GetFileRevisionUploaderAsync(
+ RevisionUid.Parse(request.CurrentActiveRevisionUid),
+ request.Size,
+ metadata,
+ cancellationToken).ConfigureAwait(false);
+ }
+
+ return new Int64Value { Value = fileUploader is null ? 0 : Interop.AllocHandle(fileUploader) };
+ }
+
+ public static async ValueTask HandleGetAvailableNameAsync(DriveClientGetAvailableNameRequest request)
+ {
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+
+ var client = Interop.GetFromHandle(request.ClientHandle);
+
+ var availableName = await client.GetAvailableNameAsync(
+ NodeUid.Parse(request.ParentFolderUid),
+ request.Name,
+ cancellationToken).ConfigureAwait(false);
+
+ return new StringValue { Value = availableName };
+ }
+
+ public static async ValueTask HandleEnumerateThumbnailsAsync(DriveClientEnumerateThumbnailsRequest request, nint bindingsHandle)
+ {
+ var yieldFunction = new InteropAction>(request.YieldAction);
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+
+ var client = Interop.GetFromHandle(request.ClientHandle);
+
+ var thumbnailsEnumerable = client.EnumerateThumbnailsAsync(
+ request.FileUids.Select(NodeUid.Parse),
+ (Nodes.ThumbnailType)request.Type,
+ cancellationToken);
+
+ await foreach (var x in thumbnailsEnumerable.ConfigureAwait(false))
+ {
+ var thumbnail = new FileThumbnail { FileUid = x.FileUid.ToString() };
+
+ if (x.Result.TryGetValueElseError(out var data, out var error))
+ {
+ thumbnail.Data = ByteString.CopyFrom(data.Span);
+ }
+ else
+ {
+ thumbnail.Error = error.ToInterop();
+ }
+
+ yieldFunction.InvokeWithMessage(bindingsHandle, thumbnail);
+ }
+
+ return null;
+ }
+
+ public static async ValueTask HandleEnumerateFolderChildrenAsync(DriveClientEnumerateFolderChildrenRequest request, nint bindingsHandle)
+ {
+ var yieldFunction = new InteropAction>(request.YieldAction);
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+
+ var client = Interop.GetFromHandle(request.ClientHandle);
+
+ await foreach (var x in client.EnumerateFolderChildrenAsync(NodeUid.Parse(request.FolderUid), cancellationToken).ConfigureAwait(false))
+ {
+ yieldFunction.InvokeWithMessage(bindingsHandle, x.ToInterop());
+ }
+
+ return null;
+ }
+
+ public static async ValueTask HandleGetMyFilesFolderAsync(DriveClientGetMyFilesFolderRequest request)
+ {
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+ var client = Interop.GetFromHandle(request.ClientHandle);
+
+ var folderNode = await client.GetMyFilesFolderAsync(cancellationToken).ConfigureAwait(false);
+
+ return folderNode.ToInterop();
+ }
+
+ public static async ValueTask HandleGetFileDownloaderAsync(DriveClientGetFileDownloaderRequest request)
+ {
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+ var client = Interop.GetFromHandle(request.ClientHandle);
+ var revisionUid = RevisionUid.Parse(request.RevisionUid);
+
+ FileDownloader? fileDownloader;
+ if (request is { HasNoWaiting: true, NoWaiting: true })
+ {
+#pragma warning disable TryTransferQueuing
+ fileDownloader = client.TryGetFileDownloader(revisionUid);
+#pragma warning restore TryTransferQueuing
+ }
+ else
+ {
+ fileDownloader = await client.GetFileDownloaderAsync(revisionUid, cancellationToken).ConfigureAwait(false);
+ }
+
+ return new Int64Value { Value = fileDownloader is null ? 0 : Interop.AllocHandle(fileDownloader) };
+ }
+
+ public static async ValueTask HandleRenameAsync(DriveClientRenameRequest request)
+ {
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+
+ var client = Interop.GetFromHandle(request.ClientHandle);
+
+ await client.RenameNodeAsync(
+ NodeUid.Parse(request.NodeUid),
+ request.NewName,
+ request.NewMediaType,
+ cancellationToken).ConfigureAwait(false);
+ return null;
+ }
+
+ public static async ValueTask HandleTrashNodesAsync(DriveClientTrashNodesRequest request)
+ {
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+
+ var client = Interop.GetFromHandle(request.ClientHandle);
+
+ var results = await client.TrashNodesAsync(
+ request.NodeUids.Select(NodeUid.Parse),
+ cancellationToken).ConfigureAwait(false);
+
+ return results.ToInterop();
+ }
+
+ public static async ValueTask HandleDeleteNodesAsync(DriveClientDeleteNodesRequest request)
+ {
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+
+ var client = Interop.GetFromHandle(request.ClientHandle);
+
+ var results = await client.DeleteNodesAsync(
+ request.NodeUids.Select(NodeUid.Parse),
+ cancellationToken).ConfigureAwait(false);
+
+ return results.ToInterop();
+ }
+
+ public static async ValueTask HandleRestoreNodesAsync(DriveClientRestoreNodesRequest request)
+ {
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+
+ var client = Interop.GetFromHandle(request.ClientHandle);
+
+ var results = await client.RestoreNodesAsync(
+ request.NodeUids.Select(NodeUid.Parse),
+ cancellationToken).ConfigureAwait(false);
+
+ return results.ToInterop();
+ }
+
+ public static async ValueTask HandleEnumerateTrashAsync(DriveClientEnumerateTrashRequest request, nint bindingsHandle)
+ {
+ var yieldFunction = new InteropAction>(request.YieldAction);
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+
+ var client = Interop.GetFromHandle(request.ClientHandle);
+
+ await foreach (var x in client.EnumerateTrashAsync(cancellationToken).ConfigureAwait(false))
+ {
+ yieldFunction.InvokeWithMessage(bindingsHandle, x.ToInterop());
+ }
+
+ return null;
+ }
+
+ public static async ValueTask HandleGetNodeAsync(DriveClientGetNodeRequest request)
+ {
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+ var client = Interop.GetFromHandle(request.ClientHandle);
+
+ var node = await client.GetNodeAsync(
+ NodeUid.Parse(request.NodeUid),
+ cancellationToken).ConfigureAwait(false);
+
+ return node?.ToInterop();
+ }
+
+ public static async ValueTask HandleEmptyTrashAsync(DriveClientEmptyTrashRequest request)
+ {
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+
+ var client = Interop.GetFromHandle(request.ClientHandle);
+
+ await client.EmptyTrashAsync(cancellationToken).ConfigureAwait(false);
+
+ return null;
+ }
+
+ public static IMessage? HandleFree(DriveClientFreeRequest request)
+ {
+ Interop.FreeHandle(request.ClientHandle);
+
+ return null;
+ }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropProtonPhotosClient.cs b/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropProtonPhotosClient.cs
new file mode 100644
index 00000000..e58ed7a9
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropProtonPhotosClient.cs
@@ -0,0 +1,302 @@
+using System.Text.Json;
+using Google.Protobuf;
+using Google.Protobuf.WellKnownTypes;
+using Proton.Drive.Sdk.Nodes;
+using Proton.Drive.Sdk.Nodes.Download;
+using Proton.Drive.Sdk.Nodes.Upload;
+using Proton.Sdk;
+using Proton.Sdk.Caching;
+using Proton.Sdk.CExports;
+using Proton.Sdk.Telemetry;
+
+namespace Proton.Drive.Sdk.CExports;
+
+internal static class InteropProtonPhotosClient
+{
+ public static IMessage HandleCreate(DrivePhotosClientCreateRequest request, nint bindingsHandle)
+ {
+ if (!request.BaseUrl.EndsWith('/'))
+ {
+ throw new UriFormatException("Base URL must end with a '/'");
+ }
+
+ var protonDriveClientOptions = new Sdk.ProtonDriveClientOptions(
+ request.ClientOptions.HasUid ? request.ClientOptions.Uid : null,
+ request.ClientOptions.HasBindingsLanguage ? request.ClientOptions.BindingsLanguage : null,
+ request.ClientOptions.HasApiCallTimeout ? request.ClientOptions.ApiCallTimeout : null,
+ request.ClientOptions.HasStorageCallTimeout ? request.ClientOptions.StorageCallTimeout : null,
+ request.ClientOptions.HasBlockTransferParallelism ? request.ClientOptions.BlockTransferParallelism : null);
+
+ var httpClientFactory = new InteropHttpClientFactory(
+ bindingsHandle,
+ request.BaseUrl,
+ protonDriveClientOptions.BindingsLanguage,
+ new InteropFunction, nint, nint>(request.HttpClient.RequestFunction),
+ new InteropFunction, nint, nint>(request.HttpClient.ResponseContentReadAction),
+ new InteropAction(request.HttpClient.CancellationAction));
+
+ var accountClient = new InteropAccountClient(bindingsHandle, new InteropAction, nint>(request.AccountRequestAction));
+
+ var entityCacheRepository = request.HasEntityCachePath
+ ? SqliteCacheRepository.OpenFile(request.EntityCachePath)
+ : SqliteCacheRepository.OpenInMemory();
+
+ var secretCacheRepository = request.HasSecretCachePath
+ ? SqliteCacheRepository.OpenFile(request.SecretCachePath)
+ : SqliteCacheRepository.OpenInMemory();
+
+ ITelemetry telemetry = request.Telemetry.ToTelemetry(bindingsHandle) is { } interopTelemetry
+ ? new DriveInteropTelemetryDecorator(interopTelemetry)
+ : NullTelemetry.Instance;
+
+ var featureFlagProvider = request.HasFeatureEnabledFunction
+ ? new InteropFeatureFlagProvider(bindingsHandle, new InteropFunction, int>(request.FeatureEnabledFunction))
+ : AlwaysDisabledFeatureFlagProvider.Instance;
+
+ var client = new ProtonPhotosClient(
+ httpClientFactory,
+ accountClient,
+ entityCacheRepository,
+ secretCacheRepository,
+ featureFlagProvider,
+ telemetry,
+ protonDriveClientOptions);
+
+ return new Int64Value
+ {
+ Value = Interop.AllocHandle(client),
+ };
+ }
+
+ public static IMessage HandleCreate(DrivePhotosClientCreateFromSessionRequest request)
+ {
+ var session = Interop.GetFromHandle(request.SessionHandle);
+
+ var client = new ProtonPhotosClient(session, request.Uid);
+
+ return new Int64Value { Value = Interop.AllocHandle(client) };
+ }
+
+ public static async ValueTask HandleTrashNodesAsync(DrivePhotosClientTrashNodesRequest request)
+ {
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+
+ var client = Interop.GetFromHandle(request.ClientHandle);
+
+ var results = await client.TrashNodesAsync(
+ request.NodeUids.Select(NodeUid.Parse),
+ cancellationToken).ConfigureAwait(false);
+
+ return results.ToInterop();
+ }
+
+ public static async ValueTask HandleDeleteNodesAsync(DrivePhotosClientDeleteNodesRequest request)
+ {
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+
+ var client = Interop.GetFromHandle(request.ClientHandle);
+
+ var results = await client.DeleteNodesAsync(
+ request.NodeUids.Select(NodeUid.Parse),
+ cancellationToken).ConfigureAwait(false);
+
+ return results.ToInterop();
+ }
+
+ public static async ValueTask HandleRestoreNodesAsync(DrivePhotosClientRestoreNodesRequest request)
+ {
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+
+ var client = Interop.GetFromHandle(request.ClientHandle);
+
+ var results = await client.RestoreNodesAsync(
+ request.NodeUids.Select(NodeUid.Parse),
+ cancellationToken).ConfigureAwait(false);
+
+ return results.ToInterop();
+ }
+
+ public static async ValueTask HandleEnumerateTrashAsync(DrivePhotosClientEnumerateTrashRequest request, nint bindingsHandle)
+ {
+ var yieldFunction = new InteropAction>(request.YieldAction);
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+
+ var client = Interop.GetFromHandle(request.ClientHandle);
+
+ await foreach (var x in client.EnumerateTrashAsync(cancellationToken).ConfigureAwait(false))
+ {
+ yieldFunction.InvokeWithMessage(bindingsHandle, x.ToInterop());
+ }
+
+ return null;
+ }
+
+ public static async ValueTask HandleEmptyTrashAsync(DrivePhotosClientEmptyTrashRequest request)
+ {
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+
+ var client = Interop.GetFromHandle(request.ClientHandle);
+
+ await client.EmptyTrashAsync(cancellationToken).ConfigureAwait(false);
+
+ return null;
+ }
+
+ public static IMessage? HandleFree(DrivePhotosClientFreeRequest request)
+ {
+ Interop.FreeHandle(request.ClientHandle);
+
+ return null;
+ }
+
+ public static async ValueTask HandleGetNodeAsync(DrivePhotosClientGetNodeRequest request)
+ {
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+ var client = Interop.GetFromHandle(request.ClientHandle);
+
+ var node = await client.GetNodeAsync(NodeUid.Parse(request.NodeUid), cancellationToken).ConfigureAwait(false);
+
+ return node?.ToInterop();
+ }
+
+ public static async ValueTask HandleEnumeratePhotosTimelineAsync(DrivePhotosClientEnumerateTimelineRequest request, nint bindingsHandle)
+ {
+ var yieldFunction = new InteropAction>(request.YieldAction);
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+ var client = Interop.GetFromHandle(request.ClientHandle);
+
+ await foreach (var x in client.EnumerateTimelineAsync(cancellationToken).ConfigureAwait(false))
+ {
+ yieldFunction.InvokeWithMessage(bindingsHandle, new PhotosTimelineItem
+ {
+ NodeUid = x.Uid.ToString(),
+ CaptureTime = x.CaptureTime.ToUniversalTime().ToTimestamp(),
+ });
+ }
+
+ return null;
+ }
+
+ public static async ValueTask HandleGetPhotosDownloaderAsync(DrivePhotosClientGetPhotoDownloaderRequest request)
+ {
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+
+ var client = Interop.GetFromHandle(request.ClientHandle);
+
+ var photoUid = NodeUid.Parse(request.PhotoUid);
+
+ PhotosFileDownloader? downloader;
+ if (request is { HasNoWaiting: true, NoWaiting: true })
+ {
+#pragma warning disable TryTransferQueuing
+ downloader = client.TryGetPhotosDownloader(photoUid);
+#pragma warning restore TryTransferQueuing
+ }
+ else
+ {
+ downloader = await client.GetPhotosDownloaderAsync(photoUid, cancellationToken).ConfigureAwait(false);
+ }
+
+ return new Int64Value { Value = downloader is null ? 0 : Interop.AllocHandle(downloader) };
+ }
+
+ public static async ValueTask HandleEnumerateThumbnailsAsync(DrivePhotosClientEnumerateThumbnailsRequest request, nint bindingsHandle)
+ {
+ var yieldFunction = new InteropAction>(request.YieldAction);
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+
+ var client = Interop.GetFromHandle(request.ClientHandle);
+
+ var thumbnailsEnumerable = client.EnumerateThumbnailsAsync(
+ request.PhotoUids.Select(NodeUid.Parse),
+ (Nodes.ThumbnailType)request.Type,
+ cancellationToken);
+
+ await foreach (var x in thumbnailsEnumerable.ConfigureAwait(false))
+ {
+ var thumbnail = new FileThumbnail { FileUid = x.FileUid.ToString() };
+ if (x.Result.TryGetValueElseError(out var data, out var error))
+ {
+ thumbnail.Data = ByteString.CopyFrom(data.Span);
+ }
+ else
+ {
+ thumbnail.Error = error.ToInterop();
+ }
+
+ yieldFunction.InvokeWithMessage(bindingsHandle, thumbnail);
+ }
+
+ return null;
+ }
+
+ public static async ValueTask HandleGetFileUploaderAsync(DrivePhotosClientGetPhotoUploaderRequest request)
+ {
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+
+ var tags = request.Metadata.Tags is { Count: > 0 }
+ ? request.Metadata.Tags.Select(t => (Nodes.PhotoTag)t)
+ : null;
+
+ var additionalMetadata = request.Metadata.AdditionalMetadata is { Count: > 0 }
+ ? request.Metadata.AdditionalMetadata.Select(x =>
+ new Nodes.AdditionalMetadataProperty(x.Name, JsonDocument.Parse(x.Utf8JsonValue.Memory).RootElement))
+ : null;
+
+ var metadata = new Nodes.PhotosFileUploadMetadata
+ {
+ AdditionalMetadata = additionalMetadata,
+ LastModificationTime = request.Metadata.LastModificationTime?.ToDateTimeFixed(),
+ CaptureTime = request.Metadata.CaptureTime?.ToDateTimeFixed(),
+ MainPhotoUid = request.Metadata.HasMainPhotoUid ? NodeUid.Parse(request.Metadata.MainPhotoUid) : null,
+ Tags = tags,
+ };
+
+ var client = Interop.GetFromHandle(request.ClientHandle);
+
+ FileUploader? uploader;
+ if (request is { HasNoWaiting: true, NoWaiting: true })
+ {
+#pragma warning disable TryTransferQueuing
+ uploader = await client.TryGetFileUploaderAsync(
+ request.Name,
+ request.MediaType,
+ request.Size,
+ metadata,
+ request.OverrideExistingDraftByOtherClient,
+ cancellationToken).ConfigureAwait(false);
+#pragma warning restore TryTransferQueuing
+ }
+ else
+ {
+ uploader = await client.GetFileUploaderAsync(
+ request.Name,
+ request.MediaType,
+ request.Size,
+ metadata,
+ request.OverrideExistingDraftByOtherClient,
+ cancellationToken).ConfigureAwait(false);
+ }
+
+ return new Int64Value { Value = uploader is null ? 0 : Interop.AllocHandle(uploader) };
+ }
+
+ public static async ValueTask HandleFindDuplicatesAsync(DrivePhotosClientFindDuplicatesRequest request, nint bindingsHandle)
+ {
+ var cancellationToken = Interop.GetCancellationToken(request.CancellationTokenSourceHandle);
+
+ var client = Interop.GetFromHandle(request.ClientHandle);
+
+ var duplicates = await client.FindDuplicatesAsync(request.Name, GenerateSha1Action, cancellationToken).ConfigureAwait(false);
+
+ var result = new ListValue();
+ result.Values.AddRange(duplicates.Select(Value.ForString));
+
+ return result;
+
+ static void GenerateSha1Action(string sha1)
+ {
+ // TODO: Implement SHA1 generation callback
+ }
+ }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropUploadController.cs b/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropUploadController.cs
new file mode 100644
index 00000000..5b505c3c
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk.CExports/InteropUploadController.cs
@@ -0,0 +1,59 @@
+using Google.Protobuf;
+using Google.Protobuf.WellKnownTypes;
+using Proton.Drive.Sdk.Nodes.Upload;
+using Proton.Sdk.CExports;
+
+namespace Proton.Drive.Sdk.CExports;
+
+internal static class InteropUploadController
+{
+ public static IMessage HandleIsPaused(UploadControllerIsPausedRequest request)
+ {
+ var uploadController = Interop.GetFromHandle(request.UploadControllerHandle);
+
+ return new BoolValue { Value = uploadController.IsPaused };
+ }
+
+ public static async ValueTask HandleAwaitCompletion(UploadControllerAwaitCompletionRequest request)
+ {
+ var uploadController = Interop.GetFromHandle(request.UploadControllerHandle);
+
+ var uploadResult = await uploadController.Completion.ConfigureAwait(false);
+
+ return new UploadResult { NodeUid = uploadResult.NodeUid.ToString(), RevisionUid = uploadResult.RevisionUid.ToString() };
+ }
+
+ public static IMessage? HandlePause(UploadControllerPauseRequest request)
+ {
+ var uploadController = Interop.GetFromHandle(request.UploadControllerHandle);
+
+ uploadController.Pause();
+
+ return null;
+ }
+
+ public static IMessage? HandleResume(UploadControllerResumeRequest request)
+ {
+ var uploadController = Interop.GetFromHandle(request.UploadControllerHandle);
+
+ uploadController.Resume();
+
+ return null;
+ }
+
+ public static async ValueTask HandleDisposeAsync(UploadControllerDisposeRequest request)
+ {
+ var uploadController = Interop.GetFromHandle(request.UploadControllerHandle);
+
+ await uploadController.DisposeAsync().ConfigureAwait(false);
+
+ return null;
+ }
+
+ public static IMessage? HandleFree(UploadControllerFreeRequest request)
+ {
+ Interop.FreeHandle(request.UploadControllerHandle);
+
+ return null;
+ }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk.CExports/NativeLibraryResolver.cs b/cs/sdk/src/Proton.Drive.Sdk.CExports/NativeLibraryResolver.cs
new file mode 100644
index 00000000..1503485b
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk.CExports/NativeLibraryResolver.cs
@@ -0,0 +1,35 @@
+using System.Collections.Concurrent;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Runtime.Loader;
+using System.Text;
+using Proton.Sdk.CExports;
+
+namespace Proton.Drive.Sdk.CExports;
+
+public static class NativeLibraryResolver
+{
+ private static readonly ConcurrentDictionary LibraryNameMap = new();
+
+ [UnmanagedCallersOnly(EntryPoint = "override_native_library_name", CallConvs = [typeof(CallConvCdecl)])]
+ private static void OverrideNativeLibraryName(InteropArray libraryNameBytes, InteropArray overridingLibraryNameBytes)
+ {
+ var libraryName = Encoding.UTF8.GetString(libraryNameBytes.AsReadOnlySpan());
+
+ LibraryNameMap[libraryName] = Encoding.UTF8.GetString(overridingLibraryNameBytes.AsReadOnlySpan());
+
+ AssemblyLoadContext.Default.ResolvingUnmanagedDll -= Resolve;
+ AssemblyLoadContext.Default.ResolvingUnmanagedDll += Resolve;
+ }
+
+ private static nint Resolve(Assembly assembly, string libraryName)
+ {
+ if (LibraryNameMap.TryGetValue(libraryName, out var overridingLibraryName))
+ {
+ libraryName = overridingLibraryName;
+ }
+
+ return NativeLibrary.Load(libraryName, assembly, null);
+ }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk.CExports/Proton.Drive.Sdk.CExports.csproj b/cs/sdk/src/Proton.Drive.Sdk.CExports/Proton.Drive.Sdk.CExports.csproj
new file mode 100644
index 00000000..7c293d6c
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk.CExports/Proton.Drive.Sdk.CExports.csproj
@@ -0,0 +1,62 @@
+
+
+
+ $(NativeLibPrefix)proton_drive_sdk
+ true
+ true
+ false
+ proton_crypto
+
+ $(NoWarn);Photos
+
+
+ $(NoWarn);IL2113
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ true
+
+
+
+ Static
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/cs/sdk/src/Proton.Drive.Sdk.CExports/TimestampExtensions.cs b/cs/sdk/src/Proton.Drive.Sdk.CExports/TimestampExtensions.cs
new file mode 100644
index 00000000..0563f7d6
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk.CExports/TimestampExtensions.cs
@@ -0,0 +1,19 @@
+using Google.Protobuf.WellKnownTypes;
+
+namespace Proton.Drive.Sdk.CExports;
+
+internal static class TimestampExtensions
+{
+ // Workaround for issue: http://github.com/protocolbuffers/protobuf/issues/26006
+ public static DateTime ToDateTimeFixed(this Timestamp timestamp)
+ {
+ try
+ {
+ return timestamp.ToDateTime();
+ }
+ catch (InvalidOperationException e)
+ {
+ throw new InvalidOperationException($"Timestamp contains invalid values: Seconds={timestamp.Seconds}; Nanos={timestamp.Nanos}", e);
+ }
+ }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/AccountClientAdapter.cs b/cs/sdk/src/Proton.Drive.Sdk/AccountClientAdapter.cs
index 1149604f..f624df44 100644
--- a/cs/sdk/src/Proton.Drive.Sdk/AccountClientAdapter.cs
+++ b/cs/sdk/src/Proton.Drive.Sdk/AccountClientAdapter.cs
@@ -8,7 +8,7 @@ internal sealed class AccountClientAdapter(ProtonApiSession session) : IAccountC
{
private readonly ProtonAccountClient _client = new(session);
- public ValueTask GetAddressAsync(ProtonDriveClient client, AddressId addressId, CancellationToken cancellationToken)
+ public ValueTask GetAddressAsync(AddressId addressId, CancellationToken cancellationToken)
{
return _client.GetAddressAsync(addressId, cancellationToken);
}
@@ -23,11 +23,6 @@ public ValueTask GetAddressPrimaryPrivateKeyAsync(AddressId addre
return _client.GetAddressPrimaryPrivateKeyAsync(addressId, cancellationToken);
}
- public ValueTask GetAddressPrivateKeyAsync(AddressId addressId, int index, CancellationToken cancellationToken)
- {
- return _client.GetAddressPrivateKeyAsync(addressId, index, cancellationToken);
- }
-
public ValueTask> GetAddressPrivateKeysAsync(AddressId addressId, CancellationToken cancellationToken)
{
return _client.GetAddressPrivateKeysAsync(addressId, cancellationToken);
diff --git a/cs/sdk/src/Proton.Drive.Sdk/AlternateFileNameGenerator.cs b/cs/sdk/src/Proton.Drive.Sdk/AlternateFileNameGenerator.cs
new file mode 100644
index 00000000..7af31c0a
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk/AlternateFileNameGenerator.cs
@@ -0,0 +1,12 @@
+namespace Proton.Drive.Sdk;
+
+internal static class AlternateFileNameGenerator
+{
+ public static IEnumerable GetNames(string originalName)
+ {
+ var nameWithoutExtension = Path.GetFileNameWithoutExtension(originalName);
+ var extension = originalName[nameWithoutExtension.Length..];
+
+ return Enumerable.Range(1, int.MaxValue).Select(i => $"{nameWithoutExtension} ({i}){extension}");
+ }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/BlockVerification/BlockVerificationApiClient.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/BlockVerification/BlockVerificationApiClient.cs
new file mode 100644
index 00000000..372b2385
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/BlockVerification/BlockVerificationApiClient.cs
@@ -0,0 +1,23 @@
+using Proton.Drive.Sdk.Api.Files;
+using Proton.Drive.Sdk.Api.Links;
+using Proton.Drive.Sdk.Serialization;
+using Proton.Drive.Sdk.Volumes;
+using Proton.Sdk.Http;
+
+namespace Proton.Drive.Sdk.Api.BlockVerification;
+
+internal sealed class BlockVerificationApiClient(HttpClient httpClient) : IBlockVerificationApiClient
+{
+ private readonly HttpClient _httpClient = httpClient;
+
+ public async ValueTask GetVerificationInputAsync(
+ VolumeId volumeId,
+ LinkId linkId,
+ RevisionId revisionId,
+ CancellationToken cancellationToken)
+ {
+ return await _httpClient
+ .Expecting(DriveApiSerializerContext.Default.BlockVerificationInputResponse)
+ .GetAsync($"v2/volumes/{volumeId}/links/{linkId}/revisions/{revisionId}/verification", cancellationToken).ConfigureAwait(false);
+ }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/IRevisionVerificationApiClient.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/BlockVerification/IBlockVerificationApiClient.cs
similarity index 68%
rename from cs/sdk/src/Proton.Drive.Sdk/Api/Files/IRevisionVerificationApiClient.cs
rename to cs/sdk/src/Proton.Drive.Sdk/Api/BlockVerification/IBlockVerificationApiClient.cs
index 7399e9a2..93939ecd 100644
--- a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/IRevisionVerificationApiClient.cs
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/BlockVerification/IBlockVerificationApiClient.cs
@@ -1,9 +1,10 @@
+using Proton.Drive.Sdk.Api.Files;
using Proton.Drive.Sdk.Api.Links;
using Proton.Drive.Sdk.Volumes;
-namespace Proton.Drive.Sdk.Api.Files;
+namespace Proton.Drive.Sdk.Api.BlockVerification;
-internal interface IRevisionVerificationApiClient
+internal interface IBlockVerificationApiClient
{
public ValueTask GetVerificationInputAsync(
VolumeId volumeId,
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/DriveApiClients.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/DriveApiClients.cs
index 8ea53ebb..55a21d17 100644
--- a/cs/sdk/src/Proton.Drive.Sdk/Api/DriveApiClients.cs
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/DriveApiClients.cs
@@ -1,18 +1,21 @@
using Proton.Drive.Sdk.Api.Files;
using Proton.Drive.Sdk.Api.Folders;
using Proton.Drive.Sdk.Api.Links;
+using Proton.Drive.Sdk.Api.Photos;
using Proton.Drive.Sdk.Api.Shares;
using Proton.Drive.Sdk.Api.Storage;
using Proton.Drive.Sdk.Api.Volumes;
namespace Proton.Drive.Sdk.Api;
-internal sealed class DriveApiClients(HttpClient httpClient) : IDriveApiClients
+internal sealed class DriveApiClients(HttpClient defaultHttpClient, HttpClient storageHttpClient) : IDriveApiClients
{
- public IVolumesApiClient Volumes { get; } = new VolumesApiClient(httpClient);
- public ISharesApiClient Shares { get; } = new SharesApiClient(httpClient);
- public ILinksApiClient Links { get; } = new LinksApiClient(httpClient);
- public IFoldersApiClient Folders { get; } = new FoldersApiClient(httpClient);
- public IFilesApiClient Files { get; } = new FilesApiClient(httpClient);
- public IStorageApiClient Storage { get; } = new StorageApiClient(httpClient);
+ public IVolumesApiClient Volumes { get; } = new VolumesApiClient(defaultHttpClient);
+ public ISharesApiClient Shares { get; } = new SharesApiClient(defaultHttpClient);
+ public ILinksApiClient Links { get; } = new LinksApiClient(defaultHttpClient);
+ public IFoldersApiClient Folders { get; } = new FoldersApiClient(defaultHttpClient);
+ public IFilesApiClient Files { get; } = new FilesApiClient(defaultHttpClient);
+ public IStorageApiClient Storage { get; } = new StorageApiClient(defaultHttpClient, storageHttpClient);
+ public ITrashApiClient Trash { get; } = new TrashApiClient(defaultHttpClient);
+ public IPhotosApiClient Photos { get; } = new PhotosApiClient(defaultHttpClient);
}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/DriveApiClientsExtensions.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/DriveApiClientsExtensions.cs
new file mode 100644
index 00000000..e1bfb451
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/DriveApiClientsExtensions.cs
@@ -0,0 +1,17 @@
+using Proton.Drive.Sdk.Api.Links;
+using Proton.Drive.Sdk.Volumes;
+
+namespace Proton.Drive.Sdk.Api;
+
+internal static class DriveApiClientsExtensions
+{
+ public static ValueTask GetLinkDetailsAsync(
+ this IDriveApiClients api,
+ VolumeId volumeId,
+ IEnumerable linkIds,
+ bool forPhotos,
+ CancellationToken cancellationToken)
+ => forPhotos
+ ? api.Photos.GetDetailsAsync(volumeId, linkIds, cancellationToken)
+ : api.Links.GetDetailsAsync(volumeId, linkIds, cancellationToken);
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Events/LatestVolumeEventResponse.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Events/LatestVolumeEventResponse.cs
new file mode 100644
index 00000000..e1a7dce6
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Events/LatestVolumeEventResponse.cs
@@ -0,0 +1,11 @@
+using System.Text.Json.Serialization;
+using Proton.Sdk.Api;
+using Proton.Sdk.Events;
+
+namespace Proton.Drive.Sdk.Api.Events;
+
+internal sealed class LatestVolumeEventResponse : ApiResponse
+{
+ [JsonPropertyName("EventID")]
+ public required DriveEventId EventId { get; init; }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Events/VolumeEventDto.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Events/VolumeEventDto.cs
new file mode 100644
index 00000000..621d9c6c
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Events/VolumeEventDto.cs
@@ -0,0 +1,15 @@
+using System.Text.Json.Serialization;
+using Proton.Sdk.Events;
+
+namespace Proton.Drive.Sdk.Api.Events;
+
+internal sealed class VolumeEventDto
+{
+ [JsonPropertyName("EventID")]
+ public required DriveEventId Id { get; init; }
+
+ [JsonPropertyName("EventType")]
+ public required VolumeEventType Type { get; init; }
+
+ public required VolumeEventLinkDto Link { get; init; }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Events/VolumeEventDtoExtensions.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Events/VolumeEventDtoExtensions.cs
new file mode 100644
index 00000000..248e6e2f
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Events/VolumeEventDtoExtensions.cs
@@ -0,0 +1,30 @@
+using System.ComponentModel;
+using Proton.Drive.Sdk.Events;
+using Proton.Drive.Sdk.Nodes;
+using Proton.Drive.Sdk.Volumes;
+
+namespace Proton.Drive.Sdk.Api.Events;
+
+internal static class VolumeEventDtoExtensions
+{
+ public static DriveEvent ToDriveEvent(this VolumeEventDto volumeEvent, VolumeId volumeId)
+ {
+ var nodeUid = new NodeUid(volumeId, volumeEvent.Link.Id);
+ NodeUid? parentNodeUid = volumeEvent.Link.ParentId is { } parentLinkId
+ ? new NodeUid(volumeId, parentLinkId)
+ : null;
+
+ return volumeEvent.Type switch
+ {
+ VolumeEventType.Create or VolumeEventType.Update or VolumeEventType.UpdateMetadata => new NodeUpdatedEvent(
+ volumeEvent.Id,
+ nodeUid,
+ parentNodeUid,
+ volumeEvent.Link.IsTrashed,
+ volumeEvent.Link.IsShared),
+ VolumeEventType.Delete => new NodeDeletedEvent(volumeEvent.Id, nodeUid, parentNodeUid),
+
+ _ => throw new InvalidEnumArgumentException(nameof(volumeEvent), (int)volumeEvent.Type, typeof(VolumeEventType)),
+ };
+ }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Events/VolumeEventLinkDto.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Events/VolumeEventLinkDto.cs
new file mode 100644
index 00000000..b4c01002
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Events/VolumeEventLinkDto.cs
@@ -0,0 +1,17 @@
+using System.Text.Json.Serialization;
+using Proton.Drive.Sdk.Api.Links;
+
+namespace Proton.Drive.Sdk.Api.Events;
+
+internal sealed class VolumeEventLinkDto
+{
+ [JsonPropertyName("LinkID")]
+ public required LinkId Id { get; init; }
+
+ [JsonPropertyName("ParentLinkID")]
+ public required LinkId? ParentId { get; init; }
+
+ public required bool IsShared { get; init; }
+
+ public required bool IsTrashed { get; init; }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Events/VolumeEventListResponse.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Events/VolumeEventListResponse.cs
new file mode 100644
index 00000000..7136e21a
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Events/VolumeEventListResponse.cs
@@ -0,0 +1,19 @@
+using System.Text.Json.Serialization;
+using Proton.Sdk.Api;
+using Proton.Sdk.Events;
+
+namespace Proton.Drive.Sdk.Api.Events;
+
+internal sealed class VolumeEventListResponse : ApiResponse
+{
+ [JsonPropertyName("EventID")]
+ public required DriveEventId LastEventId { get; init; }
+
+ public required IReadOnlyList Events { get; init; }
+
+ [JsonPropertyName("More")]
+ public required bool MoreEntriesExist { get; init; }
+
+ [JsonPropertyName("Refresh")]
+ public required bool RefreshRequired { get; init; }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Events/VolumeEventType.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Events/VolumeEventType.cs
new file mode 100644
index 00000000..e256f6a2
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Events/VolumeEventType.cs
@@ -0,0 +1,9 @@
+namespace Proton.Drive.Sdk.Api.Events;
+
+public enum VolumeEventType
+{
+ Delete = 0,
+ Create = 1,
+ Update = 2,
+ UpdateMetadata = 3,
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/ActiveRevisionDto.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/ActiveRevisionDto.cs
index 484e31e0..486640fd 100644
--- a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/ActiveRevisionDto.cs
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/ActiveRevisionDto.cs
@@ -16,12 +16,14 @@ internal sealed class ActiveRevisionDto
[JsonPropertyName("EncryptedSize")]
public required long StorageQuotaConsumption { get; init; }
+ public bool? ChecksumVerified { get; init; }
+
public PgpArmoredSignature? ManifestSignature { get; init; }
[JsonPropertyName("XAttr")]
public PgpArmoredMessage? ExtendedAttributes { get; init; }
- public IReadOnlyList? Thumbnails { get; init; }
+ public required IReadOnlyList Thumbnails { get; init; }
[JsonPropertyName("SignatureEmail")]
public string? SignatureEmailAddress { get; init; }
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/Block.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/Block.cs
deleted file mode 100644
index b967a57c..00000000
--- a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/Block.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using System.Text.Json.Serialization;
-using Proton.Sdk.Cryptography;
-
-namespace Proton.Drive.Sdk.Api.Files;
-
-internal sealed class Block
-{
- public required int Index { get; init; }
-
- [JsonPropertyName("URL")]
- public required string Url { get; init; }
-
- [JsonPropertyName("EncSignature")]
- public PgpArmoredMessage? EncryptedSignature { get; init; }
-
- [JsonPropertyName("SignatureEmail")]
- public string? SignatureEmailAddress { get; init; }
-}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/BlockDto.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/BlockDto.cs
new file mode 100644
index 00000000..2b2e33e2
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/BlockDto.cs
@@ -0,0 +1,13 @@
+using System.Text.Json.Serialization;
+
+namespace Proton.Drive.Sdk.Api.Files;
+
+internal sealed class BlockDto
+{
+ public required int Index { get; init; }
+
+ [JsonPropertyName("BareURL")]
+ public required string BareUrl { get; init; }
+
+ public required string Token { get; init; }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/BlockListingRevisionDto.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/BlockListingRevisionDto.cs
index 2c674b4e..e76fb66d 100644
--- a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/BlockListingRevisionDto.cs
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/BlockListingRevisionDto.cs
@@ -2,5 +2,5 @@
internal sealed class BlockListingRevisionDto : RevisionDto
{
- public required IReadOnlyList Blocks { get; init; }
+ public required IReadOnlyList Blocks { get; init; }
}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/CommonExtendedAttributes.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/CommonExtendedAttributes.cs
new file mode 100644
index 00000000..a3628e85
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/CommonExtendedAttributes.cs
@@ -0,0 +1,17 @@
+using System.Text.Json.Serialization;
+using Proton.Drive.Sdk.Serialization;
+using Proton.Sdk;
+
+namespace Proton.Drive.Sdk.Api.Files;
+
+internal sealed class CommonExtendedAttributes
+{
+ public long? Size { get; init; }
+
+ [JsonConverter(typeof(Iso8601DateTimeResultJsonConverter))]
+ public Result? ModificationTime { get; init; }
+
+ public IReadOnlyList? BlockSizes { get; init; }
+
+ public FileContentDigestsDto? Digests { get; init; }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/ExtendedAttributes.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/ExtendedAttributes.cs
new file mode 100644
index 00000000..08036fb4
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/ExtendedAttributes.cs
@@ -0,0 +1,12 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Proton.Drive.Sdk.Api.Files;
+
+internal struct ExtendedAttributes
+{
+ public CommonExtendedAttributes? Common { get; init; }
+
+ [JsonExtensionData]
+ public Dictionary? AdditionalMetadata { get; set; }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/FileContentDigestsDto.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/FileContentDigestsDto.cs
new file mode 100644
index 00000000..a26523d4
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/FileContentDigestsDto.cs
@@ -0,0 +1,11 @@
+using System.Text.Json.Serialization;
+using Proton.Sdk.Serialization;
+
+namespace Proton.Drive.Sdk.Api.Files;
+
+internal readonly struct FileContentDigestsDto
+{
+ [JsonPropertyName("SHA1")]
+ [JsonConverter(typeof(ForgivingBytesToHexJsonConverter))]
+ public ReadOnlyMemory? Sha1 { get; init; }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/FileCreationRequest.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/FileCreationRequest.cs
index 81014866..69700606 100644
--- a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/FileCreationRequest.cs
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/FileCreationRequest.cs
@@ -11,10 +11,14 @@ internal sealed class FileCreationRequest : NodeCreationRequest
public required ReadOnlyMemory ContentKeyPacket { get; init; }
- public required PgpArmoredSignature ContentKeyPacketSignature { get; init; }
+ [JsonPropertyName("ContentKeyPacketSignature")]
+ public required PgpArmoredSignature ContentKeySignature { get; init; }
[JsonPropertyName("ClientUID")]
public string? ClientUid { get; init; }
public long? IntendedUploadSize { get; init; }
+
+ [JsonPropertyName("SignatureAddress")]
+ public required string SignatureEmailAddress { get; init; }
}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/FileDto.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/FileDto.cs
index ab9142f9..e67178b5 100644
--- a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/FileDto.cs
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/FileDto.cs
@@ -3,17 +3,17 @@
namespace Proton.Drive.Sdk.Api.Files;
-internal sealed class FileDto
+internal class FileDto
{
public required string MediaType { get; init; }
[JsonPropertyName("TotalEncryptedSize")]
- public required long TotalStorageQuotaUsage { get; init; }
+ public required long TotalSizeOnStorage { get; init; }
public required ReadOnlyMemory ContentKeyPacket { get; init; }
[JsonPropertyName("ContentKeyPacketSignature")]
- public PgpArmoredSignature ContentKeySignature { get; init; }
+ public PgpArmoredSignature? ContentKeySignature { get; init; }
public ActiveRevisionDto? ActiveRevision { get; init; }
}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/FilesApiClient.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/FilesApiClient.cs
index 22331ab2..7bf8e324 100644
--- a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/FilesApiClient.cs
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/FilesApiClient.cs
@@ -1,3 +1,4 @@
+using System.Text;
using Proton.Drive.Sdk.Api.Links;
using Proton.Drive.Sdk.Serialization;
using Proton.Drive.Sdk.Volumes;
@@ -14,11 +15,27 @@ internal sealed class FilesApiClient(HttpClient httpClient) : IFilesApiClient
public async ValueTask CreateFileAsync(VolumeId volumeId, FileCreationRequest request, CancellationToken cancellationToken)
{
return await _httpClient
- .Expecting(DriveApiSerializerContext.Default.FileCreationResponse, DriveApiSerializerContext.Default.RevisionConflictResponse)
+ .Expecting(DriveApiSerializerContext.Default.FileCreationResponse, DriveApiSerializerContext.Default.RevisionErrorResponse)
.PostAsync($"v2/volumes/{volumeId}/files", request, DriveApiSerializerContext.Default.FileCreationRequest, cancellationToken)
.ConfigureAwait(false);
}
+ public async ValueTask CreateRevisionAsync(
+ VolumeId volumeId,
+ LinkId linkId,
+ RevisionCreationRequest request,
+ CancellationToken cancellationToken)
+ {
+ return await _httpClient
+ .Expecting(DriveApiSerializerContext.Default.RevisionCreationResponse, DriveApiSerializerContext.Default.RevisionErrorResponse)
+ .PostAsync(
+ $"v2/volumes/{volumeId}/files/{linkId}/revisions",
+ request,
+ DriveApiSerializerContext.Default.RevisionCreationRequest,
+ cancellationToken)
+ .ConfigureAwait(false);
+ }
+
public async ValueTask PrepareBlockUploadAsync(BlockUploadPreparationRequest request, CancellationToken cancellationToken)
{
return await _httpClient
@@ -42,30 +59,56 @@ public async ValueTask UpdateRevisionAsync(
cancellationToken).ConfigureAwait(false);
}
- public async ValueTask GetVerificationInputAsync(
+ public async ValueTask GetRevisionAsync(
VolumeId volumeId,
LinkId linkId,
RevisionId revisionId,
+ int? fromBlockIndex,
+ int? pageSize,
+ bool withoutBlockUrls,
CancellationToken cancellationToken)
{
+ var routeBuilder = new StringBuilder();
+
+ routeBuilder.Append($"v2/volumes/{volumeId}/files/{linkId}/revisions/{revisionId}?");
+
+ if (fromBlockIndex is not null)
+ {
+ routeBuilder.Append($"FromBlockIndex={fromBlockIndex}&");
+ }
+
+ if (pageSize is not null)
+ {
+ routeBuilder.Append($"PageSize={pageSize}&");
+ }
+
+ routeBuilder.Append($"NoBlockUrls={(withoutBlockUrls ? 1 : 0)}");
+
return await _httpClient
- .Expecting(DriveApiSerializerContext.Default.BlockVerificationInputResponse)
- .GetAsync($"v2/volumes/{volumeId}/links/{linkId}/revisions/{revisionId}/verification", cancellationToken).ConfigureAwait(false);
+ .Expecting(DriveApiSerializerContext.Default.RevisionResponse)
+ .GetAsync(routeBuilder.ToString(), cancellationToken).ConfigureAwait(false);
}
- public async ValueTask GetRevisionAsync(
+ public async ValueTask DeleteRevisionAsync(VolumeId volumeId, LinkId linkId, RevisionId revisionId, CancellationToken cancellationToken)
+ {
+ return await _httpClient
+ .Expecting(DriveApiSerializerContext.Default.ApiResponse)
+ .DeleteAsync($"v2/volumes/{volumeId}/files/{linkId}/revisions/{revisionId}", cancellationToken)
+ .ConfigureAwait(false);
+ }
+
+ public async ValueTask GetThumbnailBlocksAsync(
VolumeId volumeId,
- LinkId linkId,
- RevisionId revisionId,
- int fromBlockIndex,
- int pageSize,
- bool withoutBlockUrls,
+ IEnumerable thumbnailIds,
CancellationToken cancellationToken)
{
return await _httpClient
- .Expecting(DriveApiSerializerContext.Default.RevisionResponse)
- .GetAsync(
- $"v2/volumes/{volumeId}/files/{linkId}/revisions/{revisionId}?FromBlockIndex={fromBlockIndex}&PageSize={pageSize}&NoBlockUrls={(withoutBlockUrls ? 1 : 0)}",
- cancellationToken).ConfigureAwait(false);
+ .Expecting(DriveApiSerializerContext.Default.ThumbnailBlockListResponse)
+ .PostAsync(
+ $"volumes/{volumeId}/thumbnails",
+ new ThumbnailBlockListRequest { ThumbnailIds = thumbnailIds },
+ DriveApiSerializerContext.Default.ThumbnailBlockListRequest,
+ cancellationToken)
+ .ConfigureAwait(false);
}
}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/IFilesApiClient.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/IFilesApiClient.cs
index 3e39ad6a..f85f3b88 100644
--- a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/IFilesApiClient.cs
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/IFilesApiClient.cs
@@ -4,10 +4,16 @@
namespace Proton.Drive.Sdk.Api.Files;
-internal interface IFilesApiClient : IRevisionVerificationApiClient
+internal interface IFilesApiClient
{
ValueTask CreateFileAsync(VolumeId volumeId, FileCreationRequest request, CancellationToken cancellationToken);
+ ValueTask CreateRevisionAsync(
+ VolumeId volumeId,
+ LinkId linkId,
+ RevisionCreationRequest request,
+ CancellationToken cancellationToken);
+
ValueTask PrepareBlockUploadAsync(BlockUploadPreparationRequest request, CancellationToken cancellationToken);
ValueTask UpdateRevisionAsync(
@@ -17,12 +23,16 @@ ValueTask UpdateRevisionAsync(
RevisionUpdateRequest request,
CancellationToken cancellationToken);
- public ValueTask GetRevisionAsync(
+ ValueTask GetRevisionAsync(
VolumeId volumeId,
LinkId linkId,
RevisionId revisionId,
- int fromBlockIndex,
- int pageSize,
+ int? fromBlockIndex,
+ int? pageSize,
bool withoutBlockUrls,
CancellationToken cancellationToken);
+
+ ValueTask GetThumbnailBlocksAsync(VolumeId volumeId, IEnumerable thumbnailIds, CancellationToken cancellationToken);
+
+ ValueTask DeleteRevisionAsync(VolumeId volumeId, LinkId linkId, RevisionId revisionId, CancellationToken cancellationToken);
}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/PhotosAttributesDto.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/PhotosAttributesDto.cs
new file mode 100644
index 00000000..37463368
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/PhotosAttributesDto.cs
@@ -0,0 +1,21 @@
+using System.Text.Json.Serialization;
+using Proton.Drive.Sdk.Api.Links;
+using Proton.Drive.Sdk.Nodes;
+using Proton.Sdk.Serialization;
+
+namespace Proton.Drive.Sdk.Api.Files;
+
+internal sealed class PhotosAttributesDto
+{
+ [JsonConverter(typeof(EpochSecondsJsonConverter))]
+ public required DateTime CaptureTime { get; init; }
+
+ [JsonPropertyName("ContentHash")]
+ [JsonConverter(typeof(ForgivingBytesToHexJsonConverter))]
+ public required ReadOnlyMemory ContentHashDigest { get; init; }
+
+ [JsonPropertyName("MainPhotoLinkID")]
+ public LinkId? MainPhotoLinkId { get; init; }
+
+ public IReadOnlySet? Tags { get; init; }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/RevisionConflict.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/RevisionConflict.cs
index 3d506723..0d9ce361 100644
--- a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/RevisionConflict.cs
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/RevisionConflict.cs
@@ -1,5 +1,8 @@
-using System.Text.Json.Serialization;
+using System.Text.Json;
+using System.Text.Json.Serialization;
using Proton.Drive.Sdk.Api.Links;
+using Proton.Drive.Sdk.Serialization;
+using Proton.Sdk.Api;
namespace Proton.Drive.Sdk.Api.Files;
@@ -16,4 +19,11 @@ internal sealed class RevisionConflict
[JsonPropertyName("ConflictDraftClientUID")]
public string? DraftClientUid { get; init; }
+
+ public static RevisionConflict? FromErrorResponse(RevisionErrorResponse? errorResponse)
+ {
+ return errorResponse?.Code is ResponseCode.AlreadyExists
+ ? errorResponse.Details?.Deserialize(DriveApiSerializerContext.Default.RevisionConflict)
+ : null;
+ }
}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/RevisionConflictResponse.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/RevisionConflictResponse.cs
deleted file mode 100644
index e7e2d012..00000000
--- a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/RevisionConflictResponse.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using System.Text.Json.Serialization;
-using Proton.Sdk.Api;
-
-namespace Proton.Drive.Sdk.Api.Files;
-
-internal sealed class RevisionConflictResponse : ApiResponse
-{
- [JsonPropertyName("Details")]
- public required RevisionConflict Conflict { get; init; }
-}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/RevisionCreationIdentity.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/RevisionCreationIdentity.cs
new file mode 100644
index 00000000..9825b8e4
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/RevisionCreationIdentity.cs
@@ -0,0 +1,9 @@
+using System.Text.Json.Serialization;
+
+namespace Proton.Drive.Sdk.Api.Files;
+
+internal readonly struct RevisionCreationIdentity
+{
+ [JsonPropertyName("ID")]
+ public required RevisionId RevisionId { get; init; }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/RevisionCreationRequest.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/RevisionCreationRequest.cs
new file mode 100644
index 00000000..41b4bf8c
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/RevisionCreationRequest.cs
@@ -0,0 +1,14 @@
+using System.Text.Json.Serialization;
+
+namespace Proton.Drive.Sdk.Api.Files;
+
+internal struct RevisionCreationRequest
+{
+ [JsonPropertyName("CurrentRevisionID")]
+ public RevisionId? CurrentRevisionId { get; init; }
+
+ [JsonPropertyName("ClientUID")]
+ public string? ClientId { get; init; }
+
+ public long? IntendedUploadSize { get; init; }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/RevisionCreationResponse.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/RevisionCreationResponse.cs
new file mode 100644
index 00000000..62604eb5
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/RevisionCreationResponse.cs
@@ -0,0 +1,10 @@
+using System.Text.Json.Serialization;
+using Proton.Sdk.Api;
+
+namespace Proton.Drive.Sdk.Api.Files;
+
+internal sealed class RevisionCreationResponse : ApiResponse
+{
+ [JsonPropertyName("Revision")]
+ public required RevisionCreationIdentity Identity { get; init; }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/RevisionDto.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/RevisionDto.cs
index daa50d9b..257c6161 100644
--- a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/RevisionDto.cs
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/RevisionDto.cs
@@ -8,7 +8,7 @@ namespace Proton.Drive.Sdk.Api.Files;
internal class RevisionDto
{
[JsonPropertyName("ID")]
- public required string Id { get; init; }
+ public required RevisionId Id { get; init; }
[JsonPropertyName("ClientUID")]
public string? ClientId { get; init; }
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/RevisionErrorResponse.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/RevisionErrorResponse.cs
new file mode 100644
index 00000000..a97bb7b3
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/RevisionErrorResponse.cs
@@ -0,0 +1,9 @@
+using System.Text.Json;
+using Proton.Sdk.Api;
+
+namespace Proton.Drive.Sdk.Api.Files;
+
+internal sealed class RevisionErrorResponse : ApiResponse
+{
+ public JsonElement? Details { get; init; }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/RevisionUpdateRequest.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/RevisionUpdateRequest.cs
index 92869748..9191cb68 100644
--- a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/RevisionUpdateRequest.cs
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/RevisionUpdateRequest.cs
@@ -10,6 +10,11 @@ internal sealed class RevisionUpdateRequest
[JsonPropertyName("SignatureAddress")]
public required string SignatureEmailAddress { get; init; }
+ public required bool ChecksumVerified { get; init; }
+
[JsonPropertyName("XAttr")]
public PgpArmoredMessage? ExtendedAttributes { get; init; }
+
+ [JsonPropertyName("Photo")]
+ public PhotosAttributesDto? PhotosAttributes { get; set; }
}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/Thumbnail.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/Thumbnail.cs
deleted file mode 100644
index e53fe61e..00000000
--- a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/Thumbnail.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using System.Text.Json.Serialization;
-
-namespace Proton.Drive.Sdk.Api.Files;
-
-internal sealed class Thumbnail
-{
- [JsonPropertyName("ThumbnailID")]
- public required string Id { get; init; }
-
- public required int Type { get; init; }
-
- [JsonPropertyName("BaseURL")]
- public required ReadOnlyMemory HashDigest { get; init; }
-
- public required int Size { get; init; }
-}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/ThumbnailBlock.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/ThumbnailBlock.cs
index fa04393d..7216959e 100644
--- a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/ThumbnailBlock.cs
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/ThumbnailBlock.cs
@@ -5,7 +5,7 @@ namespace Proton.Drive.Sdk.Api.Files;
internal sealed class ThumbnailBlock
{
[JsonPropertyName("ThumbnailID")]
- public required string Id { get; init; }
+ public required string ThumbnailId { get; init; }
[JsonPropertyName("BareURL")]
public required string BareUrl { get; init; }
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/ThumbnailBlockError.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/ThumbnailBlockError.cs
new file mode 100644
index 00000000..11ee8ab1
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/ThumbnailBlockError.cs
@@ -0,0 +1,15 @@
+using System.Text.Json.Serialization;
+
+namespace Proton.Drive.Sdk.Api.Files;
+
+internal sealed class ThumbnailBlockError
+{
+ [JsonPropertyName("ThumbnailID")]
+ public required string ThumbnailId { get; init; }
+
+ [JsonPropertyName("Error")]
+ public required string Error { get; init; }
+
+ [JsonPropertyName("Code")]
+ public required int Code { get; init; }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/ThumbnailBlockListRequest.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/ThumbnailBlockListRequest.cs
new file mode 100644
index 00000000..ad5ef850
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/ThumbnailBlockListRequest.cs
@@ -0,0 +1,9 @@
+using System.Text.Json.Serialization;
+
+namespace Proton.Drive.Sdk.Api.Files;
+
+internal struct ThumbnailBlockListRequest
+{
+ [JsonPropertyName("ThumbnailIDs")]
+ public required IEnumerable ThumbnailIds { get; init; }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/ThumbnailBlockListResponse.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/ThumbnailBlockListResponse.cs
new file mode 100644
index 00000000..4c0d5db7
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/ThumbnailBlockListResponse.cs
@@ -0,0 +1,13 @@
+using System.Text.Json.Serialization;
+using Proton.Sdk.Api;
+
+namespace Proton.Drive.Sdk.Api.Files;
+
+internal sealed class ThumbnailBlockListResponse : ApiResponse
+{
+ [JsonPropertyName("Thumbnails")]
+ public required IReadOnlyList Blocks { get; init; }
+
+ [JsonPropertyName("Errors")]
+ public required IReadOnlyList Errors { get; init; }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/ThumbnailDto.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/ThumbnailDto.cs
index 7d68a4d1..f3740959 100644
--- a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/ThumbnailDto.cs
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/ThumbnailDto.cs
@@ -5,7 +5,7 @@ namespace Proton.Drive.Sdk.Api.Files;
internal sealed class ThumbnailDto
{
[JsonPropertyName("ThumbnailID")]
- public string? Id { get; init; }
+ public required string Id { get; init; }
public required ThumbnailType Type { get; init; }
@@ -13,5 +13,5 @@ internal sealed class ThumbnailDto
public required ReadOnlyMemory HashDigest { get; init; }
[JsonPropertyName("Size")]
- public required int StorageQuotaUsage { get; init; }
+ public required int SizeOnCloudStorage { get; init; }
}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/ThumbnailDtoV2.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/ThumbnailDtoV2.cs
index eee9cc3a..dc54aae1 100644
--- a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/ThumbnailDtoV2.cs
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/ThumbnailDtoV2.cs
@@ -5,7 +5,7 @@ namespace Proton.Drive.Sdk.Api.Files;
internal sealed class ThumbnailDtoV2
{
[JsonPropertyName("ThumbnailID")]
- public string? Id { get; init; }
+ public required string Id { get; init; }
public required ThumbnailType Type { get; init; }
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Files/TrashApiClient.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/TrashApiClient.cs
new file mode 100644
index 00000000..ae6b856f
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Files/TrashApiClient.cs
@@ -0,0 +1,69 @@
+using Proton.Drive.Sdk.Api.Links;
+using Proton.Drive.Sdk.Api.Volumes;
+using Proton.Drive.Sdk.Serialization;
+using Proton.Drive.Sdk.Volumes;
+using Proton.Sdk.Api;
+using Proton.Sdk.Http;
+using Proton.Sdk.Serialization;
+
+namespace Proton.Drive.Sdk.Api.Files;
+
+internal sealed class TrashApiClient(HttpClient httpClient) : ITrashApiClient
+{
+ private readonly HttpClient _httpClient = httpClient;
+
+ public async ValueTask GetTrashAsync(VolumeId volumeId, int pageSize, int page, CancellationToken cancellationToken)
+ {
+ return await _httpClient
+ .Expecting(DriveApiSerializerContext.Default.VolumeTrashResponse)
+ .GetAsync($"volumes/{volumeId}/trash?pageSize={pageSize}&page={page}", cancellationToken).ConfigureAwait(false);
+ }
+
+ public async ValueTask> TrashMultipleAsync(
+ VolumeId volumeId,
+ MultipleLinksNullaryRequest request,
+ CancellationToken cancellationToken)
+ {
+ return await _httpClient
+ .Expecting(DriveApiSerializerContext.Default.AggregateApiResponseLinkIdResponsePair)
+ .PostAsync($"v2/volumes/{volumeId}/trash_multiple", request, DriveApiSerializerContext.Default.MultipleLinksNullaryRequest, cancellationToken)
+ .ConfigureAwait(false);
+ }
+
+ public async ValueTask> DeleteMultipleAsync(
+ VolumeId volumeId,
+ MultipleLinksNullaryRequest request,
+ CancellationToken cancellationToken)
+ {
+ return await _httpClient
+ .Expecting(DriveApiSerializerContext.Default.AggregateApiResponseLinkIdResponsePair)
+ .PostAsync(
+ $"v2/volumes/{volumeId}/trash/delete_multiple",
+ request,
+ DriveApiSerializerContext.Default.MultipleLinksNullaryRequest,
+ cancellationToken)
+ .ConfigureAwait(false);
+ }
+
+ public async ValueTask> RestoreMultipleAsync(
+ VolumeId volumeId,
+ MultipleLinksNullaryRequest request,
+ CancellationToken cancellationToken)
+ {
+ return await _httpClient
+ .Expecting(DriveApiSerializerContext.Default.AggregateApiResponseLinkIdResponsePair)
+ .PutAsync(
+ $"v2/volumes/{volumeId}/trash/restore_multiple",
+ request,
+ DriveApiSerializerContext.Default.MultipleLinksNullaryRequest,
+ cancellationToken)
+ .ConfigureAwait(false);
+ }
+
+ public async ValueTask EmptyAsync(VolumeId volumeId, CancellationToken cancellationToken)
+ {
+ return await _httpClient
+ .Expecting(ProtonApiSerializerContext.Default.ApiResponse)
+ .DeleteAsync($"volumes/{volumeId}/trash", cancellationToken).ConfigureAwait(false);
+ }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Folders/FolderChildrenResponse.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Folders/FolderChildrenResponse.cs
index d13d182a..be7becd0 100644
--- a/cs/sdk/src/Proton.Drive.Sdk/Api/Folders/FolderChildrenResponse.cs
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Folders/FolderChildrenResponse.cs
@@ -9,6 +9,7 @@ internal sealed class FolderChildrenResponse : ApiResponse
[JsonPropertyName("LinkIDs")]
public required IReadOnlyList LinkIds { get; init; }
+ [JsonPropertyName("AnchorID")]
public LinkId? AnchorId { get; init; }
[JsonPropertyName("More")]
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Folders/FolderCreationRequest.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Folders/FolderCreationRequest.cs
index 49b1bb5d..64dff306 100644
--- a/cs/sdk/src/Proton.Drive.Sdk/Api/Folders/FolderCreationRequest.cs
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Folders/FolderCreationRequest.cs
@@ -8,4 +8,10 @@ internal sealed class FolderCreationRequest : NodeCreationRequest
{
[JsonPropertyName("NodeHashKey")]
public required PgpArmoredMessage HashKey { get; init; }
+
+ [JsonPropertyName("SignatureEmail")]
+ public required string SignatureEmailAddress { get; init; }
+
+ [JsonPropertyName("XAttr")]
+ public PgpArmoredMessage? ExtendedAttributes { get; init; }
}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/IDriveApiClients.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/IDriveApiClients.cs
index ea810c85..f1892029 100644
--- a/cs/sdk/src/Proton.Drive.Sdk/Api/IDriveApiClients.cs
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/IDriveApiClients.cs
@@ -1,6 +1,7 @@
using Proton.Drive.Sdk.Api.Files;
using Proton.Drive.Sdk.Api.Folders;
using Proton.Drive.Sdk.Api.Links;
+using Proton.Drive.Sdk.Api.Photos;
using Proton.Drive.Sdk.Api.Shares;
using Proton.Drive.Sdk.Api.Storage;
using Proton.Drive.Sdk.Api.Volumes;
@@ -15,4 +16,6 @@ internal interface IDriveApiClients
IFoldersApiClient Folders { get; }
IFilesApiClient Files { get; }
IStorageApiClient Storage { get; }
+ ITrashApiClient Trash { get; }
+ IPhotosApiClient Photos { get; }
}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Links/ILinksApiClient.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Links/ILinksApiClient.cs
index 933174b0..1f96c569 100644
--- a/cs/sdk/src/Proton.Drive.Sdk/Api/Links/ILinksApiClient.cs
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Links/ILinksApiClient.cs
@@ -15,18 +15,14 @@ internal interface ILinksApiClient
ValueTask RenameAsync(VolumeId volumeId, LinkId linkId, RenameLinkRequest request, CancellationToken cancellationToken);
- ValueTask> TrashMultipleAsync(
- VolumeId volumeId,
- MultipleLinksNullaryRequest request,
- CancellationToken cancellationToken);
-
ValueTask> DeleteMultipleAsync(
VolumeId volumeId,
- MultipleLinksNullaryRequest request,
+ IEnumerable linkIds,
CancellationToken cancellationToken);
- ValueTask> RestoreMultipleAsync(
+ ValueTask GetAvailableNames(
VolumeId volumeId,
- MultipleLinksNullaryRequest request,
+ LinkId folderId,
+ NodeNameAvailabilityRequest request,
CancellationToken cancellationToken);
}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Links/ITrashApiClient.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Links/ITrashApiClient.cs
new file mode 100644
index 00000000..1071b617
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Links/ITrashApiClient.cs
@@ -0,0 +1,27 @@
+using Proton.Drive.Sdk.Api.Volumes;
+using Proton.Drive.Sdk.Volumes;
+using Proton.Sdk.Api;
+
+namespace Proton.Drive.Sdk.Api.Links;
+
+internal interface ITrashApiClient
+{
+ ValueTask GetTrashAsync(VolumeId volumeId, int pageSize, int page, CancellationToken cancellationToken);
+
+ ValueTask> TrashMultipleAsync(
+ VolumeId volumeId,
+ MultipleLinksNullaryRequest request,
+ CancellationToken cancellationToken);
+
+ ValueTask> RestoreMultipleAsync(
+ VolumeId volumeId,
+ MultipleLinksNullaryRequest request,
+ CancellationToken cancellationToken);
+
+ ValueTask> DeleteMultipleAsync(
+ VolumeId volumeId,
+ MultipleLinksNullaryRequest request,
+ CancellationToken cancellationToken);
+
+ ValueTask EmptyAsync(VolumeId volumeId, CancellationToken cancellationToken);
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Links/LinkDetailsDto.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Links/LinkDetailsDto.cs
index 433d8530..d639aaaf 100644
--- a/cs/sdk/src/Proton.Drive.Sdk/Api/Links/LinkDetailsDto.cs
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Links/LinkDetailsDto.cs
@@ -1,5 +1,6 @@
using Proton.Drive.Sdk.Api.Files;
using Proton.Drive.Sdk.Api.Folders;
+using Proton.Drive.Sdk.Api.Photos;
namespace Proton.Drive.Sdk.Api.Links;
@@ -8,13 +9,26 @@ internal sealed class LinkDetailsDto
public required LinkDto Link { get; init; }
public FolderDto? Folder { get; init; }
public FileDto? File { get; init; }
+ public PhotoDto? Photo { get; init; }
+ public FolderDto? Album { get; init; }
+ public LinkSharingDto? Sharing { get; init; }
public ShareMembershipSummaryDto? Membership { get; init; }
- public void Deconstruct(out LinkDto link, out FolderDto? folder, out FileDto? file, out ShareMembershipSummaryDto? membership)
+ public void Deconstruct(
+ out LinkDto link,
+ out FolderDto? folder,
+ out FileDto? file,
+ out PhotoDto? photo,
+ out FolderDto? album,
+ out LinkSharingDto? sharing,
+ out ShareMembershipSummaryDto? membership)
{
link = Link;
folder = Folder;
file = File;
+ photo = Photo;
+ album = Album;
+ sharing = Sharing;
membership = Membership;
}
}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Links/LinkDto.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Links/LinkDto.cs
index a4903f30..bd78939b 100644
--- a/cs/sdk/src/Proton.Drive.Sdk/Api/Links/LinkDto.cs
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Links/LinkDto.cs
@@ -1,4 +1,4 @@
-using System.Text.Json.Serialization;
+using System.Text.Json.Serialization;
using Proton.Sdk.Cryptography;
using Proton.Sdk.Serialization;
@@ -35,7 +35,7 @@ internal sealed class LinkDto
public required ReadOnlyMemory NameHashDigest { get; init; }
[JsonPropertyName("NodeKey")]
- public required PgpArmoredPrivateKey Key { get; init; }
+ public required PgpArmoredSecretKey Key { get; init; }
[JsonPropertyName("NodePassphrase")]
public required PgpArmoredMessage Passphrase { get; init; }
@@ -48,4 +48,7 @@ internal sealed class LinkDto
[JsonPropertyName("NameSignatureEmail")]
public string? NameSignatureEmailAddress { get; init; }
+
+ [JsonPropertyName("OwnedBy")]
+ public OwnedByDto? OwnedBy { get; init; }
}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Links/LinkSharingDto.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Links/LinkSharingDto.cs
new file mode 100644
index 00000000..5bb34da7
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Links/LinkSharingDto.cs
@@ -0,0 +1,10 @@
+using System.Text.Json.Serialization;
+using Proton.Drive.Sdk.Api.Shares;
+
+namespace Proton.Drive.Sdk.Api.Links;
+
+internal sealed class LinkSharingDto
+{
+ [JsonPropertyName("ShareID")]
+ public required ShareId ShareId { get; init; }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Links/LinkType.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Links/LinkType.cs
index 83d8270f..76c0a2db 100644
--- a/cs/sdk/src/Proton.Drive.Sdk/Api/Links/LinkType.cs
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Links/LinkType.cs
@@ -4,4 +4,5 @@ internal enum LinkType
{
Folder = 1,
File = 2,
+ Album = 3,
}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Links/LinksApiClient.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Links/LinksApiClient.cs
index aaa2c2e0..406e8363 100644
--- a/cs/sdk/src/Proton.Drive.Sdk/Api/Links/LinksApiClient.cs
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Links/LinksApiClient.cs
@@ -18,6 +18,7 @@ public async ValueTask GetDetailsAsync(VolumeId volumeId, I
.ConfigureAwait(false);
}
+ // FIXME use recursive lookup instead, remove this
public async ValueTask GetContextShareAsync(VolumeId volumeId, LinkId linkId, CancellationToken cancellationToken)
{
return await _httpClient
@@ -49,43 +50,33 @@ public async ValueTask RenameAsync(VolumeId volumeId, LinkId linkId
.ConfigureAwait(false);
}
- public async ValueTask> TrashMultipleAsync(
- VolumeId volumeId,
- MultipleLinksNullaryRequest request,
- CancellationToken cancellationToken)
- {
- return await _httpClient
- .Expecting(DriveApiSerializerContext.Default.AggregateApiResponseLinkIdResponsePair)
- .PostAsync($"v2/volumes/{volumeId}/trash_multiple", request, DriveApiSerializerContext.Default.MultipleLinksNullaryRequest, cancellationToken)
- .ConfigureAwait(false);
- }
-
public async ValueTask> DeleteMultipleAsync(
VolumeId volumeId,
- MultipleLinksNullaryRequest request,
+ IEnumerable linkIds,
CancellationToken cancellationToken)
{
return await _httpClient
.Expecting(DriveApiSerializerContext.Default.AggregateApiResponseLinkIdResponsePair)
.PostAsync(
- $"v2/volumes/{volumeId}/trash/delete_multiple",
- request,
+ $"v2/volumes/{volumeId}/delete_multiple",
+ new MultipleLinksNullaryRequest { LinkIds = linkIds },
DriveApiSerializerContext.Default.MultipleLinksNullaryRequest,
cancellationToken)
.ConfigureAwait(false);
}
- public async ValueTask> RestoreMultipleAsync(
+ public async ValueTask GetAvailableNames(
VolumeId volumeId,
- MultipleLinksNullaryRequest request,
+ LinkId folderId,
+ NodeNameAvailabilityRequest request,
CancellationToken cancellationToken)
{
return await _httpClient
- .Expecting(DriveApiSerializerContext.Default.AggregateApiResponseLinkIdResponsePair)
- .PutAsync(
- $"v2/volumes/{volumeId}/trash/restore_multiple",
+ .Expecting(DriveApiSerializerContext.Default.NodeNameAvailabilityResponse)
+ .PostAsync(
+ $"v2/volumes/{volumeId}/links/{folderId}/checkAvailableHashes",
request,
- DriveApiSerializerContext.Default.MultipleLinksNullaryRequest,
+ DriveApiSerializerContext.Default.NodeNameAvailabilityRequest,
cancellationToken)
.ConfigureAwait(false);
}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Links/NameHashDigestUnavailabilityDto.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Links/NameHashDigestUnavailabilityDto.cs
new file mode 100644
index 00000000..d29f3c28
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Links/NameHashDigestUnavailabilityDto.cs
@@ -0,0 +1,19 @@
+using System.Text.Json.Serialization;
+using Proton.Drive.Sdk.Api.Files;
+
+namespace Proton.Drive.Sdk.Api.Links;
+
+internal sealed class NameHashDigestUnavailabilityDto
+{
+ [JsonPropertyName("Hash")]
+ public required string NameHashDigest { get; init; }
+
+ [JsonPropertyName("RevisionID")]
+ public required RevisionId RevisionId { get; init; }
+
+ [JsonPropertyName("LinkID")]
+ public required LinkId LinkId { get; set; }
+
+ [JsonPropertyName("ClientUID")]
+ public required string ClientUid { get; set; }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Links/NodeCreationRequest.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Links/NodeCreationRequest.cs
index 2d7eb3ac..08e53f4c 100644
--- a/cs/sdk/src/Proton.Drive.Sdk/Api/Links/NodeCreationRequest.cs
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Links/NodeCreationRequest.cs
@@ -21,9 +21,6 @@ internal abstract class NodeCreationRequest
[JsonPropertyName("NodePassphraseSignature")]
public required PgpArmoredSignature PassphraseSignature { get; init; }
- [JsonPropertyName("SignatureAddress")]
- public required string SignatureEmailAddress { get; init; }
-
[JsonPropertyName("NodeKey")]
- public required PgpArmoredPrivateKey Key { get; init; }
+ public required PgpArmoredSecretKey Key { get; init; }
}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Links/NodeNameAvailabilityRequest.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Links/NodeNameAvailabilityRequest.cs
new file mode 100644
index 00000000..e170a8ce
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Links/NodeNameAvailabilityRequest.cs
@@ -0,0 +1,12 @@
+using System.Text.Json.Serialization;
+
+namespace Proton.Drive.Sdk.Api.Links;
+
+internal sealed class NodeNameAvailabilityRequest
+{
+ [JsonPropertyName("Hashes")]
+ public required IReadOnlyCollection NameHashDigests { get; init; }
+
+ [JsonPropertyName("ClientUID")]
+ public required IEnumerable ClientUid { get; init; }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Links/NodeNameAvailabilityResponse.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Links/NodeNameAvailabilityResponse.cs
new file mode 100644
index 00000000..a4fa4b3c
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Links/NodeNameAvailabilityResponse.cs
@@ -0,0 +1,13 @@
+using System.Text.Json.Serialization;
+using Proton.Sdk.Api;
+
+namespace Proton.Drive.Sdk.Api.Links;
+
+internal sealed class NodeNameAvailabilityResponse : ApiResponse
+{
+ [JsonPropertyName("AvailableHashes")]
+ public required IReadOnlyList AvailableNameHashDigests { get; init; }
+
+ [JsonPropertyName("PendingHashes")]
+ public required IReadOnlyList UnavailableNameHashDigests { get; init; }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Links/OwnedByDto.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Links/OwnedByDto.cs
new file mode 100644
index 00000000..0d5a9330
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Links/OwnedByDto.cs
@@ -0,0 +1,12 @@
+using System.Text.Json.Serialization;
+
+namespace Proton.Drive.Sdk.Api.Links;
+
+internal sealed class OwnedByDto
+{
+ [JsonPropertyName("Email")]
+ public string? Email { get; init; }
+
+ [JsonPropertyName("Organization")]
+ public string? Organization { get; init; }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Photos/IPhotosApiClient.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Photos/IPhotosApiClient.cs
new file mode 100644
index 00000000..abaa925e
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Photos/IPhotosApiClient.cs
@@ -0,0 +1,17 @@
+using Proton.Drive.Sdk.Api.Links;
+using Proton.Drive.Sdk.Api.Shares;
+using Proton.Drive.Sdk.Api.Volumes;
+using Proton.Drive.Sdk.Volumes;
+
+namespace Proton.Drive.Sdk.Api.Photos;
+
+internal interface IPhotosApiClient
+{
+ ValueTask CreateVolumeAsync(PhotosVolumeCreationRequest request, CancellationToken cancellationToken);
+
+ ValueTask GetRootShareAsync(CancellationToken cancellationToken);
+
+ ValueTask GetTimelinePhotosAsync(TimelinePhotoListRequest request, CancellationToken cancellationToken);
+
+ ValueTask GetDetailsAsync(VolumeId volumeId, IEnumerable linkIds, CancellationToken cancellationToken);
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Photos/PhotoAlbumInclusionDto.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Photos/PhotoAlbumInclusionDto.cs
new file mode 100644
index 00000000..ff3fe974
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Photos/PhotoAlbumInclusionDto.cs
@@ -0,0 +1,21 @@
+using System.Text.Json.Serialization;
+using Proton.Drive.Sdk.Api.Links;
+using Proton.Sdk.Serialization;
+
+namespace Proton.Drive.Sdk.Api.Photos;
+
+internal sealed class PhotoAlbumInclusionDto
+{
+ [JsonPropertyName("AlbumLinkID")]
+ public required LinkId Id { get; init; }
+
+ [JsonPropertyName("Hash")]
+ public required string NameHash { get; init; }
+
+ [JsonConverter(typeof(ForgivingBytesToHexJsonConverter))]
+ public required ReadOnlyMemory ContentHash { get; init; }
+
+ [JsonConverter(typeof(EpochSecondsJsonConverter))]
+ [JsonPropertyName("AddedTime")]
+ public required DateTime CreationTime { get; init; }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Photos/PhotoDto.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Photos/PhotoDto.cs
new file mode 100644
index 00000000..3c3c9871
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Photos/PhotoDto.cs
@@ -0,0 +1,33 @@
+using System.Text.Json.Serialization;
+using Proton.Drive.Sdk.Api.Files;
+using Proton.Drive.Sdk.Api.Links;
+using Proton.Drive.Sdk.Nodes;
+using Proton.Sdk.Serialization;
+
+namespace Proton.Drive.Sdk.Api.Photos;
+
+internal sealed class PhotoDto : FileDto
+{
+ [JsonPropertyName("LinkID")]
+ public LinkId? Id { get; init; }
+
+ [JsonConverter(typeof(EpochSecondsJsonConverter))]
+ public required DateTime CaptureTime { get; init; }
+
+ [JsonConverter(typeof(ForgivingBytesToHexJsonConverter))]
+ public ReadOnlyMemory? ContentHash { get; init; }
+
+ [JsonPropertyName("Hash")]
+ public string? NameHash { get; init; }
+
+ [JsonPropertyName("MainPhotoLinkID")]
+ public string? MainPhotoLinkId { get; init; }
+
+ [JsonPropertyName("RelatedPhotosLinkIDs")]
+ public required IReadOnlyList RelatedPhotosLinkIds { get; init; } = [];
+
+ public required IReadOnlyList Tags { get; init; } = [];
+
+ [JsonPropertyName("Albums")]
+ public required IReadOnlyList AlbumInclusions { get; init; } = [];
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Photos/PhotosApiClient.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Photos/PhotosApiClient.cs
new file mode 100644
index 00000000..b6ec5e66
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Photos/PhotosApiClient.cs
@@ -0,0 +1,48 @@
+using Proton.Drive.Sdk.Api.Links;
+using Proton.Drive.Sdk.Api.Shares;
+using Proton.Drive.Sdk.Api.Volumes;
+using Proton.Drive.Sdk.Serialization;
+using Proton.Drive.Sdk.Volumes;
+using Proton.Sdk.Http;
+
+namespace Proton.Drive.Sdk.Api.Photos;
+
+internal sealed class PhotosApiClient(HttpClient httpClient) : IPhotosApiClient
+{
+ private readonly HttpClient _httpClient = httpClient;
+
+ public async ValueTask CreateVolumeAsync(PhotosVolumeCreationRequest request, CancellationToken cancellationToken)
+ {
+ return await _httpClient
+ .Expecting(DriveApiSerializerContext.Default.VolumeCreationResponse)
+ .PostAsync("photos/volumes", request, PhotosApiSerializerContext.Default.PhotosVolumeCreationRequest, cancellationToken).ConfigureAwait(false);
+ }
+
+ public async ValueTask GetRootShareAsync(CancellationToken cancellationToken)
+ {
+ return await _httpClient
+ .Expecting(DriveApiSerializerContext.Default.ShareResponseV2)
+ .GetAsync("v2/shares/photos", cancellationToken).ConfigureAwait(false);
+ }
+
+ public async ValueTask GetTimelinePhotosAsync(TimelinePhotoListRequest request, CancellationToken cancellationToken)
+ {
+ var query = request.PreviousPageLastLinkId is not null ? $"?PreviousPageLastLinkID={request.PreviousPageLastLinkId}" : string.Empty;
+
+ return await _httpClient
+ .Expecting(PhotosApiSerializerContext.Default.TimelinePhotoListResponse)
+ .GetAsync($"volumes/{request.VolumeId}/photos{query}", cancellationToken).ConfigureAwait(false);
+ }
+
+ public async ValueTask GetDetailsAsync(VolumeId volumeId, IEnumerable linkIds, CancellationToken cancellationToken)
+ {
+ return await _httpClient
+ .Expecting(DriveApiSerializerContext.Default.LinkDetailsResponse)
+ .PostAsync(
+ $"photos/volumes/{volumeId}/links",
+ new LinkDetailsRequest(linkIds),
+ DriveApiSerializerContext.Default.LinkDetailsRequest,
+ cancellationToken)
+ .ConfigureAwait(false);
+ }
+}
diff --git a/cs/sdk/src/Proton.Drive.Sdk/Api/Photos/PhotosLinksApiClient.cs b/cs/sdk/src/Proton.Drive.Sdk/Api/Photos/PhotosLinksApiClient.cs
new file mode 100644
index 00000000..e64db819
--- /dev/null
+++ b/cs/sdk/src/Proton.Drive.Sdk/Api/Photos/PhotosLinksApiClient.cs
@@ -0,0 +1,63 @@
+using Proton.Drive.Sdk.Api.Links;
+using Proton.Drive.Sdk.Serialization;
+using Proton.Drive.Sdk.Volumes;
+using Proton.Sdk.Api;
+using Proton.Sdk.Http;
+
+namespace Proton.Drive.Sdk.Api.Photos;
+
+internal sealed class PhotosLinksApiClient(HttpClient httpClient) : ILinksApiClient
+{
+ private readonly HttpClient _httpClient = httpClient;
+
+ private readonly ILinksApiClient _driveImplementation = new LinksApiClient(httpClient);
+
+ public async ValueTask GetDetailsAsync(VolumeId volumeId, IEnumerable linkIds, CancellationToken cancellationToken)
+ {
+ return await _httpClient
+ .Expecting(DriveApiSerializerContext.Default.LinkDetailsResponse)
+ .PostAsync(
+ $"photos/volumes/{volumeId}/links",
+ new LinkDetailsRequest(linkIds),
+ DriveApiSerializerContext.Default.LinkDetailsRequest,
+ cancellationToken)
+ .ConfigureAwait(false);
+ }
+
+ public ValueTask GetContextShareAsync(VolumeId volumeId, LinkId linkId, CancellationToken cancellationToken)
+ {
+ return _driveImplementation.GetContextShareAsync(volumeId, linkId, cancellationToken);
+ }
+
+ public ValueTask MoveAsync(VolumeId volumeId, LinkId linkId, MoveSingleLinkRequest request, CancellationToken cancellationToken)
+ {
+ return _driveImplementation.MoveAsync(volumeId, linkId, request, cancellationToken);
+ }
+
+ public ValueTask