How to Access Uploaded Files from a Form Publicly (Azure Storage)

2025/11/13 3:41 PM

Hi everyone,

Can someone help me figure out how to fetch files from Azure Storage that were uploaded through Forms?
Is it correct to check the files in this path: ~/assets/bizformfiles?

I’d really appreciate any guidance on how to do this.


Environment

Tags:
Kentico .NET Azure Integrations

Answers

2025/11/14 1:11 AM

I have this code that will call an this API endpoint but this still doesn't worked. I'm not sure if this is the right path so any guidance is appreciated.

[HttpGet("get")]
[ProducesResponseType(typeof(FileResult), 200)]
[ProducesResponseType(typeof(object), 400)]
[ProducesResponseType(typeof(object), 404)]
[ProducesResponseType(typeof(object), 500)]
public IActionResult GetFile([FromQuery] string filename)
{
    try
    {
        // Security: Validate input
        if (string.IsNullOrWhiteSpace(filename))
        {
            _eventLog.LogWarning("FormFileApi", "INVALID-INPUT", "Empty filename provided");
            return BadRequest(new { error = "Filename is required" });
        }

        // URL decode the filename (spaces, special chars may be encoded)
        var decodedFilename = Uri.UnescapeDataString(filename);
        _eventLog.LogInformation("FormFileApi", "FILENAME-RECEIVED", $"Original: {filename}, Decoded: {decodedFilename}");

        // Security: Sanitize and validate filename
        var sanitizedFilename = SanitizeFilename(decodedFilename);
        
        _eventLog.LogInformation("FormFileApi", "FILENAME-SANITIZED", $"Sanitized: {sanitizedFilename}");
        if (string.IsNullOrEmpty(sanitizedFilename))
        {
            _eventLog.LogWarning("FormFileApi", "INVALID-FILENAME", $"Invalid filename format: {filename}");
            return BadRequest(new { error = "Invalid filename format" });
        }

        // Security: Check filename length
        if (sanitizedFilename.Length > MaxFilenameLength)
        {
            _eventLog.LogWarning("FormFileApi", "FILENAME-TOO-LONG", $"Filename too long: {sanitizedFilename.Length} characters");
            return BadRequest(new { error = "Filename is too long" });
        }

        // Security: Validate file extension
        var extension = System.IO.Path.GetExtension(sanitizedFilename);
        if (string.IsNullOrEmpty(extension) || !AllowedExtensions.Contains(extension))
        {
            _eventLog.LogWarning("FormFileApi", "INVALID-EXTENSION", $"Invalid file extension: {extension}");
            return BadRequest(new { error = "File type not allowed" });
        }

        // Search for the file in form submissions
        var fileResult = FindFileInSubmissions(sanitizedFilename);
        
        if (fileResult == null)
        {
            _eventLog.LogInformation("FormFileApi", "FILE-NOT-FOUND", $"File not found: {sanitizedFilename}");
            return NotFound(new { error = "File not found" });
        }

        // Security: Log successful access
        _eventLog.LogInformation("FormFileApi", "FILE-ACCESSED", 
            $"File accessed: {sanitizedFilename}, SubmissionID: {fileResult.SubmissionItemID}, FormID: {fileResult.FormID}");

        // Get virtual file path for Azure Storage using SystemFileName (GUID)
        // Files are stored as ~/assets/BizFormFiles/{SystemFileName}
        // SystemFileName is the GUID filename used in Azure Storage
        if (string.IsNullOrWhiteSpace(fileResult.SystemFileName))
        {
            _eventLog.LogWarning("FormFileApi", "MISSING-SYSTEM-FILENAME", 
                $"SystemFileName is missing for file: {sanitizedFilename}");
            return NotFound(new { error = "File not found" });
        }

        // Construct virtual path - SystemFileName is the GUID filename
        // Note: ~/assets/ is mapped to Azure Storage in StorageInitializationModule
        // Try multiple path variations as files might be stored differently
        var systemFileName = fileResult.SystemFileName;
        
        // Try different path variations
        var pathVariations = new List<string>
        {
            $"/assets/BizFormFiles/{systemFileName}",  // Standard path with extension
            $"/assets/bizformfiles/{systemFileName}",  // Lowercase folder name
            $"/assets/BizFormFiles/{systemFileName.ToLowerInvariant()}",  // Lowercase filename
        };
        
        // If SystemFileName has an extension, also try without it
        if (systemFileName.Contains('.'))
        {
            var guidWithoutExt = systemFileName.Substring(0, systemFileName.LastIndexOf('.'));
            pathVariations.Add($"/assets/BizFormFiles/{guidWithoutExt}");
            pathVariations.Add($"/assets/bizformfiles/{guidWithoutExt}");
        }
        
        _eventLog.LogInformation("FormFileApi", "ATTEMPTING-FILE-ACCESS", 
            $"Attempting to access file: OriginalFileName={fileResult.OriginalFileName}, SystemFileName={systemFileName}, Trying {pathVariations.Count} path variations");
        
        CMS.IO.FileInfo? fileInfo = null;
        string? actualFilePath = null;
        
        // Try each path variation until we find the file
        foreach (var virtualFilePath in pathVariations)
        {
            // Security: Verify the file is within the BizFormFiles directory (prevent path traversal)
            if (!virtualFilePath.StartsWith("/assets/", StringComparison.OrdinalIgnoreCase) ||
                (!virtualFilePath.Contains("BizFormFiles", StringComparison.OrdinalIgnoreCase) && 
                 !virtualFilePath.Contains("bizformfiles", StringComparison.OrdinalIgnoreCase)))
            {
                continue; // Skip invalid paths
            }
            
            try
            {
                fileInfo = CMS.IO.FileInfo.New(virtualFilePath);
                if (fileInfo != null && fileInfo.Exists)
                {
                    actualFilePath = virtualFilePath;
                    _eventLog.LogInformation("FormFileApi", "FILE-FOUND-VARIATION", 
                        $"File found using path variation: {virtualFilePath}");
                    break;
                }
            }
            catch (Exception ex)
            {
                _eventLog.LogException("FormFileApi", "FILE-INFO-CREATION-ERROR", ex,
                    additionalMessage: $"VirtualPath: {virtualFilePath}");
            }
        }
        
        if (fileInfo == null || !fileInfo.Exists || string.IsNullOrEmpty(actualFilePath))
        {
            // Try to list files in the BizFormFiles directory to help debug
            try
            {
                // Extract GUID from SystemFileName (remove extension if present)
                var guidToFind = systemFileName;
                if (systemFileName.Contains('.'))
                {
                    guidToFind = systemFileName.Substring(0, systemFileName.LastIndexOf('.'));
                }
                
                var directoriesToCheck = new[] { "/assets/BizFormFiles", "/assets/bizformfiles" };
                
                foreach (var dirPath in directoriesToCheck)
                {
                    var dirInfo = CMS.IO.DirectoryInfo.New(dirPath);
                    if (dirInfo.Exists)
                    {
                        // Get all files
                        var allFiles = dirInfo.GetFiles();
                        var fileNames = allFiles.Select(f => f.Name).Take(20).ToList();
                        
                        // Search for files containing the GUID
                        var matchingFiles = allFiles.Where(f => 
                            f.Name.Contains(guidToFind, StringComparison.OrdinalIgnoreCase)).ToList();
                        
                        _eventLog.LogInformation("FormFileApi", "DIRECTORY-LISTING", 
                            $"Directory {dirPath} exists. Total files: {allFiles.Length}. Files matching GUID '{guidToFind}': {matchingFiles.Count}. " +
                            $"Matching files: {string.Join(", ", matchingFiles.Select(f => f.Name))}. " +
                            $"First 20 files: {string.Join(", ", fileNames)}");
                        
                        // If we found matching files, try using the actual filename from the directory
                        if (matchingFiles.Any())
                        {
                            foreach (var matchingFile in matchingFiles)
                            {
                                var actualPath = $"{dirPath}/{matchingFile.Name}";
                                try
                                {
                                    var testFileInfo = CMS.IO.FileInfo.New(actualPath);
                                    if (testFileInfo != null && testFileInfo.Exists)
                                    {
                                        _eventLog.LogInformation("FormFileApi", "FOUND-MATCHING-FILE", 
                                            $"Found matching file: {actualPath}, Size: {testFileInfo.Length} bytes");
                                        // Use this file
                                        fileInfo = testFileInfo;
                                        actualFilePath = actualPath;
                                        break;
                                    }
                                }
                                catch (Exception ex)
                                {
                                    _eventLog.LogException("FormFileApi", "TEST-FILE-ACCESS-ERROR", ex,
                                        additionalMessage: $"Path: {actualPath}");
                                }
                            }
                            
                            if (fileInfo != null && fileInfo.Exists && !string.IsNullOrEmpty(actualFilePath))
                            {
                                break; // Found the file, exit the directory loop
                            }
                        }
                    }
                }
                
                if (fileInfo == null || !fileInfo.Exists || string.IsNullOrEmpty(actualFilePath))
                {
                    _eventLog.LogWarning("FormFileApi", "DIRECTORY-NOT-FOUND-OR-EMPTY", 
                        "BizFormFiles directory does not exist or is empty in both cases");
                }
            }
            catch (Exception ex)
            {
                _eventLog.LogException("FormFileApi", "DIRECTORY-LISTING-ERROR", ex);
            }
            
            _eventLog.LogWarning("FormFileApi", "FILE-NOT-FOUND-STORAGE", 
                $"File not found in storage after trying {pathVariations.Count} variations. OriginalFileName={fileResult.OriginalFileName}, SystemFileName={systemFileName}, Tried paths: {string.Join(", ", pathVariations)}");
            return NotFound(new { error = "File not found on server" });
        }
        
        _eventLog.LogInformation("FormFileApi", "FILE-FOUND", 
            $"File found in storage: VirtualPath={actualFilePath}, Size={fileInfo.Length} bytes");

        // Get file content type from original filename
        var contentType = GetContentType(fileResult.OriginalFileName);
        
        // Read file from Azure Storage using CMS.IO (works with mapped storage providers)
        byte[] fileBytes;
        using (var fileStream = IOFile.OpenRead(actualFilePath))
        {
            fileBytes = new byte[fileStream.Length];
            fileStream.Read(fileBytes, 0, (int)fileStream.Length);
        }
        
        // Use original filename for download
        return File(fileBytes, contentType, fileResult.OriginalFileName);
    }
    catch (Exception ex)
    {
        _eventLog.LogException("FormFileApi", "GET-FILE-ERROR", ex, 
            additionalMessage: $"Filename: {filename}");
        return StatusCode(500, new { error = "An error occurred while retrieving the file", details = ex.Message });
    }
}
2025/11/14 7:27 AM
Accepted answer

