Direct-to-S3 / Azure tus 1.0 resume IndexedDB Golden Retriever 30 locales Webcam & screen capture

Folder uploads that keep their structure

Browsers can hand you an entire directory tree - but most upload stacks flatten it into a pile of file names. This guide covers how folder selection actually works, where the structure gets lost, and how to preserve and recreate it safely.

Two ways a browser delivers a folder

  • The directory picker. An <input type="file" webkitdirectory> opens a folder dialog; every file arrives with a webkitRelativePath like photos/2026/beach.jpg.
  • Drag-and-drop traversal. A dropped folder shows up as a DataTransferItem whose webkitGetAsEntry() yields a directory entry. The uploader walks it recursively with createReader().readEntries(); each file entry carries a fullPath such as /photos/2026/beach.jpg.

Both paths exist only at selection time. The moment a plain upload request is built, the standard multipart/form-data body carries a bare filename - and the structure is gone. That is why "folder upload" support in many libraries really means "we enumerate the files"; the tree itself never reaches the server.

Preserving the path end to end

CoreUpload captures the relative path at selection time and carries it the whole way:

// Client - nothing to configure; folder drops and directory picks
// populate task.relativePath automatically ('' for plain files).
CoreUpload.create('#uploader', {
    uploadUrl: '/api/upload/upload',
    webkitdirectory: true,
    onTaskComplete: function (task) {
        console.log(task.relativePath);   // 'photos/2026/beach.jpg'
    }
});

On the wire it travels as the relativePath form field (single uploads, with an X-Upload-Relative-Path header mirror) and inside the chunk-complete body for chunked transfers. Server-side it is sanitized and stored on the upload's metadata as UploadedFileInfo.RelativePath, and raised on FileUploadedEvent.RelativePath.

The part everyone gets wrong: sanitizing

A client-supplied path is attacker-controlled text. Combine it into a destination naively and ../../web.config is a valid "relative path". CoreUpload sanitizes once, at the door, before the value enters the system:

  • Separators normalized to /; empty, . and .. segments dropped.
  • Invalid file-name characters (plus :, so C: cannot address a drive) replaced with _; trailing dots and spaces stripped per segment.
  • Oversized input and absurd nesting rejected outright; a result with no real structure (a bare file name) is treated as absent.

Recreating the tree is then one line - still under a fixed root you choose:

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

See it live

The Folder Upload demo lists each completed file's preserved path as it lands. Drop a nested folder and compare the list against your disk.