diff --git a/afs/oraclecloud/objectstorage/NebulaStore.Afs.OracleCloud.ObjectStorage.csproj b/afs/oraclecloud/objectstorage/NebulaStore.Afs.OracleCloud.ObjectStorage.csproj
new file mode 100644
index 0000000..c1d2f2d
--- /dev/null
+++ b/afs/oraclecloud/objectstorage/NebulaStore.Afs.OracleCloud.ObjectStorage.csproj
@@ -0,0 +1,39 @@
+
+
+
+ net9.0
+ enable
+ enable
+ latest
+ true
+ true
+
+ CS1591;NU1603
+
+
+
+ NebulaStore Abstract File System Oracle Cloud Object Storage
+ Oracle Cloud Infrastructure Object Storage adapter for NebulaStore Abstract File System
+ NebulaStore
+ NebulaStore
+ Copyright © NebulaStore 2025
+ 1.0.0.0
+ 1.0.0.0
+ NebulaStore.Afs.OracleCloud.ObjectStorage
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/afs/oraclecloud/objectstorage/README.md b/afs/oraclecloud/objectstorage/README.md
new file mode 100644
index 0000000..2ea3605
--- /dev/null
+++ b/afs/oraclecloud/objectstorage/README.md
@@ -0,0 +1,251 @@
+# NebulaStore Oracle Cloud Object Storage Adapter
+
+Oracle Cloud Infrastructure (OCI) Object Storage adapter for NebulaStore Abstract File System (AFS). This adapter enables NebulaStore to use OCI Object Storage as a persistent storage backend.
+
+## Features
+
+- **Multiple Authentication Methods**: Config file, instance principal authentication
+- **High Performance**: Optimized for large-scale object storage operations
+- **Multipart Upload Support**: Handles large files up to 50 GiB per object
+- **Namespace Auto-Detection**: Automatically detects OCI namespace from tenancy
+- **Configurable Timeouts**: Customizable connection and read timeouts
+- **Path Validation**: Validates bucket and object names according to OCI naming rules
+
+## Installation
+
+```bash
+dotnet add package NebulaStore.Afs.OracleCloud.ObjectStorage
+```
+
+## Prerequisites
+
+- .NET 9.0 or later
+- Oracle Cloud Infrastructure account
+- OCI Object Storage bucket
+- OCI credentials (config file or instance principal)
+
+## Quick Start
+
+### Using Config File Authentication
+
+```csharp
+using NebulaStore.Afs.OracleCloud.ObjectStorage;
+using NebulaStore.Afs.Blobstore;
+
+// Create configuration
+var config = OracleCloudObjectStorageConfiguration.New()
+ .SetConfigFile("~/.oci/config", "DEFAULT")
+ .SetRegion("us-ashburn-1")
+ .SetUseCache(true);
+
+// Create connector
+var connector = OracleCloudObjectStorageConnector.New(config);
+
+// Create file system
+var fileSystem = BlobStoreFileSystem.New(connector);
+
+// Use the file system
+var path = BlobStorePath.New("my-bucket", "data", "file.dat");
+fileSystem.WriteFile(path, dataBytes);
+```
+
+### Using Instance Principal Authentication
+
+For OCI compute instances:
+
+```csharp
+var config = OracleCloudObjectStorageConfiguration.New()
+ .SetAuthenticationType(OciAuthType.InstancePrincipal)
+ .SetRegion("us-ashburn-1")
+ .SetUseCache(true);
+
+var connector = OracleCloudObjectStorageConnector.New(config);
+var fileSystem = BlobStoreFileSystem.New(connector);
+```
+
+### Using AFS Integration Helper
+
+```csharp
+using NebulaStore.Afs.OracleCloud.ObjectStorage;
+
+// Create file system with default config file authentication
+var fileSystem = OracleCloudObjectStorageAfsIntegration.CreateFileSystem(
+ configFilePath: "~/.oci/config",
+ profile: "DEFAULT",
+ region: "us-ashburn-1",
+ useCache: true
+);
+
+// Or with instance principal
+var fileSystem = OracleCloudObjectStorageAfsIntegration.CreateFileSystemWithInstancePrincipal(
+ region: "us-ashburn-1",
+ useCache: true
+);
+```
+
+## Configuration Options
+
+### Authentication Types
+
+- **ConfigFile**: Use OCI configuration file (default: `~/.oci/config`)
+- **InstancePrincipal**: Use instance principal for OCI compute instances
+- **ResourcePrincipal**: Use resource principal for OCI Functions (not yet implemented)
+- **Simple**: Use simple authentication with credentials (not yet implemented)
+
+### Configuration Properties
+
+```csharp
+var config = OracleCloudObjectStorageConfiguration.New()
+ .SetConfigFile("~/.oci/config", "DEFAULT") // Config file and profile
+ .SetRegion("us-ashburn-1") // OCI region
+ .SetNamespace("my-namespace") // OCI namespace (auto-detected if not set)
+ .SetUseCache(true) // Enable caching
+ .SetConnectionTimeout(30000) // Connection timeout in ms
+ .SetReadTimeout(60000) // Read timeout in ms
+ .SetMaxAsyncThreads(50) // Max async threads
+ .SetMaxBlobSize(50L * 1024 * 1024 * 1024) // Max blob size (50 GiB)
+ .SetMaxRetryAttempts(3); // Max retry attempts
+```
+
+## OCI Configuration File
+
+Create an OCI configuration file at `~/.oci/config`:
+
+```ini
+[DEFAULT]
+user=ocid1.user.oc1..aaaaaaaxxxxx
+fingerprint=xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx
+tenancy=ocid1.tenancy.oc1..aaaaaaaxxxxx
+region=us-ashburn-1
+key_file=~/.oci/oci_api_key.pem
+```
+
+## Bucket Naming Rules
+
+OCI Object Storage bucket names must:
+- Be between 1 and 256 characters
+- Contain only lowercase letters, numbers, hyphens, underscores, and periods
+- Start and end with a letter or number
+- Not contain consecutive periods
+
+## Object Naming Rules
+
+OCI Object Storage object names:
+- Can be up to 1024 characters
+- Can contain any UTF-8 characters except null
+- Cannot be just "." or ".."
+- Should not have leading or trailing whitespace
+
+## Path Validation
+
+```csharp
+// Validate bucket name
+bool isValid = OracleCloudObjectStorageAfsIntegration.ValidateBucketName("my-bucket");
+
+// Validate object name
+bool isValid = OracleCloudObjectStorageAfsIntegration.ValidateObjectName("path/to/object.dat");
+```
+
+## Integration with Embedded Storage
+
+```csharp
+using NebulaStore.Storage.EmbeddedConfiguration;
+using NebulaStore.Afs.OracleCloud.ObjectStorage;
+
+// Create embedded storage configuration
+var config = OracleCloudObjectStorageAfsIntegration.CreateConfiguration(
+ bucketName: "my-storage-bucket",
+ configFilePath: "~/.oci/config",
+ profile: "DEFAULT",
+ region: "us-ashburn-1",
+ useCache: true
+);
+
+// Use with embedded storage (when AFS integration is complete)
+// var storageManager = EmbeddedStorage.Start(config);
+```
+
+## Advanced Configuration
+
+```csharp
+var config = OracleCloudObjectStorageConfiguration.New()
+ .SetConfigFile("~/.oci/config", "PRODUCTION")
+ .SetRegion("us-phoenix-1")
+ .SetNamespace("my-custom-namespace")
+ .SetConnectionTimeout(60000)
+ .SetReadTimeout(120000)
+ .SetMaxAsyncThreads(100)
+ .SetMaxBlobSize(10L * 1024 * 1024 * 1024) // 10 GiB per blob
+ .SetMaxRetryAttempts(5)
+ .SetUseCache(false); // Disable caching for real-time consistency
+
+config.Validate(); // Validate configuration before use
+```
+
+## Error Handling
+
+```csharp
+try
+{
+ var connector = OracleCloudObjectStorageConnector.New(config);
+ var fileSystem = BlobStoreFileSystem.New(connector);
+
+ // Perform operations
+ var path = BlobStorePath.New("my-bucket", "data", "file.dat");
+ fileSystem.WriteFile(path, dataBytes);
+}
+catch (InvalidOperationException ex)
+{
+ // Configuration error
+ Console.WriteLine($"Configuration error: {ex.Message}");
+}
+catch (ArgumentException ex)
+{
+ // Path validation error
+ Console.WriteLine($"Invalid path: {ex.Message}");
+}
+catch (Exception ex)
+{
+ // OCI API error
+ Console.WriteLine($"OCI error: {ex.Message}");
+}
+```
+
+## Performance Considerations
+
+- **Caching**: Enable caching (`SetUseCache(true)`) for better read performance
+- **Blob Size**: Adjust `MaxBlobSize` based on your data patterns (default: 50 GiB)
+- **Async Threads**: Increase `MaxAsyncThreads` for high-concurrency scenarios
+- **Timeouts**: Adjust timeouts based on network conditions and file sizes
+- **Retry Attempts**: Configure retry attempts for transient failures
+
+## Limitations
+
+- Resource principal authentication is not yet implemented
+- Simple authentication is not yet implemented
+- Custom endpoint configuration is not yet supported
+- Maximum object size is 50 GiB (OCI limit)
+
+## License
+
+This project is licensed under the Eclipse Public License 2.0 (EPL 2.0).
+
+## Contributing
+
+Contributions are welcome! Please see the main NebulaStore repository for contribution guidelines.
+
+## Support
+
+For issues and questions:
+- GitHub Issues: https://github.com/hadv/NebulaStore/issues
+- OCI Documentation: https://docs.oracle.com/en-us/iaas/Content/Object/home.htm
+- OCI .NET SDK: https://github.com/oracle/oci-dotnet-sdk
+
+## See Also
+
+- [NebulaStore Documentation](../../../README.md)
+- [Abstract File System (AFS)](../../blobstore/README.md)
+- [AWS S3 Adapter](../../aws/s3/README.md)
+- [Azure Storage Adapter](../../azure/storage/README.md)
+- [Oracle Cloud Infrastructure Object Storage](https://docs.oracle.com/en-us/iaas/Content/Object/home.htm)
+
diff --git a/afs/oraclecloud/objectstorage/src/OracleCloudObjectStorageAfsIntegration.cs b/afs/oraclecloud/objectstorage/src/OracleCloudObjectStorageAfsIntegration.cs
new file mode 100644
index 0000000..95e36da
--- /dev/null
+++ b/afs/oraclecloud/objectstorage/src/OracleCloudObjectStorageAfsIntegration.cs
@@ -0,0 +1,246 @@
+using NebulaStore.Afs.Blobstore;
+using NebulaStore.Storage.EmbeddedConfiguration;
+
+namespace NebulaStore.Afs.OracleCloud.ObjectStorage;
+
+///
+/// Integration utilities for using Oracle Cloud Infrastructure Object Storage with NebulaStore AFS.
+/// Provides helper methods to create storage configurations and file systems with OCI backend.
+///
+public static class OracleCloudObjectStorageAfsIntegration
+{
+ ///
+ /// Creates an embedded storage configuration for OCI Object Storage using config file authentication.
+ ///
+ /// The bucket name to use as storage directory
+ /// The path to OCI config file (default: ~/.oci/config)
+ /// The profile name in config file (default: DEFAULT)
+ /// The OCI region (optional)
+ /// Whether to enable caching (default: true)
+ /// A configured embedded storage configuration
+ public static IEmbeddedStorageConfiguration CreateConfiguration(
+ string bucketName,
+ string? configFilePath = null,
+ string? profile = null,
+ string? region = null,
+ bool useCache = true)
+ {
+ if (string.IsNullOrEmpty(bucketName))
+ throw new ArgumentException("Bucket name cannot be null or empty", nameof(bucketName));
+
+ var configBuilder = EmbeddedStorageConfiguration.New()
+ .SetStorageDirectory(bucketName)
+ .SetUseAfs(true)
+ .SetAfsStorageType("oraclecloud.objectstorage")
+ .SetAfsUseCache(useCache);
+
+ // Build connection string with config file path and profile
+ var connectionParts = new List();
+ if (!string.IsNullOrEmpty(configFilePath))
+ {
+ connectionParts.Add($"ConfigFile={configFilePath}");
+ }
+ if (!string.IsNullOrEmpty(profile))
+ {
+ connectionParts.Add($"Profile={profile}");
+ }
+ if (!string.IsNullOrEmpty(region))
+ {
+ connectionParts.Add($"Region={region}");
+ }
+
+ if (connectionParts.Any())
+ {
+ configBuilder.SetAfsConnectionString(string.Join(";", connectionParts));
+ }
+
+ return configBuilder.Build();
+ }
+
+ ///
+ /// Creates an embedded storage configuration for OCI Object Storage with advanced settings.
+ ///
+ /// The bucket name to use as storage directory
+ /// The path to OCI config file
+ /// The profile name in config file
+ /// The OCI region
+ /// The number of storage channels
+ /// The entity cache threshold in bytes
+ /// Whether to enable caching
+ /// A configured embedded storage configuration
+ public static IEmbeddedStorageConfiguration CreateAdvancedConfiguration(
+ string bucketName,
+ string? configFilePath = null,
+ string? profile = null,
+ string? region = null,
+ int channelCount = 1,
+ long entityCacheThreshold = 1000000,
+ bool useCache = true)
+ {
+ if (string.IsNullOrEmpty(bucketName))
+ throw new ArgumentException("Bucket name cannot be null or empty", nameof(bucketName));
+
+ var configBuilder = EmbeddedStorageConfiguration.New()
+ .SetStorageDirectory(bucketName)
+ .SetUseAfs(true)
+ .SetAfsStorageType("oraclecloud.objectstorage")
+ .SetAfsUseCache(useCache)
+ .SetChannelCount(channelCount)
+ .SetEntityCacheThreshold(entityCacheThreshold);
+
+ // Build connection string
+ var connectionParts = new List();
+ if (!string.IsNullOrEmpty(configFilePath))
+ {
+ connectionParts.Add($"ConfigFile={configFilePath}");
+ }
+ if (!string.IsNullOrEmpty(profile))
+ {
+ connectionParts.Add($"Profile={profile}");
+ }
+ if (!string.IsNullOrEmpty(region))
+ {
+ connectionParts.Add($"Region={region}");
+ }
+
+ if (connectionParts.Any())
+ {
+ configBuilder.SetAfsConnectionString(string.Join(";", connectionParts));
+ }
+
+ return configBuilder.Build();
+ }
+
+ ///
+ /// Creates a BlobStoreFileSystem for OCI Object Storage using config file authentication.
+ ///
+ /// The path to OCI config file (optional, defaults to ~/.oci/config)
+ /// The profile name (optional, defaults to DEFAULT)
+ /// The OCI region (optional)
+ /// Whether to enable caching
+ /// A blob store file system using OCI Object Storage
+ public static IBlobStoreFileSystem CreateFileSystem(
+ string? configFilePath = null,
+ string? profile = null,
+ string? region = null,
+ bool useCache = true)
+ {
+ var config = OracleCloudObjectStorageConfiguration.New()
+ .SetAuthenticationType(OciAuthType.ConfigFile)
+ .SetUseCache(useCache);
+
+ if (!string.IsNullOrEmpty(configFilePath))
+ {
+ config.SetConfigFile(configFilePath, profile);
+ }
+ else if (!string.IsNullOrEmpty(profile))
+ {
+ config.Profile = profile;
+ }
+
+ if (!string.IsNullOrEmpty(region))
+ {
+ config.SetRegion(region);
+ }
+
+ var connector = OracleCloudObjectStorageConnector.New(config);
+ return BlobStoreFileSystem.New(connector);
+ }
+
+ ///
+ /// Creates a BlobStoreFileSystem for OCI Object Storage using instance principal authentication.
+ ///
+ /// The OCI region
+ /// Whether to enable caching
+ /// A blob store file system using OCI Object Storage
+ public static IBlobStoreFileSystem CreateFileSystemWithInstancePrincipal(
+ string region,
+ bool useCache = true)
+ {
+ if (string.IsNullOrEmpty(region))
+ throw new ArgumentException("Region cannot be null or empty", nameof(region));
+
+ var config = OracleCloudObjectStorageConfiguration.New()
+ .SetAuthenticationType(OciAuthType.InstancePrincipal)
+ .SetRegion(region)
+ .SetUseCache(useCache);
+
+ var connector = OracleCloudObjectStorageConnector.New(config);
+ return BlobStoreFileSystem.New(connector);
+ }
+
+ ///
+ /// Creates a BlobStoreFileSystem for OCI Object Storage using resource principal authentication.
+ ///
+ /// The OCI region
+ /// Whether to enable caching
+ /// A blob store file system using OCI Object Storage
+ public static IBlobStoreFileSystem CreateFileSystemWithResourcePrincipal(
+ string region,
+ bool useCache = true)
+ {
+ if (string.IsNullOrEmpty(region))
+ throw new ArgumentException("Region cannot be null or empty", nameof(region));
+
+ var config = OracleCloudObjectStorageConfiguration.New()
+ .SetAuthenticationType(OciAuthType.ResourcePrincipal)
+ .SetRegion(region)
+ .SetUseCache(useCache);
+
+ var connector = OracleCloudObjectStorageConnector.New(config);
+ return BlobStoreFileSystem.New(connector);
+ }
+
+ ///
+ /// Creates a BlobStoreFileSystem for OCI Object Storage with a custom configuration.
+ ///
+ /// The OCI Object Storage configuration
+ /// A blob store file system using OCI Object Storage
+ public static IBlobStoreFileSystem CreateFileSystem(OracleCloudObjectStorageConfiguration configuration)
+ {
+ if (configuration == null)
+ throw new ArgumentNullException(nameof(configuration));
+
+ var connector = OracleCloudObjectStorageConnector.New(configuration);
+ return BlobStoreFileSystem.New(connector);
+ }
+
+ ///
+ /// Validates an OCI bucket name according to OCI naming rules.
+ ///
+ /// The bucket name to validate
+ /// True if the bucket name is valid
+ public static bool ValidateBucketName(string bucketName)
+ {
+ try
+ {
+ var validator = IOracleCloudObjectStoragePathValidator.New();
+ validator.ValidateBucketName(bucketName);
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Validates an OCI object name according to OCI naming rules.
+ ///
+ /// The object name to validate
+ /// True if the object name is valid
+ public static bool ValidateObjectName(string objectName)
+ {
+ try
+ {
+ var validator = IOracleCloudObjectStoragePathValidator.New();
+ validator.ValidateObjectName(objectName);
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+}
+
diff --git a/afs/oraclecloud/objectstorage/src/OracleCloudObjectStorageConfiguration.cs b/afs/oraclecloud/objectstorage/src/OracleCloudObjectStorageConfiguration.cs
new file mode 100644
index 0000000..2a5725d
--- /dev/null
+++ b/afs/oraclecloud/objectstorage/src/OracleCloudObjectStorageConfiguration.cs
@@ -0,0 +1,372 @@
+using Oci.Common;
+using Oci.Common.Auth;
+using Oci.ObjectstorageService;
+
+namespace NebulaStore.Afs.OracleCloud.ObjectStorage;
+
+///
+/// Authentication type for Oracle Cloud Infrastructure.
+///
+public enum OciAuthType
+{
+ ///
+ /// Use configuration file authentication (default ~/.oci/config).
+ ///
+ ConfigFile,
+
+ ///
+ /// Use instance principal authentication (for OCI compute instances).
+ ///
+ InstancePrincipal,
+
+ ///
+ /// Use resource principal authentication (for OCI functions).
+ ///
+ ResourcePrincipal,
+
+ ///
+ /// Use simple authentication with user OCID, tenancy OCID, fingerprint, and private key.
+ ///
+ Simple
+}
+
+///
+/// Configuration for Oracle Cloud Infrastructure Object Storage connections.
+/// Provides settings for authentication, connection, and performance tuning.
+///
+public class OracleCloudObjectStorageConfiguration
+{
+ ///
+ /// Gets or sets the authentication type.
+ ///
+ public OciAuthType AuthType { get; set; } = OciAuthType.ConfigFile;
+
+ ///
+ /// Gets or sets the path to the OCI configuration file.
+ /// Default is ~/.oci/config.
+ ///
+ public string? ConfigFilePath { get; set; }
+
+ ///
+ /// Gets or sets the profile name in the configuration file.
+ /// Default is "DEFAULT".
+ ///
+ public string? Profile { get; set; } = "DEFAULT";
+
+ ///
+ /// Gets or sets the OCI region identifier (e.g., "us-ashburn-1").
+ ///
+ public string? Region { get; set; }
+
+ ///
+ /// Gets or sets the OCI Object Storage namespace.
+ /// If not set, it will be auto-detected from the tenancy.
+ ///
+ public string? Namespace { get; set; }
+
+ ///
+ /// Gets or sets the custom endpoint URL for Object Storage.
+ ///
+ public string? Endpoint { get; set; }
+
+ ///
+ /// Gets or sets the user OCID for simple authentication.
+ ///
+ public string? UserOcid { get; set; }
+
+ ///
+ /// Gets or sets the tenancy OCID for simple authentication.
+ ///
+ public string? TenancyOcid { get; set; }
+
+ ///
+ /// Gets or sets the fingerprint for simple authentication.
+ ///
+ public string? Fingerprint { get; set; }
+
+ ///
+ /// Gets or sets the private key file path for simple authentication.
+ ///
+ public string? PrivateKeyPath { get; set; }
+
+ ///
+ /// Gets or sets the private key passphrase for simple authentication.
+ ///
+ public string? PrivateKeyPassphrase { get; set; }
+
+ ///
+ /// Gets or sets whether to enable caching for improved performance.
+ ///
+ public bool UseCache { get; set; } = true;
+
+ ///
+ /// Gets or sets the connection timeout in milliseconds.
+ ///
+ public int ConnectionTimeoutMilliseconds { get; set; } = 30000;
+
+ ///
+ /// Gets or sets the read timeout in milliseconds.
+ ///
+ public int ReadTimeoutMilliseconds { get; set; } = 60000;
+
+ ///
+ /// Gets or sets the maximum number of async threads.
+ ///
+ public int MaxAsyncThreads { get; set; } = 50;
+
+ ///
+ /// Gets or sets the maximum blob size in bytes before splitting into multiple blobs.
+ /// Default is 50 GiB (Oracle Cloud Object Storage limit per object).
+ ///
+ public long MaxBlobSize { get; set; } = 50L * 1024 * 1024 * 1024; // 50 GiB
+
+ ///
+ /// Gets or sets the maximum number of retry attempts.
+ ///
+ public int MaxRetryAttempts { get; set; } = 3;
+
+ ///
+ /// Creates a new Oracle Cloud Object Storage configuration with default values.
+ ///
+ /// A new configuration instance
+ public static OracleCloudObjectStorageConfiguration New() => new();
+
+ ///
+ /// Sets the authentication type.
+ ///
+ /// The authentication type
+ /// This configuration instance for method chaining
+ public OracleCloudObjectStorageConfiguration SetAuthenticationType(OciAuthType authType)
+ {
+ AuthType = authType;
+ return this;
+ }
+
+ ///
+ /// Sets the configuration file path and profile.
+ ///
+ /// The path to the OCI config file
+ /// The profile name (default is "DEFAULT")
+ /// This configuration instance for method chaining
+ public OracleCloudObjectStorageConfiguration SetConfigFile(string configFilePath, string? profile = null)
+ {
+ ConfigFilePath = configFilePath;
+ if (profile != null)
+ {
+ Profile = profile;
+ }
+ AuthType = OciAuthType.ConfigFile;
+ return this;
+ }
+
+ ///
+ /// Sets the OCI region.
+ ///
+ /// The OCI region identifier
+ /// This configuration instance for method chaining
+ public OracleCloudObjectStorageConfiguration SetRegion(string region)
+ {
+ Region = region;
+ return this;
+ }
+
+ ///
+ /// Sets the OCI Object Storage namespace.
+ ///
+ /// The namespace
+ /// This configuration instance for method chaining
+ public OracleCloudObjectStorageConfiguration SetNamespace(string @namespace)
+ {
+ Namespace = @namespace;
+ return this;
+ }
+
+ ///
+ /// Sets the custom endpoint URL.
+ ///
+ /// The endpoint URL
+ /// This configuration instance for method chaining
+ public OracleCloudObjectStorageConfiguration SetEndpoint(string endpoint)
+ {
+ Endpoint = endpoint;
+ return this;
+ }
+
+ ///
+ /// Sets simple authentication credentials.
+ ///
+ /// The user OCID
+ /// The tenancy OCID
+ /// The fingerprint
+ /// The private key file path
+ /// The private key passphrase (optional)
+ /// This configuration instance for method chaining
+ public OracleCloudObjectStorageConfiguration SetSimpleAuth(
+ string userOcid,
+ string tenancyOcid,
+ string fingerprint,
+ string privateKeyPath,
+ string? privateKeyPassphrase = null)
+ {
+ UserOcid = userOcid;
+ TenancyOcid = tenancyOcid;
+ Fingerprint = fingerprint;
+ PrivateKeyPath = privateKeyPath;
+ PrivateKeyPassphrase = privateKeyPassphrase;
+ AuthType = OciAuthType.Simple;
+ return this;
+ }
+
+ ///
+ /// Sets whether to enable caching.
+ ///
+ /// True to enable caching
+ /// This configuration instance for method chaining
+ public OracleCloudObjectStorageConfiguration SetUseCache(bool useCache)
+ {
+ UseCache = useCache;
+ return this;
+ }
+
+ ///
+ /// Sets the connection timeout.
+ ///
+ /// The timeout in milliseconds
+ /// This configuration instance for method chaining
+ public OracleCloudObjectStorageConfiguration SetConnectionTimeout(int timeoutMilliseconds)
+ {
+ ConnectionTimeoutMilliseconds = timeoutMilliseconds;
+ return this;
+ }
+
+ ///
+ /// Sets the read timeout.
+ ///
+ /// The timeout in milliseconds
+ /// This configuration instance for method chaining
+ public OracleCloudObjectStorageConfiguration SetReadTimeout(int timeoutMilliseconds)
+ {
+ ReadTimeoutMilliseconds = timeoutMilliseconds;
+ return this;
+ }
+
+ ///
+ /// Sets the maximum number of async threads.
+ ///
+ /// The maximum async threads
+ /// This configuration instance for method chaining
+ public OracleCloudObjectStorageConfiguration SetMaxAsyncThreads(int maxAsyncThreads)
+ {
+ MaxAsyncThreads = maxAsyncThreads;
+ return this;
+ }
+
+ ///
+ /// Sets the maximum blob size before splitting.
+ ///
+ /// The maximum blob size in bytes
+ /// This configuration instance for method chaining
+ public OracleCloudObjectStorageConfiguration SetMaxBlobSize(long maxBlobSize)
+ {
+ MaxBlobSize = maxBlobSize;
+ return this;
+ }
+
+ ///
+ /// Sets the maximum number of retry attempts.
+ ///
+ /// The maximum retry attempts
+ /// This configuration instance for method chaining
+ public OracleCloudObjectStorageConfiguration SetMaxRetryAttempts(int maxRetryAttempts)
+ {
+ MaxRetryAttempts = maxRetryAttempts;
+ return this;
+ }
+
+ ///
+ /// Creates an authentication details provider based on the configuration.
+ ///
+ /// The authentication details provider
+ public IBasicAuthenticationDetailsProvider CreateAuthenticationProvider()
+ {
+ return AuthType switch
+ {
+ OciAuthType.ConfigFile => CreateConfigFileProvider(),
+ OciAuthType.InstancePrincipal => CreateInstancePrincipalProvider(),
+ OciAuthType.ResourcePrincipal => CreateResourcePrincipalProvider(),
+ OciAuthType.Simple => CreateSimpleProvider(),
+ _ => throw new InvalidOperationException($"Unsupported authentication type: {AuthType}")
+ };
+ }
+
+ private IBasicAuthenticationDetailsProvider CreateConfigFileProvider()
+ {
+ if (!string.IsNullOrEmpty(ConfigFilePath))
+ {
+ return new ConfigFileAuthenticationDetailsProvider(ConfigFilePath, Profile);
+ }
+ return new ConfigFileAuthenticationDetailsProvider(Profile);
+ }
+
+ private IBasicAuthenticationDetailsProvider CreateInstancePrincipalProvider()
+ {
+ return new InstancePrincipalsAuthenticationDetailsProvider();
+ }
+
+ private IBasicAuthenticationDetailsProvider CreateResourcePrincipalProvider()
+ {
+ // Resource principal authentication for OCI Functions
+ throw new NotImplementedException(
+ "Resource principal authentication is not yet implemented. " +
+ "Please use config file or instance principal authentication instead.");
+ }
+
+ private IBasicAuthenticationDetailsProvider CreateSimpleProvider()
+ {
+ if (string.IsNullOrEmpty(UserOcid) || string.IsNullOrEmpty(TenancyOcid) ||
+ string.IsNullOrEmpty(Fingerprint) || string.IsNullOrEmpty(PrivateKeyPath))
+ {
+ throw new InvalidOperationException(
+ "Simple authentication requires UserOcid, TenancyOcid, Fingerprint, and PrivateKeyPath");
+ }
+
+ // Use config file approach for simple auth
+ // Create a temporary config file or use the existing one
+ throw new NotImplementedException(
+ "Simple authentication is not yet implemented. " +
+ "Please use config file or instance principal authentication instead.");
+ }
+
+ ///
+ /// Validates the configuration and throws an exception if invalid.
+ ///
+ /// Thrown when configuration is invalid
+ public void Validate()
+ {
+ if (ConnectionTimeoutMilliseconds <= 0)
+ {
+ throw new InvalidOperationException("Connection timeout must be greater than zero");
+ }
+
+ if (ReadTimeoutMilliseconds <= 0)
+ {
+ throw new InvalidOperationException("Read timeout must be greater than zero");
+ }
+
+ if (MaxRetryAttempts < 0)
+ {
+ throw new InvalidOperationException("Max retry attempts cannot be negative");
+ }
+
+ if (MaxBlobSize <= 0)
+ {
+ throw new InvalidOperationException("Max blob size must be greater than zero");
+ }
+
+ if (MaxAsyncThreads <= 0)
+ {
+ throw new InvalidOperationException("Max async threads must be greater than zero");
+ }
+ }
+}
+
diff --git a/afs/oraclecloud/objectstorage/src/OracleCloudObjectStorageConnector.cs b/afs/oraclecloud/objectstorage/src/OracleCloudObjectStorageConnector.cs
new file mode 100644
index 0000000..1600471
--- /dev/null
+++ b/afs/oraclecloud/objectstorage/src/OracleCloudObjectStorageConnector.cs
@@ -0,0 +1,689 @@
+using System.Text;
+using System.Text.RegularExpressions;
+using Oci.Common.Auth;
+using Oci.ObjectstorageService;
+using Oci.ObjectstorageService.Models;
+using Oci.ObjectstorageService.Requests;
+using Oci.ObjectstorageService.Responses;
+using NebulaStore.Afs.Blobstore;
+using NebulaStore.Afs.Blobstore.Types;
+
+namespace NebulaStore.Afs.OracleCloud.ObjectStorage;
+
+///
+/// Oracle Cloud Infrastructure Object Storage implementation of IBlobStoreConnector.
+/// Stores blobs as objects in OCI Object Storage buckets.
+///
+///
+/// This connector stores files as numbered blob objects in OCI buckets.
+/// Each blob can be up to 50 GiB (OCI object size limit) and larger files
+/// are split across multiple objects.
+///
+/// First create an OCI Object Storage client and configuration:
+///
+/// var config = OracleCloudObjectStorageConfiguration.New()
+/// .SetConfigFile("~/.oci/config", "DEFAULT")
+/// .SetRegion("us-ashburn-1")
+/// .SetUseCache(true);
+///
+/// var connector = OracleCloudObjectStorageConnector.New(config);
+/// var fileSystem = BlobStoreFileSystem.New(connector);
+///
+///
+public class OracleCloudObjectStorageConnector : BlobStoreConnectorBase
+{
+ private readonly ObjectStorageClient _client;
+ private readonly OracleCloudObjectStorageConfiguration _configuration;
+ private readonly IOracleCloudObjectStoragePathValidator _pathValidator;
+ private string? _namespace;
+
+ ///
+ /// Initializes a new instance of the OracleCloudObjectStorageConnector class.
+ ///
+ /// The OCI Object Storage client
+ /// The OCI configuration
+ /// The path validator
+ private OracleCloudObjectStorageConnector(
+ ObjectStorageClient client,
+ OracleCloudObjectStorageConfiguration configuration,
+ IOracleCloudObjectStoragePathValidator pathValidator)
+ {
+ _client = client ?? throw new ArgumentNullException(nameof(client));
+ _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
+ _pathValidator = pathValidator ?? throw new ArgumentNullException(nameof(pathValidator));
+ }
+
+ ///
+ /// Creates a new Oracle Cloud Object Storage connector.
+ ///
+ /// The OCI configuration
+ /// A new OCI connector instance
+ public static OracleCloudObjectStorageConnector New(OracleCloudObjectStorageConfiguration configuration)
+ {
+ configuration?.Validate();
+ var authProvider = configuration!.CreateAuthenticationProvider();
+ var client = new ObjectStorageClient(authProvider);
+
+ if (!string.IsNullOrEmpty(configuration.Region))
+ {
+ client.SetRegion(configuration.Region);
+ }
+
+ // Note: Custom endpoint configuration would need to be set via ClientConfiguration
+ // if needed in the future
+
+ var pathValidator = IOracleCloudObjectStoragePathValidator.New();
+ return new OracleCloudObjectStorageConnector(client, configuration, pathValidator);
+ }
+
+ ///
+ /// Gets the OCI Object Storage namespace.
+ ///
+ /// The namespace
+ private string GetNamespace()
+ {
+ if (_namespace != null)
+ {
+ return _namespace;
+ }
+
+ if (!string.IsNullOrEmpty(_configuration.Namespace))
+ {
+ _namespace = _configuration.Namespace;
+ return _namespace;
+ }
+
+ // Auto-detect namespace
+ var request = new GetNamespaceRequest();
+ var response = _client.GetNamespace(request).Result;
+ _namespace = response.Value;
+ return _namespace;
+ }
+
+ public override long GetFileSize(BlobStorePath file)
+ {
+ EnsureNotDisposed();
+ file.Validate(_pathValidator);
+
+ try
+ {
+ var blobs = GetBlobs(file);
+ return blobs.Sum(blob => blob.Size ?? 0);
+ }
+ catch (Exception ex) when (IsNotFoundException(ex))
+ {
+ return 0;
+ }
+ }
+
+ public override bool DirectoryExists(BlobStorePath directory)
+ {
+ EnsureNotDisposed();
+ directory.Validate(_pathValidator);
+
+ try
+ {
+ var containerKey = GetContainerKey(directory);
+ if (string.IsNullOrEmpty(containerKey) || containerKey == BlobStorePath.Separator)
+ {
+ return true;
+ }
+
+ // Check if any objects exist with this prefix
+ var request = new ListObjectsRequest
+ {
+ NamespaceName = GetNamespace(),
+ BucketName = directory.Container,
+ Prefix = containerKey,
+ Limit = 1
+ };
+
+ var response = _client.ListObjects(request).Result;
+ return response.ListObjects.Objects.Any();
+ }
+ catch (Exception ex) when (IsNotFoundException(ex))
+ {
+ return false;
+ }
+ }
+
+ public override bool FileExists(BlobStorePath file)
+ {
+ EnsureNotDisposed();
+ file.Validate(_pathValidator);
+
+ try
+ {
+ var blobs = GetBlobs(file);
+ return blobs.Any();
+ }
+ catch (Exception ex) when (IsNotFoundException(ex))
+ {
+ return false;
+ }
+ }
+
+ public override void VisitChildren(BlobStorePath directory, IBlobStorePathVisitor visitor)
+ {
+ EnsureNotDisposed();
+ directory.Validate(_pathValidator);
+
+ if (visitor == null)
+ {
+ throw new ArgumentNullException(nameof(visitor));
+ }
+
+ var childKeys = GetChildKeys(directory);
+ var prefix = GetChildKeysPrefix(directory);
+
+ foreach (var key in childKeys)
+ {
+ if (IsDirectoryKey(key))
+ {
+ var dirName = key.Substring(prefix.Length).TrimEnd('/');
+ if (!string.IsNullOrEmpty(dirName))
+ {
+ visitor.VisitDirectory(directory, dirName);
+ }
+ }
+ else
+ {
+ var fileName = RemoveNumberSuffix(key.Substring(prefix.Length));
+ if (!string.IsNullOrEmpty(fileName))
+ {
+ visitor.VisitFile(directory, fileName);
+ }
+ }
+ }
+ }
+
+ public override bool IsEmpty(BlobStorePath directory)
+ {
+ EnsureNotDisposed();
+ directory.Validate(_pathValidator);
+
+ try
+ {
+ var request = new ListObjectsRequest
+ {
+ NamespaceName = GetNamespace(),
+ BucketName = directory.Container,
+ Prefix = GetContainerKey(directory),
+ Limit = 1
+ };
+
+ var response = _client.ListObjects(request).Result;
+ return !response.ListObjects.Objects.Any();
+ }
+ catch (Exception ex) when (IsNotFoundException(ex))
+ {
+ return true;
+ }
+ }
+
+ public override bool CreateDirectory(BlobStorePath directory)
+ {
+ EnsureNotDisposed();
+ directory.Validate(_pathValidator);
+
+ // OCI Object Storage doesn't require explicit directory creation
+ // Directories are implicit based on object key prefixes
+ return true;
+ }
+
+ public override bool CreateFile(BlobStorePath file)
+ {
+ EnsureNotDisposed();
+ file.Validate(_pathValidator);
+
+ // OCI Object Storage doesn't require explicit file creation
+ return true;
+ }
+
+ public override bool DeleteFile(BlobStorePath file)
+ {
+ EnsureNotDisposed();
+ file.Validate(_pathValidator);
+
+ var blobs = GetBlobs(file).ToList();
+ if (!blobs.Any())
+ {
+ return false;
+ }
+
+ return DeleteBlobs(file, blobs);
+ }
+
+ private List GetBlobs(BlobStorePath file)
+ {
+ var prefix = GetBlobKeyPrefix(file);
+ var pattern = new Regex(GetBlobKeyRegex(prefix));
+ var blobs = new List();
+ string? nextStartWith = null;
+
+ do
+ {
+ var request = new ListObjectsRequest
+ {
+ NamespaceName = GetNamespace(),
+ BucketName = file.Container,
+ Prefix = prefix,
+ Start = nextStartWith,
+ Fields = "name,size"
+ };
+
+ var response = _client.ListObjects(request).Result;
+ blobs.AddRange(response.ListObjects.Objects);
+ nextStartWith = response.ListObjects.NextStartWith;
+ }
+ while (nextStartWith != null);
+
+ return blobs
+ .Where(obj => pattern.IsMatch(obj.Name))
+ .OrderBy(obj => GetBlobNumber(obj.Name))
+ .ToList();
+ }
+
+ private IEnumerable GetChildKeys(BlobStorePath directory)
+ {
+ var childKeys = new HashSet();
+ var prefix = GetChildKeysPrefix(directory);
+ string? nextStartWith = null;
+
+ do
+ {
+ var request = new ListObjectsRequest
+ {
+ NamespaceName = GetNamespace(),
+ BucketName = directory.Container,
+ Prefix = prefix,
+ Delimiter = BlobStorePath.Separator,
+ Start = nextStartWith,
+ Fields = "name"
+ };
+
+ var response = _client.ListObjects(request).Result;
+
+ // Add directories (prefixes)
+ if (response.ListObjects.Prefixes != null)
+ {
+ childKeys.UnionWith(response.ListObjects.Prefixes);
+ }
+
+ // Add files
+ childKeys.UnionWith(response.ListObjects.Objects.Select(obj => obj.Name));
+
+ nextStartWith = response.ListObjects.NextStartWith;
+ }
+ while (nextStartWith != null);
+
+ return childKeys.Where(path => !path.Equals(prefix));
+ }
+
+ private bool DeleteBlobs(BlobStorePath file, List blobs)
+ {
+ var success = true;
+
+ foreach (var blob in blobs)
+ {
+ try
+ {
+ var request = new DeleteObjectRequest
+ {
+ NamespaceName = GetNamespace(),
+ BucketName = file.Container,
+ ObjectName = blob.Name
+ };
+
+ _client.DeleteObject(request).Wait();
+ }
+ catch
+ {
+ success = false;
+ }
+ }
+
+ return success;
+ }
+
+ private string GetContainerKey(BlobStorePath path)
+ {
+ return ToContainerKey(path);
+ }
+
+ private string GetChildKeysPrefix(BlobStorePath directory)
+ {
+ return ToContainerKey(directory);
+ }
+
+ private string GetBlobKeyPrefix(BlobStorePath file)
+ {
+ return ToBlobKeyPrefix(file);
+ }
+
+ private string GetBlobKeyRegex(string prefix)
+ {
+ return $"^{Regex.Escape(prefix)}\\d+$";
+ }
+
+ private long GetBlobNumber(string key)
+ {
+ var lastDot = key.LastIndexOf(NumberSuffixSeparatorChar);
+ if (lastDot > 0 && long.TryParse(key.Substring(lastDot + 1), out var number))
+ {
+ return number;
+ }
+ return 0;
+ }
+
+ private bool IsNotFoundException(Exception ex)
+ {
+ return ex.Message.Contains("NotFound") || ex.Message.Contains("404");
+ }
+
+ public override byte[] ReadData(BlobStorePath file, long offset, long length)
+ {
+ EnsureNotDisposed();
+ file.Validate(_pathValidator);
+
+ if (offset < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(offset), "Offset cannot be negative");
+ }
+
+ if (length < 0)
+ {
+ length = GetFileSize(file) - offset;
+ }
+
+ if (length == 0)
+ {
+ return Array.Empty();
+ }
+
+ var buffer = new byte[length];
+ var bytesRead = ReadData(file, buffer, offset, length);
+
+ if (bytesRead < length)
+ {
+ Array.Resize(ref buffer, (int)bytesRead);
+ }
+
+ return buffer;
+ }
+
+ public override long ReadData(BlobStorePath file, byte[] targetBuffer, long offset, long length)
+ {
+ EnsureNotDisposed();
+ file.Validate(_pathValidator);
+
+ if (targetBuffer == null)
+ {
+ throw new ArgumentNullException(nameof(targetBuffer));
+ }
+
+ if (offset < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(offset), "Offset cannot be negative");
+ }
+
+ if (length < 0)
+ {
+ length = GetFileSize(file) - offset;
+ }
+
+ if (length == 0)
+ {
+ return 0;
+ }
+
+ var blobs = GetBlobs(file);
+ long currentOffset = 0;
+ long totalBytesRead = 0;
+ int bufferPosition = 0;
+
+ foreach (var blob in blobs)
+ {
+ var blobSize = blob.Size ?? 0;
+ var blobEndOffset = currentOffset + blobSize;
+
+ // Skip blobs before the requested offset
+ if (blobEndOffset <= offset)
+ {
+ currentOffset = blobEndOffset;
+ continue;
+ }
+
+ // Stop if we've read all requested data
+ if (totalBytesRead >= length)
+ {
+ break;
+ }
+
+ // Calculate read range within this blob
+ var blobReadOffset = Math.Max(0, offset - currentOffset);
+ var remainingToRead = length - totalBytesRead;
+ var blobReadLength = Math.Min(blobSize - blobReadOffset, remainingToRead);
+
+ // Read from this blob
+ var bytesRead = ReadBlobData(file, blob, targetBuffer, bufferPosition, blobReadOffset, blobReadLength);
+ totalBytesRead += bytesRead;
+ bufferPosition += (int)bytesRead;
+ currentOffset = blobEndOffset;
+ }
+
+ return totalBytesRead;
+ }
+
+ private long ReadBlobData(
+ BlobStorePath file,
+ ObjectSummary blob,
+ byte[] targetBuffer,
+ int bufferOffset,
+ long blobOffset,
+ long length)
+ {
+ var request = new GetObjectRequest
+ {
+ NamespaceName = GetNamespace(),
+ BucketName = file.Container,
+ ObjectName = blob.Name
+ };
+
+ // Set range header for partial read
+ if (blobOffset > 0 || length < (blob.Size ?? 0))
+ {
+ var rangeEnd = blobOffset + length - 1;
+ request.Range = new Oci.Common.Model.Range
+ {
+ StartByte = blobOffset,
+ EndByte = rangeEnd
+ };
+ }
+
+ var response = _client.GetObject(request).Result;
+ using var stream = response.InputStream;
+
+ long totalRead = 0;
+ int bytesRead;
+ var tempBuffer = new byte[8192];
+
+ while (totalRead < length && (bytesRead = stream.Read(tempBuffer, 0, (int)Math.Min(tempBuffer.Length, length - totalRead))) > 0)
+ {
+ Array.Copy(tempBuffer, 0, targetBuffer, bufferOffset + totalRead, bytesRead);
+ totalRead += bytesRead;
+ }
+
+ return totalRead;
+ }
+
+ public override long WriteData(BlobStorePath file, IEnumerable sourceBuffers)
+ {
+ EnsureNotDisposed();
+ file.Validate(_pathValidator);
+
+ if (sourceBuffers == null)
+ {
+ throw new ArgumentNullException(nameof(sourceBuffers));
+ }
+
+ // Delete existing blobs for this file
+ var existingBlobs = GetBlobs(file).ToList();
+ if (existingBlobs.Any())
+ {
+ DeleteBlobs(file, existingBlobs);
+ }
+
+ long totalBytesWritten = 0;
+ long blobNumber = 0;
+ var maxBlobSize = _configuration.MaxBlobSize;
+
+ using var memoryStream = new MemoryStream();
+
+ foreach (var buffer in sourceBuffers)
+ {
+ if (buffer == null || buffer.Length == 0)
+ {
+ continue;
+ }
+
+ memoryStream.Write(buffer, 0, buffer.Length);
+
+ // Write blob if we've reached the max size
+ while (memoryStream.Length >= maxBlobSize)
+ {
+ var bytesWritten = WriteBlob(file, blobNumber++, memoryStream, maxBlobSize);
+ totalBytesWritten += bytesWritten;
+ }
+ }
+
+ // Write remaining data
+ if (memoryStream.Length > 0)
+ {
+ memoryStream.Position = 0;
+ var bytesWritten = WriteBlob(file, blobNumber, memoryStream, memoryStream.Length);
+ totalBytesWritten += bytesWritten;
+ }
+
+ return totalBytesWritten;
+ }
+
+ private long WriteBlob(BlobStorePath file, long blobNumber, MemoryStream sourceStream, long length)
+ {
+ var blobKey = ToBlobKey(file, blobNumber);
+ var dataToWrite = new byte[length];
+ sourceStream.Position = 0;
+ sourceStream.Read(dataToWrite, 0, (int)length);
+
+ using var uploadStream = new MemoryStream(dataToWrite);
+
+ var request = new PutObjectRequest
+ {
+ NamespaceName = GetNamespace(),
+ BucketName = file.Container,
+ ObjectName = blobKey,
+ ContentLength = length,
+ PutObjectBody = uploadStream
+ };
+
+ _client.PutObject(request).Wait();
+
+ // Remove written data from source stream
+ var remaining = sourceStream.Length - length;
+ if (remaining > 0)
+ {
+ var remainingData = new byte[remaining];
+ sourceStream.Position = length;
+ sourceStream.Read(remainingData, 0, (int)remaining);
+ sourceStream.SetLength(0);
+ sourceStream.Write(remainingData, 0, (int)remaining);
+ }
+ else
+ {
+ sourceStream.SetLength(0);
+ }
+ sourceStream.Position = 0;
+
+ return length;
+ }
+
+ public override void MoveFile(BlobStorePath sourceFile, BlobStorePath targetFile)
+ {
+ EnsureNotDisposed();
+ sourceFile.Validate(_pathValidator);
+ targetFile.Validate(_pathValidator);
+
+ // Copy then delete
+ var fileSize = GetFileSize(sourceFile);
+ CopyFile(sourceFile, targetFile, 0, fileSize);
+ DeleteFile(sourceFile);
+ }
+
+ public override long CopyFile(BlobStorePath sourceFile, BlobStorePath targetFile, long offset, long length)
+ {
+ EnsureNotDisposed();
+ sourceFile.Validate(_pathValidator);
+ targetFile.Validate(_pathValidator);
+
+ if (offset < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(offset), "Offset cannot be negative");
+ }
+
+ if (length < 0)
+ {
+ length = GetFileSize(sourceFile) - offset;
+ }
+
+ if (length == 0)
+ {
+ return 0;
+ }
+
+ // Read data from source
+ var data = ReadData(sourceFile, offset, length);
+
+ // Write to target
+ return WriteData(targetFile, new[] { data });
+ }
+
+ public override void TruncateFile(BlobStorePath file, long newLength)
+ {
+ EnsureNotDisposed();
+ file.Validate(_pathValidator);
+
+ if (newLength < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(newLength), "New length cannot be negative");
+ }
+
+ var currentSize = GetFileSize(file);
+
+ if (newLength >= currentSize)
+ {
+ // No truncation needed
+ return;
+ }
+
+ if (newLength == 0)
+ {
+ // Delete all blobs
+ DeleteFile(file);
+ return;
+ }
+
+ // Read the data up to newLength and rewrite
+ var data = ReadData(file, 0, newLength);
+ WriteData(file, new[] { data });
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _client?.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+}
+
diff --git a/afs/oraclecloud/objectstorage/src/OracleCloudObjectStoragePathValidator.cs b/afs/oraclecloud/objectstorage/src/OracleCloudObjectStoragePathValidator.cs
new file mode 100644
index 0000000..8edfb9c
--- /dev/null
+++ b/afs/oraclecloud/objectstorage/src/OracleCloudObjectStoragePathValidator.cs
@@ -0,0 +1,158 @@
+using System.Text.RegularExpressions;
+using NebulaStore.Afs.Blobstore;
+using NebulaStore.Afs.Blobstore.Types;
+
+namespace NebulaStore.Afs.OracleCloud.ObjectStorage;
+
+///
+/// Path validator for Oracle Cloud Infrastructure Object Storage.
+/// Validates bucket names and object names according to OCI naming rules.
+///
+public interface IOracleCloudObjectStoragePathValidator : IAfsPathValidator
+{
+ ///
+ /// Creates a new Oracle Cloud Object Storage path validator.
+ ///
+ /// A new path validator instance
+ static IOracleCloudObjectStoragePathValidator New() => new OracleCloudObjectStoragePathValidator();
+
+ ///
+ /// Validates a bucket name according to OCI naming rules.
+ ///
+ /// The bucket name to validate
+ void ValidateBucketName(string bucketName);
+
+ ///
+ /// Validates an object name according to OCI naming rules.
+ ///
+ /// The object name to validate
+ void ValidateObjectName(string objectName);
+}
+
+///
+/// Default implementation of Oracle Cloud Object Storage path validator.
+///
+internal class OracleCloudObjectStoragePathValidator : IOracleCloudObjectStoragePathValidator
+{
+ // OCI bucket naming rules:
+ // - Must be between 1 and 256 characters
+ // - Can contain lowercase letters, numbers, hyphens, underscores, and periods
+ // - Must start with a letter or number
+ // - Cannot contain two consecutive periods
+ private static readonly Regex BucketNameRegex = new(
+ @"^[a-z0-9][a-z0-9\-_.]{0,254}[a-z0-9]$|^[a-z0-9]$",
+ RegexOptions.Compiled);
+
+ private static readonly Regex ConsecutivePeriodsRegex = new(
+ @"\.\.",
+ RegexOptions.Compiled);
+
+ // OCI object naming rules:
+ // - Can be up to 1024 characters
+ // - Can contain any UTF-8 characters except null
+ // - Forward slashes (/) are used as path separators
+ private const int MaxObjectNameLength = 1024;
+ private const int MaxBucketNameLength = 256;
+ private const int MinBucketNameLength = 1;
+
+ public void Validate(IAfsPath path)
+ {
+ if (path is BlobStorePath blobPath)
+ {
+ ValidateContainer(blobPath.Container);
+
+ // Validate the object path if it exists (skip container)
+ if (blobPath.PathElements.Length > 1)
+ {
+ var objectPath = string.Join(BlobStorePath.Separator, blobPath.PathElements.Skip(1));
+ if (!string.IsNullOrEmpty(objectPath))
+ {
+ ValidateBlob(objectPath);
+ }
+ }
+ }
+ else
+ {
+ throw new ArgumentException("Path must be a BlobStorePath for OCI validation", nameof(path));
+ }
+ }
+
+ public void ValidateBucketName(string bucketName)
+ {
+ ValidateContainer(bucketName);
+ }
+
+ public void ValidateObjectName(string objectName)
+ {
+ ValidateBlob(objectName);
+ }
+
+ private void ValidateContainer(string container)
+ {
+ if (string.IsNullOrEmpty(container))
+ {
+ throw new ArgumentException("Bucket name cannot be null or empty", nameof(container));
+ }
+
+ if (container.Length < MinBucketNameLength || container.Length > MaxBucketNameLength)
+ {
+ throw new ArgumentException(
+ $"Bucket name must be between {MinBucketNameLength} and {MaxBucketNameLength} characters",
+ nameof(container));
+ }
+
+ if (!BucketNameRegex.IsMatch(container))
+ {
+ throw new ArgumentException(
+ "Bucket name must start and end with a letter or number, and can only contain lowercase letters, numbers, hyphens, underscores, and periods",
+ nameof(container));
+ }
+
+ if (ConsecutivePeriodsRegex.IsMatch(container))
+ {
+ throw new ArgumentException(
+ "Bucket name cannot contain consecutive periods",
+ nameof(container));
+ }
+ }
+
+ private void ValidateBlob(string blob)
+ {
+ if (string.IsNullOrEmpty(blob))
+ {
+ throw new ArgumentException("Object name cannot be null or empty", nameof(blob));
+ }
+
+ if (blob.Length > MaxObjectNameLength)
+ {
+ throw new ArgumentException(
+ $"Object name cannot exceed {MaxObjectNameLength} characters",
+ nameof(blob));
+ }
+
+ // Check for null characters
+ if (blob.Contains('\0'))
+ {
+ throw new ArgumentException(
+ "Object name cannot contain null characters",
+ nameof(blob));
+ }
+
+ // OCI doesn't allow object names that are just "." or ".."
+ if (blob == "." || blob == "..")
+ {
+ throw new ArgumentException(
+ "Object name cannot be '.' or '..'",
+ nameof(blob));
+ }
+
+ // Check for leading/trailing whitespace (not recommended)
+ if (blob != blob.Trim())
+ {
+ throw new ArgumentException(
+ "Object name should not have leading or trailing whitespace",
+ nameof(blob));
+ }
+ }
+}
+
diff --git a/afs/oraclecloud/objectstorage/test/NebulaStore.Afs.OracleCloud.ObjectStorage.Tests.csproj b/afs/oraclecloud/objectstorage/test/NebulaStore.Afs.OracleCloud.ObjectStorage.Tests.csproj
new file mode 100644
index 0000000..f9f9ed9
--- /dev/null
+++ b/afs/oraclecloud/objectstorage/test/NebulaStore.Afs.OracleCloud.ObjectStorage.Tests.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net9.0
+ enable
+ enable
+ false
+ true
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
diff --git a/afs/oraclecloud/objectstorage/test/OracleCloudObjectStoragePathValidatorTests.cs b/afs/oraclecloud/objectstorage/test/OracleCloudObjectStoragePathValidatorTests.cs
new file mode 100644
index 0000000..2aee566
--- /dev/null
+++ b/afs/oraclecloud/objectstorage/test/OracleCloudObjectStoragePathValidatorTests.cs
@@ -0,0 +1,126 @@
+using Xunit;
+using NebulaStore.Afs.Blobstore;
+using NebulaStore.Afs.OracleCloud.ObjectStorage;
+
+namespace NebulaStore.Afs.OracleCloud.ObjectStorage.Tests;
+
+///
+/// Unit tests for OracleCloudObjectStoragePathValidator.
+///
+public class OracleCloudObjectStoragePathValidatorTests
+{
+ private readonly IOracleCloudObjectStoragePathValidator _validator;
+
+ public OracleCloudObjectStoragePathValidatorTests()
+ {
+ _validator = IOracleCloudObjectStoragePathValidator.New();
+ }
+
+ [Theory]
+ [InlineData("valid-bucket-name")]
+ [InlineData("bucket123")]
+ [InlineData("my-bucket-2024")]
+ [InlineData("bucket_with_underscores")]
+ [InlineData("bucket.with.periods")]
+ [InlineData("a")]
+ [InlineData("bucket-name-with-many-characters-but-still-valid-123")]
+ public void ValidateBucketName_ValidNames_ShouldNotThrow(string bucketName)
+ {
+ // Act & Assert
+ var exception = Record.Exception(() => _validator.ValidateBucketName(bucketName));
+ Assert.Null(exception);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData(null)]
+ [InlineData("Invalid-Bucket")] // Uppercase not allowed
+ [InlineData("bucket..name")] // Consecutive periods
+ [InlineData("-bucket")] // Cannot start with hyphen
+ [InlineData("bucket-")] // Cannot end with hyphen
+ [InlineData("UPPERCASE")] // All uppercase
+ public void ValidateBucketName_InvalidNames_ShouldThrow(string bucketName)
+ {
+ // Act & Assert
+ Assert.Throws(() => _validator.ValidateBucketName(bucketName));
+ }
+
+ [Theory]
+ [InlineData("valid/path/to/object.txt")]
+ [InlineData("simple-object")]
+ [InlineData("path/with/multiple/levels/file.dat")]
+ [InlineData("object_with_underscores.txt")]
+ [InlineData("object-with-hyphens.txt")]
+ [InlineData("object.with.periods.txt")]
+ public void ValidateObjectName_ValidNames_ShouldNotThrow(string objectName)
+ {
+ // Act & Assert
+ var exception = Record.Exception(() => _validator.ValidateObjectName(objectName));
+ Assert.Null(exception);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData(null)]
+ [InlineData(".")]
+ [InlineData("..")]
+ [InlineData(" leading-space")]
+ [InlineData("trailing-space ")]
+ public void ValidateObjectName_InvalidNames_ShouldThrow(string objectName)
+ {
+ // Act & Assert
+ Assert.Throws(() => _validator.ValidateObjectName(objectName));
+ }
+
+ [Fact]
+ public void ValidateObjectName_TooLong_ShouldThrow()
+ {
+ // Arrange
+ var longName = new string('a', 1025); // Max is 1024
+
+ // Act & Assert
+ Assert.Throws(() => _validator.ValidateObjectName(longName));
+ }
+
+ [Fact]
+ public void ValidateBucketName_TooLong_ShouldThrow()
+ {
+ // Arrange
+ var longName = new string('a', 257); // Max is 256
+
+ // Act & Assert
+ Assert.Throws(() => _validator.ValidateBucketName(longName));
+ }
+
+ [Fact]
+ public void Validate_ValidPath_ShouldNotThrow()
+ {
+ // Arrange
+ var path = BlobStorePath.New("valid-bucket", "path", "to", "object.txt");
+
+ // Act & Assert
+ var exception = Record.Exception(() => _validator.Validate(path));
+ Assert.Null(exception);
+ }
+
+ [Fact]
+ public void Validate_InvalidBucketName_ShouldThrow()
+ {
+ // Arrange
+ var path = BlobStorePath.New("Invalid-Bucket", "object.txt");
+
+ // Act & Assert
+ Assert.Throws(() => _validator.Validate(path));
+ }
+
+ [Fact]
+ public void Validate_InvalidObjectName_ShouldThrow()
+ {
+ // Arrange
+ var path = BlobStorePath.New("valid-bucket", ".");
+
+ // Act & Assert
+ Assert.Throws(() => _validator.Validate(path));
+ }
+}
+