diff --git a/README.md b/README.md index 9c3d022..60e74c9 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,61 @@ # RolandK.Formats.Gpx -### Common Information +## Common Information A .NET Standard library for reading and writing GPX (GPS Exchange Format) files. This library was build for my [GpxViewer](https://github.com/RolandKoenig/GpxViewer) project. It is based on the System.Xml.Serialization.XmlSerializer class and therefore available for .NET Framework and .NET Core projects. -### Build +## Build [![Continuous integration](https://github.com/RolandKoenig/RolandK.Formats.Gpx/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/RolandKoenig/RolandK.Formats.Gpx/actions/workflows/continuous-integration.yml) -### Feature overview +## Feature overview - Full document model for gpx files - Load gpx files - Write gpx files - - Add custom metadata to gpx files - - Don't lose other custom metadate after loading and saving gpx files \ No newline at end of file + - Add custom xml extensions to gpx files + - Don't lose other custom xml extensions information after loading and saving gpx files + +## Samples +### Load GPX file +Load file file (here Kösseine.gpx) and get total count of tracks, routes and waypoints in the file +```csharp +var gpxFile = await GpxFile.LoadAsync("Kösseine.gpx"); + +var countTracks = gpxFile.Tracks.Count; +var countRoutes = gpxFile.Routes.Count; +var countWaypoints = gpxFile.Waypoints.Count; +``` + +### Save GPX file +Save a previously loaded / created / modified file +```csharp +await GpxFile.SaveAsync(gpxFile, "MyFile.gpx"); +``` + +### Add custom xml extension +Your have to define your own xml extension types and give them a namespace +```csharp +[XmlType("MyTrackExtension", Namespace = "http://testing.rolandk.net/")] +public class MyTrackExtension +{ + public bool AlreadyDone { get; set; } = false; +} +``` + +Then you have to register these xml extension types and their namespaces to GpxFile. +You should do this somewhere in your startup code of your application. +```csharp +// You have to register your own xml extension types +GpxFile.RegisterExtensionType(typeof(MyTrackExtension)); +GpxFile.RegisterNamespace("rktest", "http://testing.rolandk.net/"); +``` + +Now you can access these xml extensions from c# code +```csharp +var gpxFile = await GpxFile.LoadAsync(inStream); +var gpxTrack = gpxFile.Tracks[0]; + +gpxTrack.Extensions ??= new GpxExtensions(); +var myTrackExtension = gpxTrack.Extensions.GetOrCreateExtension(); + +myTrackExtension.AlreadyDone = true; // This property comes from our own xml extension +``` \ No newline at end of file diff --git a/src/RolandK.Formats.Gpx.Tests/FileLoad/GpxFileCustomExtensionsTests.cs b/src/RolandK.Formats.Gpx.Tests/FileLoad/GpxFileCustomExtensionsTests.cs new file mode 100644 index 0000000..1fa65d1 --- /dev/null +++ b/src/RolandK.Formats.Gpx.Tests/FileLoad/GpxFileCustomExtensionsTests.cs @@ -0,0 +1,48 @@ +using System.Xml.Serialization; + +namespace RolandK.Formats.Gpx.Tests.FileLoad; + +public class GpxFileCustomExtensionsTests +{ + [Fact] + public async Task AddCustomMetadata() + { + // Arrange + await using var inStream = GpxTestUtilities.ReadFromEmbeddedResource( + typeof(GpxFileLoadTests),"Test_Gpx1_1.gpx"); + + // Act + GpxFile.RegisterExtensionType(typeof(MyTrackExtension)); + GpxFile.RegisterNamespace("rktest", "http://testing.rolandk.net/"); + + var originalGpxFile = await GpxFile.LoadAsync(inStream); + var originalGpxTrack = originalGpxFile.Tracks[0]; + + originalGpxTrack.Extensions ??= new GpxExtensions(); + var myTrackExtension = originalGpxTrack.Extensions.GetOrCreateExtension(); + + myTrackExtension.AlreadyDone = true; + + using var writingMemoryStream = new MemoryStream(1024 * 100); + await GpxFile.SaveAsync(originalGpxFile, writingMemoryStream); + + using var readingMemoryStream = new MemoryStream(writingMemoryStream.GetBuffer()); + var reloadedFile = await GpxFile.LoadAsync(readingMemoryStream); + + // Assert + Assert.Single(reloadedFile.Tracks); + + var reloadedTrack = reloadedFile.Tracks[0]; + Assert.NotNull(reloadedTrack.Extensions); + + var reloadedExtension = reloadedTrack.Extensions.TryGetSingleExtension(); + Assert.NotNull(reloadedExtension); + Assert.True(reloadedExtension.AlreadyDone); + } + + [XmlType("MyTrackExtension", Namespace = "http://testing.rolandk.net/")] + public class MyTrackExtension + { + public bool AlreadyDone { get; set; } + } +} diff --git a/src/RolandK.Formats.Gpx.Tests/FileLoad/GpxFileLoadAsyncTests.cs b/src/RolandK.Formats.Gpx.Tests/FileLoad/GpxFileLoadAsyncTests.cs new file mode 100644 index 0000000..13ae951 --- /dev/null +++ b/src/RolandK.Formats.Gpx.Tests/FileLoad/GpxFileLoadAsyncTests.cs @@ -0,0 +1,107 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace RolandK.Formats.Gpx.Tests.FileLoad; + +[SuppressMessage("ReSharper", "RedundantArgumentDefaultValue")] +public class GpxFileLoadAsyncTests +{ + [Fact] + public async Task GpxVersion1_1_CompatibilityMode() + { + // Arrange + await using var inStream = GpxTestUtilities.ReadFromEmbeddedResource( + typeof(GpxFileLoadTests),"Test_Gpx1_1.gpx"); + + // Act + var gpxFile = await GpxFile.LoadAsync(inStream, GpxFileDeserializationMethod.Compatibility); + + // Assert + Assert.NotNull(gpxFile); + Assert.NotNull(gpxFile.Metadata); + Assert.Equal("Kösseine", gpxFile.Metadata.Name); + Assert.Single(gpxFile.Tracks); + Assert.Single(gpxFile.Tracks[0].Segments); + Assert.Equal(228, gpxFile.Tracks[0].Segments[0].Points.Count); + } + + [Fact] + public async Task GpxVersion1_1_Gpx1_1Mode() + { + // Arrange + await using var inStream = GpxTestUtilities.ReadFromEmbeddedResource( + typeof(GpxFileLoadTests),"Test_Gpx1_1.gpx"); + + // Act + var gpxFile = await GpxFile.LoadAsync(inStream, GpxFileDeserializationMethod.OnlyGpx1_1); + + // Assert + Assert.NotNull(gpxFile); + Assert.NotNull(gpxFile.Metadata); + Assert.Equal("Kösseine", gpxFile.Metadata.Name); + Assert.Single(gpxFile.Tracks); + Assert.Single(gpxFile.Tracks[0].Segments); + Assert.Equal(228, gpxFile.Tracks[0].Segments[0].Points.Count); + } + + [Fact] + public async Task GpxVersion1_1_on_xml_1_1() + { + // Arrange + await using var inStream = GpxTestUtilities.ReadFromEmbeddedResource( + typeof(GpxFileLoadTests),"Test_Gpx1_1_on_xml_1_1.gpx"); + + // Act + var gpxFile = await GpxFile.LoadAsync(inStream, GpxFileDeserializationMethod.Compatibility); + + // Assert + Assert.NotNull(gpxFile); + Assert.NotNull(gpxFile.Metadata); + Assert.Equal("Kösseine", gpxFile.Metadata.Name); + Assert.Single(gpxFile.Tracks); + Assert.Single(gpxFile.Tracks[0].Segments); + Assert.Equal(228, gpxFile.Tracks[0].Segments[0].Points.Count); + } + + [Fact] + public async Task GpxVersion1_0() + { + // Arrange + await using var inStream = GpxTestUtilities.ReadFromEmbeddedResource( + typeof(GpxFileLoadTests),"Test_Gpx1_0.gpx"); + + // Act + var gpxFile = await GpxFile.LoadAsync(inStream, GpxFileDeserializationMethod.Compatibility); + + // Assert + Assert.NotNull(gpxFile); + Assert.NotNull(gpxFile.Metadata); + Assert.Equal("Kösseine", gpxFile.Metadata.Name); + Assert.Single(gpxFile.Tracks); + Assert.Single(gpxFile.Tracks[0].Segments); + Assert.Equal(228, gpxFile.Tracks[0].Segments[0].Points.Count); + } + + [Fact] + public async Task GpxVersion1_0_SaveAs1_1() + { + // Arrange + await using var inStream = GpxTestUtilities.ReadFromEmbeddedResource( + typeof(GpxFileLoadTests),"Test_Gpx1_0.gpx"); + + // Act + var gpxFile = await GpxFile.LoadAsync(inStream, GpxFileDeserializationMethod.Compatibility); + var outStrBuilder = new StringBuilder(33000); + using (var strWriter = new StringWriter(outStrBuilder)) + { + await GpxFile.SaveAsync(gpxFile, strWriter); + } + var writtenFile = outStrBuilder.ToString(); + + // Assert + Assert.True(writtenFile.Contains("version=\"1.1\""), "Version attribute"); + Assert.True(writtenFile.Contains("xmlns=\"http://www.topografix.com/GPX/1/1\""), "Default namespace"); + + Assert.Equal("1.0", gpxFile.Version); + } +} \ No newline at end of file diff --git a/src/RolandK.Formats.Gpx.Tests/FileLoad/GpxFileLoadTests.cs b/src/RolandK.Formats.Gpx.Tests/FileLoad/GpxFileLoadTests.cs index 343f345..2b698d9 100644 --- a/src/RolandK.Formats.Gpx.Tests/FileLoad/GpxFileLoadTests.cs +++ b/src/RolandK.Formats.Gpx.Tests/FileLoad/GpxFileLoadTests.cs @@ -1,84 +1,107 @@ -using System.Text; +using System.Diagnostics.CodeAnalysis; +using System.Text; namespace RolandK.Formats.Gpx.Tests.FileLoad; +[SuppressMessage("ReSharper", "RedundantArgumentDefaultValue")] public class GpxFileLoadTests { [Fact] public void GpxVersion1_1_CompatibilityMode() { + // Arrange using var inStream = GpxTestUtilities.ReadFromEmbeddedResource( typeof(GpxFileLoadTests),"Test_Gpx1_1.gpx"); - var gpxFile = GpxFile.Deserialize(inStream, GpxFileDeserializationMethod.Compatibility); + // Act + var gpxFile = GpxFile.Load(inStream, GpxFileDeserializationMethod.Compatibility); + // Assert Assert.NotNull(gpxFile); Assert.NotNull(gpxFile.Metadata); - Assert.Equal("Kösseine", gpxFile!.Metadata!.Name); + Assert.Equal("Kösseine", gpxFile.Metadata.Name); Assert.Single(gpxFile.Tracks); + Assert.Single(gpxFile.Tracks[0].Segments); + Assert.Equal(228, gpxFile.Tracks[0].Segments[0].Points.Count); } [Fact] public void GpxVersion1_1_Gpx1_1Mode() { + // Arrange using var inStream = GpxTestUtilities.ReadFromEmbeddedResource( typeof(GpxFileLoadTests),"Test_Gpx1_1.gpx"); - var gpxFile = GpxFile.Deserialize(inStream, GpxFileDeserializationMethod.OnlyGpx1_1); + // Act + var gpxFile = GpxFile.Load(inStream, GpxFileDeserializationMethod.OnlyGpx1_1); + // Assert Assert.NotNull(gpxFile); Assert.NotNull(gpxFile.Metadata); - Assert.Equal("Kösseine", gpxFile!.Metadata!.Name); + Assert.Equal("Kösseine", gpxFile.Metadata.Name); Assert.Single(gpxFile.Tracks); + Assert.Single(gpxFile.Tracks[0].Segments); + Assert.Equal(228, gpxFile.Tracks[0].Segments[0].Points.Count); } [Fact] public void GpxVersion1_1_on_xml_1_1() { + // Arrange using var inStream = GpxTestUtilities.ReadFromEmbeddedResource( typeof(GpxFileLoadTests),"Test_Gpx1_1_on_xml_1_1.gpx"); - var gpxFile = GpxFile.Deserialize(inStream, GpxFileDeserializationMethod.Compatibility); + // Act + var gpxFile = GpxFile.Load(inStream, GpxFileDeserializationMethod.Compatibility); + // Assert Assert.NotNull(gpxFile); Assert.NotNull(gpxFile.Metadata); - Assert.Equal("Kösseine", gpxFile!.Metadata!.Name); + Assert.Equal("Kösseine", gpxFile.Metadata.Name); Assert.Single(gpxFile.Tracks); + Assert.Single(gpxFile.Tracks[0].Segments); + Assert.Equal(228, gpxFile.Tracks[0].Segments[0].Points.Count); } [Fact] public void GpxVersion1_0() { + // Arrange using var inStream = GpxTestUtilities.ReadFromEmbeddedResource( typeof(GpxFileLoadTests),"Test_Gpx1_0.gpx"); - var gpxFile = GpxFile.Deserialize(inStream, GpxFileDeserializationMethod.Compatibility); + // Act + var gpxFile = GpxFile.Load(inStream, GpxFileDeserializationMethod.Compatibility); + // Assert Assert.NotNull(gpxFile); Assert.NotNull(gpxFile.Metadata); - Assert.Equal("Kösseine", gpxFile!.Metadata!.Name); + Assert.Equal("Kösseine", gpxFile.Metadata.Name); Assert.Single(gpxFile.Tracks); + Assert.Single(gpxFile.Tracks[0].Segments); + Assert.Equal(228, gpxFile.Tracks[0].Segments[0].Points.Count); } [Fact] public void GpxVersion1_0_SaveAs1_1() { + // Arrange using var inStream = GpxTestUtilities.ReadFromEmbeddedResource( typeof(GpxFileLoadTests),"Test_Gpx1_0.gpx"); - var gpxFile = GpxFile.Deserialize(inStream, GpxFileDeserializationMethod.Compatibility); + // Act + var gpxFile = GpxFile.Load(inStream, GpxFileDeserializationMethod.Compatibility); var outStrBuilder = new StringBuilder(33000); using (var strWriter = new StringWriter(outStrBuilder)) { - GpxFile.Serialize(gpxFile, strWriter); + GpxFile.Save(gpxFile, strWriter); } var writtenFile = outStrBuilder.ToString(); - // Check output + // Assert Assert.True(writtenFile.Contains("version=\"1.1\""), "Version attribute"); Assert.True(writtenFile.Contains("xmlns=\"http://www.topografix.com/GPX/1/1\""), "Default namespace"); - // Check original data Assert.Equal("1.0", gpxFile.Version); } } \ No newline at end of file diff --git a/src/RolandK.Formats.Gpx/GpxFile.cs b/src/RolandK.Formats.Gpx/GpxFile.cs index 2d70ec2..86253e1 100644 --- a/src/RolandK.Formats.Gpx/GpxFile.cs +++ b/src/RolandK.Formats.Gpx/GpxFile.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; +using System.Threading.Tasks; using System.Xml; using System.Xml.Serialization; using RolandK.Formats.Gpx.Metadata; @@ -44,31 +45,6 @@ static GpxFile() s_cachedSerializer = new ConcurrentDictionary(); } - public GpxTrack CreateAndAddDummyTrack(string name, params GpxWaypoint[] waypoints) - { - var gpxTrack = new GpxTrack(); - gpxTrack.Name = name; - this.Tracks.Add(gpxTrack); - - var gpxTrackSegment = new GpxTrackSegment(); - gpxTrack.Segments.Add(gpxTrackSegment); - - gpxTrackSegment.Points.AddRange(waypoints); - - return gpxTrack; - } - - public GpxRoute CreateAndAddDummyRoute(string name, params GpxWaypoint[] waypoints) - { - var gpxRoute = new GpxRoute(); - gpxRoute.Name = name; - this.Routes.Add(gpxRoute); - - gpxRoute.RoutePoints.AddRange(waypoints); - - return gpxRoute; - } - public void EnsureNamespaceDeclarations() { if(s_extensionNamespaces != null) @@ -140,24 +116,64 @@ public static XmlSerializer GetSerializer(GpxVersion version) return result; } - public static void Serialize(GpxFile gpxFile, TextWriter textWriter) + public static void Save(GpxFile gpxFile, TextWriter textWriter) { gpxFile.EnsureNamespaceDeclarations(); + var fileToSave = PrepareGpxFileForSaving(gpxFile); + + GetSerializer(GpxVersion.V1_1).Serialize(textWriter, fileToSave); + } + + public static async Task SaveAsync(GpxFile gpxFile, TextWriter textWriter) + { + gpxFile.EnsureNamespaceDeclarations(); + + var fileToSave = PrepareGpxFileForSaving(gpxFile); + + await Task.Run(() => GetSerializer(GpxVersion.V1_1).Serialize(textWriter, fileToSave)); + } + + public static void Save(GpxFile gpxFile, Stream stream) + { + using var streamWriter = new StreamWriter(stream); + Save(gpxFile, streamWriter); + } + + public static async Task SaveAsync(GpxFile gpxFile, Stream stream) + { + using var streamWriter = new StreamWriter(stream); + await SaveAsync(gpxFile, streamWriter); + } + + public static void Save(GpxFile gpxFile, string targetFile) + { + using var streamWriter = new StreamWriter(File.Create(targetFile)); + Save(gpxFile, streamWriter); + } + + public static async Task SaveAsync(GpxFile gpxFile, string targetFile) + { + using var streamWriter = new StreamWriter(File.Create(targetFile)); + await SaveAsync(gpxFile, streamWriter); + } + + private static GpxFile PrepareGpxFileForSaving(GpxFile originalFile) + { // Copy given GpxFile object to new one to enable some modifications before serializing // The original GpxFile object will not be modified during this process var fileToSave = new GpxFile(); - fileToSave.Waypoints.AddRange(gpxFile.Waypoints); - fileToSave.Extensions = gpxFile.Extensions; - fileToSave.Routes.AddRange(gpxFile.Routes); - fileToSave.Metadata = gpxFile.Metadata; - fileToSave.Tracks.AddRange(gpxFile.Tracks); + fileToSave.Waypoints.AddRange(originalFile.Waypoints); + fileToSave.Extensions = originalFile.Extensions; + fileToSave.Routes.AddRange(originalFile.Routes); + fileToSave.Metadata = originalFile.Metadata; + fileToSave.Tracks.AddRange(originalFile.Tracks); fileToSave.Creator = "RK GpxViewer"; // Force http://www.topografix.com/GPX/1/1 to be default namespace - if (gpxFile.Xmlns != null) + if (originalFile.Xmlns != null) { - var namespaceArray = gpxFile.Xmlns.ToArray(); + var namespaceArray = originalFile.Xmlns.ToArray(); var newNamespaces = new List(namespaceArray.Length); for (var loop = 0; loop < namespaceArray.Length; loop++) { @@ -182,27 +198,55 @@ public static void Serialize(GpxFile gpxFile, TextWriter textWriter) } fileToSave.Version = "1.1"; - // Serialization logic - GetSerializer(GpxVersion.V1_1).Serialize(textWriter, fileToSave); + return fileToSave; } - public static void Serialize(GpxFile gpxFile, Stream stream) + public static GpxFile Load(TextReader textReader, GpxFileDeserializationMethod method = GpxFileDeserializationMethod.Compatibility) { - using(var streamWriter = new StreamWriter(stream)) + switch (method) { - Serialize(gpxFile, streamWriter); - } - } + case GpxFileDeserializationMethod.Compatibility: + // Read whole file to memory to do some checking / manipulations first + // - Correct xml namespace + // - Correct xml version (.Net does not support xml 1.1) + var fullText = textReader.ReadToEnd(); + var gpxVersion = fullText.Contains("xmlns=\"http://www.topografix.com/GPX/1/1\"") ? GpxVersion.V1_1 : GpxVersion.V1_0; - public static void Serialize(GpxFile gpxFile, string targetFile) - { - using(var streamWriter = new StreamWriter(File.Create(targetFile))) - { - Serialize(gpxFile, streamWriter); + using (var strReader = new StringReader(fullText)) + { + // Discard initial xml header + // In this way the XmlSerializer does also try to read xml 1.1 content + if (fullText.StartsWith("", StringComparison.Ordinal); + if (endTagIndex < 0) + { + throw new InvalidOperationException($"Unable to process xml declaration!"); + } + strReader.ReadBlock(new char[endTagIndex + 2], 0, endTagIndex + 2); + } + + // Try to deserialize + if (GetSerializer(gpxVersion).Deserialize(strReader) is not GpxFile result1) + { + throw new GpxFileException($"Unable to deserialize {nameof(GpxFile)}: Unknown error"); + } + return result1; + } + + case GpxFileDeserializationMethod.OnlyGpx1_1: + if (GetSerializer(GpxVersion.V1_1).Deserialize(textReader) is not GpxFile result2) + { + throw new GpxFileException($"Unable to deserialize {nameof(GpxFile)}: Unknown error"); + } + return result2; + + default: + throw new ArgumentException($"Unknown deserialization method {method}", nameof(method)); } } - public static GpxFile Deserialize(TextReader textReader, GpxFileDeserializationMethod method) + public static async Task LoadAsync(TextReader textReader, GpxFileDeserializationMethod method = GpxFileDeserializationMethod.Compatibility) { switch (method) { @@ -210,7 +254,7 @@ public static GpxFile Deserialize(TextReader textReader, GpxFileDeserializationM // Read whole file to memory to do some checking / manipulations first // - Correct xml namespace // - Correct xml version (.Net does not support xml 1.1) - var fullText = textReader.ReadToEnd(); + var fullText = await textReader.ReadToEndAsync().ConfigureAwait(false); var gpxVersion = fullText.Contains("xmlns=\"http://www.topografix.com/GPX/1/1\"") ? GpxVersion.V1_1 : GpxVersion.V1_0; using (var strReader = new StringReader(fullText)) @@ -224,11 +268,13 @@ public static GpxFile Deserialize(TextReader textReader, GpxFileDeserializationM { throw new InvalidOperationException($"Unable to process xml declaration!"); } - strReader.ReadBlock(new char[endTagIndex + 2], 0, endTagIndex + 2); + await strReader.ReadBlockAsync(new char[endTagIndex + 2], 0, endTagIndex + 2); } // Try to deserialize - if (GetSerializer(gpxVersion).Deserialize(strReader) is not GpxFile result1) + var loadedObject1 = await Task.Run(() => GetSerializer(gpxVersion).Deserialize(strReader)) + .ConfigureAwait(false); + if (loadedObject1 is not GpxFile result1) { throw new GpxFileException($"Unable to deserialize {nameof(GpxFile)}: Unknown error"); } @@ -236,7 +282,9 @@ public static GpxFile Deserialize(TextReader textReader, GpxFileDeserializationM } case GpxFileDeserializationMethod.OnlyGpx1_1: - if (GetSerializer(GpxVersion.V1_1).Deserialize(textReader) is not GpxFile result2) + var loadedObject2 = await Task.Run(() => GetSerializer(GpxVersion.V1_1).Deserialize(textReader)) + .ConfigureAwait(false); + if (loadedObject2 is not GpxFile result2) { throw new GpxFileException($"Unable to deserialize {nameof(GpxFile)}: Unknown error"); } @@ -245,21 +293,33 @@ public static GpxFile Deserialize(TextReader textReader, GpxFileDeserializationM default: throw new ArgumentException($"Unknown deserialization method {method}", nameof(method)); } + } + public static GpxFile Load(Stream stream, GpxFileDeserializationMethod method = GpxFileDeserializationMethod.Compatibility) + { + using var streamReader = new StreamReader(stream); + + return Load(streamReader, method); } - public static GpxFile Deserialize(Stream stream, GpxFileDeserializationMethod method) + public static async Task LoadAsync(Stream stream, GpxFileDeserializationMethod method = GpxFileDeserializationMethod.Compatibility) { using var streamReader = new StreamReader(stream); - return Deserialize(streamReader, method); + return await LoadAsync(streamReader, method); + } + + public static GpxFile Load(string sourceFile, GpxFileDeserializationMethod method = GpxFileDeserializationMethod.Compatibility) + { + using var fileStream = File.OpenRead(sourceFile); + return Load(fileStream, method); } - public static GpxFile Deserialize(string sourceFile, GpxFileDeserializationMethod method) + public static async Task LoadAsync(string sourceFile, GpxFileDeserializationMethod method = GpxFileDeserializationMethod.Compatibility) { using var fileStream = File.OpenRead(sourceFile); - return Deserialize(fileStream, method); + return await LoadAsync(fileStream, method); } } \ No newline at end of file