Skip to content
Merged
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,218 @@
using KendoUI.FileManager.BlobStorage.Models;
using Microsoft.AspNetCore.Mvc;
using KendoUI.FileManager.BlobStorage.Services;
using System.Diagnostics;
using System.Text.Json;

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] FileManagerReadRequest request)
{
try
{
// Retrieve the list of blobs/folders from the specified path
var target = NormalizePath(request.Target ?? request.Path ?? string.Empty);
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] FileManagerCreateRequest request)
{
try
{
var target = NormalizePath(request.Target ??
request.Path ??
request.Source ??
request.SourcePath ??
string.Empty);
var name = request.Name?.Trim();
var entry = request.Entry;

if (string.IsNullOrWhiteSpace(name))
{
return BadRequest(new { error = "Name is required for create operation" });
}

// Parse the form data to determine if this is a folder creation, file upload, or copy operation
var context = FileManagerCreateContext.FromRequest(request);
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([FromForm] FileManagerUpdateRequest request)
{
try
{
var targetPath = request.Path;
var newName = request.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] FileManagerDestroyRequest request)
{
try
{
// Parse the request to extract the path of the item to delete
var targetPath = ResolveTargetPath(request);
if (string.IsNullOrEmpty(targetPath))
{
return BadRequest(new { error = "No target path provided for deletion." });
}

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] FileManagerUploadRequest request)
{
try
{
if (request.File == null || request.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(request);
var result = await _fileManagerService.UploadAsync(resolvedTarget, request.File);
return Json(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to upload file.");
return BadRequest(new { error = ex.Message });
}
}

private static string NormalizePath(string? target)
{
if (string.IsNullOrWhiteSpace(target))
{
return string.Empty;
}

return target.Trim('/');
}

private static string NormalizeUploadTarget(FileManagerUploadRequest request)
{
var resolvedTarget = request.Target ?? request.Path ?? string.Empty;
return NormalizePath(resolvedTarget);
}

private static string? ResolveTargetPath(FileManagerDestroyRequest request)
{
var targetPath = request.Path ?? request.Target ?? request.Name;

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

if (string.IsNullOrWhiteSpace(request.Models))
{
return null;
}

try
{
var modelsArray = JsonSerializer.Deserialize<JsonElement[]>(request.Models);
if (modelsArray is { Length: > 0 })
{
var firstModel = modelsArray[0];
if (firstModel.TryGetProperty("path", out var pathElement))
{
return pathElement.GetString();
}
}
}
catch
{
// Swallow JSON parsing errors and fall back to form values
}

return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using System.IO;

namespace KendoUI.FileManager.BlobStorage.Helpers
{
internal static class BlobPathHelper
{
public static string BuildPrefix(string? target)
{
return string.IsNullOrWhiteSpace(target)
? string.Empty
: target.TrimEnd('/') + "/";
}

public static string CombinePath(string? target, string name)
{
var prefix = BuildPrefix(target);
return string.IsNullOrEmpty(prefix) ? name : prefix + name;
}

public static string EnsureTrailingSlash(string? path)
{
if (string.IsNullOrEmpty(path))
{
return path ?? string.Empty;
}

return path.EndsWith('/') ? path : path + "/";
}

public static bool ShouldTreatAsDirectory(string name, string? isDirectoryFlag, int entry)
{
if (!string.IsNullOrEmpty(isDirectoryFlag) && bool.TryParse(isDirectoryFlag, out var isDirectory) && isDirectory)
{
return true;
}

if (entry == 1)
{
return true;
}

return !HasExtension(name);
}

public static string BuildNewPath(string targetPath, string newName, bool isDirectory)
{
var lastSlashIndex = targetPath.LastIndexOf('/');

if (isDirectory)
{
return lastSlashIndex >= 0
? targetPath.Substring(0, lastSlashIndex + 1) + newName
: newName;
}

var originalExtension = Path.GetExtension(targetPath);
var newExtension = Path.GetExtension(newName);

if (string.IsNullOrEmpty(newExtension) && !string.IsNullOrEmpty(originalExtension))
{
newName += originalExtension;
}

return lastSlashIndex >= 0
? targetPath.Substring(0, lastSlashIndex + 1) + newName
: newName;
}

public static bool HasExtension(string? name)
{
return !string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(Path.GetExtension(name));
}
}
}
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,31 @@
using Microsoft.AspNetCore.Http;
using System.Linq;

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 FromRequest(FileManagerCreateRequest request)
{
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}

return new FileManagerCreateContext
{
UploadedFile = request.Files?.FirstOrDefault(),
SourcePath = request.Path ??
request.Source ??
request.SourcePath,
Extension = request.Extension,
IsDirectoryFlag = request.IsDirectoryFlag
};
}
}
}
Loading