diff --git a/Nibriboard/CommandConsole.cs b/Nibriboard/CommandConsole.cs index 680a0df..81f26e1 100644 --- a/Nibriboard/CommandConsole.cs +++ b/Nibriboard/CommandConsole.cs @@ -1,10 +1,12 @@ using System; +using System.Diagnostics; using System.IO; using System.Net; using System.Net.Sockets; using System.Threading.Tasks; using Nibriboard.Client; using Nibriboard.RippleSpace; +using Nibriboard.Utilities; namespace Nibriboard { @@ -74,9 +76,12 @@ namespace Nibriboard break; case "save": await destination.WriteAsync("Saving ripple space - "); - await server.PlaneManager.Save(); + Stopwatch timer = Stopwatch.StartNew(); + long bytesWritten = await server.PlaneManager.Save(); + long msTaken = timer.ElapsedMilliseconds; await destination.WriteLineAsync("done."); - await destination.WriteLineAsync($"Save is now {BytesToString(server.PlaneManager.LastSaveFileSize)} in size."); + await destination.WriteLineAsync($"{Formatters.HumanSize(bytesWritten)} written in {msTaken}ms."); + await destination.WriteLineAsync($"Save is now {Formatters.HumanSize(server.PlaneManager.LastSaveSize)} in size."); break; case "plane": if(commandParts.Length < 2) { @@ -194,16 +199,5 @@ namespace Nibriboard break; } } - - public static string BytesToString(long byteCount) - { - string[] suf = { "B", "KB", "MB", "GB", "TB", "PB", "EB" }; // Longs run out around EB - if(byteCount == 0) - return "0" + suf[0]; - long bytes = Math.Abs(byteCount); - int place = (int)Math.Floor(Math.Log(bytes, 1024)); - double num = Math.Round(bytes / Math.Pow(1024, place), 1); - return (Math.Sign(byteCount) * num).ToString() + suf[place]; - } } } diff --git a/Nibriboard/Nibriboard.csproj b/Nibriboard/Nibriboard.csproj index 6f0bbac..fbbcc54 100644 --- a/Nibriboard/Nibriboard.csproj +++ b/Nibriboard/Nibriboard.csproj @@ -144,6 +144,7 @@ + @@ -179,10 +180,7 @@ - - - - + diff --git a/Nibriboard/NibriboardServer.cs b/Nibriboard/NibriboardServer.cs index d54723f..9bcd40a 100644 --- a/Nibriboard/NibriboardServer.cs +++ b/Nibriboard/NibriboardServer.cs @@ -46,22 +46,20 @@ namespace Nibriboard public readonly int CommandPort = 31587; public readonly int Port = 31586; - public RippleSpaceManager PlaneManager; - public NibriboardApp AppServer; + public readonly RippleSpaceManager PlaneManager; + public readonly NibriboardApp AppServer; public NibriboardServer(string pathToRippleSpace, int inPort = 31586) { Port = inPort; - // Load the specified packed ripple space file if it exists - otherwise save it to disk - if(File.Exists(pathToRippleSpace)) - { - PlaneManager = RippleSpaceManager.FromFile(pathToRippleSpace).Result; + // Load the specified ripple space if it exists - otherwise save it to disk + if(Directory.Exists(pathToRippleSpace)) { + PlaneManager = RippleSpaceManager.FromDirectory(pathToRippleSpace).Result; } - else - { + else { Log.WriteLine("[NibriboardServer] Couldn't find packed ripple space at {0} - creating new ripple space instead.", pathToRippleSpace); - PlaneManager = new RippleSpaceManager() { SourceFilename = pathToRippleSpace }; + PlaneManager = new RippleSpaceManager(pathToRippleSpace); } clientSettings = new ClientSettings() { diff --git a/Nibriboard/Program.cs b/Nibriboard/Program.cs index 120d0c5..bcd0bf4 100644 --- a/Nibriboard/Program.cs +++ b/Nibriboard/Program.cs @@ -10,7 +10,7 @@ namespace Nibriboard { public static void Main(string[] args) { - string packedRippleSpaceFile = "./default.ripplespace.zip"; + string packedRippleSpaceFile = "./default-ripplespace"; for(int i = 0; i < args.Length; i++) { diff --git a/Nibriboard/RippleSpace/Chunk.cs b/Nibriboard/RippleSpace/Chunk.cs index b3d2ce5..34fa5ab 100644 --- a/Nibriboard/RippleSpace/Chunk.cs +++ b/Nibriboard/RippleSpace/Chunk.cs @@ -7,6 +7,11 @@ using System.Runtime.Serialization; using Newtonsoft.Json; using Nibriboard.Utilities; +using SharpCompress.Archives; +using SharpCompress.Readers; +using SharpCompress.Common; +using SharpCompress.Compressors.BZip2; +using SharpCompress.Compressors; namespace Nibriboard.RippleSpace { @@ -90,6 +95,14 @@ namespace Nibriboard.RippleSpace /// public DateTime TimeLastAccessed { get; private set; } = DateTime.Now; + /// + /// Whether this chunk is currently empty or not. + /// + public bool IsEmpty { + get { + return lines.Count == 0; + } + } /// /// Whether this is a primary chunk. /// Primary chunks are always loaded. @@ -225,13 +238,16 @@ namespace Nibriboard.RippleSpace public static async Task FromFile(Plane plane, string filename) { - StreamReader chunkSource = new StreamReader(filename); + Stream chunkSource = File.OpenRead(filename); return await FromStream(plane, chunkSource); } - public static async Task FromStream(Plane plane, StreamReader chunkSource) + public static async Task FromStream(Plane plane, Stream chunkSource) { + BZip2Stream decompressor = new BZip2Stream(chunkSource, CompressionMode.Decompress); + StreamReader decompressedSource = new StreamReader(decompressor); + Chunk loadedChunk = JsonConvert.DeserializeObject( - await chunkSource.ReadToEndAsync(), + await decompressedSource.ReadToEndAsync(), new JsonSerializerSettings() { MissingMemberHandling = MissingMemberHandling.Ignore, NullValueHandling = NullValueHandling.Ignore @@ -256,9 +272,13 @@ namespace Nibriboard.RippleSpace /// Saves this chunk to the specified stream. /// /// The destination stream to save the chunk to. - public async Task SaveTo(StreamWriter destination) + public async Task SaveTo(Stream destination) { - await destination.WriteLineAsync(JsonConvert.SerializeObject(this)); + BZip2Stream compressor = new BZip2Stream(destination, CompressionMode.Compress); + StreamWriter destWriter = new StreamWriter(compressor) { AutoFlush = true }; + + await destWriter.WriteLineAsync(JsonConvert.SerializeObject(this)); + compressor.Close(); destination.Close(); } diff --git a/Nibriboard/RippleSpace/ChunkReference.cs b/Nibriboard/RippleSpace/ChunkReference.cs index c1b2cac..0631a0a 100644 --- a/Nibriboard/RippleSpace/ChunkReference.cs +++ b/Nibriboard/RippleSpace/ChunkReference.cs @@ -15,6 +15,28 @@ namespace Nibriboard.RippleSpace /// public class ChunkReference : Reference { + /// + /// The region size to use. + /// Regions are used when saving and loading to avoid too many files being stored in a + /// single directory. + /// + public static int RegionSize = 32; + + /// + /// Converts this ChunkReference into a RegionReference. + /// A RegionReference is used when saving, to work out which folder it should go in. + /// The size of the regions used is determined by the property. + /// + public ChunkReference RegionReference { + get { + return new ChunkReference( + Plane, + (int)Math.Floor((float)X / (float)RegionSize), + (int)Math.Floor((float)Y / (float)RegionSize) + ); + } + } + public ChunkReference(Plane inPlane, int inX, int inY) : base(inPlane, inX, inY) { @@ -54,12 +76,12 @@ namespace Nibriboard.RippleSpace ); } - public string AsFilename() + public string AsFilepath() { - return $"{Plane.Name}-{X},{Y}.chunk"; + return Path.Combine($"Region_{RegionReference.X},{RegionReference.Y}", $"{X},{Y}.chunk"); } - public override int GetHashCode () + public override int GetHashCode() { return $"({Plane.Name})+{X}+{Y}".GetHashCode(); } diff --git a/Nibriboard/RippleSpace/Plane.cs b/Nibriboard/RippleSpace/Plane.cs index 8d2e8b3..8ff6c09 100644 --- a/Nibriboard/RippleSpace/Plane.cs +++ b/Nibriboard/RippleSpace/Plane.cs @@ -179,7 +179,7 @@ namespace Nibriboard.RippleSpace // Uh-oh! The chunk isn't loaded at moment. Load it quick & then // return it fast. - string chunkFilePath = Path.Combine(StorageDirectory, chunkLocation.AsFilename()); + string chunkFilePath = Path.Combine(StorageDirectory, chunkLocation.AsFilepath()); Chunk loadedChunk; if(File.Exists(chunkFilePath)) // If the chunk exists on disk, load it loadedChunk = await Chunk.FromFile(this, chunkFilePath); @@ -210,7 +210,7 @@ namespace Nibriboard.RippleSpace if(loadedChunkspace.ContainsKey(chunkLocation)) return true; - string chunkFilePath = Path.Combine(StorageDirectory, chunkLocation.AsFilename()); + string chunkFilePath = Path.Combine(StorageDirectory, chunkLocation.AsFilepath()); if(File.Exists(chunkFilePath)) return true; @@ -220,16 +220,19 @@ namespace Nibriboard.RippleSpace public async Task SaveChunk(ChunkReference chunkLocation) { // It doesn't exist, so we can't save it :P - if(!loadedChunkspace.ContainsKey(chunkLocation)) + if (!loadedChunkspace.ContainsKey(chunkLocation)) return; Chunk chunk = loadedChunkspace[chunkLocation]; - string chunkFilePath = Path.Combine(StorageDirectory, chunkLocation.AsFilename()); - using(StreamWriter chunkDestination = new StreamWriter(chunkFilePath)) - { + // If it's empty, then there's no point in saving it + if (chunk.IsEmpty) + return; + + string chunkFilePath = Path.Combine(StorageDirectory, chunkLocation.AsFilepath()); + + using (Stream chunkDestination = File.Open(chunkFilePath, FileMode.OpenOrCreate)) await chunk.SaveTo(chunkDestination); - } } public async Task AddLine(DrawnLine newLine) @@ -279,7 +282,7 @@ namespace Nibriboard.RippleSpace // This chunk has been inactive for a while - let's serialise it and save it to disk Stream chunkSerializationSink = new FileStream( - Path.Combine(StorageDirectory, chunkEntry.Key.AsFilename()), + Path.Combine(StorageDirectory, chunkEntry.Key.AsFilepath()), FileMode.Create, FileAccess.Write, FileShare.None @@ -292,7 +295,7 @@ namespace Nibriboard.RippleSpace } } - public async Task Save(Stream destination) + public async Task Save() { // Save all the chunks to disk List chunkSavers = new List(); @@ -301,32 +304,31 @@ namespace Nibriboard.RippleSpace // Figure out where to put the chunk and create the relevant directories string chunkDestinationFilename = CalcPaths.ChunkFilepath(StorageDirectory, loadedChunkItem.Key); Directory.CreateDirectory(Path.GetDirectoryName(chunkDestinationFilename)); - // Ask the chunk to save itself - StreamWriter chunkDestination = new StreamWriter(chunkDestinationFilename); + // Ask the chunk to save itself, but only if it isn't empty + if (loadedChunkItem.Value.IsEmpty) + continue; + + Stream chunkDestination = File.Open(chunkDestinationFilename, FileMode.OpenOrCreate); chunkSavers.Add(loadedChunkItem.Value.SaveTo(chunkDestination)); } await Task.WhenAll(chunkSavers); // Save the plane information - StreamWriter planeInfoWriter = new StreamWriter(CalcPaths.UnpackedPlaneIndex(StorageDirectory)); + StreamWriter planeInfoWriter = new StreamWriter(CalcPaths.PlaneIndex(StorageDirectory)); await planeInfoWriter.WriteLineAsync(JsonConvert.SerializeObject(Info)); planeInfoWriter.Close(); - // Pack the chunks & plane information into an nplane file - WriterOptions packingOptions = new WriterOptions(CompressionType.Deflate); - - IEnumerable chunkFiles = Directory.GetFiles(StorageDirectory.TrimEnd("/".ToCharArray())); - using(IWriter packer = WriterFactory.Open(destination, ArchiveType.Zip, packingOptions)) + // Calculate the total number bytes written + long totalSize = 0; + foreach (KeyValuePair loadedChunkItem in loadedChunkspace) { - packer.Write("plane-index.json", CalcPaths.UnpackedPlaneIndex(StorageDirectory)); - - foreach(string nextChunkFile in chunkFiles) - { - packer.Write($"{Name}/{Path.GetFileName(nextChunkFile)}", nextChunkFile); - } + string destFilename = CalcPaths.ChunkFilepath(StorageDirectory, loadedChunkItem.Key); + if (!File.Exists(destFilename)) // Don't assume that the file exists - it might be an empty chunk + continue; + totalSize += (new FileInfo(destFilename)).Length; } - destination.Flush(); - destination.Close(); + + return totalSize; } /// @@ -351,38 +353,21 @@ namespace Nibriboard.RippleSpace } /// - /// Loads a plane form a given nplane file. + /// Loads a plane from a given nplane file. /// - /// The name of the plane to load. - /// The directory to which the plane should be unpacked. - /// The path to the nplane file to load. - /// Whether the source file should be deleted once the plane has been loaded. + /// The directory from which the plane should be loaded. /// The loaded plane. - public static async Task FromFile(string planeName, string storageDirectoryRoot, string sourceFilename, bool deleteSource) + public static async Task FromDirectory(string planeDirectory) { - string targetUnpackingPath = CalcPaths.UnpackedPlaneDir(storageDirectoryRoot, planeName); - - // Unpack the plane to the temporary directory - using(Stream sourceStream = File.OpenRead(sourceFilename)) - using(IReader unpacker = ReaderFactory.Open(sourceStream)) - { - Directory.CreateDirectory(targetUnpackingPath); - unpacker.WriteAllToDirectory(targetUnpackingPath); - } - PlaneInfo planeInfo = JsonConvert.DeserializeObject( - File.ReadAllText(CalcPaths.UnpackedPlaneIndex(targetUnpackingPath)) + File.ReadAllText(CalcPaths.PlaneIndex(planeDirectory)) ); - planeInfo.Name = planeName; - Plane loadedPlane = new Plane(planeInfo, targetUnpackingPath); + Plane loadedPlane = new Plane(planeInfo, planeDirectory); - // Load the primary chunks from disk inot the plane + // Load the primary chunks into the plane await loadedPlane.LoadPrimaryChunks(); - if(deleteSource) - File.Delete(sourceFilename); - return loadedPlane; } } diff --git a/Nibriboard/RippleSpace/RippleSpaceManager.cs b/Nibriboard/RippleSpace/RippleSpaceManager.cs index d327437..a237446 100644 --- a/Nibriboard/RippleSpace/RippleSpaceManager.cs +++ b/Nibriboard/RippleSpace/RippleSpaceManager.cs @@ -13,16 +13,10 @@ namespace Nibriboard.RippleSpace { public class RippleSpaceManager { - /// - /// The filename from which this ripplespace was loaded, and the filename to which it should be saved again. - /// - /// The source filename. - public string SourceFilename { get; set; } - /// /// The temporary directory in which we are currently storing our unpacked planes temporarily. /// - public string UnpackedDirectory { get; set; } + public string SourceDirectory { get; set; } /// /// The master list of planes that this PlaneManager is in charge of. @@ -46,28 +40,31 @@ namespace Nibriboard.RippleSpace /// Returns 0 if this RippleSpace hasn't been saved yet. /// /// The last size of the save file. - public long LastSaveFileSize { + public long LastSaveSize { get { - if(!File.Exists(SourceFilename)) + if(!Directory.Exists(SourceDirectory)) return 0; - - return (new FileInfo(SourceFilename)).Length; + + return (new DirectoryInfo(SourceDirectory)) + .GetFiles("*", SearchOption.AllDirectories) + .Sum(file => file.Length); } } - public RippleSpaceManager() + public RippleSpaceManager(string inSourceDirectory) { - // Create a temporary directory in which to store our unpacked planes - UnpackedDirectory = Path.GetTempFileName(); - File.Delete(UnpackedDirectory); - UnpackedDirectory = Path.GetDirectoryName(UnpackedDirectory) + "/ripplespace-" + Path.GetFileName(UnpackedDirectory) + "/"; - Directory.CreateDirectory(UnpackedDirectory); + SourceDirectory = inSourceDirectory; - Log.WriteLine("[RippleSpace] New blank ripplespace initialised."); + // Make sure that the source directory exists + if (!Directory.Exists(SourceDirectory)) { + Directory.CreateDirectory(SourceDirectory); + + Log.WriteLine("[RippleSpace] New blank ripplespace initialised."); + } } ~RippleSpaceManager() { - Directory.Delete(UnpackedDirectory, true); + Directory.Delete(SourceDirectory, true); } /// @@ -110,7 +107,7 @@ namespace Nibriboard.RippleSpace Plane newPlane = new Plane( newPlaneInfo, - CalcPaths.UnpackedPlaneDir(UnpackedDirectory, newPlaneInfo.Name) + CalcPaths.PlaneDirectory(SourceDirectory, newPlaneInfo.Name) ); Planes.Add(newPlane); return newPlane; @@ -142,88 +139,72 @@ namespace Nibriboard.RippleSpace } } - public async Task Save() + public async Task Save() { Stopwatch timer = Stopwatch.StartNew(); // Save the planes to disk - List planeSavers = new List(); - StreamWriter indexWriter = new StreamWriter(UnpackedDirectory + "index.list"); - foreach(Plane item in Planes) + List> planeSavers = new List>(); + StreamWriter indexWriter = new StreamWriter(SourceDirectory + "index.list"); + foreach(Plane currentPlane in Planes) { // Add the plane to the index - await indexWriter.WriteLineAsync(item.Name); - - // Figure out where the plane should save itself to and create the appropriate directories - string planeSavePath = CalcPaths.UnpackedPlaneFile(UnpackedDirectory, item.Name); - Directory.CreateDirectory(Path.GetDirectoryName(planeSavePath)); + await indexWriter.WriteLineAsync(currentPlane.Name); // Ask the plane to save to the directory - planeSavers.Add(item.Save(File.OpenWrite(planeSavePath))); + planeSavers.Add(currentPlane.Save()); } indexWriter.Close(); await Task.WhenAll(planeSavers); + long totalBytesWritten = planeSavers.Sum((Task saver) => saver.Result); - // Pack the planes into the ripplespace archive - Stream destination = File.OpenWrite(SourceFilename); - string[] planeFiles = Directory.GetFiles(UnpackedDirectory, "*.nplane.zip", SearchOption.TopDirectoryOnly); - - using(IWriter rippleSpacePacker = WriterFactory.Open(destination, ArchiveType.Zip, new WriterOptions(CompressionType.Deflate))) - { - rippleSpacePacker.Write("index.list", UnpackedDirectory + "index.list"); - foreach(string planeFilename in planeFiles) - { - rippleSpacePacker.Write(Path.GetFileName(planeFilename), planeFilename); - } - } - destination.Close(); - - Log.WriteLine("[Command/Save] Save complete in {0}ms", timer.ElapsedMilliseconds); - } - - public static async Task FromFile(string filename) - { - if(!File.Exists(filename)) - throw new FileNotFoundException($"Error: Couldn't find the packed ripplespace at {filename}"); - - RippleSpaceManager rippleSpace = new RippleSpaceManager(); - rippleSpace.SourceFilename = filename; - - using(Stream packedRippleSpaceStream = File.OpenRead(filename)) - using(IReader rippleSpaceUnpacker = ReaderFactory.Open(packedRippleSpaceStream)) - { - Log.WriteLine($"[Core] Unpacking ripplespace packed with {rippleSpaceUnpacker.ArchiveType} from {filename}."); - rippleSpaceUnpacker.WriteAllToDirectory(rippleSpace.UnpackedDirectory); - } - Log.WriteLine("[Core] done!"); - - if(!File.Exists(rippleSpace.UnpackedDirectory + "index.list")) - throw new InvalidDataException($"Error: The packed ripplespace at {filename} doesn't appear to contain an index file."); - - Log.WriteLine("[Core] Importing planes"); - - StreamReader planes = new StreamReader(rippleSpace.UnpackedDirectory + "index.list"); - List> planeReaders = new List>(); - string nextPlane; - int planeCount = 0; - while((nextPlane = await planes.ReadLineAsync()) != null) - { - planeReaders.Add(Plane.FromFile( - planeName: nextPlane, - storageDirectoryRoot: rippleSpace.UnpackedDirectory, - sourceFilename: CalcPaths.UnpackedPlaneFile(rippleSpace.UnpackedDirectory, nextPlane), - deleteSource: true - )); - planeCount++; - } - await Task.WhenAll(planeReaders); - - rippleSpace.Planes.AddRange( - planeReaders.Select((Task planeReader) => planeReader.Result) + Log.WriteLine( + "[Command/Save] Save complete - {0} written in {1}ms", + Formatters.HumanSize(totalBytesWritten), + timer.ElapsedMilliseconds ); - Log.WriteLine("[Core] done! {0} planes loaded.", planeCount); + return totalBytesWritten; + } + + public static async Task FromDirectory(string sourceDirectory) + { + + RippleSpaceManager rippleSpace = new RippleSpaceManager(sourceDirectory); + + if (!Directory.Exists(sourceDirectory)) + { + Log.WriteLine($"[Core] Creating new ripplespace in {sourceDirectory}."); + return rippleSpace; + } + + Log.WriteLine($"[Core] Loading ripplespace from {sourceDirectory}."); + + // Load the planes in + if (!File.Exists(rippleSpace.SourceDirectory + "index.list")) + throw new InvalidDataException($"Error: The ripplespace at {sourceDirectory} doesn't appear to contain an index file."); + + Log.WriteLine("[Core] Importing planes"); + Stopwatch timer = Stopwatch.StartNew(); + + StreamReader planeList = new StreamReader(sourceDirectory + "index.list"); + + List> planeLoaders = new List>(); + string nextPlaneName = string.Empty; + while ((nextPlaneName = await planeList.ReadLineAsync()) != null) + { + planeLoaders.Add(Plane.FromDirectory(CalcPaths.PlaneDirectory(sourceDirectory, nextPlaneName))); + } + await Task.WhenAll(planeLoaders); + + + rippleSpace.Planes.AddRange( + planeLoaders.Select((Task planeLoader) => planeLoader.Result) + ); + + long msTaken = timer.ElapsedMilliseconds; + Log.WriteLine($"[Core] done! {rippleSpace.Planes.Count} plane{(rippleSpace.Planes.Count != 1?"s":"")} loaded in {msTaken}ms."); return rippleSpace; } diff --git a/Nibriboard/Utilities/CalcPaths.cs b/Nibriboard/Utilities/CalcPaths.cs index 44ae3de..4fc55da 100644 --- a/Nibriboard/Utilities/CalcPaths.cs +++ b/Nibriboard/Utilities/CalcPaths.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using Nibriboard.RippleSpace; namespace Nibriboard.Utilities @@ -8,40 +9,29 @@ namespace Nibriboard.Utilities /// /// Returns the directory in which a plane's data should be unpacked to. /// - /// The root directory to which everything is going to be unpacked. + /// The root directory to which everything is going to be unpacked. /// The name of the plane that will be unpacked. /// The directory to which a plane should unpack it's data to. - public static string UnpackedPlaneDir(string unpackingRoot, string planeName) + public static string PlaneDirectory(string storageRoot, string planeName) { - string result = $"{unpackingRoot}Planes/{planeName}/"; + string result = Path.Combine(storageRoot, "Planes", planeName); return result; } /// /// Returns the path to the plane index file given a directory that a plane has been unpacked to. /// - /// The directory to which a plane's data has been unpacked. + /// The directory to which a plane's data has been unpacked. /// The path to the plane index file. - public static string UnpackedPlaneIndex(string unpackingPlaneDir) + public static string PlaneIndex(string planeDirectory) { - return $"{unpackingPlaneDir}/plane-index.json"; - } - - /// - /// Calculates the path to a packed plane file. - /// - /// The directory to which the nplane files were unpacked. - /// The name of the plane to fetch the filepath for. - /// The path to the packed plane file. - public static string UnpackedPlaneFile(string unpackingDir, string planeName) - { - return $"{unpackingDir}{planeName}.nplane.zip"; + return Path.Combine(planeDirectory, "plane-index.json"); } public static string ChunkFilepath(string planeStorageDirectory, ChunkReference chunkRef) { - return $"{planeStorageDirectory}{chunkRef.AsFilename()}"; + return Path.Combine(planeStorageDirectory, chunkRef.AsFilepath()); } } } diff --git a/Nibriboard/Utilities/Formatters.cs b/Nibriboard/Utilities/Formatters.cs new file mode 100644 index 0000000..7b3a910 --- /dev/null +++ b/Nibriboard/Utilities/Formatters.cs @@ -0,0 +1,18 @@ +using System; + +namespace Nibriboard.Utilities +{ + public static class Formatters + { + public static string HumanSize(long byteCount) + { + string[] suf = { "B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB" }; // longs run out around EiB + if (byteCount == 0) + return "0" + suf[0]; + long bytes = Math.Abs(byteCount); + int place = Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024))); + double num = Math.Round(bytes / Math.Pow(1024, place), 1); + return (Math.Sign(byteCount) * num).ToString() + suf[place]; + } + } +}