namespace AIStudio.Tools.PowerPoint; /// /// Creates a presentation file from a template and a slide deck. /// /// /// The presentation comes about in a copy of the template in the temporary folder, and only the /// complete presentation is streamed into the file the user chose. Nothing is created next to that /// file: inside the Flatpak, the save dialog grants access to exactly the chosen file, and to nothing /// next to it. A failure before the presentation is complete therefore leaves the target untouched, /// and neither the template nor the presentation has to be held in memory. /// internal static class PresentationFile { /// /// The folder below the temporary folder of the system, where the presentations come about. /// private const string TEMPORARY_DIRECTORY = "mindwork-ai-studio-presentations"; /// /// Writes the slide deck into a copy of the template, and the result into the target. /// /// /// The template is read once more rather than reused: it may have changed since the slides were /// planned, and the writer has to see the layouts of the very file it edits. /// /// The path of the template, a .pptx or a .potx. /// The path of the file the user chose. It may exist; the save dialog asked about overwriting it. /// The slide deck, as checked against the template when it was planned. /// What the deck was planned on, as PresentationTemplate.DescribeBasisOf told it then. /// What the user chose for the presentation as a whole. /// Where the pictures the deck refers to come from. /// Where to report a temporary file which cannot be removed. /// The cancellation token. /// Whether the presentation was written, and which pictures it lacks. public static async Task CreateAsync(string templatePath, string targetPath, SlideDeckSpec deck, IReadOnlyDictionary basis, PresentationWriteOptions options, IPictureSource pictures, ILogger logger, CancellationToken token) { var temporaryDirectory = Path.Join(Path.GetTempPath(), TEMPORARY_DIRECTORY); Directory.CreateDirectory(temporaryDirectory); var temporaryPath = Path.Join(temporaryDirectory, $"{Guid.NewGuid():N}.pptx"); try { await using (var templateFile = new FileStream(templatePath, FileMode.Open, FileAccess.Read, PresentationTemplate.FILE_SHARE)) await using (var copy = new FileStream(temporaryPath, FileMode.CreateNew, FileAccess.Write, FileShare.None)) await templateFile.CopyToAsync(copy, token); PresentationWriteResult written; await using (var presentationFile = new FileStream(temporaryPath, FileMode.Open, FileAccess.ReadWrite, FileShare.None)) { var currentTemplate = PresentationTemplate.Read(presentationFile); presentationFile.Position = 0; if (!SlideDeckSpecValidator.StillFits(deck, currentTemplate, basis)) return PresentationFileResult.TEMPLATE_CHANGED; written = await PresentationWriter.WriteAsync(presentationFile, currentTemplate, deck, options, pictures, token); } await CopyIntoTargetAsync(temporaryPath, targetPath, logger, token); return new PresentationFileResult(true, written.SkippedPictures); } finally { DeleteQuietly(temporaryPath, logger); } } /// /// Streams the complete presentation into the file the user chose. /// /// /// The file is written over rather than removed and created anew, which the Flatpak would not /// allow. Once writing has begun, a failure removes the file: a half-written presentation is /// worse than none. A file which cannot even be opened, e.g., because PowerPoint shows it, stays /// as it is. /// private static async Task CopyIntoTargetAsync(string presentationPath, string targetPath, ILogger logger, CancellationToken token) { await using var presentation = new FileStream(presentationPath, FileMode.Open, FileAccess.Read, FileShare.Read); var target = new FileStream(targetPath, FileMode.Create, FileAccess.Write, FileShare.None); try { await using (target) await presentation.CopyToAsync(target, token); } catch { DeleteQuietly(targetPath, logger); throw; } } /// /// Removes a file which is of no use anymore, and only reports when that fails. /// /// /// A copy of the template left behind would pile up in the temporary folder with every attempt. /// private static void DeleteQuietly(string path, ILogger logger) { try { if (File.Exists(path)) File.Delete(path); } catch (Exception e) { logger.LogWarning(e, "The file '{FilePath}' cannot be deleted.", path); } } }