Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KendoUI.FileManager.BlobStorage", "KendoUI.FileManager.BlobStorage\KendoUI.FileManager.BlobStorage.csproj", "{8C002550-8D5E-43CE-8F1F-A3A7D5149F48}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8C002550-8D5E-43CE-8F1F-A3A7D5149F48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8C002550-8D5E-43CE-8F1F-A3A7D5149F48}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8C002550-8D5E-43CE-8F1F-A3A7D5149F48}.Debug|x64.ActiveCfg = Debug|Any CPU
{8C002550-8D5E-43CE-8F1F-A3A7D5149F48}.Debug|x64.Build.0 = Debug|Any CPU
{8C002550-8D5E-43CE-8F1F-A3A7D5149F48}.Debug|x86.ActiveCfg = Debug|Any CPU
{8C002550-8D5E-43CE-8F1F-A3A7D5149F48}.Debug|x86.Build.0 = Debug|Any CPU
{8C002550-8D5E-43CE-8F1F-A3A7D5149F48}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8C002550-8D5E-43CE-8F1F-A3A7D5149F48}.Release|Any CPU.Build.0 = Release|Any CPU
{8C002550-8D5E-43CE-8F1F-A3A7D5149F48}.Release|x64.ActiveCfg = Release|Any CPU
{8C002550-8D5E-43CE-8F1F-A3A7D5149F48}.Release|x64.Build.0 = Release|Any CPU
{8C002550-8D5E-43CE-8F1F-A3A7D5149F48}.Release|x86.ActiveCfg = Release|Any CPU
{8C002550-8D5E-43CE-8F1F-A3A7D5149F48}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
using KendoUI.FileManager.BlobStorage.Models;
using KendoUI.FileManager.BlobStorage.Helpers;
using Microsoft.AspNetCore.Mvc;
using KendoUI.FileManager.BlobStorage.Services;
using System.Diagnostics;

