2025-12-30 18:30:32 +01:00
using System.Text.Json.Serialization ;
2025-12-28 16:50:36 +01:00
using AIStudio.Tools.Rust ;
namespace AIStudio.Chat ;
/// <summary>
/// Represents an immutable file attachment with details about its type, name, path, and size.
/// </summary>
/// <param name="Type">The type of the file attachment.</param>
/// <param name="FileName">The name of the file, including extension.</param>
2026-10-15 16:42:02 +02:00
/// <param name="FilePath">The full path to the file, including the filename and extension. A file below the data directory, such as a transcript, is stored with a path relative to it.</param>
2025-12-28 16:50:36 +01:00
/// <param name="FileSizeBytes">The size of the file in bytes.</param>
2025-12-30 18:30:32 +01:00
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
[JsonDerivedType(typeof(FileAttachment), typeDiscriminator: "file")]
[JsonDerivedType(typeof(FileAttachmentImage), typeDiscriminator: "image")]
2026-07-15 12:53:30 +02:00
[JsonDerivedType(typeof(ManagedTranscriptAttachment), typeDiscriminator: "managed_transcript")]
2026-10-15 16:42:02 +02:00
public record FileAttachment ( FileAttachmentType Type , string FileName , [ property : JsonConverter ( typeof ( DataDirectoryRelativePathConverter ))] string FilePath , long FileSizeBytes )
2025-12-28 16:50:36 +01:00
{
/// <summary>
2025-12-30 18:30:32 +01:00
/// Gets a value indicating whether the file type is forbidden and should not be attached.
2025-12-28 16:50:36 +01:00
/// </summary>
2025-12-30 18:30:32 +01:00
/// <remarks>
/// The state is determined once during construction and does not change.
/// </remarks>
public bool IsForbidden { get ; } = Type == FileAttachmentType . FORBIDDEN ;
2025-12-28 16:50:36 +01:00
/// <summary>
2025-12-30 18:30:32 +01:00
/// Gets a value indicating whether the file type is valid and allowed to be attached.
2025-12-28 16:50:36 +01:00
/// </summary>
2025-12-30 18:30:32 +01:00
/// <remarks>
/// The state is determined once during construction and does not change.
/// </remarks>
public bool IsValid { get ; } = Type != FileAttachmentType . FORBIDDEN ;
2025-12-28 16:50:36 +01:00
/// <summary>
2025-12-30 18:30:32 +01:00
/// Gets a value indicating whether the file type is an image.
2025-12-28 16:50:36 +01:00
/// </summary>
2025-12-30 18:30:32 +01:00
/// <remarks>
/// The state is determined once during construction and does not change.
/// </remarks>
public bool IsImage { get ; } = Type == FileAttachmentType . IMAGE ;
/// <summary>
/// Gets the file path for loading the file from the web browser-side (Blazor).
/// </summary>
public string FilePathAsUrl { get ; } = FileHandler . CreateFileUrl ( FilePath );
/// <summary>
/// Gets a value indicating whether the file still exists on the file system.
/// </summary>
/// <remarks>
2026-10-09 20:11:15 +02:00
/// This property checks the file system each time it is accessed. It is therefore excluded from
/// serialization: a path on a network share which is out of reach blocks until the SMB timeout,
/// and the settings carry such attachments in chat templates. Every store of the settings would
/// wait that long. Reading never used the value, so older files with the field still load.
2025-12-30 18:30:32 +01:00
/// </remarks>
2026-10-09 20:11:15 +02:00
[JsonIgnore]
2025-12-30 18:30:32 +01:00
public bool Exists => File . Exists ( this . FilePath );
2025-12-28 16:50:36 +01:00
2026-04-09 16:01:47 +02:00
/// <summary>
/// Rebuilds the attachment from its current file path so file type detection uses the latest rules.
/// </summary>
2026-07-15 12:53:30 +02:00
public virtual FileAttachment Normalize () => FromPath ( this . FilePath );
2026-04-09 16:01:47 +02:00
2026-10-15 16:42:02 +02:00
/// <summary>
/// Makes the same attachment for its file at another place, e.g. after the chat holding the file moved.
/// </summary>
/// <remarks>
/// Deliberately no with expression: that one keeps whatever was derived from the old path when
/// the attachment was made, such as FilePathAsUrl, and the source of an image.
/// </remarks>
/// <param name="filePath">The path of the file now.</param>
/// <returns>An attachment of the same kind, with the new path.</returns>
public virtual FileAttachment WithFilePath ( string filePath ) => new ( this . Type , this . FileName , filePath , this . FileSizeBytes );
2025-12-28 16:50:36 +01:00
/// <summary>
/// Creates a FileAttachment from a file path by automatically determining the type,
/// extracting the filename, and reading the file size.
/// </summary>
/// <param name="filePath">The full path to the file.</param>
/// <returns>A FileAttachment instance with populated properties.</returns>
public static FileAttachment FromPath ( string filePath )
{
var fileName = Path . GetFileName ( filePath );
var fileSize = File . Exists ( filePath ) ? new FileInfo ( filePath ). Length : 0 ;
var type = DetermineFileType ( filePath );
2025-12-30 18:30:32 +01:00
return type switch
{
FileAttachmentType . DOCUMENT => new FileAttachment ( type , fileName , filePath , fileSize ),
FileAttachmentType . IMAGE => new FileAttachmentImage ( fileName , filePath , fileSize ),
_ => new FileAttachment ( type , fileName , filePath , fileSize ),
};
2025-12-28 16:50:36 +01:00
}
/// <summary>
/// Determines the file attachment type based on the file extension.
2026-04-09 11:53:30 +02:00
/// Uses centrally defined file type filters from <see cref="FileTypes"/>.
2025-12-28 16:50:36 +01:00
/// </summary>
/// <param name="filePath">The file path to analyze.</param>
/// <returns>The corresponding FileAttachmentType.</returns>
private static FileAttachmentType DetermineFileType ( string filePath )
{
2026-04-09 11:53:30 +02:00
// Check if it's an executable:
if ( FileTypes . IsAllowedPath ( filePath , FileTypes . EXECUTABLES ))
2026-03-12 12:51:44 +01:00
return FileAttachmentType . FORBIDDEN ;
2025-12-28 16:50:36 +01:00
// Check if it's an image file:
2026-04-09 11:53:30 +02:00
if ( FileTypes . IsAllowedPath ( filePath , FileTypes . IMAGE ))
2025-12-28 16:50:36 +01:00
return FileAttachmentType . IMAGE ;
// Check if it's an audio file:
2026-04-09 11:53:30 +02:00
if ( FileTypes . IsAllowedPath ( filePath , FileTypes . AUDIO ))
2025-12-28 16:50:36 +01:00
return FileAttachmentType . AUDIO ;
2026-04-09 11:53:30 +02:00
// Check if it's an allowed document file (PDF, Text, LaTeX, or Office):
if ( FileTypes . IsAllowedPath ( filePath , FileTypes . DOCUMENT ))
2025-12-28 16:50:36 +01:00
return FileAttachmentType . DOCUMENT ;
return FileAttachmentType . FORBIDDEN ;
}
}