AI-Studio/app/MindWork AI Studio/Tools/ChatArchive/ChatArchiveAttachmentPaths.cs
j-erler a8e8edc028 Added data backup and restore for chats
The settings gain a data backup panel. It writes the chats of the chosen
workspaces, and optionally the temporary chats, into a single archive file
with the extension .aistudio-chats, and it imports such archives again.
The archive mirrors the chat storage, so restoring needs no conversion.

Documents the user attached are never copied into the archive. The chats
keep the absolute path of each document, which keeps them usable wherever
the documents live and avoids a second copy. Files AI Studio created itself,
such as transcripts, exist nowhere else; when the selection holds any, the
user decides whether they travel with the archive.

On import, chats which already exist, in any workspace, are either kept or
imported as additional chats with a new ID. Workspaces are merged by their
identity. An imported chat gets its own copy of every transcript the archive
did not carry, as a copied chat does, so deleting it never removes the
transcripts of the chat it came from.

An existing archive is only replaced once the new one is complete. Chats
which could not be read are reported instead of missing silently. Both
directions can be stopped at any time and report what was written.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 16:25:39 +02:00

63 lines
2.5 KiB
C#

using AIStudio.Chat;
namespace AIStudio.Tools.ChatArchive;
/// <summary>
/// Visits every file path stored inside a chat thread. Export and import use this to
/// translate between absolute paths on this machine and archive-relative paths.
/// </summary>
public static class ChatArchiveAttachmentPaths
{
/// <summary>
/// Replaces every file path of the given chat thread by the result of the rewrite function.
/// Paths which the rewrite function returns unchanged stay untouched.
/// </summary>
/// <param name="chat">The chat thread to process. It is modified in place.</param>
/// <param name="rewrite">Maps an existing path to its replacement.</param>
public static void Rewrite(ChatThread chat, Func<string, string> rewrite)
{
foreach (var block in chat.Blocks)
{
if (block.Content is not { } content)
continue;
RewriteAttachments(content.FileAttachments, rewrite);
// Images might reference a file on this machine as well:
if (content is ContentImage { SourceType: ContentImageSource.LOCAL_PATH } image && !string.IsNullOrWhiteSpace(image.Source))
image.Source = rewrite(image.Source);
}
// Transcripts which were prepared for the composer but not sent yet:
for (var index = 0; index < chat.PendingMediaTranscripts.Count; index++)
{
var transcript = chat.PendingMediaTranscripts[index];
if (string.IsNullOrWhiteSpace(transcript.FilePath))
continue;
chat.PendingMediaTranscripts[index] = transcript with { FilePath = rewrite(transcript.FilePath) };
}
}
private static void RewriteAttachments(List<FileAttachment> attachments, Func<string, string> rewrite)
{
for (var index = 0; index < attachments.Count; index++)
{
var attachment = attachments[index];
if (string.IsNullOrWhiteSpace(attachment.FilePath))
continue;
var rewrittenPath = rewrite(attachment.FilePath);
if (rewrittenPath == attachment.FilePath)
continue;
attachments[index] = attachment switch
{
ManagedTranscriptAttachment managed => managed with { FilePath = rewrittenPath },
FileAttachmentImage image => image with { FilePath = rewrittenPath },
_ => attachment with { FilePath = rewrittenPath },
};
}
}
}