1
0
Fork 0
mirror of https://github.com/sbrl/Nibriboard.git synced 2018-01-10 21:33:49 +00:00

[server] Completely refactor saving / loading system to utilise a nested-directory based structure.

This commit is contained in:
Starbeamrainbowlabs 2017-12-26 22:26:44 +00:00
parent 395e92dc05
commit f2aafece24
Signed by: sbrl
GPG key ID: 1BE5172E637709C2
10 changed files with 194 additions and 188 deletions

View file

@ -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];
}
}
}

View file

@ -144,6 +144,7 @@
<Compile Include="Utilities\LineSimplifier.cs" />
<Compile Include="Client\Messages\LineRemoveMessage.cs" />
<Compile Include="CommandConsole.cs" />
<Compile Include="Utilities\Formatters.cs" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="commit-hash.txt" />
@ -179,10 +180,7 @@
<MonoDevelop>
<Properties>
<Policies>
<DotNetNamingPolicy ResourceNamePolicy="FileFormatDefault" DirectoryNamespaceAssociation="PrefixedHierarchical">
<inheritsSet />
<inheritsScope />
</DotNetNamingPolicy>
<DotNetNamingPolicy ResourceNamePolicy="FileFormatDefault" DirectoryNamespaceAssociation="PrefixedHierarchical" />
</Policies>
</Properties>
</MonoDevelop>

View file

@ -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() {

View file

@ -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++)
{

View file

@ -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
/// </summary>
public DateTime TimeLastAccessed { get; private set; } = DateTime.Now;
/// <summary>
/// Whether this chunk is currently empty or not.
/// </summary>
public bool IsEmpty {
get {
return lines.Count == 0;
}
}
/// <summary>
/// Whether this <see cref="Chunk"/> is a primary chunk.
/// Primary chunks are always loaded.
@ -225,13 +238,16 @@ namespace Nibriboard.RippleSpace
public static async Task<Chunk> FromFile(Plane plane, string filename)
{
StreamReader chunkSource = new StreamReader(filename);
Stream chunkSource = File.OpenRead(filename);
return await FromStream(plane, chunkSource);
}
public static async Task<Chunk> FromStream(Plane plane, StreamReader chunkSource)
public static async Task<Chunk> FromStream(Plane plane, Stream chunkSource)
{
BZip2Stream decompressor = new BZip2Stream(chunkSource, CompressionMode.Decompress);
StreamReader decompressedSource = new StreamReader(decompressor);
Chunk loadedChunk = JsonConvert.DeserializeObject<Chunk>(
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.
/// </summary>
/// <param name="destination">The destination stream to save the chunk to.</param>
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();
}

View file

@ -15,6 +15,28 @@ namespace Nibriboard.RippleSpace
/// </remarks>
public class ChunkReference : Reference<int>
{
/// <summary>
/// The region size to use.
/// Regions are used when saving and loading to avoid too many files being stored in a
/// single directory.
/// </summary>
public static int RegionSize = 32;
/// <summary>
/// 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 <see cref="RegionSize" /> property.
/// </summary>
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();
}

View file

@ -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,17 +220,20 @@ 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<long> Save()
{
// Save all the chunks to disk
List<Task> chunkSavers = new List<Task>();
@ -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<string> 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<ChunkReference, Chunk> loadedChunkItem in loadedChunkspace)
{
packer.Write("plane-index.json", CalcPaths.UnpackedPlaneIndex(StorageDirectory));
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;
}
foreach(string nextChunkFile in chunkFiles)
{
packer.Write($"{Name}/{Path.GetFileName(nextChunkFile)}", nextChunkFile);
}
}
destination.Flush();
destination.Close();
return totalSize;
}
/// <summary>
@ -351,38 +353,21 @@ namespace Nibriboard.RippleSpace
}
/// <summary>
/// Loads a plane form a given nplane file.
/// Loads a plane from a given nplane file.
/// </summary>
/// <param name="planeName">The name of the plane to load.</param>
/// <param name="storageDirectoryRoot">The directory to which the plane should be unpacked.</param>
/// <param name="sourceFilename">The path to the nplane file to load.</param>
/// <param name="deleteSource">Whether the source file should be deleted once the plane has been loaded.</param>
/// <param name="planeDirectory">The directory from which the plane should be loaded.</param>
/// <returns>The loaded plane.</returns>
public static async Task<Plane> FromFile(string planeName, string storageDirectoryRoot, string sourceFilename, bool deleteSource)
public static async Task<Plane> 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<PlaneInfo>(
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;
}
}

View file

@ -13,16 +13,10 @@ namespace Nibriboard.RippleSpace
{
public class RippleSpaceManager
{
/// <summary>
/// The filename from which this ripplespace was loaded, and the filename to which it should be saved again.
/// </summary>
/// <value>The source filename.</value>
public string SourceFilename { get; set; }
/// <summary>
/// The temporary directory in which we are currently storing our unpacked planes temporarily.
/// </summary>
public string UnpackedDirectory { get; set; }
public string SourceDirectory { get; set; }
/// <summary>
/// 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.
/// </summary>
/// <value>The last size of the save file.</value>
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;
// 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);
}
/// <summary>
@ -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<long> Save()
{
Stopwatch timer = Stopwatch.StartNew();
// Save the planes to disk
List<Task> planeSavers = new List<Task>();
StreamWriter indexWriter = new StreamWriter(UnpackedDirectory + "index.list");
foreach(Plane item in Planes)
List<Task<long>> planeSavers = new List<Task<long>>();
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<long> 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<RippleSpaceManager> 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<Task<Plane>> planeReaders = new List<Task<Plane>>();
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<Plane> 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<RippleSpaceManager> 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<Task<Plane>> planeLoaders = new List<Task<Plane>>();
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<Plane> 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;
}

View file

@ -1,4 +1,5 @@
using System;
using System.IO;
using Nibriboard.RippleSpace;
namespace Nibriboard.Utilities
@ -8,40 +9,29 @@ namespace Nibriboard.Utilities
/// <summary>
/// Returns the directory in which a plane's data should be unpacked to.
/// </summary>
/// <param name="unpackingRoot">The root directory to which everything is going to be unpacked.</param>
/// <param name="storageRoot">The root directory to which everything is going to be unpacked.</param>
/// <param name="planeName">The name of the plane that will be unpacked.</param>
/// <returns>The directory to which a plane should unpack it's data to.</returns>
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;
}
/// <summary>
/// Returns the path to the plane index file given a directory that a plane has been unpacked to.
/// </summary>
/// <param name="unpackingPlaneDir">The directory to which a plane's data has been unpacked.</param>
/// <param name="planeDirectory">The directory to which a plane's data has been unpacked.</param>
/// <returns>The path to the plane index file.</returns>
public static string UnpackedPlaneIndex(string unpackingPlaneDir)
public static string PlaneIndex(string planeDirectory)
{
return $"{unpackingPlaneDir}/plane-index.json";
}
/// <summary>
/// Calculates the path to a packed plane file.
/// </summary>
/// <param name="unpackingDir">The directory to which the nplane files were unpacked.</param>
/// <param name="planeName">The name of the plane to fetch the filepath for.</param>
/// <returns>The path to the packed plane file.</returns>
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());
}
}
}

View file

@ -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];
}
}
}