Skip to content

Commit 72d1f19

Browse files
committed
Stream worksheet updates in OpenXml editor
1 parent c819fc4 commit 72d1f19

1 file changed

Lines changed: 178 additions & 37 deletions

File tree

‎src/MiniExcel.OpenXml/Api/OpenXmlEditor.cs‎

Lines changed: 178 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,7 @@ private async Task SavePathAsync(CancellationToken cancellationToken)
8484
using (var source = new FileStream(_path!, FileMode.Open, FileAccess.Read, FileShare.Read))
8585
using (var temporaryStream = new FileStream(temporaryPath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None))
8686
{
87-
await source.CopyToAsync(temporaryStream, 81920, cancellationToken).ConfigureAwait(false);
88-
temporaryStream.Position = 0;
89-
await ApplyUpdatesAsync(temporaryStream, cancellationToken).ConfigureAwait(false);
87+
await ApplyUpdatesAsync(source, temporaryStream, cancellationToken).ConfigureAwait(false);
9088
await temporaryStream.FlushAsync(cancellationToken).ConfigureAwait(false);
9189
}
9290

@@ -109,9 +107,7 @@ private async Task SaveStreamAsync(Stream stream, CancellationToken cancellation
109107
try
110108
{
111109
using var temporaryStream = new FileStream(temporaryPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
112-
await stream.CopyToAsync(temporaryStream, 81920, cancellationToken).ConfigureAwait(false);
113-
temporaryStream.Position = 0;
114-
await ApplyUpdatesAsync(temporaryStream, cancellationToken).ConfigureAwait(false);
110+
await ApplyUpdatesAsync(stream, temporaryStream, cancellationToken).ConfigureAwait(false);
115111

116112
temporaryStream.Position = 0;
117113
stream.Position = 0;
@@ -125,49 +121,49 @@ private async Task SaveStreamAsync(Stream stream, CancellationToken cancellation
125121
}
126122
}
127123

128-
private async Task ApplyUpdatesAsync(Stream stream, CancellationToken cancellationToken)
124+
private async Task ApplyUpdatesAsync(Stream inputStream, Stream outputStream, CancellationToken cancellationToken)
129125
{
130-
stream.Seek(0, SeekOrigin.Begin);
131-
using var archive = new ZipArchive(stream, ZipArchiveMode.Update, leaveOpen: true);
126+
inputStream.Seek(0, SeekOrigin.Begin);
127+
using var inputArchive = new ZipArchive(inputStream, ZipArchiveMode.Read, leaveOpen: true);
132128

133-
var contentTypes = await LoadDocumentAsync(GetRequiredEntry(archive, ExcelFileNames.ContentTypes), cancellationToken).ConfigureAwait(false);
129+
var contentTypes = await LoadDocumentAsync(GetRequiredEntry(inputArchive, ExcelFileNames.ContentTypes), cancellationToken).ConfigureAwait(false);
134130
if (contentTypes.Descendants().Attributes("ContentType")
135131
.Any(attribute => attribute.Value.IndexOf("macroEnabled", StringComparison.OrdinalIgnoreCase) >= 0))
136132
throw new NotSupportedException("MiniExcel's OpenXml editor does not support the .xlsm format.");
137133

138-
var workbook = await LoadDocumentAsync(GetRequiredEntry(archive, ExcelFileNames.Workbook), cancellationToken).ConfigureAwait(false);
139-
var workbookRelationships = await LoadDocumentAsync(GetRequiredEntry(archive, ExcelFileNames.WorkbookRels), cancellationToken).ConfigureAwait(false);
134+
var workbook = await LoadDocumentAsync(GetRequiredEntry(inputArchive, ExcelFileNames.Workbook), cancellationToken).ConfigureAwait(false);
135+
var workbookRelationships = await LoadDocumentAsync(GetRequiredEntry(inputArchive, ExcelFileNames.WorkbookRels), cancellationToken).ConfigureAwait(false);
140136
var sheets = GetSheets(workbook, workbookRelationships);
141137
var pendingUpdates = ResolveUpdates(sheets);
142138

143-
var stylesEntry = GetRequiredEntry(archive, ExcelFileNames.Styles);
139+
var stylesEntry = GetRequiredEntry(inputArchive, ExcelFileNames.Styles);
144140
var styles = await LoadDocumentAsync(stylesEntry, cancellationToken).ConfigureAwait(false);
145141
var styleContext = new StyleUpdateContext(styles);
146-
var worksheetDocuments = new Dictionary<string, XDocument>(StringComparer.OrdinalIgnoreCase);
142+
var updatesBySheet = pendingUpdates
143+
.GroupBy(update => update.Sheet.Path, StringComparer.OrdinalIgnoreCase)
144+
.ToList();
145+
var worksheetPaths = new HashSet<string>(
146+
updatesBySheet.Select(group => group.Key),
147+
StringComparer.OrdinalIgnoreCase);
147148

148-
foreach (var update in pendingUpdates)
149+
using var outputArchive = new ZipArchive(outputStream, ZipArchiveMode.Create, leaveOpen: true);
150+
foreach (var inputEntry in inputArchive.Entries)
149151
{
150-
cancellationToken.ThrowIfCancellationRequested();
151-
152-
if (!worksheetDocuments.TryGetValue(update.Sheet.Path, out var worksheet))
153-
{
154-
worksheet = await LoadDocumentAsync(GetRequiredEntry(archive, update.Sheet.Path), cancellationToken).ConfigureAwait(false);
155-
worksheetDocuments.Add(update.Sheet.Path, worksheet);
156-
}
152+
if (inputEntry.FullName.Equals(ExcelFileNames.Styles, StringComparison.OrdinalIgnoreCase)
153+
|| worksheetPaths.Contains(inputEntry.FullName))
154+
continue;
157155

158-
var worksheetNamespace = worksheet.Root?.Name.Namespace
159-
?? throw new InvalidDataException($"Worksheet '{update.Sheet.Name}' has no root element.");
160-
var cell = worksheet.Descendants(worksheetNamespace + "c")
161-
.FirstOrDefault(element => string.Equals(element.Attribute("r")?.Value, update.CellReference, StringComparison.OrdinalIgnoreCase))
162-
?? throw new InvalidDataException($"Cell '{update.CellReference}' does not exist in worksheet '{update.Sheet.Name}'.");
156+
await CopyEntryAsync(inputEntry, outputArchive, cancellationToken).ConfigureAwait(false);
157+
}
163158

164-
var originalStyleIndex = ParseStyleIndex(cell.Attribute("s")?.Value, update.CellReference);
165-
cell.SetAttributeValue("s", styleContext.GetStyleIndex(originalStyleIndex, update.FontColor));
159+
foreach (var sheetUpdates in updatesBySheet)
160+
{
161+
var inputEntry = GetRequiredEntry(inputArchive, sheetUpdates.Key);
162+
var outputEntry = CreateEntry(outputArchive, inputEntry);
163+
await RewriteWorksheetAsync(inputEntry, outputEntry, sheetUpdates, styleContext, cancellationToken).ConfigureAwait(false);
166164
}
167165

168-
await ReplaceEntryAsync(archive, ExcelFileNames.Styles, styles, cancellationToken).ConfigureAwait(false);
169-
foreach (var worksheet in worksheetDocuments)
170-
await ReplaceEntryAsync(archive, worksheet.Key, worksheet.Value, cancellationToken).ConfigureAwait(false);
166+
await WriteDocumentEntryAsync(outputArchive, stylesEntry, styles, cancellationToken).ConfigureAwait(false);
171167
}
172168

173169
private static void ReplaceFile(string sourcePath, string destinationPath)
@@ -253,12 +249,157 @@ private static async Task<XDocument> LoadDocumentAsync(ZipArchiveEntry entry, Ca
253249
return await XDocument.LoadAsync(stream, LoadOptions.PreserveWhitespace, cancellationToken).ConfigureAwait(false);
254250
}
255251

256-
private static async Task ReplaceEntryAsync(ZipArchive archive, string path, XDocument document, CancellationToken cancellationToken)
252+
private static async Task CopyEntryAsync(ZipArchiveEntry inputEntry, ZipArchive outputArchive, CancellationToken cancellationToken)
257253
{
258-
archive.GetEntry(path)?.Delete();
259-
var entry = archive.CreateEntry(path, CompressionLevel.Optimal);
260-
using var stream = await entry.OpenAsync(cancellationToken).ConfigureAwait(false);
261-
await document.SaveAsync(stream, SaveOptions.DisableFormatting, cancellationToken).ConfigureAwait(false);
254+
var outputEntry = CreateEntry(outputArchive, inputEntry);
255+
if (inputEntry.FullName.EndsWith("/", StringComparison.Ordinal))
256+
return;
257+
258+
using var inputStream = await inputEntry.OpenAsync(cancellationToken).ConfigureAwait(false);
259+
using var outputStream = await outputEntry.OpenAsync(cancellationToken).ConfigureAwait(false);
260+
await inputStream.CopyToAsync(outputStream, 81920, cancellationToken).ConfigureAwait(false);
261+
}
262+
263+
private static async Task RewriteWorksheetAsync(ZipArchiveEntry inputEntry, ZipArchiveEntry outputEntry,
264+
IEnumerable<ResolvedStyleUpdate> updates, StyleUpdateContext styleContext, CancellationToken cancellationToken)
265+
{
266+
var pendingUpdates = updates.ToDictionary(update => update.CellReference, StringComparer.OrdinalIgnoreCase);
267+
using var inputStream = await inputEntry.OpenAsync(cancellationToken).ConfigureAwait(false);
268+
using var outputStream = await outputEntry.OpenAsync(cancellationToken).ConfigureAwait(false);
269+
using var reader = XmlReader.Create(inputStream, new XmlReaderSettings
270+
{
271+
Async = true,
272+
CloseInput = false,
273+
DtdProcessing = DtdProcessing.Prohibit
274+
});
275+
using var writer = XmlWriter.Create(outputStream, new XmlWriterSettings
276+
{
277+
Async = true,
278+
CloseOutput = false,
279+
Encoding = new UTF8Encoding(false),
280+
Indent = false
281+
});
282+
283+
while (await reader.ReadAsync().ConfigureAwait(false))
284+
{
285+
cancellationToken.ThrowIfCancellationRequested();
286+
287+
if (reader.NodeType == XmlNodeType.Element && reader.LocalName == "c"
288+
&& reader.GetAttribute("r") is { } cellReference
289+
&& pendingUpdates.TryGetValue(cellReference, out var update))
290+
{
291+
var originalStyleIndex = ParseStyleIndex(reader.GetAttribute("s"), cellReference);
292+
var styleIndex = styleContext.GetStyleIndex(originalStyleIndex, update.FontColor);
293+
await WriteCellStartElementAsync(reader, writer, styleIndex).ConfigureAwait(false);
294+
pendingUpdates.Remove(cellReference);
295+
continue;
296+
}
297+
298+
await WriteCurrentNodeAsync(reader, writer).ConfigureAwait(false);
299+
}
300+
301+
if (pendingUpdates.Count > 0)
302+
{
303+
var missingCell = pendingUpdates.Values.OrderBy(update => update.Row).ThenBy(update => update.Column).First();
304+
throw new InvalidDataException($"Cell '{missingCell.CellReference}' does not exist in worksheet '{missingCell.Sheet.Name}'.");
305+
}
306+
307+
await writer.FlushAsync().ConfigureAwait(false);
308+
}
309+
310+
private static async Task WriteCellStartElementAsync(XmlReader reader, XmlWriter writer, int styleIndex)
311+
{
312+
await writer.WriteStartElementAsync(reader.Prefix, reader.LocalName, reader.NamespaceURI).ConfigureAwait(false);
313+
var wroteStyle = false;
314+
if (reader.MoveToFirstAttribute())
315+
{
316+
do
317+
{
318+
if (reader.LocalName == "s" && reader.NamespaceURI.Length == 0)
319+
{
320+
await writer.WriteAttributeStringAsync(null, "s", null, styleIndex.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false);
321+
wroteStyle = true;
322+
}
323+
else
324+
{
325+
await writer.WriteAttributeStringAsync(reader.Prefix, reader.LocalName, reader.NamespaceURI, reader.Value).ConfigureAwait(false);
326+
}
327+
}
328+
while (reader.MoveToNextAttribute());
329+
330+
reader.MoveToElement();
331+
}
332+
333+
if (!wroteStyle)
334+
await writer.WriteAttributeStringAsync(null, "s", null, styleIndex.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false);
335+
336+
if (reader.IsEmptyElement)
337+
await writer.WriteEndElementAsync().ConfigureAwait(false);
338+
}
339+
340+
private static async Task WriteCurrentNodeAsync(XmlReader reader, XmlWriter writer)
341+
{
342+
switch (reader.NodeType)
343+
{
344+
case XmlNodeType.Element:
345+
await writer.WriteStartElementAsync(reader.Prefix, reader.LocalName, reader.NamespaceURI).ConfigureAwait(false);
346+
if (reader.MoveToFirstAttribute())
347+
{
348+
do
349+
{
350+
await writer.WriteAttributeStringAsync(reader.Prefix, reader.LocalName, reader.NamespaceURI, reader.Value).ConfigureAwait(false);
351+
}
352+
while (reader.MoveToNextAttribute());
353+
354+
reader.MoveToElement();
355+
}
356+
if (reader.IsEmptyElement)
357+
await writer.WriteEndElementAsync().ConfigureAwait(false);
358+
break;
359+
case XmlNodeType.EndElement:
360+
await writer.WriteFullEndElementAsync().ConfigureAwait(false);
361+
break;
362+
case XmlNodeType.Text:
363+
await writer.WriteStringAsync(reader.Value).ConfigureAwait(false);
364+
break;
365+
case XmlNodeType.CDATA:
366+
await writer.WriteCDataAsync(reader.Value).ConfigureAwait(false);
367+
break;
368+
case XmlNodeType.Whitespace:
369+
case XmlNodeType.SignificantWhitespace:
370+
await writer.WriteWhitespaceAsync(reader.Value).ConfigureAwait(false);
371+
break;
372+
case XmlNodeType.Comment:
373+
await writer.WriteCommentAsync(reader.Value).ConfigureAwait(false);
374+
break;
375+
case XmlNodeType.ProcessingInstruction:
376+
await writer.WriteProcessingInstructionAsync(reader.Name, reader.Value).ConfigureAwait(false);
377+
break;
378+
case XmlNodeType.XmlDeclaration:
379+
await writer.WriteStartDocumentAsync().ConfigureAwait(false);
380+
break;
381+
case XmlNodeType.DocumentType:
382+
await writer.WriteDocTypeAsync(reader.Name, reader.GetAttribute("PUBLIC"), reader.GetAttribute("SYSTEM"), reader.Value).ConfigureAwait(false);
383+
break;
384+
case XmlNodeType.EntityReference:
385+
await writer.WriteEntityRefAsync(reader.Name).ConfigureAwait(false);
386+
break;
387+
}
388+
}
389+
390+
private static async Task WriteDocumentEntryAsync(ZipArchive outputArchive, ZipArchiveEntry inputEntry,
391+
XDocument document, CancellationToken cancellationToken)
392+
{
393+
var outputEntry = CreateEntry(outputArchive, inputEntry);
394+
using var outputStream = await outputEntry.OpenAsync(cancellationToken).ConfigureAwait(false);
395+
await document.SaveAsync(outputStream, SaveOptions.DisableFormatting, cancellationToken).ConfigureAwait(false);
396+
}
397+
398+
private static ZipArchiveEntry CreateEntry(ZipArchive archive, ZipArchiveEntry sourceEntry)
399+
{
400+
var entry = archive.CreateEntry(sourceEntry.FullName, CompressionLevel.Optimal);
401+
entry.LastWriteTime = sourceEntry.LastWriteTime;
402+
return entry;
262403
}
263404

264405
private sealed class StyleUpdateContext

0 commit comments

Comments
 (0)