1
0
Fork 0

[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;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Net; using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading.Tasks; using System.Threading.Tasks;
using Nibriboard.Client; using Nibriboard.Client;
using Nibriboard.RippleSpace; using Nibriboard.RippleSpace;
using Nibriboard.Utilities;
namespace Nibriboard namespace Nibriboard
{ {
@ -74,9 +76,12 @@ namespace Nibriboard
break; break;
case "save": case "save":
await destination.WriteAsync("Saving ripple space - "); 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("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; break;
case "plane": case "plane":
if(commandParts.Length < 2) { if(commandParts.Length < 2) {
@ -194,16 +199,5 @@ namespace Nibriboard
break; 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="Utilities\LineSimplifier.cs" />
<Compile Include="Client\Messages\LineRemoveMessage.cs" /> <Compile Include="Client\Messages\LineRemoveMessage.cs" />
<Compile Include="CommandConsole.cs" /> <Compile Include="CommandConsole.cs" />
<Compile Include="Utilities\Formatters.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="commit-hash.txt" /> <EmbeddedResource Include="commit-hash.txt" />
@ -179,10 +180,7 @@
<MonoDevelop> <MonoDevelop>
<Properties> <Properties>
<Policies> <Policies>
<DotNetNamingPolicy ResourceNamePolicy="FileFormatDefault" DirectoryNamespaceAssociation="PrefixedHierarchical"> <DotNetNamingPolicy ResourceNamePolicy="FileFormatDefault" DirectoryNamespaceAssociation="PrefixedHierarchical" />
<inheritsSet />
<inheritsScope />
</DotNetNamingPolicy>
</Policies> </Policies>
</Properties> </Properties>
</MonoDevelop> </MonoDevelop>

View File

@ -46,22 +46,20 @@ namespace Nibriboard
public readonly int CommandPort = 31587; public readonly int CommandPort = 31587;
public readonly int Port = 31586; public readonly int Port = 31586;
public RippleSpaceManager PlaneManager; public readonly RippleSpaceManager PlaneManager;
public NibriboardApp AppServer; public readonly NibriboardApp AppServer;
public NibriboardServer(string pathToRippleSpace, int inPort = 31586) public NibriboardServer(string pathToRippleSpace, int inPort = 31586)
{ {
Port = inPort; Port = inPort;
// Load the specified packed ripple space file if it exists - otherwise save it to disk // Load the specified ripple space if it exists - otherwise save it to disk
if(File.Exists(pathToRippleSpace)) if(Directory.Exists(pathToRippleSpace)) {
{ PlaneManager = RippleSpaceManager.FromDirectory(pathToRippleSpace).Result;
PlaneManager = RippleSpaceManager.FromFile(pathToRippleSpace).Result;
} }
else else {
{
Log.WriteLine("[NibriboardServer] Couldn't find packed ripple space at {0} - creating new ripple space instead.", pathToRippleSpace); 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() { clientSettings = new ClientSettings() {

View File

@ -10,7 +10,7 @@ namespace Nibriboard
{ {
public static void Main(string[] args) public static void Main(string[] args)
{ {
string packedRippleSpaceFile = "./default.ripplespace.zip"; string packedRippleSpaceFile = "./default-ripplespace";
for(int i = 0; i < args.Length; i++) for(int i = 0; i < args.Length; i++)
{ {

View File

@ -7,6 +7,11 @@ using System.Runtime.Serialization;
using Newtonsoft.Json; using Newtonsoft.Json;
using Nibriboard.Utilities; using Nibriboard.Utilities;
using SharpCompress.Archives;
using SharpCompress.Readers;
using SharpCompress.Common;
using SharpCompress.Compressors.BZip2;
using SharpCompress.Compressors;
namespace Nibriboard.RippleSpace namespace Nibriboard.RippleSpace
{ {
@ -90,6 +95,14 @@ namespace Nibriboard.RippleSpace
/// </summary> /// </summary>
public DateTime TimeLastAccessed { get; private set; } = DateTime.Now; 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> /// <summary>
/// Whether this <see cref="Chunk"/> is a primary chunk. /// Whether this <see cref="Chunk"/> is a primary chunk.
/// Primary chunks are always loaded. /// Primary chunks are always loaded.
@ -225,13 +238,16 @@ namespace Nibriboard.RippleSpace
public static async Task<Chunk> FromFile(Plane plane, string filename) 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); 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>( Chunk loadedChunk = JsonConvert.DeserializeObject<Chunk>(
await chunkSource.ReadToEndAsync(), await decompressedSource.ReadToEndAsync(),
new JsonSerializerSettings() { new JsonSerializerSettings() {
MissingMemberHandling = MissingMemberHandling.Ignore, MissingMemberHandling = MissingMemberHandling.Ignore,
NullValueHandling = NullValueHandling.Ignore NullValueHandling = NullValueHandling.Ignore
@ -256,9 +272,13 @@ namespace Nibriboard.RippleSpace
/// Saves this chunk to the specified stream. /// Saves this chunk to the specified stream.
/// </summary> /// </summary>
/// <param name="destination">The destination stream to save the chunk to.</param> /// <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(); destination.Close();
} }

View File

@ -15,6 +15,28 @@ namespace Nibriboard.RippleSpace
/// </remarks> /// </remarks>
public class ChunkReference : Reference<int> 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) 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(); 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 // Uh-oh! The chunk isn't loaded at moment. Load it quick & then
// return it fast. // return it fast.
string chunkFilePath = Path.Combine(StorageDirectory, chunkLocation.AsFilename()); string chunkFilePath = Path.Combine(StorageDirectory, chunkLocation.AsFilepath());
Chunk loadedChunk; Chunk loadedChunk;
if(File.Exists(chunkFilePath)) // If the chunk exists on disk, load it if(File.Exists(chunkFilePath)) // If the chunk exists on disk, load it
loadedChunk = await Chunk.FromFile(this, chunkFilePath); loadedChunk = await Chunk.FromFile(this, chunkFilePath);
@ -210,7 +210,7 @@ namespace Nibriboard.RippleSpace
if(loadedChunkspace.ContainsKey(chunkLocation)) if(loadedChunkspace.ContainsKey(chunkLocation))
return true; return true;
string chunkFilePath = Path.Combine(StorageDirectory, chunkLocation.AsFilename()); string chunkFilePath = Path.Combine(StorageDirectory, chunkLocation.AsFilepath());
if(File.Exists(chunkFilePath)) if(File.Exists(chunkFilePath))
return true; return true;
@ -220,16 +220,19 @@ namespace Nibriboard.RippleSpace
public async Task SaveChunk(ChunkReference chunkLocation) public async Task SaveChunk(ChunkReference chunkLocation)
{ {
// It doesn't exist, so we can't save it :P // It doesn't exist, so we can't save it :P
if(!loadedChunkspace.ContainsKey(chunkLocation)) if (!loadedChunkspace.ContainsKey(chunkLocation))
return; return;
Chunk chunk = loadedChunkspace[chunkLocation]; 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); await chunk.SaveTo(chunkDestination);
}
} }
public async Task AddLine(DrawnLine newLine) 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 // This chunk has been inactive for a while - let's serialise it and save it to disk
Stream chunkSerializationSink = new FileStream( Stream chunkSerializationSink = new FileStream(
Path.Combine(StorageDirectory, chunkEntry.Key.AsFilename()), Path.Combine(StorageDirectory, chunkEntry.Key.AsFilepath()),
FileMode.Create, FileMode.Create,
FileAccess.Write, FileAccess.Write,
FileShare.None 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 // Save all the chunks to disk
List<Task> chunkSavers = new List<Task>(); 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 // Figure out where to put the chunk and create the relevant directories
string chunkDestinationFilename = CalcPaths.ChunkFilepath(StorageDirectory, loadedChunkItem.Key); string chunkDestinationFilename = CalcPaths.ChunkFilepath(StorageDirectory, loadedChunkItem.Key);
Directory.CreateDirectory(Path.GetDirectoryName(chunkDestinationFilename)); Directory.CreateDirectory(Path.GetDirectoryName(chunkDestinationFilename));
// Ask the chunk to save itself // Ask the chunk to save itself, but only if it isn't empty
StreamWriter chunkDestination = new StreamWriter(chunkDestinationFilename); if (loadedChunkItem.Value.IsEmpty)
continue;
Stream chunkDestination = File.Open(chunkDestinationFilename, FileMode.OpenOrCreate);
chunkSavers.Add(loadedChunkItem.Value.SaveTo(chunkDestination)); chunkSavers.Add(loadedChunkItem.Value.SaveTo(chunkDestination));
} }
await Task.WhenAll(chunkSavers); await Task.WhenAll(chunkSavers);
// Save the plane information // Save the plane information
StreamWriter planeInfoWriter = new StreamWriter(CalcPaths.UnpackedPlaneIndex(StorageDirectory)); StreamWriter planeInfoWriter = new StreamWriter(CalcPaths.PlaneIndex(StorageDirectory));
await planeInfoWriter.WriteLineAsync(JsonConvert.SerializeObject(Info)); await planeInfoWriter.WriteLineAsync(JsonConvert.SerializeObject(Info));
planeInfoWriter.Close(); planeInfoWriter.Close();
// Pack the chunks & plane information into an nplane file // Calculate the total number bytes written
WriterOptions packingOptions = new WriterOptions(CompressionType.Deflate); long totalSize = 0;
foreach (KeyValuePair<ChunkReference, Chunk> loadedChunkItem in loadedChunkspace)
IEnumerable<string> chunkFiles = Directory.GetFiles(StorageDirectory.TrimEnd("/".ToCharArray()));
using(IWriter packer = WriterFactory.Open(destination, ArchiveType.Zip, packingOptions))
{ {
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
foreach(string nextChunkFile in chunkFiles) continue;
{ totalSize += (new FileInfo(destFilename)).Length;
packer.Write($"{Name}/{Path.GetFileName(nextChunkFile)}", nextChunkFile);
}
} }
destination.Flush();
destination.Close(); return totalSize;
} }
/// <summary> /// <summary>
@ -351,38 +353,21 @@ namespace Nibriboard.RippleSpace
} }
/// <summary> /// <summary>
/// Loads a plane form a given nplane file. /// Loads a plane from a given nplane file.
/// </summary> /// </summary>
/// <param name="planeName">The name of the plane to load.</param> /// <param name="planeDirectory">The directory from which the plane should be loaded.</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>
/// <returns>The loaded plane.</returns> /// <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>( 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(); await loadedPlane.LoadPrimaryChunks();
if(deleteSource)
File.Delete(sourceFilename);
return loadedPlane; return loadedPlane;
} }
} }

View File

@ -13,16 +13,10 @@ namespace Nibriboard.RippleSpace
{ {
public class RippleSpaceManager 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> /// <summary>
/// The temporary directory in which we are currently storing our unpacked planes temporarily. /// The temporary directory in which we are currently storing our unpacked planes temporarily.
/// </summary> /// </summary>
public string UnpackedDirectory { get; set; } public string SourceDirectory { get; set; }
/// <summary> /// <summary>
/// The master list of planes that this PlaneManager is in charge of. /// 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. /// Returns 0 if this RippleSpace hasn't been saved yet.
/// </summary> /// </summary>
/// <value>The last size of the save file.</value> /// <value>The last size of the save file.</value>
public long LastSaveFileSize { public long LastSaveSize {
get { get {
if(!File.Exists(SourceFilename)) if(!Directory.Exists(SourceDirectory))
return 0; 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 SourceDirectory = inSourceDirectory;
UnpackedDirectory = Path.GetTempFileName();
File.Delete(UnpackedDirectory);
UnpackedDirectory = Path.GetDirectoryName(UnpackedDirectory) + "/ripplespace-" + Path.GetFileName(UnpackedDirectory) + "/";
Directory.CreateDirectory(UnpackedDirectory);
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() ~RippleSpaceManager()
{ {
Directory.Delete(UnpackedDirectory, true); Directory.Delete(SourceDirectory, true);
} }
/// <summary> /// <summary>
@ -110,7 +107,7 @@ namespace Nibriboard.RippleSpace
Plane newPlane = new Plane( Plane newPlane = new Plane(
newPlaneInfo, newPlaneInfo,
CalcPaths.UnpackedPlaneDir(UnpackedDirectory, newPlaneInfo.Name) CalcPaths.PlaneDirectory(SourceDirectory, newPlaneInfo.Name)
); );
Planes.Add(newPlane); Planes.Add(newPlane);
return newPlane; return newPlane;
@ -142,88 +139,72 @@ namespace Nibriboard.RippleSpace
} }
} }
public async Task Save() public async Task<long> Save()
{ {
Stopwatch timer = Stopwatch.StartNew(); Stopwatch timer = Stopwatch.StartNew();
// Save the planes to disk // Save the planes to disk
List<Task> planeSavers = new List<Task>(); List<Task<long>> planeSavers = new List<Task<long>>();
StreamWriter indexWriter = new StreamWriter(UnpackedDirectory + "index.list"); StreamWriter indexWriter = new StreamWriter(SourceDirectory + "index.list");
foreach(Plane item in Planes) foreach(Plane currentPlane in Planes)
{ {
// Add the plane to the index // Add the plane to the index
await indexWriter.WriteLineAsync(item.Name); await indexWriter.WriteLineAsync(currentPlane.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));
// Ask the plane to save to the directory // Ask the plane to save to the directory
planeSavers.Add(item.Save(File.OpenWrite(planeSavePath))); planeSavers.Add(currentPlane.Save());
} }
indexWriter.Close(); indexWriter.Close();
await Task.WhenAll(planeSavers); await Task.WhenAll(planeSavers);
long totalBytesWritten = planeSavers.Sum((Task<long> saver) => saver.Result);
// Pack the planes into the ripplespace archive Log.WriteLine(
Stream destination = File.OpenWrite(SourceFilename); "[Command/Save] Save complete - {0} written in {1}ms",
string[] planeFiles = Directory.GetFiles(UnpackedDirectory, "*.nplane.zip", SearchOption.TopDirectoryOnly); Formatters.HumanSize(totalBytesWritten),
timer.ElapsedMilliseconds
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("[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; return rippleSpace;
} }

View File

@ -1,4 +1,5 @@
using System; using System;
using System.IO;
using Nibriboard.RippleSpace; using Nibriboard.RippleSpace;
namespace Nibriboard.Utilities namespace Nibriboard.Utilities
@ -8,40 +9,29 @@ namespace Nibriboard.Utilities
/// <summary> /// <summary>
/// Returns the directory in which a plane's data should be unpacked to. /// Returns the directory in which a plane's data should be unpacked to.
/// </summary> /// </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> /// <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> /// <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; return result;
} }
/// <summary> /// <summary>
/// Returns the path to the plane index file given a directory that a plane has been unpacked to. /// Returns the path to the plane index file given a directory that a plane has been unpacked to.
/// </summary> /// </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> /// <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"; return Path.Combine(planeDirectory, "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";
} }
public static string ChunkFilepath(string planeStorageDirectory, ChunkReference chunkRef) 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];
}
}
}