using System; using System.Threading.Tasks; using System.IO; using System.IO.Compression; using System.Collections.Generic; using Microsoft.EntityFrameworkCore; using Newtonsoft.Json.Linq; using AyaNova.Models; using AyaNova.Biz; using System.Linq; namespace AyaNova.Util { /* - Quickly generate large files in windows: http://tweaks.com/windows/62755/quickly-generate-large-test-files-in-windows/ */ internal static class FileUtil { #region Folder ensurance /// /// Ensurs folders exist and are not identical /// Throws an exception of they are found to be identical preventing startup /// The reason for this is to prevent a future erase database operation (which erases all attachment files) /// from erasing backups which might prevent recovery in case someone accidentally erases their database /// /// /// internal static void EnsureUserAndUtilityFoldersExistAndAreNotIdentical(string contentRootPath) { //UserFiles if (string.IsNullOrWhiteSpace(ServerBootConfig.AYANOVA_FOLDER_USER_FILES)) { ServerBootConfig.AYANOVA_FOLDER_USER_FILES = Path.Combine(contentRootPath, "userfiles"); } //BackupFiles if (ServerBootConfig.AYANOVA_FOLDER_BACKUP_FILES == null) { ServerBootConfig.AYANOVA_FOLDER_BACKUP_FILES = Path.Combine(contentRootPath, "backupfiles"); } //Prevent using the same folder for both if (string.Equals(Path.GetFullPath(ServerBootConfig.AYANOVA_FOLDER_USER_FILES), Path.GetFullPath(ServerBootConfig.AYANOVA_FOLDER_BACKUP_FILES), StringComparison.OrdinalIgnoreCase)) { throw new System.NotSupportedException("E1040: The configuration settings AYANOVA_FOLDER_USER_FILES and the AYANOVA_FOLDER_BACKUP_FILES must not point to the exact same location"); } EnsurePath(ServerBootConfig.AYANOVA_FOLDER_USER_FILES); EnsurePath(ServerBootConfig.AYANOVA_FOLDER_BACKUP_FILES); } //create path if doesn't exist already private static void EnsurePath(string path) { if (!Directory.Exists(path)) Directory.CreateDirectory(path); } #endregion folder ensurance #region Utility file handling /// /// Get a path combining supplied file name and backup files folder /// /// internal static string GetFullPathForUtilityFile(string fileName) { return Path.Combine(UtilityFilesFolder, fileName); } /// /// Get backup folder /// /// internal static string UtilityFilesFolder { get { return ServerBootConfig.AYANOVA_FOLDER_BACKUP_FILES; } } /// /// Delete a utility file (backup folder file) /// /// internal static void DeleteUtilityFile(string fileName) { var utilityFilePath = GetFullPathForUtilityFile(fileName); if (File.Exists(utilityFilePath)) { File.Delete(utilityFilePath); } } /// /// Get a list of files in the utility folder /// /// /// search pattern for files desired, leave blank for any /// internal static List UtilityFileList(string searchPattern = "*") { List returnList = new List(); foreach (string file in Directory.EnumerateFiles(UtilityFilesFolder, searchPattern)) { returnList.Add(Path.GetFileName(file)); } returnList.Sort(); return returnList; } /// /// Confirm if a file exists in the utility folder /// /// name of utility file /// duh! internal static bool UtilityFileExists(string fileName) { if (string.IsNullOrWhiteSpace(fileName)) return false; var utilityFilePath = GetFullPathForUtilityFile(fileName); return File.Exists(utilityFilePath); } #endregion Utility file handling #region Zip handling //////////////////////////////////////////////////////////////////////////////////////// //ZIP handling /// /// Get zip entries for a utlity file /// /// /// internal static List ZipGetUtilityFileEntries(string zipFileName) { return ZipGetEntries(GetFullPathForUtilityFile(zipFileName)); } /// /// Get zip entries for full path and file name /// returns the entry fullname sorted alphabetically so that folders stay together /// /// /// internal static List ZipGetEntries(string zipPath) { List zipEntries = new List(); using (ZipArchive archive = ZipFile.OpenRead(zipPath)) { foreach (ZipArchiveEntry entry in archive.Entries) { zipEntries.Add(entry.FullName); } } zipEntries.Sort(); return zipEntries; } /// /// Import utility - get individual files specified in zip archive as JSON objects /// /// /// Name of utility zip import file /// Name of entries in utility file archive to fetch /// internal static List ZipGetUtilityArchiveEntriesAsJsonObjects(List entryList, string zipFileName) { List jList = new List(); var zipPath = GetFullPathForUtilityFile(zipFileName); using (ZipArchive archive = ZipFile.OpenRead(zipPath)) { foreach (string importFileName in entryList) { ZipArchiveEntry entry = archive.GetEntry(importFileName); if (entry != null) { //stream entry into a new jobject and add it to the list StreamReader reader = new StreamReader(entry.Open()); string text = reader.ReadToEnd(); var j = JObject.Parse(text); //Here add v7 import file name as sometimes it's needed later (Translations) j.Add("V7_SOURCE_FILE_NAME", JToken.FromObject(importFileName)); jList.Add(j); } } } return jList; } #endregion Zip handling #region Attachment file handling /// /// Get user folder /// /// internal static string UserFilesFolder { get { return ServerBootConfig.AYANOVA_FOLDER_USER_FILES; } } /// /// Get a random file name /// /// internal static string NewRandomFileName { get { return Path.GetRandomFileName(); } } /// /// Get a random file name with path to attachments folder /// /// internal static string NewRandomAttachmentFileName { get { return Path.Combine(UserFilesFolder, NewRandomFileName); } } /// /// Store a file attachment /// /// internal static async Task StoreFileAttachmentAsync(string tempFilePath, string contentType, string fileName, DateTime lastModified, AyaTypeId attachToObject, string notes, AyContext ct) { //calculate hash var hash = FileHash.GetChecksum(tempFilePath); //Move to folder based on hash var permanentPath = GetPermanentAttachmentPath(hash); EnsurePath(permanentPath); var permanentFilePath = Path.Combine(permanentPath, hash); //See if the file was already uploaded, if so then ignore it for now if (File.Exists(permanentFilePath)) { //delete the temp file, it's already stored File.Delete(tempFilePath); } else { System.IO.File.Move(tempFilePath, permanentFilePath); } //Build AyFileInfo FileAttachment fi = new FileAttachment() { StoredFileName = hash, DisplayFileName = fileName, Notes = notes, ContentType = contentType, AttachToObjectId = attachToObject.ObjectId, AttachToObjectType = attachToObject.ObjectType, LastModified = lastModified }; //Store in DB await ct.FileAttachment.AddAsync(fi); await ct.SaveChangesAsync(); //Return AyFileInfo object return fi; } /// ///use first three characters for name of folders one character per folder, i.e.: ///if the checksum is f6a5b1236dbba1647257cc4646308326 ///it would be stored in userfiles/f/6/a/f6a5b1236dbba1647257cc4646308326 /// /// /// Path without the file internal static string GetPermanentAttachmentPath(string hash) { return Path.Combine(UserFilesFolder, hash[0].ToString(), hash[1].ToString(), hash[2].ToString()); } /// /// Get the whole path including file name not just the folder /// /// /// internal static string GetPermanentAttachmentFilePath(string hash) { return Path.Combine(UserFilesFolder, hash[0].ToString(), hash[1].ToString(), hash[2].ToString(), hash); } /// /// Delete a file attachment /// checks ref count and if would be zero deletes file physically /// otherwise just deletes pointer in db /// /// /// /// internal static async Task DeleteFileAttachmentAsync(FileAttachment fileAttachmentToBeDeleted, AyContext ct) { //check ref count of file var count = await ct.FileAttachment.LongCountAsync(w => w.StoredFileName == fileAttachmentToBeDeleted.StoredFileName); //Store in DB ct.FileAttachment.Remove(fileAttachmentToBeDeleted); await ct.SaveChangesAsync(); if (count < 2) { //remove the file completely var permanentPath = GetPermanentAttachmentPath(fileAttachmentToBeDeleted.StoredFileName); var permanentFilePath = Path.Combine(permanentPath, fileAttachmentToBeDeleted.StoredFileName); if (File.Exists(permanentFilePath)) { //delete the temp file, it's already stored File.Delete(permanentFilePath); } } //Return AyFileInfo object return fileAttachmentToBeDeleted; } /// /// DANGER: Erases all user files /// internal static void EraseEntireContentsOfUserFilesFolder() { System.IO.DirectoryInfo di = new DirectoryInfo(UserFilesFolder); foreach (FileInfo file in di.EnumerateFiles()) { file.Delete(); } foreach (DirectoryInfo dir in di.EnumerateDirectories()) { dir.Delete(true); } } #endregion attachment stuff #region General utilities /// /// Attachments / user files folder size info /// /// internal static FolderSizeInfo GetAttachmentFolderSizeInfo() { return GetDirectorySize(new DirectoryInfo(UserFilesFolder)); } /// /// Utility / backup folder file size info /// /// internal static FolderSizeInfo GetUtilityFolderSizeInfo() { return GetDirectorySize(new DirectoryInfo(UtilityFilesFolder)); } /// /// Calculate disk space usage under . If is provided, /// then return subdirectory disk usages as well, up to levels deep. /// If levels is not provided or is 0, return a list with a single element representing the /// directory specified by . /// /// FROM https://stackoverflow.com/a/28094795/8939 /// /// /// public static FolderSizeInfo GetDirectorySize(DirectoryInfo root, int levels = 0) { var currentDirectory = new FolderSizeInfo(); // Add file sizes. FileInfo[] fis = root.GetFiles(); currentDirectory.Size = 0; foreach (FileInfo fi in fis) { currentDirectory.Size += fi.Length; } // Add subdirectory sizes. DirectoryInfo[] dis = root.GetDirectories(); currentDirectory.Path = root; currentDirectory.SizeWithChildren = currentDirectory.Size; currentDirectory.DirectoryCount = dis.Length; currentDirectory.DirectoryCountWithChildren = dis.Length; currentDirectory.FileCount = fis.Length; currentDirectory.FileCountWithChildren = fis.Length; if (levels >= 0) currentDirectory.Children = new List(); foreach (DirectoryInfo di in dis) { var dd = GetDirectorySize(di, levels - 1); if (levels >= 0) currentDirectory.Children.Add(dd); currentDirectory.SizeWithChildren += dd.SizeWithChildren; currentDirectory.DirectoryCountWithChildren += dd.DirectoryCountWithChildren; currentDirectory.FileCountWithChildren += dd.FileCountWithChildren; } return currentDirectory; } public class FolderSizeInfo { public DirectoryInfo Path { get; set; } public long SizeWithChildren { get; set; } public long Size { get; set; } public int DirectoryCount { get; set; } public int DirectoryCountWithChildren { get; set; } public int FileCount { get; set; } public int FileCountWithChildren { get; set; } public List Children { get; set; } } #endregion general utilities }//eoc }//eons