Skip to content

Commit

Permalink
feat: support insert sheet (#709)
Browse files Browse the repository at this point in the history
* feat: support insert sheet

* optimize code

* fix: async method bug
  • Loading branch information
izanhzh authored Jan 19, 2025
1 parent 972663f commit 578d98c
Show file tree
Hide file tree
Showing 16 changed files with 2,782 additions and 318 deletions.
25 changes: 15 additions & 10 deletions src/MiniExcel/Csv/CsvWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,21 @@ public void SaveAs()
}
}

public async Task SaveAsAsync(CancellationToken cancellationToken = default)
{
await Task.Run(() => SaveAs(), cancellationToken).ConfigureAwait(false);
}

public void Insert(bool overwriteSheet = false)
{
SaveAs();
}

public async Task InsertAsync(bool overwriteSheet = false, CancellationToken cancellationToken = default)
{
await Task.Run(() => SaveAs(), cancellationToken).ConfigureAwait(false);
}

private void GenerateSheetByIEnumerable(IEnumerable values, string seperator, string newLine, StreamWriter writer)
{
Type genericType = null;
Expand Down Expand Up @@ -130,16 +145,6 @@ private void GenerateSheetByIEnumerable(IEnumerable values, string seperator, st
}
}

public void Insert()
{
SaveAs();
}

public async Task SaveAsAsync(CancellationToken cancellationToken = default(CancellationToken))
{
await Task.Run(() => SaveAs(), cancellationToken).ConfigureAwait(false);
}

private void GenerateSheetByIDataReader(IDataReader reader, string seperator, string newLine, StreamWriter writer)
{
int fieldCount = reader.FieldCount;
Expand Down
5 changes: 3 additions & 2 deletions src/MiniExcel/IExcelWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ namespace MiniExcelLibs
internal interface IExcelWriter
{
void SaveAs();
Task SaveAsAsync(CancellationToken cancellationToken = default(CancellationToken));
void Insert();
Task SaveAsAsync(CancellationToken cancellationToken = default);
void Insert(bool overwriteSheet = false);
Task InsertAsync(bool overwriteSheet = false, CancellationToken cancellationToken = default);
}
}
52 changes: 52 additions & 0 deletions src/MiniExcel/MiniExcel.Async.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,67 @@
namespace MiniExcelLibs
{
using MiniExcelLibs.OpenXml;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Utils;

public static partial class MiniExcel
{
public static async Task InsertAsync(string path, object value, string sheetName = "Sheet1", ExcelType excelType = ExcelType.UNKNOWN, IConfiguration configuration = null, bool printHeader = true, bool overwriteSheet = false, CancellationToken cancellationToken = default)
{
if (Path.GetExtension(path).ToLowerInvariant() == ".xlsm")
throw new NotSupportedException("MiniExcel Insert not support xlsm");

if (!File.Exists(path))
{
await SaveAsAsync(path, value, printHeader, sheetName, excelType, cancellationToken: cancellationToken);
}
else
{
if (excelType == ExcelType.CSV)
{
using (var stream = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.Read, 4096, FileOptions.SequentialScan))
await InsertAsync(stream, value, sheetName, ExcelTypeHelper.GetExcelType(path, excelType), configuration, printHeader, overwriteSheet, cancellationToken);
}
else
{
using (var stream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.Read, 4096, FileOptions.SequentialScan))
await InsertAsync(stream, value, sheetName, ExcelTypeHelper.GetExcelType(path, excelType), configuration, printHeader, overwriteSheet, cancellationToken);
}
}
}

public static async Task InsertAsync(this Stream stream, object value, string sheetName = "Sheet1", ExcelType excelType = ExcelType.XLSX, IConfiguration configuration = null, bool printHeader = true, bool overwriteSheet = false, CancellationToken cancellationToken = default)
{
stream.Seek(0, SeekOrigin.End);
// reuse code
if (excelType == ExcelType.CSV)
{
object v = null;
{
if (!(value is IEnumerable) && !(value is IDataReader) && !(value is IDictionary<string, object>) && !(value is IDictionary))
v = Enumerable.Range(0, 1).Select(s => value);
else
v = value;
}
await ExcelWriterFactory.GetProvider(stream, v, sheetName, excelType, configuration, false).InsertAsync(overwriteSheet);
}
else
{
if (configuration == null)
{
configuration = new OpenXmlConfiguration { FastMode = true };
}
await ExcelWriterFactory.GetProvider(stream, value, sheetName, excelType, configuration, printHeader).InsertAsync(overwriteSheet);
}
}

public static async Task SaveAsAsync(string path, object value, bool printHeader = true, string sheetName = "Sheet1", ExcelType excelType = ExcelType.UNKNOWN, IConfiguration configuration = null, bool overwriteFile = false, CancellationToken cancellationToken = default(CancellationToken))
{
if (Path.GetExtension(path).ToLowerInvariant() == ".xlsm")
Expand Down
56 changes: 39 additions & 17 deletions src/MiniExcel/MiniExcel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,31 +24,53 @@ public static MiniExcelDataReader GetReader(this Stream stream, bool useHeaderRo
return new MiniExcelDataReader(stream, useHeaderRow, sheetName, excelType, startCell, configuration);
}

public static void Insert(string path, object value, string sheetName = "Sheet1", ExcelType excelType = ExcelType.UNKNOWN, IConfiguration configuration = null)
public static void Insert(string path, object value, string sheetName = "Sheet1", ExcelType excelType = ExcelType.UNKNOWN, IConfiguration configuration = null, bool printHeader = true, bool overwriteSheet = false)
{
if (Path.GetExtension(path).ToLowerInvariant() != ".csv")
throw new NotSupportedException("MiniExcel only support csv insert now");
if (Path.GetExtension(path).ToLowerInvariant() == ".xlsm")
throw new NotSupportedException("MiniExcel Insert not support xlsm");

using (var stream = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.Read, 4096, FileOptions.SequentialScan))
Insert(stream, value, sheetName, ExcelTypeHelper.GetExcelType(path, excelType), configuration);
if (!File.Exists(path))
{
SaveAs(path, value, printHeader, sheetName, excelType);
}
else
{
if (excelType == ExcelType.CSV)
{
using (var stream = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.Read, 4096, FileOptions.SequentialScan))
Insert(stream, value, sheetName, ExcelTypeHelper.GetExcelType(path, excelType), configuration, printHeader, overwriteSheet);
}
else
{
using (var stream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.Read, 4096, FileOptions.SequentialScan))
Insert(stream, value, sheetName, ExcelTypeHelper.GetExcelType(path, excelType), configuration, printHeader, overwriteSheet);
}
}
}

public static void Insert(this Stream stream, object value, string sheetName = "Sheet1", ExcelType excelType = ExcelType.XLSX, IConfiguration configuration = null)
public static void Insert(this Stream stream, object value, string sheetName = "Sheet1", ExcelType excelType = ExcelType.XLSX, IConfiguration configuration = null, bool printHeader = true, bool overwriteSheet = false)
{
if (excelType != ExcelType.CSV)
throw new NotSupportedException("MiniExcel only support csv insert now");

stream.Seek(0, SeekOrigin.End);
// reuse code
object v = null;
if (excelType == ExcelType.CSV)
{
if (!(value is IEnumerable) && !(value is IDataReader) && !(value is IDictionary<string, object>) && !(value is IDictionary))
v = Enumerable.Range(0, 1).Select(s => value);
else
v = value;
object v = null;
{
if (!(value is IEnumerable) && !(value is IDataReader) && !(value is IDictionary<string, object>) && !(value is IDictionary))
v = Enumerable.Range(0, 1).Select(s => value);
else
v = value;
}
ExcelWriterFactory.GetProvider(stream, v, sheetName, excelType, configuration, false).Insert(overwriteSheet);
}
else
{
if (configuration == null)
{
configuration = new OpenXmlConfiguration { FastMode = true };
}
ExcelWriterFactory.GetProvider(stream, value, sheetName, excelType, configuration, printHeader).Insert(overwriteSheet);
}

stream.Seek(0, SeekOrigin.End);
ExcelWriterFactory.GetProvider(stream, v, sheetName, excelType, configuration, false).Insert();
}

