Public Access
Archive meeting workflow and screenshot changes
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user