Files
meeting-assistant/MeetingAssistant/Recording/MeetingRunArtifactCleaner.cs
T

140 lines
5.3 KiB
C#

using System.Text.RegularExpressions;
using MeetingAssistant.MeetingNotes;
namespace MeetingAssistant.Recording;
public interface IMeetingRunArtifactCleaner
{
Task DeleteRunArtifactsAsync(
MeetingSessionArtifacts artifacts,
MeetingAssistantOptions options,
CancellationToken cancellationToken);
}
public sealed class MeetingRunArtifactCleaner : IMeetingRunArtifactCleaner
{
private static readonly Regex MarkdownImageLinkPattern = new("!\\[[^\\]]*\\]\\((?<path>[^)]+)\\)", RegexOptions.Compiled);
private static readonly Regex GeneratedScreenshotFileNamePattern = new(
@"^\d{8}-\d{6}-\d{3}-\d{6}(?:-cropped)?\.png$",
RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
private readonly ILogger<MeetingRunArtifactCleaner>? logger;
public MeetingRunArtifactCleaner(ILogger<MeetingRunArtifactCleaner>? logger = null)
{
this.logger = logger;
}
public async Task DeleteRunArtifactsAsync(
MeetingSessionArtifacts artifacts,
MeetingAssistantOptions options,
CancellationToken cancellationToken)
{
var linkedFiles = await GetLinkedScreenshotAttachmentFilesAsync(
artifacts.AssistantContextPath,
options.Screenshots.AttachmentsFolder,
cancellationToken);
foreach (var linkedFile in linkedFiles)
{
await DeleteFileIfExistsAsync(linkedFile, cancellationToken);
}
await DeleteFileIfExistsAsync(artifacts.SummaryPath, cancellationToken);
await DeleteFileIfExistsAsync(artifacts.AssistantContextPath, cancellationToken);
await DeleteFileIfExistsAsync(artifacts.TranscriptPath, cancellationToken);
await DeleteFileIfExistsAsync(artifacts.MeetingNotePath, cancellationToken);
}
private static async Task<IReadOnlyList<string>> GetLinkedScreenshotAttachmentFilesAsync(
string assistantContextPath,
string configuredAttachmentsFolder,
CancellationToken cancellationToken)
{
if (!File.Exists(assistantContextPath))
{
return [];
}
var contextDirectory = Path.GetDirectoryName(Path.GetFullPath(assistantContextPath));
if (string.IsNullOrWhiteSpace(contextDirectory))
{
return [];
}
var content = await File.ReadAllTextAsync(assistantContextPath, cancellationToken);
var attachmentsFolder = ResolveAttachmentsFolder(contextDirectory, configuredAttachmentsFolder);
return MarkdownImageLinkPattern
.Matches(content)
.Select(match => match.Groups["path"].Value.Trim())
.Where(path => !string.IsNullOrWhiteSpace(path))
.Select(path => ResolveScreenshotAttachmentLink(contextDirectory, attachmentsFolder, path))
.Where(path => path is not null)
.Select(path => path!)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
}
private static string? ResolveScreenshotAttachmentLink(
string contextDirectory,
string attachmentsFolder,
string linkPath)
{
if (Uri.TryCreate(linkPath, UriKind.Absolute, out var uri) && !uri.IsFile)
{
return null;
}
var withoutFragment = linkPath.Split('#')[0].Split('?')[0];
var decoded = Uri.UnescapeDataString(withoutFragment);
var fullPath = Path.IsPathRooted(decoded)
? Path.GetFullPath(decoded)
: Path.GetFullPath(Path.Combine(contextDirectory, decoded));
var attachmentsRoot = Path.GetFullPath(attachmentsFolder).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) +
Path.DirectorySeparatorChar;
var fileName = Path.GetFileName(fullPath);
return fullPath.StartsWith(attachmentsRoot, StringComparison.OrdinalIgnoreCase) &&
GeneratedScreenshotFileNamePattern.IsMatch(fileName)
? fullPath
: null;
}
private static string ResolveAttachmentsFolder(string contextDirectory, string configuredFolder)
{
var expanded = Environment.ExpandEnvironmentVariables(
string.IsNullOrWhiteSpace(configuredFolder) ? "Attachments" : configuredFolder);
return Path.IsPathRooted(expanded)
? Path.GetFullPath(expanded)
: Path.GetFullPath(Path.Combine(contextDirectory, expanded));
}
private async Task DeleteFileIfExistsAsync(string path, CancellationToken cancellationToken)
{
var deadline = DateTimeOffset.UtcNow.AddSeconds(2);
while (true)
{
try
{
if (File.Exists(path))
{
File.Delete(path);
logger?.LogInformation("Deleted aborted meeting artifact {ArtifactPath}", path);
}
return;
}
catch (IOException) when (DateTimeOffset.UtcNow < deadline)
{
await Task.Delay(25, cancellationToken);
}
catch (UnauthorizedAccessException) when (DateTimeOffset.UtcNow < deadline)
{
await Task.Delay(25, cancellationToken);
}
catch (Exception exception)
{
logger?.LogWarning(exception, "Could not delete aborted meeting artifact {ArtifactPath}", path);
return;
}
}
}
}