Однажды HR предложили мне в качестве тестового задания сделать проводник на вебе. Примерное описание ТЗ содержится в заголовке. Задание меня заинтересовало.

Непродолжительный поиск в гугле ничего похожего не дал. Тем интереснее.
Для работы с файлами нам достаточно стандартных классов:
-
(System.IO) DriveInfo- Предоставляет информацию о дисках. Так мы узнаем какие диски подключены, их имена, емкость и свободное пространство
-
(System.IO) DirectoryInfo — Предоставляет информацию о папках. получение информации о вложенных папках и файлах
-
(System.IO) FileInfo — Предоставляет информацию о файлах. получение размера
Первое, с чего предлагаю начать – создадим модельку которая смотрит подключенные диски, чтобы смотреть по каким путям нам вообще ходить, показать какие диски есть и отобразить их размеры:
Для этого в папке Models создал папку FilesModels и в ней DisksModel.cs и вставляем код:
public class DisksModel { public decimal TotalFreeSpace { get; } public decimal TotalSize { get; } public List<DriveInfo> Disks { get; } public DisksModel() { Disks = DriveInfo.GetDrives().Where(r => r.IsReady).Where(r => r.DriveType == DriveType.Fixed).ToList(); TotalSize = Disks.Sum(r => r.TotalSize); TotalFreeSpace = Disks.Sum(r => r.TotalFreeSpace); } }
В конструкторе мы получаем информацию по имеющимся дискам, выбираем только готовые к работе и не берем флэшки.
Также дополнительно, чтобы возвращать размерность в нужных единицах измерения заодно вставил статичный класс.
public static class DiskMetods { public static decimal ToKB(this decimal size, int decimals = 2) { return Math.Round(size / 1024, decimals); } public static decimal ToMB(this decimal size, int decimals = 2) { return Math.Round(size.ToKB(0) / 1024, decimals); } public static decimal ToGB(this decimal size, int decimals = 2) { return Math.Round(size.ToMB(0) / 1024, decimals); } public static decimal ToTB(this decimal size, int decimals = 2) { return Math.Round(size.ToGB(0) / 1024, decimals); } public static decimal ToKB(this long size, int decimals = 2) { return Math.Round((decimal)size / 1024, decimals); } public static decimal ToMB(this long size, int decimals = 2) { return Math.Round(size.ToKB(0) / 1024, decimals); } public static decimal ToGB(this long size, int decimals = 2) { return Math.Round(size.ToMB(0) / 1024, decimals); } public static decimal ToTB(this long size, int decimals = 2) { return Math.Round(size.ToGB(0) / 1024, decimals); } }
Создаем контролер для работы с файлами в папке FilesControllers создаем FilesController.cs.
Указываем какую страницу открывать и в нем же статично получаем информацию о дисках для дальнейшего использования.
public static DisksModel disk = new DisksModel(); [HttpGet] public IActionResult Files() { return View("~/Views/FilesViews/Files.cshtml"); }
Далее в папке Views создаем папку FilesViews и страницу Files.cshtml.
И создаём там таблицы, в которых будут отображаться информация о дисках на компьютере заодно в прогресс баре указывается сколько места заполнено. И как раз используется информация из статического класса в контроллере и переводим в гигабайты за счет ранее написанных методов.
<div id="diskInfo"> <div id="tableDisk"> <table onclick="VieUnvie()" style="cursor: pointer"> <tr> <td> <img src="~/img/hd_disk_harddisk_162.png" style="height:45px;width:45px" /> </td> <td> <table> <tr> <td> Все пространство дисков </td> </tr> <tr> <td> <progress value="@(WebFileManager.Controllers.FilesController.disk.TotalSize.ToGB(0) - WebFileManager.Controllers.FilesController.disk.TotalFreeSpace.ToGB(0))" max="@WebFileManager.Controllers.FilesController.disk.TotalSize.ToGB(0)"></progress> </td> </tr> <tr> <td> @WebFileManager.Controllers.FilesController.disk.TotalFreeSpace.ToGB() ГБ свободно из @WebFileManager.Controllers.FilesController.disk.TotalSize.ToGB() ГБ </td> </tr> </table> </td> </tr> </table> <table id="allDisk" style="display:none; cursor: pointer"> @{foreach (DriveInfo driveInfo in WebFileManager.Controllers.FilesController.disk.Disks) { <tr class="DiskRow" onclick="getFolder(null,'@(driveInfo.Name+"\\")', null, false)"> <td width="45px"> </td> <td> <img src="~/img/hd_disk_harddisk_162.png" style="height:45px;width:45px" /> </td> <td> <table> <tr> <td> локальный диск (@driveInfo.Name.Replace("\\", "")) </td> </tr> <tr> <td> <progress value="@(driveInfo.TotalSize.ToGB(0) - driveInfo.TotalFreeSpace.ToGB(0))" max="@driveInfo.TotalSize.ToGB(0)"></progress> </td> </tr> <tr> <td> @driveInfo.TotalFreeSpace.ToGB() ГБ свободно из @driveInfo.TotalSize.ToGB() ГБ </td> </tr> </table> </td> </tr> } } </table> </div> </div>
Добавим скрипт украшательства ради для сворачивания/разворачивания дисков в/из общего объема, а onclick=»getFolder(null,’@(driveInfo.Name+»\» встретится чуть позже.
function VieUnvie() { var table = document.getElementById("allDisk"); if (table.style.display == "none") { table.style.display = "block"; } else { table.style.display = "none"; } }
Прописываем еще одну кнопку в _Layout.cshtml
<li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Files" asp-action="Files">Проводник</a> </li>
Получим:

Теперь необходимо отобразить размеры файлов и папок. В этом есть проблема, т.к. в DirectoryInfo нет информации о размере, и, как я понял, тот же проводник windows просто каждый раз считает размеры файлов в папке, дальше идет по вложенным папкам и потом возвращает конечную сумму файлов (например, когда вы нажимаете свойство папки, какое-то время идет подсчет размера на ваших глазах и время подсчета зависит в первую очередь от количества файлов внутри). Так же поступим и мы.
Понадобится модель для хранения информации. Предлагаю такую:
public class FolderModel { public DirectoryInfo ThisDirectoryInfo { get; set; } public FileInfo[] Files { get; } public List<FolderModel> Folders { get; } public decimal Size { get; } = 0; public FolderModel() { } public FolderModel(string path):this(new DirectoryInfo(path)) { } public FolderModel(DirectoryInfo directoryInfo) { ThisDirectoryInfo = directoryInfo; Files = directoryInfo.GetFiles(); Folders = ThisDirectoryInfo.GetDirectories().Where(r => !r.Attributes.HasFlag(FileAttributes.System) & !r.Attributes.HasFlag(FileAttributes.Hidden)).ToArray().GetFolderModels(); Size += Files.Sum(r => r.Length) + Folders.Sum(r => r.Size); } public static List<FolderModel> GetFolderModels() { List<FolderModel> folderModels = new List<FolderModel>(); foreach (var el in WebFileManager.Controllers.FilesController.disk.Disks) { folderModels.Add(new FolderModel(el.Name)); } //folderModels.Add(new FolderModel("C:\\")); return folderModels; } }
Мы получили массив файлов в папке с их размерами, рекурсивно ходим по вложенным папкам возвращая их размер (т.е. размер файлов в них), и конечную сумму размеров.
Дополнительные функции, одна из которых понадобится для получения нашей модели по указанному пути:
public static class FolderModelMetods { /// <summary> /// возвращает FolderModel по массиву DirectoryInfo /// </summary> /// <param name="directoryInfos"></param> /// <returns></returns> public static List<FolderModel> GetFolderModels(this DirectoryInfo[] directoryInfos) { List<FolderModel> folderModels = new List<FolderModel>(); foreach(DirectoryInfo directoryInfo in directoryInfos) { try { folderModels.Add(new FolderModel(directoryInfo)); } catch { } } return folderModels; } /// <summary> /// возвращает FolderModel по указанному пути /// </summary> /// <param name="folderModels"> откуда ищет</param> /// <param name="fullPath">путь по которому брать модель</param> /// <returns></returns> public static FolderModel GetCurentFolderModel(this List<FolderModel> folderModels, string fullPath) { string[] pathEls = fullPath.Split('\\').Where(r=>r!="").ToArray(); FolderModel folderModel = folderModels.FirstOrDefault(r => r.ThisDirectoryInfo.Name.Replace("\\", "") == pathEls[0]); for (int i = 1; i < pathEls.Length; i++) { folderModel = folderModel.Folders.FirstOrDefault(r => r.ThisDirectoryInfo.Name.Trim() == pathEls[i].Trim()); } return folderModel; } }
Здесь стоит трай катч. Это был быстрый способ для обхода ошибок связанных с чтением системных закрытых файлов и папок. Теперь вопрос: как это представить и что выводить на экран. Проводник windows обычно выводит имя, дату изменения, тип и размер файлов, значит и мы выведем такое. Первое, что пришло в голову — написать отдельную модель.
public class FolderViewModel { public string CurentPath { get; set; } public string Name { get; set; } public string Type { get; set; } public decimal Size { get; set; } public DateTime ChangeDate { get; set; } }
В этой модели мы как раз реализуем все то что хотим увидеть. Ну и заодно сразу добавим методы в статичном классе.
public static class FolderViewModelM { public static List<FolderViewModel> GetFolderViewModel(this List<FolderModel> folderModels, string path, string folderName, string SortName, string SortType) { List<FolderViewModel> folderViewModels; if(folderName==null) { folderViewModels = folderModels.GetFolderViewModel((path).Replace("\\\\", "\\")); } else { folderViewModels = folderModels.GetFolderViewModel((path + "\\" + folderName).Replace("\\\\", "\\")); } if (SortName != null) { switch (SortName) { case "Name": if (SortType == "Asc") { folderViewModels = folderViewModels.OrderBy(r => r.Name).ToList(); } else { folderViewModels = folderViewModels.OrderByDescending(r => r.Name).ToList(); } break; case "Type": if (SortType == "Asc") { folderViewModels = folderViewModels.OrderBy(r => r.Type).ToList(); } else { folderViewModels = folderViewModels.OrderByDescending(r => r.Type).ToList(); } break; case "Size": if (SortType == "Asc") { folderViewModels = folderViewModels.OrderBy(r => r.Size).ToList(); } else { folderViewModels = folderViewModels.OrderByDescending(r => r.Size).ToList(); } break; case "ChangeDate": if (SortType == "Asc") { folderViewModels = folderViewModels.OrderBy(r => r.ChangeDate).ToList(); } else { folderViewModels = folderViewModels.OrderByDescending(r => r.ChangeDate).ToList(); } break; } } return folderViewModels; } public static List<FolderViewModel> GetFolderViewModel(this List<FolderModel> folderModels, string fullPath) { FolderModel folderModel = folderModels.GetCurentFolderModel(fullPath); return folderModel.ToFolderViewModel(); } public static List<FolderViewModel> ToFolderViewModel(this FolderModel folderModel) { List<FolderViewModel> folderViewModels = new List<FolderViewModel>(); foreach (var folder in folderModel.Folders) { folderViewModels.Add(new FolderViewModel() { Name = folder.ThisDirectoryInfo.Name, ChangeDate = folder.ThisDirectoryInfo.LastWriteTime, Type = "Folder", Size = folder.Size.ToMB(), CurentPath = folderModel.ThisDirectoryInfo.FullName }); } foreach (var file in folderModel.Files) { folderViewModels.Add(new FolderViewModel() { Name = file.Name, ChangeDate = file.LastWriteTime, Type = "File", Size = file.Length.ToMB(), CurentPath = folderModel.ThisDirectoryInfo.FullName }); } return folderViewModels; } public static string ToJson(this List<FolderViewModel> folderViewModels) { return JsonSerializer.Serialize(folderViewModels); } }
Мы реализуем перевод в json для передачи на страницу и несколько методов перевода из предыдущей модели в нашу новую. Также в одном из методов реализована сортировка.
Надо придумать представление. Самым простым будет использование все той же таблицы. Сначала в наше ранее используемое представление (в то, где мы рисовали диски) записываем:
@using WebFileManager.Models.FilesModels; @using System.IO;
Указываем, какую используем модель и дополнительно пространство. Теперь сделаем саму таблицу.
<div id="FileBrowser" style="width:100%; height:auto"> <div id="PanelPath" style="width:100%"> <div id="PanelPathButtons"> <button id="left" onclick="Back()"><img src="~/img/left.png" width="20px" height="10px" /></button> <button id="right" onclick="Next()"><img src="~/img/right.png" width="20px" height="10px" /></button> </div> <div id="PathNow">@WebFileManager.Controllers.FilesController.folderModels[0].ThisDirectoryInfo.FullName</div> </div> <div id="FileManager"> <div id="PathView" class="FileManager" style="width:30%;"> fff </div> <div id="FilesView" class="FileManager" style="width: 70%; "> <table id="FilesAndFolders"> <tr> <th onclick="Sort(this)" style="cursor: pointer"> Name </th> <th onclick="Sort(this)" style="cursor: pointer">Change date</th> <th onclick="Sort(this)" style="cursor: pointer"> Type </th> <th onclick="Sort(this)" style="cursor: pointer"> Size </th> </tr> @{ foreach (var folder in WebFileManager.Controllers.FilesController.folderModels[0].Folders) { <tr class="FileFolderRow"> <td> <img src="~/img/folder.png" /> @folder.ThisDirectoryInfo.Name </td> <td> @folder.ThisDirectoryInfo.LastWriteTime </td> <td> Folder </td> <td> @folder.Size.ToMB() MB </td> </tr> } foreach (var file in WebFileManager.Controllers.FilesController.folderModels[0].Files) { <tr class="FileFolderRow"> <td> <img src="~/img/file.png" />@file.Name </td> <td> File </td> <td> @file.Length.ToMB() MB </td> </tr> } } <tr> <td></td> </tr> </table> </div> </div> </div>
При открытии страницы сначала показываем содержимое первого диска из списка. Также делаем кнопки назад и вперед, место, где указываем путь.
Также функции:
function Back() { getFolder(null, $('#PathNow')[0].innerText, null, true); $("#right").prop("disabled", false) }
Соответственно возврат назад:
function getFolder(FN, PN, Sor, BK) { $.ajax({ url: '@Url.Action("NewFolder", "Files")', data:{ folderName: FN , pathNow: PN , orderBy: Sor , back:BK } , type: 'POST' , success: function (data) { $(".FileFolderRow").remove(); GetFileFolderRow(data); pathNow: $('#PathNow')[0].innerText = data[0]["CurentPath"]; setSes("maxPath", data[0]["CurentPath"]); } }) }
Получение представления по указанному пути — как раз та функция, которая встречалась еще в отображении дисков на странице.
function GetFileFolderRow(data) { for (var i = 0; i < data.length; i++) { let tr = document.createElement("tr"), td0 = document.createElement("td"), td1 = document.createElement("td"), td2 = document.createElement("td"), td3 = document.createElement("td"), img = document.createElement("img"); if (data[i]["Type"] == "File") { img.src = "/img/file.png" } else { img.src = "/img/folder.png" } td0.appendChild(img); td0.appendChild(document.createTextNode(data[i]["Name"])); td1.innerText = data[i]["ChangeDate"]; td2.innerText = data[i]["Type"]; td3.innerText = data[i]["Size"] + " MB"; tr.className = "FileFolderRow"; tr.append(td0); tr.append(td1); tr.append(td2); tr.append(td3); $("#FilesAndFolders").append(tr); } $(dbClickRow()) }
Записываем в таблицу полученную модель.
И функция для нажатия на строку в таблице. Если папка, то пытается пройти дальше, а на файл просто отвечает, что не может его открыть.
function dbClickRow () { $(".FileFolderRow").dblclick(function (e) { var newFolder = this.getElementsByTagName("td")[0].innerText.trim(); if (this.getElementsByTagName("td")[2].innerText == "Folder") { getFolder(newFolder, $('#PathNow')[0].innerText, null, false) } else { alert("i can't open files "); } })
Добавляем совсем короткий скрипт для сортировки, который по сути так же делает запрос на получение страницы и заодно передает по какому полю сортировать.
function Sort(e) { getFolder(null, $('#PathNow')[0].innerText, e.innerText, false); }
Вспомним про метод, возвращающей модель для представления с сортировкой. Он определяет, по какому полю сортировать. Соответственно где-то надо хранить для пользователей их значения. Самым очевидным способом является сессия. Поэтому в startup.cs добавляем services.AddMvc() и app.UseSession(), выглядит примерно так:

Осталось реализовать в контролере как будем возвращать страницы.
В fileController добавляем:
public static List<FolderModel> folderModels = FolderModel.GetFolderModels();
Сам код:
[HttpPost] public ContentResult NewFolder(string folderName, string pathNow, string orderBy=null, bool back=false) { if (orderBy != null) { if (HttpContext.Session.GetString("OrderBy") == orderBy) { if (HttpContext.Session.GetString("SortType") == "Asc") { HttpContext.Session.SetString("SortType", "Desc"); } else { HttpContext.Session.SetString("SortType", "Asc"); } } else { HttpContext.Session.SetString("OrderBy", orderBy); HttpContext.Session.SetString("SortType", "Asc"); } } return Content(folderModels.GetFolderViewModel(!back ? pathNow: pathNow.Substring(0, pathNow.LastIndexOf("\\")), folderName, HttpContext.Session.GetString("OrderBy"), HttpContext.Session.GetString("SortType")).ToJson(), "application/json"); }
Если запрос приходит с указанием сортировки, то пишем в сессию по какому полю, а если такое поле уже указанно, то меняем порядок и отправляем модель.

Надеюсь, кому-то пригодится данный материал, также каждый желающий может аргументированно провести автора лицом по коду.
→ Весь проект лежит тут github
Спасибо за внимание.
Автор статьи: @Sith_Lord
ссылка на оригинал статьи https://habr.com/ru/articles/592681/
Добавить комментарий