public static void SaveAs(string path, object value, bool printHeader = true, string sheetName = "Sheet1", ExcelType excelType = ExcelType.UNKNOWN, IConfiguration configuration = null, bool overwriteFile = false)
Expand Down
131 changes: 106 additions & 25 deletions src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.Async.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using MiniExcelLibs.OpenXml.Constants;
using MiniExcelLibs.OpenXml.Models;
using MiniExcelLibs.OpenXml.Styles;
using MiniExcelLibs.Utils;
using MiniExcelLibs.Zip;
using System;
Expand All @@ -15,7 +17,7 @@ namespace MiniExcelLibs.OpenXml
{
internal partial class ExcelOpenXmlSheetWriter : IExcelWriter
{
public async Task SaveAsAsync(CancellationToken cancellationToken = default(CancellationToken))
public async Task SaveAsAsync(CancellationToken cancellationToken = default)
{
await GenerateDefaultOpenXmlAsync(cancellationToken);

Expand All @@ -32,6 +34,67 @@ internal partial class ExcelOpenXmlSheetWriter : IExcelWriter
_archive.Dispose();
}

public async Task InsertAsync(bool overwriteSheet = false, CancellationToken cancellationToken = default)
{
if (!_configuration.FastMode)
{
throw new InvalidOperationException("Insert requires fast mode to be enabled");
}

var sheetRecords = new ExcelOpenXmlSheetReader(_stream, _configuration).GetWorkbookRels(_archive.Entries).ToArray();
foreach (var sheetRecord in sheetRecords.OrderBy(o => o.Id))
{
_sheets.Add(new SheetDto { Name = sheetRecord.Name, SheetIdx = (int)sheetRecord.Id, State = sheetRecord.State });
}
var existSheetDto = _sheets.SingleOrDefault(s => s.Name == _defaultSheetName);
if (existSheetDto != null && !overwriteSheet)
{
throw new Exception($"Sheet “{_defaultSheetName}” already exist");
}

await GenerateStylesXmlAsync(cancellationToken);//GenerateStylesXml必须在校验overwriteSheet之后,避免不必要的样式更改

if (existSheetDto == null)
{
currentSheetIndex = (int)sheetRecords.Max(m => m.Id) + 1;
var insertSheetInfo = GetSheetInfos(_defaultSheetName);
var insertSheetDto = insertSheetInfo.ToDto(currentSheetIndex);
_sheets.Add(insertSheetDto);
await CreateSheetXmlAsync(_value, insertSheetDto.Path, cancellationToken);
}
else
{
currentSheetIndex = existSheetDto.SheetIdx;
_archive.Entries.Single(s => s.FullName == existSheetDto.Path).Delete();
_archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.DrawingRels(currentSheetIndex))?.Delete();
_archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.Drawing(currentSheetIndex))?.Delete();
await CreateSheetXmlAsync(_value, existSheetDto.Path, cancellationToken);
}

await AddFilesToZipAsync(cancellationToken);

await GenerateDrawinRelXmlAsync(currentSheetIndex, cancellationToken);

await GenerateDrawingXmlAsync(currentSheetIndex, cancellationToken);

GenerateWorkBookXmls(out StringBuilder workbookXml, out StringBuilder workbookRelsXml, out Dictionary<int, string> sheetsRelsXml);

foreach (var sheetRelsXml in sheetsRelsXml)
{
var sheetRelsXmlPath = ExcelFileNames.SheetRels(sheetRelsXml.Key);
_archive.Entries.SingleOrDefault(s => s.FullName == sheetRelsXmlPath)?.Delete();
await CreateZipEntryAsync(sheetRelsXmlPath, null, ExcelXml.DefaultSheetRelXml.Replace("{{format}}", sheetRelsXml.Value), cancellationToken);
}

_archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.Workbook)?.Delete();
await CreateZipEntryAsync(ExcelFileNames.Workbook, ExcelContentTypes.Workbook, ExcelXml.DefaultWorkbookXml.Replace("{{sheets}}", workbookXml.ToString()), cancellationToken);

_archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.WorkbookRels)?.Delete();
await CreateZipEntryAsync(ExcelFileNames.WorkbookRels, null, ExcelXml.DefaultWorkbookXmlRels.Replace("{{sheets}}", workbookRelsXml.ToString()), cancellationToken);

_archive.Dispose();
}

