using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; namespace FileManager { /// Get valid filename. Advanced features incl. parameterized template public class FileNamingTemplate : NamingTemplate { public ReplacementCharacters ReplacementCharacters { get; } /// Proposed file name with optional html-styled template tags. public FileNamingTemplate(string template, ReplacementCharacters replacement) : base(template) { ReplacementCharacters = replacement ?? ReplacementCharacters.Default; } /// Generate a valid path for this file or directory public LongPath GetFilePath(string fileExtension, bool returnFirstExisting = false) { string fileName = Template.EndsWith(Path.DirectorySeparatorChar) || Template.EndsWith(Path.AltDirectorySeparatorChar) ? FileUtility.RemoveLastCharacter(Template) : Template; List pathParts = new(); var paramReplacements = ParameterReplacements.ToDictionary(r => $"<{formatKey(r.Key)}>", r => formatValue(r.Value, ReplacementCharacters)); while (!string.IsNullOrEmpty(fileName)) { var file = Path.GetFileName(fileName); if (Path.IsPathRooted(Template) && file == string.Empty) { pathParts.Add(fileName); break; } else { pathParts.Add(file); fileName = Path.GetDirectoryName(fileName); } } pathParts.Reverse(); var fileNamePart = pathParts[^1]; pathParts.Remove(fileNamePart); fileNamePart = fileNamePart[..^fileExtension.Length]; LongPath directory = Path.Join(pathParts.Select(p => replaceFileName(p, paramReplacements, LongPath.MaxFilenameLength)).ToArray()); //If file already exists, GetValidFilename will append " (n)" to the filename. //This could cause the filename length to exceed MaxFilenameLength, so reduce //allowable filename length by 5 chars, allowing for up to 99 duplicates. return FileUtility .GetValidFilename( Path.Join(directory, replaceFileName(fileNamePart, paramReplacements, LongPath.MaxFilenameLength - fileExtension.Length - 5)) + fileExtension, ReplacementCharacters, fileExtension, returnFirstExisting ); } private static string replaceFileName(string filename, Dictionary paramReplacements, int maxFilenameLength) { List filenameParts = new(); //Build the filename in parts, replacing replacement parameters with //their values, and storing the parts in a list. while (!string.IsNullOrEmpty(filename)) { int openIndex = filename.IndexOf('<'); int closeIndex = filename.IndexOf('>'); if (openIndex == 0 && closeIndex > 0) { var key = filename[..(closeIndex + 1)]; if (paramReplacements.ContainsKey(key)) filenameParts.Add(new StringBuilder(paramReplacements[key])); else filenameParts.Add(new StringBuilder(key)); filename = filename[(closeIndex + 1)..]; } else if (openIndex > 0 && closeIndex > openIndex) { var other = filename[..openIndex]; filenameParts.Add(new StringBuilder(other)); filename = filename[openIndex..]; } else { filenameParts.Add(new StringBuilder(filename)); filename = string.Empty; } } //Remove 1 character from the end of the longest filename part until //the total filename is less than max filename length while (filenameParts.Sum(p => LongPath.GetFilesystemStringLength(p)) > maxFilenameLength) { int maxLength = filenameParts.Max(p => p.Length); var maxEntry = filenameParts.First(p => p.Length == maxLength); maxEntry.Remove(maxLength - 1, 1); } return string.Join("", filenameParts); } private static string formatValue(object value, ReplacementCharacters replacements) { if (value is null) return ""; // Other illegal characters will be taken care of later. Must take care of slashes now so params can't introduce new folders. // Esp important for file templates. return replacements.ReplaceFilenameChars(value.ToString()); } } }