tgstation-server 5.12.7
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
11
13{
18 {
22 public const string CurrentDirectory = ".";
23
27 public const int DefaultBufferSize = 4096;
28
32 public const TaskCreationOptions BlockingTaskCreationOptions = TaskCreationOptions.None;
33
39 static void NormalizeAndDelete(DirectoryInfo dir, CancellationToken cancellationToken)
40 {
41 cancellationToken.ThrowIfCancellationRequested();
42
43 // check if we are a symbolic link
44 if (!dir.Attributes.HasFlag(FileAttributes.Directory) || dir.Attributes.HasFlag(FileAttributes.ReparsePoint))
45 {
46 dir.Delete();
47 return;
48 }
49
50 foreach (var subDir in dir.EnumerateDirectories())
51 NormalizeAndDelete(subDir, cancellationToken);
52
53 foreach (var file in dir.EnumerateFiles())
54 {
55 cancellationToken.ThrowIfCancellationRequested();
56 try
57 {
58 file.Attributes = FileAttributes.Normal;
59 file.Delete();
60 }
61 catch (FileNotFoundException)
62 {
63 // has happened before with .dyn.rsc.lk
64 }
65 }
66
67 cancellationToken.ThrowIfCancellationRequested();
68 dir.Delete(true);
69 }
70
72 public async Task CopyDirectory(
73 IEnumerable<string> ignore,
74 Func<string, string, Task> postCopyCallback,
75 string src,
76 string dest,
77 int? taskThrottle,
78 CancellationToken cancellationToken)
79 {
80 ArgumentNullException.ThrowIfNull(src);
81 ArgumentNullException.ThrowIfNull(src);
82
83 if (taskThrottle.HasValue && taskThrottle < 1)
84 throw new ArgumentOutOfRangeException(nameof(taskThrottle), taskThrottle, "taskThrottle must be at least 1!");
85
86 src = ResolvePath(src);
87 dest = ResolvePath(dest);
88
89 using var semaphore = taskThrottle.HasValue ? new SemaphoreSlim(taskThrottle.Value) : null;
90 await Task.WhenAll(CopyDirectoryImpl(src, dest, ignore, postCopyCallback, semaphore, cancellationToken));
91 }
92
94 public string ConcatPath(params string[] paths) => Path.Combine(paths);
95
97 public async Task CopyFile(string src, string dest, CancellationToken cancellationToken)
98 {
99 ArgumentNullException.ThrowIfNull(src);
100 ArgumentNullException.ThrowIfNull(dest);
101
102 // tested to hell and back, these are the optimal buffer sizes
103 await using var srcStream = new FileStream(
104 ResolvePath(src),
105 FileMode.Open,
106 FileAccess.Read,
107 FileShare.Read | FileShare.Delete,
109 FileOptions.Asynchronous | FileOptions.SequentialScan);
110 await using var destStream = CreateAsyncSequentialWriteStream(dest);
111
112 // value taken from documentation
113 await srcStream.CopyToAsync(destStream, 81920, cancellationToken);
114 }
115
117 public Task CreateDirectory(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => Directory.CreateDirectory(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current);
118
120 public Task DeleteDirectory(string path, CancellationToken cancellationToken)
121 {
122 path = ResolvePath(path);
123 var di = new DirectoryInfo(path);
124 if (!di.Exists)
125 return Task.CompletedTask;
126
127 return Task.Factory.StartNew(
128 () => NormalizeAndDelete(di, cancellationToken),
129 cancellationToken,
131 TaskScheduler.Current);
132 }
133
135 public Task DeleteFile(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => File.Delete(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current);
136
138 public Task<bool> FileExists(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => File.Exists(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current);
139
141 public Task<bool> DirectoryExists(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => Directory.Exists(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current);
142
144 public string GetDirectoryName(string path) => Path.GetDirectoryName(path ?? throw new ArgumentNullException(nameof(path)));
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 if (extension == null)
158 throw new ArgumentNullException(extension);
159 var results = new List<string>();
160 foreach (var fileName in Directory.EnumerateFiles(
161 path,
162 $"*.{extension}",
163 recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly))
164 {
165 cancellationToken.ThrowIfCancellationRequested();
166 results.Add(fileName);
167 }
168
169 return results;
170 },
171 cancellationToken,
173 TaskScheduler.Current);
174
176 public Task MoveFile(string source, string destination, CancellationToken cancellationToken) => Task.Factory.StartNew(
177 () =>
178 {
179 ArgumentNullException.ThrowIfNull(destination);
180 source = ResolvePath(source ?? throw new ArgumentNullException(nameof(source)));
181 destination = ResolvePath(destination);
182 File.Move(source, destination);
183 },
184 cancellationToken,
186 TaskScheduler.Current);
187
189 public Task MoveDirectory(string source, string destination, CancellationToken cancellationToken) => Task.Factory.StartNew(
190 () =>
191 {
192 ArgumentNullException.ThrowIfNull(destination);
193 source = ResolvePath(source ?? throw new ArgumentNullException(nameof(source)));
194 destination = ResolvePath(destination);
195 Directory.Move(source, destination);
196 },
197 cancellationToken,
199 TaskScheduler.Current);
200
202 public async Task<byte[]> ReadAllBytes(string path, CancellationToken cancellationToken)
203 {
204 path = ResolvePath(path);
205 await using var file = new FileStream(
206 path,
207 FileMode.Open,
208 FileAccess.Read,
209 FileShare.ReadWrite | FileShare.Delete,
211 FileOptions.Asynchronous | FileOptions.SequentialScan);
212 byte[] buf;
213 buf = new byte[file.Length];
214 await file.ReadAsync(buf, cancellationToken);
215 return buf;
216 }
217
220
222 public virtual string ResolvePath(string path) => Path.GetFullPath(path ?? throw new ArgumentNullException(nameof(path)));
223
225 public async Task WriteAllBytes(string path, byte[] contents, CancellationToken cancellationToken)
226 {
227 await using var file = CreateAsyncSequentialWriteStream(path);
228 await file.WriteAsync(contents, cancellationToken);
229 }
230
232 public FileStream CreateAsyncSequentialWriteStream(string path)
233 {
234 path = ResolvePath(path);
235 return new FileStream(
236 path,
237 FileMode.Create,
238 FileAccess.Write,
239 FileShare.Read | FileShare.Delete,
241 FileOptions.Asynchronous | FileOptions.SequentialScan);
242 }
243
245 public Task<IReadOnlyList<string>> GetDirectories(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(
246 () =>
247 {
248 path = ResolvePath(path);
249 var results = new List<string>();
250 cancellationToken.ThrowIfCancellationRequested();
251 foreach (var directoryName in Directory.EnumerateDirectories(path))
252 {
253 results.Add(directoryName);
254 cancellationToken.ThrowIfCancellationRequested();
255 }
256
257 return (IReadOnlyList<string>)results;
258 },
259 cancellationToken,
261 TaskScheduler.Current);
262
264 public Task<IReadOnlyList<string>> GetFiles(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(
265 () =>
266 {
267 path = ResolvePath(path);
268 var results = new List<string>();
269 cancellationToken.ThrowIfCancellationRequested();
270 foreach (var fileName in Directory.EnumerateFiles(path))
271 {
272 results.Add(fileName);
273 cancellationToken.ThrowIfCancellationRequested();
274 }
275
276 return (IReadOnlyList<string>)results;
277 },
278 cancellationToken,
280 TaskScheduler.Current);
281
283 public Task ZipToDirectory(string path, Stream zipFile, CancellationToken cancellationToken) => Task.Factory.StartNew(
284 () =>
285 {
286 path = ResolvePath(path);
287 ArgumentNullException.ThrowIfNull(zipFile);
288
289#if NET7_0_OR_GREATER
290#warning Check if zip file seeking has been addressesed. See https://github.com/tgstation/tgstation-server/issues/1531
291#endif
292
293 // ZipArchive does a synchronous copy on unseekable streams we want to avoid
294 if (!zipFile.CanSeek)
295 throw new ArgumentException("Stream does not support seeking!", nameof(zipFile));
296
297 using var archive = new ZipArchive(zipFile, ZipArchiveMode.Read, true);
298 archive.ExtractToDirectory(path);
299 },
300 cancellationToken,
302 TaskScheduler.Current);
303
305 public bool PathContainsParentAccess(string path) => path
306 ?.Split(
307 new[]
308 {
309 Path.DirectorySeparatorChar,
310 Path.AltDirectorySeparatorChar,
311 })
312 .Any(x => x == "..")
313 ?? throw new ArgumentNullException(nameof(path));
314
316 public Task<DateTimeOffset> GetLastModified(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(
317 () =>
318 {
319 path = ResolvePath(path ?? throw new ArgumentNullException(nameof(path)));
320 var fileInfo = new FileInfo(path);
321 return new DateTimeOffset(fileInfo.LastWriteTimeUtc);
322 },
323 cancellationToken,
325 TaskScheduler.Current);
326
328 public FileStream GetFileStream(string path, bool shareWrite) => new (
329 ResolvePath(path),
330 FileMode.Open,
331 FileAccess.Read,
332 FileShare.Read | FileShare.Delete | (shareWrite ? FileShare.Write : FileShare.None),
334 true);
335
346 IEnumerable<Task> CopyDirectoryImpl(
347 string src,
348 string dest,
349 IEnumerable<string> ignore,
350 Func<string, string, Task> postCopyCallback,
351 SemaphoreSlim semaphore,
352 CancellationToken cancellationToken)
353 {
354 var dir = new DirectoryInfo(src);
355 Task subdirCreationTask = null;
356 foreach (var subDirectory in dir.EnumerateDirectories())
357 {
358 if (ignore != null && ignore.Contains(subDirectory.Name))
359 continue;
360
361 var checkingSubdirCreationTask = true;
362 foreach (var copyTask in CopyDirectoryImpl(subDirectory.FullName, Path.Combine(dest, subDirectory.Name), null, postCopyCallback, semaphore, cancellationToken))
363 {
364 if (subdirCreationTask == null)
365 {
366 subdirCreationTask = copyTask;
367 yield return subdirCreationTask;
368 }
369 else if (!checkingSubdirCreationTask)
370 yield return copyTask;
371
372 checkingSubdirCreationTask = false;
373 }
374 }
375
376 foreach (var fileInfo in dir.EnumerateFiles())
377 {
378 if (subdirCreationTask == null)
379 {
380 subdirCreationTask = CreateDirectory(dest, cancellationToken);
381 yield return subdirCreationTask;
382 }
383
384 if (ignore != null && ignore.Contains(fileInfo.Name))
385 continue;
386
387 var sourceFile = fileInfo.FullName;
388 var destFile = ConcatPath(dest, fileInfo.Name);
389
390 async Task CopyThisFile()
391 {
392 await subdirCreationTask.WithToken(cancellationToken);
393 using var lockContext = semaphore != null
394 ? await SemaphoreSlimContext.Lock(semaphore, cancellationToken)
395 : null;
396 await CopyFile(sourceFile, destFile, cancellationToken);
397 if (postCopyCallback != null)
398 await postCopyCallback(sourceFile, destFile);
399 }
400
401 yield return CopyThisFile();
402 }
403 }
404 }
405}
IIOManager that resolves paths to Environment.CurrentDirectory.
async Task CopyDirectory(IEnumerable< string > ignore, Func< string, string, Task > postCopyCallback, string src, string dest, int? taskThrottle, CancellationToken cancellationToken)
Copies a directory from src to dest . A Task representing the running operation.
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 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...
Task< IReadOnlyList< string > > GetFiles(string path, CancellationToken cancellationToken)
Returns 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.
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.
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.
async Task CopyFile(string src, string dest, CancellationToken cancellationToken)
Copy a file from src to dest . A Task representing the running operation.
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.
async Task< byte[]> ReadAllBytes(string path, CancellationToken cancellationToken)
Returns all the contents of a file at path as a byte array. A Task that results in the contents of a...
Task DeleteFile(string path, CancellationToken cancellationToken)
Deletes a file at path . A Task representing the running operation.
IEnumerable< Task > CopyDirectoryImpl(string src, string dest, IEnumerable< string > ignore, Func< string, string, Task > postCopyCallback, SemaphoreSlim semaphore, CancellationToken cancellationToken)
Copies a directory from src to dest .
string GetFileName(string path)
Gets the file name portion of a path . The file name portion of path .
async Task WriteAllBytes(string path, byte[] contents, CancellationToken cancellationToken)
Writes some contents to a file at path overwriting previous content. A Task representing the runnin...
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