tgstation-server 6.12.0
The /tg/station 13 server suite
Loading...
Searching...
No Matches
DefaultIOManager.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.IO;
4using System.IO.Compression;
5using System.Linq;
6using System.Threading;
7using System.Threading.Tasks;
8
10
12{
17 {
21 public const string CurrentDirectory = ".";
22
26 public const int DefaultBufferSize = 4096;
27
31 public const TaskCreationOptions BlockingTaskCreationOptions = TaskCreationOptions.None;
32
38 static void NormalizeAndDelete(DirectoryInfo dir, CancellationToken cancellationToken)
39 {
40 cancellationToken.ThrowIfCancellationRequested();
41
42 // check if we are a symbolic link
43 if (!dir.Attributes.HasFlag(FileAttributes.Directory) || dir.Attributes.HasFlag(FileAttributes.ReparsePoint))
44 {
45 dir.Delete();
46 return;
47 }
48
49 List<Exception>? exceptions = null;
50 foreach (var subDir in dir.EnumerateDirectories())
51 try
52 {
53 NormalizeAndDelete(subDir, cancellationToken);
54 }
55 catch (AggregateException ex)
56 {
57 exceptions ??= new List<Exception>();
58 exceptions.AddRange(ex.InnerExceptions);
59 }
60
61 foreach (var file in dir.EnumerateFiles())
62 {
63 cancellationToken.ThrowIfCancellationRequested();
64 try
65 {
66 file.Attributes = FileAttributes.Normal;
67 file.Delete();
68 }
69 catch (Exception ex)
70 {
71 exceptions ??= new List<Exception>();
72 exceptions.Add(ex);
73 }
74 }
75
76 cancellationToken.ThrowIfCancellationRequested();
77 try
78 {
79 dir.Delete(true);
80 }
81 catch (Exception ex)
82 {
83 exceptions ??= new List<Exception>();
84 exceptions.Add(ex);
85 }
86
87 if (exceptions != null)
88 throw new AggregateException(exceptions);
89 }
90
92 public async ValueTask CopyDirectory(
93 IEnumerable<string>? ignore,
94 Func<string, string, ValueTask>? postCopyCallback,
95 string src,
96 string dest,
97 int? taskThrottle,
98 CancellationToken cancellationToken)
99 {
100 ArgumentNullException.ThrowIfNull(src);
101 ArgumentNullException.ThrowIfNull(src);
102
103 if (taskThrottle.HasValue && taskThrottle < 1)
104 throw new ArgumentOutOfRangeException(nameof(taskThrottle), taskThrottle, "taskThrottle must be at least 1!");
105
106 src = ResolvePath(src);
107 dest = ResolvePath(dest);
108
109 using var semaphore = taskThrottle.HasValue ? new SemaphoreSlim(taskThrottle.Value) : null;
110 await Task.WhenAll(CopyDirectoryImpl(src, dest, ignore, postCopyCallback, semaphore, cancellationToken));
111 }
112
114 public string ConcatPath(params string[] paths) => Path.Combine(paths);
115
117 public async ValueTask CopyFile(string src, string dest, CancellationToken cancellationToken)
118 {
119 ArgumentNullException.ThrowIfNull(src);
120 ArgumentNullException.ThrowIfNull(dest);
121
122 // tested to hell and back, these are the optimal buffer sizes
123 await using var srcStream = new FileStream(
124 ResolvePath(src),
125 FileMode.Open,
126 FileAccess.Read,
127 FileShare.Read | FileShare.Delete,
129 FileOptions.Asynchronous | FileOptions.SequentialScan);
130 await using var destStream = CreateAsyncSequentialWriteStream(dest);
131
132 // value taken from documentation
133 await srcStream.CopyToAsync(destStream, 81920, cancellationToken);
134 }
135
137 public Task CreateDirectory(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => Directory.CreateDirectory(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current);
138
140 public Task DeleteDirectory(string path, CancellationToken cancellationToken)
141 {
142 path = ResolvePath(path);
143 var di = new DirectoryInfo(path);
144 if (!di.Exists)
145 return Task.CompletedTask;
146
147 return Task.Factory.StartNew(
148 () => NormalizeAndDelete(di, cancellationToken),
149 cancellationToken,
151 TaskScheduler.Current);
152 }
153
155 public Task DeleteFile(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => File.Delete(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current);
156
158 public Task<bool> FileExists(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => File.Exists(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current);
159
161 public Task<bool> DirectoryExists(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => Directory.Exists(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current);
162
164 public string GetDirectoryName(string path) => Path.GetDirectoryName(path ?? throw new ArgumentNullException(nameof(path)))
165 ?? throw new InvalidOperationException($"Null was returned. Path ({path}) must be rooted. This is not supported!");
166
168 public string GetFileName(string path) => Path.GetFileName(path ?? throw new ArgumentNullException(nameof(path)));
169
171 public string GetFileNameWithoutExtension(string path) => Path.GetFileNameWithoutExtension(path ?? throw new ArgumentNullException(nameof(path)));
172
174 public Task<List<string>> GetFilesWithExtension(string path, string extension, bool recursive, CancellationToken cancellationToken) => Task.Factory.StartNew(
175 () =>
176 {
177 path = ResolvePath(path);
178 ArgumentNullException.ThrowIfNull(extension);
179 var results = new List<string>();
180 foreach (var fileName in Directory.EnumerateFiles(
181 path,
182 $"*.{extension}",
183 recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly))
184 {
185 cancellationToken.ThrowIfCancellationRequested();
186 results.Add(fileName);
187 }
188
189 return results;
190 },
191 cancellationToken,
193 TaskScheduler.Current);
194
196 public Task MoveFile(string source, string destination, CancellationToken cancellationToken) => Task.Factory.StartNew(
197 () =>
198 {
199 ArgumentNullException.ThrowIfNull(destination);
200 source = ResolvePath(source ?? throw new ArgumentNullException(nameof(source)));
201 destination = ResolvePath(destination);
202 File.Move(source, destination);
203 },
204 cancellationToken,
206 TaskScheduler.Current);
207
209 public Task MoveDirectory(string source, string destination, CancellationToken cancellationToken) => Task.Factory.StartNew(
210 () =>
211 {
212 ArgumentNullException.ThrowIfNull(destination);
213 source = ResolvePath(source ?? throw new ArgumentNullException(nameof(source)));
214 destination = ResolvePath(destination);
215 Directory.Move(source, destination);
216 },
217 cancellationToken,
219 TaskScheduler.Current);
220
222 public async ValueTask<byte[]> ReadAllBytes(string path, CancellationToken cancellationToken)
223 {
224 await using var file = CreateAsyncSequentialReadStream(path);
225 byte[] buf;
226 buf = new byte[file.Length];
227 await file.ReadAsync(buf, cancellationToken);
228 return buf;
229 }
230
233
235 public virtual string ResolvePath(string path) => Path.GetFullPath(path ?? throw new ArgumentNullException(nameof(path)));
236
238 public async ValueTask WriteAllBytes(string path, byte[] contents, CancellationToken cancellationToken)
239 {
240 await using var file = CreateAsyncSequentialWriteStream(path);
241 await file.WriteAsync(contents, cancellationToken);
242 }
243
245 public FileStream CreateAsyncSequentialWriteStream(string path)
246 {
247 path = ResolvePath(path);
248 return new FileStream(
249 path,
250 FileMode.Create,
251 FileAccess.Write,
252 FileShare.Read | FileShare.Delete,
254 FileOptions.Asynchronous | FileOptions.SequentialScan);
255 }
256
258 public FileStream CreateAsyncSequentialReadStream(string path)
259 {
260 path = ResolvePath(path);
261 return new FileStream(
262 path,
263 FileMode.Open,
264 FileAccess.Read,
265 FileShare.ReadWrite | FileShare.Delete,
267 FileOptions.Asynchronous | FileOptions.SequentialScan);
268 }
269
271 public Task<IReadOnlyList<string>> GetDirectories(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(
272 () =>
273 {
274 path = ResolvePath(path);
275 var results = new List<string>();
276 cancellationToken.ThrowIfCancellationRequested();
277 foreach (var directoryName in Directory.EnumerateDirectories(path))
278 {
279 results.Add(directoryName);
280 cancellationToken.ThrowIfCancellationRequested();
281 }
282
283 return (IReadOnlyList<string>)results;
284 },
285 cancellationToken,
287 TaskScheduler.Current);
288
290 public Task<IReadOnlyList<string>> GetFiles(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(
291 () =>
292 {
293 path = ResolvePath(path);
294 var results = new List<string>();
295 cancellationToken.ThrowIfCancellationRequested();
296 foreach (var fileName in Directory.EnumerateFiles(path))
297 {
298 results.Add(fileName);
299 cancellationToken.ThrowIfCancellationRequested();
300 }
301
302 return (IReadOnlyList<string>)results;
303 },
304 cancellationToken,
306 TaskScheduler.Current);
307
309 public Task ZipToDirectory(string path, Stream zipFile, CancellationToken cancellationToken) => Task.Factory.StartNew(
310 () =>
311 {
312 path = ResolvePath(path);
313 ArgumentNullException.ThrowIfNull(zipFile);
314
315#if NET9_0_OR_GREATER
316#error Check if zip file seeking has been addressesed. See https://github.com/tgstation/tgstation-server/issues/1531
317#endif
318
319 // ZipArchive does a synchronous copy on unseekable streams we want to avoid
320 if (!zipFile.CanSeek)
321 throw new ArgumentException("Stream does not support seeking!", nameof(zipFile));
322
323 using var archive = new ZipArchive(zipFile, ZipArchiveMode.Read, true);
324 archive.ExtractToDirectory(path);
325 },
326 cancellationToken,
328 TaskScheduler.Current);
329
331 public bool PathContainsParentAccess(string path) => path
332 ?.Split(
333 [
334 Path.DirectorySeparatorChar,
335 Path.AltDirectorySeparatorChar,
336 ])
337 .Any(x => x == "..")
338 ?? throw new ArgumentNullException(nameof(path));
339
341 public Task<DateTimeOffset> GetLastModified(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(
342 () =>
343 {
344 path = ResolvePath(path ?? throw new ArgumentNullException(nameof(path)));
345 var fileInfo = new FileInfo(path);
346 return new DateTimeOffset(fileInfo.LastWriteTimeUtc);
347 },
348 cancellationToken,
350 TaskScheduler.Current);
351
353 public FileStream GetFileStream(string path, bool shareWrite) => new(
354 ResolvePath(path),
355 FileMode.Open,
356 FileAccess.Read,
357 FileShare.Read | FileShare.Delete | (shareWrite ? FileShare.Write : FileShare.None),
359 true);
360
362 public Task<bool> PathIsChildOf(string parentPath, string childPath, CancellationToken cancellationToken) => Task.Factory.StartNew(
363 () =>
364 {
365 parentPath = ResolvePath(parentPath);
366 childPath = ResolvePath(childPath);
367
368 if (parentPath == childPath)
369 return true;
370
371 // https://stackoverflow.com/questions/5617320/given-full-path-check-if-path-is-subdirectory-of-some-other-path-or-otherwise?lq=1
372 var di1 = new DirectoryInfo(parentPath);
373 var di2 = new DirectoryInfo(childPath);
374 while (di2.Parent != null)
375 {
376 if (di2.Parent.FullName == di1.FullName)
377 return true;
378
379 di2 = di2.Parent;
380 }
381
382 return false;
383 },
384 cancellationToken,
386 TaskScheduler.Current);
387
398 IEnumerable<Task> CopyDirectoryImpl(
399 string src,
400 string dest,
401 IEnumerable<string>? ignore,
402 Func<string, string, ValueTask>? postCopyCallback,
403 SemaphoreSlim? semaphore,
404 CancellationToken cancellationToken)
405 {
406 var dir = new DirectoryInfo(src);
407 Task? subdirCreationTask = null;
408 foreach (var subDirectory in dir.EnumerateDirectories())
409 {
410 if (ignore != null && ignore.Contains(subDirectory.Name))
411 continue;
412
413 var checkingSubdirCreationTask = true;
414 foreach (var copyTask in CopyDirectoryImpl(subDirectory.FullName, Path.Combine(dest, subDirectory.Name), null, postCopyCallback, semaphore, cancellationToken))
415 {
416 if (subdirCreationTask == null)
417 {
418 subdirCreationTask = copyTask;
419 yield return subdirCreationTask;
420 }
421 else if (!checkingSubdirCreationTask)
422 yield return copyTask;
423
424 checkingSubdirCreationTask = false;
425 }
426 }
427
428 foreach (var fileInfo in dir.EnumerateFiles())
429 {
430 if (subdirCreationTask == null)
431 {
432 subdirCreationTask = CreateDirectory(dest, cancellationToken);
433 yield return subdirCreationTask;
434 }
435
436 if (ignore != null && ignore.Contains(fileInfo.Name))
437 continue;
438
439 var sourceFile = fileInfo.FullName;
440 var destFile = ConcatPath(dest, fileInfo.Name);
441
442 async Task CopyThisFile()
443 {
444 await subdirCreationTask.WaitAsync(cancellationToken);
445 using var lockContext = semaphore != null
446 ? await SemaphoreSlimContext.Lock(semaphore, cancellationToken)
447 : null;
448 await CopyFile(sourceFile, destFile, cancellationToken);
449 if (postCopyCallback != null)
450 await postCopyCallback(sourceFile, destFile);
451 }
452
453 yield return CopyThisFile();
454 }
455 }
456 }
457}
IIOManager that resolves paths to Environment.CurrentDirectory.
IEnumerable< Task > CopyDirectoryImpl(string src, string dest, IEnumerable< string >? ignore, Func< string, string, ValueTask >? postCopyCallback, SemaphoreSlim? semaphore, CancellationToken cancellationToken)
Copies a directory from src to dest .
async ValueTask< byte[]> ReadAllBytes(string path, CancellationToken cancellationToken)
Returns all the contents of a file at path as a byte array.A ValueTask that results in the contents ...
Task DeleteDirectory(string path, CancellationToken cancellationToken)
Recursively delete a directory, removes and does not enter any symlinks encounterd....
virtual string ResolvePath(string path)
Retrieve the full path of some path given a relative path. Must be used before passing relative path...
Task MoveFile(string source, string destination, CancellationToken cancellationToken)
Moves a file at source to destination .A Task representing the running operation.
string ResolvePath()
Retrieve the full path of the current working directory.The full path of the current working director...
Task CreateDirectory(string path, CancellationToken cancellationToken)
Create a directory at path .A Task representing the running operation.
FileStream CreateAsyncSequentialWriteStream(string path)
Creates an asynchronous FileStream for sequential writing.The open FileStream.
Task< IReadOnlyList< string > > GetDirectories(string path, CancellationToken cancellationToken)
Returns full directory names in a given path .A Task<TResult> resulting in the directories in path .
Task< List< string > > GetFilesWithExtension(string path, string extension, bool recursive, CancellationToken cancellationToken)
Gets a list of files in path with the given extension .A Task resulting in a list of paths to files ...
async ValueTask CopyFile(string src, string dest, CancellationToken cancellationToken)
Copy a file from src to dest .A ValueTask representing the running operation.
Task< IReadOnlyList< string > > GetFiles(string path, CancellationToken cancellationToken)
Returns full file names in a given path .A Task<TResult> resulting in the files in path .
Task< bool > FileExists(string path, CancellationToken cancellationToken)
Check that the file at path exists.A Task<TResult> resulting in true if the file at path exists,...
Task MoveDirectory(string source, string destination, CancellationToken cancellationToken)
Moves a directory at source to destination .A Task representing the running operation.
string GetFileNameWithoutExtension(string path)
Gets the file name portion of a path with.The file name portion of path .
Task ZipToDirectory(string path, Stream zipFile, CancellationToken cancellationToken)
Extract a set of zipFile to a given path .A Task representing the running operation.
async ValueTask WriteAllBytes(string path, byte[] contents, CancellationToken cancellationToken)
Writes some contents to a file at path overwriting previous content.A ValueTask representing the ru...
Task< bool > PathIsChildOf(string parentPath, string childPath, CancellationToken cancellationToken)
Check if a given parentPath is a parent of a given parentPath .A Task<TResult> resulting in true if ...
Task< DateTimeOffset > GetLastModified(string path, CancellationToken cancellationToken)
Get the DateTimeOffset of when a given path was last modified.A Task<TResult> resulting in the DateT...
FileStream GetFileStream(string path, bool shareWrite)
Gets the Stream for a given file path .The FileStream of the file.This function is sychronous.
async ValueTask CopyDirectory(IEnumerable< string >? ignore, Func< string, string, ValueTask >? postCopyCallback, string src, string dest, int? taskThrottle, CancellationToken cancellationToken)
Copies a directory from src to dest .A ValueTask representing the running operation.
const int DefaultBufferSize
Default FileStream buffer size used by .NET.
FileStream CreateAsyncSequentialReadStream(string path)
Creates an asynchronous FileStream for sequential reading.The open FileStream.
const string CurrentDirectory
Path to the current working directory for the IIOManager.
string GetDirectoryName(string path)
Gets the directory portion of a given path .The directory portion of the given path .
bool PathContainsParentAccess(string path)
Check if a path contains the '..' parent directory accessor.true if path contains a '....
string ConcatPath(params string[] paths)
Combines an array of strings into a path.The combined path.
const TaskCreationOptions BlockingTaskCreationOptions
The TaskCreationOptions used to spawn Tasks for potentially long running, blocking operations.
static void NormalizeAndDelete(DirectoryInfo dir, CancellationToken cancellationToken)
Recursively empty a directory.
Task DeleteFile(string path, CancellationToken cancellationToken)
Deletes a file at path .A Task representing the running operation.
string GetFileName(string path)
Gets the file name portion of a path .The file name portion of path .
Task< bool > DirectoryExists(string path, CancellationToken cancellationToken)
Check that the directory at path exists.A Task resulting in true if the directory at path exists,...
static async ValueTask< SemaphoreSlimContext > Lock(SemaphoreSlim semaphore, CancellationToken cancellationToken, ILogger? logger=null)
Asyncronously locks a semaphore .
Interface for using filesystems.
Definition IIOManager.cs:13