Hey, I’ve implemented and successfully tested a controller that serves files uploaded through forms.
Just a small disclaimer: before reading your question, I had only limited knowledge of this topic. With the help of agentic coding and the Kentico Docs MCP server, I was able to find a working solution within about an hour. I mention this mainly to highlight that this approach, which is also recommended by Kentico, can be very effective.

I would kindly recommend exploring agentic coding together with the Kentico Docs MCP server. Now to the answer:

Yes, ~/assets/bizformfiles/ is the correct path for files uploaded through forms. Files are stored there and mapped to Azure Blob Storage when configured.

Use CMS.IO APIs to access files. CMS.IO abstracts storage and works with Azure Blob Storage, local file system, and other providers.

Here's a controller that retrieves form files: https://gist.github.com/MilanLund/5667dea40e8d7665e7434c5c6c032cd7

Key Points

  • Use CMS.IO APIs:
    CMS.IO.FileInfo.New() and CMS.IO.File.ReadAllBytes() work with Azure Storage when ~/assets/ is mapped.

  • Path format:
    ~/assets/bizformfiles/{SystemFileName} where SystemFileName is the GUID-based filename from BizFormUploadFile.

  • Storage mapping:
    Ensure StorageInitializationModule maps ~/assets/ to Azure Blob Storage in production.

  • File retrieval:
    Use BizFormUploadFileSerializer to deserialize file data from form submissions, then use the SystemFileName to locate the file.

