tgstation-server 6.0.1
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 foreach (var subDir in dir.EnumerateDirectories())
50 NormalizeAndDelete(subDir, cancellationToken);
51
52 foreach (var file in dir.EnumerateFiles())
53 {
54 cancellationToken.ThrowIfCancellationRequested();
55 try
56 {
57 file.Attributes = FileAttributes.Normal;
58 file.Delete();
59 }
60 catch (FileNotFoundException)
61 {
62 // has happened before with .dyn.rsc.lk
63 }
64 }
65
66 cancellationToken.ThrowIfCancellationRequested();
67 dir.Delete(true);
68 }
69
71 public async ValueTask CopyDirectory(
72 IEnumerable<string>? ignore,
73 Func<string, string, ValueTask>? postCopyCallback,
74 string src,
75 string dest,
76 int? taskThrottle,
77 CancellationToken cancellationToken)
78 {
79 ArgumentNullException.ThrowIfNull(src);
80 ArgumentNullException.ThrowIfNull(src);
81
82 if (taskThrottle.HasValue && taskThrottle < 1)
83 throw new ArgumentOutOfRangeException(nameof(taskThrottle), taskThrottle, "taskThrottle must be at least 1!");
84
85 src = ResolvePath(src);
86 dest = ResolvePath(dest);
87
88 using var semaphore = taskThrottle.HasValue ? new SemaphoreSlim(taskThrottle.Value) : null;
89 await Task.WhenAll(CopyDirectoryImpl(src, dest, ignore, postCopyCallback, semaphore, cancellationToken));
90 }
91
93 public string ConcatPath(params string[] paths) => Path.Combine(paths);
94
96 public async ValueTask CopyFile(string src, string dest, CancellationToken cancellationToken)
97 {
98 ArgumentNullException.ThrowIfNull(src);
99 ArgumentNullException.ThrowIfNull(dest);
100
101 // tested to hell and back, these are the optimal buffer sizes
102 await using var srcStream = new FileStream(
103 ResolvePath(src),
104 FileMode.Open,
105 FileAccess.Read,
106 FileShare.Read | FileShare.Delete,
108 FileOptions.Asynchronous | FileOptions.SequentialScan);
109 await using var destStream = CreateAsyncSequentialWriteStream(dest);
110
111 // value taken from documentation
112 await srcStream.CopyToAsync(destStream, 81920, cancellationToken);
113 }
114
116 public Task CreateDirectory(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => Directory.CreateDirectory(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current);
117
119 public Task DeleteDirectory(string path, CancellationToken cancellationToken)
120 {
121 path = ResolvePath(path);
122 var di = new DirectoryInfo(path);
123 if (!di.Exists)
124 return Task.CompletedTask;
125
126 return Task.Factory.StartNew(
127 () => NormalizeAndDelete(di, cancellationToken),
128 cancellationToken,
130 TaskScheduler.Current);
131 }
132
134 public Task DeleteFile(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => File.Delete(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current);
135
137 public Task<bool> FileExists(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => File.Exists(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current);
138
140 public Task<bool> DirectoryExists(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => Directory.Exists(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current);
141
143 public string GetDirectoryName(string path) => Path.GetDirectoryName(path ?? throw new ArgumentNullException(nameof(path)))
144 ?? throw new InvalidOperationException($"Null was returned. Path ({path}) must be rooted. This is not supported!");
145
147 public string GetFileName(string path) => Path.GetFileName(path ?? throw new ArgumentNullException(nameof(path)));
148
150 public string GetFileNameWithoutExtension(string path) => Path.GetFileNameWithoutExtension(path ?? throw new ArgumentNullException(nameof(path)));
151
153 public Task<List<string>> GetFilesWithExtension(string path, string extension, bool recursive, CancellationToken cancellationToken) => Task.Factory.StartNew(
154 () =>
155 {
156 path = ResolvePath(path);
157 ArgumentNullException.ThrowIfNull(extension);
158 var results = new List<string>();
159 foreach (var fileName in Directory.EnumerateFiles(
160 path,
161 $"*.{extension}",
162 recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly))
163 {
164 cancellationToken.ThrowIfCancellationRequested();
165 results.Add(fileName);
166 }
167
168 return results;
169 },
170 cancellationToken,
172 TaskScheduler.Current);
173
175 public Task MoveFile(string source, string destination, CancellationToken cancellationToken) => Task.Factory.StartNew(
176 () =>
177 {
178 ArgumentNullException.ThrowIfNull(destination);
179 source = ResolvePath(source ?? throw new ArgumentNullException(nameof(source)));
180 destination = ResolvePath(destination);
181 File.Move(source, destination);
182 },
183 cancellationToken,
185 TaskScheduler.Current);
186
188 public Task MoveDirectory(string source, string destination, CancellationToken cancellationToken) => Task.Factory.StartNew(
189 () =>
190 {
191 ArgumentNullException.ThrowIfNull(destination);
192 source = ResolvePath(source ?? throw new ArgumentNullException(nameof(source)));
193 destination = ResolvePath(destination);
194 Directory.Move(source, destination);
195 },
196 cancellationToken,
198 TaskScheduler.Current);
199
201 public async ValueTask<byte[]> ReadAllBytes(string path, CancellationToken cancellationToken)
202 {
203 path = ResolvePath(path);
204 await using var file = new FileStream(
205 path,
206 FileMode.Open,
207 FileAccess.Read,
208 FileShare.ReadWrite | FileShare.Delete,
210 FileOptions.Asynchronous | FileOptions.SequentialScan);
211 byte[] buf;
212 buf = new byte[file.Length];
213 await file.ReadAsync(buf, cancellationToken);
214 return buf;
215 }
216
219
221 public virtual string ResolvePath(string path) => Path.GetFullPath(path ?? throw new ArgumentNullException(nameof(path)));
222
224 public async ValueTask WriteAllBytes(string path, byte[] contents, CancellationToken cancellationToken)
225 {
226 await using var file = CreateAsyncSequentialWriteStream(path);
227 await file.WriteAsync(contents, cancellationToken);
228 }
229
231 public FileStream CreateAsyncSequentialWriteStream(string path)
232 {
233 path = ResolvePath(path);
234 return new FileStream(
235 path,
236 FileMode.Create,
237 FileAccess.Write,
238 FileShare.Read | FileShare.Delete,
240 FileOptions.Asynchronous | FileOptions.SequentialScan);
241 }
242
244 public Task<IReadOnlyList<string>> GetDirectories(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(
245 () =>
246 {
247 path = ResolvePath(path);
248 var results = new List<string>();
249 cancellationToken.ThrowIfCancellationRequested();
250 foreach (var directoryName in Directory.EnumerateDirectories(path))
251 {
252 results.Add(directoryName);
253 cancellationToken.ThrowIfCancellationRequested();
254 }
255
256 return (IReadOnlyList<string>)results;
257 },
258 cancellationToken,
260 TaskScheduler.Current);
261
263 public Task<IReadOnlyList<string>> GetFiles(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(
264 () =>
265 {
266 path = ResolvePath(path);
267 var results = new List<string>();
268 cancellationToken.ThrowIfCancellationRequested();
269 foreach (var fileName in Directory.EnumerateFiles(path))
270 {
271 results.Add(fileName);
272 cancellationToken.ThrowIfCancellationRequested();
273 }
274
275 return (IReadOnlyList<string>)results;
276 },
277 cancellationToken,
279 TaskScheduler.Current);
280
282 public Task ZipToDirectory(string path, Stream zipFile, CancellationToken cancellationToken) => Task.Factory.StartNew(
283 () =>
284 {
285 path = ResolvePath(path);
286 ArgumentNullException.ThrowIfNull(zipFile);
287
288#if NET9_0_OR_GREATER
289#error Check if zip file seeking has been addressesed. See https://github.com/tgstation/tgstation-server/issues/1531
290#endif
291
292 // ZipArchive does a synchronous copy on unseekable streams we want to avoid
293 if (!zipFile.CanSeek)
294 throw new ArgumentException("Stream does not support seeking!", nameof(zipFile));
295
296 using var archive = new ZipArchive(zipFile, ZipArchiveMode.Read, true);
297 archive.ExtractToDirectory(path);
298 },
299 cancellationToken,
301 TaskScheduler.Current);
302
304 public bool PathContainsParentAccess(string path) => path
305 ?.Split(
306 [
307 Path.DirectorySeparatorChar,
308 Path.AltDirectorySeparatorChar,
309 ])
310 .Any(x => x == "..")
311 ?? throw new ArgumentNullException(nameof(path));
312
314 public Task<DateTimeOffset> GetLastModified(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(
315 () =>
316 {
317 path = ResolvePath(path ?? throw new ArgumentNullException(nameof(path)));
318 var fileInfo = new FileInfo(path);
319 return new DateTimeOffset(fileInfo.LastWriteTimeUtc);
320 },
321 cancellationToken,
323 TaskScheduler.Current);
324
326 public FileStream GetFileStream(string path, bool shareWrite) => new(
327 ResolvePath(path),
328 FileMode.Open,
329 FileAccess.Read,
330 FileShare.Read | FileShare.Delete | (shareWrite ? FileShare.Write : FileShare.None),
332 true);
333
344 IEnumerable<Task> CopyDirectoryImpl(
345 string src,
346 string dest,
347 IEnumerable<string>? ignore,
348 Func<string, string, ValueTask>? postCopyCallback,
349 SemaphoreSlim? semaphore,
350 CancellationToken cancellationToken)
351 {
352 var dir = new DirectoryInfo(src);
353 Task? subdirCreationTask = null;
354 foreach (var subDirectory in dir.EnumerateDirectories())
355 {
356 if (ignore != null && ignore.Contains(subDirectory.Name))
357 continue;
358
359 var checkingSubdirCreationTask = true;
360 foreach (var copyTask in CopyDirectoryImpl(subDirectory.FullName, Path.Combine(dest, subDirectory.Name), null, postCopyCallback, semaphore, cancellationToken))
361 {
362 if (subdirCreationTask == null)
363 {
364 subdirCreationTask = copyTask;
365 yield return subdirCreationTask;
366 }
367 else if (!checkingSubdirCreationTask)
368 yield return copyTask;
369
370 checkingSubdirCreationTask = false;
371 }
372 }
373
374 foreach (var fileInfo in dir.EnumerateFiles())
375 {
376 if (subdirCreationTask == null)
377 {
378 subdirCreationTask = CreateDirectory(dest, cancellationToken);
379 yield return subdirCreationTask;
380 }
381
382 if (ignore != null && ignore.Contains(fileInfo.Name))
383 continue;
384
385 var sourceFile = fileInfo.FullName;
386 var destFile = ConcatPath(dest, fileInfo.Name);
387
388 async Task CopyThisFile()
389 {
390 await subdirCreationTask.WaitAsync(cancellationToken);
391 using var lockContext = semaphore != null
392 ? await SemaphoreSlimContext.Lock(semaphore, cancellationToken)
393 : null;
394 await CopyFile(sourceFile, destFile, cancellationToken);
395 if (postCopyCallback != null)
396 await postCopyCallback(sourceFile, destFile);
397 }
398
399 yield return CopyThisFile();
400 }
401 }
402 }
403}
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 directo...
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 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 r...
Task< DateTimeOffset > GetLastModified(string path, CancellationToken cancellationToken)
Get the DateTimeOffset of when a given path was last modified. A Task<TResult> resulting in the Date...
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.
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)
Asyncronously locks a semaphore .
Interface for using filesystems.
Definition: IIOManager.cs:13