-
Notifications
You must be signed in to change notification settings - Fork 58
chore: add file manager blob storage example #105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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) | ||
| { | ||
| 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"]; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| 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) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() ?? | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The comment about |
||
| 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; } | ||
| } | ||
| } |
There was a problem hiding this comment.
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 formRequestIt's a minor nitpick, but I think it can be a bit cleaner.