Usage

Access files via:
GET /api/formfileapi/get?filename=your-file.pdf

The controller:

  • Searches all form submissions for the file

  • Resolves the path using CMS.IO (Azure Storage compatible)

  • Returns the file with the proper content type

2025/11/14 9:00 AM

Thank you for this — I appreciate it. I will look into it and get back to you to let you know how it goes.

I am also curious: how did you get the MCP server to work? I tried using it in Cursor, but I’m getting an error.
If you are not familiar with this issue, feel free to ignore the question. Thank you.

2025/11/14 9:42 AM

I tried the code you have shared but I'm still getting File not found.

This line of code always return false. I also confirmed that the file exist on Azure storage. Do you know if I have missed something here?

 var fileInfo = FileInfo.New(virtualPath);
 if (fileInfo != null && fileInfo.Exists)
 {
     return new FilePathResult { FilePath = virtualPath, IsPhysicalPath = false };
 }


This is how I setup StorageInitializationModule.cs

2025/11/14 10:22 AM

Regarding the MCP server, create a .cursor folder in your project root and place the mcp.json file inside it with the following content:

{
  "mcpServers": {
    "kentico.docs.mcp": {
      "url": "https://docs.kentico.com/mcp"
    }
  }
}

That should do the job.

For Cursor, a reasonable alternative might be using Docs indexing:

