Работа с дисковым пространством и файлами

от автора

Однажды 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *