-
Notifications
You must be signed in to change notification settings - Fork 0
/
BinaryFileExtensions.cs
166 lines (142 loc) · 7.01 KB
/
BinaryFileExtensions.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
//
// Copyright (c) Roland Pihlakas 2019 - 2022
//
// Roland Pihlakas licenses this file to you under the GNU Lesser General Public License, ver 2.1.
// See the LICENSE file for more information.
//
#define ASYNC
using System;
using System.Data.Linq;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace AsyncToSyncCodeRoundtripSynchroniserMonitor
{
public static partial class FileExtensions
{
public static long MaxByteArraySize = 0x7FFFFFC7; //https://docs.microsoft.com/en-us/dotnet/framework/configure-apps/file-schema/runtime/gcallowverylargeobjects-element?redirectedfrom=MSDN#remarks
//https://stackoverflow.com/questions/18472867/checking-equality-for-two-byte-arrays/
public static bool BinaryEqual(Binary a, Binary b)
{
return a.Equals(b);
}
public static async Task<Tuple<byte[], long>> ReadAllBytesAsync(string path, CancellationToken cancellationToken = default(CancellationToken), long maxFileSize = 0, int retryCount = 0)
{
if (path == null)
throw new ArgumentNullException(nameof(path));
if (path.Length == 0)
throw new ArgumentException("Argument_EmptyPath: {0}", nameof(path));
retryCount = Math.Max(0, retryCount);
for (int i = -1; i < retryCount; i++)
{
if (cancellationToken.IsCancellationRequested)
return await Task.FromCanceled<Tuple<byte[], long>>(cancellationToken);
try
{
using (var stream = new FileStream(
path,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite,
bufferSize: 1024 * 1024,
useAsync: true
))
{
long len = stream.Length; //NB! the length might change during the code execution, so need to save it into separate variable
maxFileSize = Math.Min(MaxByteArraySize, maxFileSize);
if (maxFileSize > 0 && len > maxFileSize)
{
return new Tuple<byte[], long>(null, len);
}
byte[] result = new byte[len];
await stream.ReadAsync(result, 0, (int)len, cancellationToken);
return new Tuple<byte[], long>(result, len);
}
}
catch (Exception ex) when (
ex is IOException
|| ex is UnauthorizedAccessException //can happen when a folder was just created //TODO: abandon retries after a certain number of attempts in this case
)
{
//retry after delay
if (i + 1 < retryCount) //do not sleep after last try
{
#if !NOASYNC
await Task.Delay(1000, cancellationToken); //TODO: config file?
#else
cancellationToken.WaitHandle.WaitOne(1000);
#endif
}
}
}
return new Tuple<byte[], long>(null, -1);
}
public static async Task WriteAllBytesAsync(string path, byte[] contents, bool createTempFileFirst, CancellationToken cancellationToken = default(CancellationToken), int writeBufferKB = 0, int bufferWriteDelayMs = 0)
{
if (path == null)
throw new ArgumentNullException(nameof(path));
if (path.Length == 0)
throw new ArgumentException("Argument_EmptyPath: {0}", nameof(path));
var tempPath = path;
if (createTempFileFirst)
tempPath += ".tmp";
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
using (var stream = new FileStream(
tempPath,
FileMode.OpenOrCreate,
FileAccess.Write,
FileShare.Read,
bufferSize: 1024 * 1024,
useAsync: true
))
{
var writeBufferLength = writeBufferKB * 1024;
if (writeBufferLength <= 0 || bufferWriteDelayMs <= 0) //NB! disable write buffer length limit if delay is 0
writeBufferLength = contents.Length;
for (int i = 0; i < contents.Length; i += writeBufferLength)
{
if (i > 0 && bufferWriteDelayMs > 0)
{
#if !NOASYNC
await Task.Delay(bufferWriteDelayMs, cancellationToken);
#else
cancellationToken.WaitHandle.WaitOne(bufferWriteDelayMs);
#endif
}
await stream.WriteAsync(contents, i, writeBufferLength, cancellationToken);
}
//make the flush operation explicit so it can be async
//This is a use case for IAsyncDisposable as proposed in async streams, a candidate feature for C# 8.0, combined with a using statement enhancement that that proposal didn't cover. C# 7.x and below don't have a "nice" way to do asynchronous dispose, so the implicit Dispose() call from your using statement does the flush synchronously. The workaround is directly calling and awaiting on FlushAsync() so that the synchronous Dispose() doesn't have to synchronously flush.
//https://stackoverflow.com/questions/21987533/streamwriter-uses-synchronous-invocation-when-calling-asynchronous-methods
await stream.FlushAsync(cancellationToken);
}
if (createTempFileFirst)
{
if (await Extensions.FSOperation(() => File.Exists(path), cancellationToken))
{
#pragma warning disable SEC0116 //Warning SEC0116 Unvalidated file paths are passed to a file delete API, which can allow unauthorized file system operations (e.g. read, write, delete) to be performed on unintended server files.
await Extensions.FSOperation(() => File.Delete(path), cancellationToken);
#pragma warning restore SEC0116
}
await Extensions.FSOperation(() => File.Move(tempPath, path), cancellationToken);
}
return; //exit while loop
}
catch (IOException)
{
//retry after delay
#if !NOASYNC
await Task.Delay(1000, cancellationToken); //TODO: config file?
#else
cancellationToken.WaitHandle.WaitOne(1000);
#endif
}
}
}
}
}