Regarding your issue, it’s a bit difficult for me to assess the situation, as your setup may differ from mine. My initial answer was intended to demonstrate the agentic coding tools that can be helpful, based on results that worked in my isolated testing. I also have limited availability to provide detailed assistance, so I recommend exploring the problem using the available modern tools and resources.

2025/11/17 5:43 PM

Thank you for the guidance about the MCP, it is working now and it's cool.

About accessing the file in Azure Storage issue using CMS.IO still no luck. I was wondering if my StorageInitializationModule.cs does not configured correctly though I have followed the project template for cloud and sure it would work but it's not. I have used AI and MCP to access the docs but still all suggestions still did not work.

Here is my code for StorageInitializationModule

protected override void OnInit()
{
    base.OnInit();

    bool isSaasEnabled = bool.TryParse(Configuration["IsSaasEnabled"], out var result) ? result : true;


    if (isSaasEnabled)
    {
        if (Environment.IsQa() ||
        Environment.IsEnvironment(CloudEnvironments.Custom) ||
        Environment.IsEnvironment(CloudEnvironments.Staging) ||
        Environment.IsProduction())
        {
            MapAzureStoragePath("/assets/", true);
            ExcludeAzureStoragePath("/assets/synchronizations/");
        }

    }
    else
    {
        MapAzureStoragePath("/assets/", true);
    }
}


private void MapAzureStoragePath(string path , bool PublicExternalFolderObject)
{
    var provider = AzureStorageProvider.Create();

    bool isSaasEnabled = bool.TryParse(Configuration["IsSaasEnabled"], out var result) ? result : true;
    string containerName;
    
    if (isSaasEnabled && 
        (Environment.IsQa() ||
         Environment.IsEnvironment(CloudEnvironments.Custom) ||
         Environment.IsEnvironment(CloudEnvironments.Staging) ||
         Environment.IsProduction()))
    {
        // SaaS environment: Use "default" container (Kentico-managed)
        containerName = CONTAINER_NAME;
    }
    else
    {
        // Self-managed Azure: Use configured container name
        containerName = Configuration["AzureStorage:ContainerName"] ?? CONTAINER_NAME;
    }

    provider.CustomRootPath = containerName;
    provider.PublicExternalFolderObject = PublicExternalFolderObject;

    // Log the mapping for debugging
    var logger = Service.Resolve<Microsoft.Extensions.Logging.ILogger<StorageInitializationModule>>();
    logger.LogInformation("Mapping storage path: {Path} to Azure Storage container: {ContainerName}, Public: {IsPublic}, IsSaaS: {IsSaaS}", 
        path, containerName, PublicExternalFolderObject, isSaasEnabled);

    StorageHelper.MapStoragePath(path, provider);
}

To response this discussion, you have to login first.