From 5c1dd4e550be6ea1674249c74a0ad34d976ef7e9 Mon Sep 17 00:00:00 2001 From: PaulKoudelka Date: Tue, 31 Mar 2026 16:41:46 +0200 Subject: [PATCH] Added support for files without extensions --- .../Assistants/I18N/allTexts.lua | 33 ++--- app/MindWork AI Studio/Chat/FileAttachment.cs | 37 ++--- .../Components/AttachDocuments.razor.cs | 7 +- .../Components/SelectFile.razor.cs | 2 +- .../Dialogs/EmbeddingProviderDialog.razor | 2 - .../Dialogs/EmbeddingProviderDialog.razor.cs | 2 - .../plugin.lua | 50 +++++-- .../plugin.lua | 50 +++++-- app/MindWork AI Studio/Tools/Rust/FileType.cs | 41 ----- .../Tools/Rust/FileTypeFilter.cs | 113 +++++--------- .../Tools/Rust/FileTypes.cs | 140 ++++++++++++------ .../Tools/Rust/SaveFileOptions.cs | 2 +- .../Tools/Rust/SelectFileOptions.cs | 2 +- .../Tools/Services/RustService.FileSystem.cs | 6 +- .../Validation/FileExtensionValidation.cs | 11 +- 15 files changed, 234 insertions(+), 264 deletions(-) delete mode 100644 app/MindWork AI Studio/Tools/Rust/FileType.cs diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index b8314057..a5a64ca5 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -6319,30 +6319,6 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::RAG::RAGPROCESSES::AISRCSELWITHRETCTXVAL::T304 -- AI source selection with AI retrieval context validation UI_TEXT_CONTENT["AISTUDIO::TOOLS::RAG::RAGPROCESSES::AISRCSELWITHRETCTXVAL::T3775725978"] = "AI source selection with AI retrieval context validation" --- Executable Files -UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPEFILTER::T2217313358"] = "Executable Files" - --- All Source Code Files -UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPEFILTER::T2460199369"] = "All Source Code Files" - --- All Audio Files -UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPEFILTER::T2575722901"] = "All Audio Files" - --- All Video Files -UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPEFILTER::T2850789856"] = "All Video Files" - --- PDF Files -UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPEFILTER::T3108466742"] = "PDF Files" - --- All Image Files -UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPEFILTER::T4086723714"] = "All Image Files" - --- Text Files -UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPEFILTER::T639143005"] = "Text Files" - --- All Office Files -UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPEFILTER::T709668067"] = "All Office Files" - -- Text UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T1041509726"] = "Text" @@ -6352,6 +6328,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T1063218378"] = "Office Files -- Executable UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T1364437037"] = "Executable" +-- Mail +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T1399880782"] = "Mail" + +-- Source like +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T1487238587"] = "Source like" + -- Image UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T1494001562"] = "Image" @@ -6373,6 +6355,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T2502277006"] = "Custom" -- Media UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T3507473059"] = "Media" +-- Source like prefix +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T378481461"] = "Source like prefix" + -- Document UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T4165204724"] = "Document" diff --git a/app/MindWork AI Studio/Chat/FileAttachment.cs b/app/MindWork AI Studio/Chat/FileAttachment.cs index 42a696c0..558b7917 100644 --- a/app/MindWork AI Studio/Chat/FileAttachment.cs +++ b/app/MindWork AI Studio/Chat/FileAttachment.cs @@ -58,14 +58,11 @@ public record FileAttachment(FileAttachmentType Type, string FileName, string Fi /// extracting the filename, and reading the file size. /// /// The full path to the file. - /// Optional: The allowed file types. /// A FileAttachment instance with populated properties. - public static FileAttachment FromPath(string filePath, FileType[]? allowedTypes=null) + public static FileAttachment FromPath(string filePath) { var fileName = Path.GetFileName(filePath); var fileSize = File.Exists(filePath) ? new FileInfo(filePath).Length : 0; - if (allowedTypes != null && !IsAllowed(filePath, allowedTypes)) - return new FileAttachment(FileAttachmentType.FORBIDDEN, fileName, filePath, fileSize); var type = DetermineFileType(filePath); return type switch @@ -79,37 +76,23 @@ public record FileAttachment(FileAttachmentType Type, string FileName, string Fi /// /// Determines the file attachment type based on the file extension. - /// Uses centrally defined file types from . + /// Uses centrally defined file type filters from . /// /// The file path to analyze. /// The corresponding FileAttachmentType. private static FileAttachmentType DetermineFileType(string filePath) { - var extension = Path.GetExtension(filePath).TrimStart('.').ToLowerInvariant(); + if (FileTypes.IsAllowedPath(filePath, FileTypes.EXECUTABLES)) + return FileAttachmentType.FORBIDDEN; - // Check if it's an image file: - if (FileTypes.OnlyAllowTypes(FileTypes.IMAGE).Contains(extension)) - { + if (FileTypes.IsAllowedPath(filePath, FileTypes.IMAGE)) return FileAttachmentType.IMAGE; - } - // Check if it's an audio file: - if (FileTypes.OnlyAllowTypes(FileTypes.AUDIO).Contains(extension)) + if (FileTypes.IsAllowedPath(filePath, FileTypes.AUDIO)) return FileAttachmentType.AUDIO; - // Check if it's an allowed document file (PDF, Text, or Office): - if (FileTypes.OnlyAllowTypes(FileTypes.DOCUMENT).Contains(extension)) - { - return FileAttachmentType.DOCUMENT; - } - - // All other file types are forbidden: - return FileAttachmentType.FORBIDDEN; + return FileTypes.IsAllowedPath(filePath, FileTypes.DOCUMENT) + ? FileAttachmentType.DOCUMENT + : FileAttachmentType.FORBIDDEN; } - - private static bool IsAllowed(string filePath, FileType[] allowedTypes) - { - var extension = Path.GetExtension(filePath).TrimStart('.').ToLowerInvariant(); - return FileTypes.OnlyAllowTypes(allowedTypes).Contains(extension); - } -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Components/AttachDocuments.razor.cs b/app/MindWork AI Studio/Components/AttachDocuments.razor.cs index be83d51b..e4a0fe0f 100644 --- a/app/MindWork AI Studio/Components/AttachDocuments.razor.cs +++ b/app/MindWork AI Studio/Components/AttachDocuments.razor.cs @@ -48,9 +48,6 @@ public partial class AttachDocuments : MSGComponentBase [Parameter] public bool UseSmallForm { get; set; } - [Parameter] - public FileType[]? AllowedFileTypes { get; set; } - /// /// When true, validate media file types before attaching. Default is true. That means that /// the user cannot attach unsupported media file types when the provider or model does not @@ -184,7 +181,7 @@ public partial class AttachDocuments : MSGComponentBase { if(!await FileExtensionValidation.IsExtensionValidWithNotifyAsync(FileExtensionValidation.UseCase.ATTACHING_CONTENT, path, this.ValidateMediaFileTypes, this.Provider)) continue; - this.DocumentPaths.Add(FileAttachment.FromPath(path, this.AllowedFileTypes)); + this.DocumentPaths.Add(FileAttachment.FromPath(path)); } await this.DocumentPathsChanged.InvokeAsync(this.DocumentPaths); @@ -228,7 +225,7 @@ public partial class AttachDocuments : MSGComponentBase if (!await FileExtensionValidation.IsExtensionValidWithNotifyAsync(FileExtensionValidation.UseCase.ATTACHING_CONTENT, selectedFilePath, this.ValidateMediaFileTypes, this.Provider)) continue; - this.DocumentPaths.Add(FileAttachment.FromPath(selectedFilePath, this.AllowedFileTypes)); + this.DocumentPaths.Add(FileAttachment.FromPath(selectedFilePath)); } await this.DocumentPathsChanged.InvokeAsync(this.DocumentPaths); diff --git a/app/MindWork AI Studio/Components/SelectFile.razor.cs b/app/MindWork AI Studio/Components/SelectFile.razor.cs index c7b4dace..91c7a667 100644 --- a/app/MindWork AI Studio/Components/SelectFile.razor.cs +++ b/app/MindWork AI Studio/Components/SelectFile.razor.cs @@ -23,7 +23,7 @@ public partial class SelectFile : MSGComponentBase public string FileDialogTitle { get; set; } = "Select File"; [Parameter] - public FileType[]? Filter { get; set; } + public FileTypeFilter[]? Filter { get; set; } [Parameter] public Func Validation { get; set; } = _ => null; diff --git a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor index 6e5a595b..85e6e6ef 100644 --- a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor +++ b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor @@ -1,6 +1,5 @@ @using AIStudio.Provider @using AIStudio.Provider.SelfHosted -@using AIStudio.Tools.Rust @inherits MSGComponentBase @@ -125,7 +124,6 @@ Validation="@this.providerValidation.ValidatingInstanceName" UserAttributes="@SPELLCHECK_ATTRIBUTES" /> - diff --git a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs index a3b66dbe..33e4db18 100644 --- a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs @@ -1,4 +1,3 @@ -using AIStudio.Chat; using AIStudio.Components; using AIStudio.Provider; using AIStudio.Settings; @@ -97,7 +96,6 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId private readonly List availableModels = new(); private readonly Encryption encryption = Program.ENCRYPTION; private readonly ProviderValidation providerValidation; - private HashSet chatDocumentPaths = []; public EmbeddingProviderDialog() { diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index c518d439..09363a3f 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -6321,29 +6321,47 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::RAG::RAGPROCESSES::AISRCSELWITHRETCTXVAL::T304 -- AI-based data source selection with AI retrieval context validation UI_TEXT_CONTENT["AISTUDIO::TOOLS::RAG::RAGPROCESSES::AISRCSELWITHRETCTXVAL::T3775725978"] = "KI-basierte Datenquellen-Auswahl mit Validierung des Abrufkontexts" --- Executable Files -UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPEFILTER::T2217313358"] = "Ausführbare Dateien" +-- Text +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T1041509726"] = "Text" --- All Source Code Files -UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPEFILTER::T2460199369"] = "Alle Quellcodedateien" +-- Office Files +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T1063218378"] = "Office-Dateien" --- All Audio Files -UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPEFILTER::T2575722901"] = "Alle Audiodateien" +-- Executable +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T1364437037"] = "Ausführbare Dateien" --- All Video Files -UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPEFILTER::T2850789856"] = "Alle Videodateien" +-- Mail +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T1399880782"] = "E-Mail" --- PDF Files -UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPEFILTER::T3108466742"] = "PDF-Dateien" +-- Source like +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T1487238587"] = "Source Code ähnlich" --- All Image Files -UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPEFILTER::T4086723714"] = "Alle Bilddateien" +-- Image +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T1494001562"] = "Bild" --- Text Files -UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPEFILTER::T639143005"] = "Textdateien" +-- Video +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T1533528076"] = "Video" --- All Office Files -UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPEFILTER::T709668067"] = "Alle Office-Dateien" +-- Source Code +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T1569048941"] = "Quellcode" + +-- Config +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T1779622119"] = "Konfiguration" + +-- Audio +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T2291602489"] = "Audio" + +-- Custom +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T2502277006"] = "Benutzerdefiniert" + +-- Media +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T3507473059"] = "Medien" + +-- Source like prefix +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T378481461"] = "Source Code ähnlicher Prefix" + +-- Document +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T4165204724"] = "Dokument" -- Pandoc Installation UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T185447014"] = "Pandoc-Installation" diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index a4fdfd5c..e9410700 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -6321,29 +6321,47 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::RAG::RAGPROCESSES::AISRCSELWITHRETCTXVAL::T304 -- AI-based data source selection with AI retrieval context validation UI_TEXT_CONTENT["AISTUDIO::TOOLS::RAG::RAGPROCESSES::AISRCSELWITHRETCTXVAL::T3775725978"] = "AI-based data source selection with AI retrieval context validation" --- Executable Files -UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPEFILTER::T2217313358"] = "Executable Files" +-- Text +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T1041509726"] = "Text" --- All Source Code Files -UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPEFILTER::T2460199369"] = "All Source Code Files" +-- Office Files +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T1063218378"] = "Office Files" --- All Audio Files -UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPEFILTER::T2575722901"] = "All Audio Files" +-- Executable +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T1364437037"] = "Executable" --- All Video Files -UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPEFILTER::T2850789856"] = "All Video Files" +-- Mail +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T1399880782"] = "Mail" --- PDF Files -UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPEFILTER::T3108466742"] = "PDF Files" +-- Source like +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T1487238587"] = "Source like" --- All Image Files -UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPEFILTER::T4086723714"] = "All Image Files" +-- Image +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T1494001562"] = "Image" --- Text Files -UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPEFILTER::T639143005"] = "Text Files" +-- Video +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T1533528076"] = "Video" --- All Office Files -UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPEFILTER::T709668067"] = "All Office Files" +-- Source Code +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T1569048941"] = "Source Code" + +-- Config +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T1779622119"] = "Config" + +-- Audio +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T2291602489"] = "Audio" + +-- Custom +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T2502277006"] = "Custom" + +-- Media +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T3507473059"] = "Media" + +-- Source like prefix +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T378481461"] = "Source like prefix" + +-- Document +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T4165204724"] = "Document" -- Pandoc Installation UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T185447014"] = "Pandoc Installation" diff --git a/app/MindWork AI Studio/Tools/Rust/FileType.cs b/app/MindWork AI Studio/Tools/Rust/FileType.cs deleted file mode 100644 index c333a691..00000000 --- a/app/MindWork AI Studio/Tools/Rust/FileType.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace AIStudio.Tools.Rust; - -/// -/// Represents a file type that can optionally contain child file types. -/// Use the static helpers , and to build readable trees. -/// -/// Display name of the type (e.g., "Document"). -/// File extensions belonging to this type (without dot). -/// Nested file types that are included when this type is selected. -public sealed record FileType(string FilterName, string[] FilterExtensions, IReadOnlyList Children) -{ - /// - /// Factory for a leaf node. - /// Example: FileType.Leaf(".NET", "cs", "razor") - /// - public static FileType Leaf(string name, params string[] extensions) => - new(name, extensions, []); - - /// - /// Factory for a parent node that only has children. - /// Example: FileType.Parent("Source Code", dotnet, java) - /// - public static FileType Parent(string name, params FileType[]? children) => - new(name, [], children ?? []); - - /// - /// Factory for a composite node that has its own extensions in addition to children. - /// - public static FileType Composite(string name, string[] extensions, params FileType[] children) => - new(name, extensions, children); - - /// - /// Collects all extensions for this type, including children. - /// - public IEnumerable FlattenExtensions() - { - return this.FilterExtensions - .Concat(this.Children.SelectMany(child => child.FlattenExtensions())) - .Distinct(StringComparer.OrdinalIgnoreCase); - } -} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Rust/FileTypeFilter.cs b/app/MindWork AI Studio/Tools/Rust/FileTypeFilter.cs index 03232070..f4cd1c7e 100644 --- a/app/MindWork AI Studio/Tools/Rust/FileTypeFilter.cs +++ b/app/MindWork AI Studio/Tools/Rust/FileTypeFilter.cs @@ -1,80 +1,49 @@ -// ReSharper disable NotAccessedPositionalProperty.Global - -using AIStudio.Tools.PluginSystem; - namespace AIStudio.Tools.Rust; /// -/// Represents a file type filter for file selection dialogs. +/// Represents a file type that can optionally contain child file types. +/// Use the static helpers , and to build readable trees. /// -/// The name of the filter. -/// The file extensions associated with the filter. -public readonly record struct FileTypeFilter(string FilterName, string[] FilterExtensions) +/// Display name of the type (e.g., "Document"). +/// File extensions belonging to this type (without dot). +/// Nested file types that are included when this type is selected. +public sealed record FileTypeFilter(string FilterName, string[] FilterExtensions, IReadOnlyList Children) { - private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(FileTypeFilter).Namespace, nameof(FileTypeFilter)); + /// + /// Factory for a leaf node. + /// Example: FileType.Leaf(".NET", "cs", "razor") + /// + public static FileTypeFilter Leaf(string name, params string[] extensions) => + new(name, extensions, []); + + /// + /// Factory for a parent node that only has children. + /// Example: FileType.Parent("Source Code", dotnet, java) + /// + public static FileTypeFilter Parent(string name, params FileTypeFilter[]? children) => + new(name, [], children ?? []); + + /// + /// Factory for a composite node that has its own extensions in addition to children. + /// + public static FileTypeFilter Composite(string name, string[] extensions, params FileTypeFilter[] children) => + new(name, extensions, children); - public static FileTypeFilter PDF => new(TB("PDF Files"), ["pdf"]); + /// + /// Collects all extensions for this type, including children. + /// + public IEnumerable FlattenExtensions() + { + return this.FilterExtensions + .Concat(this.Children.SelectMany(child => child.FlattenExtensions())) + .Distinct(StringComparer.OrdinalIgnoreCase); + } - public static FileTypeFilter Text => new(TB("Text Files"), ["txt", "md"]); - - public static FileTypeFilter AllOffice => new(TB("All Office Files"), ["docx", "xlsx", "pptx", "doc", "xls", "ppt", "pdf"]); - - public static FileTypeFilter AllImages => new(TB("All Image Files"), ["jpg", "jpeg", "png", "gif", "bmp", "tiff", "svg", "webp", "heic"]); - - public static FileTypeFilter AllVideos => new(TB("All Video Files"), ["mp4", "m4v", "avi", "mkv", "mov", "wmv", "flv", "webm"]); - - public static FileTypeFilter AllAudio => new(TB("All Audio Files"), ["mp3", "wav", "wave", "aac", "flac", "ogg", "m4a", "wma", "alac", "aiff", "m4b"]); - - public static FileTypeFilter AllSourceCode => new(TB("All Source Code Files"), - [ - // .NET - "cs", "vb", "fs", "razor", "aspx", "cshtml", "csproj", - - // Java: - "java", - - // Python: - "py", - - // JavaScript/TypeScript: - "js", "ts", - - // C/C++: - "c", "cpp", "h", "hpp", - - // Ruby: - "rb", - - // Go: - "go", - - // Rust: - "rs", - - // Lua: - "lua", - - // PHP: - "php", - - // HTML/CSS: - "html", "css", - - // Swift/Kotlin: - "swift", "kt", - - // Shell scripts: - "sh", "bash", - - // Logging files: - "log", - - // JSON/YAML/XML: - "json", "yaml", "yml", "xml", - - // Config files: - "ini", "cfg", "toml", "plist", - ]); - - public static FileTypeFilter Executables => new(TB("Executable Files"), ["exe", "app", "bin", "appimage"]); + public bool ContainsType(FileTypeFilter target) + { + if (this == target) + return true; + + return this.Children.Any(child => child.ContainsType(target)); + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Rust/FileTypes.cs b/app/MindWork AI Studio/Tools/Rust/FileTypes.cs index ff6fcd88..a6e8fd58 100644 --- a/app/MindWork AI Studio/Tools/Rust/FileTypes.cs +++ b/app/MindWork AI Studio/Tools/Rust/FileTypes.cs @@ -8,77 +8,123 @@ namespace AIStudio.Tools.Rust; /// public static class FileTypes { - private static string TB(string fallbackEn) => I18N.I.T(fallbackEn, typeof(FileType).Namespace, nameof(FileType)); + private static string TB(string fallbackEn) => I18N.I.T(fallbackEn, typeof(FileTypeFilter).Namespace, nameof(FileTypeFilter)); + + // Keep SOURCE_LIKE in the same leaf style as the other file types. + // These values are not sufficient for Dockerfile-style files without extensions, + // therefore IsAllowedSourceLikeFileName is still required for real matching. + public static readonly FileTypeFilter SOURCE_LIKE_FILE_NAMES = FileTypeFilter.Leaf(TB("Source like"), + "Dockerfile", "Containerfile", "Jenkinsfile", "Makefile", "GNUmakefile", "Procfile", "Vagrantfile", + "Tiltfile", "Justfile", "Brewfile", "Caddyfile", "Gemfile", "Podfile", "Fastfile", "Appfile", "Rakefile", "Dangerfile", + "BUILD", "WORKSPACE", "BUCK"); + + public static readonly FileTypeFilter SOURCE_LIKE_FILE_NAME_PREFIXES = FileTypeFilter.Leaf(TB("Source like prefix"), + "Dockerfile", "Containerfile", "Jenkinsfile", "Procfile", "Caddyfile"); // Source code hierarchy: SourceCode -> (.NET, Java, Python, Web, C/C++, Config, ...) - public static readonly FileType DOTNET = FileType.Leaf(".NET", "cs", "razor", "vb", "fs", "aspx", "cshtml", "csproj"); - public static readonly FileType JAVA = FileType.Leaf("Java", "java"); - public static readonly FileType PYTHON = FileType.Leaf("Python", "py"); - public static readonly FileType JAVASCRIPT = FileType.Leaf("JavaScript/TypeScript", "js", "ts"); - public static readonly FileType CFAMILY = FileType.Leaf("C/C++", "c", "cpp", "h", "hpp"); - public static readonly FileType RUBY = FileType.Leaf("Ruby", "rb"); - public static readonly FileType GO = FileType.Leaf("Go", "go"); - public static readonly FileType RUST = FileType.Leaf("Rust", "rs"); - public static readonly FileType LUA = FileType.Leaf("Lua", "lua"); - public static readonly FileType PHP = FileType.Leaf("PHP", "php"); - public static readonly FileType WEB = FileType.Leaf("HTML/CSS", "html", "css"); - public static readonly FileType APP = FileType.Leaf("Swift/Kotlin", "swift", "kt"); - public static readonly FileType SHELL = FileType.Leaf("Shell", "sh", "bash", "zsh"); - public static readonly FileType LOG = FileType.Leaf("Log", "log"); - public static readonly FileType JSON = FileType.Leaf("JSON", "json"); - public static readonly FileType XML = FileType.Leaf("XML", "xml"); - public static readonly FileType YAML = FileType.Leaf("YAML", "yaml", "yml"); - public static readonly FileType CONFIG = FileType.Leaf(TB("Config"), "ini", "cfg", "toml", "plist"); - - public static readonly FileType SOURCE_CODE = FileType.Parent(TB("Source Code"), - DOTNET, JAVA, PYTHON, JAVASCRIPT, CFAMILY, RUBY, GO, RUST, LUA, PHP, WEB, APP, SHELL, LOG, JSON, XML, YAML, CONFIG); + public static readonly FileTypeFilter DOTNET = FileTypeFilter.Leaf(".NET", "cs", "razor", "vb", "fs", "aspx", "cshtml", "csproj"); + public static readonly FileTypeFilter JAVA = FileTypeFilter.Leaf("Java", "java"); + public static readonly FileTypeFilter PYTHON = FileTypeFilter.Leaf("Python", "py"); + public static readonly FileTypeFilter JAVASCRIPT = FileTypeFilter.Leaf("JavaScript/TypeScript", "js", "ts"); + public static readonly FileTypeFilter CFAMILY = FileTypeFilter.Leaf("C/C++", "c", "cpp", "h", "hpp"); + public static readonly FileTypeFilter RUBY = FileTypeFilter.Leaf("Ruby", "rb"); + public static readonly FileTypeFilter GO = FileTypeFilter.Leaf("Go", "go"); + public static readonly FileTypeFilter RUST = FileTypeFilter.Leaf("Rust", "rs"); + public static readonly FileTypeFilter LUA = FileTypeFilter.Leaf("Lua", "lua"); + public static readonly FileTypeFilter PHP = FileTypeFilter.Leaf("PHP", "php"); + public static readonly FileTypeFilter WEB = FileTypeFilter.Leaf("HTML/CSS", "html", "css"); + public static readonly FileTypeFilter APP = FileTypeFilter.Leaf("Swift/Kotlin", "swift", "kt"); + public static readonly FileTypeFilter SHELL = FileTypeFilter.Leaf("Shell", "sh", "bash", "zsh"); + public static readonly FileTypeFilter LOG = FileTypeFilter.Leaf("Log", "log"); + public static readonly FileTypeFilter JSON = FileTypeFilter.Leaf("JSON", "json"); + public static readonly FileTypeFilter XML = FileTypeFilter.Leaf("XML", "xml"); + public static readonly FileTypeFilter YAML = FileTypeFilter.Leaf("YAML", "yaml", "yml"); + public static readonly FileTypeFilter CONFIG = FileTypeFilter.Leaf(TB("Config"), "ini", "cfg", "toml", "plist"); + + public static readonly FileTypeFilter SOURCE_CODE = FileTypeFilter.Parent(TB("Source Code"), + DOTNET, JAVA, PYTHON, JAVASCRIPT, CFAMILY, RUBY, GO, RUST, LUA, PHP, WEB, APP, SHELL, LOG, JSON, XML, YAML, CONFIG, SOURCE_LIKE_FILE_NAMES, SOURCE_LIKE_FILE_NAME_PREFIXES); // Document hierarchy - public static readonly FileType PDF = FileType.Leaf("PDF", "pdf"); - public static readonly FileType TEXT = FileType.Leaf(TB("Text"), "txt", "md"); - public static readonly FileType MS_WORD = FileType.Leaf("Microsoft Word", "docx"); - public static readonly FileType WORD = FileType.Composite("Word", ["docx"], MS_WORD); - public static readonly FileType EXCEL = FileType.Leaf("Excel", "xls", "xlsx"); - public static readonly FileType POWER_POINT = FileType.Leaf("PowerPoint", "ppt", "pptx"); - - public static readonly FileType OFFICE_FILES = FileType.Parent(TB("Office Files"), + public static readonly FileTypeFilter PDF = FileTypeFilter.Leaf("PDF", "pdf"); + public static readonly FileTypeFilter TEXT = FileTypeFilter.Leaf(TB("Text"), "txt", "md"); + public static readonly FileTypeFilter MS_WORD = FileTypeFilter.Leaf("Microsoft Word", "docx"); + public static readonly FileTypeFilter WORD = FileTypeFilter.Composite("Word", ["doc"], MS_WORD); + public static readonly FileTypeFilter EXCEL = FileTypeFilter.Leaf("Excel", "xls", "xlsx"); + public static readonly FileTypeFilter POWER_POINT = FileTypeFilter.Leaf("PowerPoint", "ppt", "pptx"); + public static readonly FileTypeFilter MAIL = FileTypeFilter.Leaf(TB("Mail"), "eml", "msg", "mbox"); + + public static readonly FileTypeFilter OFFICE_FILES = FileTypeFilter.Parent(TB("Office Files"), WORD, EXCEL, POWER_POINT, PDF); - public static readonly FileType DOCUMENT = FileType.Parent(TB("Document"), - TEXT, OFFICE_FILES, SOURCE_CODE); + public static readonly FileTypeFilter DOCUMENT = FileTypeFilter.Parent(TB("Document"), + TEXT, OFFICE_FILES, SOURCE_CODE, MAIL); // Media hierarchy - public static readonly FileType IMAGE = FileType.Leaf(TB("Image"), + public static readonly FileTypeFilter IMAGE = FileTypeFilter.Leaf(TB("Image"), "jpg", "jpeg", "png", "gif", "bmp", "tiff", "svg", "webp", "heic"); - public static readonly FileType AUDIO = FileType.Leaf(TB("Audio"), + public static readonly FileTypeFilter AUDIO = FileTypeFilter.Leaf(TB("Audio"), "mp3", "wav", "wave", "aac", "flac", "ogg", "m4a", "wma", "alac", "aiff", "m4b"); - public static readonly FileType VIDEO = FileType.Leaf(TB("Video"), + public static readonly FileTypeFilter VIDEO = FileTypeFilter.Leaf(TB("Video"), "mp4", "m4v", "avi", "mkv", "mov", "wmv", "flv", "webm"); - - public static readonly FileType MEDIA = FileType.Parent(TB("Media"), IMAGE, AUDIO, VIDEO); + + public static readonly FileTypeFilter MEDIA = FileTypeFilter.Parent(TB("Media"), IMAGE, AUDIO, VIDEO); // Other standalone types - public static readonly FileType EXECUTABLES = FileType.Leaf(TB("Executable"), "exe", "app", "bin", "appimage"); + public static readonly FileTypeFilter EXECUTABLES = FileTypeFilter.Leaf(TB("Executable"), "exe", "app", "bin", "appimage"); + + public static FileTypeFilter? AsOneFileType(params FileTypeFilter[]? types) + { + if (types == null || types.Length == 0) + return null; + + if (types.Length == 1) return types[0]; - /// - /// Builds a distinct, lower-cased list of extensions allowed for the provided types. - /// Accepts both composite types (e.g., Document) and leaves (e.g., Pdf). - /// - public static string[] OnlyAllowTypes(params FileType[] types) + return FileTypeFilter.Composite(TB("Custom"), OnlyAllowTypes(types)); + } + + public static string[] OnlyAllowTypes(params FileTypeFilter[] types) { if (types.Length == 0) return []; return types + .Where(t => t != SOURCE_LIKE_FILE_NAMES && t != SOURCE_LIKE_FILE_NAME_PREFIXES) .SelectMany(t => t.FlattenExtensions()) .Select(ext => ext.ToLowerInvariant()) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); } - public static FileType? AsOneFileType(params FileType[]? types) + /// + /// Validates a file path against the provided filters. + /// Supports extension-based matching and source-like file names (e.g. Dockerfile). + /// + public static bool IsAllowedPath(string filePath, params FileTypeFilter[]? types) { - if (types == null || types.Length == 0) - return null; - return FileType.Composite(TB("Custom"), OnlyAllowTypes(types)); + if (types == null || types.Length == 0 || string.IsNullOrWhiteSpace(filePath)) + return false; + + var extension = Path.GetExtension(filePath).TrimStart('.'); + if (!string.IsNullOrWhiteSpace(extension)) + { + if (OnlyAllowTypes(types).Contains(extension, StringComparer.OrdinalIgnoreCase)) + return true; + } + + var fileName = Path.GetFileName(filePath); + if (string.IsNullOrWhiteSpace(fileName)) + { + return false; + } + + if (types.Any(t => t.ContainsType(SOURCE_LIKE_FILE_NAMES))) + { + if (SOURCE_LIKE_FILE_NAMES.FilterExtensions.Contains(fileName)) return true; + } + + if (types.Any(t => t.ContainsType(SOURCE_LIKE_FILE_NAME_PREFIXES))){ + if (SOURCE_LIKE_FILE_NAME_PREFIXES.FilterExtensions.Any(prefix => fileName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))) return true; + } + + return false; } } diff --git a/app/MindWork AI Studio/Tools/Rust/SaveFileOptions.cs b/app/MindWork AI Studio/Tools/Rust/SaveFileOptions.cs index f1300ac1..107e581a 100644 --- a/app/MindWork AI Studio/Tools/Rust/SaveFileOptions.cs +++ b/app/MindWork AI Studio/Tools/Rust/SaveFileOptions.cs @@ -6,5 +6,5 @@ public class SaveFileOptions public PreviousFile? PreviousFile { get; init; } - public FileType? Filter { get; init; } + public FileTypeFilter? Filter { get; init; } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Rust/SelectFileOptions.cs b/app/MindWork AI Studio/Tools/Rust/SelectFileOptions.cs index fac7d5f4..28d16809 100644 --- a/app/MindWork AI Studio/Tools/Rust/SelectFileOptions.cs +++ b/app/MindWork AI Studio/Tools/Rust/SelectFileOptions.cs @@ -6,5 +6,5 @@ public sealed class SelectFileOptions public PreviousFile? PreviousFile { get; init; } - public FileType? Filter { get; init; } + public FileTypeFilter? Filter { get; init; } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Services/RustService.FileSystem.cs b/app/MindWork AI Studio/Tools/Services/RustService.FileSystem.cs index c55b6a8b..76519eb2 100644 --- a/app/MindWork AI Studio/Tools/Services/RustService.FileSystem.cs +++ b/app/MindWork AI Studio/Tools/Services/RustService.FileSystem.cs @@ -17,7 +17,7 @@ public sealed partial class RustService return await result.Content.ReadFromJsonAsync(this.jsonRustSerializerOptions); } - public async Task SelectFile(string title, FileType[]? filter = null, string? initialFile = null) + public async Task SelectFile(string title, FileTypeFilter[]? filter = null, string? initialFile = null) { var payload = new SelectFileOptions { @@ -36,7 +36,7 @@ public sealed partial class RustService return await result.Content.ReadFromJsonAsync(this.jsonRustSerializerOptions); } - public async Task SelectFiles(string title, FileType[]? filter = null, string? initialFile = null) + public async Task SelectFiles(string title, FileTypeFilter[]? filter = null, string? initialFile = null) { var payload = new SelectFileOptions { @@ -63,7 +63,7 @@ public sealed partial class RustService /// An optional initial file path to pre-fill in the dialog. /// A object containing information about whether the user canceled the /// operation and whether the select operation was successful. - public async Task SaveFile(string title, FileType[]? filter = null, string? initialFile = null) + public async Task SaveFile(string title, FileTypeFilter[]? filter = null, string? initialFile = null) { var payload = new SaveFileOptions { diff --git a/app/MindWork AI Studio/Tools/Validation/FileExtensionValidation.cs b/app/MindWork AI Studio/Tools/Validation/FileExtensionValidation.cs index a9a87367..d38a8c08 100644 --- a/app/MindWork AI Studio/Tools/Validation/FileExtensionValidation.cs +++ b/app/MindWork AI Studio/Tools/Validation/FileExtensionValidation.cs @@ -43,8 +43,7 @@ public static class FileExtensionValidation /// True if valid, false if invalid (error/warning already sent via MessageBus). public static async Task IsExtensionValidWithNotifyAsync(UseCase useCae, string filePath, bool validateMediaFileTypes = true, Settings.Provider? provider = null) { - var ext = Path.GetExtension(filePath).TrimStart('.').ToLowerInvariant(); - if(FileTypes.EXECUTABLES.FlattenExtensions().Contains(ext)) + if (FileTypes.IsAllowedPath(filePath, FileTypes.EXECUTABLES)) { await MessageBus.INSTANCE.SendError(new( Icons.Material.Filled.AppBlocking, @@ -53,7 +52,7 @@ public static class FileExtensionValidation } var capabilities = provider?.GetModelCapabilities() ?? new(); - if (FileTypes.IMAGE.FlattenExtensions().Contains(ext)) + if (FileTypes.IsAllowedPath(filePath, FileTypes.IMAGE)) { switch (useCae) { @@ -88,7 +87,7 @@ public static class FileExtensionValidation } } - if(FileTypes.VIDEO.FlattenExtensions().Contains(ext)) + if (FileTypes.IsAllowedPath(filePath, FileTypes.VIDEO)) { await MessageBus.INSTANCE.SendWarning(new( Icons.Material.Filled.FeaturedVideo, @@ -96,7 +95,7 @@ public static class FileExtensionValidation return false; } - if(FileTypes.AUDIO.FlattenExtensions().Contains(ext)) + if (FileTypes.IsAllowedPath(filePath, FileTypes.AUDIO)) { await MessageBus.INSTANCE.SendWarning(new( Icons.Material.Filled.AudioFile, @@ -123,7 +122,7 @@ public static class FileExtensionValidation return false; } - if (FileTypes.IMAGE.FlattenExtensions().Any(x => x.Equals(ext, StringComparison.OrdinalIgnoreCase))) + if (FileTypes.IsAllowedPath(filePath, FileTypes.IMAGE)) { await MessageBus.INSTANCE.SendError(new( Icons.Material.Filled.ImageNotSupported,