Compare commits

...
Sign in to create a new pull request.

61 commits

Author SHA1 Message Date
c88eea1f82 [#58] Observable client cut
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2025-04-11 13:12:31 +00:00
5f451c8818 [#60] Wallet tools in SDK
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2025-04-11 16:07:20 +03:00
42f4507638 [#55] Fix Replica mapper
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2025-04-11 11:36:39 +00:00
b390778201 [#57] ci: Disable automatic publishing of unsigned nugets
We're switching to non-automatic process for publishing signed nugets,
unsigned workflow will still be available as an escape hatch but it won't
ever be triggered automatically.

Signed-off-by: Vitaliy Potyarkin <v.potyarkin@yadro.com>
2025-04-11 10:33:19 +03:00
30af614558 [#57] Add helpers for signing Nuget packages
Discussion: OBJECT-16744
Signed-off-by: Vitaliy Potyarkin <v.potyarkin@yadro.com>
2025-04-10 18:40:13 +03:00
45e73a6f8e [#43] Client: Set nuget version
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2025-03-31 13:51:45 +03:00
87fe8db674 [#43] Client: Memory optimization
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2025-03-31 11:40:04 +03:00
5e86f53b0e [#41] Client: Add attributes for nuget
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2025-03-13 14:10:07 +03:00
9eb742da77 [#50] ci: Publish NuGet packages at git.frostfs.info
Signed-off-by: Vitaliy Potyarkin <v.potyarkin@yadro.com>
2025-03-13 14:10:07 +03:00
98cfd82313 [#40] Client: Add memory optimization for hash: update version
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2025-03-12 10:44:39 +03:00
f93e33b49b [#40] Client: Add memory optimization for hash
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2025-03-12 10:37:12 +03:00
6ae96c1d77 [#41] Client: Remove ranges
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2025-03-12 00:11:50 +03:00
809bd90352 [#40] Client: Add memory optimization for hash
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2025-03-11 22:56:28 +03:00
32a7e64538 [#39] Client: add memory usage optimizations
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2025-03-07 15:29:45 +03:00
d6fe034453 [#37] Client: Add AssemblyInfo files
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2025-03-04 20:12:42 +03:00
9364d60b96 [#36] Client: Remove .net Range implementation
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2025-03-04 19:45:40 +03:00
6988fcedae [#35] Client: rollback to PutSingleObject for client cut upload
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2025-03-03 16:18:44 +03:00
8835b23ed3 [#34] Client: Add rules deserialization
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2025-02-28 10:36:12 +03:00
bd8eb7cc60 [#33] Client: Add extended life tests
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2025-02-26 12:35:27 +03:00
2e56c13946 [#31] Client: fix for session
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2025-02-18 09:56:39 +03:00
195854a45b [#30] Client: Add object model for Rules
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2025-02-12 17:27:30 +03:00
43e300c773 [#29] Client: Add PlacementVector unit tests
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2025-01-13 10:34:44 +03:00
568bdc67e8 [#29] Client: Add object placement methods
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-12-24 17:32:29 +03:00
8637515869 [#28] Client: Move CalculateObjectId from IFrostFsClient to statis tools
Get payload hash as an argument

Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-12-12 12:28:45 +00:00
db9b93b2e6 [#28] Client: Move CalculateObjectId from IFrostFsClient to statis tools
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-12-12 12:28:45 +00:00
543247e4d9 [#28] Client: add method to calculate ObjectId
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-12-12 12:28:45 +00:00
abd9b5d0d0 [#39] Add CODEOWNERS
Signed-off-by: Vitaliy Potyarkin <v.potyarkin@yadro.com>
2024-12-10 18:52:18 +03:00
c9418a1894 [#28] Client: Use external GRPC Channnel
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-12-06 09:01:33 +03:00
9bb7b5eff8 [#28] Clients: Make immutable parameters
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-12-02 19:33:45 +03:00
749000a090 [#28] Client: Apply code optimizations
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-11-18 17:00:19 +03:00
766f61a5f7 [#26] All: Remove V2 from naming
Rename project, namespaces and class names

Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-11-18 11:33:50 +03:00
c406df1a78 [#13] Execute .NET code analyzers in CI
Signed-off-by: Vitaliy Potyarkin <v.potyarkin@yadro.com>
2024-11-18 07:07:57 +00:00
14dc76898e [#25] Client Implement PAtch and Range methods
Fix the specific type in interfaces

Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-11-14 11:24:14 +03:00
003b7fdfdd [#25] Client: Implement Patch and Range methods
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-11-08 10:38:50 +03:00
bff8d67867 [#24] Client: Implement pool part2
Unicode fix

Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-11-01 10:41:17 +03:00
ee20798379 [#24] Client: Implement pool part2
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-11-01 10:30:28 +03:00
c9a75ea025 [#24] Client: Implement pool part1
first iteration - base classes and methods

Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-10-21 10:48:00 +03:00
d1271df207 [#13] Client: Use code analyzers
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-09-23 19:25:59 +03:00
d7dbbf8da8 [#24] Client: Add Ape manager
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-09-23 10:20:40 +03:00
Pavel Gross
704ce41173 [#23] Client: Refactoring to optimize memory usage
Signed-off-by: Pavel Gross <p.gross@yando.com>
2024-09-12 11:56:26 +03:00
Pavel Gross
6562aa27a5 [#23] Client: Refactoring to optimize memory usage
Signed-off-by: Pavel Gross <p.gross@yando.com>
2024-09-11 10:58:00 +03:00
Pavel Gross
1a02ac2ae7 [#22] Client: Container session
Signed-off-by: Pavel Gross <p.gross@yando.com>
2024-08-19 14:18:11 +03:00
Pavel Gross
22e2a53551 [#16] Client: Unit tests
Signed-off-by: Pavel Gross <p.gross@yando.com>
2024-08-16 12:09:17 +03:00
Pavel Gross
2a28806ace [#21] Client: Allows multinenant client
Using one client for several owners

Signed-off-by: Pavel Gross <p.gross@yando.com>
2024-08-12 10:53:00 +03:00
Pavel Gross
18126ea763 [#20] Optimize memory usage
Provide custom buffer and use ArrayPool

Signed-off-by: Pavel Gross <p.gross@yando.com>
2024-08-05 11:21:05 +03:00
Pavel Gross
6083834582 [#20] Client: Fix typo
Signed-off-by: Pavel Gross <p.gross@yando.com>
2024-08-01 18:02:59 +03:00
Pavel Gross
0ddde467cd [#20] Client: Optimize memory usage
Avoid memory allocation, use cache and static

Signed-off-by: Pavel Gross <p.gross@yando.com>
2024-08-01 16:18:19 +03:00
Pavel Gross
35fe791406 [#19] Client: Use specific classes for search
Signed-off-by: Pavel Gross <p.gross@yando.com>
2024-07-25 14:37:58 +03:00
Pavel Gross
3206abc33e [#18] Client: Rename parameters
Signed-off-by: Pavel Gross <p.gross@yando.com>
2024-07-25 12:28:38 +03:00
p.gross
816e1eb2f1 [#17] Models: Remove internal visibility
Signed-off-by: Pavel Gross <p.gross@yando.com>
2024-07-22 14:50:21 +03:00
7b9c19f37c [#17] Client: Add extra parameter
API methods' parameters types with optional session, polling settings, xHeaders etc. and corresponding handlers have been added

Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-07-18 15:33:40 +03:00
00a1e9412f [#16] Remove Tz fix formating
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-07-11 11:34:08 +03:00
fefa2da218 [#16] Unit tests
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-07-09 17:56:44 +03:00
ae67b12313 [#14] Add interceptors
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-07-01 15:11:52 +03:00
605463ec24 [#13] Drop comments
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-06-27 12:38:14 +03:00
17492ee871 [#13] Add cancellation token to GetChank method
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-06-26 15:24:15 +03:00
f5d1899dd2 [#13] Change GetObject result to stream
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-06-26 15:15:58 +03:00
c988ff3c76 [#11] Add Network Snapshot
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-06-26 12:29:33 +03:00
b69d22966f [#7] Client cut internal 2024-06-14 12:07:15 +00:00
545e647d7b [#4] infrastructure and sample Client Cut
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-06-10 11:31:36 +03:00
p.gross
0c4723c705 [#3] Move to netstandard 2.0
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-05-30 11:47:51 +03:00
307 changed files with 18125 additions and 3844 deletions

901
.editorconfig Normal file
View file

@ -0,0 +1,901 @@
[*.cs]
# CA1001: Types that own disposable fields should be disposable
dotnet_diagnostic.CA1001.severity = warning
# CA1000: Do not declare static members on generic types
dotnet_diagnostic.CA1000.severity = warning
# CA1002: Do not expose generic lists
dotnet_diagnostic.CA1002.severity = warning
# CA1003: Use generic event handler instances
dotnet_diagnostic.CA1003.severity = warning
# CA1005: Avoid excessive parameters on generic types
dotnet_diagnostic.CA1005.severity = warning
# CA1008: Enums should have zero value
dotnet_diagnostic.CA1008.severity = warning
# CA1010: Generic interface should also be implemented
dotnet_diagnostic.CA1010.severity = warning
# CA1012: Abstract types should not have public constructors
dotnet_diagnostic.CA1012.severity = warning
# CA1014: Mark assemblies with CLSCompliant
dotnet_diagnostic.CA1014.severity = warning
# CA1016: Mark assemblies with assembly version
dotnet_diagnostic.CA1016.severity = warning
# CA1017: Mark assemblies with ComVisible
dotnet_diagnostic.CA1017.severity = warning
# CA1018: Mark attributes with AttributeUsageAttribute
dotnet_diagnostic.CA1018.severity = warning
# CA1019: Define accessors for attribute arguments
dotnet_diagnostic.CA1019.severity = warning
# CA1021: Avoid out parameters
dotnet_diagnostic.CA1021.severity = warning
# CA1024: Use properties where appropriate
dotnet_diagnostic.CA1024.severity = warning
# CA1027: Mark enums with FlagsAttribute
dotnet_diagnostic.CA1027.severity = warning
# CA1028: Enum Storage should be Int32
dotnet_diagnostic.CA1028.severity = warning
# CA1030: Use events where appropriate
dotnet_diagnostic.CA1030.severity = warning
# CA1031: Do not catch general exception types
dotnet_diagnostic.CA1031.severity = warning
# CA1033: Interface methods should be callable by child types
dotnet_diagnostic.CA1033.severity = warning
# CA1034: Nested types should not be visible
dotnet_diagnostic.CA1034.severity = warning
# CA1036: Override methods on comparable types
dotnet_diagnostic.CA1036.severity = warning
# CA1040: Avoid empty interfaces
dotnet_diagnostic.CA1040.severity = warning
# CA1041: Provide ObsoleteAttribute message
dotnet_diagnostic.CA1041.severity = warning
# CA1043: Use Integral Or String Argument For Indexers
dotnet_diagnostic.CA1043.severity = warning
# CA1044: Properties should not be write only
dotnet_diagnostic.CA1044.severity = warning
# CA1045: Do not pass types by reference
dotnet_diagnostic.CA1045.severity = warning
# CA1046: Do not overload equality operator on reference types
dotnet_diagnostic.CA1046.severity = warning
# CA1050: Declare types in namespaces
dotnet_diagnostic.CA1050.severity = warning
# CA1051: Do not declare visible instance fields
dotnet_diagnostic.CA1051.severity = warning
# CA1052: Static holder types should be Static or NotInheritable
dotnet_diagnostic.CA1052.severity = warning
# CA1054: URI-like parameters should not be strings
dotnet_diagnostic.CA1054.severity = warning
# CA1055: URI-like return values should not be strings
dotnet_diagnostic.CA1055.severity = warning
# CA1056: URI-like properties should not be strings
dotnet_diagnostic.CA1056.severity = warning
# CA1058: Types should not extend certain base types
dotnet_diagnostic.CA1058.severity = warning
# CA1060: Move pinvokes to native methods class
dotnet_diagnostic.CA1060.severity = warning
# CA1061: Do not hide base class methods
dotnet_diagnostic.CA1061.severity = warning
# CA1062: Validate arguments of public methods
dotnet_diagnostic.CA1062.severity = warning
# CA1063: Implement IDisposable Correctly
dotnet_diagnostic.CA1063.severity = warning
# CA1064: Exceptions should be public
dotnet_diagnostic.CA1064.severity = warning
# CA1065: Do not raise exceptions in unexpected locations
dotnet_diagnostic.CA1065.severity = warning
# CA1066: Implement IEquatable when overriding Object.Equals
dotnet_diagnostic.CA1066.severity = warning
# CA1067: Override Object.Equals(object) when implementing IEquatable<T>
dotnet_diagnostic.CA1067.severity = warning
# CA1068: CancellationToken parameters must come last
dotnet_diagnostic.CA1068.severity = warning
# CA1069: Enums values should not be duplicated
dotnet_diagnostic.CA1069.severity = warning
# CA1070: Do not declare event fields as virtual
dotnet_diagnostic.CA1070.severity = warning
# CA1303: Do not pass literals as localized parameters
dotnet_diagnostic.CA1303.severity = warning
# CA1304: Specify CultureInfo
dotnet_diagnostic.CA1304.severity = warning
# CA1305: Specify IFormatProvider
dotnet_diagnostic.CA1305.severity = warning
# CA1307: Specify StringComparison for clarity
dotnet_diagnostic.CA1307.severity = warning
# CA1308: Normalize strings to uppercase
dotnet_diagnostic.CA1308.severity = warning
# CA1310: Specify StringComparison for correctness
dotnet_diagnostic.CA1310.severity = warning
# CA1401: P/Invokes should not be visible
dotnet_diagnostic.CA1401.severity = warning
# CA1416: Validate platform compatibility
dotnet_diagnostic.CA1416.severity = warning
# CA1417: Do not use 'OutAttribute' on string parameters for P/Invokes
dotnet_diagnostic.CA1417.severity = warning
# CA1418: Use valid platform string
dotnet_diagnostic.CA1418.severity = warning
# CA1419: Provide a parameterless constructor that is as visible as the containing type for concrete types derived from 'System.Runtime.InteropServices.SafeHandle'
dotnet_diagnostic.CA1419.severity = warning
# CA1420: Property, type, or attribute requires runtime marshalling
dotnet_diagnostic.CA1420.severity = warning
# CA1421: This method uses runtime marshalling even when the 'DisableRuntimeMarshallingAttribute' is applied
dotnet_diagnostic.CA1421.severity = warning
# CA1422: Validate platform compatibility
dotnet_diagnostic.CA1422.severity = warning
# CA1501: Avoid excessive inheritance
dotnet_diagnostic.CA1501.severity = warning
# CA1502: Avoid excessive complexity
dotnet_diagnostic.CA1502.severity = warning
# CA1505: Avoid unmaintainable code
dotnet_diagnostic.CA1505.severity = warning
# CA1506: Avoid excessive class coupling
# dotnet_diagnostic.CA1506.severity = warning
# CA1509: Invalid entry in code metrics rule specification file
dotnet_diagnostic.CA1509.severity = warning
# CA1510: Use ArgumentNullException throw helper
dotnet_diagnostic.CA1510.severity = warning
# CA1511: Use ArgumentException throw helper
dotnet_diagnostic.CA1511.severity = warning
# CA1512: Use ArgumentOutOfRangeException throw helper
dotnet_diagnostic.CA1512.severity = warning
# CA1513: Use ObjectDisposedException throw helper
dotnet_diagnostic.CA1513.severity = warning
# CA1700: Do not name enum values 'Reserved'
dotnet_diagnostic.CA1700.severity = warning
# CA1707: Identifiers should not contain underscores
dotnet_diagnostic.CA1707.severity = warning
# CA1708: Identifiers should differ by more than case
dotnet_diagnostic.CA1708.severity = none
# CA1710: Identifiers should have correct suffix
dotnet_diagnostic.CA1710.severity = warning
# CA1711: Identifiers should not have incorrect suffix
dotnet_diagnostic.CA1711.severity = warning
# CA1712: Do not prefix enum values with type name
dotnet_diagnostic.CA1712.severity = warning
# CA1713: Events should not have 'Before' or 'After' prefix
dotnet_diagnostic.CA1713.severity = warning
# CA1715: Identifiers should have correct prefix
dotnet_diagnostic.CA1715.severity = warning
# CA1716: Identifiers should not match keywords
dotnet_diagnostic.CA1716.severity = none
# CA1720: Identifier contains type name
dotnet_diagnostic.CA1720.severity = warning
# CA1721: Property names should not match get methods
dotnet_diagnostic.CA1721.severity = warning
# CA1724: Type names should not match namespaces
dotnet_diagnostic.CA1724.severity = warning
# CA1725: Parameter names should match base declaration
dotnet_diagnostic.CA1725.severity = warning
# CA1727: Use PascalCase for named placeholders
dotnet_diagnostic.CA1727.severity = warning
# CA1806: Do not ignore method results
dotnet_diagnostic.CA1806.severity = warning
# CA1810: Initialize reference type static fields inline
dotnet_diagnostic.CA1810.severity = warning
# CA1813: Avoid unsealed attributes
dotnet_diagnostic.CA1813.severity = warning
# CA1814: Prefer jagged arrays over multidimensional
dotnet_diagnostic.CA1814.severity = warning
# CA1815: Override equals and operator equals on value types
dotnet_diagnostic.CA1815.severity = warning
# CA1816: Dispose methods should call SuppressFinalize
dotnet_diagnostic.CA1816.severity = warning
# CA1819: Properties should not return arrays
# dotnet_diagnostic.CA1819.severity = warning
# CA1820: Test for empty strings using string length
dotnet_diagnostic.CA1820.severity = warning
# CA1821: Remove empty Finalizers
dotnet_diagnostic.CA1821.severity = warning
# CA1822: Mark members as static
dotnet_diagnostic.CA1822.severity = warning
# CA1823: Avoid unused private fields
dotnet_diagnostic.CA1823.severity = warning
# CA1826: Do not use Enumerable methods on indexable collections
dotnet_diagnostic.CA1826.severity = warning
# CA1827: Do not use Count() or LongCount() when Any() can be used
dotnet_diagnostic.CA1827.severity = warning
# CA1828: Do not use CountAsync() or LongCountAsync() when AnyAsync() can be used
dotnet_diagnostic.CA1828.severity = warning
# CA1829: Use Length/Count property instead of Count() when available
dotnet_diagnostic.CA1829.severity = warning
# CA1830: Prefer strongly-typed Append and Insert method overloads on StringBuilder
dotnet_diagnostic.CA1830.severity = warning
# CA1831: Use AsSpan or AsMemory instead of Range-based indexers when appropriate
dotnet_diagnostic.CA1831.severity = warning
# CA1832: Use AsSpan or AsMemory instead of Range-based indexers when appropriate
dotnet_diagnostic.CA1832.severity = warning
# CA1833: Use AsSpan or AsMemory instead of Range-based indexers when appropriate
dotnet_diagnostic.CA1833.severity = warning
# CA1834: Consider using 'StringBuilder.Append(char)' when applicable
dotnet_diagnostic.CA1834.severity = warning
# CA1835: Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync'
dotnet_diagnostic.CA1835.severity = warning
# CA1836: Prefer IsEmpty over Count
dotnet_diagnostic.CA1836.severity = warning
# CA1837: Use 'Environment.ProcessId'
dotnet_diagnostic.CA1837.severity = warning
# CA1838: Avoid 'StringBuilder' parameters for P/Invokes
dotnet_diagnostic.CA1838.severity = warning
# CA1839: Use 'Environment.ProcessPath'
dotnet_diagnostic.CA1839.severity = warning
# CA1840: Use 'Environment.CurrentManagedThreadId'
dotnet_diagnostic.CA1840.severity = warning
# CA1842: Do not use 'WhenAll' with a single task
dotnet_diagnostic.CA1842.severity = warning
# CA1843: Do not use 'WaitAll' with a single task
dotnet_diagnostic.CA1843.severity = warning
# CA1844: Provide memory-based overrides of async methods when subclassing 'Stream'
dotnet_diagnostic.CA1844.severity = warning
# CA1846: Prefer 'AsSpan' over 'Substring'
dotnet_diagnostic.CA1846.severity = warning
# CA1847: Use char literal for a single character lookup
dotnet_diagnostic.CA1847.severity = warning
# CA1848: Use the LoggerMessage delegates
dotnet_diagnostic.CA1848.severity = warning
# CA1849: Call async methods when in an async method
dotnet_diagnostic.CA1849.severity = warning
# CA1850: Prefer static 'HashData' method over 'ComputeHash'
dotnet_diagnostic.CA1850.severity = warning
# CA1852: Seal internal types
dotnet_diagnostic.CA1852.severity = warning
# CA1853: Unnecessary call to 'Dictionary.ContainsKey(key)'
dotnet_diagnostic.CA1853.severity = warning
# CA1854: Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method
dotnet_diagnostic.CA1854.severity = warning
# CA1858: Use 'StartsWith' instead of 'IndexOf'
dotnet_diagnostic.CA1858.severity = warning
# CA1859: Use concrete types when possible for improved performance
dotnet_diagnostic.CA1859.severity = warning
# CA1860: Avoid using 'Enumerable.Any()' extension method
dotnet_diagnostic.CA1860.severity = warning
# CA1861: Avoid constant arrays as arguments
dotnet_diagnostic.CA1861.severity = warning
# CA1862: Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
dotnet_diagnostic.CA1862.severity = warning
# CA1863: Use 'CompositeFormat'
dotnet_diagnostic.CA1863.severity = warning
# CA1864: Prefer the 'IDictionary.TryAdd(TKey, TValue)' method
dotnet_diagnostic.CA1864.severity = warning
# CA1868: Unnecessary call to 'Contains(item)'
dotnet_diagnostic.CA1868.severity = warning
# CA1869: Cache and reuse 'JsonSerializerOptions' instances
dotnet_diagnostic.CA1869.severity = warning
# CA2000: Dispose objects before losing scope
dotnet_diagnostic.CA2000.severity = warning
# CA2002: Do not lock on objects with weak identity
dotnet_diagnostic.CA2002.severity = warning
# CA2007: Consider calling ConfigureAwait on the awaited task
dotnet_diagnostic.CA2007.severity = warning
# CA2008: Do not create tasks without passing a TaskScheduler
dotnet_diagnostic.CA2008.severity = warning
# CA2009: Do not call ToImmutableCollection on an ImmutableCollection value
dotnet_diagnostic.CA2009.severity = warning
# CA2011: Avoid infinite recursion
dotnet_diagnostic.CA2011.severity = warning
# CA2012: Use ValueTasks correctly
dotnet_diagnostic.CA2012.severity = warning
# CA2013: Do not use ReferenceEquals with value types
dotnet_diagnostic.CA2013.severity = warning
# CA2015: Do not define finalizers for types derived from MemoryManager<T>
dotnet_diagnostic.CA2015.severity = warning
# CA2017: Parameter count mismatch
dotnet_diagnostic.CA2017.severity = warning
# CA2018: 'Buffer.BlockCopy' expects the number of bytes to be copied for the 'count' argument
dotnet_diagnostic.CA2018.severity = warning
# CA2019: Improper 'ThreadStatic' field initialization
dotnet_diagnostic.CA2019.severity = warning
# CA2021: Do not call Enumerable.Cast<T> or Enumerable.OfType<T> with incompatible types
dotnet_diagnostic.CA2021.severity = warning
# CA2100: Review SQL queries for security vulnerabilities
dotnet_diagnostic.CA2100.severity = warning
# CA2101: Specify marshaling for P/Invoke string arguments
dotnet_diagnostic.CA2101.severity = warning
# CA2119: Seal methods that satisfy private interfaces
dotnet_diagnostic.CA2119.severity = warning
# CA2153: Do Not Catch Corrupted State Exceptions
dotnet_diagnostic.CA2153.severity = warning
# CA2200: Rethrow to preserve stack details
dotnet_diagnostic.CA2200.severity = warning
# CA2201: Do not raise reserved exception types
dotnet_diagnostic.CA2201.severity = warning
# CA2207: Initialize value type static fields inline
dotnet_diagnostic.CA2207.severity = warning
# CA2208: Instantiate argument exceptions correctly
dotnet_diagnostic.CA2208.severity = warning
# CA2211: Non-constant fields should not be visible
dotnet_diagnostic.CA2211.severity = warning
# CA2213: Disposable fields should be disposed
dotnet_diagnostic.CA2213.severity = warning
# CA2214: Do not call overridable methods in constructors
dotnet_diagnostic.CA2214.severity = warning
# CA2215: Dispose methods should call base class dispose
dotnet_diagnostic.CA2215.severity = warning
# CA2216: Disposable types should declare finalizer
dotnet_diagnostic.CA2216.severity = warning
# CA2217: Do not mark enums with FlagsAttribute
dotnet_diagnostic.CA2217.severity = warning
# CA2219: Do not raise exceptions in finally clauses
dotnet_diagnostic.CA2219.severity = warning
# CA2225: Operator overloads have named alternates
dotnet_diagnostic.CA2225.severity = warning
# CA2226: Operators should have symmetrical overloads
dotnet_diagnostic.CA2226.severity = warning
# CA2227: Collection properties should be read only
dotnet_diagnostic.CA2227.severity = warning
# CA2231: Overload operator equals on overriding value type Equals
dotnet_diagnostic.CA2231.severity = warning
# CA2235: Mark all non-serializable fields
dotnet_diagnostic.CA2235.severity = warning
# CA2237: Mark ISerializable types with serializable
dotnet_diagnostic.CA2237.severity = warning
# CA2241: Provide correct arguments to formatting methods
dotnet_diagnostic.CA2241.severity = warning
# CA2242: Test for NaN correctly
dotnet_diagnostic.CA2242.severity = warning
# CA2243: Attribute string literals should parse correctly
dotnet_diagnostic.CA2243.severity = warning
# CA2244: Do not duplicate indexed element initializations
dotnet_diagnostic.CA2244.severity = warning
# CA2245: Do not assign a property to itself
dotnet_diagnostic.CA2245.severity = warning
# CA2246: Assigning symbol and its member in the same statement
dotnet_diagnostic.CA2246.severity = warning
# CA2247: Argument passed to TaskCompletionSource constructor should be TaskCreationOptions enum instead of TaskContinuationOptions enum
dotnet_diagnostic.CA2247.severity = warning
# CA2248: Provide correct 'enum' argument to 'Enum.HasFlag'
dotnet_diagnostic.CA2248.severity = warning
# CA2249: Consider using 'string.Contains' instead of 'string.IndexOf'
dotnet_diagnostic.CA2249.severity = warning
# CA2250: Use 'ThrowIfCancellationRequested'
dotnet_diagnostic.CA2250.severity = warning
# CA2251: Use 'string.Equals'
dotnet_diagnostic.CA2251.severity = warning
# CA2253: Named placeholders should not be numeric values
dotnet_diagnostic.CA2253.severity = warning
# CA2254: Template should be a static expression
dotnet_diagnostic.CA2254.severity = warning
# CA2255: The 'ModuleInitializer' attribute should not be used in libraries
dotnet_diagnostic.CA2255.severity = warning
# CA2256: All members declared in parent interfaces must have an implementation in a DynamicInterfaceCastableImplementation-attributed interface
dotnet_diagnostic.CA2256.severity = warning
# CA2257: Members defined on an interface with the 'DynamicInterfaceCastableImplementationAttribute' should be 'static'
dotnet_diagnostic.CA2257.severity = warning
# CA2258: Providing a 'DynamicInterfaceCastableImplementation' interface in Visual Basic is unsupported
dotnet_diagnostic.CA2258.severity = warning
# CA2259: 'ThreadStatic' only affects static fields
dotnet_diagnostic.CA2259.severity = warning
# CA2261: Do not use ConfigureAwaitOptions.SuppressThrowing with Task<TResult>
dotnet_diagnostic.CA2261.severity = warning
# CA2300: Do not use insecure deserializer BinaryFormatter
dotnet_diagnostic.CA2300.severity = warning
# CA2301: Do not call BinaryFormatter.Deserialize without first setting BinaryFormatter.Binder
dotnet_diagnostic.CA2301.severity = warning
# CA2302: Ensure BinaryFormatter.Binder is set before calling BinaryFormatter.Deserialize
dotnet_diagnostic.CA2302.severity = warning
# CA2305: Do not use insecure deserializer LosFormatter
dotnet_diagnostic.CA2305.severity = warning
# CA2310: Do not use insecure deserializer NetDataContractSerializer
dotnet_diagnostic.CA2310.severity = warning
# CA2311: Do not deserialize without first setting NetDataContractSerializer.Binder
dotnet_diagnostic.CA2311.severity = warning
# CA2312: Ensure NetDataContractSerializer.Binder is set before deserializing
dotnet_diagnostic.CA2312.severity = warning
# CA2315: Do not use insecure deserializer ObjectStateFormatter
dotnet_diagnostic.CA2315.severity = warning
# CA2321: Do not deserialize with JavaScriptSerializer using a SimpleTypeResolver
dotnet_diagnostic.CA2321.severity = warning
# CA2322: Ensure JavaScriptSerializer is not initialized with SimpleTypeResolver before deserializing
dotnet_diagnostic.CA2322.severity = warning
# CA2326: Do not use TypeNameHandling values other than None
dotnet_diagnostic.CA2326.severity = warning
# CA2327: Do not use insecure JsonSerializerSettings
dotnet_diagnostic.CA2327.severity = warning
# CA2328: Ensure that JsonSerializerSettings are secure
dotnet_diagnostic.CA2328.severity = warning
# CA2329: Do not deserialize with JsonSerializer using an insecure configuration
dotnet_diagnostic.CA2329.severity = warning
# CA2330: Ensure that JsonSerializer has a secure configuration when deserializing
dotnet_diagnostic.CA2330.severity = warning
# CA2350: Do not use DataTable.ReadXml() with untrusted data
dotnet_diagnostic.CA2350.severity = warning
# CA2351: Do not use DataSet.ReadXml() with untrusted data
dotnet_diagnostic.CA2351.severity = warning
# CA2361: Ensure auto-generated class containing DataSet.ReadXml() is not used with untrusted data
dotnet_diagnostic.CA2361.severity = warning
# CA3001: Review code for SQL injection vulnerabilities
dotnet_diagnostic.CA3001.severity = warning
# CA3002: Review code for XSS vulnerabilities
dotnet_diagnostic.CA3002.severity = warning
# CA3003: Review code for file path injection vulnerabilities
dotnet_diagnostic.CA3003.severity = warning
# CA3004: Review code for information disclosure vulnerabilities
dotnet_diagnostic.CA3004.severity = warning
# CA3005: Review code for LDAP injection vulnerabilities
dotnet_diagnostic.CA3005.severity = warning
# CA3006: Review code for process command injection vulnerabilities
dotnet_diagnostic.CA3006.severity = warning
# CA3007: Review code for open redirect vulnerabilities
dotnet_diagnostic.CA3007.severity = warning
# CA3008: Review code for XPath injection vulnerabilities
dotnet_diagnostic.CA3008.severity = warning
# CA3009: Review code for XML injection vulnerabilities
dotnet_diagnostic.CA3009.severity = warning
# CA3010: Review code for XAML injection vulnerabilities
dotnet_diagnostic.CA3010.severity = warning
# CA3011: Review code for DLL injection vulnerabilities
dotnet_diagnostic.CA3011.severity = warning
# CA3012: Review code for regex injection vulnerabilities
dotnet_diagnostic.CA3012.severity = warning
# CA3061: Do Not Add Schema By URL
dotnet_diagnostic.CA3061.severity = warning
# CA3075: Insecure DTD processing in XML
dotnet_diagnostic.CA3075.severity = warning
# CA3076: Insecure XSLT script processing
dotnet_diagnostic.CA3076.severity = warning
# CA3077: Insecure Processing in API Design, XmlDocument and XmlTextReader
dotnet_diagnostic.CA3077.severity = warning
# CA3147: Mark Verb Handlers With Validate Antiforgery Token
dotnet_diagnostic.CA3147.severity = warning
# CA5350: Do Not Use Weak Cryptographic Algorithms
dotnet_diagnostic.CA5350.severity = warning
# CA5351: Do Not Use Broken Cryptographic Algorithms
dotnet_diagnostic.CA5351.severity = warning
# CA5358: Review cipher mode usage with cryptography experts
dotnet_diagnostic.CA5358.severity = warning
# CA5359: Do Not Disable Certificate Validation
dotnet_diagnostic.CA5359.severity = warning
# CA5360: Do Not Call Dangerous Methods In Deserialization
dotnet_diagnostic.CA5360.severity = warning
# CA5361: Do Not Disable SChannel Use of Strong Crypto
dotnet_diagnostic.CA5361.severity = warning
# CA5362: Potential reference cycle in deserialized object graph
dotnet_diagnostic.CA5362.severity = warning
# CA5363: Do Not Disable Request Validation
dotnet_diagnostic.CA5363.severity = warning
# CA5364: Do Not Use Deprecated Security Protocols
dotnet_diagnostic.CA5364.severity = warning
# CA5365: Do Not Disable HTTP Header Checking
dotnet_diagnostic.CA5365.severity = warning
# CA5366: Use XmlReader for 'DataSet.ReadXml()'
dotnet_diagnostic.CA5366.severity = warning
# CA5367: Do Not Serialize Types With Pointer Fields
dotnet_diagnostic.CA5367.severity = warning
# CA5368: Set ViewStateUserKey For Classes Derived From Page
dotnet_diagnostic.CA5368.severity = warning
# CA5369: Use XmlReader for 'XmlSerializer.Deserialize()'
dotnet_diagnostic.CA5369.severity = warning
# CA5370: Use XmlReader for XmlValidatingReader constructor
dotnet_diagnostic.CA5370.severity = warning
# CA5371: Use XmlReader for 'XmlSchema.Read()'
dotnet_diagnostic.CA5371.severity = warning
# CA5372: Use XmlReader for XPathDocument constructor
dotnet_diagnostic.CA5372.severity = warning
# CA5373: Do not use obsolete key derivation function
dotnet_diagnostic.CA5373.severity = warning
# CA5374: Do Not Use XslTransform
dotnet_diagnostic.CA5374.severity = warning
# CA5375: Do Not Use Account Shared Access Signature
dotnet_diagnostic.CA5375.severity = warning
# CA5376: Use SharedAccessProtocol HttpsOnly
dotnet_diagnostic.CA5376.severity = warning
# CA5377: Use Container Level Access Policy
dotnet_diagnostic.CA5377.severity = warning
# CA5378: Do not disable ServicePointManagerSecurityProtocols
dotnet_diagnostic.CA5378.severity = warning
# CA5379: Ensure Key Derivation Function algorithm is sufficiently strong
dotnet_diagnostic.CA5379.severity = warning
# CA5380: Do Not Add Certificates To Root Store
dotnet_diagnostic.CA5380.severity = warning
# CA5381: Ensure Certificates Are Not Added To Root Store
dotnet_diagnostic.CA5381.severity = warning
# CA5382: Use Secure Cookies In ASP.NET Core
dotnet_diagnostic.CA5382.severity = warning
# CA5383: Ensure Use Secure Cookies In ASP.NET Core
dotnet_diagnostic.CA5383.severity = warning
# CA5384: Do Not Use Digital Signature Algorithm (DSA)
dotnet_diagnostic.CA5384.severity = warning
# CA5385: Use Rivest-Shamir-Adleman (RSA) Algorithm With Sufficient Key Size
dotnet_diagnostic.CA5385.severity = warning
# CA5386: Avoid hardcoding SecurityProtocolType value
dotnet_diagnostic.CA5386.severity = warning
# CA5387: Do Not Use Weak Key Derivation Function With Insufficient Iteration Count
dotnet_diagnostic.CA5387.severity = warning
# CA5388: Ensure Sufficient Iteration Count When Using Weak Key Derivation Function
dotnet_diagnostic.CA5388.severity = warning
# CA5389: Do Not Add Archive Item's Path To The Target File System Path
dotnet_diagnostic.CA5389.severity = warning
# CA5390: Do not hard-code encryption key
dotnet_diagnostic.CA5390.severity = warning
# CA5391: Use antiforgery tokens in ASP.NET Core MVC controllers
dotnet_diagnostic.CA5391.severity = warning
# CA5392: Use DefaultDllImportSearchPaths attribute for P/Invokes
dotnet_diagnostic.CA5392.severity = warning
# CA5393: Do not use unsafe DllImportSearchPath value
dotnet_diagnostic.CA5393.severity = warning
# CA5394: Do not use insecure randomness
dotnet_diagnostic.CA5394.severity = warning
# CA5395: Miss HttpVerb attribute for action methods
dotnet_diagnostic.CA5395.severity = warning
# CA5396: Set HttpOnly to true for HttpCookie
dotnet_diagnostic.CA5396.severity = warning
# CA5397: Do not use deprecated SslProtocols values
dotnet_diagnostic.CA5397.severity = warning
# CA5398: Avoid hardcoded SslProtocols values
dotnet_diagnostic.CA5398.severity = warning
# CA5399: HttpClients should enable certificate revocation list checks
dotnet_diagnostic.CA5399.severity = warning
# CA5400: Ensure HttpClient certificate revocation list check is not disabled
dotnet_diagnostic.CA5400.severity = warning
# CA5401: Do not use CreateEncryptor with non-default IV
dotnet_diagnostic.CA5401.severity = warning
# CA5402: Use CreateEncryptor with the default IV
dotnet_diagnostic.CA5402.severity = warning
# CA5403: Do not hard-code certificate
dotnet_diagnostic.CA5403.severity = warning
# CA5404: Do not disable token validation checks
dotnet_diagnostic.CA5404.severity = warning
# CA5405: Do not always skip token validation in delegates
dotnet_diagnostic.CA5405.severity = warning
# CA1032: Implement standard exception constructors
dotnet_diagnostic.CA1032.severity = warning
# CA1200: Avoid using cref tags with a prefix
dotnet_diagnostic.CA1200.severity = warning
# CA1309: Use ordinal string comparison
dotnet_diagnostic.CA1309.severity = warning
# CA1311: Specify a culture or use an invariant version
dotnet_diagnostic.CA1311.severity = warning
# CA1507: Use nameof to express symbol names
dotnet_diagnostic.CA1507.severity = warning
# CA1508: Avoid dead conditional code
dotnet_diagnostic.CA1508.severity = warning
# CA1802: Use literals where appropriate
dotnet_diagnostic.CA1802.severity = warning
# CA1805: Do not initialize unnecessarily
dotnet_diagnostic.CA1805.severity = warning
# CA1812: Avoid uninstantiated internal classes
dotnet_diagnostic.CA1812.severity = warning
# CA1824: Mark assemblies with NeutralResourcesLanguageAttribute
dotnet_diagnostic.CA1824.severity = warning
# CA1825: Avoid zero-length array allocations
dotnet_diagnostic.CA1825.severity = warning
# CA1841: Prefer Dictionary.Contains methods
dotnet_diagnostic.CA1841.severity = warning
# CA1845: Use span-based 'string.Concat'
dotnet_diagnostic.CA1845.severity = warning
# CA1851: Possible multiple enumerations of 'IEnumerable' collection
dotnet_diagnostic.CA1851.severity = warning
# CA1855: Prefer 'Clear' over 'Fill'
dotnet_diagnostic.CA1855.severity = warning
# CA1856: Incorrect usage of ConstantExpected attribute
dotnet_diagnostic.CA1856.severity = warning
# CA1857: A constant is expected for the parameter
dotnet_diagnostic.CA1857.severity = warning
# CA1865: Use char overload
dotnet_diagnostic.CA1865.severity = warning
# CA1866: Use char overload
dotnet_diagnostic.CA1866.severity = warning
# CA1867: Use char overload
dotnet_diagnostic.CA1867.severity = warning
# CA1870: Use a cached 'SearchValues' instance
dotnet_diagnostic.CA1870.severity = warning
# CA2014: Do not use stackalloc in loops
dotnet_diagnostic.CA2014.severity = warning
# CA2016: Forward the 'CancellationToken' parameter to methods
dotnet_diagnostic.CA2016.severity = warning
# CA2020: Prevent behavioral change
dotnet_diagnostic.CA2020.severity = warning
# CA2234: Pass system uri objects instead of strings
dotnet_diagnostic.CA2234.severity = warning
# CA2252: This API requires opting into preview features
dotnet_diagnostic.CA2252.severity = warning
# CA2260: Use correct type parameter
dotnet_diagnostic.CA2260.severity = warning
# CA2352: Unsafe DataSet or DataTable in serializable type can be vulnerable to remote code execution attacks
dotnet_diagnostic.CA2352.severity = warning
# CA2353: Unsafe DataSet or DataTable in serializable type
dotnet_diagnostic.CA2353.severity = warning
# CA2354: Unsafe DataSet or DataTable in deserialized object graph can be vulnerable to remote code execution attacks
dotnet_diagnostic.CA2354.severity = warning
# CA2355: Unsafe DataSet or DataTable type found in deserializable object graph
dotnet_diagnostic.CA2355.severity = warning
# CA2356: Unsafe DataSet or DataTable type in web deserializable object graph
dotnet_diagnostic.CA2356.severity = warning
# CA2362: Unsafe DataSet or DataTable in auto-generated serializable type can be vulnerable to remote code execution attacks
dotnet_diagnostic.CA2362.severity = warning

View file

@ -0,0 +1,22 @@
name: lint-build
on:
push:
pull_request:
jobs:
lint-build:
name: dotnet${{ matrix.dotnet }}
runs-on: docker
container: git.frostfs.info/truecloudlab/env:dotnet-${{ matrix.dotnet }}
strategy:
matrix:
dotnet:
- '8.0'
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
# Dotnet runs code analyzers on build (if configured):
# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/overview?tabs=net-8#enable-on-build
- run: dotnet build

View file

@ -0,0 +1,35 @@
on:
workflow_dispatch:
jobs:
image:
name: Publish NuGet packages
runs-on: docker
container: git.frostfs.info/truecloudlab/env:dotnet-8.0
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Build NuGet packages
# `dotnet build` implies and replaces `dotnet pack` thanks to `GeneratePackageOnBuild`
run: dotnet build
- name: Publish unsigned NuGet packages
run: |-
dotnet nuget add source \
--name "$NUGET_REGISTRY" \
--username "$NUGET_REGISTRY_USER" \
--password "$NUGET_REGISTRY_PASSWORD" \
--store-password-in-clear-text \
"$NUGET_REGISTRY_URL"
find -iname '*.nupkg' | grep . | xargs -d'\n' -t -n1 \
dotnet nuget push --source "$NUGET_REGISTRY"
env:
NUGET_REGISTRY: TrueCloudLab
NUGET_REGISTRY_URL: https://git.frostfs.info/api/packages/TrueCloudLab/nuget/index.json
NUGET_REGISTRY_USER: ${{secrets.NUGET_REGISTRY_USER}}
NUGET_REGISTRY_PASSWORD: ${{secrets.NUGET_REGISTRY_PASSWORD}}
if: >-
startsWith(github.ref, 'refs/tags/v') &&
(github.event_name == 'workflow_dispatch' || github.event_name == 'push')

8
.gitignore vendored
View file

@ -4,6 +4,7 @@
*.dll
*.so
*.dylib
*.pdb
# Test binary, built with `go test -c`
*.test
@ -17,6 +18,7 @@ vendor/
# IDE
.idea
.vscode
.vs
# coverage
coverage.txt
@ -30,4 +32,8 @@ antlr-*.jar
# binary
bin/
release/
obj/
# Repository signing keys
release/maintainer.*
release/ca.*

3
CODEOWNERS Normal file
View file

@ -0,0 +1,3 @@
.* @PavelGrossSpb
.forgejo/.* @potyarkin
Makefile @potyarkin

View file

@ -1,12 +1,20 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrostFS.SDK.ProtosV2", "src\FrostFS.SDK.ProtosV2\FrostFS.SDK.ProtosV2.csproj", "{A087047E-2505-4137-97CC-689A5AD58A0C}"
# Visual Studio Version 17
VisualStudioVersion = 17.5.002.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FrostFS.SDK.Client", "src\FrostFS.SDK.Client\FrostFS.SDK.Client.csproj", "{50D8F61F-C302-4AC9-8D8A-AB0B8C0988C3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrostFS.SDK.ClientV2", "src\FrostFS.SDK.ClientV2\FrostFS.SDK.ClientV2.csproj", "{F9CE6347-111A-4CE6-8BB2-545469838F47}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FrostFS.SDK.Cryptography", "src\FrostFS.SDK.Cryptography\FrostFS.SDK.Cryptography.csproj", "{3D804F4A-B0B2-47A5-B006-BE447BE64B50}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrostFS.SDK.Cryptography", "src\FrostFS.SDK.Cryptography\FrostFS.SDK.Cryptography.csproj", "{599E2FF8-12C0-414D-B295-4C971A0A1A63}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FrostFS.SDK.Protos", "src\FrostFS.SDK.Protos\FrostFS.SDK.Protos.csproj", "{5012EF96-9C9E-4E77-BC78-B4111EE54107}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrostFS.SDK.ModelsV2", "src\FrostFS.SDK.ModelsV2\FrostFS.SDK.ModelsV2.csproj", "{3F6E5EE6-2AAC-45BE-A5E2-B83D11F90CC3}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FrostFS.SDK.Tests", "src\FrostFS.SDK.Tests\FrostFS.SDK.Tests.csproj", "{8FDA7E0D-9C75-4874-988E-6592CD28F76C}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{2F030ACD-F87C-4E83-9A68-4CC5DF03AD90}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -14,25 +22,24 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A087047E-2505-4137-97CC-689A5AD58A0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A087047E-2505-4137-97CC-689A5AD58A0C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A087047E-2505-4137-97CC-689A5AD58A0C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A087047E-2505-4137-97CC-689A5AD58A0C}.Release|Any CPU.Build.0 = Release|Any CPU
{F9CE6347-111A-4CE6-8BB2-545469838F47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F9CE6347-111A-4CE6-8BB2-545469838F47}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F9CE6347-111A-4CE6-8BB2-545469838F47}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F9CE6347-111A-4CE6-8BB2-545469838F47}.Release|Any CPU.Build.0 = Release|Any CPU
{B04EBDF5-A446-49D6-B1D7-7D729D94E8E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B04EBDF5-A446-49D6-B1D7-7D729D94E8E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B04EBDF5-A446-49D6-B1D7-7D729D94E8E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B04EBDF5-A446-49D6-B1D7-7D729D94E8E0}.Release|Any CPU.Build.0 = Release|Any CPU
{599E2FF8-12C0-414D-B295-4C971A0A1A63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{599E2FF8-12C0-414D-B295-4C971A0A1A63}.Debug|Any CPU.Build.0 = Debug|Any CPU
{599E2FF8-12C0-414D-B295-4C971A0A1A63}.Release|Any CPU.ActiveCfg = Release|Any CPU
{599E2FF8-12C0-414D-B295-4C971A0A1A63}.Release|Any CPU.Build.0 = Release|Any CPU
{3F6E5EE6-2AAC-45BE-A5E2-B83D11F90CC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3F6E5EE6-2AAC-45BE-A5E2-B83D11F90CC3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3F6E5EE6-2AAC-45BE-A5E2-B83D11F90CC3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3F6E5EE6-2AAC-45BE-A5E2-B83D11F90CC3}.Release|Any CPU.Build.0 = Release|Any CPU
{50D8F61F-C302-4AC9-8D8A-AB0B8C0988C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{50D8F61F-C302-4AC9-8D8A-AB0B8C0988C3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{50D8F61F-C302-4AC9-8D8A-AB0B8C0988C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{50D8F61F-C302-4AC9-8D8A-AB0B8C0988C3}.Release|Any CPU.Build.0 = Release|Any CPU
{3D804F4A-B0B2-47A5-B006-BE447BE64B50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3D804F4A-B0B2-47A5-B006-BE447BE64B50}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3D804F4A-B0B2-47A5-B006-BE447BE64B50}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3D804F4A-B0B2-47A5-B006-BE447BE64B50}.Release|Any CPU.Build.0 = Release|Any CPU
{5012EF96-9C9E-4E77-BC78-B4111EE54107}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5012EF96-9C9E-4E77-BC78-B4111EE54107}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5012EF96-9C9E-4E77-BC78-B4111EE54107}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5012EF96-9C9E-4E77-BC78-B4111EE54107}.Release|Any CPU.Build.0 = Release|Any CPU
{8FDA7E0D-9C75-4874-988E-6592CD28F76C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8FDA7E0D-9C75-4874-988E-6592CD28F76C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8FDA7E0D-9C75-4874-988E-6592CD28F76C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8FDA7E0D-9C75-4874-988E-6592CD28F76C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

58
Makefile Normal file
View file

@ -0,0 +1,58 @@
DOTNET?=dotnet
DOCKER?=docker
NUGET_REGISTRY?=TrueCloudLab
NUGET_REGISTRY_URL?=https://git.frostfs.info/api/packages/TrueCloudLab/nuget/index.json
NUGET_REGISTRY_USER?=
NUGET_REGISTRY_PASSWORD?=
NUPKG=find -iname '*.nupkg' | grep . | xargs -d'\n' -t -r -n1
RFC3161_TSA?=http://timestamp.digicert.com
.PHONY: build
build:
$(DOTNET) build
.PHONY: sign
sign: export NUGET_CERT_REVOCATION_MODE=offline
sign: release/maintainer.pfx
$(NUPKG) $(DOTNET) nuget sign --overwrite --certificate-path $< --timestamper "$(RFC3161_TSA)"
@rm -v "$<" # maintainer.pfx is not password protected and must be ephemeral
$(NUPKG) $(DOTNET) nuget verify
.PHONY: publish
publish:
$(NUPKG) $(DOTNET) nuget verify
$(NUPKG) $(DOTNET) nuget push --source "$(NUGET_REGISTRY)"
.PHONY: nuget-registry
nuget-registry:
ifeq (,$(NUGET_REGISTRY_USER))
$(error NUGET_REGISTRY_USER not set)
endif
ifeq (,$(NUGET_REGISTRY_PASSWORD))
$(error NUGET_REGISTRY_PASSWORD not set)
endif
$(DOTNET) nuget add source \
--name "$(NUGET_REGISTRY)" \
--username "$(NUGET_REGISTRY_USER)" \
--password "$(NUGET_REGISTRY_PASSWORD)" \
--store-password-in-clear-text \
"$(NUGET_REGISTRY_URL)"
.PHONY: clean
clean:
-$(NUPKG) rm -v
.PHONY: container
container:
$(DOCKER) run --pull=always --rm -it -v "$$PWD:/src" -w /src git.frostfs.info/truecloudlab/env:dotnet-8.0
include release/codesign.mk

102
README.md
View file

@ -21,60 +21,62 @@ neo-go wallet export -w <path_to_your_wallet> -d <address_from_p1>
### Container
```csharp
using FrostFS.SDK;
using FrostFS.SDK.ClientV2;
using FrostFS.SDK.ModelsV2;
using FrostFS.SDK.ModelsV2.Enums;
using FrostFS.SDK.ModelsV2.Netmap;
var fsClient = new Client(<your_key>, <your_host>);
using Microsoft.Extensions.Options;
// List containers
var containersIds = await fsClient.ListContainersAsync();
var Key = "KwHDAJ66o8FoLBjVbjP2sWBmgBMGjt7Vv4boA7xQrBoAYBE397Aq";
var Host = "http://172.22.33.44:8080";
// Create container
var placementPolicy = new PlacementPolicy(true, new Replica(1));
var containerId = await fsClient.CreateContainerAsync(
new Container(
BasicAcl.PublicRW,
placementPolicy
)
);
var options = Options.Create(new SingleOwnerClientSettings
{
Key = Key,
Host = Host
});
// Get container
var container = await fsClient.GetContainerAsync(cId);
using var client = Client.GetSingleOwnerInstance(options);
// Delete container
await fsClient.DeleteContainerAsync(containerId);
await foreach (var cid in client.ListContainersAsync())
{
await client.DeleteContainerAsync(new PrmContainerDelete(cid));
}
var placementPolicy = new FrostFsPlacementPolicy(true, new FrostFsReplica(1));
var createContainerParam = new PrmContainerCreate(
new FrostFsContainerInfo(BasicAcl.PublicRW, new FrostFsPlacementPolicy(true, new FrostFsReplica(1))));
var containerId = await client.PutContainerAsync(createContainerParam);
using var fileStream = File.OpenRead(@"cat.jpeg");
var param = new PrmObjectPut
{
Header = new FrostFsObjectHeader(
containerId: containerId,
type: FrostFsObjectType.Regular,
[new FrostFsAttribute("fileName", "test")]),
Payload = fileStream
};
FrostFsObjectId objectId = await client.PutObjectAsync(param);
var filter = new FilterByAttribute(FrostFsMatchType.Equals, "fileName", "test");
await foreach (var objId in client.SearchObjectsAsync(new PrmObjectSearch(containerId) { Filters = [filter] }))
{
var objHeader = await client.GetObjectHeadAsync(new PrmObjectHeadGet(containerId, objId));
}
var @object = await client.GetObjectAsync(new PrmObjectGet(containerId, objectId));
var downloadedBytes = new byte[@object.Header.PayloadLength];
MemoryStream ms = new(downloadedBytes);
ReadOnlyMemory<byte>? chunk = null;
while ((chunk = await @object.ObjectReader!.ReadChunk()) != null)
{
ms.Write(chunk.Value.Span);
}
```
### Object
```csharp
using FrostFS.SDK.ClientV2;
using FrostFS.SDK.ModelsV2;
using FrostFS.SDK.ModelsV2.Enums;
using FrostFS.SDK.ModelsV2.Netmap;
var fsClient = new Client(<your_key>, <your_host>);
// Search regular objects
var objectsIds = await fsClient.SearchObjectAsync(
cId,
ObjectFilter.RootFilter()
);
// Put object
var f = File.OpenRead("cat.jpg");
var cat = new ObjectHeader(
containerId: cId,
type: ObjectType.Regular,
new ObjectAttribute("Filename", "cat.jpg")
);
var oId = await fsClient.PutObjectAsync(cat, f);
// Get object header
var objHeader = await fsClient.GetObjectHeadAsync(cId, oId);
// Get object
var obj = await fsClient.GetObjectAsync(cId, oId);
```

82
release/README.md Normal file
View file

@ -0,0 +1,82 @@
# Release process
## Preparing release
_TBD_
## Trusting TrueCloudLab code signing CA certificate
Verifying signatures (and signing) TrueCloudLab packages requires adding
[TrueCloudLab Code Signing CA](ca.cert) to the list of trusted roots.
On Linux this can be done by appending [release/ca.cert](ca.cert) to one of:
- `/etc/pki/ca-trust/extracted/pem/objsign-ca-bundle.pem`: compatible with
[update-ca-trust] and originally proposed in [.NET design docs]
- `…/dotnet/sdk/X.Y.ZZZ/trustedroots/codesignctl.pem`: [fallback] codesigning certificate trust list for .NET
[update-ca-trust]: https://www.linux.org/docs/man8/update-ca-trust.html
[.NET design docs]: https://github.com/dotnet/designs/blob/main/accepted/2021/signed-package-verification/re-enable-signed-package-verification-technical.md#linux
[fallback]: https://github.com/dotnet/sdk/blob/11150c0ec9020625308edeec555a8b78dbfb2aa5/src/Layout/redist/trustedroots/README.md
## Signing Nuget packages
Repository maintainer places `maintainer.cert` and `maintainer.key` (see below
regarding obtaining these files) into `release/` directory and then
executes:
```console
$ make build sign
```
## Uploading packages to Nuget registry
**IMPORTANT: the following steps upload all `*.nupkg` files located under
`src/`. Maintainer MUST make sure that no unnecessary package versions will be
uploaded to the registry.**
Configure registry credentials (once per machine):
```console
$ make nuget-registry NUGET_REGISTRY_USER=username NUGET_REGISTRY_PASSWORD=token
```
Publish all locally built packages (implicitly clear existing `*.nupkg` and
rebuild current version only):
```console
$ make clean build sign publish
```
## Obtaining release signing certificate
Repository maintainer owns and keeps safe the release signing key
(`maintainer.key`). Private key should never leave maintainer's machine and
should be considered a highly sensitive secret.
- Generating new maintainer key and the corresponding CSR:
```console
$ make maintainer.csr
...lines skipped...
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:
-----
IMPORTANT: Keep maintainer.key private!
Certificate signing request is ready.
Send maintainer.csr to CA administrator to obtain the certificate.
```
Resulting CSR (`maintainer.csr`) does not contain any sensitive
cryptographic material and may be passed to CA administrator through regular
communication channels.
- CA administrator then issues the certificate (`make maintainer.cert`) and
sends it back to the maintainer to be used in combination with
`maintainer.key`
This procedure should be repeated once per machine per `maintainer.cert`
lifetime (1 year) - typically just once per year since we expect the
maintainer to use only a single computer to sign releases.

34
release/ca.cert Normal file
View file

@ -0,0 +1,34 @@
-----BEGIN CERTIFICATE-----
MIIF2zCCA8OgAwIBAgIUa9xC/RgvFtUG/xeR016nn0B4K0YwDQYJKoZIhvcNAQEL
BQAwdTELMAkGA1UEBhMCUlUxFTATBgNVBAoMDFRydWVDbG91ZExhYjEVMBMGA1UE
CwwMVHJ1ZUNsb3VkTGFiMTgwNgYDVQQDDC9UcnVlQ2xvdWRMYWIgQ29kZSBTaWdu
aW5nIENlcnRpZmljYXRlIEF1dGhvcml0eTAeFw0yNTA0MTAxNTI2MTFaFw0zNTA0
MDgxNTI2MTFaMHUxCzAJBgNVBAYTAlJVMRUwEwYDVQQKDAxUcnVlQ2xvdWRMYWIx
FTATBgNVBAsMDFRydWVDbG91ZExhYjE4MDYGA1UEAwwvVHJ1ZUNsb3VkTGFiIENv
ZGUgU2lnbmluZyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEB
AQUAA4ICDwAwggIKAoICAQCyANB4cjf+ZEAFx9RiUYXCAOPMV+jyqgcVbhzh2YKc
9SlvGKRlc00Ar1RlFcrycdkIrTKmhhobiWsFp7UgphwLqRTCb5NB6qfUoWhnfiD9
m0OBgeVX5wivVaibRI9PSTbFDcIhYUiNvwFJ6GduH/9zOxf1BvuL7LMaoyhIDcg/
XVLuekE2lnX83zsedv0v/2jyyMY9Ct6N2BXzyHSAzSdYYg0F9Qu9fIMAPjoKhWPc
PnotqaACjb1DScLUr3E/o2W1FfprTT2Pip/0AXxO4wixl4QWh9HeOKV22KRcCHo6
3wNdg5q1ZVGTNBW0+yoB4jsSG8/JM+2Ujhc1ZnYH10armvGq/0Oc2YQE00960Wy8
t0drCFWJUO1XHNeBxkkupmj7N1TAPbixtfiGZJhECOWOJwyMpcKixlt5P0cNH4N/
p3vjyrGQxGLBIkgV/QgjfGkpTHKT1/H40YK6DliWJc01KfNTqn0K+/TIyF0n26kD
BWYVlvDh5P1+V9DGuD2zeXB3PstoifD/Pd7D8wuqpm17noFE19MLp94xv03q9nEa
jRMEd2J2buTLvMh5BBVH0Sm38QAHpSIZ9O3dSLvvjlALbVtwmcsNE9fgxiue3vTB
iXNW8wWs+/DMYwbWyBoCwORxVOdOyc1JLn7qAAEUBweilPVXpWuzMLdUsifPiqrV
dQIDAQABo2MwYTAdBgNVHQ4EFgQUEz4y/RvQMmbUFvf5JbGe/0PZR90wHwYDVR0j
BBgwFoAUEz4y/RvQMmbUFvf5JbGe/0PZR90wDwYDVR0TAQH/BAUwAwEB/zAOBgNV
HQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADggIBAF79W9hMGnrKUWLDOcoeXDEn
+gZxd5rjeF0tNEQGbWKGTJAGQdprOkzWQ47PewpFeS+ePpjOglBAt7cV2ZnOugAT
Brx31vYpNAvYnUCYn+/IUqD8S/U4uErVS9zG9QjirZWo56eJP62vnScKuApCQCbA
pp0zrIyJ+2lQKzlMOENRqzYYA/UTOqYTtnW6x2D8goVqCmDXpgpxEp5XliFjJSr6
dOjiopNWMvaV3R/Bnd4i41taM7M6HpIV+gbXmEKEFS0ejVfzT8z1pTigN7GBqbxf
nXD03eLUIsbMDv4ZQPrheN7nKnjRUn8kxz0SSK1m2YDrXW51m8fOs6aTvwC/cNe+
FJMqQMF32i4IXVfUbyUJi+JMvawqm2wEY46vrh7siprY6rXsAzCKJo07i6jvUwnT
TXMldSCPgTEqzT2JBzzr0tRfuPKsv0/NqflHvwfuMRCpcZ7jJZ700iN92xXkiQHP
DmCZOILXcNclAth3nAnyY4XE5a8myv8bwYaPdJdIFlV+BoU/8mClDeA8ck4rDy12
T5YChKew2oiL4j4B6v9/yrDjD1IT0gv4BWyPhb/n390BCEXt8g9auNcT0s6O8kEc
VUDVc1519ocMCuWVuqUK9o2w0zu50/pBn4hVLfT3QyW8sqtlRKghOWtqZzigvCWF
VjATeO5F/Z7OSDebHUGv
-----END CERTIFICATE-----

74
release/codesign.mk Normal file
View file

@ -0,0 +1,74 @@
PKI_ROLE?=maintainer
PKI_DIR?=release
# Note: Only RSA signatures are supported (NU3013)
# https://learn.microsoft.com/en-us/nuget/reference/errors-and-warnings/nu3013)
ifeq ($(PKI_ROLE),maintainer)
.PHONY: maintainer.csr
maintainer.csr: $(PKI_DIR)/maintainer.csr
$(PKI_DIR)/maintainer.csr: KEY=$(patsubst %.csr,%.key,$@)
$(PKI_DIR)/maintainer.csr:
openssl req \
-new \
-newkey rsa:4096 \
-keyout $(KEY) \
-out $@ \
-sha256 \
-addext keyUsage=critical,digitalSignature \
-addext extendedKeyUsage=critical,codeSigning,msCodeCom \
-subj "/C=RU/O=TrueCloudLab/OU=TrueCloudLab/CN=frostfs-sdk-csharp Release Team"
@echo "IMPORTANT: Keep $(KEY) private!\n"
@echo "Certificate signing request is ready.\nSend $@ to CA administrator to obtain the certificate."
$(PKI_DIR)/maintainer.pfx: $(PKI_DIR)/maintainer.cert $(PKI_DIR)/maintainer.key $(PKI_DIR)/ca.cert
openssl verify \
-CAfile $(PKI_DIR)/ca.cert \
$(PKI_DIR)/maintainer.cert
openssl pkcs12 \
-export \
-out $@ \
-inkey $(PKI_DIR)/maintainer.key \
-in $(PKI_DIR)/maintainer.cert \
-CAfile $(PKI_DIR)/ca.cert \
-chain \
-passout pass:
endif
ifeq ($(PKI_ROLE),ca)
.PHONY: maintainer.cert
maintainer.cert: $(PKI_DIR)/maintainer.cert
$(PKI_DIR)/maintainer.cert: CSR=$(patsubst %.cert,%.csr,$@)
$(PKI_DIR)/maintainer.cert: $(PKI_DIR)/ca.key $(PKI_DIR)/ca.cert
openssl req -noout -text -in $(CSR)
@read -p "Review the CSR above. Press Enter to continue, Ctrl+C to cancel " -r null
openssl x509 \
-req \
-days 365 \
-in $(CSR) \
-copy_extensions copy \
-ext keyUsage,extendedKeyUsage \
-CA $(PKI_DIR)/ca.cert \
-CAkey $(PKI_DIR)/ca.key \
-CAcreateserial \
-out $@
echo >> $@
cat $(PKI_DIR)/ca.cert >> $@
openssl x509 -noout -text -in $@ -fingerprint -sha256
@echo "Certificate is ready.\nSend $@ back to maintainer."
$(PKI_DIR)/ca.key: CERT=$(patsubst %.key,%.cert,$@)
$(PKI_DIR)/ca.key:
openssl req \
-x509 \
-newkey rsa:4096 \
-keyout $@ \
-out $(CERT) \
-sha256 \
-days 3650 \
-addext keyUsage=critical,keyCertSign \
-subj "/C=RU/O=TrueCloudLab/OU=TrueCloudLab/CN=TrueCloudLab Code Signing Certificate Authority"
@echo "IMPORTANT: Keep $@ private!\n"
endif

View file

@ -0,0 +1,36 @@
namespace FrostFS.SDK.Client;
public struct Actions(bool inverted, string[] names) : System.IEquatable<Actions>
{
public bool Inverted { get; set; } = inverted;
public string[] Names { get; set; } = names;
public override readonly bool Equals(object obj)
{
if (obj == null || obj is not Actions)
return false;
return Equals((Actions)obj);
}
public override readonly int GetHashCode()
{
return Inverted.GetHashCode() ^ string.Join(string.Empty, Names).GetHashCode();
}
public static bool operator ==(Actions left, Actions right)
{
return left.Equals(right);
}
public static bool operator !=(Actions left, Actions right)
{
return !(left == right);
}
public readonly bool Equals(Actions other)
{
return this.GetHashCode().Equals(other.GetHashCode());
}
}

View file

@ -0,0 +1,63 @@
using System;
using Frostfs.V2.Ape;
namespace FrostFS.SDK.Client;
public struct FrostFsChainTarget(FrostFsTargetType type, string name) : IEquatable<FrostFsChainTarget>
{
private ChainTarget? chainTarget;
public FrostFsTargetType Type { get; } = type;
public string Name { get; } = name;
internal ChainTarget GetChainTarget()
{
return chainTarget ??= new ChainTarget
{
Type = GetTargetType(Type),
Name = Name
};
}
private static TargetType GetTargetType(FrostFsTargetType type)
{
return type switch
{
FrostFsTargetType.Undefined => TargetType.Undefined,
FrostFsTargetType.Namespace => TargetType.Namespace,
FrostFsTargetType.Container => TargetType.Container,
FrostFsTargetType.User => TargetType.User,
FrostFsTargetType.Group => TargetType.Group,
_ => throw new ArgumentException("Unexpected value for TargetType", nameof(type)),
};
}
public override readonly bool Equals(object obj)
{
if (obj == null || obj is not FrostFsChainTarget)
return false;
return Equals((FrostFsChainTarget)obj);
}
public readonly bool Equals(FrostFsChainTarget other)
{
return Type == other.Type && Name.Equals(other.Name, StringComparison.Ordinal);
}
public override readonly int GetHashCode()
{
return Name.GetHashCode() ^ (int)Type;
}
public static bool operator ==(FrostFsChainTarget left, FrostFsChainTarget right)
{
return left.Equals(right);
}
public static bool operator !=(FrostFsChainTarget left, FrostFsChainTarget right)
{
return !(left == right);
}
}

View file

@ -0,0 +1,43 @@
namespace FrostFS.SDK.Client;
public struct Condition : System.IEquatable<Condition>
{
public ConditionType Op { get; set; }
public ConditionKindType Kind { get; set; }
public string? Key { get; set; }
public string? Value { get; set; }
public override bool Equals(object obj)
{
if (obj == null || obj is not Condition)
return false;
return Equals((Condition)obj);
}
public override readonly int GetHashCode()
{
return Op.GetHashCode()
^ Kind.GetHashCode()
^ (Key != null ? Key.GetHashCode() : 0)
^ (Value != null ? Value.GetHashCode() : 0);
}
public static bool operator ==(Condition left, Condition right)
{
return left.Equals(right);
}
public static bool operator !=(Condition left, Condition right)
{
return !(left == right);
}
public readonly bool Equals(Condition other)
{
return this.GetHashCode().Equals(other.GetHashCode());
}
}

View file

@ -0,0 +1,7 @@
namespace FrostFS.SDK.Client;
public enum ConditionKindType
{
Resource,
Request
}

View file

@ -0,0 +1,36 @@
namespace FrostFS.SDK.Client;
public enum ConditionType
{
CondStringEquals,
CondStringNotEquals,
CondStringEqualsIgnoreCase,
CondStringNotEqualsIgnoreCase,
CondStringLike,
CondStringNotLike,
CondStringLessThan,
CondStringLessThanEquals,
CondStringGreaterThan,
CondStringGreaterThanEquals,
// Numeric condition operators.
CondNumericEquals,
CondNumericNotEquals,
CondNumericLessThan,
CondNumericLessThanEquals,
CondNumericGreaterThan,
CondNumericGreaterThanEquals,
CondSliceContains,
CondIPAddress,
CondNotIPAddress,
}

View file

@ -0,0 +1,10 @@
namespace FrostFS.SDK.Client;
public enum FrostFsTargetType
{
Undefined,
Namespace,
Container,
User,
Group
}

View file

@ -0,0 +1,9 @@
namespace FrostFS.SDK.Client;
public enum RuleStatus
{
Allow,
NoRuleFound,
AccessDenied,
QuotaLimitReached
}

View file

@ -0,0 +1,10 @@
namespace FrostFS.SDK.Client;
public class FrostFsChain
{
public byte[] ID { get; set; } = [];
public FrostFsRule[] Rules { get; set; } = [];
public RuleMatchType MatchType { get; set; }
}

View file

@ -0,0 +1,18 @@
namespace FrostFS.SDK.Client;
public class FrostFsRule
{
public RuleStatus Status { get; set; }
// Actions the operation is applied to.
public Actions Actions { get; set; }
// List of the resources the operation is applied to.
public Resources Resources { get; set; }
// True iff individual conditions must be combined with the logical OR.
// By default AND is used, so _each_ condition must pass.
public bool Any { get; set; }
public Condition[]? Conditions { get; set; }
}

View file

@ -0,0 +1,10 @@
namespace FrostFS.SDK.Client;
public enum RuleMatchType
{
// DenyPriority rejects the request if any `Deny` is specified.
DenyPriority,
// FirstMatch returns the first rule action matched to the request.
FirstMatch
}

View file

@ -0,0 +1,36 @@
namespace FrostFS.SDK.Client;
public struct Resources(bool inverted, string[] names) : System.IEquatable<Resources>
{
public bool Inverted { get; set; } = inverted;
public string[] Names { get; set; } = names;
public override readonly bool Equals(object obj)
{
if (obj == null || obj is not Resources)
return false;
return Equals((Resources)obj);
}
public override readonly int GetHashCode()
{
return Inverted.GetHashCode() ^ string.Join(string.Empty, Names).GetHashCode();
}
public static bool operator ==(Resources left, Resources right)
{
return left.Equals(right);
}
public static bool operator !=(Resources left, Resources right)
{
return !(left == right);
}
public readonly bool Equals(Resources other)
{
return this.GetHashCode().Equals(other.GetHashCode());
}
}

View file

@ -0,0 +1,505 @@
using System;
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("FrostFS.SDK.Tests")]
namespace FrostFS.SDK.Client;
internal static class RuleSerializer
{
const byte Version = 0; // increase if breaking change
const int ByteSize = 1;
const int UInt8Size = ByteSize;
const int BoolSize = ByteSize;
const long NullSlice = -1;
const int NullSliceSize = 1;
const byte ByteTrue = 1;
const byte ByteFalse = 0;
/// <summary>
/// maxSliceLen taken from https://github.com/neo-project/neo/blob/38218bbee5bbe8b33cd8f9453465a19381c9a547/src/Neo/IO/Helper.cs#L77
/// </summary>
const int MaxSliceLen = 0x1000000;
const int ChainMarshalVersion = 0;
internal static byte[] Serialize(FrostFsChain chain)
{
int s = UInt8Size // Marshaller version
+ UInt8Size // Chain version
+ SliceSize(chain.ID, b => ByteSize)
+ SliceSize(chain.Rules, RuleSize)
+ UInt8Size; // MatchType
byte[] buf = new byte[s];
int offset = UInt8Marshal(buf, 0, Version);
offset = UInt8Marshal(buf, offset, ChainMarshalVersion);
offset = SliceMarshal(buf, offset, chain.ID, ByteMarshal);
offset = SliceMarshal(buf, offset, chain.Rules, MarshalRule);
offset = UInt8Marshal(buf, offset, (byte)chain.MatchType);
VerifyMarshal(buf, offset);
return buf;
}
internal static FrostFsChain Deserialize(byte[] data)
{
if (data is null)
{
throw new ArgumentNullException(nameof(data));
}
FrostFsChain chain = new();
var (offset, version) = UInt8Unmarshal(data, 0);
if (version != Version)
{
throw new FrostFsException($"unsupported marshaller version {version}");
}
(offset, byte chainVersion) = UInt8Unmarshal(data, offset);
if (chainVersion != ChainMarshalVersion)
{
throw new FrostFsException($"unsupported chain version {chainVersion}");
}
(offset, chain.ID) = SliceUnmarshal(data, offset, UInt8Unmarshal);
(offset, chain.Rules) = SliceUnmarshal(data, offset, UnmarshalRule);
(offset, var matchTypeV) = UInt8Unmarshal(data, offset);
chain.MatchType = (RuleMatchType)matchTypeV;
VerifyUnmarshal(data, offset);
return chain;
}
private static int Int64Size(long value)
{
// https://cs.opensource.google/go/go/+/master:src/encoding/binary/varint.go;l=92;drc=dac9b9ddbd5160c5f4552410f5f8281bd5eed38c
// and
// https://cs.opensource.google/go/go/+/master:src/encoding/binary/varint.go;l=41;drc=dac9b9ddbd5160c5f4552410f5f8281bd5eed38c
ulong ux = (ulong)value << 1;
if (value < 0)
{
ux = ~ux;
}
int size = 0;
while (ux >= 0x80)
{
size++;
ux >>= 7;
}
return size + 1;
}
private static int SliceSize<T>(T[] slice, Func<T, int> sizeOf)
{
if (slice == null)
{
return NullSliceSize;
}
// Assuming Int64Size is the size of the slice
var size = Int64Size(slice.Length);
foreach (var v in slice)
{
size += sizeOf(v);
}
return size;
}
private static int StringSize(string? s)
{
var len = s != null ? s.Length : 0;
return Int64Size(len) + len;
}
private static int ActionsSize(Actions action)
{
return BoolSize // Inverted
+ SliceSize(action.Names, StringSize);
}
private static int ResourcesSize(Resources resource)
{
return BoolSize // Inverted
+ SliceSize(resource.Names, StringSize);
}
private static int ConditionSize(Condition condition)
{
return ByteSize // Op
+ ByteSize // Object
+ StringSize(condition.Key)
+ StringSize(condition.Value);
}
private static int RuleSize(FrostFsRule rule)
{
if (rule is null)
{
throw new ArgumentNullException(nameof(rule));
}
return ByteSize // Status
+ ActionsSize(rule.Actions)
+ ResourcesSize(rule.Resources)
+ BoolSize // Any
+ SliceSize(rule.Conditions!, ConditionSize);
}
private static int UInt8Marshal(byte[] buf, int offset, byte value)
{
if (buf.Length - offset < 1)
{
throw new FrostFsException("Not enough bytes left to serialize value of type byte");
}
buf[offset] = value;
return offset + 1;
}
private static int ByteMarshal(byte[] buf, int offset, byte value)
{
return UInt8Marshal(buf, offset, value);
}
// PutVarint encodes an int64 into buf and returns the number of bytes written.
// If the buffer is too small, PutVarint will panic.
private static int PutVarint(byte[] buf, int offset, long x)
{
var ux = (ulong)x << 1;
if (x < 0)
{
ux = ~ux;
}
return PutUvarint(buf, offset, ux);
}
private static int PutUvarint(byte[] buf, int offset, ulong x)
{
while (x >= 0x80)
{
buf[offset] = (byte)(x | 0x80);
x >>= 7;
offset++;
}
buf[offset] = (byte)x;
return offset + 1;
}
private static int Int64Marshal(byte[] buf, int offset, long v)
{
if (buf.Length - offset < Int64Size(v))
{
throw new FrostFsException("Not enough bytes left to serialize value of type long");
}
return PutVarint(buf, offset, v);
}
private static int SliceMarshal<T>(byte[] buf, int offset, T[] slice, Func<byte[], int, T, int> marshalT)
{
if (slice == null)
{
return Int64Marshal(buf, offset, NullSlice);
}
if (slice.Length > MaxSliceLen)
{
throw new FrostFsException($"slice size if too big: {slice.Length}");
}
offset = Int64Marshal(buf, offset, slice.Length);
foreach (var v in slice)
{
offset = marshalT(buf, offset, v);
}
return offset;
}
private static int BoolMarshal(byte[] buf, int offset, bool value)
{
return UInt8Marshal(buf, offset, value ? ByteTrue : ByteFalse);
}
private static int StringMarshal(byte[] buf, int offset, string value)
{
if (value == null)
{
throw new FrostFsException($"string value is null");
}
if (value.Length > MaxSliceLen)
{
throw new FrostFsException($"string is too long: {value.Length}");
}
if (buf.Length - offset < Int64Size(value.Length) + value.Length)
{
throw new FrostFsException($"Not enough bytes left to serialize value of type string with length {value.Length}");
}
offset = Int64Marshal(buf, offset, value.Length);
if (string.IsNullOrEmpty(value))
{
return offset;
}
Buffer.BlockCopy(System.Text.Encoding.UTF8.GetBytes(value), 0, buf, offset, value.Length);
return offset + value.Length;
}
private static int MarshalActions(byte[] buf, int offset, Actions action)
{
offset = BoolMarshal(buf, offset, action.Inverted);
return SliceMarshal(buf, offset, action.Names, StringMarshal);
}
private static int MarshalCondition(byte[] buf, int offset, Condition condition)
{
offset = ByteMarshal(buf, offset, (byte)condition.Op);
offset = ByteMarshal(buf, offset, (byte)condition.Kind);
offset = StringMarshal(buf, offset, condition.Key!);
return StringMarshal(buf, offset, condition.Value!);
}
private static int MarshalRule(byte[] buf, int offset, FrostFsRule rule)
{
if (rule is null)
{
throw new ArgumentNullException(nameof(rule));
}
offset = ByteMarshal(buf, offset, (byte)rule.Status);
offset = MarshalActions(buf, offset, rule.Actions);
offset = MarshalResources(buf, offset, rule.Resources);
offset = BoolMarshal(buf, offset, rule.Any);
return SliceMarshal(buf, offset, rule.Conditions!, MarshalCondition);
}
private static int MarshalResources(byte[] buf, int offset, Resources resources)
{
offset = BoolMarshal(buf, offset, resources.Inverted);
return SliceMarshal(buf, offset, resources.Names, StringMarshal);
}
private static void VerifyMarshal(byte[] buf, int lastOffset)
{
if (buf.Length != lastOffset)
{
throw new FrostFsException("actual data size differs from expected");
}
}
private static (int, bool) BoolUnmarshal(byte[] buf, int offset)
{
(offset, byte val) = UInt8Unmarshal(buf, offset);
return (offset, val == ByteTrue);
}
private static (int, string) StringUnmarshal(byte[] buf, int offset)
{
(offset, long size) = Int64Unmarshal(buf, offset);
if (size == 0)
{
return (offset, string.Empty);
}
if (size > MaxSliceLen)
{
throw new FrostFsException($"string is too long: '{size}'");
}
if (size < 0)
{
throw new FrostFsException($"invalid string size: '{size}'");
}
if (buf.Length - offset < size)
{
throw new FrostFsException($"not enough bytes left to string value");
}
return (offset + (int)size, System.Text.Encoding.UTF8.GetString(buf, offset, (int)size));
}
private static (int, Actions) UnmarshalActions(byte[] buf, int offset)
{
Actions action = new();
(offset, action.Inverted) = BoolUnmarshal(buf, offset);
(offset, action.Names) = SliceUnmarshal(buf, offset, StringUnmarshal);
return (offset, action);
}
private static (int, Resources) UnmarshalResources(byte[] buf, int offset)
{
Resources res = new();
(offset, res.Inverted) = BoolUnmarshal(buf, offset);
(offset, res.Names) = SliceUnmarshal(buf, offset, StringUnmarshal);
return (offset, res);
}
private static (int, Condition) UnmarshalCondition(byte[] buf, int offset)
{
Condition cond = new();
(offset, var op) = UInt8Unmarshal(buf, offset);
cond.Op = (ConditionType)op;
(offset, var kind) = UInt8Unmarshal(buf, offset);
cond.Kind = (ConditionKindType)kind;
(offset, cond.Key) = StringUnmarshal(buf, offset);
(offset, cond.Value) = StringUnmarshal(buf, offset);
return (offset, cond);
}
private static (int, FrostFsRule) UnmarshalRule(byte[] buf, int offset)
{
FrostFsRule rule = new();
(offset, byte statusV) = UInt8Unmarshal(buf, offset);
rule.Status = (RuleStatus)statusV;
(offset, rule.Actions) = UnmarshalActions(buf, offset);
(offset, rule.Resources) = UnmarshalResources(buf, offset);
(offset, rule.Any) = BoolUnmarshal(buf, offset);
(offset, rule.Conditions) = SliceUnmarshal(buf, offset, UnmarshalCondition);
return (offset, rule);
}
private static (int, byte) UInt8Unmarshal(byte[] buf, int offset)
{
if (buf.Length - offset < 1)
{
throw new FrostFsException($"not enough bytes left to read a value of type 'byte' from offset {offset}");
}
return (offset + 1, buf[offset]);
}
private static (int, long) Int64Unmarshal(byte[] buf, int offset)
{
if (buf.Length - offset < sizeof(long))
{
throw new FrostFsException($"not enough bytes left to read a value of type 'long' from offset {offset}");
}
return Varint(buf, offset);
}
private static (int, T[]) SliceUnmarshal<T>(byte[] buf, int offset, Func<byte[], int, (int, T)> unmarshalT)
{
var (newOffset, size) = Varint(buf, offset);
if (size == NullSlice)
{
return (newOffset, []);
}
if (size > MaxSliceLen)
{
throw new FrostFsException($"slice size is too big: '{size}'");
}
if (size < 0)
{
throw new FrostFsException($"invalid slice size: '{size}'");
}
var result = new T[size];
for (int i = 0; i < result.Length; i++)
{
(newOffset, result[i]) = unmarshalT(buf, newOffset);
}
return (newOffset, result);
}
private static void VerifyUnmarshal(byte[] buf, int offset)
{
if (buf.Length != offset)
{
throw new FrostFsException("unmarshalled bytes left");
}
}
private static int MaxVarIntLen64 = 10;
public static (int, long) Varint(byte[] buf, int offset)
{
var (ux, n) = Uvarint(buf, offset); // ok to continue in presence of error
long x = (long)ux >> 1;
if ((ux & 1) != 0)
{
x = ~x;
}
return (n, x);
}
public static (ulong, int) Uvarint(byte[] buf, int offset)
{
ulong x = 0;
int s = 0;
for (int i = offset; i < buf.Length; i++)
{
byte b = buf[i];
if (i == MaxVarIntLen64)
{
return (0, -(i + 1)); // overflow
}
if (b < 0x80)
{
if (i == MaxVarIntLen64 - 1 && b > 1)
{
return (0, -(i + 1)); // overflow
}
return (x | ((ulong)b << s), i + 1);
}
x |= (ulong)(b & 0x7f) << s;
s += 7;
}
return (0, 0);
}
}

View file

@ -0,0 +1,8 @@
using System.Reflection;
[assembly: AssemblyCompany("FrostFS.SDK.Client")]
[assembly: AssemblyFileVersion("1.0.4.0")]
[assembly: AssemblyInformationalVersion("1.0.0+d6fe0344538a223303c9295452f0ad73681ca376")]
[assembly: AssemblyProduct("FrostFS.SDK.Client")]
[assembly: AssemblyTitle("FrostFS.SDK.Client")]
[assembly: AssemblyVersion("1.0.4")]

View file

@ -0,0 +1,13 @@
using Microsoft.Extensions.Caching.Memory;
namespace FrostFS.SDK.Client;
internal static class Caches
{
private static readonly IMemoryCache _ownersCache = new MemoryCache(new MemoryCacheOptions
{
SizeLimit = 256
});
internal static IMemoryCache Owners => _ownersCache;
}

View file

@ -0,0 +1,18 @@
using System.Security.Cryptography;
using FrostFS.SDK.Cryptography;
using Google.Protobuf;
namespace FrostFS.SDK.Client;
public class ClientKey(ECDsa key)
{
internal ECDsa ECDsaKey { get; } = key;
internal ByteString PublicKeyProto { get; } = ByteString.CopyFrom(key.PublicKey());
internal string PublicKey { get; } = Base58.Encode(key.PublicKey());
internal FrostFsOwner Owner { get; } = new FrostFsOwner(key.PublicKey().PublicKeyToAddress());
}

View file

@ -0,0 +1,18 @@
using System;
namespace FrostFS.SDK.Client;
public class FrostFsException : Exception
{
public FrostFsException()
{
}
public FrostFsException(string message) : base(message)
{
}
public FrostFsException(string message, Exception innerException) : base(message, innerException)
{
}
}

View file

@ -0,0 +1,18 @@
using System;
namespace FrostFS.SDK.Client;
public class FrostFsInvalidObjectException : FrostFsException
{
public FrostFsInvalidObjectException()
{
}
public FrostFsInvalidObjectException(string message) : base(message)
{
}
public FrostFsInvalidObjectException(string message, Exception innerException) : base(message, innerException)
{
}
}

View file

@ -0,0 +1,26 @@
using System;
namespace FrostFS.SDK.Client;
public class FrostFsResponseException : FrostFsException
{
public FrostFsResponseStatus? Status { get; private set; }
public FrostFsResponseException()
{
}
public FrostFsResponseException(FrostFsResponseStatus status)
: base(status != null ? status.Message != null ? "" : "" : "")
{
Status = status;
}
public FrostFsResponseException(string message) : base(message)
{
}
public FrostFsResponseException(string message, Exception innerException) : base(message, innerException)
{
}
}

View file

@ -0,0 +1,18 @@
using System;
namespace FrostFS.SDK.Client;
public class FrostFsStreamException : FrostFsException
{
public FrostFsStreamException()
{
}
public FrostFsStreamException(string message) : base(message)
{
}
public FrostFsStreamException(string message, Exception innerException) : base(message, innerException)
{
}
}

View file

@ -0,0 +1,18 @@
using System;
namespace FrostFS.SDK.Client;
public class SessionExpiredException : FrostFsException
{
public SessionExpiredException()
{
}
public SessionExpiredException(string message) : base(message)
{
}
public SessionExpiredException(string message, Exception innerException) : base(message, innerException)
{
}
}

View file

@ -0,0 +1,18 @@
using System;
namespace FrostFS.SDK.Client;
public class SessionNotFoundException : FrostFsException
{
public SessionNotFoundException()
{
}
public SessionNotFoundException(string message) : base(message)
{
}
public SessionNotFoundException(string message, Exception innerException) : base(message, innerException)
{
}
}

View file

@ -0,0 +1,62 @@
using System;
using System.Security.Cryptography;
using Google.Protobuf;
namespace FrostFS.SDK.Cryptography;
public static class FrostFsExtensions
{
public static byte[] Sha256(this IMessage data)
{
using var sha256 = SHA256.Create();
using HashStream stream = new(sha256);
data.WriteTo(stream);
return stream.Hash();
}
public static Guid ToUuid(this ByteString id)
{
if (id == null)
throw new ArgumentNullException(nameof(id));
return new Guid(
(id[0] << 24) + (id[1] << 16) + (id[2] << 8) + id[3],
(short)((id[4] << 8) + id[5]),
(short)((id[6] << 8) + id[7]),
id[8],
id[9],
id[10],
id[11],
id[12],
id[13],
id[14],
id[15]);
}
/// <summary>
/// Serializes Guid to binary representation in direct order bytes format
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public unsafe static void ToBytes(this Guid id, Span<byte> span)
{
var pGuid = (byte*)&id;
span[0] = pGuid[3];
span[1] = pGuid[2];
span[2] = pGuid[1];
span[3] = pGuid[0];
span[4] = pGuid[5];
span[5] = pGuid[4];
span[6] = pGuid[7];
span[7] = pGuid[6];
span[8] = pGuid[8];
span[9] = pGuid[9];
span[10] = pGuid[10];
span[11] = pGuid[11];
span[12] = pGuid[12];
span[13] = pGuid[13];
span[14] = pGuid[14];
span[15] = pGuid[15];
}
}

View file

@ -0,0 +1,56 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>12.0</LangVersion>
<Nullable>enable</Nullable>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<PackageId>FrostFS.SDK.Client</PackageId>
<Version>1.0.4</Version>
<Description>
C# SDK for FrostFS gRPC native protocol
</Description>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>
<PropertyGroup>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
</PropertyGroup>
<PropertyGroup>
<CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup>
<_SkipUpgradeNetAnalyzersNuGetWarning>true</_SkipUpgradeNetAnalyzersNuGetWarning>
</PropertyGroup>
<PropertyGroup>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>
<PropertyGroup>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="7.0.0" />
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="7.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="7.0.1" />
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="7.0.0" />
<PackageReference Include="System.Runtime.Caching" Version="7.0.0" />
<PackageReference Include="System.Text.Json" Version="7.0.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FrostFS.SDK.Cryptography\FrostFS.SDK.Cryptography.csproj" />
<ProjectReference Include="..\FrostFS.SDK.Protos\FrostFS.SDK.Protos.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,434 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FrostFS.SDK.Client.Interfaces;
using FrostFS.SDK.Client.Services;
using FrostFS.SDK.Cryptography;
using FrostFS.Session;
using Grpc.Core;
using Grpc.Core.Interceptors;
using Microsoft.Extensions.Options;
using static Frostfs.V2.Apemanager.APEManagerService;
using static FrostFS.Accounting.AccountingService;
using static FrostFS.Container.ContainerService;
using static FrostFS.Netmap.NetmapService;
using static FrostFS.Object.ObjectService;
using static FrostFS.Session.SessionService;
namespace FrostFS.SDK.Client;
public class FrostFSClient : IFrostFSClient
{
internal ContainerServiceClient? ContainerServiceClient { get; set; }
internal ContainerServiceProvider? ContainerServiceProvider { get; set; }
internal NetmapServiceClient? NetmapServiceClient { get; set; }
internal NetmapServiceProvider? NetmapServiceProvider { get; set; }
internal APEManagerServiceClient? ApeManagerServiceClient { get; set; }
internal ApeManagerServiceProvider? ApeManagerServiceProvider { get; set; }
internal SessionServiceClient? SessionServiceClient { get; set; }
internal SessionServiceProvider? SessionServiceProvider { get; set; }
internal ObjectServiceClient? ObjectServiceClient { get; set; }
internal ObjectServiceProvider? ObjectServiceProvider { get; set; }
internal AccountingServiceClient? AccountingServiceClient { get; set; }
internal AccountingServiceProvider? AccountingServiceProvider { get; set; }
internal ClientContext ClientCtx { get; set; }
public static IFrostFSClient GetInstance(IOptions<ClientSettings> clientOptions, Func<string, ChannelBase> grpcChannelFactory)
{
if (clientOptions is null)
{
throw new ArgumentNullException(nameof(clientOptions));
}
if (grpcChannelFactory is null)
{
throw new ArgumentNullException(nameof(grpcChannelFactory));
}
return new FrostFSClient(clientOptions, grpcChannelFactory);
}
/// <summary>
/// For test only. Provide custom implementation or mock object to inject required logic instead of internal gRPC client.
/// </summary>
/// <param name="clientOptions">Global setting for client</param>
/// <param name="channelOptions">Setting for gRPC channel</param>
/// <param name="containerService">ContainerService.ContainerServiceClient implementation</param>
/// <param name="netmapService">Netmap.NetmapService.NetmapServiceClient implementation</param>
/// <param name="sessionService">Session.SessionService.SessionServiceClient implementation</param>
/// <param name="objectService">Object.ObjectService.ObjectServiceClient implementation</param>
/// <returns></returns>
public static IFrostFSClient GetTestInstance(
IOptions<ClientSettings> settings,
Func<string, ChannelBase> grpcChannelFactory,
NetmapServiceClient netmapService,
SessionServiceClient sessionService,
ContainerServiceClient containerService,
ObjectServiceClient objectService)
{
if (settings is null)
{
throw new ArgumentNullException(nameof(settings));
}
if (grpcChannelFactory is null)
{
throw new ArgumentNullException(nameof(grpcChannelFactory));
}
if (netmapService is null)
{
throw new ArgumentNullException(nameof(netmapService));
}
if (sessionService is null)
{
throw new ArgumentNullException(nameof(sessionService));
}
if (containerService is null)
{
throw new ArgumentNullException(nameof(containerService));
}
if (objectService is null)
{
throw new ArgumentNullException(nameof(objectService));
}
return new FrostFSClient(
settings, channel: grpcChannelFactory(settings.Value.Host), containerService, netmapService, sessionService, objectService);
}
private FrostFSClient(
IOptions<ClientSettings> settings,
ChannelBase channel,
ContainerServiceClient containerService,
NetmapServiceClient netmapService,
SessionServiceClient sessionService,
ObjectServiceClient objectService)
{
if (settings is null)
{
throw new ArgumentNullException(nameof(settings));
}
var ecdsaKey = settings.Value.Key.LoadWif();
ClientCtx = new ClientContext(
client: this,
key: new ClientKey(ecdsaKey),
owner: FrostFsOwner.FromKey(ecdsaKey),
channel: channel,
version: new FrostFsVersion(2, 13))
{
SessionCache = new SessionCache(0),
Callback = settings.Value.Callback,
Interceptors = settings.Value.Interceptors
};
ContainerServiceClient = containerService ?? throw new ArgumentNullException(nameof(containerService));
NetmapServiceClient = netmapService ?? throw new ArgumentNullException(nameof(netmapService));
SessionServiceClient = sessionService ?? throw new ArgumentNullException(nameof(sessionService));
ObjectServiceClient = objectService ?? throw new ArgumentNullException(nameof(objectService));
}
private FrostFSClient(IOptions<ClientSettings> settings, Func<string, ChannelBase> grpcChannelFactory)
{
var clientSettings = (settings?.Value) ?? throw new ArgumentNullException(nameof(settings), "Options value must be initialized");
clientSettings.Validate();
var ecdsaKey = clientSettings.Key.LoadWif();
ClientCtx = new ClientContext(
this,
key: new ClientKey(ecdsaKey),
owner: FrostFsOwner.FromKey(ecdsaKey),
channel: grpcChannelFactory(settings.Value.Host),
version: new FrostFsVersion(2, 13))
{
SessionCache = new SessionCache(0),
Callback = settings.Value.Callback,
Interceptors = settings.Value.Interceptors
};
}
#region ApeManagerImplementation
public Task<ReadOnlyMemory<byte>> AddChainAsync(PrmApeChainAdd args, CallContext ctx)
{
return GetApeManagerService().AddChainAsync(args, ctx);
}
public Task RemoveChainAsync(PrmApeChainRemove args, CallContext ctx)
{
return GetApeManagerService().RemoveChainAsync(args, ctx);
}
public Task<FrostFsChain[]> ListChainAsync(PrmApeChainList args, CallContext ctx)
{
return GetApeManagerService().ListChainAsync(args, ctx);
}
#endregion
#region ContainerImplementation
public Task<FrostFsContainerInfo> GetContainerAsync(PrmContainerGet args, CallContext ctx)
{
return GetContainerService().GetContainerAsync(args, ctx);
}
public IAsyncEnumerable<FrostFsContainerId> ListContainersAsync(PrmContainerGetAll args, CallContext ctx)
{
return GetContainerService().ListContainersAsync(args, ctx);
}
[Obsolete("Use PutContainerAsync method")]
public Task<FrostFsContainerId> CreateContainerAsync(PrmContainerCreate args, CallContext ctx)
{
return GetContainerService().PutContainerAsync(args, ctx);
}
public Task<FrostFsContainerId> PutContainerAsync(PrmContainerCreate args, CallContext ctx)
{
return GetContainerService().PutContainerAsync(args, ctx);
}
public Task DeleteContainerAsync(PrmContainerDelete args, CallContext ctx)
{
return GetContainerService().DeleteContainerAsync(args, ctx);
}
#endregion
#region NetworkImplementation
public Task<FrostFsNetmapSnapshot> GetNetmapSnapshotAsync(CallContext ctx)
{
return GetNetmapService().GetNetmapSnapshotAsync(ctx);
}
public Task<FrostFsNodeInfo> GetNodeInfoAsync(CallContext ctx)
{
return GetNetmapService().GetLocalNodeInfoAsync(ctx);
}
public Task<NetworkSettings> GetNetworkSettingsAsync(CallContext ctx)
{
return GetNetmapService().GetNetworkSettingsAsync(ctx);
}
#endregion
#region ObjectImplementation
public Task<FrostFsHeaderResult> GetObjectHeadAsync(PrmObjectHeadGet args, CallContext ctx)
{
return GetObjectService().GetObjectHeadAsync(args, ctx);
}
public Task<FrostFsObject> GetObjectAsync(PrmObjectGet args, CallContext ctx)
{
return GetObjectService().GetObjectAsync(args, ctx);
}
public Task<RangeReader> GetRangeAsync(PrmRangeGet args, CallContext ctx)
{
return GetObjectService().GetRangeAsync(args, ctx);
}
public Task<ReadOnlyMemory<byte>[]> GetRangeHashAsync(PrmRangeHashGet args, CallContext ctx)
{
return GetObjectService().GetRangeHashAsync(args, ctx);
}
public Task<IObjectWriter> PutObjectAsync(PrmObjectPut args, CallContext ctx)
{
return GetObjectService().PutStreamObjectAsync(args, ctx);
}
public Task<FrostFsObjectId> PutClientCutObjectAsync(PrmObjectClientCutPut args, CallContext ctx)
{
return GetObjectService().PutClientCutSingleObjectAsync(args, ctx);
// return GetObjectService().PutClientCutObjectAsync(args, ctx);
}
public Task<FrostFsObjectId> PutSingleObjectAsync(PrmSingleObjectPut args, CallContext ctx)
{
return GetObjectService().PutSingleObjectAsync(args, ctx);
}
public Task<FrostFsObjectId> PatchObjectAsync(PrmObjectPatch args, CallContext ctx)
{
return GetObjectService().PatchObjectAsync(args, ctx);
}
public Task DeleteObjectAsync(PrmObjectDelete args, CallContext ctx)
{
return GetObjectService().DeleteObjectAsync(args, ctx);
}
public IAsyncEnumerable<FrostFsObjectId> SearchObjectsAsync(PrmObjectSearch args, CallContext ctx)
{
return GetObjectService().SearchObjectsAsync(args, ctx);
}
#endregion
#region Session Implementation
public async Task<FrostFsSessionToken> CreateSessionAsync(PrmSessionCreate args, CallContext ctx)
{
var token = await CreateSessionInternalAsync(args, ctx).ConfigureAwait(false);
return new FrostFsSessionToken(token);
}
internal Task<SessionToken> CreateSessionInternalAsync(PrmSessionCreate args, CallContext ctx)
{
var service = GetSessionService();
return service.CreateSessionAsync(args, ctx);
}
#endregion
#region Accounting Implementation
public async Task<Accounting.Decimal> GetBalanceAsync(CallContext ctx)
{
return await GetAccouningService().GetBallance(ctx).ConfigureAwait(false);
}
#endregion
private CallInvoker? CreateInvoker()
{
CallInvoker? callInvoker = null;
if (ClientCtx.Interceptors != null)
{
foreach (var interceptor in ClientCtx.Interceptors)
callInvoker = AddInvoker(callInvoker, interceptor);
}
if (ClientCtx.Callback != null)
callInvoker = AddInvoker(callInvoker, new MetricsInterceptor(ClientCtx.Callback));
if (ClientCtx.PoolErrorHandler != null)
callInvoker = AddInvoker(callInvoker, new ErrorInterceptor(ClientCtx.PoolErrorHandler));
return callInvoker;
CallInvoker AddInvoker(CallInvoker? callInvoker, Interceptor interceptor)
{
if (callInvoker == null)
callInvoker = ClientCtx.Channel.Intercept(interceptor);
else
callInvoker = callInvoker.Intercept(interceptor);
return callInvoker;
}
}
private NetmapServiceProvider GetNetmapService()
{
if (NetmapServiceProvider == null)
{
var invoker = CreateInvoker();
NetmapServiceClient ??= (
invoker != null
? new NetmapServiceClient(invoker)
: new NetmapServiceClient(ClientCtx.Channel));
NetmapServiceProvider = new NetmapServiceProvider(NetmapServiceClient, ClientCtx);
}
return NetmapServiceProvider;
}
private SessionServiceProvider GetSessionService()
{
if (SessionServiceProvider == null)
{
var invoker = CreateInvoker();
SessionServiceClient ??= (
invoker != null
? new SessionServiceClient(invoker)
: new SessionServiceClient(ClientCtx.Channel));
SessionServiceProvider = new SessionServiceProvider(SessionServiceClient, ClientCtx);
}
return SessionServiceProvider;
}
private ApeManagerServiceProvider GetApeManagerService()
{
if (ApeManagerServiceProvider == null)
{
var invoker = CreateInvoker();
ApeManagerServiceClient ??= (
invoker != null
? new APEManagerServiceClient(invoker)
: new APEManagerServiceClient(ClientCtx.Channel));
ApeManagerServiceProvider = new ApeManagerServiceProvider(ApeManagerServiceClient, ClientCtx);
}
return ApeManagerServiceProvider;
}
private AccountingServiceProvider GetAccouningService()
{
if (this.AccountingServiceProvider == null)
{
var invoker = CreateInvoker();
AccountingServiceClient ??= (
invoker != null
? new AccountingServiceClient(invoker)
: new AccountingServiceClient(ClientCtx.Channel));
AccountingServiceProvider = new AccountingServiceProvider(AccountingServiceClient, ClientCtx);
}
return AccountingServiceProvider;
}
private ContainerServiceProvider GetContainerService()
{
if (this.ContainerServiceProvider == null)
{
var invoker = CreateInvoker();
ContainerServiceClient ??= (
invoker != null
? new ContainerServiceClient(invoker)
: new ContainerServiceClient(ClientCtx.Channel));
ContainerServiceProvider = new ContainerServiceProvider(ContainerServiceClient, ClientCtx);
}
return ContainerServiceProvider;
}
private ObjectServiceProvider GetObjectService()
{
if (this.ObjectServiceProvider == null)
{
var invoker = CreateInvoker();
ObjectServiceClient ??= (
invoker != null
? new ObjectServiceClient(invoker)
: new ObjectServiceClient(ClientCtx.Channel));
ObjectServiceProvider = new ObjectServiceProvider(ObjectServiceClient, ClientCtx);
}
return ObjectServiceProvider;
}
}

View file

@ -0,0 +1,8 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "<Pending>", Scope = "member", Target = "~M:FrostFS.SDK.Client.Sampler.Next~System.Int32")]

View file

@ -0,0 +1,68 @@
using System;
using System.Threading.Tasks;
using Grpc.Core;
using Grpc.Core.Interceptors;
namespace FrostFS.SDK.Client;
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods",
Justification = "parameters are provided by GRPC infrastructure")]
public class ErrorInterceptor(Action<Exception> handler) : Interceptor
{
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
TRequest request,
ClientInterceptorContext<TRequest, TResponse> context,
AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
{
var call = continuation(request, context);
return new AsyncUnaryCall<TResponse>(
HandleUnaryResponse(call),
call.ResponseHeadersAsync,
call.GetStatus,
call.GetTrailers,
call.Dispose);
}
public override AsyncClientStreamingCall<TRequest, TResponse> AsyncClientStreamingCall<TRequest, TResponse>(
ClientInterceptorContext<TRequest, TResponse> context,
AsyncClientStreamingCallContinuation<TRequest, TResponse> continuation)
{
var call = continuation(context);
return new AsyncClientStreamingCall<TRequest, TResponse>(
call.RequestStream,
HandleStreamResponse(call),
call.ResponseHeadersAsync,
call.GetStatus,
call.GetTrailers,
call.Dispose);
}
private async Task<TResponse> HandleUnaryResponse<TResponse>(AsyncUnaryCall<TResponse> call)
{
try
{
return await call;
}
catch (Exception ex)
{
handler(ex);
throw;
}
}
private async Task<TResponse> HandleStreamResponse<TRequest, TResponse>(AsyncClientStreamingCall<TRequest, TResponse> call)
{
try
{
return await call;
}
catch (Exception ex)
{
handler(ex);
throw;
}
}
}

View file

@ -0,0 +1,75 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Grpc.Core;
using Grpc.Core.Interceptors;
namespace FrostFS.SDK.Client;
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods",
Justification = "parameters are provided by GRPC infrastructure")]
public class MetricsInterceptor(Action<CallStatistics> callback) : Interceptor
{
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
TRequest request,
ClientInterceptorContext<TRequest, TResponse> context,
AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
{
var call = continuation(request, context);
return new AsyncUnaryCall<TResponse>(
HandleUnaryResponse(call),
call.ResponseHeadersAsync,
call.GetStatus,
call.GetTrailers,
call.Dispose);
}
public override AsyncClientStreamingCall<TRequest, TResponse> AsyncClientStreamingCall<TRequest, TResponse>(
ClientInterceptorContext<TRequest, TResponse> context,
AsyncClientStreamingCallContinuation<TRequest, TResponse> continuation)
{
var call = continuation(context);
return new AsyncClientStreamingCall<TRequest, TResponse>(
call.RequestStream,
HandleStreamResponse(call),
call.ResponseHeadersAsync,
call.GetStatus,
call.GetTrailers,
call.Dispose);
}
private async Task<TResponse> HandleUnaryResponse<TResponse>(AsyncUnaryCall<TResponse> call)
{
var watch = new Stopwatch();
watch.Start();
var response = await call;
watch.Stop();
var elapsed = watch.ElapsedTicks * 1_000_000 / Stopwatch.Frequency;
callback(new CallStatistics { MethodName = call.ToString(), ElapsedMicroSeconds = elapsed });
return response;
}
private async Task<TResponse> HandleStreamResponse<TRequest, TResponse>(AsyncClientStreamingCall<TRequest, TResponse> call)
{
var watch = new Stopwatch();
watch.Start();
var response = await call;
watch.Stop();
var elapsed = watch.ElapsedTicks * 1_000_000 / Stopwatch.Frequency;
callback(new CallStatistics { MethodName = call.ToString(), ElapsedMicroSeconds = elapsed });
return response;
}
}

View file

@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace FrostFS.SDK.Client.Interfaces;
public interface IFrostFSClient
{
#region Network
Task<FrostFsNetmapSnapshot> GetNetmapSnapshotAsync(CallContext ctx);
Task<FrostFsNodeInfo> GetNodeInfoAsync(CallContext ctx);
Task<NetworkSettings> GetNetworkSettingsAsync(CallContext ctx);
#endregion
#region Session
Task<FrostFsSessionToken> CreateSessionAsync(PrmSessionCreate args, CallContext ctx);
#endregion
#region ApeManager
Task<ReadOnlyMemory<byte>> AddChainAsync(PrmApeChainAdd args, CallContext ctx);
Task RemoveChainAsync(PrmApeChainRemove args, CallContext ctx);
Task<FrostFsChain[]> ListChainAsync(PrmApeChainList args, CallContext ctx);
#endregion
#region Container
Task<FrostFsContainerInfo> GetContainerAsync(PrmContainerGet args, CallContext ctx);
IAsyncEnumerable<FrostFsContainerId> ListContainersAsync(PrmContainerGetAll args, CallContext ctx);
[Obsolete("Use PutContainerAsync method")]
Task<FrostFsContainerId> CreateContainerAsync(PrmContainerCreate args, CallContext ctx);
Task<FrostFsContainerId> PutContainerAsync(PrmContainerCreate args, CallContext ctx);
Task DeleteContainerAsync(PrmContainerDelete args, CallContext ctx);
#endregion
#region Object
Task<FrostFsHeaderResult> GetObjectHeadAsync(PrmObjectHeadGet args, CallContext ctx);
Task<FrostFsObject> GetObjectAsync(PrmObjectGet args, CallContext ctx);
Task<RangeReader> GetRangeAsync(PrmRangeGet args, CallContext ctx);
Task<ReadOnlyMemory<byte>[]> GetRangeHashAsync(PrmRangeHashGet args, CallContext ctx);
Task<IObjectWriter> PutObjectAsync(PrmObjectPut args, CallContext ctx);
Task<FrostFsObjectId> PutClientCutObjectAsync(PrmObjectClientCutPut args, CallContext ctx);
Task<FrostFsObjectId> PutSingleObjectAsync(PrmSingleObjectPut args, CallContext ctx);
Task<FrostFsObjectId> PatchObjectAsync(PrmObjectPatch args, CallContext ctx);
Task DeleteObjectAsync(PrmObjectDelete args, CallContext ctx);
IAsyncEnumerable<FrostFsObjectId> SearchObjectsAsync(PrmObjectSearch args, CallContext ctx);
#endregion
#region Account
Task<Accounting.Decimal> GetBalanceAsync(CallContext ctx);
#endregion
}

View file

@ -0,0 +1,12 @@
using System;
using System.Threading.Tasks;
namespace FrostFS.SDK.Client.Interfaces
{
public interface IObjectWriter : IDisposable
{
Task WriteAsync(ReadOnlyMemory<byte> memory);
Task<FrostFsObjectId> CompleteAsync();
}
}

View file

@ -0,0 +1,24 @@
using Microsoft.Extensions.Logging;
namespace FrostFS.SDK.Client;
internal static partial class FrostFsMessages
{
[LoggerMessage(100,
LogLevel.Warning,
"Failed to create frostfs session token for client. Address {address}, {error}",
EventName = nameof(SessionCreationError))]
internal static partial void SessionCreationError(ILogger logger, string address, string error);
[LoggerMessage(101,
LogLevel.Warning,
"Error threshold reached. Address {address}, threshold {threshold}",
EventName = nameof(ErrorЕhresholdReached))]
internal static partial void ErrorЕhresholdReached(ILogger logger, string address, uint threshold);
[LoggerMessage(102,
LogLevel.Warning,
"Health has changed: {address} healthy {healthy}, reason {error}",
EventName = nameof(HealthChanged))]
internal static partial void HealthChanged(ILogger logger, string address, bool healthy, string error);
}

View file

@ -0,0 +1,22 @@
using System;
using System.Linq;
using FrostFS.SDK.Cryptography;
namespace FrostFS.SDK.Client.Mappers.GRPC;
public static class ContainerMapper
{
public static FrostFsContainerInfo ToModel(this Container.Container container)
{
if (container == null)
throw new ArgumentNullException(nameof(container));
return new FrostFsContainerInfo(
container.PlacementPolicy.ToModel(),
container.Attributes?.Select(a => new FrostFsAttributePair(a.Key, a.Value)).ToArray(),
container.Version?.ToModel(),
container.OwnerId?.ToModel(),
container.Nonce?.ToUuid());
}
}

View file

@ -0,0 +1,38 @@
using System;
using FrostFS.Refs;
using FrostFS.SDK.Cryptography;
using Google.Protobuf;
namespace FrostFS.SDK.Client.Mappers.GRPC;
public static class ContainerIdMapper
{
public static ContainerID ToMessage(this FrostFsContainerId model)
{
if (model is null)
{
throw new ArgumentNullException(nameof(model));
}
var containerId = model.GetValue() ?? throw new ArgumentNullException(nameof(model));
var message = new ContainerID
{
Value = UnsafeByteOperations.UnsafeWrap(Base58.Decode(containerId))
};
return message!;
}
public static FrostFsContainerId ToModel(this ContainerID message)
{
if (message is null)
{
throw new ArgumentNullException(nameof(message));
}
return new FrostFsContainerId(Base58.Encode(message.Value.Span));
}
}

View file

@ -0,0 +1,23 @@
using System;
using FrostFS.Session;
namespace FrostFS.SDK.Client.Mappers.GRPC;
public static class MetaHeaderMapper
{
public static RequestMetaHeader ToMessage(this MetaHeader metaHeader)
{
if (metaHeader is null)
{
throw new ArgumentNullException(nameof(metaHeader));
}
return new RequestMetaHeader
{
Version = metaHeader.Version.ToMessage(),
Epoch = (uint)metaHeader.Epoch,
Ttl = (uint)metaHeader.Ttl
};
}
}

View file

@ -0,0 +1,23 @@
using System;
using System.Linq;
using FrostFS.Netmap;
namespace FrostFS.SDK.Client;
public static class NetmapMapper
{
public static FrostFsNetmapSnapshot ToModel(this NetmapSnapshotResponse netmap)
{
if (netmap is null)
{
throw new ArgumentNullException(nameof(netmap));
}
return new FrostFsNetmapSnapshot(
netmap.Body.Netmap.Epoch,
netmap.Body.Netmap.Nodes
.Select(n => n.ToModel(netmap.MetaHeader.Version))
.ToArray());
}
}

View file

@ -0,0 +1,45 @@
using System;
using System.Linq;
using FrostFS.Netmap;
using FrostFS.SDK.Client.Mappers.GRPC;
namespace FrostFS.SDK.Client;
public static class NodeInfoMapper
{
public static FrostFsNodeInfo ToModel(this LocalNodeInfoResponse.Types.Body node)
{
if (node is null)
{
throw new ArgumentNullException(nameof(node));
}
return node.NodeInfo.ToModel(node.Version);
}
public static FrostFsNodeInfo ToModel(this NodeInfo nodeInfo, Refs.Version version)
{
if (nodeInfo is null)
{
throw new ArgumentNullException(nameof(nodeInfo));
}
NodeState state = nodeInfo.State switch
{
NodeInfo.Types.State.Unspecified => NodeState.Unspecified,
NodeInfo.Types.State.Online => NodeState.Online,
NodeInfo.Types.State.Offline => NodeState.Offline,
NodeInfo.Types.State.Maintenance => NodeState.Maintenance,
_ => throw new ArgumentException($"Unknown NodeState. Value: '{nodeInfo.State}'.")
};
return new FrostFsNodeInfo(
version: version.ToModel(),
state: state,
addresses: [.. nodeInfo.Addresses],
attributes: nodeInfo.Attributes.ToDictionary(n => n.Key, n => n.Value),
publicKey: nodeInfo.PublicKey.Memory
);
}
}

View file

@ -0,0 +1,25 @@
using System;
using System.Linq;
using FrostFS.Netmap;
namespace FrostFS.SDK.Client;
public static class PlacementPolicyMapper
{
public static FrostFsPlacementPolicy ToModel(this PlacementPolicy placementPolicy)
{
if (placementPolicy is null)
{
throw new ArgumentNullException(nameof(placementPolicy));
}
return new FrostFsPlacementPolicy(
placementPolicy.Unique,
placementPolicy.ContainerBackupFactor,
[.. placementPolicy.Selectors.Select(s => s.ToModel())],
[.. placementPolicy.Filters.Select(f => f.ToModel())],
[.. placementPolicy.Replicas.Select(r => r.ToModel())]
);
}
}

View file

@ -0,0 +1,106 @@
using System;
using System.Linq;
using FrostFS.Netmap;
namespace FrostFS.SDK.Client;
public static class PolicyMapper
{
public static Replica ToMessage(this FrostFsReplica replica)
{
return new Replica
{
Count = (uint)replica.Count,
Selector = replica.Selector,
EcDataCount = replica.EcDataCount,
EcParityCount = replica.EcParityCount
};
}
public static FrostFsReplica ToModel(this Replica replica)
{
if (replica is null)
{
throw new ArgumentNullException(nameof(replica));
}
return new FrostFsReplica((int)replica.Count, replica.Selector)
{
EcDataCount = replica.EcDataCount,
EcParityCount = replica.EcParityCount
};
}
public static Selector ToMessage(this FrostFsSelector selector)
{
if (selector is null)
{
throw new ArgumentNullException(nameof(selector));
}
return new Selector
{
Name = selector.Name,
Count = selector.Count,
Clause = (Clause)selector.Clause,
Attribute = selector.Attribute,
Filter = selector.Filter
};
}
public static FrostFsSelector ToModel(this Selector selector)
{
if (selector is null)
{
throw new ArgumentNullException(nameof(selector));
}
var model = new FrostFsSelector(selector.Name)
{
Count = selector.Count,
Clause = (int)selector.Clause,
Attribute = selector.Attribute,
Filter = selector.Filter
};
return model;
}
public static Filter ToMessage(this FrostFsFilter filter)
{
if (filter is null)
{
throw new ArgumentNullException(nameof(filter));
}
var message = new Filter
{
Name = filter.Name,
Key = filter.Key,
Op = (Operation)filter.Operation,
Value = filter.Value,
};
message.Filters.AddRange(filter.Filters.Select(f => f.ToMessage()));
return message;
}
public static FrostFsFilter ToModel(this Filter filter)
{
if (filter is null)
{
throw new ArgumentNullException(nameof(filter));
}
var model = new FrostFsFilter(
filter.Name,
filter.Key,
(int)filter.Op,
filter.Value,
[.. filter.Filters.Select(f => f.ToModel())]);
return model;
}
}

View file

@ -0,0 +1,12 @@
namespace FrostFS.SDK.Client.Mappers.GRPC;
internal static class ObjectMapper
{
internal static FrostFsObject ToModel(this Object.Object obj)
{
return new FrostFsObject(obj.Header.ToModel())
{
ObjectId = FrostFsObjectId.FromHash(obj.ObjectId.Value.Span)
};
}
}

View file

@ -0,0 +1,27 @@
using System;
using FrostFS.Object;
namespace FrostFS.SDK.Client.Mappers.GRPC;
public static class ObjectAttributeMapper
{
public static Header.Types.Attribute ToMessage(this FrostFsAttributePair attribute)
{
return new Header.Types.Attribute
{
Key = attribute.Key,
Value = attribute.Value
};
}
public static FrostFsAttributePair ToModel(this Header.Types.Attribute attribute)
{
if (attribute is null)
{
throw new ArgumentNullException(nameof(attribute));
}
return new FrostFsAttributePair(attribute.Key, attribute.Value);
}
}

View file

@ -0,0 +1,34 @@
using System;
using FrostFS.Object;
namespace FrostFS.SDK.Client.Mappers.GRPC;
public static class ObjectFilterMapper
{
public static SearchRequest.Types.Body.Types.Filter ToMessage(this IObjectFilter filter)
{
if (filter is null)
{
throw new ArgumentNullException(nameof(filter));
}
var objMatchTypeName = filter.MatchType switch
{
FrostFsMatchType.Unspecified => MatchType.Unspecified,
FrostFsMatchType.Equals => MatchType.StringEqual,
FrostFsMatchType.NotEquals => MatchType.StringNotEqual,
FrostFsMatchType.KeyAbsent => MatchType.NotPresent,
FrostFsMatchType.StartsWith => MatchType.CommonPrefix,
_ => throw new ArgumentException($"Unknown MatchType. Value: '{filter.MatchType}'.")
};
return new SearchRequest.Types.Body.Types.Filter
{
MatchType = objMatchTypeName,
Key = filter.Key,
Value = filter.GetSerializedValue()
};
}
}

View file

@ -0,0 +1,63 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using FrostFS.Object;
using FrostFS.SDK.Cryptography;
namespace FrostFS.SDK.Client.Mappers.GRPC;
public static class ObjectHeaderMapper
{
public static FrostFsObjectHeader ToModel(this Header header)
{
if (header is null)
{
throw new ArgumentNullException(nameof(header));
}
var objTypeName = header.ObjectType switch
{
ObjectType.Regular => FrostFsObjectType.Regular,
ObjectType.Lock => FrostFsObjectType.Lock,
ObjectType.Tombstone => FrostFsObjectType.Tombstone,
_ => throw new ArgumentException($"Unknown ObjectType. Value: '{header.ObjectType}'.")
};
FrostFsSplit? split = header!.Split != null
? header.Split.ToModel()
: null;
var model = new FrostFsObjectHeader(
new FrostFsContainerId(Base58.Encode(header.ContainerId.Value.Span)),
objTypeName,
[.. header.Attributes.Select(attribute => attribute.ToModel())],
split,
header.OwnerId.ToModel(),
header.Version.ToModel())
{
PayloadLength = header.PayloadLength,
};
return model;
}
public static FrostFsSplit ToModel(this Header.Types.Split split)
{
if (split is null)
{
throw new ArgumentNullException(nameof(split));
}
var children = split!.Children.Count != 0
? new ReadOnlyCollection<FrostFsObjectId>([.. split.Children.Select(x => x.ToModel())])
: null;
return new FrostFsSplit(new SplitId(split.SplitId.ToUuid()),
split.Previous?.ToModel(),
split.Parent?.ToModel(),
split.ParentHeader?.ToModel(),
null,
children);
}
}

View file

@ -0,0 +1,33 @@
using System;
using FrostFS.Refs;
using Google.Protobuf;
namespace FrostFS.SDK.Client.Mappers.GRPC;
public static class ObjectIdMapper
{
public static ObjectID ToMessage(this FrostFsObjectId objectId)
{
if (objectId is null)
{
throw new ArgumentNullException(nameof(objectId));
}
return new ObjectID
{
Value = UnsafeByteOperations.UnsafeWrap(objectId.ToHash())
};
}
public static FrostFsObjectId ToModel(this ObjectID objectId)
{
if (objectId is null)
{
throw new ArgumentNullException(nameof(objectId));
}
return FrostFsObjectId.FromHash(objectId.Value.Span);
}
}

View file

@ -0,0 +1,54 @@
using System;
using FrostFS.Refs;
using FrostFS.SDK.Cryptography;
using Google.Protobuf;
using Microsoft.Extensions.Caching.Memory;
namespace FrostFS.SDK.Client.Mappers.GRPC;
public static class OwnerIdMapper
{
private static readonly MemoryCacheEntryOptions _oneHourExpiration = new MemoryCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromHours(1))
.SetSize(1);
public static OwnerID ToMessage(this FrostFsOwner model)
{
if (model is null)
{
throw new ArgumentNullException(nameof(model));
}
if (!Caches.Owners.TryGetValue(model, out OwnerID? message))
{
message = new OwnerID
{
Value = UnsafeByteOperations.UnsafeWrap(model.ToHash())
};
Caches.Owners.Set(model, message, _oneHourExpiration);
}
return message!;
}
public static FrostFsOwner ToModel(this OwnerID message)
{
if (message is null)
{
throw new ArgumentNullException(nameof(message));
}
if (!Caches.Owners.TryGetValue(message, out FrostFsOwner? model))
{
model = new FrostFsOwner(Base58.Encode(message.Value.Span));
Caches.Owners.Set(message, model, _oneHourExpiration);
}
return model!;
}
}

View file

@ -0,0 +1,31 @@
using System;
using Google.Protobuf;
namespace FrostFS.SDK.Client.Mappers.GRPC;
public static class SignatureMapper
{
public static Refs.Signature ToMessage(this FrostFsSignature signature)
{
if (signature is null)
{
throw new ArgumentNullException(nameof(signature));
}
var scheme = signature.Scheme switch
{
SignatureScheme.EcdsaRfc6979Sha256 => Refs.SignatureScheme.EcdsaRfc6979Sha256,
SignatureScheme.EcdsaRfc6979Sha256WalletConnect => Refs.SignatureScheme.EcdsaRfc6979Sha256WalletConnect,
SignatureScheme.EcdsaSha512 => Refs.SignatureScheme.EcdsaSha512,
_ => throw new ArgumentException(nameof(signature.Scheme), $"Unexpected enum value: {signature.Scheme}")
};
return new Refs.Signature
{
Key = UnsafeByteOperations.UnsafeWrap(signature.Key),
Scheme = scheme,
Sign = UnsafeByteOperations.UnsafeWrap(signature.Sign)
};
}
}

View file

@ -0,0 +1,22 @@
using System;
using System.Linq;
namespace FrostFS.SDK.Client.Mappers.GRPC;
public static class StatusMapper
{
public static FrostFsResponseStatus ToModel(this Status.Status status)
{
if (status is null)
return new FrostFsResponseStatus(FrostFsStatusCode.Success);
var codeName = Enum.GetName(typeof(FrostFsStatusCode), status.Code);
return codeName is null
? throw new ArgumentException($"Unknown StatusCode. Value: '{status.Code}'.")
: new FrostFsResponseStatus(
(FrostFsStatusCode)status.Code,
status.Message,
string.Join(", ", status.Details.Select(d => System.Text.Encoding.UTF8.GetString([.. d.Value]))));
}
}

View file

@ -0,0 +1,85 @@
using System.Collections;
using System.Threading;
using FrostFS.Refs;
namespace FrostFS.SDK.Client.Mappers.GRPC;
public static class VersionMapper
{
private static readonly Hashtable _cacheMessages = [];
private static readonly Hashtable _cacheModels = [];
private static SpinLock _spinlock;
public static Version ToMessage(this FrostFsVersion model)
{
if (model is null)
{
throw new System.ArgumentNullException(nameof(model));
}
var key = model.Major << 16 + model.Minor;
if (!_cacheMessages.ContainsKey(key))
{
bool lockTaken = false;
try
{
_spinlock.Enter(ref lockTaken);
var message = new Version
{
Major = (uint)model.Major,
Minor = (uint)model.Minor
};
_cacheMessages.Add(key, message);
return message;
}
catch (System.ArgumentException)
{
// ignore attempt to add duplicate
}
finally
{
if (lockTaken)
_spinlock.Exit(false);
}
}
return (Version)_cacheMessages[key];
}
public static FrostFsVersion ToModel(this Version message)
{
if (message is null)
{
throw new System.ArgumentNullException(nameof(message));
}
var key = (int)message.Major << 16 + (int)message.Minor;
if (!_cacheModels.ContainsKey(key))
{
bool lockTaken = false;
try
{
_spinlock.Enter(ref lockTaken);
var model = new FrostFsVersion((int)message.Major, (int)message.Minor);
_cacheModels.Add(key, model);
return model;
}
catch (System.ArgumentException)
{
// ignore attempt to add duplicate
}
finally
{
if (lockTaken)
_spinlock.Exit(false);
}
}
return (FrostFsVersion)_cacheModels[key];
}
}

View file

@ -0,0 +1,37 @@
using System;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Text;
using Grpc.Core.Interceptors;
namespace FrostFS.SDK;
public class ClientSettings
{
protected static readonly string errorTemplate = "{0} is required parameter";
public string Host { get; set; } = string.Empty;
public string Key { get; set; } = string.Empty;
public Action<CallStatistics>? Callback { get; set; }
public Collection<Interceptor> Interceptors { get; } = [];
public void Validate()
{
StringBuilder? errors = null;
if (string.IsNullOrWhiteSpace(Host))
(errors = new StringBuilder(128)).AppendLine(string.Format(CultureInfo.InvariantCulture, errorTemplate, nameof(Host)));
if (string.IsNullOrWhiteSpace(Key))
(errors ??= new StringBuilder(128)).AppendLine(string.Format(CultureInfo.InvariantCulture, errorTemplate, nameof(Key)));
if (errors != null)
{
throw new ArgumentException(errors.ToString());
}
}
}

View file

@ -0,0 +1,55 @@
using FrostFS.Refs;
using FrostFS.SDK.Client;
using FrostFS.SDK.Client.Mappers.GRPC;
using FrostFS.SDK.Cryptography;
namespace FrostFS.SDK;
public class FrostFsContainerId
{
private string? modelId;
private ContainerID? containerID;
public FrostFsContainerId(string id)
{
this.modelId = id;
}
internal FrostFsContainerId(ContainerID id)
{
this.containerID = id;
}
public string GetValue()
{
if (this.modelId != null)
return this.modelId;
if (containerID != null)
{
this.modelId = Base58.Encode(containerID.Value.Span);
return this.modelId;
}
throw new FrostFsInvalidObjectException();
}
public ContainerID GetContainerID()
{
if (this.containerID != null)
return this.containerID;
if (modelId != null)
{
this.containerID = this.ToMessage();
return this.containerID;
}
throw new FrostFsInvalidObjectException();
}
public override string ToString()
{
return GetValue();
}
}

View file

@ -0,0 +1,112 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using FrostFS.SDK.Client;
using FrostFS.SDK.Client.Mappers.GRPC;
using FrostFS.SDK.Cryptography;
using Google.Protobuf;
namespace FrostFS.SDK;
public class FrostFsContainerInfo
{
private Container.Container.Types.Attribute[]? grpsAttributes;
private ReadOnlyCollection<FrostFsAttributePair>? attributes;
private FrostFsPlacementPolicy? placementPolicy;
private Guid? nonce;
private Container.Container? container;
public FrostFsContainerInfo(
FrostFsPlacementPolicy placementPolicy,
FrostFsAttributePair[]? attributes = null,
FrostFsVersion? version = null,
FrostFsOwner? owner = null,
Guid? nonce = null)
{
this.placementPolicy = placementPolicy;
Version = version;
Owner = owner;
this.nonce = nonce;
if (attributes != null)
this.attributes = new ReadOnlyCollection<FrostFsAttributePair>(attributes);
}
internal FrostFsContainerInfo(Container.Container container)
{
this.container = container;
}
public Guid Nonce
{
get
{
nonce ??= container?.Nonce != null ? container.Nonce.ToUuid() : Guid.NewGuid();
return nonce.Value;
}
}
public FrostFsPlacementPolicy? PlacementPolicy
{
get
{
placementPolicy ??= container?.PlacementPolicy?.ToModel();
return placementPolicy;
}
}
public ReadOnlyCollection<FrostFsAttributePair>? Attributes
{
get
{
if (attributes == null && grpsAttributes != null)
attributes = new ReadOnlyCollection<FrostFsAttributePair>(grpsAttributes.Select(a => new FrostFsAttributePair(a.Key, a.Value)).ToList());
return attributes;
}
}
public FrostFsVersion? Version { get; private set; }
public FrostFsOwner? Owner { get; private set; }
internal Container.Container.Types.Attribute[]? GetGrpsAttributes()
{
grpsAttributes ??= Attributes?
.Select(a => new Container.Container.Types.Attribute { Key = a.Key, Value = a.Value })
.ToArray();
return grpsAttributes;
}
internal Container.Container GetContainer()
{
if (this.container == null)
{
if (PlacementPolicy == null)
{
throw new ArgumentNullException("PlacementPolicy is null");
}
Span<byte> nonce = stackalloc byte[16];
Nonce.ToBytes(nonce);
this.container = new Container.Container()
{
PlacementPolicy = PlacementPolicy.Value.GetPolicy(),
Nonce = ByteString.CopyFrom(nonce),
OwnerId = Owner?.OwnerID,
Version = Version?.VersionID
};
var attribs = GetGrpsAttributes();
if (attribs != null)
this.container.Attributes.AddRange(attribs);
}
return this.container;
}
}

View file

@ -1,6 +1,6 @@
namespace FrostFS.SDK.ModelsV2.Enums;
namespace FrostFS.SDK;
public enum ObjectMatchType
public enum FrostFsMatchType
{
Unspecified = 0,
Equals = 1,

View file

@ -0,0 +1,8 @@
namespace FrostFS.SDK;
public enum FrostFsObjectType
{
Regular = 0,
Tombstone = 1,
Lock = 3
}

View file

@ -1,6 +1,6 @@
namespace FrostFS.SDK.ModelsV2.Enums;
namespace FrostFS.SDK;
public enum StatusCode
public enum FrostFsStatusCode
{
Success = 0,
Internal = 1024,

View file

@ -1,4 +1,4 @@
namespace FrostFS.SDK.ModelsV2.Enums;
namespace FrostFS.SDK;
public enum NodeState
{

View file

@ -0,0 +1,8 @@
namespace FrostFS.SDK;
public enum SignatureScheme
{
EcdsaSha512,
EcdsaRfc6979Sha256,
EcdsaRfc6979Sha256WalletConnect
}

View file

@ -0,0 +1,7 @@
namespace FrostFS.SDK;
public class CallStatistics
{
public string? MethodName { get; set; }
public long ElapsedMicroSeconds { get; set; }
}

View file

@ -0,0 +1,21 @@
using System;
using FrostFS.SDK.Cryptography;
namespace FrostFS.SDK;
public class CheckSum
{
private byte[]? hash;
private string? text;
public static CheckSum CreateCheckSum(byte[] content)
{
return new CheckSum { hash = DataHasher.Sha256(content.AsSpan()) };
}
public override string ToString()
{
return text ??= BitConverter.ToString(hash).Replace("-", "");
}
}

View file

@ -0,0 +1,52 @@
namespace FrostFS.SDK;
public static class Constants
{
public const int ObjectChunkSize = 3 * (1 << 20);
public const int Sha256HashLength = 32;
// HeaderPrefix is a prefix of key to object header value or property.
public const string HeaderPrefix = "$Object:";
// FilterHeaderVersion is a filter key to "version" field of the object header.
public const string FilterHeaderVersion = HeaderPrefix + "version";
// FilterHeaderObjectID is a filter key to "object_id" field of the object.
public const string FilterHeaderObjectID = HeaderPrefix + "objectID";
// FilterHeaderContainerID is a filter key to "container_id" field of the object header.
public const string FilterHeaderContainerID = HeaderPrefix + "containerID";
// FilterHeaderOwnerID is a filter key to "owner_id" field of the object header.
public const string FilterHeaderOwnerID = HeaderPrefix + "ownerID";
// FilterHeaderCreationEpoch is a filter key to "creation_epoch" field of the object header.
public const string FilterHeaderCreationEpoch = HeaderPrefix + "creationEpoch";
// FilterHeaderPayloadLength is a filter key to "payload_length" field of the object header.
public const string FilterHeaderPayloadLength = HeaderPrefix + "payloadLength";
// FilterHeaderPayloadHash is a filter key to "payload_hash" field of the object header.
public const string FilterHeaderPayloadHash = HeaderPrefix + "payloadHash";
// FilterHeaderObjectType is a filter key to "object_type" field of the object header.
public const string FilterHeaderObjectType = HeaderPrefix + "objectType";
// FilterHeaderHomomorphicHash is a filter key to "homomorphic_hash" field of the object header.
public const string FilterHeaderHomomorphicHash = HeaderPrefix + "homomorphicHash";
// FilterHeaderParent is a filter key to "split.parent" field of the object header.
public const string FilterHeaderParent = HeaderPrefix + "split.parent";
// FilterHeaderSplitID is a filter key to "split.splitID" field of the object header.
public const string FilterHeaderSplitID = HeaderPrefix + "split.splitID";
// FilterHeaderECParent is a filter key to "ec.parent" field of the object header.
public const string FilterHeaderECParent = HeaderPrefix + "ec.parent";
// FilterPropertyRoot is a filter key to check if regular object is on top of split hierarchy.
public const string FilterHeaderRoot = HeaderPrefix + "ROOT";
// FilterPropertyPhy is a filter key to check if an object physically stored on a node.
public const string FilterHeaderPhy = HeaderPrefix + "PHY";
}

View file

@ -0,0 +1,28 @@
using System.Linq;
using FrostFS.Netmap;
namespace FrostFS.SDK;
public class FrostFsFilter(string name, string key, int operation, string value, FrostFsFilter[] filters) : IFrostFsFilter
{
public string Name { get; } = name;
public string Key { get; } = key;
public int Operation { get; } = operation;
public string Value { get; } = value;
public FrostFsFilter[] Filters { get; } = filters;
internal Filter GetMessage()
{
var filter = new Filter()
{
Name = Name,
Key = Key,
Op = (Operation)Operation,
Value = Value,
};
filter.Filters.AddRange(Filters.Select(f => f.GetMessage()));
return filter;
}
}

View file

@ -0,0 +1,246 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FrostFS.SDK.Client;
using FrostFS.SDK.Client.Models.Netmap.Placement;
using FrostFS.SDK.Cryptography;
namespace FrostFS.SDK;
public class FrostFsNetmapSnapshot(ulong epoch, IReadOnlyList<FrostFsNodeInfo> nodeInfoCollection)
{
public ulong Epoch { get; private set; } = epoch;
public IReadOnlyList<FrostFsNodeInfo> NodeInfoCollection { get; private set; } = nodeInfoCollection;
internal static INormalizer NewReverseMinNorm(double minV)
{
return new ReverseMinNorm { min = minV };
}
// newSigmoidNorm returns a normalizer which
// normalize values in range of 0.0 to 1.0 to a scaled sigmoid.
internal static INormalizer NewSigmoidNorm(double scale)
{
return new SigmoidNorm(scale);
}
// PlacementVectors sorts container nodes returned by ContainerNodes method
// and returns placement vectors for the entity identified by the given pivot.
// For example, in order to build node list to store the object, binary-encoded
// object identifier can be used as pivot. Result is deterministic for
// the fixed NetMap and parameters.
public FrostFsNodeInfo[][] PlacementVectors(FrostFsNodeInfo[][] vectors, byte[] pivot)
{
if (vectors is null)
{
throw new ArgumentNullException(nameof(vectors));
}
using var murmur3 = new Murmur3(0);
var hash = murmur3.GetCheckSum64(pivot);
var wf = Tools.DefaultWeightFunc(NodeInfoCollection.ToArray());
var result = new FrostFsNodeInfo[vectors.Length][];
var maxSize = vectors.Max(x => x.Length);
var spanWeigths = new double[maxSize];
for (int i = 0; i < vectors.Length; i++)
{
result[i] = new FrostFsNodeInfo[vectors[i].Length];
for (int j = 0; j < vectors[i].Length; j++)
{
result[i][j] = vectors[i][j];
}
Tools.AppendWeightsTo(result[i], wf, ref spanWeigths);
result[i] = Tools.SortHasherSliceByWeightValue(result[i].ToList<FrostFsNodeInfo>(), spanWeigths, hash).ToArray();
}
return result;
}
// SelectFilterNodes returns a two-dimensional list of nodes as a result of applying the
// given SelectFilterExpr to the NetMap.
// If the SelectFilterExpr contains only filters, the result contains a single row with the
// result of the last filter application.
// If the SelectFilterExpr contains only selectors, the result contains the selection rows
// of the last select application.
List<List<FrostFsNodeInfo>> SelectFilterNodes(SelectFilterExpr expr)
{
var policy = new FrostFsPlacementPolicy(false, expr.Cbf, [expr.Selector], expr.Filters);
var ctx = new Context(this)
{
Cbf = expr.Cbf
};
ctx.ProcessFilters(policy);
ctx.ProcessSelectors(policy);
var ret = new List<List<FrostFsNodeInfo>>();
if (expr.Selector == null)
{
var lastFilter = expr.Filters.Last();
var subCollestion = new List<FrostFsNodeInfo>();
ret.Add(subCollestion);
foreach (var nodeInfo in NodeInfoCollection)
{
if (ctx.Match(ctx.ProcessedFilters[lastFilter.Name], nodeInfo))
{
subCollestion.Add(nodeInfo);
}
}
}
else if (expr.Selector.Name != null)
{
var sel = ctx.GetSelection(ctx.ProcessedSelectors[expr.Selector.Name]);
foreach (var ns in sel)
{
var subCollestion = new List<FrostFsNodeInfo>();
ret.Add(subCollestion);
foreach (var n in ns)
{
subCollestion.Add(n);
}
}
}
return ret;
}
internal static Func<FrostFsNodeInfo, double> NewWeightFunc(INormalizer capNorm, INormalizer priceNorm)
{
return new Func<FrostFsNodeInfo, double>((FrostFsNodeInfo nodeInfo) =>
{
return capNorm.Normalize(nodeInfo.GetCapacity()) * priceNorm.Normalize(nodeInfo.Price);
});
}
private static FrostFsNodeInfo[] FlattenNodes(List<List<FrostFsNodeInfo>> nodes)
{
int sz = 0;
foreach (var ns in nodes)
{
sz += ns.Count;
}
var result = new FrostFsNodeInfo[sz];
int i = 0;
foreach (var ns in nodes)
{
foreach (var n in ns)
{
result[i++] = n;
}
}
return result;
}
// ContainerNodes returns two-dimensional list of nodes as a result of applying
// given PlacementPolicy to the NetMap. Each line of the list corresponds to a
// replica descriptor. Line order corresponds to order of ReplicaDescriptor list
// in the policy. Nodes are pre-filtered according to the Filter list from
// the policy, and then selected by Selector list. Result is deterministic for
// the fixed NetMap and parameters.
//
// Result can be used in PlacementVectors.
public FrostFsNodeInfo[][] ContainerNodes(FrostFsPlacementPolicy p, byte[]? pivot)
{
var c = new Context(this)
{
Cbf = p.BackupFactor == 0 ? 3 : p.BackupFactor
};
if (pivot != null && pivot.Length > 0)
{
c.HrwSeed = pivot;
using var murmur = new Murmur3(0);
c.HrwSeedHash = murmur.GetCheckSum64(pivot);
}
c.ProcessFilters(p);
c.ProcessSelectors(p);
var unique = p.IsUnique();
var result = new List<List<FrostFsNodeInfo>>(p.Replicas.Length);
for (int i = 0; i < p.Replicas.Length; i++)
{
result.Add([]);
}
// Note that the cached selectors are not used when the policy contains the UNIQUE flag.
// This is necessary because each selection vector affects potentially the subsequent vectors
// and thus we call getSelection in such case, in order to take into account nodes previously
// marked as used by earlier replicas.
for (int i = 0; i < p.Replicas.Length; i++)
{
var sName = p.Replicas[i].Selector;
if (string.IsNullOrEmpty(sName) && !(p.Replicas.Length == 1 && p.Selectors.Count == 1))
{
var s = new FrostFsSelector(string.Empty)
{
Count = p.Replicas[i].CountNodes(),
Filter = Context.mainFilterName
};
var nodes = c.GetSelection(s);
result[i].AddRange(FlattenNodes(nodes));
if (unique)
{
foreach (var n in result[i])
{
c.UsedNodes[n.Hash()] = true;
}
}
continue;
}
if (unique)
{
if (!c.ProcessedSelectors.TryGetValue(sName, out var s) || s == null)
{
throw new FrostFsException($"selector not found: {sName}");
}
var nodes = c.GetSelection(c.ProcessedSelectors[sName]);
result[i].AddRange(FlattenNodes(nodes));
foreach (var n in result[i])
{
c.UsedNodes[n.Hash()] = true;
}
}
else
{
var nodes = c.Selections[sName];
result[i].AddRange(FlattenNodes(nodes));
}
}
var collection = new FrostFsNodeInfo[result.Count][];
for (int i = 0; i < result.Count; i++)
{
collection[i] = [.. result[i]];
}
return collection;
}
}

View file

@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using FrostFS.SDK.Client.Models.Netmap.Placement;
using FrostFS.SDK.Cryptography;
namespace FrostFS.SDK;
public class FrostFsNodeInfo(
FrostFsVersion version,
NodeState state,
IReadOnlyCollection<string> addresses,
IReadOnlyDictionary<string, string> attributes,
ReadOnlyMemory<byte> publicKey) : IHasher
{
private ulong _hash;
// attrPrice is a key to the node attribute that indicates the
// price in GAS tokens for storing one GB of data during one Epoch.
internal const string AttrPrice = "Price";
// attrCapacity is a key to the node attribute that indicates the
// total available disk space in Gigabytes.
internal const string AttrCapacity = "Capacity";
// attrExternalAddr is a key for the attribute storing node external addresses.
internal const string AttrExternalAddr = "ExternalAddr";
// sepExternalAddr is a separator for multi-value ExternalAddr attribute.
internal const string SepExternalAddr = ",";
private ulong price = ulong.MaxValue;
public NodeState State { get; } = state;
public FrostFsVersion Version { get; } = version;
public IReadOnlyCollection<string> Addresses { get; } = addresses;
public IReadOnlyDictionary<string, string> Attributes { get; } = attributes;
public ReadOnlyMemory<byte> PublicKey { get; } = publicKey;
public ulong Hash()
{
if (_hash == 0)
{
using var murmur3 = new Murmur3(0);
murmur3.Initialize();
_hash = murmur3.GetCheckSum64(PublicKey.ToArray());
}
return _hash;
}
internal ulong GetCapacity()
{
if (!Attributes.TryGetValue(AttrCapacity, out var val))
return 0;
return ulong.Parse(val, CultureInfo.InvariantCulture);
}
internal ulong Price
{
get
{
if (price == ulong.MaxValue)
{
if (!Attributes.TryGetValue(AttrPrice, out var val))
price = 0;
else
price = uint.Parse(val, CultureInfo.InvariantCulture);
}
return price;
}
}
}

View file

@ -0,0 +1,109 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using FrostFS.Netmap;
using FrostFS.SDK.Client;
namespace FrostFS.SDK;
public struct FrostFsPlacementPolicy(bool unique,
uint backupFactor,
Collection<FrostFsSelector> selectors,
Collection<FrostFsFilter> filters,
params FrostFsReplica[] replicas)
: IEquatable<FrostFsPlacementPolicy>
{
private PlacementPolicy? policy;
public FrostFsReplica[] Replicas { get; } = replicas;
public Collection<FrostFsSelector> Selectors { get; } = selectors;
public Collection<FrostFsFilter> Filters { get; } = filters;
public bool Unique { get; } = unique;
public uint BackupFactor { get; } = backupFactor;
public override readonly bool Equals(object obj)
{
if (obj is null)
{
return false;
}
var other = (FrostFsPlacementPolicy)obj;
return Equals(other);
}
public PlacementPolicy GetPolicy()
{
if (policy == null)
{
policy = new PlacementPolicy
{
Filters = { },
Selectors = { },
Replicas = { },
Unique = Unique,
ContainerBackupFactor = BackupFactor
};
if (Selectors != null && Selectors.Count > 0)
{
policy.Selectors.AddRange(Selectors.Select(s => s.GetMessage()));
}
if (Filters != null && Filters.Count > 0)
{
policy.Filters.AddRange(Filters.Select(s => s.ToMessage()));
}
foreach (var replica in Replicas)
{
policy.Replicas.Add(replica.ToMessage());
}
}
return policy;
}
internal readonly bool IsUnique()
{
return Unique || Replicas.Any(r => r.EcDataCount != 0 || r.EcParityCount != 0);
}
public override readonly int GetHashCode()
{
return Unique ? 17 : 0 + Replicas.GetHashCode();
}
public static bool operator ==(FrostFsPlacementPolicy left, FrostFsPlacementPolicy right)
{
return left.Equals(right);
}
public static bool operator !=(FrostFsPlacementPolicy left, FrostFsPlacementPolicy right)
{
return !(left == right);
}
public readonly bool Equals(FrostFsPlacementPolicy other)
{
var notEqual = Unique != other.Unique
|| Replicas.Length != other.Replicas.Length;
if (notEqual)
return false;
foreach (var replica in Replicas)
{
if (!other.Replicas.Any(r => r.Equals(replica)))
return false;
}
return true;
}
}

View file

@ -0,0 +1,59 @@
using System;
namespace FrostFS.SDK;
public struct FrostFsReplica : IEquatable<FrostFsReplica>
{
public int Count { get; set; }
public string Selector { get; set; }
public uint EcDataCount { get; set; }
public uint EcParityCount { get; set; }
public FrostFsReplica(int count, string? selector = null)
{
selector ??= string.Empty;
Count = count;
Selector = selector;
}
public override readonly bool Equals(object obj)
{
if (obj is null)
{
return false;
}
var other = (FrostFsReplica)obj;
return Count == other.Count && Selector == other.Selector;
}
public readonly uint CountNodes()
{
return Count != 0 ? (uint)Count : EcDataCount + EcParityCount;
}
public override readonly int GetHashCode()
{
return (Count + Selector.GetHashCode()) ^ (int)EcDataCount ^ (int)EcParityCount;
}
public static bool operator ==(FrostFsReplica left, FrostFsReplica right)
{
return left.Equals(right);
}
public static bool operator !=(FrostFsReplica left, FrostFsReplica right)
{
return !(left == right);
}
public readonly bool Equals(FrostFsReplica other)
{
return Count == other.Count
&& Selector == other.Selector
&& EcDataCount == other.EcDataCount
&& EcParityCount == other.EcParityCount;
}
}

View file

@ -0,0 +1,24 @@
using FrostFS.Netmap;
namespace FrostFS.SDK;
public class FrostFsSelector(string name)
{
public string Name { get; } = name;
public uint Count { get; set; }
public int Clause { get; set; }
public string? Attribute { get; set; }
public string? Filter { get; set; }
internal Selector GetMessage()
{
return new Selector()
{
Name = Name,
Clause = (Clause)Clause,
Count = Count,
Filter = Filter ?? string.Empty,
Attribute = Attribute ?? string.Empty,
};
}
}

View file

@ -0,0 +1,36 @@
using FrostFS.Refs;
using FrostFS.SDK.Client.Mappers.GRPC;
namespace FrostFS.SDK;
public class FrostFsVersion(int major, int minor)
{
private Version? version;
public int Major { get; set; } = major;
public int Minor { get; set; } = minor;
internal Version VersionID
{
get
{
this.version ??= this.ToMessage();
return this.version;
}
}
public bool IsSupported(FrostFsVersion version)
{
if (version is null)
{
throw new System.ArgumentNullException(nameof(version));
}
return Major == version.Major;
}
public override string ToString()
{
return $"v{Major}.{Minor}";
}
}

View file

@ -0,0 +1,11 @@
namespace FrostFS.SDK
{
public interface IFrostFsFilter
{
FrostFsFilter[] Filters { get; }
string Key { get; }
string Name { get; }
int Operation { get; }
string Value { get; }
}
}

View file

@ -0,0 +1,7 @@
namespace FrostFS.SDK;
struct NodeAttrPair
{
internal string attr;
internal FrostFsNodeInfo[] nodes;
}

View file

@ -0,0 +1,8 @@
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
public enum FrostFsClause
{
Unspecified = 0,
Same,
Distinct
}

View file

@ -0,0 +1,456 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
internal struct Context
{
private const string errInvalidFilterName = "filter name is invalid";
private const string errInvalidFilterOp = "invalid filter operation";
private const string errFilterNotFound = "filter not found";
private const string errNonEmptyFilters = "simple filter contains sub-filters";
private const string errNotEnoughNodes = "not enough nodes to SELECT from";
private const string errUnnamedTopFilter = "unnamed top-level filter";
internal const string mainFilterName = "*";
internal const string likeWildcard = "*";
// network map to operate on
internal FrostFsNetmapSnapshot NetMap { get; }
// cache of processed filters
internal Dictionary<string, FrostFsFilter> ProcessedFilters { get; } = [];
// cache of processed selectors
internal Dictionary<string, FrostFsSelector> ProcessedSelectors { get; } = [];
// stores results of selector processing
internal Dictionary<string, List<List<FrostFsNodeInfo>>> Selections { get; } = [];
// cache of parsed numeric values
internal Dictionary<string, ulong> NumCache { get; } = [];
internal byte[]? HrwSeed { get; set; }
// hrw.Hash of hrwSeed
internal ulong HrwSeedHash { get; set; }
// container backup factor
internal uint Cbf { get; set; }
// nodes already used in previous selections, which is needed when the placement
// policy uses the UNIQUE flag. Nodes marked as used are not used in subsequent
// base selections.
internal Dictionary<ulong, bool> UsedNodes { get; } = [];
// If true, returns an error when netmap does not contain enough nodes for selection.
// By default best effort is taken.
internal bool Strict { get; set; }
// weightFunc is a weighting function for determining node priority
// which combines low price and high performance
private readonly Func<FrostFsNodeInfo, double> weightFunc;
public Context(FrostFsNetmapSnapshot netMap)
{
NetMap = netMap;
weightFunc = Tools.DefaultWeightFunc(NetMap.NodeInfoCollection);
}
internal void ProcessFilters(FrostFsPlacementPolicy policy)
{
foreach (var filter in policy.Filters)
{
ProcessFilter(filter, true);
}
}
readonly void ProcessFilter(FrostFsFilter filter, bool top)
{
var filterName = filter.Name;
if (filterName == mainFilterName)
{
throw new FrostFsException($"{errInvalidFilterName}: '{errInvalidFilterName}' is reserved");
}
if (top && string.IsNullOrEmpty(filterName))
{
throw new FrostFsException(errUnnamedTopFilter);
}
if (!top && !string.IsNullOrEmpty(filterName) && !ProcessedFilters.ContainsKey(filterName))
{
throw new FrostFsException(errFilterNotFound);
}
if (filter.Operation == (int)Operation.AND ||
filter.Operation == (int)Operation.OR ||
filter.Operation == (int)Operation.NOT)
{
foreach (var f in filter.Filters)
ProcessFilter(f, false);
}
else
{
if (filter.Filters.Length != 0)
{
throw new FrostFsException(errNonEmptyFilters);
}
else if (!top && !string.IsNullOrEmpty(filterName))
{
// named reference
return;
}
switch (filter.Operation)
{
case (int)Operation.EQ:
case (int)Operation.NE:
case (int)Operation.LIKE:
break;
case (int)Operation.GT:
case (int)Operation.GE:
case (int)Operation.LT:
case (int)Operation.LE:
{
var n = uint.Parse(filter.Value, CultureInfo.InvariantCulture);
NumCache[filter.Value] = n;
break;
}
default:
throw new FrostFsException($"{errInvalidFilterOp}: {filter.Operation}");
}
}
if (top)
{
ProcessedFilters[filterName] = filter;
}
}
// processSelectors processes selectors and returns error is any of them is invalid.
internal void ProcessSelectors(FrostFsPlacementPolicy policy)
{
foreach (var selector in policy.Selectors)
{
var filterName = selector.Filter;
if (filterName != mainFilterName)
{
if (selector.Filter == null || !ProcessedFilters.ContainsKey(selector.Filter))
{
throw new FrostFsException($"{errFilterNotFound}: SELECT FROM '{filterName}'");
}
}
ProcessedSelectors[selector.Name] = selector;
var selection = GetSelection(selector);
Selections[selector.Name] = selection;
}
}
// calcNodesCount returns number of buckets and minimum number of nodes in every bucket
// for the given selector.
static (int bucketCount, int nodesInBucket) CalcNodesCount(FrostFsSelector selector)
{
return selector.Clause == (int)FrostFsClause.Same
? (1, (int)selector.Count)
: ((int)selector.Count, 1);
}
// getSelectionBase returns nodes grouped by selector attribute.
// It it guaranteed that each pair will contain at least one node.
internal NodeAttrPair[] GetSelectionBase(FrostFsSelector selector)
{
var fName = selector.Filter ?? throw new FrostFsException("Filter name for selector is empty");
_ = ProcessedFilters.TryGetValue(fName, out var f);
var isMain = fName == mainFilterName;
var result = new List<NodeAttrPair>();
var nodeMap = new Dictionary<string, List<FrostFsNodeInfo>>();
var attr = selector.Attribute;
foreach (var node in NetMap.NodeInfoCollection)
{
if (UsedNodes.ContainsKey(node.Hash()))
{
continue;
}
if (isMain || Match(f, node))
{
if (attr == null)
{
// Default attribute is transparent identifier which is different for every node.
result.Add(new NodeAttrPair { attr = "", nodes = [node] });
}
else
{
var v = node.Attributes[attr];
if (!nodeMap.TryGetValue(v, out var nodes) || nodes == null)
{
nodeMap[v] = [];
}
nodeMap[v].Add(node);
}
}
}
if (!string.IsNullOrEmpty(attr))
{
foreach (var v in nodeMap)
{
result.Add(new NodeAttrPair() { attr = v.Key, nodes = [.. v.Value] });
}
}
if (HrwSeed != null && HrwSeed.Length != 0)
{
double[] ws = [];
var sortedNodes = new NodeAttrPair[result.Count];
for (int i = 0; i < result.Count; i++)
{
var res = result[i];
Tools.AppendWeightsTo(res.nodes, weightFunc, ref ws);
sortedNodes[i].nodes = Tools.SortHasherSliceByWeightValue(res.nodes.ToList(), ws, HrwSeedHash).ToArray();
sortedNodes[i].attr = result[i].attr;
}
return sortedNodes;
}
return [.. result];
}
static double CalcBucketWeight(List<FrostFsNodeInfo> ns, MeanIQRAgg a, Func<FrostFsNodeInfo, double> wf)
{
foreach (var node in ns)
{
a.Add(wf(node));
}
return a.Compute();
}
// getSelection returns nodes grouped by s.attribute.
// Last argument specifies if more buckets can be used to fulfill CBF.
internal List<List<FrostFsNodeInfo>> GetSelection(FrostFsSelector s)
{
var (bucketCount, nodesInBucket) = CalcNodesCount(s);
var buckets = GetSelectionBase(s);
if (Strict && buckets.Length < bucketCount)
throw new FrostFsException($"errNotEnoughNodes: '{s.Name}'");
// We need deterministic output in case there is no pivot.
// If pivot is set, buckets are sorted by HRW.
// However, because initial order influences HRW order for buckets with equal weights,
// we also need to have deterministic input to HRW sorting routine.
if (HrwSeed == null || HrwSeed.Length == 0)
{
buckets = string.IsNullOrEmpty(s.Attribute)
? [.. buckets.OrderBy(b => b.nodes[0].Hash())]
: [.. buckets.OrderBy(b => b.attr)];
}
var maxNodesInBucket = nodesInBucket * (int)Cbf;
var res = new List<List<FrostFsNodeInfo>>(buckets.Length);
var fallback = new List<List<FrostFsNodeInfo>>(buckets.Length);
for (int i = 0; i < buckets.Length; i++)
{
var ns = buckets[i].nodes;
if (ns.Length >= maxNodesInBucket)
{
res.Add(ns.Take(maxNodesInBucket).ToList());
}
else if (ns.Length >= nodesInBucket)
{
fallback.Add(new List<FrostFsNodeInfo>(ns));
}
}
if (res.Count < bucketCount)
{
// Fallback to using minimum allowed backup factor (1).
res.AddRange(fallback);
if (Strict && res.Count < bucketCount)
{
throw new FrostFsException($"{errNotEnoughNodes}: {s}");
}
}
if (HrwSeed != null && HrwSeed.Length != 0)
{
var weights = new double[res.Count];
var a = new MeanIQRAgg();
for (int i = 0; i < res.Count; i++)
{
a.Clear();
weights[i] = CalcBucketWeight(res[i], a, weightFunc);
}
var hashers = res.Select(r => new HasherList(r)).ToList();
hashers = Tools.SortHasherSliceByWeightValue(hashers, weights, HrwSeedHash);
for (int i = 0; i < res.Count; i++)
{
res[i] = hashers[i].Nodes;
}
}
if (res.Count < bucketCount)
{
if (Strict && res.Count == 0)
{
throw new FrostFsException(errNotEnoughNodes);
}
bucketCount = res.Count;
}
if (string.IsNullOrEmpty(s.Attribute))
{
fallback = res.Skip(bucketCount).ToList();
res = res.Take(bucketCount).ToList();
for (int i = 0; i < fallback.Count; i++)
{
var index = i % bucketCount;
if (res[index].Count >= maxNodesInBucket)
{
break;
}
res[index].AddRange(fallback[i]);
}
}
return res.Take(bucketCount).ToList();
}
internal bool MatchKeyValue(FrostFsFilter f, FrostFsNodeInfo nodeInfo)
{
switch (f.Operation)
{
case (int)Operation.EQ:
return nodeInfo.Attributes.TryGetValue(f.Key, out var val) && val == f.Value;
case (int)Operation.LIKE:
{
var hasPrefix = f.Value.StartsWith(likeWildcard, StringComparison.Ordinal);
var hasSuffix = f.Value.EndsWith(likeWildcard, StringComparison.Ordinal);
var start = hasPrefix ? likeWildcard.Length : 0;
var end = hasSuffix ? f.Value.Length - likeWildcard.Length : f.Value.Length;
var str = f.Value.Substring(start, end - start);
if (hasPrefix && hasSuffix)
return nodeInfo.Attributes[f.Key].Contains(str);
if (hasPrefix && !hasSuffix)
return nodeInfo.Attributes[f.Key].EndsWith(str, StringComparison.Ordinal);
if (!hasPrefix && hasSuffix)
return nodeInfo.Attributes[f.Key].StartsWith(str, StringComparison.Ordinal);
return nodeInfo.Attributes[f.Key] == f.Value;
}
case (int)Operation.NE:
return nodeInfo.Attributes[f.Key] != f.Value;
default:
{
ulong attr;
switch (f.Key)
{
case FrostFsNodeInfo.AttrPrice:
attr = nodeInfo.Price;
break;
case FrostFsNodeInfo.AttrCapacity:
attr = nodeInfo.GetCapacity();
break;
default:
if (!ulong.TryParse(nodeInfo.Attributes[f.Key], NumberStyles.Integer, CultureInfo.InvariantCulture, out attr))
return false;
break;
}
switch (f.Operation)
{
case (int)Operation.GT:
return attr > NumCache[f.Value];
case (int)Operation.GE:
return attr >= NumCache[f.Value];
case (int)Operation.LT:
return attr < NumCache[f.Value];
case (int)Operation.LE:
return attr <= NumCache[f.Value];
default:
// do nothing and return false
break;
}
}
break;
}
// will not happen if context was created from f (maybe panic?)
return false;
}
// match matches f against b. It returns no errors because
// filter should have been parsed during context creation
// and missing node properties are considered as a regular fail.
internal bool Match(FrostFsFilter f, FrostFsNodeInfo nodeInfo)
{
switch (f.Operation)
{
case (int)Operation.NOT:
{
var inner = f.Filters;
var fSub = inner[0];
if (!string.IsNullOrEmpty(inner[0].Name))
{
fSub = ProcessedFilters[inner[0].Name];
}
return !Match(fSub, nodeInfo);
}
case (int)Operation.AND:
case (int)Operation.OR:
{
for (int i = 0; i < f.Filters.Length; i++)
{
var fSub = f.Filters[i];
if (!string.IsNullOrEmpty(f.Filters[i].Name))
{
fSub = ProcessedFilters[f.Filters[i].Name];
}
var ok = Match(fSub, nodeInfo);
if (ok == (f.Operation == (int)Operation.OR))
{
return ok;
}
}
return f.Operation == (int)Operation.AND;
}
default:
return MatchKeyValue(f, nodeInfo);
}
}
}

View file

@ -0,0 +1,26 @@
using System.Collections.Generic;
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
internal sealed class HasherList : IHasher
{
private readonly List<FrostFsNodeInfo> _nodes;
internal HasherList(List<FrostFsNodeInfo> nodes)
{
_nodes = nodes;
}
internal List<FrostFsNodeInfo> Nodes
{
get
{
return _nodes;
}
}
public ulong Hash()
{
return _nodes.Count > 0 ? _nodes[0].Hash() : 0;
}
}

View file

@ -0,0 +1,8 @@
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
internal interface IAggregator
{
void Add(double d);
double Compute();
}

View file

@ -0,0 +1,6 @@
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
internal interface IHasher
{
ulong Hash();
}

View file

@ -0,0 +1,6 @@
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
interface INormalizer
{
double Normalize(double w);
}

View file

@ -0,0 +1,20 @@
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
internal struct MeanAgg
{
private double mean;
private int count;
internal void Add(double n)
{
int c = count + 1;
mean = mean * count / c + n / c;
count++;
}
internal readonly double Compute()
{
return mean;
}
}

View file

@ -0,0 +1,65 @@
using System.Collections.ObjectModel;
using System.Linq;
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
internal struct MeanIQRAgg : IAggregator
{
private const int minLn = 4;
internal Collection<double> arr = [];
public MeanIQRAgg()
{
}
public readonly void Add(double d)
{
arr.Add(d);
}
public readonly double Compute()
{
var length = arr.Count;
if (length == 0)
{
return 0;
}
var sorted = arr.OrderBy(p => p).ToArray();
double minV, maxV;
if (arr.Count < minLn)
{
minV = sorted[0];
maxV = sorted[length - 1];
}
else
{
var start = length / minLn;
var end = length * 3 / minLn - 1;
minV = sorted[start];
maxV = sorted[end];
}
var count = 0;
double sum = 0;
foreach (var e in sorted)
{
if (e >= minV && e <= maxV)
{
sum += e;
count++;
}
}
return sum / count;
}
internal readonly void Clear()
{
arr.Clear();
}
}

View file

@ -0,0 +1,27 @@
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
internal struct MinAgg
{
private double min;
private bool minFound;
internal void Add(double n)
{
if (!minFound)
{
min = n;
minFound = true;
return;
}
if (n < min)
{
min = n;
}
}
internal readonly double Compute()
{
return min;
}
}

View file

@ -0,0 +1,16 @@
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
public enum Operation
{
Unspecified = 0,
EQ,
NE,
GT,
GE,
LT,
LE,
OR,
AND,
NOT,
LIKE
}

View file

@ -0,0 +1,8 @@
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
internal struct ReverseMinNorm : INormalizer
{
internal double min;
public readonly double Normalize(double w) => (min + 1) / (w + 1);
}

View file

@ -0,0 +1,10 @@
using System.Collections.ObjectModel;
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
internal struct SelectFilterExpr(uint cbf, FrostFsSelector selector, Collection<FrostFsFilter> filters)
{
internal uint Cbf { get; } = cbf;
internal FrostFsSelector Selector { get; } = selector;
internal Collection<FrostFsFilter> Filters { get; } = filters;
}

View file

@ -0,0 +1,23 @@
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
internal readonly struct SigmoidNorm : INormalizer
{
private readonly double _scale;
internal SigmoidNorm(double scale)
{
_scale = scale;
}
public readonly double Normalize(double w)
{
if (_scale == 0)
{
return 0;
}
var x = w / _scale;
return x / (1 + x);
}
}

View file

@ -0,0 +1,138 @@
using System;
using System.Collections.Generic;
using System.Linq;
using static FrostFS.SDK.FrostFsNetmapSnapshot;
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
public static class Tools
{
internal static ulong Distance(ulong x, ulong y)
{
var acc = x ^ y;
acc ^= acc >> 33;
acc *= 0xff51afd7ed558ccd;
acc ^= acc >> 33;
acc *= 0xc4ceb9fe1a85ec53;
acc ^= acc >> 33;
return acc;
}
internal static double ReverceNormalize(double r, double w)
{
return (r + 1) / (w + 1);
}
internal static double Normalize(double r, double w)
{
if (r == 0)
{
return 0;
}
var x = w / r;
return x / (1 + x);
}
internal static void AppendWeightsTo(FrostFsNodeInfo[] nodes, Func<FrostFsNodeInfo, double> wf, ref double[] weights)
{
if (weights.Length < nodes.Length)
{
weights = new double[nodes.Length];
}
for (int i = 0; i < nodes.Length; i++)
{
weights[i] = wf(nodes[i]);
}
}
internal static List<T> SortHasherSliceByWeightValue<T>(List<T> nodes, Span<double> weights, ulong hash) where T : IHasher
{
if (nodes.Count == 0)
{
return nodes;
}
var allEquals = true;
if (weights.Length > 1)
{
for (int i = 1; i < weights.Length; i++)
{
if (weights[i] != weights[0])
{
allEquals = false;
break;
}
}
}
var dist = new double[nodes.Count];
if (allEquals)
{
for (int i = 0; i < dist.Length; i++)
{
var x = nodes[i].Hash();
dist[i] = Distance(x, hash);
}
return SortHasherByDistance(nodes, dist, true);
}
for (int i = 0; i < dist.Length; i++)
{
var d = Distance(nodes[i].Hash(), hash);
dist[i] = (ulong.MaxValue - d) * weights[i];
}
return SortHasherByDistance(nodes, dist, false);
}
internal static List<T> SortHasherByDistance<T, N>(List<T> nodes, N[] dist, bool asc)
{
IndexedValue<T, N>[] indexes = new IndexedValue<T, N>[nodes.Count];
for (int i = 0; i < dist.Length; i++)
{
indexes[i] = new IndexedValue<T, N>() { nodeInfo = nodes[i], dist = dist[i] };
}
if (asc)
{
return new List<T>(indexes
.OrderBy(x => x.dist)
.Select(x => x.nodeInfo).ToArray());
}
else
{
return new List<T>(indexes
.OrderByDescending(x => x.dist)
.Select(x => x.nodeInfo));
}
}
internal static Func<FrostFsNodeInfo, double> DefaultWeightFunc(IReadOnlyList<FrostFsNodeInfo> nodes)
{
MeanAgg mean = new();
MinAgg minV = new();
foreach (var node in nodes)
{
mean.Add(node.GetCapacity());
minV.Add(node.Price);
}
return NewWeightFunc(
NewSigmoidNorm(mean.Compute()),
NewReverseMinNorm(minV.Compute()));
}
private struct IndexedValue<T, N>
{
internal T nodeInfo;
internal N dist;
}
}

View file

@ -0,0 +1,48 @@
using FrostFS.Refs;
using FrostFS.SDK.Client.Mappers.GRPC;
namespace FrostFS.SDK;
public class FrostFsAddress
{
private FrostFsObjectId? frostFsObjectId;
private FrostFsContainerId? frostFsContainerId;
private ObjectID? objectId;
private ContainerID? containerId;
public FrostFsAddress(FrostFsContainerId frostFsContainerId, FrostFsObjectId frostFsObjectId)
{
FrostFsObjectId = frostFsObjectId ?? throw new System.ArgumentNullException(nameof(frostFsObjectId));
FrostFsContainerId = frostFsContainerId ?? throw new System.ArgumentNullException(nameof(frostFsContainerId));
}
internal FrostFsAddress(ObjectID objectId, ContainerID containerId)
{
ObjectId = objectId ?? throw new System.ArgumentNullException(nameof(objectId));
ContainerId = containerId ?? throw new System.ArgumentNullException(nameof(containerId));
}
public FrostFsObjectId FrostFsObjectId
{
get => frostFsObjectId ??= objectId!.ToModel();
set => frostFsObjectId = value;
}
public FrostFsContainerId FrostFsContainerId
{
get => frostFsContainerId ??= containerId!.ToModel();
set => frostFsContainerId = value;
}
public ObjectID ObjectId
{
get => objectId ??= frostFsObjectId!.ToMessage();
set => objectId = value;
}
public ContainerID ContainerId
{
get => containerId ??= frostFsContainerId!.ToMessage();
set => containerId = value;
}
}

View file

@ -0,0 +1,36 @@
namespace FrostFS.SDK;
public struct FrostFsAttributePair(string key, string value) : System.IEquatable<FrostFsAttributePair>
{
public string Key { get; set; } = key;
public string Value { get; set; } = value;
public override bool Equals(object obj)
{
if (obj == null || obj is not FrostFsAttributePair)
return false;
return Equals((FrostFsAttributePair)obj);
}
public override int GetHashCode()
{
return Key.GetHashCode() ^ Value.GetHashCode();
}
public static bool operator ==(FrostFsAttributePair left, FrostFsAttributePair right)
{
return left.Equals(right);
}
public static bool operator !=(FrostFsAttributePair left, FrostFsAttributePair right)
{
return !(left == right);
}
public bool Equals(FrostFsAttributePair other)
{
return GetHashCode().Equals(other.GetHashCode());
}
}

View file

@ -0,0 +1,8 @@
namespace FrostFS.SDK;
public class FrostFsHeaderResult
{
public FrostFsObjectHeader? HeaderInfo { get; internal set; }
public FrostFsSplitInfo? SplitInfo { get; internal set; }
}

View file

@ -0,0 +1,9 @@
namespace FrostFS.SDK;
public class FrostFsLargeObject(FrostFsContainerId container) : FrostFsObject(container)
{
public ulong PayloadLength
{
get { return Header!.PayloadLength; }
}
}

View file

@ -0,0 +1,21 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace FrostFS.SDK;
public class FrostFsLinkObject : FrostFsObject
{
public FrostFsLinkObject(FrostFsContainerId containerId,
SplitId splitId,
FrostFsObjectHeader largeObjectHeader,
IList<FrostFsObjectId> children)
: base(containerId)
{
Header!.Split = new FrostFsSplit(splitId,
null,
null,
largeObjectHeader,
null,
new ReadOnlyCollection<FrostFsObjectId>(children));
}
}

View file

@ -0,0 +1,64 @@
using System;
namespace FrostFS.SDK;
public class FrostFsObject
{
/// <summary>
/// Creates new instance from <c>ObjectHeader</c>
/// </summary>
/// <param name="header"></param> <summary>
public FrostFsObject(FrostFsObjectHeader header)
{
Header = header;
}
/// <summary>
/// Creates new instance with specified parameters
/// </summary>
/// <param name="container"></param>
/// <param name="objectType"></param>
public FrostFsObject(FrostFsContainerId container, FrostFsObjectType objectType = FrostFsObjectType.Regular)
{
Header = new FrostFsObjectHeader(containerId: container, type: objectType);
}
/// <summary>
/// Header contains metadata for the object
/// </summary>
/// <value></value>
public FrostFsObjectHeader Header { get; set; }
/// <summary>
/// The value is calculated internally as a hash of ObjectHeader. Do not use pre-calculated value is the object has been changed.
/// </summary>
public FrostFsObjectId? ObjectId
{
get; set;
}
/// <summary>
/// A payload is obtained via stream reader
/// </summary>
/// <value>Reader for received data</value>
public IObjectReader? ObjectReader { get; set; }
public ReadOnlyMemory<byte> SingleObjectPayload { get; set; }
/// <summary>
/// Provide SHA256 hash of the payload. If null, the hash is calculated by internal logic
/// </summary>
public byte[]? PayloadHash { get; set; }
/// <summary>
/// Applied only for the last Object in chain in case of manual multipart uploading
/// </summary>
/// <param name="largeObject">Parent for multipart object</param>
public void SetParent(FrostFsObjectHeader largeObjectHeader)
{
if (Header?.Split == null)
throw new ArgumentNullException(nameof(largeObjectHeader), "Split value must not be null");
Header.Split.ParentHeader = largeObjectHeader;
}
}

View file

@ -0,0 +1,111 @@
namespace FrostFS.SDK;
public interface IObjectFilter
{
public FrostFsMatchType MatchType { get; set; }
public string Key { get; set; }
string? GetSerializedValue();
}
public abstract class FrostFsObjectFilter<T>(FrostFsMatchType matchType, string key, T value) : IObjectFilter
{
public FrostFsMatchType MatchType { get; set; } = matchType;
public string Key { get; set; } = key;
public T Value { get; set; } = value;
public string? GetSerializedValue()
{
return Value?.ToString();
}
}
/// <summary>
/// Creates filter to search by Attribute
/// </summary>
/// <param name="matchType">Match type</param>
/// <param name="key">Attribute key</param>
/// <param name="value">Attribute value</param>
public class FilterByAttributePair(FrostFsMatchType matchType, string key, string value) : FrostFsObjectFilter<string>(matchType, key, value) { }
/// <summary>
/// Creates filter to search by ObjectId
/// </summary>
/// <param name="matchType">Match type</param>
/// <param name="objectId">ObjectId</param>
public class FilterByObjectId(FrostFsMatchType matchType, FrostFsObjectId objectId) : FrostFsObjectFilter<FrostFsObjectId>(matchType, Constants.FilterHeaderObjectID, objectId) { }
/// <summary>
/// Creates filter to search by OwnerId
/// </summary>
/// <param name="matchType">Match type</param>
/// <param name="ownerId">ObjectId</param>
public class FilterByOwnerId(FrostFsMatchType matchType, FrostFsOwner ownerId) : FrostFsObjectFilter<FrostFsOwner>(matchType, Constants.FilterHeaderOwnerID, ownerId) { }
/// <summary>
/// Creates filter to search by Version
/// </summary>
/// <param name="matchType">Match type</param>
/// <param name="version">Version</param>
public class FilterByVersion(FrostFsMatchType matchType, FrostFsVersion version) : FrostFsObjectFilter<FrostFsVersion>(matchType, Constants.FilterHeaderVersion, version) { }
/// <summary>
/// Creates filter to search by ContainerId
/// </summary>
/// <param name="matchType">Match type</param>
/// <param name="containerId">ContainerId</param>
public class FilterByContainerId(FrostFsMatchType matchType, FrostFsContainerId containerId) : FrostFsObjectFilter<FrostFsContainerId>(matchType, Constants.FilterHeaderContainerID, containerId) { }
/// <summary>
/// Creates filter to search by creation Epoch
/// </summary>
/// <param name="matchType">Match type</param>
/// <param name="epoch">Creation Epoch</param>
public class FilterByEpoch(FrostFsMatchType matchType, ulong epoch) : FrostFsObjectFilter<ulong>(matchType, Constants.FilterHeaderCreationEpoch, epoch) { }
/// <summary>
/// Creates filter to search by Payload Length
/// </summary>
/// <param name="matchType">Match type</param>
/// <param name="payloadLength">Payload Length</param>
public class FilterByPayloadLength(FrostFsMatchType matchType, ulong payloadLength) : FrostFsObjectFilter<ulong>(matchType, Constants.FilterHeaderPayloadLength, payloadLength) { }
/// <summary>
/// Creates filter to search by Payload Hash
/// </summary>
/// <param name="matchType">Match type</param>
/// <param name="payloadHash">Payload Hash</param>
public class FilterByPayloadHash(FrostFsMatchType matchType, CheckSum payloadHash) : FrostFsObjectFilter<CheckSum>(matchType, Constants.FilterHeaderPayloadHash, payloadHash) { }
/// <summary>
/// Creates filter to search by Parent
/// </summary>
/// <param name="matchType">Match type</param>
/// <param name="parentId">Parent</param>
public class FilterByParent(FrostFsMatchType matchType, FrostFsObjectId parentId) : FrostFsObjectFilter<FrostFsObjectId>(matchType, Constants.FilterHeaderParent, parentId) { }
/// <summary>
/// Creates filter to search by SplitId
/// </summary>
/// <param name="matchType">Match type</param>
/// <param name="splitId">SplitId</param>
public class FilterBySplitId(FrostFsMatchType matchType, SplitId splitId) : FrostFsObjectFilter<SplitId>(matchType, Constants.FilterHeaderSplitID, splitId) { }
/// <summary>
/// Creates filter to search by Payload Hash
/// </summary>
/// <param name="matchType">Match type</param>
/// <param name="ecParentId">Payload Hash</param>
public class FilterByECParent(FrostFsMatchType matchType, FrostFsObjectId ecParentId) : FrostFsObjectFilter<FrostFsObjectId>(matchType, Constants.FilterHeaderECParent, ecParentId) { }
/// <summary>
/// Creates filter to search Root objects
/// </summary>
public class FilterByRootObject() : FrostFsObjectFilter<string>(FrostFsMatchType.Unspecified, Constants.FilterHeaderRoot, string.Empty) { }
/// <summary>
/// Creates filter to search objects that are physically stored on the server
/// </summary
public class FilterByPhysicallyStored() : FrostFsObjectFilter<string>(FrostFsMatchType.Unspecified, Constants.FilterHeaderPhy, string.Empty) { }

View file

@ -0,0 +1,90 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using FrostFS.Object;
using FrostFS.SDK.Client.Mappers.GRPC;
namespace FrostFS.SDK;
public class FrostFsObjectHeader(
FrostFsContainerId containerId,
FrostFsObjectType type = FrostFsObjectType.Regular,
FrostFsAttributePair[]? attributes = null,
FrostFsSplit? split = null,
FrostFsOwner? owner = null,
FrostFsVersion? version = null)
{
private Header? header;
private Container.Container.Types.Attribute[]? grpsAttributes;
public ReadOnlyCollection<FrostFsAttributePair>? Attributes { get; internal set; } =
attributes == null ? null :
new ReadOnlyCollection<FrostFsAttributePair>(attributes);
public FrostFsContainerId ContainerId { get; } = containerId;
public ulong PayloadLength { get; set; }
public byte[]? PayloadCheckSum { get; set; }
public FrostFsObjectType ObjectType { get; } = type;
public FrostFsOwner? OwnerId { get; internal set; } = owner;
public FrostFsVersion? Version { get; internal set; } = version;
public FrostFsSplit? Split { get; internal set; } = split;
internal Container.Container.Types.Attribute[]? GetGrpsAttributes()
{
grpsAttributes ??= Attributes?
.Select(a => new Container.Container.Types.Attribute { Key = a.Key, Value = a.Value })
.ToArray();
return grpsAttributes;
}
public Header GetHeader()
{
if (header == null)
{
var objTypeName = ObjectType switch
{
FrostFsObjectType.Regular => Object.ObjectType.Regular,
FrostFsObjectType.Lock => Object.ObjectType.Lock,
FrostFsObjectType.Tombstone => Object.ObjectType.Tombstone,
_ => throw new ArgumentException($"Unknown ObjectType. Value: '{ObjectType}'.")
};
this.header = new Header
{
OwnerId = OwnerId?.ToMessage(),
Version = Version?.ToMessage(),
ContainerId = ContainerId.ToMessage(),
ObjectType = objTypeName,
PayloadLength = PayloadLength
};
if (Attributes != null)
{
foreach (var attribute in Attributes)
{
this.header.Attributes.Add(attribute.ToMessage());
}
}
var split = Split;
if (split != null)
{
this.header.Split = new Header.Types.Split
{
SplitId = split!.SplitId != null ? split.SplitId.GetSplitId() : null
};
}
}
return this.header;
}
}

View file

@ -0,0 +1,28 @@
using System;
using FrostFS.SDK.Cryptography;
namespace FrostFS.SDK;
public class FrostFsObjectId(string id)
{
public string Value { get; } = id;
public static FrostFsObjectId FromHash(ReadOnlySpan<byte> hash)
{
if (hash.Length != Constants.Sha256HashLength)
throw new FormatException("ObjectID must be a sha256 hash.");
return new FrostFsObjectId(Base58.Encode(hash));
}
public byte[] ToHash()
{
return Base58.Decode(Value);
}
public override string ToString()
{
return Value;
}
}

Some files were not shown because too many files have changed in this diff Show more