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:
parent
395e92dc05
commit
f2aafece24
10 changed files with 194 additions and 188 deletions
|
@ -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];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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++)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,17 +220,20 @@ 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
|
||||||
|
continue;
|
||||||
|
totalSize += (new FileInfo(destFilename)).Length;
|
||||||
|
}
|
||||||
|
|
||||||
foreach(string nextChunkFile in chunkFiles)
|
return totalSize;
|
||||||
{
|
|
||||||
packer.Write($"{Name}/{Path.GetFileName(nextChunkFile)}", nextChunkFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
destination.Flush();
|
|
||||||
destination.Close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
// Make sure that the source directory exists
|
||||||
UnpackedDirectory = Path.GetDirectoryName(UnpackedDirectory) + "/ripplespace-" + Path.GetFileName(UnpackedDirectory) + "/";
|
if (!Directory.Exists(SourceDirectory)) {
|
||||||
Directory.CreateDirectory(UnpackedDirectory);
|
Directory.CreateDirectory(SourceDirectory);
|
||||||
|
|
||||||
Log.WriteLine("[RippleSpace] New blank ripplespace initialised.");
|
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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
18
Nibriboard/Utilities/Formatters.cs
Normal file
18
Nibriboard/Utilities/Formatters.cs
Normal 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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue