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.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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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++)
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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<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));
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
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