namespace KendoUI.FileManager.BlobStorage.Controllers
{
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
// Inject the service that handles Azure Blob Storage operations
private readonly IBlobFileManagerService _fileManagerService;

public HomeController(ILogger<HomeController> logger, IBlobFileManagerService fileManagerService)
{
_logger = logger;
_fileManagerService = fileManagerService;
}

public IActionResult Index()
{
return View();
}

public IActionResult Alternative()
{
return View("Index_Alternative");
}

public IActionResult About()
{
ViewData["Message"] = "Your application description page.";
return View();
}

public IActionResult Contact()
{
ViewData["Message"] = "Your contact page.";
return View();
}

public IActionResult Error()
{
return View();
}

// Handles reading files and folders from Azure Blob Storage for the FileManager
[HttpPost]
public async Task<IActionResult> FileManager_Read([FromForm] string target)
{
try
{
// Retrieve the list of blobs/folders from the specified path
var files = await _fileManagerService.ReadAsync(target);
return Json(files);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to read FileManager contents.");
return BadRequest(new { error = ex.Message });
}
}

// Handles creating new folders or copying/pasting files in Azure Blob Storage
[HttpPost]
public async Task<IActionResult> FileManager_Create([FromForm] string target, [FromForm] string name, [FromForm] int entry)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use a class instead of individual parameters for each prop? E.g. [FromForm] FormRequest formRequest

It's a minor nitpick, but I think it can be a bit cleaner.

{
try
{
// Parse the form data to determine if this is a folder creation, file upload, or copy operation
var context = FileManagerCreateContext.FromForm(Request.Form);
var result = await _fileManagerService.CreateAsync(target, name, entry, context);
return Json(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to create entry in FileManager.");
return BadRequest(new { error = ex.Message });
}
}

// Handles renaming files or folders in Azure Blob Storage
[HttpPost]
public async Task<IActionResult> FileManager_Update()
{
try
{
var targetPath = Request.Form["path"];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be slightly better(not sure) if we use the model-binder pattern with [FromForm] FormRequest formRequest instead of relying on Request.Form. We'll have direct access to properties that way, instead of using magic strings.

var newName = Request.Form["name"];

if (string.IsNullOrEmpty(targetPath) || string.IsNullOrEmpty(newName))
{
return BadRequest(new { error = "Path and name are required for rename operation" });
}

// Rename is implemented by copying the blob to a new path and deleting the old one
var result = await _fileManagerService.UpdateAsync(targetPath!, newName!);
return Json(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to rename FileManager entry.");
return BadRequest(new { error = ex.Message });
}
}

// Handles deleting files or folders from Azure Blob Storage
[HttpPost]
public async Task<IActionResult> FileManager_Destroy([FromForm] string models)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the above comment

{
try
{
// Parse the request to extract the path of the item to delete
var targetPath = FileManagerRequestParser.ResolveTargetPath(Request.Form, models);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need an entirely separate helper to just get the target path?

if (string.IsNullOrEmpty(targetPath))
{
var formData = string.Join(", ", Request.Form.Select(kvp => $"{kvp.Key}={kvp.Value}"));
return BadRequest(new { error = "No target path provided for deletion. Received: " + formData });
}

await _fileManagerService.DeleteAsync(targetPath);
return Json(Array.Empty<object>());
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to delete FileManager entry.");
return BadRequest(new { error = ex.Message });
}
}

// Handles file uploads to Azure Blob Storage
[HttpPost]
public async Task<IActionResult> FileManager_Upload([FromForm] string target, IFormFile file)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the above comment

{
try
{
if (file == null || file.Length == 0)
{
return BadRequest(new { error = "No file uploaded" });
}

// Normalize the target path and upload the file to the blob container
var resolvedTarget = NormalizeUploadTarget(target);
var result = await _fileManagerService.UploadAsync(resolvedTarget, file);
return Json(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to upload file.");
return BadRequest(new { error = ex.Message });
}
}

private string NormalizeUploadTarget(string? target)
{
var resolvedTarget = target ??
Request.Form["target"].FirstOrDefault() ??
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment about Request.Form concerns all places where it is currently being used.

Request.Form["path"].FirstOrDefault() ??
string.Empty;

return string.IsNullOrEmpty(resolvedTarget)
? string.Empty
: resolvedTarget.Trim('/');
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.Text.Json;
using Microsoft.AspNetCore.Http;

namespace KendoUI.FileManager.BlobStorage.Helpers
{
public static class FileManagerRequestParser
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to the previous comments, but we may not need the parser at all.

{
public static string? ResolveTargetPath(IFormCollection form, string? models)
{
if (form is null)
{
throw new ArgumentNullException(nameof(form));
}

var targetPath = form["path"].FirstOrDefault() ??
form["target"].FirstOrDefault() ??
form["Name"].FirstOrDefault() ??
form["name"].FirstOrDefault();

if (string.IsNullOrWhiteSpace(models))
{
return targetPath;
}

try
{
var modelsArray = JsonSerializer.Deserialize<JsonElement[]>(models);
if (modelsArray is not { Length: > 0 })
{
return targetPath;
}

var firstModel = modelsArray[0];
if (firstModel.TryGetProperty("path", out var pathElement))
{
targetPath = pathElement.GetString();
}
}
catch
{
// Intentionally swallow JSON parsing errors and fall back to form values
}

return targetPath;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.Storage.Blobs" Version="12.26.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="10.0.0" />
<PackageReference Include="Telerik.UI.for.AspNet.Core" Version="2025.4.1111" />
</ItemGroup>

<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DefineConstants>$(DefineConstants);RELEASE</DefineConstants>
</PropertyGroup>

<ItemGroup>
<Compile Remove="Templates\**" />
<Content Remove="Templates\**" />
<EmbeddedResource Remove="Templates\**" />
<None Remove="Templates\**" />
</ItemGroup>

<ProjectExtensions>
<VisualStudio>
<UserProperties UseCdnSupport="True" />
</VisualStudio>
</ProjectExtensions>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Microsoft.AspNetCore.Http;

namespace KendoUI.FileManager.BlobStorage.Models
{
public sealed class FileManagerCreateContext
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we double-check if this class is really needed or if we can simplify?

{
public IFormFile? UploadedFile { get; init; }
public string? SourcePath { get; init; }
public string? Extension { get; init; }
public string? IsDirectoryFlag { get; init; }

public static FileManagerCreateContext FromForm(IFormCollection form)
{
if (form is null)
{
throw new ArgumentNullException(nameof(form));
}

return new FileManagerCreateContext
{
UploadedFile = form.Files.FirstOrDefault(),
SourcePath = form["path"].FirstOrDefault() ??
form["source"].FirstOrDefault() ??
form["sourcePath"].FirstOrDefault(),
Extension = form["extension"].FirstOrDefault(),
IsDirectoryFlag = form["isDirectory"].FirstOrDefault()
};
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Text.Json.Serialization;

namespace KendoUI.FileManager.BlobStorage.Models
{
public sealed class FileManagerEntry
{
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;

[JsonPropertyName("isDirectory")]
public bool IsDirectory { get; init; }

[JsonPropertyName("hasDirectories")]
public bool HasDirectories { get; init; }

[JsonPropertyName("path")]
public string Path { get; init; } = string.Empty;

[JsonPropertyName("extension")]
public string Extension { get; init; } = string.Empty;

[JsonPropertyName("size")]
public long Size { get; init; }

[JsonPropertyName("created")]
public DateTime Created { get; init; }

[JsonPropertyName("createdUtc")]
public DateTime CreatedUtc { get; init; }

[JsonPropertyName("modified")]
public DateTime Modified { get; init; }

[JsonPropertyName("modifiedUtc")]
public DateTime ModifiedUtc { get; init; }

[JsonPropertyName("dateCreated")]
public DateTime DateCreated { get; init; }

[JsonPropertyName("dateModified")]
public DateTime DateModified { get; init; }
}
}
Loading