internal async Task GenerateDefaultOpenXmlAsync(CancellationToken cancellationToken)
{
await CreateZipEntryAsync(ExcelFileNames.Rels, ExcelContentTypes.Relationships, ExcelXml.DefaultRels, cancellationToken);
Expand Down Expand Up @@ -589,7 +652,7 @@ private async Task WriteColumnsWidthsAsync(MiniExcelAsyncStreamWriter writer, IE
await writer.WriteAsync(WorksheetXml.EndCols);
}

private static async Task PrintHeaderAsync(MiniExcelAsyncStreamWriter writer, List<ExcelColumnInfo> props)
private async Task PrintHeaderAsync(MiniExcelAsyncStreamWriter writer, List<ExcelColumnInfo> props)
{
var xIndex = 1;
var yIndex = 1;
Expand Down Expand Up @@ -658,9 +721,9 @@ private async Task<int> GenerateSheetByColumnInfoAsync<T>(MiniExcelAsyncStreamWr
return yIndex - 1;
}

private static async Task WriteCellAsync(MiniExcelAsyncStreamWriter writer, string cellReference, string columnName)
private async Task WriteCellAsync(MiniExcelAsyncStreamWriter writer, string cellReference, string columnName)
{
await writer.WriteAsync(WorksheetXml.Cell(cellReference, "str", "1", ExcelOpenXmlUtils.EncodeXML(columnName)));
await writer.WriteAsync(WorksheetXml.Cell(cellReference, "str", GetCellXfId("1"), ExcelOpenXmlUtils.EncodeXML(columnName)));
}

private async Task WriteCellAsync(MiniExcelAsyncStreamWriter writer, int rowIndex, int cellIndex, object value, ExcelColumnInfo p, ExcelWidthCollection widthCollection)
Expand All @@ -670,7 +733,7 @@ private async Task WriteCellAsync(MiniExcelAsyncStreamWriter writer, int rowInde

if (_configuration.EnableWriteNullValueCell && valueIsNull)
{
await writer.WriteAsync(WorksheetXml.EmptyCell(columnReference, "2"));
await writer.WriteAsync(WorksheetXml.EmptyCell(columnReference, GetCellXfId("2")));
return;
}

Expand All @@ -697,7 +760,7 @@ private async Task WriteCellAsync(MiniExcelAsyncStreamWriter writer, int rowInde
}
}

await writer.WriteAsync(WorksheetXml.Cell(columnReference, dataType, styleIndex, cellValue, preserveSpace: preserveSpace, columnType: columnType));
await writer.WriteAsync(WorksheetXml.Cell(columnReference, dataType, GetCellXfId(styleIndex), cellValue, preserveSpace: preserveSpace, columnType: columnType));
widthCollection?.AdjustWidth(cellIndex, cellValue);
}

Expand Down Expand Up @@ -727,41 +790,59 @@ private async Task AddFilesToZipAsync(CancellationToken cancellationToken)
/// </summary>
private async Task GenerateStylesXmlAsync(CancellationToken cancellationToken)
{
var styleXml = GetStylesXml(_configuration.DynamicColumns);

await CreateZipEntryAsync(
ExcelFileNames.Styles,
ExcelContentTypes.Styles,
styleXml,
cancellationToken);
using (var context = new SheetStyleBuildContext(_zipDictionary, _archive, _utf8WithBom, _configuration.DynamicColumns))
{
var builder = (ISheetStyleBuilder)null;
switch (_configuration.TableStyles)
{
case TableStyles.None:
builder = new MinimalSheetStyleBuilder(context);
break;
case TableStyles.Default:
builder = new DefaultSheetStyleBuilder(context);
break;
}
var result = await builder.BuildAsync(cancellationToken);
cellXfIdMap = result.CellXfIdMap;
}
}

private async Task GenerateDrawinRelXmlAsync(CancellationToken cancellationToken)
{
for (int sheetIndex = 0; sheetIndex < _sheets.Count; sheetIndex++)
{
var drawing = GetDrawingRelationshipXml(sheetIndex);
await CreateZipEntryAsync(
ExcelFileNames.DrawingRels(sheetIndex),
string.Empty,
ExcelXml.DefaultDrawingXmlRels.Replace("{{format}}", drawing),
cancellationToken);
await GenerateDrawinRelXmlAsync(sheetIndex, cancellationToken);
}
}

private async Task GenerateDrawinRelXmlAsync(int sheetIndex, CancellationToken cancellationToken)
{
var drawing = GetDrawingRelationshipXml(sheetIndex);
await CreateZipEntryAsync(
ExcelFileNames.DrawingRels(sheetIndex),
string.Empty,
ExcelXml.DefaultDrawingXmlRels.Replace("{{format}}", drawing),
cancellationToken);
}

private async Task GenerateDrawingXmlAsync(CancellationToken cancellationToken)
{
for (int sheetIndex = 0; sheetIndex < _sheets.Count; sheetIndex++)
{
var drawing = GetDrawingXml(sheetIndex);
await CreateZipEntryAsync(
ExcelFileNames.Drawing(sheetIndex),
ExcelContentTypes.Drawing,
ExcelXml.DefaultDrawing.Replace("{{format}}", drawing),
cancellationToken);
await GenerateDrawingXmlAsync(sheetIndex, cancellationToken);
}
}

private async Task GenerateDrawingXmlAsync(int sheetIndex, CancellationToken cancellationToken)
{
var drawing = GetDrawingXml(sheetIndex);
await CreateZipEntryAsync(
ExcelFileNames.Drawing(sheetIndex),
ExcelContentTypes.Drawing,
ExcelXml.DefaultDrawing.Replace("{{format}}", drawing),
cancellationToken);
}

/// <summary>
/// workbook.xml 、 workbookRelsXml
/// </summary>
Expand Down
Loading

0 comments on commit 578d98c

Please sign in to comment.