Public Access
140 lines
5.3 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
}
|