diff --git a/src/app/dev/DevToys.Core/FileHelper.cs b/src/app/dev/DevToys.Core/FileHelper.cs index 6fc1375669..b68770b65b 100644 --- a/src/app/dev/DevToys.Core/FileHelper.cs +++ b/src/app/dev/DevToys.Core/FileHelper.cs @@ -1,9 +1,12 @@ using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; namespace DevToys.Core; public static class FileHelper { + private static readonly ILogger logger = typeof(FileHelper).Log(); + private static readonly ConcurrentBag tempFiles = new(); public static FileInfo CreateTempFile(string baseFolder, string? desiredFileExtension) @@ -28,6 +31,8 @@ public static FileInfo CreateTempFile(string baseFolder, string? desiredFileExte public static void ClearTempFiles(string baseFolder) { + DateTime startTime = DateTime.UtcNow; + FileInfo[] files = tempFiles.ToArray(); foreach (FileInfo tempFile in files) { @@ -65,5 +70,8 @@ public static void ClearTempFiles(string baseFolder) } } } + + double elapsedMilliseconds = (DateTime.UtcNow - startTime).TotalMilliseconds; + logger.LogInformation("Cleared temp files in {elapsedMilliseconds}ms", elapsedMilliseconds); } } diff --git a/src/app/dev/platforms/desktop/DevToys.CLI/Core/FileStorage/FileStorage.cs b/src/app/dev/platforms/desktop/DevToys.CLI/Core/FileStorage/FileStorage.cs index 969fac9e42..926d058a26 100644 --- a/src/app/dev/platforms/desktop/DevToys.CLI/Core/FileStorage/FileStorage.cs +++ b/src/app/dev/platforms/desktop/DevToys.CLI/Core/FileStorage/FileStorage.cs @@ -63,38 +63,187 @@ public FileStream OpenWriteFile(string relativeOrAbsoluteFilePath, bool replaceI public ValueTask PickSaveFileAsync(params string[] fileTypes) { - // TODO: Limit input to the indicated file types. - Console.WriteLine(CliStrings.PromptSaveFile); - string? filePath = Console.ReadLine(); - - if (string.IsNullOrWhiteSpace(filePath)) + do { - return new ValueTask(Task.FromResult(null)); - } + Console.WriteLine(CliStrings.PromptSaveFile); + string? filePath = Console.ReadLine(); + + if (filePath is null || string.IsNullOrWhiteSpace(filePath)) + { + return new ValueTask(Task.FromResult(null)); + } + + // Remove quotes in case the user copy-pasted the file path from the file explorer. + filePath = TrimFilePath(filePath); + + if (IsFileOfType(filePath, fileTypes)) + { + return new ValueTask(File.OpenWrite(filePath)); + } - return new ValueTask(File.OpenWrite(filePath!)); + Console.Error.WriteLine(CliStrings.InvalidFileType, string.Join(", ", fileTypes)); + } while (true); } public ValueTask PickOpenFileAsync(params string[] fileTypes) { - // TODO: prompt the user to type in the console a relative or absolute file path that has one of the file types indicated. - throw new NotImplementedException(); + do + { + Console.WriteLine(CliStrings.PromptOpenFile); + string? filePath = Console.ReadLine(); + + if (filePath is null || string.IsNullOrWhiteSpace(filePath)) + { + return new ValueTask(Task.FromResult(null)); + } + + // Remove quotes in case the user copy-pasted the file path from the file explorer. + filePath = TrimFilePath(filePath); + + var fileInfo = new FileInfo(filePath); + if (fileInfo.Exists) + { + if (IsFileOfType(filePath, fileTypes)) + { + return new ValueTask(SandboxedFileReader.FromFileInfo(fileInfo)); + } + else + { + Console.Error.WriteLine(CliStrings.InvalidFileType, string.Join(", ", fileTypes)); + } + } + else + { + Console.Error.WriteLine(CliStrings.FileNotFound, fileInfo.FullName); + } + } while (true); } public ValueTask PickOpenFilesAsync(params string[] fileTypes) { - // TODO: prompt the user to type in the console a relative or absolute file path that has one of the file types indicated. - throw new NotImplementedException(); + do + { + Console.WriteLine(CliStrings.PromptOpenFiles); + string? filePaths = Console.ReadLine(); + + if (filePaths is null || string.IsNullOrWhiteSpace(filePaths)) + { + return new ValueTask(Task.FromResult(Array.Empty())); + } + + string[] paths = filePaths.Split(',', StringSplitOptions.RemoveEmptyEntries); + var filesInfo = new FileInfo[paths.Length]; + bool succeeded = true; + + for (int i = 0; i < paths.Length; i++) + { + string filePath = paths[i]; + + // Remove quotes in case the user copy-pasted the file path from the file explorer. + filePath = TrimFilePath(filePath); + + var fileInfo = new FileInfo(filePath); + if (fileInfo.Exists) + { + if (IsFileOfType(filePath, fileTypes)) + { + filesInfo[i] = fileInfo; + } + else + { + succeeded = false; + Console.Error.WriteLine(CliStrings.InvalidFileType, string.Join(", ", fileTypes)); + break; + } + } + else + { + succeeded = false; + Console.Error.WriteLine(CliStrings.FileNotFound, fileInfo.FullName); + break; + } + } + + if (succeeded) + { + var readers = new SandboxedFileReader[filesInfo.Length]; + for (int i = 0; i < filesInfo.Length; i++) + { + readers[i] = SandboxedFileReader.FromFileInfo(filesInfo[i]); + } + + return new ValueTask(Task.FromResult(readers)); + } + } while (true); } public ValueTask PickFolderAsync() { - // TODO: prompt the user to type in the console a relative or absolute file path that has one of the file types indicated. - throw new NotImplementedException(); + Console.WriteLine(CliStrings.PromptOpenFolder); + string? folderPath = Console.ReadLine(); + + if (folderPath is null || string.IsNullOrWhiteSpace(folderPath)) + { + return new ValueTask(Task.FromResult(null)); + } + + // Remove quotes in case the user copy-pasted the file path from the file explorer. + folderPath = TrimFilePath(folderPath); + + DirectoryInfo directoryInfo = Directory.CreateDirectory(folderPath); + + return new ValueTask(Task.FromResult(directoryInfo.FullName)); } public FileInfo CreateSelfDestroyingTempFile(string? desiredFileExtension = null) { return FileHelper.CreateTempFile(Constants.AppTempFolder, desiredFileExtension); } + + private static bool IsFileOfType(string filePath, string[] fileTypes) + { + if (AreAnyFileTypesValid(fileTypes)) + { + return true; + } + + string fileExtension = Path.GetExtension(filePath); + for (int i = 0; i < fileTypes.Length; i++) + { + string fileType = fileTypes[i]; + string editedFileType = "." + fileType.Trim('*').Trim('.').ToLower(); + if (string.Equals(fileExtension, editedFileType, StringComparison.CurrentCultureIgnoreCase)) + { + return true; + } + } + + return false; + } + + private static bool AreAnyFileTypesValid(string[] fileTypes) + { + if (fileTypes is null || fileTypes.Length == 0) + { + return true; + } + + return Array.Exists(fileTypes, fileType => string.Equals(fileType, "*.*", StringComparison.CurrentCultureIgnoreCase)); + } + + private static string TrimFilePath(string filePath) + { + // Remove quotes in case the user copy-pasted the file path from the file explorer. + // We do these trim in a loop in case the user input is something like ` "" C:\file.txt "" `. + + int fileLength; + + do + { + fileLength = filePath.Length; + filePath = filePath.Trim(' ').Trim('\"').Trim('\''); + } while (filePath.Length != fileLength); + + return filePath; + } } diff --git a/src/app/dev/platforms/desktop/DevToys.CLI/Strings/CliStrings/CliStrings.Designer.cs b/src/app/dev/platforms/desktop/DevToys.CLI/Strings/CliStrings/CliStrings.Designer.cs index 9436c84105..e8479ea42a 100644 --- a/src/app/dev/platforms/desktop/DevToys.CLI/Strings/CliStrings/CliStrings.Designer.cs +++ b/src/app/dev/platforms/desktop/DevToys.CLI/Strings/CliStrings/CliStrings.Designer.cs @@ -61,7 +61,52 @@ internal CliStrings() { } /// - /// Looks up a localized string similar to Please type a relative or absolute file path to write in:. + /// Looks up a localized string similar to The file '{0}' doesn't exist.. + /// + internal static string FileNotFound { + get { + return ResourceManager.GetString("FileNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid file path. The file should have one of the following extensions: {0}. + /// + internal static string InvalidFileType { + get { + return ResourceManager.GetString("InvalidFileType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please type a relative or absolute file path to read (or press Enter to cancel):. + /// + internal static string PromptOpenFile { + get { + return ResourceManager.GetString("PromptOpenFile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please type one or many relative or absolute file paths to read, separated by a comma (or press Enter to cancel):. + /// + internal static string PromptOpenFiles { + get { + return ResourceManager.GetString("PromptOpenFiles", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please type a relative or absolute folder path (or press Enter to cancel):. + /// + internal static string PromptOpenFolder { + get { + return ResourceManager.GetString("PromptOpenFolder", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please type a relative or absolute file path to write in (or press Enter to cancel):. /// internal static string PromptSaveFile { get { diff --git a/src/app/dev/platforms/desktop/DevToys.CLI/Strings/CliStrings/CliStrings.resx b/src/app/dev/platforms/desktop/DevToys.CLI/Strings/CliStrings/CliStrings.resx index bf2e0ad9e3..d45ed80fa8 100644 --- a/src/app/dev/platforms/desktop/DevToys.CLI/Strings/CliStrings/CliStrings.resx +++ b/src/app/dev/platforms/desktop/DevToys.CLI/Strings/CliStrings/CliStrings.resx @@ -117,7 +117,22 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + The file '{0}' doesn't exist. + + + Invalid file path. The file should have one of the following extensions: {0} + + + Please type a relative or absolute file path to read (or press Enter to cancel): + + + Please type one or many relative or absolute file paths to read, separated by a comma (or press Enter to cancel): + + + Please type a relative or absolute folder path (or press Enter to cancel): + - Please type a relative or absolute file path to write in: + Please type a relative or absolute file path to write in (or press Enter to cancel): \ No newline at end of file