SAVING UPLOADED FILES
=====================

THE ONE THING TO UNDERSTAND
---------------------------
Uploads land in a TEMPORARY store first (the configured TempDirectory) and are
swept after CoreUploadOptions.TempFileExpiry (default 1 hour). An upload that
"succeeds" is NOT saved anywhere permanent until YOUR code saves it.

WHAT YOU SEE ON DISK, STAGE BY STAGE
------------------------------------
  Stage                              Temp store (TempDirectory)          Your save folder
  ---------------------------------  ----------------------------------  -----------------
  1. File uploaded                   <guid>.upload + <guid>.meta appear  (unchanged)
  2. Handler runs (same request)     copy: pair stays / move: removed    saved file appears
  3. No handler registered           pair swept after TempFileExpiry     nothing - ever

Unlike Web Forms there is NO postback step: IUploadEventHandler runs during the
upload request itself. If nothing is saved, no handler is registered - this demo
app registers SaveUploadsToDisk in Program.cs.

THE COMPLETE PATTERN
--------------------
Register an IUploadEventHandler - it runs during the upload request itself
(no postback concept in ASP.NET Core):

    builder.Services.AddSingleton<IUploadEventHandler, SaveUploadsToDisk>();

    public sealed class SaveUploadsToDisk : IUploadEventHandler
    {
        private readonly IUploaderProvider _provider;
        private readonly IWebHostEnvironment _env;
        public SaveUploadsToDisk(IUploaderProvider p, IWebHostEnvironment e)
        { _provider = p; _env = e; }

        public async Task OnFileUploadedAsync(FileUploadedEvent e,
            CancellationToken ct = default)
        {
            var dir = Path.Combine(_env.WebRootPath, "uploads");
            Directory.CreateDirectory(dir);
            // Never trust the client's file name - keep only the extension.
            var name = Guid.NewGuid().ToString("N") +
                       Path.GetExtension(e.FileName ?? "");
            await _provider.CopyToAsync(e.FileGuid, Path.Combine(dir, name), ct);
        }
    }

THIS DEMO APP DOES EXACTLY THAT
-------------------------------
SaveUploadsToDisk.cs in the DemoApp root is registered in Program.cs, so every
demo upload in this app is copied to wwwroot/uploads - open the folder after
any upload and the file is there under a server-generated name.

ALTERNATIVES
------------
- Per-request: call IUploadService.MoveFileAsync/CopyFileAsync from your own
  endpoint after the upload completes.
- Direct-to-cloud (S3/Azure/GCS): the file never touches your server; the
  "save" is choosing the object key at create time - see the strategy demos.

FOLDER UPLOADS: KEEPING THE STRUCTURE
-------------------------------------
Folder drops and directory-picker selections carry each file's relative path
("photos/2026/beach.jpg"). It is sanitized server-side and stored as
UploadedFileInfo.RelativePath (also raised on FileUploadedEvent.RelativePath);
null for plain files. Recreate the tree under a root you choose:

    public async Task OnFileUploadedAsync(FileUploadedEvent e, CancellationToken ct)
    {
        var relative = e.RelativePath ?? e.FileName;
        var dest = Path.Combine(root, relative.Replace('/', Path.DirectorySeparatorChar));
        Directory.CreateDirectory(Path.GetDirectoryName(dest)!);
        // copy from the temp store to dest...
    }

See it live: DemoApp > Drag & Drop > Folder Upload (demo 47).

INSTANT UPLOAD (CONTENT DEDUPE, OPT-IN)
---------------------------------------
Server: options.EnableInstantUpload = true  (hashes every stored upload with
SHA-256 and answers GET /exists?hash=&size=). Client: instantUpload: true.
Content the server already stores completes instantly with the existing file's
id - zero bytes transferred; result.deduped is true and onInstantUpload fires.
Off by default: the probe reveals whether a content hash exists on the server,
so enable it only where that is acceptable (or add your own auth in front).
The server never trusts client-claimed hashes. Demo: Chunked Upload > 124.
