Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/MiniExcel.Core/Helpers/Polyfills.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,19 @@ public static TNumber Clamp<TNumber>(TNumber value, TNumber min, TNumber max) wh
return value;
}
}

[EditorBrowsable(EditorBrowsableState.Advanced)]
public static IEnumerable<TSource> ExceptBy<TSource, TKey>(this IEnumerable<TSource> first, IEnumerable<TKey> second, Func<TSource, TKey> keySelector, IEqualityComparer<TKey>? comparer)
{
var set = new HashSet<TKey>(second, comparer);
foreach (var element in first)
{
if (set.Add(keySelector(element)))
{
yield return element;
}
}
}
#endif

#if !NET10_0_OR_GREATER
Expand Down
72 changes: 71 additions & 1 deletion src/MiniExcel.OpenXml/Api/OpenXmlExporter.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using MiniExcelLib.OpenXml.Writer;

// ReSharper disable once CheckNamespace
namespace MiniExcelLib.OpenXml;

Expand Down Expand Up @@ -46,7 +48,7 @@ public async Task<int> InsertSheetAsync(string path, object value, string sheetN
/// <summary>
/// Inserts a new worksheet into an existing OpenXml document.
/// </summary>
/// <param name="stream">The stream containing the OpenXml document to modify.</param>
/// <param name="stream">The stream containing the OpenXml document to copy.</param>
/// <param name="value">The data object to insert into the new sheet. This can be an enumerable collection of a reference type, a <c>IEnumeable&lt;IDictionary&lt;string, object&gt;&gt;</c>, a <see cref="DataTable" /> or a <see cref="IDataReader"/>.</param>
/// <param name="sheetName">The name to assign to the new worksheet.</param>
/// <param name="printHeader">If <c>true</c>, includes the header row in the new sheet; otherwise, only data rows are written.</param>
Expand Down Expand Up @@ -74,6 +76,74 @@ public async Task<int> InsertSheetAsync(Stream stream, object value, string shee
return await writer.InsertAsync(overwriteSheet, progress, cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Copies an existing OpenXml document and inserts a new worksheet with the provided data.
/// </summary>
/// <param name="inputStream">A readable stream containing the source OpenXml document to copy.</param>
/// <param name="outputStream">A writable stream where the modified OpenXml document will be written.</param>
/// <param name="value">The data object to insert into the new sheet. This can be an enumerable collection of a reference type, a <see cref="DataTable" /> or a <see cref="IDataReader"/>.</param>
/// <param name="sheetName">The name to assign to the new worksheet. Defaults to "Sheet1".</param>
/// <param name="printHeader">If <c>true</c>, includes the header row in the new sheet; otherwise, only data rows are written.</param>
/// <param name="overwriteSheet">If <c>true</c>, overwrites any existing sheet with the same name; otherwise, an exception will be raised if the sheet already exists.</param>
/// <param name="configuration">Optional configuration settings for the insert operation.</param>
/// <param name="progress">Optional progress reporter to track the operation progress. The report value represents the number of cells processed for during the inserting sheet process.</param>
/// <param name="cancellationToken">A cancellation token to monitor for cancellation requests.</param>
/// <returns>The number of rows written to the new sheet.</returns>
/// <remarks>
/// This method requires FastMode to be disabled.
/// </remarks>
[CreateSyncVersion]
public async Task<int> CopyAndAddSheetAsync(Stream inputStream, Stream outputStream, object value, string sheetName = "Sheet1",
bool printHeader = true, bool overwriteSheet = false, OpenXmlConfiguration? configuration = null,
IProgress<int>? progress = null, CancellationToken cancellationToken = default)
{
var writer = await OpenXmlWriter
.CreateForCopyAsync(inputStream, outputStream, value, sheetName, printHeader, configuration, cancellationToken)
.ConfigureAwait(false);

return await writer.CopyAndInsertAsync(overwriteSheet, progress, cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Copies an existing OpenXml document and inserts a new worksheet with the provided data.
/// </summary>
/// <param name="inputFile">The path to the OpenXml document to copy.</param>
/// <param name="outputFile">The path where the OpenXml document will be written.</param>
/// <param name="value">The data object to insert into the new sheet. This can be an enumerable collection of a reference type, a <see cref="DataTable" /> or a <see cref="IDataReader"/>.</param>
/// <param name="sheetName">The name to assign to the new worksheet. Defaults to "Sheet1".</param>
/// <param name="printHeader">If <c>true</c>, includes the header row in the new sheet; otherwise, only data rows are written. Defaults to <c>true</c>.</param>
/// <param name="overwriteFile">If <c>true</c>, overwrites the file at the specified path, otherwise a <see cref="IOException"/> will be raised if the file already exists.</param>
/// <param name="overwriteSheet">If <c>true</c>, overwrites any existing sheet with the same name; otherwise, an exception will be raised if the sheet already exists. Defaults to <c>false</c>.</param>
/// <param name="configuration">Optional configuration settings for the copy and insert operation.</param>
/// <param name="progress">Optional progress reporter to track the operation progress. The report value represents the number of cells processed.</param>
/// <param name="cancellationToken">A cancellation token to monitor for cancellation requests.</param>
/// <returns>The number of rows written to the new sheet.</returns>
/// <remarks>
/// FastMode needs to be disabled for the method to work.
/// </remarks>
/// <exception cref="ArgumentException">Thrown if the input and output paths reference the same file.</exception>
[CreateSyncVersion]
public async Task<int> CopyAndAddSheetAsync(string inputFile, string outputFile, object value, string sheetName = "Sheet1",
bool printHeader = true, bool overwriteFile = false, bool overwriteSheet = false, OpenXmlConfiguration? configuration = null,
IProgress<int>? progress = null, CancellationToken cancellationToken = default)
{
if (inputFile.Equals(outputFile, StringComparison.InvariantCultureIgnoreCase))
throw new ArgumentException("The generated file must not have the same path as the original file.");

#if NET8_0_OR_GREATER
var inputStream = new FileStream(inputFile, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.RandomAccess);
await using var disposableInputStream = inputStream.ConfigureAwait(false);

var outputStream = new FileStream(outputFile, overwriteFile ? FileMode.Create : FileMode.CreateNew, FileAccess.Write, FileShare.None, 4096, FileOptions.SequentialScan);
await using var disposableOutputStream = outputStream.ConfigureAwait(false);
#else
using var inputStream = new FileStream(inputFile, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.RandomAccess);
using var outputStream = new FileStream(outputFile, mode: overwriteFile ? FileMode.Create : FileMode.CreateNew, FileAccess.Write, FileShare.None, 4096, FileOptions.SequentialScan);
#endif

return await CopyAndAddSheetAsync(inputStream, outputStream, value, sheetName, printHeader, overwriteSheet, configuration, progress, cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Exports data to a file as an OpenXml document.
/// </summary>
Expand Down
3 changes: 2 additions & 1 deletion src/MiniExcel.OpenXml/Constants/ExcelFileNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ internal static class ExcelFileNames
internal const string Styles = "xl/styles.xml";
internal const string Workbook = "xl/workbook.xml";
internal const string WorkbookRels = "xl/_rels/workbook.xml.rels";
internal const string Worksheet = "xl/worksheets/sheet";
internal const string WorksheetBase = "xl/worksheets/sheet";

internal static string Worksheet(int sheetId) => $"xl/worksheets/sheet{sheetId}.xml";
internal static string SheetRels(int sheetId) => $"xl/worksheets/_rels/sheet{sheetId}.xml.rels";
internal static string Drawing(int sheetIndex) => $"xl/drawings/drawing{sheetIndex + 1}.xml";
internal static string DrawingRels(int sheetIndex) => $"xl/drawings/_rels/drawing{sheetIndex + 1}.xml.rels";
Expand Down
2 changes: 2 additions & 0 deletions src/MiniExcel.OpenXml/FluentMapping/MappingWriter.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using MiniExcelLib.OpenXml.Writer;

namespace MiniExcelLib.OpenXml.FluentMapping;

internal static partial class MappingWriter<T> where T : class
Expand Down
2 changes: 1 addition & 1 deletion src/MiniExcel.OpenXml/Models/FileDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ internal class FileDto
internal string Extension { get; set; }
internal string Path => $"xl/media/{ID}.{Extension}";
internal string Path2 => $"/xl/media/{ID}.{Extension}";
internal byte[] Byte { get; set; }
internal byte[] Contents { get; set; }
internal int RowIndex { get; set; }
internal int CellIndex { get; set; }
internal bool IsImage { get; set; }
Expand Down
4 changes: 2 additions & 2 deletions src/MiniExcel.OpenXml/OpenXmlReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ private ZipArchiveEntry GetSheetEntry(string? sheetName)
{
// if sheets count > 1 need to read xl/_rels/workbook.xml.rels
var sheets = Archive.EntryCollection
.Where(w => w.FullName.TrimStart('/').StartsWith(ExcelFileNames.Worksheet, StringComparison.OrdinalIgnoreCase))
.Where(w => w.FullName.TrimStart('/').StartsWith(ExcelFileNames.WorksheetBase, StringComparison.OrdinalIgnoreCase))
.ToArray();

ZipArchiveEntry sheetEntry;
Expand Down Expand Up @@ -747,7 +747,7 @@ internal async Task<IList<ExcelRange>> GetDimensionsAsync(CancellationToken canc
var ranges = new List<ExcelRange>();

var sheets = Archive.EntryCollection.Where(e =>
e.FullName.TrimStart('/').StartsWith(ExcelFileNames.Worksheet, StringComparison.OrdinalIgnoreCase));
e.FullName.TrimStart('/').StartsWith(ExcelFileNames.WorksheetBase, StringComparison.OrdinalIgnoreCase));

foreach (var sheet in sheets)
{
Expand Down
82 changes: 28 additions & 54 deletions src/MiniExcel.OpenXml/Utils/SharedStringsDiskCache.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace MiniExcelLib.OpenXml.Utils;

internal class SharedStringsDiskCache : IDictionary<int, string>, IDisposable
internal sealed class SharedStringsDiskCache : IDictionary<int, string>, IDisposable
{
private const int ExcelCellMaxLength = 32767;
private static readonly Encoding Encoding = new UTF8Encoding(true);
Expand Down Expand Up @@ -34,7 +34,7 @@ public SharedStringsDiskCache(string sharedStringsCacheDir)
}

// index must start with 0-N
private void Add(int index, string value)
public void Add(int index, string value)
{
if (index > _maxIndex)
_maxIndex = index;
Expand All @@ -50,6 +50,9 @@ private void Add(int index, string value)

private string GetValue(int index)
{
if (index > _maxIndex)
throw new KeyNotFoundException();

_positionFs.Position = index * 4;
var bytes = new byte[4];
_ = _positionFs.Read(bytes, 0, 4);
Expand All @@ -67,42 +70,39 @@ private string GetValue(int index)
return Encoding.GetString(bytes);
}

public ICollection<int> Keys => throw new NotImplementedException();
public ICollection<string> Values => throw new NotImplementedException();
public ICollection<int> Keys => throw new NotSupportedException();
public ICollection<string> Values => throw new NotSupportedException();
public bool IsReadOnly => throw new NotImplementedException();
public bool Remove(int key)
{
throw new NotImplementedException();
throw new NotSupportedException();
}

public bool TryGetValue(int key, out string value)
{
throw new NotImplementedException();
throw new NotSupportedException();
}

public void Add(KeyValuePair<int, string> item)
{
throw new NotImplementedException();
}
public void Add(KeyValuePair<int, string> item) => Add(item.Key, item.Value);

public void Clear()
{
throw new NotImplementedException();
throw new NotSupportedException();
}

public bool Contains(KeyValuePair<int, string> item)
{
throw new NotImplementedException();
throw new NotSupportedException();
}

public void CopyTo(KeyValuePair<int, string>[] array, int arrayIndex)
{
throw new NotImplementedException();
throw new NotSupportedException();
}

public bool Remove(KeyValuePair<int, string> item)
{
throw new NotImplementedException();
throw new NotSupportedException();
}

public IEnumerator<KeyValuePair<int, string>> GetEnumerator()
Expand All @@ -111,51 +111,25 @@ public IEnumerator<KeyValuePair<int, string>> GetEnumerator()
yield return new KeyValuePair<int, string>(i, this[i]);
}

IEnumerator IEnumerable.GetEnumerator()
{
for (int i = 0; i <= _maxIndex; i++)
yield return this[i];
}

void IDictionary<int, string>.Add(int key, string value)
{
throw new NotImplementedException();
}


~SharedStringsDiskCache()
{
Dispose(disposing: false);
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}

protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
// TODO: dispose managed state (managed objects)
}

_positionFs.Dispose();
if (File.Exists(_positionFs.Name))
File.Delete(_positionFs.Name);
if (_disposedValue)
return;

_positionFs.Dispose();
if (File.Exists(_positionFs.Name))
File.Delete(_positionFs.Name);

_lengthFs.Dispose();
if (File.Exists(_lengthFs.Name))
File.Delete(_lengthFs.Name);
_lengthFs.Dispose();
if (File.Exists(_lengthFs.Name))
File.Delete(_lengthFs.Name);

_valueFs.Dispose();
if (File.Exists(_valueFs.Name))
File.Delete(_valueFs.Name);
_valueFs.Dispose();
if (File.Exists(_valueFs.Name))
File.Delete(_valueFs.Name);

_disposedValue = true;
}
_disposedValue = true;
}
}
Loading
Loading