276 lines
7.7 KiB
C#
276 lines
7.7 KiB
C#
|
|
namespace QWERTYkez.ExcelProcessor;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Предоставляет потокобезопасный процессор только для чтения документов Excel (xlsx / xlsm) формата.
|
|||
|
|
/// <para>Не поддерживает операции изменения документа.</para>
|
|||
|
|
/// </summary>
|
|||
|
|
internal class ExcelReader : IDisposable, IExcelReader
|
|||
|
|
{
|
|||
|
|
internal MemoryStream _ms = null!;
|
|||
|
|
internal SpreadsheetDocument _doc = null!;
|
|||
|
|
internal bool _disposed;
|
|||
|
|
internal readonly object _syncLock = new();
|
|||
|
|
internal string? _originalSourcePath;
|
|||
|
|
|
|||
|
|
internal ExcelReader() { }
|
|||
|
|
|
|||
|
|
#region Factory Methods
|
|||
|
|
|
|||
|
|
internal static ExcelReader? CreateInternal(FileInfo sourceFile)
|
|||
|
|
{
|
|||
|
|
if (sourceFile is null || !sourceFile.Exists)
|
|||
|
|
{
|
|||
|
|
#if DEBUG
|
|||
|
|
Debug.WriteLine($"[DEBUG] Source file is null or does not exist: {sourceFile?.FullName}");
|
|||
|
|
#endif
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
MemoryStream? ms = null;
|
|||
|
|
SpreadsheetDocument? doc = null;
|
|||
|
|
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
ms = new MemoryStream();
|
|||
|
|
using (var file = new FileStream(sourceFile.FullName,
|
|||
|
|
FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
|
|||
|
|
{
|
|||
|
|
file.CopyTo(ms);
|
|||
|
|
}
|
|||
|
|
ms.Position = 0;
|
|||
|
|
|
|||
|
|
doc = SpreadsheetDocument.Open(ms, isEditable: false,
|
|||
|
|
new OpenSettings { AutoSave = false });
|
|||
|
|
|
|||
|
|
if (doc is not null)
|
|||
|
|
{
|
|||
|
|
var processor = new ExcelReader
|
|||
|
|
{
|
|||
|
|
_ms = ms,
|
|||
|
|
_doc = doc,
|
|||
|
|
_originalSourcePath = sourceFile.FullName,
|
|||
|
|
FilePath = sourceFile.FullName
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return processor;
|
|||
|
|
}
|
|||
|
|
#if DEBUG
|
|||
|
|
Debug.WriteLine("[DEBUG] Document body is null or empty");
|
|||
|
|
#endif
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
#if DEBUG
|
|||
|
|
Debug.WriteLine($"[DEBUG] Error creating read-only processor: {ex.GetType().Name}: {ex.Message}");
|
|||
|
|
#endif
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
doc?.Dispose();
|
|||
|
|
ms?.Dispose();
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
internal static ExcelReader? CreateFromData(ReadOnlyMemory<byte> data)
|
|||
|
|
{
|
|||
|
|
if (data.IsEmpty)
|
|||
|
|
return null;
|
|||
|
|
|
|||
|
|
var ms = new MemoryStream();
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
ms.Write(data.ToArray(), 0, data.Length);
|
|||
|
|
ms.Position = 0;
|
|||
|
|
|
|||
|
|
var doc = SpreadsheetDocument.Open(ms, false,
|
|||
|
|
new OpenSettings { AutoSave = false });
|
|||
|
|
|
|||
|
|
if (doc is not null)
|
|||
|
|
{
|
|||
|
|
var processor = new ExcelReader
|
|||
|
|
{
|
|||
|
|
_ms = ms,
|
|||
|
|
_doc = doc,
|
|||
|
|
FilePath = null // из памяти – нет файла
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return processor;
|
|||
|
|
}
|
|||
|
|
doc?.Dispose();
|
|||
|
|
}
|
|||
|
|
catch { }
|
|||
|
|
|
|||
|
|
ms?.Dispose();
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region Writers
|
|||
|
|
|
|||
|
|
public bool TryWrite(string destinationPath, Action<IExcelWriter> action)
|
|||
|
|
{
|
|||
|
|
ThrowIfDisposed();
|
|||
|
|
|
|||
|
|
if (string.IsNullOrEmpty(destinationPath))
|
|||
|
|
throw new ArgumentException("Destination path cannot be null or empty", nameof(destinationPath));
|
|||
|
|
if (action is null)
|
|||
|
|
throw new ArgumentNullException(nameof(action));
|
|||
|
|
|
|||
|
|
// Копируем данные из текущего потока
|
|||
|
|
byte[] data;
|
|||
|
|
lock (_syncLock)
|
|||
|
|
{
|
|||
|
|
_ms.Position = 0;
|
|||
|
|
data = _ms.ToArray();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
using var writable = ExcelWriter.CreateFromData(new ReadOnlyMemory<byte>(data), destinationPath);
|
|||
|
|
if (writable is null)
|
|||
|
|
return false;
|
|||
|
|
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
action(writable);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
#if DEBUG
|
|||
|
|
Debug.WriteLine($"[DEBUG] Error in TryWrite action: {ex.Message}");
|
|||
|
|
#endif
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public bool TryWrite(Action<IExcelWriter> write, Action<IExcelReader> read)
|
|||
|
|
{
|
|||
|
|
ThrowIfDisposed();
|
|||
|
|
if (write is null) throw new ArgumentNullException(nameof(write));
|
|||
|
|
if (read is null) throw new ArgumentNullException(nameof(read));
|
|||
|
|
|
|||
|
|
// Копируем текущие данные
|
|||
|
|
byte[] data;
|
|||
|
|
lock (_syncLock)
|
|||
|
|
{
|
|||
|
|
_ms.Position = 0;
|
|||
|
|
data = _ms.ToArray();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ExcelWriter? writable = null;
|
|||
|
|
ExcelReader? resultReader = null;
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// Создаём редактируемую копию (без привязки к файлу)
|
|||
|
|
writable = ExcelWriter.CreateFromData(data);
|
|||
|
|
if (writable is null)
|
|||
|
|
return false;
|
|||
|
|
|
|||
|
|
// Применяем изменения
|
|||
|
|
write(writable);
|
|||
|
|
|
|||
|
|
// Сохраняем изменения в поток и создаём read-only процессор
|
|||
|
|
resultReader = writable.ToReader();
|
|||
|
|
|
|||
|
|
if (resultReader is null)
|
|||
|
|
return false;
|
|||
|
|
|
|||
|
|
// Работаем с изменённой копией
|
|||
|
|
read(resultReader);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
#if DEBUG
|
|||
|
|
Debug.WriteLine($"[DEBUG] Error in TryWrite(write, read): {ex.Message}");
|
|||
|
|
#endif
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
finally
|
|||
|
|
{
|
|||
|
|
// Гарантированно освобождаем созданные процессоры
|
|||
|
|
resultReader?.Dispose();
|
|||
|
|
writable?.Dispose();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region Properties
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Получает путь к исходному файлу.
|
|||
|
|
/// </summary>
|
|||
|
|
public string? FilePath { get; protected set; }
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Получает значение, указывающее, находится ли процессор в допустимом состоянии для операций.
|
|||
|
|
/// </summary>
|
|||
|
|
public bool IsValid => !_disposed && _doc is not null && _ms is not null;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Получает текущий размер документа в байтах.
|
|||
|
|
/// </summary>
|
|||
|
|
public long DocumentSize => _ms.Length;
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region Read Operations
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Находит все уникальные плейсхолдеры в формате $...$ в документе.
|
|||
|
|
/// <para>Ищет только внутри параграфов. Игнорирует вхождения, которые пересекают границы параграфов.</para>
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="comparisonType">Способ сравнения строк при поиске (по умолчанию: без учета регистра)</param>
|
|||
|
|
/// <returns>Коллекция уникальных найденных плейсхолдеров</returns>
|
|||
|
|
public ISet<string> FindPlaceholders()
|
|||
|
|
{
|
|||
|
|
ThrowIfDisposed();
|
|||
|
|
|
|||
|
|
lock (_syncLock)
|
|||
|
|
{
|
|||
|
|
return PlaceholderFinder.FindInDocument(_doc);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region Dispose Pattern
|
|||
|
|
|
|||
|
|
internal void ThrowIfDisposed()
|
|||
|
|
{
|
|||
|
|
if (_disposed)
|
|||
|
|
throw new ObjectDisposedException(GetType().Name);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
protected virtual void Dispose(bool disposing)
|
|||
|
|
{
|
|||
|
|
if (_disposed)
|
|||
|
|
return;
|
|||
|
|
|
|||
|
|
lock (_syncLock)
|
|||
|
|
{
|
|||
|
|
if (disposing)
|
|||
|
|
{
|
|||
|
|
_doc.Dispose();
|
|||
|
|
_ms.Dispose();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_doc = null!;
|
|||
|
|
_ms = null!;
|
|||
|
|
_disposed = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void Dispose()
|
|||
|
|
{
|
|||
|
|
Dispose(disposing: true);
|
|||
|
|
GC.SuppressFinalize(this);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
~ExcelReader()
|
|||
|
|
{
|
|||
|
|
Dispose(disposing: false);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
}
|