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 () {