.NET 8.x client implementation for the Bunny.net APIs. Full disclosure, I wanted to try out github co-pilot, and this seemed like a useful project. I am reviewing what co-pilot writes and updating anything I find broken or not correct.
SharpBunny is a comprehensive .NET client library for interacting with Bunny.net APIs including Stream API, Edge Storage API, and General API. It provides a type-safe, async-first approach to managing video collections, videos, file storage, and CDN resources in your Bunny.net services.
- ✅ Collection Management: Create, read, update, and delete video collections
- ✅ Video Management: Manage videos including upload, metadata updates, and deletion
- ✅ Type-Safe Models: Strongly-typed models for all API responses
- ✅ Async/Await Support: Full async support for all operations
- ✅ Error Handling: Comprehensive error handling with custom exceptions
- ✅ HTTP Client Integration: Built on HttpClient with support for dependency injection
- ✅ .NET 8 Compatible: Built for .NET 8.0 with modern C# features
- ✅ File Management: Upload, download, delete files and browse directories
- ✅ Path Support: Full support for nested directory structures
- ✅ Checksum Validation: SHA256 checksum support for upload integrity
- ✅ Multiple Storage Regions: Support for different storage zone endpoints
- ✅ Type-Safe File Metadata: Strongly-typed models for file information
- ✅ Countries: Get country information including tax rates and EU status
- ✅ Regions: Retrieve CDN region data with pricing tiers
- ✅ Pull Zones: Full CDN management including creation, configuration, and deletion
- ✅ DNS Zones: DNS zone management with record operations
- ✅ Purge: Cache purging for URLs and entire pull zones
- ✅ Statistics: Comprehensive analytics for pull zones and billing data
dotnet add package SharpBunnyusing SharpBunny;
// Initialize the General API client
var generalApi = new BunnyGeneralApi("your-general-api-key");
// Get all countries
var countries = await generalApi.Countries.GetCountriesAsync();
foreach (var country in countries)
{
Console.WriteLine($"Country: {country.Name} ({country.Code}) - EU: {country.IsEU}");
}
// Get all pull zones
var pullZones = await generalApi.PullZones.GetPullZonesAsync();
foreach (var pullZone in pullZones.Items)
{
Console.WriteLine($"Pull Zone: {pullZone.Name} - Origin: {pullZone.OriginUrl}");
Console.WriteLine($"Status: {(pullZone.Enabled ? "Enabled" : "Disabled")}");
}
// Create a new pull zone
var createRequest = new CreatePullZoneRequest
{
Name = "my-new-pull-zone",
OriginUrl = "https://myorigin.example.com",
Type = 0 // Standard pull zone
};
var newPullZone = await generalApi.PullZones.CreatePullZoneAsync(createRequest);
// Get pull zone statistics
var stats = await generalApi.Statistics.GetPullZoneStatisticsAsync(
newPullZone.Id,
dateFrom: DateTime.UtcNow.AddDays(-30),
dateTo: DateTime.UtcNow);
Console.WriteLine($"Bandwidth used: {stats.TotalBandwidthUsed:N0} bytes");
Console.WriteLine($"Requests served: {stats.TotalRequestsServed:N0}");
Console.WriteLine($"Cache hit rate: {stats.CacheHitRate:P2}");using SharpBunny;
// Initialize the Stream API client
var streamApi = new BunnyStreamApi("your-stream-api-key");
// Get all collections for a library
var libraryId = 12345;
var collections = await streamApi.Collections.GetCollectionsAsync(libraryId);
foreach (var collection in collections.Items)
{
Console.WriteLine($"Collection: {collection.Name} ({collection.VideoCount} videos)");
}
// Create a new collection
var newCollection = await streamApi.Collections.CreateCollectionAsync(libraryId, "My New Collection");
Console.WriteLine($"Created collection: {newCollection.Guid}");
// Get all videos in the library
var videos = await streamApi.Videos.GetVideosAsync(libraryId);
foreach (var video in videos.Items)
{
Console.WriteLine($"Video: {video.Title} - Views: {video.Views}");
}using SharpBunny;
using System.Text;
// Initialize the Edge Storage API client
var edgeStorageApi = new BunnyEdgeStorageApi("your-storage-zone-password");
// List files in the root directory
var files = await edgeStorageApi.EdgeStorage.ListFilesAsync(
"your-storage-zone",
storageZonePassword: "your-storage-zone-password");
foreach (var file in files)
{
var type = file.IsDirectory ? "DIR" : "FILE";
Console.WriteLine($"{type}: {file.ObjectName} ({file.Length} bytes)");
}
// Upload a file
var content = Encoding.UTF8.GetBytes("Hello, World!");
await edgeStorageApi.EdgeStorage.UploadFileAsync(
"your-storage-zone",
"hello.txt",
content,
path: "my-folder",
storageZonePassword: "your-storage-zone-password");
// Download a file
var downloadedContent = await edgeStorageApi.EdgeStorage.DownloadFileAsync(
"your-storage-zone",
"hello.txt",
path: "my-folder",
storageZonePassword: "your-storage-zone-password");
var text = Encoding.UTF8.GetString(downloadedContent);
Console.WriteLine($"Downloaded: {text}");
// Delete a file
await edgeStorageApi.EdgeStorage.DeleteFileAsync(
"your-storage-zone",
"hello.txt",
path: "my-folder",
storageZonePassword: "your-storage-zone-password");The main entry point for the General API client.
// With automatic HttpClient management
var api = new BunnyGeneralApi("your-general-api-key");
// With custom HttpClient (for DI scenarios)
var api = new BunnyGeneralApi(httpClient, "your-general-api-key");The main entry point for the Stream API client.
// With automatic HttpClient management
var api = new BunnyStreamApi("your-stream-api-key");
// With custom HttpClient (for DI scenarios)
var api = new BunnyStreamApi(httpClient, "your-stream-api-key");The main entry point for the Edge Storage API client.
// With automatic HttpClient management
var api = new BunnyEdgeStorageApi("your-storage-zone-password");
// With custom HttpClient (for DI scenarios)
var api = new BunnyEdgeStorageApi(httpClient, "your-storage-zone-password");var collections = await api.Collections.GetCollectionsAsync(
libraryId: 12345,
page: 1,
itemsPerPage: 100,
search: "search term", // optional
orderBy: "date" // optional
);var collection = await api.Collections.GetCollectionAsync(libraryId, collectionId);var newCollection = await api.Collections.CreateCollectionAsync(libraryId, "Collection Name");var updatedCollection = await api.Collections.UpdateCollectionAsync(
libraryId,
collectionId,
"New Collection Name"
);await api.Collections.DeleteCollectionAsync(libraryId, collectionId);var videos = await api.Videos.GetVideosAsync(
libraryId: 12345,
page: 1,
itemsPerPage: 100,
search: "search term", // optional
collection: "collection-id", // optional
orderBy: "date" // optional
);var video = await api.Videos.GetVideoAsync(libraryId, videoId);var newVideo = await api.Videos.CreateVideoAsync(
libraryId,
"Video Title",
collectionId // optional
);var updatedVideo = await api.Videos.UpdateVideoAsync(
libraryId,
videoId,
title: "New Title", // optional
collectionId: "new-collection-id" // optional
);await api.Videos.DeleteVideoAsync(libraryId, videoId);using var fileStream = File.OpenRead("video.mp4");
var success = await api.Videos.UploadVideoAsync(
libraryId,
videoId,
fileStream,
"video.mp4"
);// Get all countries
var countries = await api.Countries.GetCountriesAsync();// Get all regions
var regions = await api.Regions.GetRegionsAsync();// Get all pull zones
var pullZones = await api.PullZones.GetPullZonesAsync(
page: 1,
perPage: 100,
search: "example" // optional
);
// Get specific pull zone
var pullZone = await api.PullZones.GetPullZoneAsync(pullZoneId);
// Create pull zone
var createRequest = new CreatePullZoneRequest
{
Name = "my-pull-zone",
OriginUrl = "https://origin.example.com",
Type = 0 // 0 = Standard, 1 = Volume
};
var newPullZone = await api.PullZones.CreatePullZoneAsync(createRequest);
// Update pull zone
var updateRequest = new UpdatePullZoneRequest
{
OriginUrl = "https://neworigin.example.com",
EnableLogging = true,
CacheControlMaxAgeOverride = 3600
};
var updatedPullZone = await api.PullZones.UpdatePullZoneAsync(pullZoneId, updateRequest);
// Delete pull zone
await api.PullZones.DeletePullZoneAsync(pullZoneId);// Get all DNS zones
var dnsZones = await api.DnsZones.GetDnsZonesAsync(
page: 1,
perPage: 100,
search: "example.com" // optional
);
// Get specific DNS zone
var dnsZone = await api.DnsZones.GetDnsZoneAsync(zoneId);
// Create DNS zone
var newDnsZone = await api.DnsZones.CreateDnsZoneAsync("example.com");
// Delete DNS zone
await api.DnsZones.DeleteDnsZoneAsync(zoneId);// Purge entire pull zone cache
await api.Purge.PurgePullZoneCacheAsync(pullZoneId);
// Purge specific URLs
var purgeRequest = new PurgeUrlsRequest
{
Urls = new List<string>
{
"https://example.com/image.jpg",
"https://example.com/style.css"
},
Async = false
};
await api.Purge.PurgeUrlsAsync(purgeRequest);
// Get purge history
var purgeHistory = await api.Purge.GetPurgeHistoryAsync(page: 1, perPage: 100);// Get pull zone statistics
var stats = await api.Statistics.GetPullZoneStatisticsAsync(
pullZoneId,
dateFrom: DateTime.UtcNow.AddDays(-30),
dateTo: DateTime.UtcNow,
hourly: false,
loadErrors: false
);
// Get billing statistics
var billingStats = await api.Statistics.GetBillingStatisticsAsync(
dateFrom: DateTime.UtcNow.AddMonths(-1),
dateTo: DateTime.UtcNow
);
// Get country statistics for a pull zone
var countryStats = await api.Statistics.GetPullZoneCountryStatisticsAsync(
pullZoneId,
dateFrom: DateTime.UtcNow.AddDays(-7),
dateTo: DateTime.UtcNow
);var files = await api.EdgeStorage.ListFilesAsync(
storageZoneName: "your-storage-zone",
path: "subfolder", // optional, defaults to root
storageZonePassword: "your-password", // optional if set in constructor
storageZoneEndpoint: "ny.storage.bunnycdn.com", // optional, defaults to storage.bunnycdn.com
cancellationToken: cancellationToken // optional
);var fileContent = File.ReadAllBytes("local-file.jpg");
await api.EdgeStorage.UploadFileAsync(
storageZoneName: "your-storage-zone",
fileName: "uploaded-file.jpg",
fileContent: fileContent,
path: "uploads", // optional
storageZonePassword: "your-password", // optional if set in constructor
storageZoneEndpoint: "ny.storage.bunnycdn.com", // optional
checksum: "sha256-checksum", // optional for integrity validation
cancellationToken: cancellationToken // optional
);var fileContent = await api.EdgeStorage.DownloadFileAsync(
storageZoneName: "your-storage-zone",
fileName: "file-to-download.jpg",
path: "uploads", // optional
storageZonePassword: "your-password", // optional if set in constructor
storageZoneEndpoint: "ny.storage.bunnycdn.com", // optional
cancellationToken: cancellationToken // optional
);
// Save to local file
await File.WriteAllBytesAsync("downloaded-file.jpg", fileContent);await api.EdgeStorage.DeleteFileAsync(
storageZoneName: "your-storage-zone",
fileName: "file-to-delete.jpg",
path: "uploads", // optional
storageZonePassword: "your-password", // optional if set in constructor
storageZoneEndpoint: "ny.storage.bunnycdn.com", // optional
cancellationToken: cancellationToken // optional
);The library throws BunnyApiException for API errors:
try
{
var collection = await api.Collections.GetCollectionAsync(libraryId, "invalid-id");
}
catch (BunnyApiException ex)
{
Console.WriteLine($"API Error: {ex.Message}");
Console.WriteLine($"Status Code: {ex.StatusCode}");
if (ex.ApiError != null)
{
Console.WriteLine($"Error Type: {ex.ApiError.Type}");
Console.WriteLine($"Error Detail: {ex.ApiError.Detail}");
}
}You can register both API clients with the .NET dependency injection container:
// In Program.cs or Startup.cs
services.AddHttpClient();
// Stream API
services.AddSingleton(provider =>
{
var httpClient = provider.GetRequiredService<HttpClient>();
return new BunnyStreamApi(httpClient, "your-stream-api-key");
});
// General API
services.AddSingleton(provider =>
{
var httpClient = provider.GetRequiredService<HttpClient>();
return new BunnyGeneralApi(httpClient, "your-general-api-key");
});
// Edge Storage API
services.AddSingleton(provider =>
{
var httpClient = provider.GetRequiredService<HttpClient>();
return new BunnyEdgeStorageApi(httpClient, "your-storage-zone-password");
});Id: Unique country identifierName: Country nameCode: ISO country code (e.g., "US", "DE")ContinentCode: Continent code (e.g., "NA", "EU")IsEU: Whether the country is in the European UnionTaxRate: Tax rate as decimal (e.g., 19 for 19%)
Id: Unique region identifierName: Region nameRegionCode: Region codeContinentCode: Continent codeCountryCode: Country codeLatitude/Longitude: Geographic coordinatesPriceTier: Pricing tier levelRegionPrice: Price per unit in this region
Id: Unique pull zone identifierName: Pull zone nameOriginUrl: Origin server URLEnabled: Whether the pull zone is activeHostnames: List of custom hostnamesStorageZoneId: Associated storage zone ID (if any)ZonePricingTier: Pricing tier (0 = Standard, 1 = High Volume)MonthlyBandwidthLimit/MonthlyBandwidthUsed: Bandwidth limits and usageMonthlyCharges: Current month chargesType: Pull zone type (0 = Standard, 1 = Volume)- Various caching, security, and logging configuration options
Id: Unique DNS zone identifierDomain: Domain nameRecordsCount: Number of DNS recordsDateCreated/DateModified: TimestampsNameservers: List of nameserversCustomNameservers: Custom nameserver configurationSoaEmail: SOA record email- Various logging and forwarding configuration options
VideoLibraryId: The video library IDGuid: Unique collection identifierName: Collection nameVideoCount: Number of videos in the collectionTotalSize: Total size of all videos in bytesPreviewVideoIds: Comma-separated list of preview video IDs
VideoLibraryId: The video library IDGuid: Unique video identifierTitle: Video titleDateUploaded: Upload timestampViews: View countIsPublic: Public visibility flagLength: Video duration in secondsStatus: Processing statusWidth/Height: Video dimensionsStorageSize: File size in bytesCollectionId: Associated collection IDCaptions: List of caption tracksChapters: List of video chaptersMetaTags: List of metadata tags
ArrayNumber: Array position numberChecksum: SHA256 checksum (null for directories)ContentType: MIME type of the fileDateCreated: ISO 8601 creation timestampGuid: Unique file identifierIsDirectory: Boolean indicating if item is a directoryLastChanged: ISO 8601 last modified timestampLength: File size in bytes (0 for directories)ObjectName: File or directory namePath: Full path to the fileReplicatedZones: Storage zone replication regionsServerId: Server ID where file is storedStorageZoneId: Storage zone identifierStorageZoneName: Storage zone nameUserId: User ID associated with the file
This library implements the core Bunny.net Stream API, Edge Storage API, and General API endpoints. If you need additional functionality or find bugs, please open an issue or submit a pull request.
This project is licensed under the MIT License.