using System.Text.Json.Serialization;
using AIStudio.Tools.Rust;
namespace AIStudio.Chat;
///
/// Represents an immutable file attachment with details about its type, name, path, and size.
///
/// The type of the file attachment.
/// The name of the file, including extension.
/// 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.
/// The size of the file in bytes.
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
[JsonDerivedType(typeof(FileAttachment), typeDiscriminator: "file")]
[JsonDerivedType(typeof(FileAttachmentImage), typeDiscriminator: "image")]
[JsonDerivedType(typeof(ManagedTranscriptAttachment), typeDiscriminator: "managed_transcript")]
public record FileAttachment(FileAttachmentType Type, string FileName, [property: JsonConverter(typeof(DataDirectoryRelativePathConverter))] string FilePath, long FileSizeBytes)
{
///
/// Gets a value indicating whether the file type is forbidden and should not be attached.
///
///
/// The state is determined once during construction and does not change.
///
public bool IsForbidden { get; } = Type == FileAttachmentType.FORBIDDEN;
///
/// Gets a value indicating whether the file type is valid and allowed to be attached.
///
///
/// The state is determined once during construction and does not change.
///
public bool IsValid { get; } = Type != FileAttachmentType.FORBIDDEN;
///
/// Gets a value indicating whether the file type is an image.
///
///
/// The state is determined once during construction and does not change.
///
public bool IsImage { get; } = Type == FileAttachmentType.IMAGE;
///
/// Gets the file path for loading the file from the web browser-side (Blazor).
///
public string FilePathAsUrl { get; } = FileHandler.CreateFileUrl(FilePath);
///
/// Gets a value indicating whether the file still exists on the file system.
///
///
/// 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.
///
[JsonIgnore]
public bool Exists => File.Exists(this.FilePath);
///
/// Rebuilds the attachment from its current file path so file type detection uses the latest rules.
///
public virtual FileAttachment Normalize() => FromPath(this.FilePath);
///
/// Makes the same attachment for its file at another place, e.g. after the chat holding the file moved.
///
///
/// 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.
///
/// The path of the file now.
/// An attachment of the same kind, with the new path.
public virtual FileAttachment WithFilePath(string filePath) => new(this.Type, this.FileName, filePath, this.FileSizeBytes);
///
/// Creates a FileAttachment from a file path by automatically determining the type,
/// extracting the filename, and reading the file size.
///
/// The full path to the file.
/// A FileAttachment instance with populated properties.
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);
return type switch
{
FileAttachmentType.DOCUMENT => new FileAttachment(type, fileName, filePath, fileSize),
FileAttachmentType.IMAGE => new FileAttachmentImage(fileName, filePath, fileSize),
_ => new FileAttachment(type, fileName, filePath, fileSize),
};
}
///
/// Determines the file attachment type based on the file extension.
/// Uses centrally defined file type filters from .
///
/// The file path to analyze.
/// The corresponding FileAttachmentType.
private static FileAttachmentType DetermineFileType(string filePath)
{
// Check if it's an executable:
if (FileTypes.IsAllowedPath(filePath, FileTypes.EXECUTABLES))
return FileAttachmentType.FORBIDDEN;
// Check if it's an image file:
if (FileTypes.IsAllowedPath(filePath, FileTypes.IMAGE))
return FileAttachmentType.IMAGE;
// Check if it's an audio file:
if (FileTypes.IsAllowedPath(filePath, FileTypes.AUDIO))
return FileAttachmentType.AUDIO;
// Check if it's an allowed document file (PDF, Text, LaTeX, or Office):
if (FileTypes.IsAllowedPath(filePath, FileTypes.DOCUMENT))
return FileAttachmentType.DOCUMENT;
return FileAttachmentType.FORBIDDEN;
}
}