Skip to content

Commit d8894b3

Browse files
authored
Move an implementation of IOctopusFileSystem to the Tentacle Core Package so users of those components don't need to roll their own. (#1151)
* Move Octopus File System to core so core components don't need to implement their own * Move Octopus File System to core so core components don't need to implement their own
1 parent cdf7832 commit d8894b3

File tree

5 files changed

+378
-363
lines changed

5 files changed

+378
-363
lines changed

source/Octopus.Tentacle.Core/Octopus.Tentacle.Core.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
</Choose>
2727
<ItemGroup>
2828
<ProjectReference Include="..\Octopus.Tentacle.Contracts\Octopus.Tentacle.Contracts.csproj" />
29+
<PackageReference Include="Polly" Version="7.2.2" />
2930
</ItemGroup>
3031
<ItemGroup>
3132
<Compile Include="..\Solution Items\VersionInfo.cs" Link="VersionInfo.cs" />
Lines changed: 373 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Reflection;
6+
using System.Text;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using Octopus.Tentacle.Core.Diagnostics;
10+
using Octopus.Tentacle.Util;
11+
using Polly;
12+
13+
namespace Octopus.Tentacle.Core.Util
14+
{
15+
public class CorePhysicalFileSystem : IOctopusFileSystem
16+
{
17+
const long FiveHundredMegabytes = 500 * 1024 * 1024;
18+
19+
public CorePhysicalFileSystem(ISystemLog log, bool isRunningAsKubernetesAgent)
20+
{
21+
Log = log;
22+
IsRunningAsKubernetesAgent = isRunningAsKubernetesAgent;
23+
}
24+
25+
ISystemLog Log { get; }
26+
bool IsRunningAsKubernetesAgent;
27+
28+
public bool FileExists(string path)
29+
=> File.Exists(path);
30+
31+
public bool DirectoryExists(string path)
32+
=> Directory.Exists(path);
33+
34+
public bool DirectoryIsEmpty(string path)
35+
{
36+
try
37+
{
38+
return !Directory.GetFileSystemEntries(path).Any();
39+
}
40+
catch (Exception ex)
41+
{
42+
Log.Error(ex, "Failed to list directory contents");
43+
return false;
44+
}
45+
}
46+
47+
public void DeleteFile(string path, DeletionOptions? options = null)
48+
{
49+
DeleteFile(path, CancellationToken.None, options).Wait();
50+
}
51+
52+
public async Task DeleteFile(string path, CancellationToken cancellationToken, DeletionOptions? options = null)
53+
{
54+
options ??= DeletionOptions.TryThreeTimes;
55+
56+
if (string.IsNullOrWhiteSpace(path))
57+
return;
58+
59+
await TryToDoSomethingMultipleTimes(i =>
60+
{
61+
if (File.Exists(path))
62+
{
63+
if (i > 1) // Did our first attempt fail?
64+
File.SetAttributes(path, FileAttributes.Normal);
65+
File.Delete(path);
66+
}
67+
},
68+
options.RetryAttempts,
69+
options.SleepBetweenAttemptsMilliseconds,
70+
options.ThrowOnFailure,
71+
cancellationToken);
72+
}
73+
74+
public void DeleteDirectory(string path, DeletionOptions? options = null)
75+
{
76+
DeleteDirectory(path, DefaultCancellationToken, options).Wait();
77+
}
78+
79+
public async Task DeleteDirectory(string path, CancellationToken cancellationToken, DeletionOptions? options = null)
80+
{
81+
await PurgeDirectoryAsync(
82+
path,
83+
cancellationToken,
84+
includeTarget: true,
85+
options);
86+
}
87+
88+
public IEnumerable<string> EnumerateFiles(string parentDirectoryPath, params string[] searchPatterns)
89+
{
90+
return searchPatterns.Length == 0
91+
? Directory.EnumerateFiles(parentDirectoryPath, "*", SearchOption.TopDirectoryOnly)
92+
: searchPatterns.SelectMany(pattern => Directory.EnumerateFiles(parentDirectoryPath, pattern, SearchOption.TopDirectoryOnly));
93+
}
94+
95+
public IEnumerable<string> EnumerateDirectories(string parentDirectoryPath)
96+
{
97+
if (!DirectoryExists(parentDirectoryPath))
98+
return Enumerable.Empty<string>();
99+
100+
return Directory.EnumerateDirectories(parentDirectoryPath);
101+
}
102+
103+
public long GetFileSize(string path)
104+
=> new FileInfo(path).Length;
105+
106+
public string ReadFile(string path)
107+
{
108+
var content = Policy<string>
109+
.Handle<IOException>()
110+
.WaitAndRetry(10, retryCount => TimeSpan.FromMilliseconds(100 * retryCount))
111+
.Execute(() => File.ReadAllText(path));
112+
return content;
113+
}
114+
115+
public void AppendToFile(string path, string contents)
116+
{
117+
File.AppendAllText(path, contents);
118+
}
119+
120+
public void OverwriteFile(string path, string contents)
121+
{
122+
File.WriteAllText(path, contents);
123+
}
124+
125+
public void OverwriteFile(string path, string contents, Encoding encoding)
126+
{
127+
File.WriteAllText(path, contents, encoding);
128+
}
129+
130+
public void CopyFile(string source, string destination, bool overwrite)
131+
{
132+
File.Copy(source, destination, overwrite);
133+
}
134+
135+
public Stream OpenFile(string path, FileAccess access, FileShare share)
136+
=> OpenFile(path, FileMode.OpenOrCreate, access, share);
137+
138+
public Stream OpenFile(string path, FileMode mode, FileAccess access, FileShare share)
139+
{
140+
try
141+
{
142+
return new FileStream(path, mode, access, share);
143+
}
144+
catch (UnauthorizedAccessException)
145+
{
146+
var fileInfo = new FileInfo(path);
147+
if (fileInfo.Exists && (fileInfo.Attributes & FileAttributes.Directory) == FileAttributes.Directory)
148+
// Throw a more helpful message than .NET's
149+
// System.UnauthorizedAccessException: Access to the path ... is denied.
150+
throw new IOException(path + " is a directory not a file");
151+
throw;
152+
}
153+
}
154+
155+
string GetTempBasePath()
156+
{
157+
var path = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData, Environment.SpecialFolderOption.DoNotVerify);
158+
EnsureDirectoryExists(path);
159+
160+
path = Path.Combine(path, Assembly.GetEntryAssembly() != null ? Assembly.GetEntryAssembly()!.GetName().Name! : "Octopus");
161+
return Path.Combine(path, "Temp");
162+
}
163+
164+
public void CreateDirectory(string path)
165+
{
166+
if (Directory.Exists(path))
167+
return;
168+
Directory.CreateDirectory(path);
169+
}
170+
171+
public string CreateTemporaryDirectory()
172+
{
173+
var path = Path.Combine(GetTempBasePath(), Guid.NewGuid().ToString());
174+
Directory.CreateDirectory(path);
175+
return path;
176+
}
177+
178+
static readonly CancellationToken DefaultCancellationToken = CancellationToken.None;
179+
180+
IEnumerable<string> DefaultFileEnumerationFunc(string target)
181+
{
182+
return EnumerateFiles(target);
183+
}
184+
185+
async Task PurgeDirectoryAsync(
186+
string targetDirectory,
187+
CancellationToken? cancel,
188+
bool? includeTarget,
189+
DeletionOptions? options)
190+
{
191+
if (!DirectoryExists(targetDirectory))
192+
return;
193+
194+
cancel ??= CancellationToken.None;
195+
includeTarget ??= false;
196+
options ??= DeletionOptions.TryThreeTimes;
197+
198+
foreach (var file in DefaultFileEnumerationFunc(targetDirectory))
199+
{
200+
await DeleteFile(file, cancel.Value, options);
201+
}
202+
203+
foreach (var directory in EnumerateDirectories(targetDirectory))
204+
{
205+
var info = new DirectoryInfo(directory);
206+
if ((info.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
207+
await TryToDoSomethingMultipleTimes(
208+
_ => info.Delete(true),
209+
options.RetryAttempts,
210+
options.SleepBetweenAttemptsMilliseconds,
211+
options.ThrowOnFailure,
212+
cancel.Value);
213+
else
214+
await PurgeDirectoryAsync(
215+
directory,
216+
cancel,
217+
true,
218+
options);
219+
}
220+
221+
if (includeTarget.Value)
222+
{
223+
await TryToDoSomethingMultipleTimes(
224+
_ =>
225+
{
226+
if (DirectoryIsEmpty(targetDirectory))
227+
{
228+
var dirInfo = new DirectoryInfo(targetDirectory)
229+
{
230+
Attributes = FileAttributes.Normal
231+
};
232+
dirInfo.Delete(true);
233+
}
234+
},
235+
options.RetryAttempts,
236+
options.SleepBetweenAttemptsMilliseconds,
237+
options.ThrowOnFailure,
238+
cancel.Value);
239+
}
240+
}
241+
242+
public void WriteAllBytes(string filePath, byte[] data)
243+
{
244+
File.WriteAllBytes(filePath, data);
245+
}
246+
247+
public void WriteAllText(string path, string contents)
248+
{
249+
File.WriteAllText(path, contents);
250+
}
251+
252+
public void EnsureDirectoryExists(string directoryPath)
253+
{
254+
if (!DirectoryExists(directoryPath))
255+
Directory.CreateDirectory(directoryPath);
256+
} // ReSharper disable AssignNullToNotNullAttribute
257+
258+
public void EnsureDiskHasEnoughFreeSpace(string directoryPath)
259+
{
260+
EnsureDiskHasEnoughFreeSpace(directoryPath, FiveHundredMegabytes);
261+
}
262+
263+
public virtual void EnsureDiskHasEnoughFreeSpace(string directoryPath, long requiredSpaceInBytes)
264+
{
265+
if (IsUncPath(directoryPath))
266+
return;
267+
268+
if (!Path.IsPathRooted(directoryPath))
269+
return;
270+
271+
//We can't perform this check in Kubernetes due to how drives are mounted and reported (always returns 0 byte sized drives)
272+
if(IsRunningAsKubernetesAgent)
273+
return;
274+
275+
var driveInfo = SafelyGetDriveInfo(directoryPath);
276+
277+
var required = requiredSpaceInBytes < 0 ? 0 : (ulong)requiredSpaceInBytes;
278+
// Make sure there is 10% (and a bit extra) more than we need
279+
required += required / 10 + 1024 * 1024;
280+
if ((ulong)driveInfo.AvailableFreeSpace < required)
281+
throw new IOException($"The drive '{driveInfo.Name}' containing the directory '{directoryPath}' on machine '{Environment.MachineName}' does not have enough free disk space available for this operation to proceed. " +
282+
$"The disk only has {driveInfo.AvailableFreeSpace.ToFileSizeString()} available; please free up at least {required.ToFileSizeString()}.");
283+
}
284+
285+
/// <remarks>
286+
/// Previously, we used to get the directory root (ie, `c:\` or `/`) before asking for the drive info
287+
/// However, that doesn't work well with mount points, as there might be enough space in that mount point,
288+
/// but not enough on the root of the drive.
289+
/// New behaviour is to directly check the free disk space on that directory, but we're feeling a bit
290+
/// risk averse here (once bitten, twice shy), so we fall back to the old behaviour
291+
/// </remarks>
292+
static DriveInfo SafelyGetDriveInfo(string directoryPath)
293+
{
294+
DriveInfo driveInfo;
295+
try
296+
{
297+
driveInfo = new DriveInfo(directoryPath);
298+
}
299+
catch
300+
{
301+
driveInfo = new DriveInfo(Directory.GetDirectoryRoot(directoryPath));
302+
}
303+
304+
return driveInfo;
305+
}
306+
307+
public string GetFullPath(string relativeOrAbsoluteFilePath)
308+
{
309+
try
310+
{
311+
if (!Path.IsPathRooted(relativeOrAbsoluteFilePath))
312+
relativeOrAbsoluteFilePath = Path.Combine(Environment.CurrentDirectory, relativeOrAbsoluteFilePath);
313+
314+
relativeOrAbsoluteFilePath = Path.GetFullPath(relativeOrAbsoluteFilePath);
315+
return relativeOrAbsoluteFilePath;
316+
}
317+
catch (ArgumentException e)
318+
{
319+
throw new ArgumentException($"Error processing path {relativeOrAbsoluteFilePath}. If the path was quoted check you are not accidentally escaping the closing quote with a \\ character. Otherwise ensure the path does not contain any illegal characters.", e);
320+
}
321+
}
322+
323+
public string ReadAllText(string scriptFile)
324+
=> File.ReadAllText(scriptFile);
325+
326+
public string[] ReadAllLines(string scriptFile)
327+
=> File.ReadAllLines(scriptFile);
328+
329+
static bool IsUncPath(string directoryPath)
330+
=> Uri.TryCreate(directoryPath, UriKind.Absolute, out var uri) && uri.IsUnc;
331+
332+
async Task TryToDoSomethingMultipleTimes(
333+
Action<int> thingToDo,
334+
int numberAttempts,
335+
int sleepTime,
336+
bool throwOnFailure,
337+
CancellationToken cancellationToken)
338+
{
339+
if (numberAttempts < 1)
340+
{
341+
Log.Error("Trying to do something less than once, doesn't make much sense");
342+
return;
343+
}
344+
345+
for (var i = 1; i <= numberAttempts; i++)
346+
{
347+
if (cancellationToken.IsCancellationRequested)
348+
break;
349+
350+
try
351+
{
352+
await Task.Run(() => thingToDo(i), cancellationToken);
353+
break;
354+
}
355+
catch (Exception e)
356+
{
357+
Thread.Sleep(sleepTime);
358+
if (i == numberAttempts)
359+
{
360+
if (throwOnFailure)
361+
{
362+
Log.Error(e, $"Failed to complete action, attempted {numberAttempts} time(s), throwing error");
363+
throw;
364+
}
365+
366+
Log.Error(e, $"Failed to complete action, attempted {numberAttempts} time(s), silently moving on...");
367+
break;
368+
}
369+
}
370+
}
371+
}
372+
}
373+
}

source/Octopus.Tentacle/Octopus.Tentacle.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@
5959
<PackageReference Include="Autofac" Version="4.6.2" />
6060
<PackageReference Include="NuGet.Packaging" Version="3.6.0-octopus-58692" />
6161
<PackageReference Include="Octopus.Time" Version="1.1.339" />
62-
<PackageReference Include="Polly" Version="7.2.2" />
6362
<PackageReference Include="TaskScheduler" Version="2.7.2" />
6463
<PackageReference Include="Nito.AsyncEx" Version="5.0.0" />
6564
<PackageReference Include="NLog" Version="5.0.4" />

0 commit comments

Comments
 (0)