Skip to content

Instantly share code, notes, and snippets.

@OFark
Created January 1, 2023 12:13
Show Gist options
  • Save OFark/371da5465e6a39def4cf96c7efc94198 to your computer and use it in GitHub Desktop.
Save OFark/371da5465e6a39def4cf96c7efc94198 to your computer and use it in GitHub Desktop.
Balances Unraid drives
<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