namespace QWERTYkez.ExcelProcessor; /// /// Предоставляет потокобезопасный процессор только для чтения документов Excel (xlsx / xlsm) формата. /// Не поддерживает операции изменения документа. /// 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 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 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(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 write, Action 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 /// /// Получает путь к исходному файлу. /// public string? FilePath { get; protected set; } /// /// Получает значение, указывающее, находится ли процессор в допустимом состоянии для операций. /// public bool IsValid => !_disposed && _doc is not null && _ms is not null; /// /// Получает текущий размер документа в байтах. /// public long DocumentSize => _ms.Length; #endregion #region Read Operations /// /// Находит все уникальные плейсхолдеры в формате $...$ в документе. /// Ищет только внутри параграфов. Игнорирует вхождения, которые пересекают границы параграфов. /// /// Способ сравнения строк при поиске (по умолчанию: без учета регистра) /// Коллекция уникальных найденных плейсхолдеров public ISet 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 }