diff --git a/LiteDB.Tests/Database/Upgrade_Tests.cs b/LiteDB.Tests/Database/Upgrade_Tests.cs index 71d9cc6e9..275f763a7 100644 --- a/LiteDB.Tests/Database/Upgrade_Tests.cs +++ b/LiteDB.Tests/Database/Upgrade_Tests.cs @@ -88,5 +88,38 @@ public void Migrage_From_V4_No_FileExtension() } } } + + [Fact] + public void Migrage_From_V4_Stream() + { + // v5 upgrades only from v4! + + var original = "../../../Utils/Legacy/v4.db"; + + using (FileStream fs = new FileStream(original, FileMode.Open, FileAccess.Read)) + using (MemoryStream ms = new MemoryStream()) + { + fs.CopyTo(ms); + ms.Flush(); + ms.Seek(0, SeekOrigin.Begin); + + using (var db = new LiteDatabase(ms, upgrade:true)) + { + // convert and open database + var col1 = db.GetCollection("col1"); + + col1.Count().Should().Be(3); + } + + ms.Seek(0, SeekOrigin.Begin); + using (var db = new LiteDatabase(ms, upgrade: true)) + { + // database already converted + var col1 = db.GetCollection("col1"); + + col1.Count().Should().Be(3); + } + } + } } } \ No newline at end of file diff --git a/LiteDB/Client/Database/LiteDatabase.cs b/LiteDB/Client/Database/LiteDatabase.cs index 96dd0bd2a..25956232f 100644 --- a/LiteDB/Client/Database/LiteDatabase.cs +++ b/LiteDB/Client/Database/LiteDatabase.cs @@ -61,8 +61,14 @@ public LiteDatabase(ConnectionString connectionString, BsonMapper mapper = null) /// DataStream reference /// BsonMapper mapper reference /// LogStream reference - public LiteDatabase(Stream stream, BsonMapper mapper = null, Stream logStream = null) + /// Try to upgrade the stream. If specifying true and the stream needs to be upgraded, the stream must be writeable + public LiteDatabase(Stream stream, BsonMapper mapper = null, Stream logStream = null, bool upgrade = false) { + if (upgrade && !LiteEngine.Upgrade(stream)) + {//Assume it's already upgraded and rewind the stream + stream.Seek(0, SeekOrigin.Begin); + } + var settings = new EngineSettings { DataStream = stream ?? throw new ArgumentNullException(nameof(stream)), diff --git a/LiteDB/Engine/Engine/Upgrade.cs b/LiteDB/Engine/Engine/Upgrade.cs index e19f17e68..cdfdd5917 100644 --- a/LiteDB/Engine/Engine/Upgrade.cs +++ b/LiteDB/Engine/Engine/Upgrade.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.IO; -using System.Linq; using System.Text; using static LiteDB.Constants; @@ -29,56 +27,97 @@ public static bool Upgrade(string filename, string password = null, Collation co settings.Filename = FileHelper.GetSufixFile(filename, "-temp", true); - var buffer = new byte[PAGE_SIZE * 2]; - IFileReader reader; using (var stream = new FileStream(filename, FileMode.Open, FileAccess.Read)) { - // read first 16k - stream.Read(buffer, 0, buffer.Length); - - // checks if v8 plain data or encrypted (first byte = 1) - if ((Encoding.UTF8.GetString(buffer, HeaderPage.P_HEADER_INFO, HeaderPage.HEADER_INFO.Length) == HeaderPage.HEADER_INFO && - buffer[HeaderPage.P_FILE_VERSION] == HeaderPage.FILE_VERSION) || - buffer[0] == 1) - { + if (!TryUpgradeStreamInternal(password, stream, settings)) return false; - } + } - // checks if v7 (plain or encrypted) - if (Encoding.UTF8.GetString(buffer, 25, HeaderPage.HEADER_INFO.Length) == HeaderPage.HEADER_INFO && - buffer[52] == 7) - { - reader = new FileReaderV7(stream, password); - } - else - { - throw new LiteException(0, "Invalid data file format to upgrade"); - } + // rename source filename to backup name + File.Move(filename, backup); + + // rename temp file into filename + File.Move(settings.Filename, filename); + + return true; + } - using (var engine = new LiteEngine(settings)) + /// + /// Upgrade old version of LiteDB into new LiteDB file structure. Returns true if database was completed converted + /// If database already in current version just return false + /// + public static bool Upgrade(Stream stream, string password = null, Collation collation = null) + { + using (MemoryStream ms = new MemoryStream()) + { + var settings = new EngineSettings { - // copy all database to new Log file with NO checkpoint during all rebuild - engine.Pragma(Pragmas.CHECKPOINT, 0); + DataStream = ms, + Password = password, + Collation = collation + }; + - engine.RebuildContent(reader); - // after rebuild, copy log bytes into data file - engine.Checkpoint(); + if (!TryUpgradeStreamInternal(password, stream, settings)) + return false; + ms.Flush(); + ms.Seek(0, SeekOrigin.Begin); + + stream.Seek(0, SeekOrigin.Begin); + stream.SetLength(0); + ms.CopyTo(stream); + + stream.Flush(); + stream.Seek(0, SeekOrigin.Begin); + return true; + } - // re-enable auto-checkpoint pragma - engine.Pragma(Pragmas.CHECKPOINT, 1000); + } - // copy userVersion from old datafile - engine.Pragma("USER_VERSION", (reader as FileReaderV7).UserVersion); - } + private static bool TryUpgradeStreamInternal(string password, Stream stream, EngineSettings settings) + { + var buffer = new byte[PAGE_SIZE * 2]; + IFileReader reader; + // read first 16k + stream.Read(buffer, 0, buffer.Length); + + // checks if v8 plain data or encrypted (first byte = 1) + if ((Encoding.UTF8.GetString(buffer, HeaderPage.P_HEADER_INFO, HeaderPage.HEADER_INFO.Length) == HeaderPage.HEADER_INFO && + buffer[HeaderPage.P_FILE_VERSION] == HeaderPage.FILE_VERSION) || + buffer[0] == 1) + { + return false; } - // rename source filename to backup name - File.Move(filename, backup); + // checks if v7 (plain or encrypted) + if (Encoding.UTF8.GetString(buffer, 25, HeaderPage.HEADER_INFO.Length) == HeaderPage.HEADER_INFO && + buffer[52] == 7) + { + reader = new FileReaderV7(stream, password); + } + else + { + throw new LiteException(0, "Invalid data file format to upgrade"); + } - // rename temp file into filename - File.Move(settings.Filename, filename); + using (var engine = new LiteEngine(settings)) + { + // copy all database to new Log file with NO checkpoint during all rebuild + engine.Pragma(Pragmas.CHECKPOINT, 0); + + engine.RebuildContent(reader); + + // after rebuild, copy log bytes into data file + engine.Checkpoint(); + + // re-enable auto-checkpoint pragma + engine.Pragma(Pragmas.CHECKPOINT, 1000); + + // copy userVersion from old datafile + engine.Pragma("USER_VERSION", (reader as FileReaderV7).UserVersion); + } return true; }