From 3e04af0291571a40a2e9da252803bd029a9f487a Mon Sep 17 00:00:00 2001 From: Alan McGovern Date: Wed, 26 Oct 2022 23:40:05 +0100 Subject: [PATCH] [core] Always create empty files when starting a torrent Empty files are always created on disk as part of starting the torrent. This simplifies some of the internal logic as nothing else needs to care about creating empty files after a torrent is started. --- .../MonoTorrent.Client.Modes/StartingMode.cs | 26 ++++++++++++++++--- .../Managers/TorrentManager.cs | 8 +++++- .../MonoTorrent.Client/ClientEngineTests.cs | 22 ++++++++++++++++ 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/MonoTorrent.Client/MonoTorrent.Client.Modes/StartingMode.cs b/src/MonoTorrent.Client/MonoTorrent.Client.Modes/StartingMode.cs index 4c56d9120..1b94ea005 100644 --- a/src/MonoTorrent.Client/MonoTorrent.Client.Modes/StartingMode.cs +++ b/src/MonoTorrent.Client/MonoTorrent.Client.Modes/StartingMode.cs @@ -36,6 +36,8 @@ using MonoTorrent.BEncoding; using MonoTorrent.Logging; +using ReusableTasks; + namespace MonoTorrent.Client.Modes { class StartingMode : Mode @@ -62,6 +64,8 @@ public async Task WaitForStartingToComplete () throw new TorrentException ("Torrents with no metadata must use 'MetadataMode', not 'StartingMode'."); try { + // If the torrent contains any files of length 0, ensure we create them now. + await CreateEmptyFiles (); await VerifyHashState (); Cancellation.Token.ThrowIfCancellationRequested (); Manager.PieceManager.Initialise (); @@ -123,6 +127,23 @@ public async Task WaitForStartingToComplete () await Manager.LocalPeerAnnounceAsync (); } + async ReusableTask CreateEmptyFiles () + { + foreach (var file in Manager.Files) { + if (file.Length == 0) { + var fileInfo = new FileInfo (file.FullPath); + if (fileInfo.Exists && fileInfo.Length == 0) + continue; + + await MainLoop.SwitchToThreadpool (); + Directory.CreateDirectory (Path.GetDirectoryName (file.FullPath)!); + // Ensure file on disk is always 0 bytes, as it's supposed to be. + using (var stream = File.OpenWrite (file.FullPath)) + stream.SetLength (0); + } + } + } + async void SendAnnounces () { try { @@ -156,12 +177,9 @@ async Task TryLoadV2HashesFromCache() async Task VerifyHashState () { // If we do not have metadata or the torrent needs a hash check, fast exit. - if (!Manager.HasMetadata || !Manager.HashChecked) + if (!Manager.HashChecked) return; - // FIXME: I should really just ensure that zero length files always exist on disk. If the first file is - // a zero length file and someone deletes it after the first piece has been written to disk, it will - // never be recreated. If the downloaded data requires this file to exist, we have an issue. foreach (ITorrentManagerFile file in Manager.Files) { if (!file.BitField.AllFalse && file.Length > 0) { if (!await DiskManager.CheckFileExistsAsync (file)) { diff --git a/src/MonoTorrent.Client/MonoTorrent.Client/Managers/TorrentManager.cs b/src/MonoTorrent.Client/MonoTorrent.Client/Managers/TorrentManager.cs index 473f1decf..ce7bb5c7e 100644 --- a/src/MonoTorrent.Client/MonoTorrent.Client/Managers/TorrentManager.cs +++ b/src/MonoTorrent.Client/MonoTorrent.Client/Managers/TorrentManager.cs @@ -670,10 +670,13 @@ internal void SetMetadata (Torrent torrent) } var currentPath = File.Exists (downloadCompleteFullPath) ? downloadCompleteFullPath : downloadIncompleteFullPath; - return new TorrentFileInfo (file, currentPath) { + var torrentFileInfo = new TorrentFileInfo (file, currentPath) { DownloadCompleteFullPath = downloadCompleteFullPath, DownloadIncompleteFullPath = downloadIncompleteFullPath }; + if (file.Length == 0) + torrentFileInfo.BitField[0] = true; + return torrentFileInfo; }).Cast ().ToList ().AsReadOnly (); PieceHashes = Torrent.CreatePieceHashes (); @@ -941,6 +944,9 @@ internal void OnPieceHashed (int index, bool hashPassed, int piecesHashed, int t var files = Files; var fileIndex = files.FindFileByPieceIndex (index); for (int i = fileIndex; i < files.Count && files[i].StartPieceIndex <= index; i++) { + // Empty files always have all bits set to 'true' as they're treated as being downloaded as soon as they exist on disk. + if (files[i].Length == 0) + continue; ((TorrentFileInfo) files[i]).BitField[index - files[i].StartPieceIndex] = hashPassed; // If we're only hashing 1 piece then we can start moving files now. This occurs when a torrent diff --git a/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client/ClientEngineTests.cs b/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client/ClientEngineTests.cs index be1d6aa39..4301c9c05 100644 --- a/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client/ClientEngineTests.cs +++ b/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client/ClientEngineTests.cs @@ -370,6 +370,28 @@ public async Task SaveRestoreState_OneTorrentFile_NoContainingDirectory () } } + [Test] + public async Task StartAsyncAlwaysCreatesEmptyFiles () + { + var files = TorrentFile.Create (Constants.BlockSize * 4, 0, 1, 2, 3); + using var writer = new TestWriter (); + using var rig = TestRig.CreateMultiFile (files, Constants.BlockSize * 4, writer); + + for (int i = 0; i < 2; i++) { + var downloadingState = rig.Manager.WaitForState (TorrentState.Downloading); + var stoppedState = rig.Manager.WaitForState (TorrentState.Stopped); + + await rig.Manager.StartAsync (); + Assert.IsTrue (downloadingState.Wait (5000), "Started"); + Assert.IsTrue (File.Exists (rig.Manager.Files[0].FullPath)); + Assert.IsTrue (rig.Manager.Files[0].BitField.AllTrue); + + await rig.Manager.StopAsync (); + Assert.IsTrue (stoppedState.Wait (5000), "Stopped"); + File.Delete (rig.Manager.Files[0].FullPath); + } + } + [Test] public async Task StopTest () {