tgstation-server 6.19.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.Abstractions;
5using System.IO.Compression;
6using System.Linq;
7using System.Threading;
8using System.Threading.Tasks;
9
12
14{
19 {
23 public const string CurrentDirectory = ".";
24
28 public const int DefaultBufferSize = 4096;
29
33 public const TaskCreationOptions BlockingTaskCreationOptions = TaskCreationOptions.None;
34
37
39 public char AltDirectorySeparatorChar => fileSystem.Path.AltDirectorySeparatorChar;
40
44 readonly IFileSystem fileSystem;
45
50 public DefaultIOManager(IFileSystem fileSystem)
51 {
52 this.fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
53 }
54
60 static void NormalizeAndDelete(IDirectoryInfo dir, CancellationToken cancellationToken)
61 {
62 cancellationToken.ThrowIfCancellationRequested();
63
64 // check if we are a symbolic link
65 if (!dir.Attributes.HasFlag(FileAttributes.Directory) || dir.Attributes.HasFlag(FileAttributes.ReparsePoint))
66 {
67 dir.Delete();
68 return;
69 }
70
71 List<Exception>? exceptions = null;
72 foreach (var subDir in dir.EnumerateDirectories())
73 try
74 {
75 NormalizeAndDelete(subDir, cancellationToken);
76 }
77 catch (AggregateException ex)
78 {
79 exceptions ??= new List<Exception>();
80 exceptions.AddRange(ex.InnerExceptions);
81 }
82
83 foreach (var file in dir.EnumerateFiles())
84 {
85 cancellationToken.ThrowIfCancellationRequested();
86 try
87 {
88 file.Attributes = FileAttributes.Normal;
89 file.Delete();
90 }
91 catch (Exception ex)
92 {
93 exceptions ??= new List<Exception>();
94 exceptions.Add(ex);
95 }
96 }
97
98 cancellationToken.ThrowIfCancellationRequested();
99 try
100 {
101 dir.Delete(true);
102 }
103 catch (Exception ex)
104 {
105 exceptions ??= new List<Exception>();
106 exceptions.Add(ex);
107 }
108
109 if (exceptions != null)
110 throw new AggregateException(exceptions);
111 }
112
114 public async ValueTask CopyDirectory(
115 IEnumerable<string>? ignore,
116 Func<string, string, ValueTask>? postCopyCallback,
117 string src,
118 string dest,
119 int? taskThrottle,
120 CancellationToken cancellationToken)
121 {
122 ArgumentNullException.ThrowIfNull(src);
123 ArgumentNullException.ThrowIfNull(src);
124
125 if (taskThrottle.HasValue && taskThrottle < 1)
126 throw new ArgumentOutOfRangeException(nameof(taskThrottle), taskThrottle, "taskThrottle must be at least 1!");
127
128 src = ResolvePath(src);
129 dest = ResolvePath(dest);
130
131 using var semaphore = taskThrottle.HasValue ? new SemaphoreSlim(taskThrottle.Value) : null;
132 await Task.WhenAll(CopyDirectoryImpl(src, dest, ignore, postCopyCallback, semaphore, cancellationToken));
133 }
134
136 public string ConcatPath(params string[] paths) => fileSystem.Path.Combine(paths);
137
139 public async ValueTask CopyFile(string src, string dest, CancellationToken cancellationToken)
140 {
141 ArgumentNullException.ThrowIfNull(src);
142 ArgumentNullException.ThrowIfNull(dest);
143
144 // tested to hell and back, these are the optimal buffer sizes
145 await using var srcStream = fileSystem.FileStream.New(
146 ResolvePath(src),
147 FileMode.Open,
148 FileAccess.Read,
149 FileShare.Read | FileShare.Delete,
151 FileOptions.Asynchronous | FileOptions.SequentialScan);
152 await using var destStream = CreateAsyncSequentialWriteStream(dest);
153
154 // value taken from documentation
155 await srcStream.CopyToAsync(destStream, 81920, cancellationToken);
156 }
157
159 public Task CreateDirectory(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => fileSystem.Directory.CreateDirectory(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current);
160
162 public Task DeleteDirectory(string path, CancellationToken cancellationToken)
163 => Task.Factory.StartNew(
164 () =>
165 {
166 var di = fileSystem.DirectoryInfo.New(
167 ResolvePath(path));
168 if (di.Exists)
169 NormalizeAndDelete(di, cancellationToken);
170 },
171 cancellationToken,
173 TaskScheduler.Current);
174
176 public Task DeleteFile(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => fileSystem.File.Delete(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current);
177
179 public Task<bool> FileExists(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => fileSystem.File.Exists(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current);
180
182 public Task<bool> DirectoryExists(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => fileSystem.Directory.Exists(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current);
183
185 public string GetDirectoryName(string path) => fileSystem.Path.GetDirectoryName(path ?? throw new ArgumentNullException(nameof(path)))
186 ?? throw new InvalidOperationException($"Null was returned. Path ({path}) must be rooted. This is not supported!");
187
189 public string GetFileName(string path) => fileSystem.Path.GetFileName(path ?? throw new ArgumentNullException(nameof(path)));
190
192 public string GetFileNameWithoutExtension(string path) => fileSystem.Path.GetFileNameWithoutExtension(path ?? throw new ArgumentNullException(nameof(path)));
193
195 public Task<List<string>> GetFilesWithExtension(string path, string extension, bool recursive, CancellationToken cancellationToken) => Task.Factory.StartNew(
196 () =>
197 {
198 path = ResolvePath(path);
199 ArgumentNullException.ThrowIfNull(extension);
200 var results = new List<string>();
201 foreach (var fileName in fileSystem.Directory.EnumerateFiles(
202 path,
203 $"*.{extension}",
204 recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly))
205 {
206 cancellationToken.ThrowIfCancellationRequested();
207 results.Add(fileName);
208 }
209
210 return results;
211 },
212 cancellationToken,
214 TaskScheduler.Current);
215
217 public Task MoveFile(string source, string destination, CancellationToken cancellationToken) => Task.Factory.StartNew(
218 () =>
219 {
220 ArgumentNullException.ThrowIfNull(destination);
221 source = ResolvePath(source ?? throw new ArgumentNullException(nameof(source)));
222 destination = ResolvePath(destination);
223 fileSystem.File.Move(source, destination);
224 },
225 cancellationToken,
227 TaskScheduler.Current);
228
230 public Task MoveDirectory(string source, string destination, CancellationToken cancellationToken) => Task.Factory.StartNew(
231 () =>
232 {
233 ArgumentNullException.ThrowIfNull(destination);
234 source = ResolvePath(source ?? throw new ArgumentNullException(nameof(source)));
235 destination = ResolvePath(destination);
236 fileSystem.Directory.Move(source, destination);
237 },
238 cancellationToken,
240 TaskScheduler.Current);
241
243 public async ValueTask<byte[]> ReadAllBytes(string path, CancellationToken cancellationToken)
244 {
245 await using var file = CreateAsyncReadStream(path, true, true);
246 byte[] buf;
247 buf = new byte[file.Length];
248 await file.ReadAsync(buf, cancellationToken);
249 return buf;
250 }
251
253 public string ResolvePath()
255
257 public string ResolvePath(string path)
258 {
259 if (fileSystem.Path.IsPathRooted(path ?? throw new ArgumentNullException(nameof(path))))
260 {
261 // Important to evaluate the path anyway to normalize front slashes to backslashes on Windows
262 // Some tools (looking at you netsh.exe) bitch if you pass them forward slashes as directory separators
263 // Can't rely on ResolvePathCore to do this either because its contract stipulates a relative path
264 return fileSystem.Path.GetFullPath(path);
265 }
266
267 return ResolvePathCore(path);
268 }
269
271 public async ValueTask WriteAllBytes(string path, byte[] contents, CancellationToken cancellationToken)
272 {
273 await using var file = CreateAsyncSequentialWriteStream(path);
274 await file.WriteAsync(contents, cancellationToken);
275 }
276
279 {
280 path = ResolvePath(path);
281 return fileSystem.FileStream.New(
282 path,
283 FileMode.Create,
284 FileAccess.Write,
285 FileShare.Read | FileShare.Delete,
287 FileOptions.Asynchronous | FileOptions.SequentialScan);
288 }
289
291 public Stream CreateAsyncReadStream(string path, bool sequental, bool shareWrite)
292 {
293 path = ResolvePath(path);
294 return fileSystem.FileStream.New(
295 path,
296 FileMode.Open,
297 FileAccess.Read,
298 FileShare.ReadWrite | FileShare.Delete | (shareWrite ? FileShare.Write : FileShare.None),
300 sequental
301 ? FileOptions.Asynchronous | FileOptions.SequentialScan
302 : FileOptions.Asynchronous);
303 }
304
306 public Task<IReadOnlyList<string>> GetDirectories(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(
307 () =>
308 {
309 path = ResolvePath(path);
310 var results = new List<string>();
311 cancellationToken.ThrowIfCancellationRequested();
312 foreach (var directoryName in fileSystem.Directory.EnumerateDirectories(path))
313 {
314 results.Add(directoryName);
315 cancellationToken.ThrowIfCancellationRequested();
316 }
317
318 return (IReadOnlyList<string>)results;
319 },
320 cancellationToken,
322 TaskScheduler.Current);
323
325 public Task<IReadOnlyList<string>> GetFiles(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(
326 () =>
327 {
328 path = ResolvePath(path);
329 var results = new List<string>();
330 cancellationToken.ThrowIfCancellationRequested();
331 foreach (var fileName in fileSystem.Directory.EnumerateFiles(path))
332 {
333 results.Add(fileName);
334 cancellationToken.ThrowIfCancellationRequested();
335 }
336
337 return (IReadOnlyList<string>)results;
338 },
339 cancellationToken,
341 TaskScheduler.Current);
342
344 public async ValueTask ZipToDirectory(string path, Stream zipFile, CancellationToken cancellationToken)
345 {
346 path = ResolvePath(path);
347 ArgumentNullException.ThrowIfNull(zipFile);
348
349#if NET9_0_OR_GREATER
350#error Check if zip file seeking has been addressesed. See https://github.com/tgstation/tgstation-server/issues/1531
351#endif
352
353 // ZipArchive does a synchronous copy on unseekable streams we want to avoid
354 if (!zipFile.CanSeek)
355 throw new ArgumentException("Stream does not support seeking!", nameof(zipFile));
356
357 using var archive = new ZipArchive(zipFile, ZipArchiveMode.Read, true);
358
359 // start async context
360 await Task.Yield();
361 foreach (var entry in archive.Entries)
362 {
363 var entryPath = fileSystem.Path.Combine(path, entry.FullName);
364
365 if (string.IsNullOrEmpty(entry.Name))
366 {
367 fileSystem.Directory.CreateDirectory(entryPath);
368 continue;
369 }
370
371 var directoryPath = fileSystem.Path.GetDirectoryName(entryPath);
372 if (directoryPath == null)
373 {
374 throw new JobException("Zip archive concatenation resulted in a null directory path!");
375 }
376
377 fileSystem.Directory.CreateDirectory(directoryPath);
378
379 using var entryStream = entry.Open();
380 using var outputStream = fileSystem.File.Create(entryPath);
381 await entryStream.CopyToAsync(outputStream, cancellationToken);
382 }
383 }
384
386 public bool PathContainsParentAccess(string path) => path
387 ?.Split(
388 [
389 fileSystem.Path.DirectorySeparatorChar,
390 fileSystem.Path.AltDirectorySeparatorChar,
391 ])
392 .Any(x => x == "..")
393 ?? throw new ArgumentNullException(nameof(path));
394
396 public Task<DateTimeOffset> GetLastModified(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(
397 () =>
398 {
399 path = ResolvePath(path ?? throw new ArgumentNullException(nameof(path)));
400 var fileInfo = fileSystem.FileInfo.New(path);
401 return new DateTimeOffset(fileInfo.LastWriteTimeUtc);
402 },
403 cancellationToken,
405 TaskScheduler.Current);
406
408 public Task<bool> PathIsChildOf(string parentPath, string childPath, CancellationToken cancellationToken) => Task.Factory.StartNew(
409 () =>
410 {
411 parentPath = ResolvePath(parentPath);
412 childPath = ResolvePath(childPath);
413
414 if (parentPath == childPath)
415 return true;
416
417 // https://stackoverflow.com/questions/5617320/given-full-path-check-if-path-is-subdirectory-of-some-other-path-or-otherwise?lq=1
418 var di1 = fileSystem.DirectoryInfo.New(parentPath);
419 var di2 = fileSystem.DirectoryInfo.New(childPath);
420 while (di2.Parent != null)
421 {
422 if (di2.Parent.FullName == di1.FullName)
423 return true;
424
425 di2 = di2.Parent;
426 }
427
428 return false;
429 },
430 cancellationToken,
432 TaskScheduler.Current);
433
435 public Task<IDirectoryInfo> DirectoryInfo(string path, CancellationToken cancellationToken)
436 => Task.Factory.StartNew(
437 () => fileSystem.DirectoryInfo.New(ResolvePath(path)),
438 cancellationToken,
440 TaskScheduler.Current);
441
443 public bool IsPathRooted(string path)
444 => fileSystem.Path.IsPathRooted(path);
445
447 public IIOManager CreateResolverForSubdirectory(string subdirectoryPath)
448 {
449 ArgumentNullException.ThrowIfNull(subdirectoryPath);
450
451 if (!fileSystem.Path.IsPathRooted(subdirectoryPath))
452 subdirectoryPath = ConcatPath(
453 ResolvePath(),
454 subdirectoryPath);
455
456 return new ResolvingIOManager(
458 subdirectoryPath);
459 }
460
466 protected virtual string ResolvePathCore(string path)
467 => fileSystem.Path.GetFullPath(path ?? throw new ArgumentNullException(nameof(path)));
468
479 IEnumerable<Task> CopyDirectoryImpl(
480 string src,
481 string dest,
482 IEnumerable<string>? ignore,
483 Func<string, string, ValueTask>? postCopyCallback,
484 SemaphoreSlim? semaphore,
485 CancellationToken cancellationToken)
486 {
487 var dir = fileSystem.DirectoryInfo.New(src);
488 Task? subdirCreationTask = null;
489 foreach (var subDirectory in dir.EnumerateDirectories())
490 {
491 if (ignore != null && ignore.Contains(subDirectory.Name))
492 continue;
493
494 var checkingSubdirCreationTask = true;
495 foreach (var copyTask in CopyDirectoryImpl(subDirectory.FullName, fileSystem.Path.Combine(dest, subDirectory.Name), null, postCopyCallback, semaphore, cancellationToken))
496 {
497 if (subdirCreationTask == null)
498 {
499 subdirCreationTask = copyTask;
500 yield return subdirCreationTask;
501 }
502 else if (!checkingSubdirCreationTask)
503 yield return copyTask;
504
505 checkingSubdirCreationTask = false;
506 }
507 }
508
509 foreach (var fileInfo in dir.EnumerateFiles())
510 {
511 if (subdirCreationTask == null)
512 {
513 subdirCreationTask = CreateDirectory(dest, cancellationToken);
514 yield return subdirCreationTask;
515 }
516
517 if (ignore != null && ignore.Contains(fileInfo.Name))
518 continue;
519
520 var sourceFile = fileInfo.FullName;
521 var destFile = ConcatPath(dest, fileInfo.Name);
522
523 async Task CopyThisFile()
524 {
525 await subdirCreationTask.WaitAsync(cancellationToken);
526 using var lockContext = semaphore != null
527 ? await SemaphoreSlimContext.Lock(semaphore, cancellationToken)
528 : null;
529 await CopyFile(sourceFile, destFile, cancellationToken);
530 if (postCopyCallback != null)
531 await postCopyCallback(sourceFile, destFile);
532 }
533
534 yield return CopyThisFile();
535 }
536 }
537 }
538}
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....
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...
virtual string ResolvePathCore(string path)
Resolve a given, non-rooted, path .
Task CreateDirectory(string path, CancellationToken cancellationToken)
Create a directory at path .A Task representing the running operation.
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 .
DefaultIOManager(IFileSystem fileSystem)
Initializes a new instance of the DefaultIOManager class.
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 .
string ResolvePath(string path)
Retrieve the full path of some path given a relative path. Must be used before passing relative 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 .
IIOManager CreateResolverForSubdirectory(string subdirectoryPath)
Create a new IIOManager that resolves paths to the specified subdirectoryPath .A new IIOManager.
char AltDirectorySeparatorChar
Gets the alternative directory separator character.
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...
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.
Stream CreateAsyncReadStream(string path, bool sequental, bool shareWrite)
Creates an asynchronous FileStream for sequential reading.The open Stream.
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 IsPathRooted(string path)
Check if a given path is at the root level of the filesystem.true if the path is rooted,...
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.
char DirectorySeparatorChar
Gets the primary directory separator character.
static void NormalizeAndDelete(IDirectoryInfo 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 .
async ValueTask ZipToDirectory(string path, Stream zipFile, CancellationToken cancellationToken)
Extract a set of zipFile to a given path .A ValueTask representing the running operation.
Stream CreateAsyncSequentialWriteStream(string path)
Creates an asynchronous FileStream for sequential writing.The open Stream.
readonly IFileSystem fileSystem
The backing IFileSystem.
Task< IDirectoryInfo > DirectoryInfo(string path, CancellationToken cancellationToken)
Gets a IDirectoryInfo for the given path .A Task<TResult> resulting in the IDirectoryInfo of the 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,...
An IIOManager that resolve relative paths from another IIOManager to a subdirectory of that.
Operation exceptions thrown from the context of a Models.Job.
static async ValueTask< SemaphoreSlimContext > Lock(SemaphoreSlim semaphore, CancellationToken cancellationToken, ILogger? logger=null)
Asyncronously locks a semaphore .
Interface for using filesystems.
Definition IIOManager.cs:14
char DirectorySeparatorChar
Gets the primary directory separator character.
Definition IIOManager.cs:18