/
docNemo
/
clothes-graph
Обзор
Документация
Войти
/
docNemo
/
clothes-graph
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
src/ClothesGraph.Model/Storage/PatternFile.cs
204 строки
8 KB
docNemo
Вкладка деталей выкройки, создание документа и пересчёт производных вкладок
09 авг 2026, 14:03
09 авг 2026, 14:03
dd71293
Код
Авторство
О чём код?
using System; using System.IO; using System.IO.Compression; using System.Text.Json; namespace ClothesGraph.Model.Storage; /// <summary> /// Чтение и запись файла проекта. /// </summary> /// <remarks> /// Контейнер — обычный архив с описанием документа внутри. Выбран из-за /// подложенных изображений: в тексте их пришлось бы кодировать, раздувая файл /// и заставляя держать картинку в памяти строкой. Архив хранит их как есть и /// заодно сжимает описание. /// </remarks> public static class PatternFile { private static readonly JsonSerializerOptions Options = new() { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull }; /// <summary> /// Сохраняет документ. Прерывание записи не разрушает прежний файл. /// </summary> /// <remarks> /// Запись идёт во временный файл рядом с целевым, и лишь готовый результат /// занимает его место. Прямая запись поверх означала бы, что сбой питания /// посреди сохранения уничтожает и новую версию, и старую. /// </remarks> public static void Save(PatternDocument document, string path) { var directory = Path.GetDirectoryName(Path.GetFullPath(path)); if (!string.IsNullOrEmpty(directory)) Directory.CreateDirectory(directory); var temporary = path + ".tmp-" + Guid.NewGuid().ToString("N")[..8]; try { using (var stream = new FileStream(temporary, FileMode.Create, FileAccess.Write)) Write(document, stream); if (File.Exists(path)) { // Замена существующего файла одной операцией: промежуточного // состояния, в котором файла нет вовсе, не возникает. File.Replace(temporary, path, destinationBackupFileName: null); } else { File.Move(temporary, path); } // Пометка снимается здесь, а не в вызывающем коде: сохранение // бывает не одно, и забыть о ней означало бы предложить сохранить // документ, который только что записан. document.MarkSaved(); } finally { if (File.Exists(temporary)) TryDelete(temporary); } } public static void Write(PatternDocument document, Stream stream) { using var archive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: true); var entry = archive.CreateEntry(FileFormat.DocumentEntry, CompressionLevel.Optimal); using (var entryStream = entry.Open()) JsonSerializer.Serialize(entryStream, DocumentMapper.ToDto(document), Options); // Изображения кладутся как есть и без сжатия: они уже сжаты своим // форматом, и повторное только тратило бы время. foreach (var underlay in document.Underlays) { var imageEntry = archive.CreateEntry( DocumentMapper.EntryFor(underlay), CompressionLevel.NoCompression); using var imageStream = imageEntry.Open(); imageStream.Write(underlay.Content); } } public static PatternDocument Load(string path) { if (!File.Exists(path)) throw new FileNotFoundException("Файл проекта не найден", path); using var stream = File.OpenRead(path); return Read(stream); } public static PatternDocument Read(Stream stream) { ZipArchive archive; try { archive = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen: true); } catch (InvalidDataException error) { throw new InvalidPatternFileException( "Файл повреждён или не является проектом выкройки", error); } using (archive) { var entry = archive.GetEntry(FileFormat.DocumentEntry) ?? throw new InvalidPatternFileException( "В файле нет описания документа"); DocumentDto? dto; using (var entryStream = entry.Open()) { try { dto = JsonSerializer.Deserialize<DocumentDto>(entryStream, Options); } catch (JsonException error) { throw new InvalidPatternFileException( "Описание документа повреждено", error); } } if (dto is null) throw new InvalidPatternFileException("Описание документа пусто"); // Файл более новой версии не открывается даже частично: пользователь // сохранил бы его обратно и потерял всё, чего эта версия не поняла. if (dto.FormatVersion > FileFormat.CurrentVersion) throw new UnsupportedFileVersionException( dto.FormatVersion, FileFormat.CurrentVersion); if (dto.FormatVersion < FileFormat.MinimumSupportedVersion) throw new InvalidPatternFileException( $"Формат версии {dto.FormatVersion} больше не поддерживается"); Migrate(dto); var document = DocumentMapper.ToDocument(dto); DocumentMapper.RestoreUnderlays(document, dto, name => { var imageEntry = archive.GetEntry(name); if (imageEntry is null) return null; using var imageStream = imageEntry.Open(); using var buffer = new MemoryStream(); imageStream.CopyTo(buffer); return buffer.ToArray(); }); return document; } } /// <summary> /// Приводит описание к текущей версии формата. /// </summary> /// <remarks> /// Пока преобразовывать нечего — версия одна. Точка расширения заведена /// заранее: когда формат изменится, пользовательские файлы уже разойдутся, /// и добавлять преобразование будет поздно. /// </remarks> private static void Migrate(DocumentDto dto) { while (dto.FormatVersion < FileFormat.CurrentVersion) { switch (dto.FormatVersion) { default: throw new InvalidPatternFileException( $"Нет преобразования из версии формата {dto.FormatVersion}"); } } } private static void TryDelete(string path) { try { File.Delete(path); } catch (IOException) { // Временный файл не удалось убрать: это не повод рушить сохранение, // которое уже прошло успешно. } } }