diff --git a/OfficeFlow.Word.OpenXml.Tests/Packaging/OpenXmlPackageTests.cs b/OfficeFlow.Word.OpenXml.Tests/Packaging/OpenXmlPackageTests.cs new file mode 100644 index 0000000..0f036c4 --- /dev/null +++ b/OfficeFlow.Word.OpenXml.Tests/Packaging/OpenXmlPackageTests.cs @@ -0,0 +1,234 @@ +using System; +using System.IO; +using System.IO.Packaging; +using System.Xml.Linq; +using FluentAssertions; +using OfficeFlow.Word.OpenXml.Packaging; +using Xunit; + +namespace OfficeFlow.Word.OpenXml.Tests.Packaging +{ + public sealed class OpenXmlPackageTests + { + [Fact] + public void Should_throw_exception_if_package_is_disposed() + { + // Arrange + var sut = OpenXmlPackage.Create(); + sut.Dispose(); + + var testCases = new Action[] + { + () => sut.EnumerateParts(), + () => sut.Save(), + () => sut.SaveTo(remoteStream: new MemoryStream()), + () => sut.SaveTo(filePath: nameof(OpenXmlPackage)) + }; + + // Act & Assert + testCases + .Should() + .AllSatisfy(testCase => + testCase + .Should() + .Throw()); + } + + [Fact] + public void Should_create_new_package_properly() + => new Action(() => OpenXmlPackage.Create()) + .Should() + .NotThrow(); + + [Fact] + public void Should_open_package_using_stream_properly() + => new Action(() => + { + using var originalStream = + PrepareTestPackageStream(); + + OpenXmlPackage.Open(originalStream); + }) + .Should() + .NotThrow(); + + [Fact] + public void Should_open_package_using_file_path_properly() + { + // Arrange + using var originalStream = + PrepareTestPackageStream(); + + var filePath = Path.GetTempFileName(); + using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite)) + originalStream.CopyTo(fileStream); + + // Act & Assert + new Action(() => OpenXmlPackage.Open(filePath)) + .Should() + .NotThrow(); + + File.Delete(filePath); + } + + [Fact] + public void Package_parts_should_be_empty_for_new_package() + { + // Arrange + using var sut = + OpenXmlPackage.Create(); + + // Act & Assert + sut.EnumerateParts() + .Should() + .BeEmpty(); + } + + [Fact] + public void Should_enumerate_package_parts_properly() + { + // Arrange + using var originalStream = + PrepareTestPackageStream(); + + using var sut = + OpenXmlPackage.Open(originalStream); + + // Act & Assert + sut.EnumerateParts() + .Should() + .NotBeEmpty(); + } + + [Fact] + public void Should_save_package_using_stream_properly() + { + // Arrange + using var originalStream = + PrepareTestPackageStream(); + + using var sut = + OpenXmlPackage.Open(originalStream); + + // Act + sut.Save(); + + // Assert + originalStream + .Position + .Should() + .Be(originalStream.Length); + } + + [Fact] + public void Should_save_package_using_file_path_properly() + { + // Arrange + using var originalStream = + PrepareTestPackageStream(); + + var filePath = Path.GetTempFileName(); + using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite)) + originalStream.CopyTo(fileStream); + + using var sut = + OpenXmlPackage.Open(filePath); + + // Act + sut.Save(); + + // Assert + originalStream + .Position + .Should() + .Be(originalStream.Length); + + File.Delete(filePath); + } + + [Fact] + public void Should_save_package_to_stream_properly() + { + // Arrange + using var originalStream = + PrepareTestPackageStream(); + + using var destinationStream = + new MemoryStream(); + + using var sut = + OpenXmlPackage.Open(originalStream); + + // Act + sut.SaveTo(destinationStream); + + // Assert + destinationStream + .Length + .Should() + .BePositive(); + } + + [Fact] + public void Should_save_package_to_file_properly() + { + // Arrange + using var originalStream = + PrepareTestPackageStream(); + + var filePath = Path.GetTempFileName(); + + using var sut = + OpenXmlPackage.Open(originalStream); + + // Act + sut.SaveTo(filePath); + + // Assert + File.ReadAllBytes(filePath) + .Length + .Should() + .BePositive(); + + File.Delete(filePath); + } + + private static MemoryStream PrepareTestPackageStream() + { + var partXml = new XElement + ( + "document", + new XElement + ( + "body", + new XText("Content") + ) + ); + + return PrepareTestPackageStream(partXml); + } + + private static MemoryStream PrepareTestPackageStream(XElement partXml) + { + var originalStream = new MemoryStream(); + + using (var package = Package.Open(originalStream, FileMode.Create, FileAccess.ReadWrite)) + { + var partUri = PackUriHelper.CreatePartUri( + new Uri(nameof(OpenXmlPackage), UriKind.Relative)); + + var part = package.CreatePart(partUri, string.Empty); + + using var contentStream = + part.GetStream(); + + using (var contentWriter = new StreamWriter(contentStream)) + partXml.Save(contentWriter); + } + + originalStream.Seek(offset: 0, SeekOrigin.Begin); + + return originalStream; + } + } +} \ No newline at end of file diff --git a/OfficeFlow.Word.OpenXml/Packaging/FlushUsingFilePath.cs b/OfficeFlow.Word.OpenXml/Packaging/FlushUsingFilePath.cs new file mode 100644 index 0000000..7275492 --- /dev/null +++ b/OfficeFlow.Word.OpenXml/Packaging/FlushUsingFilePath.cs @@ -0,0 +1,20 @@ +using System.IO; +using OfficeFlow.Word.OpenXml.Packaging.Interfaces; + +namespace OfficeFlow.Word.OpenXml.Packaging +{ + internal sealed class FlushUsingFilePath : IPackageFlushStrategy + { + private readonly string _filePath; + + public FlushUsingFilePath(string filePath) + => _filePath = filePath; + + /// + public void Flush(MemoryStream internalStream) + { + using var fileStream = new FileStream(_filePath, FileMode.Create, FileAccess.Write); + internalStream.WriteTo(fileStream); + } + } +} \ No newline at end of file diff --git a/OfficeFlow.Word.OpenXml/Packaging/FlushUsingStream.cs b/OfficeFlow.Word.OpenXml/Packaging/FlushUsingStream.cs new file mode 100644 index 0000000..dd54ae9 --- /dev/null +++ b/OfficeFlow.Word.OpenXml/Packaging/FlushUsingStream.cs @@ -0,0 +1,27 @@ +using System.IO; +using OfficeFlow.Word.OpenXml.Packaging.Interfaces; + +namespace OfficeFlow.Word.OpenXml.Packaging +{ + internal sealed class FlushUsingStream : IPackageFlushStrategy + { + private readonly Stream _remoteStream; + + public FlushUsingStream(Stream remoteStream) + => _remoteStream = remoteStream; + + /// + public void Flush(MemoryStream internalStream) + { + if (_remoteStream == internalStream) + { + return; + } + + _remoteStream.SetLength(value: 0); + _remoteStream.Seek(offset: 0, SeekOrigin.Begin); + + internalStream.WriteTo(_remoteStream); + } + } +} \ No newline at end of file diff --git a/OfficeFlow.Word.OpenXml/Packaging/Interfaces/IPackageFlushStrategy.cs b/OfficeFlow.Word.OpenXml/Packaging/Interfaces/IPackageFlushStrategy.cs new file mode 100644 index 0000000..569be14 --- /dev/null +++ b/OfficeFlow.Word.OpenXml/Packaging/Interfaces/IPackageFlushStrategy.cs @@ -0,0 +1,9 @@ +using System.IO; + +namespace OfficeFlow.Word.OpenXml.Packaging.Interfaces +{ + internal interface IPackageFlushStrategy + { + void Flush(MemoryStream internalStream); + } +} \ No newline at end of file diff --git a/OfficeFlow.Word.OpenXml/Packaging/OpenXmlPackage.cs b/OfficeFlow.Word.OpenXml/Packaging/OpenXmlPackage.cs new file mode 100644 index 0000000..69d4cec --- /dev/null +++ b/OfficeFlow.Word.OpenXml/Packaging/OpenXmlPackage.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.Packaging; +using System.Linq; +using OfficeFlow.Word.OpenXml.Packaging.Interfaces; +using OfficeFlow.Word.OpenXml.Packaging.Parts; + +namespace OfficeFlow.Word.OpenXml.Packaging +{ + public sealed class OpenXmlPackage : IDisposable + { + public static OpenXmlPackage Create() + { + var flushStrategy = new PreserveFlush(); + var internalStream = new MemoryStream(); + return Load(flushStrategy, internalStream, FileMode.Create); + } + + public static OpenXmlPackage Open(Stream remoteStream) + { + var flushStrategy = new FlushUsingStream(remoteStream); + + var internalStream = new MemoryStream(); + remoteStream.Seek(offset: 0, SeekOrigin.Begin); + remoteStream.CopyTo(internalStream); + + return Load(flushStrategy, internalStream, FileMode.Open); + } + + public static OpenXmlPackage Open(string filePath) + { + var flushStrategy = new FlushUsingFilePath(filePath); + + using var fileStream = + new FileStream(filePath, FileMode.Open, FileAccess.Read); + + var internalStream = new MemoryStream(); + fileStream.CopyTo(internalStream); + + return Load(flushStrategy, internalStream, FileMode.Open); + } + + private static OpenXmlPackage Load( + IPackageFlushStrategy flushStrategy, + MemoryStream internalStream, + FileMode mode) + => new OpenXmlPackage( + source: Package.Open(internalStream, mode, FileAccess.ReadWrite), + flushStrategy, + internalStream); + + private bool _isDisposed; + private Package _source; + private IPackageFlushStrategy _flushStrategy; + private readonly MemoryStream _internalStream; + + private OpenXmlPackage( + Package source, + IPackageFlushStrategy flushStrategy, + MemoryStream internalStream) + { + _source = source; + _flushStrategy = flushStrategy; + _internalStream = internalStream; + } + + public IEnumerable EnumerateParts() + { + ThrowIfDisposed(); + + return _source + .GetParts() + .Select(OpenXmlPackagePart.Load); + } + + public void Save() + { + ThrowIfDisposed(); + + Debug.WriteIf( + _flushStrategy is PreserveFlush, + $"To save a new package, you must use the {nameof(SaveTo)} method"); + + foreach (var packagePart in EnumerateParts()) + { + packagePart.Flush(); + } + + Flush(); + } + + public void SaveTo(Stream remoteStream) + { + ThrowIfDisposed(); + + SetFlushStrategy( + new FlushUsingStream(remoteStream)); + + Save(); + } + + public void SaveTo(string filePath) + { + ThrowIfDisposed(); + + SetFlushStrategy( + new FlushUsingFilePath(filePath)); + + Save(); + } + + public void Close() + => Dispose(); + + /// + public void Dispose() + { + if (_isDisposed) + { + return; + } + + Save(); + + _source.Close(); + _internalStream.Dispose(); + + _isDisposed = true; + } + + /// https://github.com/dotnet/runtime/issues/24149 + private void Flush() + { + _source.Close(); // Fill _internalStream & close package + + _flushStrategy.Flush(_internalStream); // Save to an external data source (remoteStream or filePath) + + _source = Package.Open(_internalStream, FileMode.Open, FileAccess.ReadWrite); // Reopen package + } + + private void SetFlushStrategy(IPackageFlushStrategy flushStrategy) + => _flushStrategy = flushStrategy; + + private void ThrowIfDisposed() + { + if (_isDisposed) + { + throw new ObjectDisposedException( + nameof(OpenXmlPackage)); + } + } + } +} \ No newline at end of file diff --git a/OfficeFlow.Word.OpenXml/Packaging/Parts/OpenXmlPackagePart.cs b/OfficeFlow.Word.OpenXml/Packaging/Parts/OpenXmlPackagePart.cs new file mode 100644 index 0000000..2c0c498 --- /dev/null +++ b/OfficeFlow.Word.OpenXml/Packaging/Parts/OpenXmlPackagePart.cs @@ -0,0 +1,42 @@ +using System.IO; +using System.IO.Packaging; +using System.Xml.Linq; + +namespace OfficeFlow.Word.OpenXml.Packaging.Parts +{ + public class OpenXmlPackagePart + { + public static OpenXmlPackagePart Load(PackagePart source) + { + using var contentStream = + source.GetStream(FileMode.Open, FileAccess.Read); + + var xml = XDocument.Load(contentStream); + + return new OpenXmlPackagePart(source, xml); + } + + private readonly PackagePart _source; + private readonly XDocument _xml; + + private OpenXmlPackagePart(PackagePart source, XDocument xml) + { + _source = source; + _xml = xml; + } + + public void Flush() + { + using var contentStream = + _source.GetStream(FileMode.Create, FileAccess.Write); + + using var synchronizedStream = + Stream.Synchronized(contentStream); + + using var contentWriter = + new StreamWriter(synchronizedStream); + + _xml.Save(contentWriter, SaveOptions.None); + } + } +} \ No newline at end of file diff --git a/OfficeFlow.Word.OpenXml/Packaging/PreserveFlush.cs b/OfficeFlow.Word.OpenXml/Packaging/PreserveFlush.cs new file mode 100644 index 0000000..ff5bfd3 --- /dev/null +++ b/OfficeFlow.Word.OpenXml/Packaging/PreserveFlush.cs @@ -0,0 +1,12 @@ +using System.IO; +using OfficeFlow.Word.OpenXml.Packaging.Interfaces; + +namespace OfficeFlow.Word.OpenXml.Packaging +{ + internal sealed class PreserveFlush : IPackageFlushStrategy + { + /// + public void Flush(MemoryStream internalStream) + { } + } +} \ No newline at end of file