Created
January 1, 2023 12:13
-
-
Save OFark/371da5465e6a39def4cf96c7efc94198 to your computer and use it in GitHub Desktop.
Balances Unraid drives
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<Query Kind="Program"> | |
<NuGetReference>ByteSize</NuGetReference> | |
<Namespace>ByteSizeLib</Namespace> | |
<Namespace>System.Diagnostics.CodeAnalysis</Namespace> | |
<Namespace>System.Runtime.InteropServices</Namespace> | |
<Namespace>System.Security</Namespace> | |
<Namespace>System.Text.Json</Namespace> | |
</Query> | |
string filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "unraidMap.Json"); | |
string[] unraidDrives = new[] { """\\Server\disk1\""", | |
"""\\Server\disk2\""", | |
"""\\Server\disk3\""", | |
"""\\Server\disk4\""", | |
"""\\Server\disk5\""", | |
"""\\Server\disk6\""" }; | |
void Main() | |
{ | |
var drives = ReadJsonMap(); | |
drives.Select(d => new { d.Path, d.FreeSpace, Available = ByteSize.FromBytes(d.FreeSpace) }).Dump(); | |
var targetFreeSpace = Math.Round(drives.Average(x => x.FreeSpace)); | |
var Moves = new List<MoveDetails>(); | |
var i = 0; | |
while (FlattenDrives(drives, out var moves)) | |
{ | |
Moves.AddRange(moves); | |
i++; | |
if (i % 100 == 0) | |
{ | |
drives.Select(d => new { d.Path, d.FreeSpace }).Dump(); | |
} | |
} | |
Moves.OrderBy(x => x.TargetDrive).ThenBy(x => x.Folder).Dump(); | |
targetFreeSpace.Dump(); | |
ByteSize.FromBytes(targetFreeSpace).Dump(); | |
drives.Select(d => new { d.Path, d.FreeSpace, Available = ByteSize.FromBytes(d.FreeSpace) }).Dump(); | |
FindEmptyFolders(drives).Dump(); | |
} | |
public bool FlattenDrives(List<DriveDetails> drives, out List<MoveDetails> Moves) | |
{ | |
var targetFreeSpace = Math.Round(drives.Average(x => x.FreeSpace)); | |
var doWork = false; | |
var moves = new List<MoveDetails>(); | |
foreach (var drive in drives) | |
{ | |
var totalSize = drive.PathDetails.Sum(pd => pd.Size); | |
var targetMoves = drive.PathDetails.Where(pd => pd.Size > 0).Select(pd => new { pd, OverTarget = targetFreeSpace - (drive.FreeSpace + pd.Size) }); | |
var possibleMoves = targetMoves.Where(m => m.OverTarget > 0 && drives.Any(d => (d.FreeSpace - m.pd.Size) > targetFreeSpace)); | |
var move = possibleMoves.OrderBy(m => m.OverTarget).FirstOrDefault(); | |
if (move is not null) | |
{ | |
doWork = true; | |
var targetDrive = drives.OrderByDescending(d => (d.FreeSpace - move.pd.Size)).First(); | |
moves.Add(new(drive.Path, targetDrive.Path, move.pd.Path, move.pd.Size)); | |
targetDrive.PathDetails.Add(move.pd); | |
targetDrive.FreeSpace -= move.pd.Size; | |
drive.PathDetails.Remove(move.pd); | |
drive.FreeSpace += move.pd.Size; | |
continue; | |
} | |
} | |
Moves = moves; | |
return doWork; | |
} | |
public IEnumerable<MoveDetails> FindEmptyFolders(List<DriveDetails> drives) | |
{ | |
var emptyFolders = new HashSet<(PathDetails pd, string sourceDrive)>(); | |
var matchedFolders = new HashSet<MoveDetails>(); | |
foreach (var drive in drives) | |
{ | |
foreach (var m in emptyFolders.IntersectBy(drive.PathDetails.Where(pd => pd.Size > 0).Select(pd => pd.Path), x => x.pd.Path)) | |
matchedFolders.Add(new(m.sourceDrive, drive.Path, m.pd.Path, 0)); | |
foreach (var f in drive.PathDetails.Where(pd => pd.Size == 0)) | |
emptyFolders.Add((f, drive.Path)); | |
} | |
return matchedFolders; | |
} | |
public List<DriveDetails> ReadJsonMap() | |
{ | |
if (!File.Exists(filePath)) | |
{ | |
GenerateJsonMap(); | |
} | |
var drives = JsonSerializer.Deserialize<List<DriveDetails>>(File.ReadAllText(filePath)); | |
return drives; | |
} | |
public void GenerateJsonMap() | |
{ | |
var drives = unraidDrives.Select(d => new DriveDetails(d)).ToList(); | |
var splitDepths = new Dictionary<string, int>() { | |
{"Media", 2} | |
}; | |
foreach (var d in drives) | |
{ | |
long free = 0, dum1 = 0, dum2 = 0; | |
if (GetDiskFreeSpaceEx(d.Path, ref free, ref dum1, ref dum2)) | |
{ | |
d.FreeSpace = free; | |
var dir = new DirectoryInfo(d.Path); | |
foreach (var sDir in dir.GetDirectories()) | |
{ | |
if (splitDepths.ContainsKey(sDir.Name)) | |
{ | |
d.PathDetails.AddRange(RecurseFolder(sDir, splitDepths[sDir.Name], sDir.Name).Select(x => new PathDetails(x))); | |
continue; | |
} | |
d.PathDetails.Add(new(sDir.Name)); | |
} | |
foreach (var pd in d.PathDetails) | |
{ | |
var di = new DirectoryInfo(Path.Combine(d.Path, pd.Path)); | |
pd.Size = di.GetDirectorySize(true); | |
} | |
} | |
} | |
File.WriteAllText(filePath, JsonSerializer.Serialize(drives)); | |
} | |
public List<string> RecurseFolder(DirectoryInfo dir, int count, string folderPath) | |
{ | |
count--; | |
var results = new List<string>(); | |
foreach (var sDir in dir.GetDirectories()) | |
{ | |
if (count > 0) | |
{ | |
results.AddRange(RecurseFolder(sDir, count, Path.Combine(folderPath, sDir.Name))); | |
continue; | |
} | |
results.Add(Path.Combine(folderPath, sDir.Name)); | |
} | |
return results; | |
} | |
public record DriveDetails(string Path) | |
{ | |
public long FreeSpace { get; set; } | |
public List<PathDetails> PathDetails { get; set; } = new(); | |
} | |
public record PathDetails(string Path) | |
{ | |
public long Size { get; set; } | |
} | |
public record MoveDetails(string SourceDrive, string TargetDrive, string Folder, long Size) | |
{ | |
} | |
[SuppressMessage("Microsoft.Security", "CA2118:ReviewSuppressUnmanagedCodeSecurityUsage"), SuppressUnmanagedCodeSecurity] | |
[DllImport("Kernel32", SetLastError = true, CharSet = CharSet.Auto)] | |
[return: MarshalAs(UnmanagedType.Bool)] | |
private static extern bool GetDiskFreeSpaceEx | |
( | |
string lpszPath, // Must name a folder, must end with '\'. | |
ref long lpFreeBytesAvailable, | |
ref long lpTotalNumberOfBytes, | |
ref long lpTotalNumberOfFreeBytes | |
); | |
public static class MyExtensions | |
{ | |
public static long GetDirectorySize(this System.IO.DirectoryInfo directoryInfo, bool recursive = true) | |
{ | |
var startDirectorySize = default(long); | |
if (directoryInfo == null || !directoryInfo.Exists) | |
return startDirectorySize; //Return 0 while Directory does not exist. | |
//Add size of files in the Current Directory to main size. | |
foreach (var fileInfo in directoryInfo.GetFiles()) | |
System.Threading.Interlocked.Add(ref startDirectorySize, fileInfo.Length); | |
if (recursive) //Loop on Sub Direcotries in the Current Directory and Calculate it's files size. | |
System.Threading.Tasks.Parallel.ForEach(directoryInfo.GetDirectories(), (subDirectory) => | |
System.Threading.Interlocked.Add(ref startDirectorySize, GetDirectorySize(subDirectory, recursive))); | |
return startDirectorySize; //Return full Size of this Directory. | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment