diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..a8c201a8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,114 @@ +# Contributor guide for coding agents + +This file is for agents contributing to **this repository**. If you are *using* the +installed `CloudinaryDotNet` NuGet package in another project, read the bundled docs +instead — they ship inside the package and are version-matched to what you have installed: + +```bash +ROOT=$(dotnet nuget locals global-packages --list | awk '{print $2}') +ls "$ROOT"/cloudinarydotnet/*/docs +``` + +The same pages are in [`docs/`](docs/README.md) in this repository. + +## Commands + +```bash +dotnet restore CloudinaryDotnet.sln +dotnet build CloudinaryDotnet.sln -c Release +dotnet test CloudinaryDotNet.Tests/CloudinaryDotNet.Tests.csproj -c Release -f net8.0 +dotnet pack CloudinaryDotNet/CloudinaryDotNet.csproj -c Release -o ./artifacts +``` + +The main project multi-targets `netstandard1.3;netstandard2.0;net452`; the test projects +target `net452;net8.0`. On macOS and Linux only the `net8.0` test target runs — pass +`-f net8.0` explicitly, or the run fails looking for the .NET Framework host. + +## Testing + +- `CloudinaryDotNet.Tests/` — unit tests, mocked, no network. These must stay offline. +- `CloudinaryDotNet.IntegrationTests/` — requires a real product environment via + `CLOUDINARY_URL`. Do not run by default; do not add tests here that consume paid add-ons + without a skip guard. +- `examples/` — runnable docs examples. Not part of the solution and not covered by the test + suite; run them by hand against a throwaway cloud (`npx @cloudinary/cloud`). +- Nondeterministic AI output (captions, tags, moderation verdicts) must be asserted by + request shape, state transition, and response schema — never by exact output values. + +## Project structure + +- `CloudinaryDotNet/` — the library. `Cloudinary.cs` plus the `Cloudinary.*.cs` partials + split the API surface (`UploadApi`, `AdminApi`, `AdminApi.MetadataFields`, …). +- `CloudinaryDotNet/Actions/` — every parameter and result type. Public API consumers need + `using CloudinaryDotNet.Actions;` for these. +- `CloudinaryDotNet/Transforms/`, `Url.cs`, `UrlBuilder.cs` — URL and transformation + building; entirely local, no network. +- `CloudinaryDotNet/Search/` — the fluent Search API. +- `CloudinaryDotNet/Provisioning/` — account provisioning, a separate client. +- `docs/` — agent-facing task documentation, **shipped in the NuGet package**. +- `examples/` — runnable counterparts to the doc pages, deliberately *not* packaged. +- `samples/` — legacy sample applications (PhotoAlbum, LargeVideoUpload). Treat as legacy: + do not modernize them as part of unrelated work. +- `Cloudinary/`, `Core/`, `Shared/`, `Shared.Tests/`, `Cloudinary.Test*/` — legacy + scaffolding retained for the old build layout. Prefer the top-level projects. + +## Code style + +- StyleCop and FxCop analyzers run with `TreatWarningsAsErrors`. A style violation fails + the build; fix it rather than suppressing it. +- XML doc comments are required on public members (`GenerateDocumentationFile` is on). +- `LangVersion` is 9.0 for the library, and it targets `netstandard1.3`, so newer BCL APIs + and language features are unavailable there. Guard target-specific code with + `#if NETSTANDARD2_0` as the existing code does (see `ApiShared.Proxy.cs`). +- Public API additions need both sync and `…Async` overloads, following the existing + pattern. + +## Error-handling contract + +This SDK **returns** Cloudinary API errors rather than throwing: results derive from +`BaseResult`, which carries `StatusCode` and `Error`. Preserve this. Do not add throwing +behaviour to an existing API path, and do not introduce a custom exception type without +discussion — there are currently none, and callers rely on that. + +## Versioning + +The version lives in **two** places and both must match: + +- `CloudinaryDotNet/CloudinaryDotNet.csproj` — `` +- `CloudinaryDotNet/CloudinaryVersion.cs` — `Full` + +`set_version.ps1` updates both. It replaces the **first** `` element in the +csproj by regex, so never add another `` above it. + +The bundled `docs/` carry **no version number** — the version-matched guarantee comes from +shipping inside the package. Do not add a version stamp to the docs. + +## Git workflow + +- Branch from `master`; keep changes focused; one topic per pull request. +- Build and run the unit tests before opening a PR. +- Do not rewrite published `CHANGELOG.md` entries; add new entries at the top. Docs-only + changes get no changelog entry. +- Never commit credentials, `.env` files, or `appsettings.json` with real values. + +## Boundaries + +**Always** +- Keep `docs/` and `examples/` consistent with the code they document. +- Verify a documented behaviour by running it against a real cloud before writing it down. + Reading the source and inferring behaviour has produced wrong documentation repeatedly. +- Keep API secrets out of examples, docs, tests, and fixtures. + +**Ask first** +- Changing the packaged file list in `CloudinaryDotNet.csproj` (the `` + items), or anything else about packaging. +- Changing target frameworks, dependencies, or the analyzer/ruleset configuration. +- Renaming or removing any public type or member — this library is widely deployed. +- Changing release, CI (`appveyor.yml`), or signing configuration. + +**Never** +- Commit credentials or real account identifiers. +- Perform live network calls from unit tests. +- Document a Cloudinary platform capability as an SDK method unless this package implements + it (see [docs/platform-capabilities.md](docs/platform-capabilities.md)). +- Add a linter, formatter, or reformat unrelated files. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/CloudinaryDotNet/CloudinaryDotNet.csproj b/CloudinaryDotNet/CloudinaryDotNet.csproj index 9f74ed69..80646194 100644 --- a/CloudinaryDotNet/CloudinaryDotNet.csproj +++ b/CloudinaryDotNet/CloudinaryDotNet.csproj @@ -58,5 +58,6 @@ + diff --git a/README.md b/README.md index 19bdee60..797a9d16 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ [![Build status](https://ci.appveyor.com/api/projects/status/vdx8o03ethg5opt4?svg=true)](https://ci.appveyor.com/project/Cloudinary/cloudinarydotnet) [![NuGet Badge](https://img.shields.io/nuget/v/CloudinaryDotNet)](https://www.nuget.org/packages/CloudinaryDotNet/) ![NuGet Downloads](https://img.shields.io/nuget/dt/CloudinaryDotNet) +[![License](https://img.shields.io/github/license/cloudinary/CloudinaryDotNet)](LICENSE) Cloudinary .NET SDK ================== @@ -20,6 +21,15 @@ For the complete documentation, see the [.NET SDK Guide](https://cloudinary.com/ - [Usage](#usage) - [Setup](#Setup) - [Transform and Optimize Assets](#Transform-and-Optimize-Assets) + - [Upload](#Upload) + - [Error handling](#Error-handling) + - [Code Samples](#Code-Samples) +- [Documentation for AI coding agents](#documentation-for-ai-coding-agents) +- [Contributions](#contributions) +- [Get Help](#get-help) +- [About Cloudinary](#about-cloudinary) +- [Additional Resources](#additional-resources) +- [Licence](#licence) ## Key Features @@ -46,6 +56,11 @@ Install using Package Manager: PM> Install-Package CloudinaryDotNet ``` +Or using the .NET CLI: +```bash +dotnet add package CloudinaryDotNet +``` + # Usage ### Setup @@ -53,9 +68,12 @@ PM> Install-Package CloudinaryDotNet using CloudinaryDotNet; using CloudinaryDotNet.Actions; -var cloudinary = new Cloudinary(); +var cloudinary = new Cloudinary(); // reads the CLOUDINARY_URL environment variable +cloudinary.Api.Secure = true; // generate https:// URLs ``` +Note that `Api.Secure` defaults to `false`, so set it unless you specifically want `http://` URLs. + ### Transform and Optimize Assets - [See full documentation](https://cloudinary.com/documentation/dotnet_image_manipulation). @@ -75,19 +93,62 @@ var uploadParams = new ImageUploadParams() var uploadResult = cloudinary.Upload(uploadParams); ``` +### Error handling + +Cloudinary API errors are **returned, not thrown**. Check `Error` on every result: + +```csharp +var uploadResult = await cloudinary.UploadAsync(uploadParams); + +if (uploadResult.Error != null) +{ + Console.Error.WriteLine($"Upload failed ({(int)uploadResult.StatusCode}): {uploadResult.Error.Message}"); + return; +} + +Console.WriteLine(uploadResult.SecureUrl); +``` + +A `try`/`catch` around an API call will not catch a Cloudinary error — only configuration +and argument problems throw. See [docs/troubleshoot-errors.md](docs/troubleshoot-errors.md). + ### Code Samples -You can find our simple and ready-to-use samples projects, along with documentations in the [samples folder](https://github.com/cloudinary/CloudinaryDotNet/tree/master/samples). +You can find our simple and ready-to-use samples projects, along with documentations in the [samples folder](https://github.com/cloudinary/CloudinaryDotNet/tree/master/samples). Please consult with the [README file](https://github.com/cloudinary/CloudinaryDotNet/blob/master/samples/README.md), for usage and explanations. +Task-focused runnable examples live in [`examples/`](examples/README.md). ### Security options - [See full documentation](https://cloudinary.com/documentation/solution_overview#security). +- To report a vulnerability, see [SECURITY.md](SECURITY.md). + +## Documentation for AI coding agents + +This package ships task documentation **inside the NuGet package**, so it always matches the +version you have installed. If you are an AI coding agent — or you use one — point it there +rather than at training data, which is frequently out of date for this SDK. + +Locate the installed copy: + +```bash +ROOT=$(dotnet nuget locals global-packages --list | awk '{print $2}') +ls "$ROOT"/cloudinarydotnet/*/docs +``` + +The same pages are browsable here: [`docs/`](docs/README.md) — covering configuration, +upload, chunked video upload, signed browser uploads, image and video delivery, search and +asset management, moderation, structured metadata, ASP.NET Core integration, and +troubleshooting. + +Agents contributing to this repository should read [AGENTS.md](AGENTS.md) instead. ## Contributions -- Ensure tests run locally -- Open a PR and ensure Travis tests pass +- Ensure tests run locally (`dotnet test CloudinaryDotNet.Tests/CloudinaryDotNet.Tests.csproj -c Release -f net8.0`) +- Open a PR and ensure the CI build passes + +See [CONTRIBUTING.md](CONTRIBUTING.md) for details. ## Get Help diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..6f08e2d3 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,39 @@ +# Security Policy + +## Supported versions + +| Version | Supported | +|---------|-----------| +| 1.x | Yes | + +## Reporting a vulnerability + +Report vulnerabilities privately through [GitHub private vulnerability reporting](https://github.com/cloudinary/CloudinaryDotNet/security/advisories/new) for this repository. + +If you cannot use GitHub reporting, contact Cloudinary support at [support.cloudinary.com](https://support.cloudinary.com/hc/en-us/requests/new) and mark the ticket as a security issue. + +Use these private channels for anything security-sensitive; public GitHub issues are for regular bugs and feature requests. + +## What to include in a report + +- The affected package version and target framework (.NET Framework, .NET Standard, or .NET version). +- A minimal reproduction or proof of concept. +- The impact you believe the issue has (for example: credential exposure, signature bypass, request forgery). +- Any suggested remediation, if you have one. + +## Response and disclosure process + +- We acknowledge reports and keep you informed while the issue is investigated. +- Fixes are released as patched package versions; the changelog notes security-relevant changes without disclosing exploit details before users can upgrade. +- Please give us reasonable time to release a fix before public disclosure. + +## Security guidance for SDK users + +- Your API secret is a server-side credential. Keep it on your server; browsers, mobile binaries, and repositories should only ever hold delivery URLs or short-lived signatures. +- **Never reference this package from a client-side project** — Blazor WebAssembly, MAUI, Unity, or a desktop app. Anything the user can download, they can read. Call your own backend instead. +- Provide credentials through the `CLOUDINARY_URL` environment variable, or a secret store, rather than hardcoding them or committing them in `appsettings.json`. +- For uploads initiated from a browser or mobile app, generate the signature on your server. See [docs/sign-browser-upload.md](docs/sign-browser-upload.md). +- For unsigned uploads, use a deliberately restricted [unsigned upload preset](https://cloudinary.com/documentation/upload_presets) ([md](https://cloudinary.com/documentation/upload_presets.md)). +- Do not log a full Admin or Upload API request object; it can contain your credentials. Log `result.Error.Message` instead. +- Verify webhook callbacks with `Api.VerifyNotificationSignature` before acting on them. +- Cloudinary platform security documentation: https://cloudinary.com/documentation/solution_overview#security diff --git a/context7.json b/context7.json new file mode 100644 index 00000000..4f4275c4 --- /dev/null +++ b/context7.json @@ -0,0 +1,4 @@ +{ + "url": "https://context7.com/cloudinary/CloudinaryDotNet", + "public_key": "pk_dAgXWo5YsHXdnbg3TCE9R" +} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..a07e3437 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,90 @@ + + +# CloudinaryDotNet — bundled documentation + +> **Version-matched:** these docs ship inside the NuGet package and always describe the +> version you have installed. Prefer them over anything remembered from training data +> or found for another version. + +Task documentation for the `CloudinaryDotNet` server-side SDK. Each page is +self-contained: `using` directives, configuration, a complete runnable flow, expected +results, and common failures. + +Runnable versions of most tasks are in the [`examples/`](https://github.com/cloudinary/CloudinaryDotNet/tree/master/examples) +directory of the repository. They are **not** shipped in the NuGet package — see +[Why examples are not in the package](#why-examples-are-not-in-the-package). + +## Start here + +- [What this SDK does and does not do](platform-capabilities.md) — the agent tooling to + set up first (Skills, MCP servers, CLI, documentation indexes), what this package + covers, and what lives elsewhere on the platform. +- [Get Cloudinary credentials](get-credentials.md) — no account needed: provision a cloud + and start building. +- [Install and call the SDK](install-and-call.md) — the `using` directives, the + `Cloudinary` entry point, and the sync/async pattern. + +## Tasks + +- [Configure Cloudinary](configure-cloudinary.md) +- [Upload an image](upload-image.md) +- [Upload a large video](upload-large-video.md) +- [Sign a browser upload](sign-browser-upload.md) +- [Transform and deliver an image](transform-and-deliver-image.md) +- [Transform and deliver a video](transform-and-deliver-video.md) +- [Search and manage assets](search-and-manage-assets.md) +- [Moderate an upload](moderate-upload.md) +- [Use structured metadata](use-structured-metadata.md) +- [Use with ASP.NET Core](use-with-aspnet-core.md) +- [Troubleshoot errors](troubleshoot-errors.md) + +## The one thing to know before you write any code + +**This SDK does not throw on Cloudinary API errors.** Every Upload and Admin API method +returns a result object carrying `StatusCode` and `Error`. A wrong `api_secret`, a +missing asset, or a rejected parameter all return *normally* — a `try`/`catch` around +the call catches nothing and your code proceeds with an empty result. + +```csharp +var result = await cloudinary.UploadAsync(uploadParams); +if (result.Error != null) +{ + Console.Error.WriteLine($"Upload failed ({(int)result.StatusCode}): {result.Error.Message}"); + return; +} +``` + +Check `result.Error` after every call. See [Troubleshoot errors](troubleshoot-errors.md) +for the full error model, including the few cases that *do* throw. + +## Security boundary + +This is a **server-side** SDK. It holds your `ApiSecret`, which belongs on your server +only. Never ship it in a desktop, mobile, Blazor WebAssembly, or any other client-side +build — anything the user can download, they can read. Client code should receive +delivery URLs or short-lived signatures generated by your server +([how](sign-browser-upload.md)). + +## Why examples are not in the package + +The NuGet package ships `docs/` only. The `examples/` directory contains files with +top-level statements and a `Main` entry point; shipping loose `.cs` files inside a +package risks them being picked up by a consumer's compile glob and colliding with their +own entry point. Every page below therefore carries its complete runnable flow inline, so +you never need the examples directory to complete a task. + +## Canonical docs + +- [.NET SDK guide](https://cloudinary.com/documentation/dotnet_integration.md) +- [Full platform reference](https://cloudinary.com/documentation/cloudinary_references.md) + +**Link convention:** documentation links in these docs end in `.md` and return raw +Markdown — the preferred format for agents and for anything that parses text. Remove the +`.md` suffix for the same page as browsable HTML. The repository README links the HTML +form first, since it is read by people. diff --git a/docs/configure-cloudinary.md b/docs/configure-cloudinary.md new file mode 100644 index 00000000..e0eb40d6 --- /dev/null +++ b/docs/configure-cloudinary.md @@ -0,0 +1,149 @@ +# Configure Cloudinary + +## When to use + +Once, when you create the `Cloudinary` instance — before any upload, admin, or +URL-generation call. + +**Prerequisite:** a cloud name, API key, and API secret. If you do not have them, see +[Get Cloudinary credentials](get-credentials.md) — `npx @cloudinary/cloud` provisions a +working cloud with no signup. + +## Recommended: environment variable + +Set `CLOUDINARY_URL` (from Console > Settings > API Keys, or written into `.env` for you +by `npx @cloudinary/cloud`): + +```bash +export CLOUDINARY_URL=cloudinary://:@ +``` + +```csharp +using CloudinaryDotNet; + +var cloudinary = new Cloudinary(); // reads CLOUDINARY_URL +cloudinary.Api.Secure = true; // see "HTTPS is not the default" below +``` + +This keeps the secret out of source control and matches how the other Cloudinary SDKs +behave. + +## Alternative: explicit `Account` + +Use this when the credentials come from a configuration system rather than the +environment — for example ASP.NET Core's `IConfiguration` +(see [Use with ASP.NET Core](use-with-aspnet-core.md)): + +```csharp +using CloudinaryDotNet; + +var account = new Account( + cloudName, // e.g. configuration["Cloudinary:CloudName"] + apiKey, + apiSecret); + +var cloudinary = new Cloudinary(account) { Api = { Secure = true } }; +``` + +## Alternative: connection-string style + +```csharp +var cloudinary = new Cloudinary("cloudinary://:@"); +``` + +## Delivery only, no credentials + +URL building is local and needs only the cloud name — no key, no secret. Use this in a +process that must never hold the secret: + +```csharp +var delivery = new Cloudinary(new Account("")); +delivery.Api.Secure = true; +var url = delivery.Api.UrlImgUp.BuildUrl("sample.jpg"); +``` + +URL generation works. Upload and Admin calls on such an instance fail in +`result.Error.Message` — `Missing required parameter - api_key` for uploads, +`Invalid credentials` for Admin calls — rather than throwing. + +## HTTPS is not the default + +`Api.Secure` is **`false`** out of the box, so generated URLs start with `http://`: + +```csharp +var cloudinary = new Cloudinary(url); +cloudinary.Api.UrlImgUp.BuildUrl("sample.jpg"); +// http://res.cloudinary.com//image/upload/sample.jpg + +cloudinary.Api.Secure = true; +cloudinary.Api.UrlImgUp.BuildUrl("sample.jpg"); +// https://res.cloudinary.com//image/upload/sample.jpg +``` + +**Set `Api.Secure = true` immediately after constructing the client.** Browsers block +mixed content, so an `http://` image URL on an HTTPS page will not load. This differs from +the Node and Python SDKs, where HTTPS is the default — do not assume parity. + +Per-URL alternative: `cloudinary.Api.UrlImgUp.Secure(true).BuildUrl(...)`. + +## Behaviour you should know + +- **Configuration is per instance, not process-global.** Two `Cloudinary` objects can + point at different clouds, and setting `Api.Secure` on one does not affect the other. +- **`CloudinaryConfiguration`'s static fields are read by `new Account()`, not by + `new Cloudinary()`.** Setting `CloudinaryConfiguration.CloudName` and then calling the + parameterless `new Cloudinary()` throws, because that constructor only ever reads + `CLOUDINARY_URL`. To use the statics, pass `new Account()` explicitly: + + ```csharp + CloudinaryConfiguration.CloudName = "my-cloud"; + CloudinaryConfiguration.ApiKey = "..."; + CloudinaryConfiguration.ApiSecret = "..."; + var cloudinary = new Cloudinary(new Account()); // note the Account() + ``` + +- Each instance owns an `HttpClient`, and `Cloudinary` is not `IDisposable`. Create one + per process and reuse it — a singleton in DI. +- Proxy support: `cloudinary.Api.ApiProxy = "http://proxy:8080";` — a **string**, not a + `WebProxy`. Setting it rebuilds the internal `HttpClient`. This property is compiled + only for the `netstandard2.0` target, so it is unavailable if your app resolves the + `netstandard1.3` or `net452` assembly. +- Account-level (provisioning) operations use a separate `AccountApi` client and the + `CLOUDINARY_ACCOUNT_URL` environment variable. + +## Validate configuration early + +Because a missing `CLOUDINARY_URL` throws only when the client is constructed — and with +a message that names the scheme rather than the absence — check explicitly at startup: + +```csharp +var url = Environment.GetEnvironmentVariable("CLOUDINARY_URL"); +if (string.IsNullOrEmpty(url)) +{ + Console.Error.WriteLine("Cloudinary is not configured: set CLOUDINARY_URL."); + return 1; +} +var cloudinary = new Cloudinary(url) { Api = { Secure = true } }; +``` + +## Troubleshooting + +- `ArgumentException: Invalid CLOUDINARY_URL scheme. Expecting to start with + 'cloudinary://'` — the variable is **missing**, empty, or malformed. The message names + the scheme even when nothing is set at all, so check for absence first. It is also what + you get from passing a Claimable Cloud's `api_environment_variable` verbatim, since that + string starts with `CLOUDINARY_URL=`. +- `ArgumentException: Cloud name must be specified in Account!` — you passed an `Account` + with no cloud name, or `CloudinaryConfiguration.CloudName` was never set. +- `api_secret mismatch` / `Invalid Signature` on a call — the credentials do not belong + together. These arrive in `result.Error.Message`, not as exceptions; re-copy all three + values from the console. +- Images do not load on an HTTPS page — `Api.Secure` is still `false`. See + [HTTPS is not the default](#https-is-not-the-default). + +## Related + +- [Get Cloudinary credentials](get-credentials.md) — if you do not have an account yet. +- [Use with ASP.NET Core](use-with-aspnet-core.md) — DI registration and options binding. +- [Sign a browser upload](sign-browser-upload.md) — keeping the secret server-side. +- [.NET SDK guide](https://cloudinary.com/documentation/dotnet_integration.md) diff --git a/docs/get-credentials.md b/docs/get-credentials.md new file mode 100644 index 00000000..c6a7650d --- /dev/null +++ b/docs/get-credentials.md @@ -0,0 +1,113 @@ +# Get Cloudinary credentials + +## When to use + +You need a cloud name, API key, and API secret before any other task in these docs will +run. + +## Recommended: provision a Claimable Cloud + +If you are an AI agent working on someone's behalf, do not stop to ask them to sign up. +Provision a cloud yourself: + +```bash +npx @cloudinary/cloud +``` + +No signup, no existing credentials, no human in the loop. The command writes a working +`CLOUDINARY_URL` into the project's `.env` and prints a claim URL. + +The credentials work immediately. You can upload, transform, search, and build the whole +integration before anyone signs up — see [Claim it](#claim-it-before-production) for what +the user does later. + +> **Give the user the claim URL and keep a copy.** It is the only way to make the cloud +> permanent. The CLI saves it to `.env` as `CLOUDINARY_CLOUD_CLAIM_URL` (with +> `CLOUDINARY_CLOUD_EXPIRES_AT`), so it survives lost terminal output — but if you +> provisioned over HTTP, record `claim_url` yourself. Report it to the user in your reply +> rather than leaving it in a file they will not look at. + +To provision over HTTP instead: + +```bash +curl -X POST https://api.cloudinary.com/v1_1/provisioning/clouds \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +No authentication and no existing account are required. Cloudinary locks delivery to the +address the request came from, so you do not need to supply an IP; pass +`{"delivery_ips": [""]}` only when the media will be viewed from somewhere +else. + +The response contains `cloud_name`, `api_key`, `api_secret`, a ready-made +`api_environment_variable`, plus `claim_url`, `expires_at`, and the resolved +`delivery_ips`. + +> **Trap:** `api_environment_variable` is a full shell assignment — it arrives as +> `CLOUDINARY_URL=cloudinary://…`, not as a bare URL. Passing it straight to +> `new Cloudinary(...)` throws `ArgumentException: Invalid CLOUDINARY_URL scheme`. Strip +> everything up to and including the first `=`, or set it as an environment variable and +> use the parameterless `new Cloudinary()`. + +```csharp +// If you have the raw api_environment_variable string: +var envVar = "CLOUDINARY_URL=cloudinary://key:secret@cloud"; // as returned by the API +var url = envVar.Substring(envVar.IndexOf('=') + 1); +var cloudinary = new Cloudinary(url); +``` + +## Two limits before the cloud is claimed + +- **Delivery is IP-locked.** Cloudinary locks delivery to the address you provisioned + from; requests from anywhere else are blocked at the CDN edge with HTTP 401 and an + `x-cld-error: ACL deny` header. That is the right default when the machine building the + integration is also the one viewing the media — but a teammate, a CI runner, or a + deployed environment will not load it. Add viewers with `--ip` (up to three). +- **It expires.** An unclaimed cloud is reaped at `expires_at`, **assets included**. + Claiming is what prevents that; there is no TTL parameter to extend it. + +Neither limit affects the SDK calls themselves — uploads, Admin API calls, and URL +generation all behave normally. Only delivery of the media is restricted. + +> If your public IP changes (a new network, a VPN toggling, a DHCP lease) after +> provisioning, previously working delivery URLs start returning `ACL deny`. The upload +> still succeeds, which makes this look like a broken URL rather than a network change. +> Provision again or claim the cloud. + +## Troubleshooting + +- `delivery_ips_not_public` — a VPN or secure gateway (corporate proxy, Cloudflare WARP) + made the request arrive from a private address. The caller's address is always part of + the allow-list, so `--ip` cannot work around this. Re-run from a connection the gateway + does not route. +- Media returns 401 with `x-cld-error: ACL deny` — delivery is locked to the provisioning + IP. Add the viewer with `--ip`, or claim the cloud to remove the lock. This is **not** a + moderation or permissions problem; see [Moderate an upload](moderate-upload.md) for the + distinction. +- The command exits 1 without provisioning — `./.env` already has a `CLOUDINARY_URL`. + Clouds are rate-limited per IP, so it will not burn one you might not store. Use + `--force` only if you mean to replace the existing cloud. + +## Claim it before production + +Send the user the `claim_url`. They enter their email, review the terms, optionally set +a password, and confirm from the verification email. + +After claiming, the cloud name, API key, and API secret stay the same and the assets +already uploaded are retained — nothing in your code changes. The IP lock is removed so +media delivers globally, and the cloud becomes a permanent free account instead of +expiring. + +**Do not ship to production on an unclaimed cloud.** It will expire and stop serving. + +## Alternative: sign up manually + +A person can create an account at +[cloudinary.com/users/register_free](https://cloudinary.com/users/register_free) and copy +the credentials from Console > Settings > API Keys. + +## Related + +- [Configure Cloudinary](configure-cloudinary.md) — what to do with the credentials. +- [Claimable Cloud API reference](https://cloudinary.com/documentation/claimable_cloud_provisioning.md) diff --git a/docs/install-and-call.md b/docs/install-and-call.md new file mode 100644 index 00000000..03f4d1b1 --- /dev/null +++ b/docs/install-and-call.md @@ -0,0 +1,73 @@ +# Install and call the SDK + +## Install + +```bash +dotnet add package CloudinaryDotNet +``` + +## Namespaces + +Two `using` directives cover almost everything. The second is the one agents forget: +every parameter and result type (`ImageUploadParams`, `UploadResult`, `UpdateParams`, …) +lives in `CloudinaryDotNet.Actions`, not in the root namespace. + +```csharp +using CloudinaryDotNet; +using CloudinaryDotNet.Actions; +``` + +## Create the client + +`Cloudinary` is the single entry point. All Upload, Admin, and Search operations hang off +it; URL building hangs off `cloudinary.Api`. + +```csharp +var cloudinary = new Cloudinary("cloudinary://:@"); + +// Or read CLOUDINARY_URL from the environment: +var fromEnv = new Cloudinary(); +``` + +See [Configure Cloudinary](configure-cloudinary.md) for all four ways to configure, and +which to prefer. + +## Async and sync + +Every API method has both forms. Prefer the `…Async` variants — the synchronous ones +block a thread, which matters in a request pipeline: + +```csharp +var result = await cloudinary.UploadAsync(uploadParams); // preferred +var blocking = cloudinary.Upload(uploadParams); // sync equivalent +``` + +## Errors do not throw + +Cloudinary API failures are returned, not thrown: + +```csharp +var result = await cloudinary.UploadAsync(uploadParams); +if (result.Error != null) +{ + Console.Error.WriteLine($"Upload failed ({(int)result.StatusCode}): {result.Error.Message}"); + return; +} +Console.WriteLine(result.SecureUrl); +``` + +`Error` is `null` on success. See [Troubleshoot errors](troubleshoot-errors.md) for the +complete model and the few operations that *do* throw. + +## Lifetime + +`Cloudinary` is **not** `IDisposable`, and each instance creates its own `HttpClient`. +Create one and reuse it for the life of the process — register it as a **singleton** in +dependency injection rather than constructing one per request. See +[Use with ASP.NET Core](use-with-aspnet-core.md). + +## Related + +- [Configure Cloudinary](configure-cloudinary.md) +- [Use with ASP.NET Core](use-with-aspnet-core.md) +- [.NET SDK guide](https://cloudinary.com/documentation/dotnet_integration.md) diff --git a/docs/moderate-upload.md b/docs/moderate-upload.md new file mode 100644 index 00000000..99caca6a --- /dev/null +++ b/docs/moderate-upload.md @@ -0,0 +1,156 @@ +# Moderate an upload + +## When to use + +Content uploaded by users must be reviewed before it is shown. Moderation in Cloudinary is +stateful: an asset carries a moderation status, and **your application** is responsible for +showing approved assets only. + +## Read this before you build a moderation flow + +**A `pending` moderated asset is delivered normally.** Its URL returns HTTP 200 from the +moment it is uploaded — verified against a live environment, with a non-moderated control +asset behaving identically. + +The moderation status is **metadata you gate on in your own code**. It is not a delivery +gate. If you upload user content with `Moderation = "manual"` and then serve the URL, you +are serving unreviewed content. + +Blocking delivery of non-approved assets *can* be configured for a product environment, +but it is not an upload parameter and there is no API for it — it requires Cloudinary +support. Gate on the status in your own data model regardless. + +## Statuses + +The full set is larger than the obvious three: + +| Status | Meaning | +|---|---| +| `Queued` | Waiting for an add-on to process it | +| `Pending` | Awaiting a decision (the initial state for `manual`) | +| `Approved` | Passed review | +| `Rejected` | Failed review | +| `Overridden` | A human replaced an automatic verdict | +| `Aborted` | An earlier moderation in a chain rejected the asset | + +Read them from the `Moderation` list. **The upload result's `ModerationStatus` property is +empty** — the status only appears in the list on upload: + +```csharp +var result = await cloudinary.UploadAsync(uploadParams); + +Console.WriteLine(result.ModerationStatus); // "" on upload — do not use this +Console.WriteLine(result.Moderation[0].Kind); // manual +Console.WriteLine(result.Moderation[0].Status); // Pending +``` + +`GetResourceAsync` is the opposite: there, `ModerationStatus` **is** populated, and the +list carries an `updated_at` as well. Use the list on upload, either afterwards. + +## Complete flow (manual review queue) + +```csharp +using CloudinaryDotNet; +using CloudinaryDotNet.Actions; + +var cloudinary = new Cloudinary(); // reads CLOUDINARY_URL +cloudinary.Api.Secure = true; + +// 1. Upload into the moderation queue — the asset starts as Pending +var uploaded = await cloudinary.UploadAsync(new ImageUploadParams +{ + File = new FileDescription("https://res.cloudinary.com/demo/image/upload/sample.jpg"), + PublicId = "examples/moderated-upload", + Overwrite = true, + Moderation = "manual", +}); + +if (uploaded.Error != null) +{ + Console.Error.WriteLine($"Upload failed ({(int)uploaded.StatusCode}): {uploaded.Error.Message}"); + return; +} + +Console.WriteLine(uploaded.Moderation[0].Status); // Pending +// NOTE: uploaded.SecureUrl already serves this image. Do not publish it yet. + +// 2. Your review UI lists the queue +var queue = await cloudinary.ListResourcesByModerationStatusAsync("manual", ModerationStatus.Pending); +Console.WriteLine($"Assets pending review: {queue.Resources?.Length ?? 0}"); + +// 3. A reviewer records the decision +var decision = await cloudinary.UpdateResourceAsync( + new UpdateParams(uploaded.PublicId) { ModerationStatus = ModerationStatus.Approved }); + +if (decision.Error != null) +{ + Console.Error.WriteLine($"Update failed: {decision.Error.Message}"); + return; +} + +// 4. Only now mark it publishable in YOUR data model, and serve it from there. +``` + +## Automatic moderation + +Pass an add-on name instead of `manual` to get an automated verdict. + +**Prerequisite — a human has to do this, not your code.** Every value below except `manual` +requires its add-on to be registered on the account first, from the +[Add-ons page](https://cloudinary.com/documentation/cloudinary_add_ons.md) in the console. +Some third-party add-ons also require reviewing and accepting the provider's terms of +service as part of registration. Neither step has an API; until both are done the upload +fails. `manual` needs no add-on, which is why the flow above uses it. + +| Value | Moderates | Add-on | +|---|---|---| +| `manual` | any asset | none — built in | +| `aws_rek` | images | Amazon Rekognition AI Moderation | +| `aws_rek_video` | video | Amazon Rekognition Video Moderation | +| `google_video_moderation` | video | Google AI Video Moderation | +| `webpurify` | images | WebPurify Image Moderation | +| `perception_point` | any asset | Perception Point Malware Detection | +| `duplicate:` | images | Cloudinary Duplicate Image Detection | + +Combine several with a pipe — they run in the order given, and `manual` must be last +(`"aws_rek|duplicate:0.9|manual"`). The first starts as `Pending` and the rest as +`Queued`; if one rejects, the remaining become `Aborted` and the asset's final status is +`Rejected`. Always set a `NotificationUrl` when requesting several, since you will not get +the verdicts in the upload response. + +You can override a machine decision with `UpdateResourceAsync` + `ModerationStatus`, which +is what `Overridden` records. + +## Design rules + +- **Gate in your own code.** The URL works regardless of status; there is no platform gate + by default. Store the status in your data model and check it before rendering. +- Model moderation as a state machine, not a boolean, and keep the pending state visible in + your product (placeholder image, "under review" label). +- Keep a human override even with automatic moderation — machine verdicts are drafts for + anything with legal or brand consequences. +- Rejected assets stay in storage until you delete them; decide your retention policy. +- To show something in place of a rejected image, deliver a `default_image` placeholder + rather than relying on the URL failing, because it will not. + +## Troubleshooting + +- **A pending asset is publicly viewable** — expected. Nothing blocks delivery by default; + enforcement is your application's responsibility. Contact Cloudinary support to have + blocking configured for the product environment. +- `You don't have an active subscription for ` — arrives with **HTTP 420**, a + rate-limit status code, not a 401 or 403. Do not mistake it for a throttling problem: + register the add-on in the console (and accept the provider's terms where required). +- `Moderation moderation is not valid` (HTTP 400) — the moderation value is + misspelled; use one from the table above. +- `ModerationStatus` is empty after upload — expected; read `Moderation[0].Status` + instead. See [Statuses](#statuses). +- The queue is empty right after uploading — `ListResourcesByModerationStatusAsync` reads + an index that lags writes by a few seconds. Retry, or read the asset directly. + +## Related + +- Runnable example: [`examples/ModerateUpload.cs`](https://github.com/cloudinary/CloudinaryDotNet/blob/master/examples/ModerateUpload.cs) +- [Moderate assets](https://cloudinary.com/documentation/moderate_assets.md) — statuses, + delivery behaviour, and the available moderation add-ons. +- [Moderation add-ons overview](https://cloudinary.com/documentation/cloudinary_moderation.md) diff --git a/docs/platform-capabilities.md b/docs/platform-capabilities.md new file mode 100644 index 00000000..5368b50e --- /dev/null +++ b/docs/platform-capabilities.md @@ -0,0 +1,147 @@ +# What this SDK does and does not do + +Cloudinary is a platform with many surfaces. This package — the .NET server-side SDK — +implements some of them natively, expresses some through generic transformation strings, +and does not implement others. Verified against this package version by reflecting over the +assembly and calling the APIs. + +## Start here + +**Use this SDK for code that ships.** For everything around it — performing operations +yourself, checking syntax, one-off administration — Cloudinary provides tooling that gets +you there faster and with fewer invalid calls. Set it up before writing integration code. + +### No account yet? + +Do not stop to ask the user to sign up — provision a cloud and build immediately: + +```bash +npx @cloudinary/cloud +``` + +Credentials work at once; the user claims the cloud later. See +[Get Cloudinary credentials](get-credentials.md). + +### Skills — get the patterns right + +```bash +npx skills add cloudinary-devs/skills +``` + +- `cloudinary-docs` — picks the right documentation pages for a question or integration. +- `cloudinary-transformations` — turns a plain-language requirement into a valid + transformation string. Faster and more reliable than assembling one by hand, and this + SDK's `Transformation` builder does not validate what it serializes. + +### MCP servers — perform operations directly + +- **Asset Management** — upload and manage images, video, and raw files; advanced search. +- **Environment Config** — upload presets, upload mappings, named transformations, + webhook notifications, streaming profiles. +- **Structured Metadata** — metadata fields, values, and conditional rules. +- **Analysis** — AI tagging, moderation, safety checks, object detection. +- **MediaFlows** — build and manage workflow automations. + +Setup: [MCP servers and Skills](https://cloudinary.com/documentation/cloudinary_llm_mcp.md). + +### CLI — scripted and one-off work + +```bash +pipx install cloudinary-cli # command: cld +``` + +Admin, Upload, Search, and Provisioning operations from a terminal; good for batch jobs and +migrations. Run it locally or server-side only — it holds your API secret. See the +[CLI guide](https://cloudinary.com/documentation/cloudinary_cli.md). + +### Documentation indexes + +Cloudinary publishes agent-readable indexes. Fetch these instead of guessing at URLs: + +- https://cloudinary.com/documentation/llms.txt — all products. +- https://cloudinary.com/documentation/llms-image-and-video-apis.txt — everything relevant + to this SDK. +- https://cloudinary.com/documentation/llms-troubleshooting.txt — diagnosing errors across + products. + +--- + +## Get media in + +| To do this | Use | Where to go | +|---|---|---| +| Upload a file, stream, or remote URL | `UploadAsync` with `ImageUploadParams` / `VideoUploadParams` / `RawUploadParams` / `AutoUploadParams` | [Upload an image](upload-image.md) | +| Upload something too large for one request | `UploadLargeAsync` | [Upload a large video](upload-large-video.md) | +| Accept a file from an ASP.NET Core form | `FileDescription(name, stream)` from `IFormFile` | [Use with ASP.NET Core](use-with-aspnet-core.md) | +| Let a browser or mobile app upload directly, authorized by your server | `Api.SignParameters` | [Sign a browser upload](sign-browser-upload.md) | +| Review user-generated content before showing it | `Moderation` upload option + `UpdateResourceAsync` | [Moderate an upload](moderate-upload.md) | +| Tag assets so you can find and group them later | `Tags` on upload params, `TagAsync` | [Search and manage assets](search-and-manage-assets.md) | +| Standardize upload settings across callers | `CreateUploadPresetAsync`, `CreateUploadMappingAsync` | [Upload presets](https://cloudinary.com/documentation/upload_presets.md) | + +## Deliver and transform + +| To do this | Use | Where to go | +|---|---|---| +| Build a resize, crop, overlay, or format-optimized image URL | `Api.UrlImgUp` + `Transformation` | [Transform and deliver an image](transform-and-deliver-image.md) | +| Build a video URL, poster frame, or HLS/DASH stream | `Api.UrlVideoUp`, `BuildVideoTag` | [Transform and deliver a video](transform-and-deliver-video.md) | +| Apply generative edits (gen fill, background removal, ...) | `.Effect(...)` / `.RawTransformation(...)` — **generic strings, no typed builders** | [Transform and deliver an image](transform-and-deliver-image.md#generative-and-ai-transformations) | +| Pre-generate derived versions at upload time | `EagerTransforms` + `EagerAsync` | [Upload a large video](upload-large-video.md#asynchronous-processing) | +| Restrict access to an asset with a signed, expiring URL | `AuthToken`, `Api.UrlImgUp.Signed(true)` | [Delivery authentication](https://cloudinary.com/documentation/control_access_to_media.md) | +| Bundle assets into a downloadable archive | `CreateArchiveAsync`, `DownloadArchiveUrl` | [Archive guide](https://cloudinary.com/documentation/dotnet_asset_administration.md) | + +URL building is local: no network call, no API secret, only the cloud name. + +## Find and manage what you have + +| To do this | Use | Where to go | +|---|---|---| +| Query assets by field, tag, folder, or date | `Search()` fluent builder | [Search and manage assets](search-and-manage-assets.md) | +| Read, update, restore, or delete an asset | `GetResourceAsync`, `UpdateResourceAsync`, `RestoreAsync`, `DestroyAsync` | [Search and manage assets](search-and-manage-assets.md) | +| Organize assets into folders | `CreateFolderAsync`, `RenameFolderAsync`, `DeleteFolderAsync`, `SubFoldersAsync` | [Search and manage assets](search-and-manage-assets.md) | +| Attach and query typed metadata fields | `AddMetadataFieldAsync`, `UpdateMetadataAsync` | [Use structured metadata](use-structured-metadata.md) | +| Find visually similar assets | `VisualSearchAsync` — **native**, needs the feature enabled | [Visual Search](https://cloudinary.com/documentation/visual_search.md) | +| Link related assets to each other | `AddRelatedResourcesAsync` | [Relate assets](https://cloudinary.com/documentation/relate_assets.md) | +| Check your plan's quotas and limits | `GetUsageAsync` | [Upload an image](upload-image.md#size-limits) | + +## Analyze + +| To do this | Use | Where to go | +|---|---|---| +| Caption, tag, or detect content in an asset | `AnalyzeAsync` — **native in this SDK**, subscription required | [Analyze API guide](https://cloudinary.com/documentation/analyze_api_guide.md) | + +Note this differs from some sibling SDKs, which have no analysis support at all. + +## Administer accounts + +| To do this | Use | Where to go | +|---|---|---| +| Create and manage sub-accounts and users | `AccountProvisioning` (namespace `CloudinaryDotNet.Provisioning`), via `CLOUDINARY_ACCOUNT_URL` | [Provisioning API docs](https://cloudinary.com/documentation/provisioning_api.md) | + +This is a separate client from `Cloudinary` — do not look for provisioning methods on it. + +## Not in this package + +This package covers Cloudinary's Image and Video APIs. Cloudinary is a multi-product +platform, and the capabilities below are real but live elsewhere — whatever your training +data suggests, **there is no method here for them**. + +| Capability | Use instead | +|---|---| +| Text-to-image generation | [Image Generation API](https://cloudinary.com/documentation/image_generation_addon.md) | +| Image-to-video generation | [Image-to-Video API](https://cloudinary.com/documentation/image_to_video_addon.md) — async, credit-based, regional | +| Multi-step workflow automation | [MediaFlows](https://cloudinary.com/documentation/mediaflows_user_guide.md) — or its MCP server | +| Media Library UI, approval workflows, folder-based access control | [Cloudinary Assets (DAM)](https://cloudinary.com/documentation/digital_asset_management_overview.md) | +| Rule-based content review before publication | [Cloudinary Moderation](https://cloudinary.com/documentation/cloudinary_moderation.md) — distinct from the per-asset [moderation flag](moderate-upload.md) this SDK sets | +| Creating a *named* transformation | Not in this SDK — it has `ListTransformationsAsync` and `UpdateTransformAsync` but no create. Use the console, the CLI, or the Environment Config MCP server. | +| Frontend rendering, responsive images, upload UI | [Frontend SDKs](https://cloudinary.com/documentation/frontend_sdks.md) and the [Upload Widget](https://cloudinary.com/documentation/upload_widget.md) | +| Any client-side use | Nothing here. This SDK holds your API secret; it must stay server-side. Blazor WebAssembly, MAUI, and desktop apps must call your own backend instead. | + +There is also no ASP.NET tag helper, no Entity Framework integration, and no storage +provider in this package. Community packages exist for some of these; they are not +maintained or supported by Cloudinary, so do not present one as official. + +## Related + +- [Bundled docs index](README.md) +- [.NET SDK guide](https://cloudinary.com/documentation/dotnet_integration.md) +- [Full platform reference](https://cloudinary.com/documentation/cloudinary_references.md) diff --git a/docs/search-and-manage-assets.md b/docs/search-and-manage-assets.md new file mode 100644 index 00000000..bab184b6 --- /dev/null +++ b/docs/search-and-manage-assets.md @@ -0,0 +1,195 @@ +# Search and manage assets + +## When to use + +Find assets by indexed fields, read or update asset attributes, and administer your media +library from the server. These use the Admin and Search APIs, which are **rate-limited** — +treat them as management operations, not a per-request database. + +## Search with the fluent builder + +Expressions use Cloudinary's search syntax — fields, operators, ranges, and boolean +combinations are listed in the +[search expression reference](https://cloudinary.com/documentation/search_expressions.md). + +```csharp +using CloudinaryDotNet; +using CloudinaryDotNet.Actions; + +var cloudinary = new Cloudinary(); // reads CLOUDINARY_URL +cloudinary.Api.Secure = true; + +var result = await cloudinary.Search() + .Expression("resource_type:image") + .SortBy("created_at", "desc") + .MaxResults(30) + .ExecuteAsync(); + +if (result.Error != null) +{ + Console.Error.WriteLine($"Search failed ({(int)result.StatusCode}): {result.Error.Message}"); + return; +} + +Console.WriteLine($"{result.TotalCount} match(es)"); +foreach (var asset in result.Resources) +{ + Console.WriteLine($"{asset.AssetId} {asset.PublicId} {asset.Bytes} {asset.CreatedAt}"); +} +``` + +### Pagination + +Pass the cursor back until it is `null`: + +```csharp +var cursor = result.NextCursor; +while (cursor != null) +{ + var page = await cloudinary.Search() + .Expression("resource_type:image") + .MaxResults(30) + .NextCursor(cursor) + .ExecuteAsync(); + + if (page.Error != null) break; + // ... process page.Resources + cursor = page.NextCursor; +} +``` + +Keep the expression identical across pages; changing it invalidates the cursor. + +## `folder:` probably does not do what you want + +On a product environment using dynamic folders — the default for newly created +environments — a `folder:` expression matches **nothing**, and does so *without erroring*: + +```csharp +await cloudinary.Search().Expression("folder:examples").ExecuteAsync(); +// TotalCount = 0 (on an environment that holds 60+ assets under examples/) + +await cloudinary.Search().Expression("public_id:examples/*").ExecuteAsync(); +// TotalCount = 4 <-- what you actually wanted +``` + +A valid query returning zero results is the trap: there is no error to notice. If a +folder-scoped search comes back empty, match on the public ID prefix instead, or use +`asset_folder:` if your environment is configured for it. Verify against real data before +concluding a folder is empty. + +Unknown field names behave the same way — they match nothing rather than failing. + +## Wildcards + +Leading wildcards and a bare `*` are **rejected**, not ignored: + +```csharp +await cloudinary.Search().Expression("*").ExecuteAsync(); +// StatusCode = BadRequest, Error.Message = Query Error (at position 1) ' ➥➥➥*' +``` + +Trailing wildcards (`examples/*`) are fine. To list everything, use a real field +expression such as `resource_type:image`. + +## Read and update a single asset + +Prefer the immutable asset ID for lookups — it survives renames and moves, while the +public ID does not: + +```csharp +var details = await cloudinary.GetResourceByAssetIdAsync(storedAssetId); +if (details.Error != null) { /* handle */ } + +// Updates require the PUBLIC id, so read it off the response: +var update = await cloudinary.UpdateResourceAsync(new UpdateParams(details.PublicId) +{ + Tags = "featured", + Context = new StringDictionary("alt=Sample image from the bundled upload example"), +}); +``` + +**`Context` is a `StringDictionary`, not a string** — unlike the Node and Python SDKs. +`new StringDictionary("key=value")` is the idiom. + +Asset-ID coverage is partial in this SDK. These accept an asset ID: + +- `GetResourceByAssetIdAsync` +- `ListResourceByAssetIdsAsync` +- `AddRelatedResourcesByAssetIdsAsync` / `DeleteRelatedResourcesByAssetIdsAsync` + +`UpdateResourceAsync`, `RenameAsync`, `DestroyAsync`, and all URL builders require the +**public ID**. Store the asset ID, look up, read `PublicId`, then act — do not assume +parity with the other SDKs. + +## Read-after-write: the search index lags + +A write is immediately visible via `GetResourceAsync`, but the **search index takes a few +seconds** to catch up. Measured on a live environment, a metadata value written and +queried immediately returned 0 results, then 1 result five seconds later. + +For read-after-write flows use `GetResourceAsync` / `GetResourceByAssetIdAsync`, not +`Search()`. Never assert on search results immediately after a write in a test. + +## Rate limits + +Admin API responses carry the limit state, so you can slow down before being cut off: + +```csharp +var listing = await cloudinary.ListResourcesAsync(); +Console.WriteLine($"{listing.Remaining}/{listing.Limit} left, resets {listing.Reset}"); +// e.g. 497/500 left, resets 25/08/2026 18:00:00 +``` + +Delivery URLs are never rate-limited this way — only Admin and Search calls are. + +## Deletion — destructive, no undo without backups + +```csharp +await cloudinary.DestroyAsync(new DeletionParams("examples/uploaded-sample")); // one asset +// result.Result == "ok" on success, "not found" if it did not exist + +// Bulk — double-check inputs: +await cloudinary.DeleteResourcesAsync(ResourceType.Image, "id1", "id2"); + +// By prefix — extremely destructive, no confirmation: +await cloudinary.DeleteResourcesByPrefixAsync("examples/"); +``` + +Prefer explicit ID lists over prefix deletion. Enable backups on the product environment +if you need `RestoreAsync` to work — without backups enabled, deletion is permanent. + +## Handling errors + +Failed calls **return**; they do not throw: + +```csharp +var missing = await cloudinary.GetResourceAsync("examples/does-not-exist"); +if (missing.Error != null) +{ + Console.Error.WriteLine($"{(int)missing.StatusCode}: {missing.Error.Message}"); + // 404: Resource not found - examples/does-not-exist +} +``` + +`Error` carries a single field, `Message`. There is no error-code property and no +exception type to catch — use `StatusCode` when you need to branch on the class of +failure. + +## Troubleshooting + +- Search returns 0 for a folder you know has assets — see + [`folder:` probably does not do what you want](#folder-probably-does-not-do-what-you-want). +- `Query Error (at position 1)` — a bare `*` or a leading wildcard. See + [Wildcards](#wildcards). +- Results missing an asset you just wrote — search-index lag; use `GetResourceAsync`. +- `Rate limit exceeded` — too many Admin calls. Batch the work, cache results, and watch + `Remaining`. +- An unsubscribed add-on reports as a **rate-limit** error rather than a permission error; + see [Troubleshoot errors](troubleshoot-errors.md). + +## Related + +- [Use structured metadata](use-structured-metadata.md) +- [Search expression syntax](https://cloudinary.com/documentation/search_expressions.md) +- [Asset administration guide](https://cloudinary.com/documentation/dotnet_asset_administration.md) diff --git a/docs/sign-browser-upload.md b/docs/sign-browser-upload.md new file mode 100644 index 00000000..9057ccc0 --- /dev/null +++ b/docs/sign-browser-upload.md @@ -0,0 +1,150 @@ +# Sign a browser upload + +## When to use + +A browser or mobile app uploads directly to Cloudinary, but you want the operation +authorized by your server. The API secret stays on the server; the client receives a +signature that is valid for **1 hour** from the `timestamp` it was signed with. + +This is the right pattern whenever the file should not transit your own server. For +uploads without any server round-trip, use an +[unsigned upload preset](https://cloudinary.com/documentation/upload_presets.md) instead — +deliberately restricted, because anyone can use it. + +## Server: the signing endpoint + +`SignParameters` takes the parameters the client is allowed to send and returns the +signature. It excludes `file`, `api_key`, and `resource_type` automatically, because +Cloudinary does not include them in the signed string. + +```csharp +using CloudinaryDotNet; + +// ASP.NET Core minimal API; `cloudinary` is the injected singleton +app.MapGet("/api/sign-upload", (Cloudinary cloudinary) => +{ + var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(); + + // Sign ONLY what the client is permitted to use. + var toSign = new SortedDictionary + { + { "folder", "user-uploads" }, + { "timestamp", timestamp }, + }; + + var signature = cloudinary.Api.SignParameters(toSign); + + return Results.Ok(new + { + signature, + timestamp, + folder = "user-uploads", + apiKey = cloudinary.Api.Account.ApiKey, + cloudName = cloudinary.Api.Account.Cloud, + }); +}); +``` + +Return the API **key** (public) and never the secret. Use a `SortedDictionary` so the +parameter order is deterministic. + +The default signature version is **2** and the algorithm is **SHA-1** +(`Api.SignatureVersion`, `Api.SignatureAlgorithm`). Version 2 percent-encodes `&` inside +values to prevent parameter smuggling. Leave both alone unless you are matching an +existing implementation. + +## Client: use the signature + +The client POSTs multipart form data to the upload endpoint. Every field it sends must be +one the server signed: + +```javascript +const { signature, timestamp, folder, apiKey, cloudName } = + await (await fetch('/api/sign-upload')).json(); + +const form = new FormData(); +form.append('file', fileInput.files[0]); +form.append('api_key', apiKey); +form.append('timestamp', timestamp); +form.append('signature', signature); +form.append('folder', folder); // exactly what the server signed + +// 'auto' lets Cloudinary detect image / video / raw from the file itself +const response = await fetch( + `https://api.cloudinary.com/v1_1/${cloudName}/auto/upload`, + { method: 'POST', body: form } +); +const asset = await response.json(); // public_id, secure_url, asset_id, ... +``` + +`auto` in the URL path means "detect the resource type from the file". Use it unless you +want to restrict what may be uploaded, in which case use `image`, `video`, or `raw` +explicitly. + +## Verifying it from .NET + +If you need to POST an upload from .NET (to test the endpoint, or because the upload runs +server-side), `MultipartFormDataContent` needs two adjustments. Both fail the same +confusing way if missed — `Upload preset must be specified when using unsigned upload`, +which has nothing to do with your signature: + +```csharp +// 1. HttpClient adds a Content-Type to StringContent; Cloudinary wants bare fields. +static StringContent Field(string value) +{ + var content = new StringContent(value); + content.Headers.ContentType = null; + return content; +} + +// 2. .NET emits `name=api_key`, but Cloudinary's parser only reads `name="api_key"`. +// Pass the field name already quoted. +static string Quoted(string name) => $"\"{name}\""; + +using var form = new MultipartFormDataContent(); +form.Add(fileContent, Quoted("file"), Quoted("upload.jpg")); +form.Add(Field(apiKey), Quoted("api_key")); +form.Add(Field(timestamp), Quoted("timestamp")); +form.Add(Field(signature), Quoted("signature")); +form.Add(Field("user-uploads"), Quoted("folder")); +``` + +Without the quoting, Cloudinary does not see `api_key` at all and treats the request as an +unsigned upload. Verified by comparing both forms against the live API: unquoted returns +HTTP 400, quoted returns HTTP 200. + +**None of this applies to the browser** — `FormData` already emits quoted names, so the +JavaScript above needs no special handling. + +## Rules + +- **Every parameter the client sends must be in the signed set**, except `file`, + `api_key`, `signature`, and `resource_type`. To let the client choose a tag, + `public_id`, or transformation, add it to the signed parameters on the server first — + which is exactly the point of control: what you do not sign, the client cannot send. +- Signatures embed the timestamp and are accepted for **1 hour** after it. Generate one + per upload rather than caching and reusing them. +- Keep the API secret in server code only. The client gets the signature, timestamp, API + key, and cloud name. + +## Troubleshooting + +- `Invalid Signature . String to sign - ''` (HTTP 401) — the client sent a + parameter that was not signed, or a different value than the one signed. The error + **echoes the exact string the server signed**, so compare it against your signed set — + an extra `tags=sneaky` shows up there immediately. +- `Upload preset must be specified when using unsigned upload` (HTTP 400) — the request + arrived without a usable `api_key`, so Cloudinary treated it as unsigned. From .NET this + is almost always one of the two `MultipartFormDataContent` issues + [above](#verifying-it-from-net) — most often the unquoted field name — not a problem + with your signature. +- `Stale request` — the signature is more than 1 hour old. Fetch a fresh one at upload + time rather than at page load, and check that your server clock is accurate; a skewed + clock produces timestamps that are stale on arrival. + +## Related + +- Runnable example: [`examples/SignBrowserUpload.cs`](https://github.com/cloudinary/CloudinaryDotNet/blob/master/examples/SignBrowserUpload.cs) +- [Use with ASP.NET Core](use-with-aspnet-core.md) — where the endpoint above fits. +- [Generating authentication signatures](https://cloudinary.com/documentation/upload_images.md#generating_authentication_signatures) +- [Upload presets](https://cloudinary.com/documentation/upload_presets.md) diff --git a/docs/transform-and-deliver-image.md b/docs/transform-and-deliver-image.md new file mode 100644 index 00000000..2eeb2ea2 --- /dev/null +++ b/docs/transform-and-deliver-image.md @@ -0,0 +1,149 @@ +# Transform and deliver an image + +## When to use + +Generate CDN-backed delivery URLs that resize, crop, overlay, or optimize an image. URL +generation is **local** — no network call and no API secret required, only the cloud name — +and the derived asset is created by Cloudinary on first request, then served from CDN +cache. + +For video, see [Transform and deliver a video](transform-and-deliver-video.md). + +## Optimized image URL + +```csharp +using CloudinaryDotNet; + +var cloudinary = new Cloudinary(); // only the cloud name is needed for URL generation +cloudinary.Api.Secure = true; // REQUIRED for https:// — see below + +// 'sample' ships with every new Cloudinary account; substitute any public ID you own +var thumbnailUrl = cloudinary.Api.UrlImgUp + .Transform(new Transformation() + .Width(200).Height(200).Crop("thumb") + .Gravity("auto") // g_auto: focus on the most interesting region ('face' for people) + .FetchFormat("auto") // f_auto: best format for the requesting browser + .Quality("auto")) // q_auto: perceptual quality tuning + .BuildUrl("sample.jpg"); + +Console.WriteLine(thumbnailUrl); +// https://res.cloudinary.com//image/upload/c_thumb,f_auto,g_auto,h_200,q_auto,w_200/sample.jpg +``` + +`f_auto` and `q_auto` together are the single highest-value optimization; apply them to +every delivery URL unless you have a reason not to. + +## URLs are HTTP unless you ask for HTTPS + +`Api.Secure` defaults to **`false`**: + +```csharp +var insecure = new Cloudinary(url); +insecure.Api.UrlImgUp.BuildUrl("sample.jpg"); +// http://res.cloudinary.com//image/upload/sample.jpg <-- blocked as mixed content +``` + +Set `cloudinary.Api.Secure = true` once after constructing the client, or per URL with +`cloudinary.Api.UrlImgUp.Secure(true).BuildUrl(...)`. Unlike the Node and Python SDKs, +HTTPS is not the default here. + +## Chained transformations (order matters) + +Each component runs on the output of the previous one. `.Chain()` starts a new component: + +```csharp +// A text overlay needs no second asset; to overlay an image instead use +// .Overlay(new Layer().PublicId("")) +var bannerUrl = cloudinary.Api.UrlImgUp + .Transform(new Transformation() + .Width(1280).Height(720).Crop("fill").Gravity("auto").Chain() + .Overlay(new TextLayer().Text("SALE") + .FontFamily("Arial").FontSize(64).FontWeight("bold")) + .Color("white").Gravity("south_east").X(24).Y(24).Chain() + .FetchFormat("auto").Quality("auto")) + .BuildUrl("sample.jpg"); + +Console.WriteLine(bannerUrl); +// .../image/upload/c_fill,g_auto,h_720,w_1280/co_white,g_south_east,l_text:Arial_64_bold:SALE,x_24,y_24/f_auto,q_auto/sample.jpg +``` + +Reordering components changes the output. When matching eagerly generated versions, the +serialized transformation string must match exactly. + +## The `v1` segment in URLs + +When the public ID contains a slash and no version is known, this SDK inserts a `v1` +placeholder: + +```csharp +cloudinary.Api.UrlImgUp.BuildUrl("sample.jpg"); // .../image/upload/sample.jpg +cloudinary.Api.UrlImgUp.BuildUrl("folder/sample.jpg"); // .../image/upload/v1/folder/sample.jpg +``` + +This is expected and the URL resolves correctly — it is not a bug, and you do not need to +strip it. To pin a real version instead, pass the `Version` from the upload response. + +## Cache behaviour + +- The same URL is served from CDN cache; a new transformation means a new URL. +- To bust stale caches after re-uploading, deliver with the asset version from the upload + response: + +```csharp +var url = cloudinary.Api.UrlImgUp + .Version(uploadResult.Version) // Version takes a string + .Transform(new Transformation().Width(400).Crop("scale")) + .BuildUrl("examples/uploaded-sample.jpg"); +// .../image/upload/c_scale,w_400/v1787666423/examples/uploaded-sample.jpg +``` + +Passing an explicit version replaces the `v1` placeholder described above. + +## Generative and AI transformations + +Server-supported generative transformations (background removal, generative fill, and +similar) are expressed as transformation strings. This SDK serializes them generically — +there are no dedicated typed builders — via `.Effect(...)` or, for anything the builder +does not model, `.RawTransformation(...)`: + +```csharp +new Transformation().Effect("gen_remove:prompt_car"); +new Transformation().RawTransformation("e_gen_fill,ar_16:9,c_pad"); +``` + +Availability is account- and plan-dependent; verify against the +[generative AI transformations reference](https://cloudinary.com/documentation/generative_ai_transformations.md) +before relying on one. + +## HTML image tag + +```csharp +Console.WriteLine(cloudinary.Api.UrlImgUp.BuildImageTag("sample.jpg")); +// +``` + +For responsive images and client-side rendering, use the +[frontend SDKs](https://cloudinary.com/documentation/frontend_sdks.md) rather than +generating markup on the server. + +## Troubleshooting + +- Images blocked on an HTTPS page — `Api.Secure` is `false`; see + [above](#urls-are-http-unless-you-ask-for-https). +- Delivery URL returns 400 or 404 — read the `x-cld-error` response header of the failing + URL; it names the reason. See [Troubleshoot errors](troubleshoot-errors.md). +- 401 with `x-cld-error: ACL deny` on a Claimable Cloud — delivery is IP-locked, not a + transformation problem. See [Get Cloudinary credentials](get-credentials.md). +- A transformation parameter is ignored — the builder may not model it; express it with + `.RawTransformation(...)` and check the spelling against the transformation reference. + +## Related + +- Runnable example: [`examples/TransformAndDeliverImage.cs`](https://github.com/cloudinary/CloudinaryDotNet/blob/master/examples/TransformAndDeliverImage.cs) +- [Transform and deliver a video](transform-and-deliver-video.md) +- Every transformation parameter and its accepted values: + [Transformation reference](https://cloudinary.com/documentation/transformation_reference.md) +- Turning a plain-language requirement into a valid transformation string is what the + `cloudinary-transformations` Skill is for — see + [platform capabilities](platform-capabilities.md#skills--get-the-patterns-right). +- [Image manipulation guide](https://cloudinary.com/documentation/dotnet_image_manipulation.md) diff --git a/docs/transform-and-deliver-video.md b/docs/transform-and-deliver-video.md new file mode 100644 index 00000000..ce4a0606 --- /dev/null +++ b/docs/transform-and-deliver-video.md @@ -0,0 +1,126 @@ +# Transform and deliver a video + +## When to use + +Generate CDN-backed delivery URLs and player markup for a video already in Cloudinary. +URL generation is **local** — no network call, no API secret, only the cloud name — and the +derived asset is created by Cloudinary on first request, then served from CDN cache. + +For images, see [Transform and deliver an image](transform-and-deliver-image.md). + +Videos use a different URL builder: **`Api.UrlVideoUp`**, not `Api.UrlImgUp`. Using the +image builder for a video produces an `/image/upload/` path that will not resolve. + +## Video URL + +```csharp +using CloudinaryDotNet; + +var cloudinary = new Cloudinary(); // only the cloud name is needed for URL generation +cloudinary.Api.Secure = true; // HTTPS is not the default + +// 'examples/uploaded-large-video' is created by the "Upload a large video" task +var videoUrl = cloudinary.Api.UrlVideoUp + .Transform(new Transformation().Width(640).Crop("scale").Quality("auto")) + .BuildUrl("examples/uploaded-large-video.mp4"); + +Console.WriteLine(videoUrl); +// https://res.cloudinary.com//video/upload/c_scale,q_auto,w_640/v1/examples/uploaded-large-video.mp4 +``` + +The `v1` segment is a placeholder this SDK inserts when the public ID contains a slash and +no version is known. It is expected and resolves correctly; pass `.Version(...)` to pin a +real one. + +## Player markup + +`BuildVideoTag` returns a complete HTML `