mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-18 21:37:59 +00:00
Added backups of all data, restorable on another computer (#1008)
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
1 parent
ab45580473
commit
936043319c
144 files changed
+10417
-424
No files matched your search
@@ -555,6 +555,53 @@ public sealed record ChatThread
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces every attachment of the chat, in its messages as well as in its draft, with what the given function makes of it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The one place which knows where a chat keeps attachments. Moving a chat, copying it, and
|
||||
/// writing it into a backup or reading it back from one all go through here, so none of them
|
||||
/// forgets the draft or a kind of message.
|
||||
/// </remarks>
|
||||
/// <param name="replace">Gives the attachment to keep in place of the given one, or the given one itself when nothing changes.</param>
|
||||
public void ReplaceAttachments(Func<FileAttachment, FileAttachment> replace)
|
||||
{
|
||||
foreach (var content in this.Blocks.Select(block => block.Content).OfType<IContent>())
|
||||
{
|
||||
for (var index = 0; index < content.FileAttachments.Count; index++)
|
||||
content.FileAttachments[index] = replace(content.FileAttachments[index]);
|
||||
}
|
||||
|
||||
this.Draft.ReplaceAttachments(replace);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces every file path the chat stores with what the given function makes of it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Covers the attachments, cf. ReplaceAttachments, and the images of the messages which are held
|
||||
/// by a local path. An attachment whose path the function returns unchanged stays as it is, and
|
||||
/// so does one without a path.
|
||||
/// </remarks>
|
||||
/// <param name="rewrite">Maps a path to its replacement, or returns it unchanged.</param>
|
||||
public void RewriteFilePaths(Func<string, string> rewrite)
|
||||
{
|
||||
this.ReplaceAttachments(attachment =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(attachment.FilePath))
|
||||
return attachment;
|
||||
|
||||
var rewrittenPath = rewrite(attachment.FilePath);
|
||||
return rewrittenPath == attachment.FilePath ? attachment : attachment.WithFilePath(rewrittenPath);
|
||||
});
|
||||
|
||||
foreach (var image in this.Blocks.Select(block => block.Content).OfType<ContentImage>())
|
||||
{
|
||||
if (image.SourceType is ContentImageSource.LOCAL_PATH && !string.IsNullOrWhiteSpace(image.Source))
|
||||
image.Source = rewrite(image.Source);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds what a provider reported for this conversation, as long as the report still describes it.
|
||||
/// </summary>
|
||||
|
||||
@@ -9,13 +9,13 @@ namespace AIStudio.Chat;
|
||||
/// </summary>
|
||||
/// <param name="Type">The type of the file attachment.</param>
|
||||
/// <param name="FileName">The name of the file, including extension.</param>
|
||||
/// <param name="FilePath">The full path to the file, including the filename and extension.</param>
|
||||
/// <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>
|
||||
/// <param name="FileSizeBytes">The size of the file in bytes.</param>
|
||||
[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, string FilePath, long FileSizeBytes)
|
||||
public record FileAttachment(FileAttachmentType Type, string FileName, [property: JsonConverter(typeof(DataDirectoryRelativePathConverter))] string FilePath, long FileSizeBytes)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the file type is forbidden and should not be attached.
|
||||
@@ -63,6 +63,17 @@ public record FileAttachment(FileAttachmentType Type, string FileName, string Fi
|
||||
/// </summary>
|
||||
public virtual FileAttachment Normalize() => FromPath(this.FilePath);
|
||||
|
||||
/// <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);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a FileAttachment from a file path by automatically determining the type,
|
||||
/// extracting the filename, and reading the file size.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AIStudio.Chat;
|
||||
|
||||
public record FileAttachmentImage(string FileName, string FilePath, long FileSizeBytes) : FileAttachment(FileAttachmentType.IMAGE, FileName, FilePath, FileSizeBytes), IImageSource
|
||||
@@ -13,5 +15,18 @@ public record FileAttachmentImage(string FileName, string FilePath, long FileSiz
|
||||
/// <summary>
|
||||
/// The image source.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// An attached image is a file, so its source is the path of that file. It is not stored a second
|
||||
/// time: the stored file path is relative when the image lies below the data directory, and the
|
||||
/// source has to follow it when the chat is read again, on whatever computer that happens.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public string Source { get; set; } = FilePath;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override FileAttachment WithFilePath(string filePath) => new FileAttachmentImage(this.FileName, filePath, this.FileSizeBytes)
|
||||
{
|
||||
SourceType = this.SourceType,
|
||||
Source = this.SourceType is ContentImageSource.LOCAL_PATH ? filePath : this.Source,
|
||||
};
|
||||
}
|
||||
@@ -15,6 +15,17 @@ namespace AIStudio.Chat;
|
||||
public sealed record ManagedTranscriptAttachment(string FileName, string FilePath, long FileSizeBytes, string OriginalFileName, bool IsStaged)
|
||||
: FileAttachment(FileAttachmentType.DOCUMENT, FileName, FilePath, FileSizeBytes)
|
||||
{
|
||||
/// <summary>The folder within a chat directory which holds the attachments AI Studio made for the chat.</summary>
|
||||
private const string ATTACHMENTS_FOLDER = "attachments";
|
||||
|
||||
/// <summary>The folder within the attachments folder which holds the transcripts.</summary>
|
||||
private const string TRANSCRIPTS_FOLDER = "transcripts";
|
||||
|
||||
/// <summary>Gets the directory which holds the transcripts of a chat.</summary>
|
||||
/// <param name="chatDirectory">The directory of the chat.</param>
|
||||
/// <returns>The transcript directory, which may not exist yet.</returns>
|
||||
public static string GetTranscriptDirectory(string chatDirectory) => Path.Combine(chatDirectory, ATTACHMENTS_FOLDER, TRANSCRIPTS_FOLDER);
|
||||
|
||||
/// <summary>Refreshes the path-derived name and current file size.</summary>
|
||||
public override FileAttachment Normalize()
|
||||
{
|
||||
@@ -22,6 +33,9 @@ public sealed record ManagedTranscriptAttachment(string FileName, string FilePat
|
||||
return this with { FileName = Path.GetFileName(this.FilePath), FileSizeBytes = size };
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override FileAttachment WithFilePath(string filePath) => new ManagedTranscriptAttachment(this.FileName, filePath, this.FileSizeBytes, this.OriginalFileName, this.IsStaged);
|
||||
|
||||
/// <summary>Creates a transcript in an operation-specific staging directory.</summary>
|
||||
/// <param name="originalPath">Original media path.</param>
|
||||
/// <param name="transcript">Provider transcript.</param>
|
||||
@@ -71,7 +85,7 @@ public sealed record ManagedTranscriptAttachment(string FileName, string FilePat
|
||||
var fullDataRoot = Path.GetFullPath(SettingsManager.DataDirectory!);
|
||||
var relative = Path.GetRelativePath(fullDataRoot, fullFilePath);
|
||||
|
||||
if (Path.IsPathRooted(relative) || relative == ".." || relative.StartsWith($"..{Path.DirectorySeparatorChar}", PathComparison))
|
||||
if (Path.IsPathRooted(relative) || relative == ".." || relative.StartsWith($"..{Path.DirectorySeparatorChar}", PathTools.COMPARISON))
|
||||
return false;
|
||||
|
||||
if (fileInfo.LinkTarget is not null || HasLinkedDirectory(fileInfo.Directory, fullDataRoot))
|
||||
@@ -82,16 +96,12 @@ public sealed record ManagedTranscriptAttachment(string FileName, string FilePat
|
||||
&& Guid.TryParseExact(segments[1], "N", out _)
|
||||
&& !string.IsNullOrWhiteSpace(segments[2]);
|
||||
|
||||
var isTemporaryChatTranscript = segments is ["tempChats", _, _, _, _]
|
||||
var isTemporaryChatTranscript = segments is ["tempChats", _, ATTACHMENTS_FOLDER, TRANSCRIPTS_FOLDER, _]
|
||||
&& Guid.TryParse(segments[1], out _);
|
||||
|
||||
var isWorkspaceChatTranscript = segments is ["workspaces", _, _, ATTACHMENTS_FOLDER, TRANSCRIPTS_FOLDER, _]
|
||||
&& Guid.TryParse(segments[1], out _)
|
||||
&& segments[2] == "attachments"
|
||||
&& segments[3] == "transcripts";
|
||||
|
||||
var isWorkspaceChatTranscript = segments is ["workspaces", _, _, _, _, _]
|
||||
&& Guid.TryParse(segments[1], out _)
|
||||
&& Guid.TryParse(segments[2], out _)
|
||||
&& segments[3] == "attachments"
|
||||
&& segments[4] == "transcripts";
|
||||
&& Guid.TryParse(segments[2], out _);
|
||||
|
||||
if (!isStaging && !isTemporaryChatTranscript && !isWorkspaceChatTranscript)
|
||||
return false;
|
||||
@@ -107,7 +117,7 @@ public sealed record ManagedTranscriptAttachment(string FileName, string FilePat
|
||||
/// <summary>Rejects paths traversing any symbolic-link or junction directory below the data root.</summary>
|
||||
private static bool HasLinkedDirectory(DirectoryInfo? directory, string fullDataRoot)
|
||||
{
|
||||
while (directory is not null && !string.Equals(Path.GetFullPath(directory.FullName), fullDataRoot, PathComparison))
|
||||
while (directory is not null && !string.Equals(Path.GetFullPath(directory.FullName), fullDataRoot, PathTools.COMPARISON))
|
||||
{
|
||||
if (directory.LinkTarget is not null)
|
||||
return true;
|
||||
@@ -163,7 +173,4 @@ public sealed record ManagedTranscriptAttachment(string FileName, string FilePat
|
||||
|
||||
await File.WriteAllTextAsync(path, markdown, new UTF8Encoding(false));
|
||||
}
|
||||
|
||||
/// <summary>Gets the platform path comparison used for canonical containment checks.</summary>
|
||||
private static StringComparison PathComparison => OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
|
||||
}
|
||||
Reference in new issue
Block a user