diff --git a/src/MonoTorrent.Client/MonoTorrent.Client.Modes/HashingMode.cs b/src/MonoTorrent.Client/MonoTorrent.Client.Modes/HashingMode.cs index b3d577b0e..4b926f708 100644 --- a/src/MonoTorrent.Client/MonoTorrent.Client.Modes/HashingMode.cs +++ b/src/MonoTorrent.Client/MonoTorrent.Client.Modes/HashingMode.cs @@ -128,6 +128,8 @@ public async Task WaitForHashingToComplete () for (int i = 0; i < Manager.Torrent!.PieceCount; i++) Manager.OnPieceHashed (i, false, i + 1, Manager.Torrent.PieceCount); } + + await Manager.RefreshAllFilesCorrectLengthAsync (); } public void Dispose () diff --git a/src/MonoTorrent.Client/MonoTorrent.Client.Modes/StartingMode.cs b/src/MonoTorrent.Client/MonoTorrent.Client.Modes/StartingMode.cs index ac39c2723..f126e34ed 100644 --- a/src/MonoTorrent.Client/MonoTorrent.Client.Modes/StartingMode.cs +++ b/src/MonoTorrent.Client/MonoTorrent.Client.Modes/StartingMode.cs @@ -90,9 +90,11 @@ 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 (); + // Run some basic validations to see if we should force a hashcheck await VerifyHashState (); + + // Create any files of length zero, and truncate any files which are too long + await CreateOrTruncateFiles (); Cancellation.Token.ThrowIfCancellationRequested (); Manager.PieceManager.Initialise (); } catch (Exception ex) { @@ -153,21 +155,33 @@ public async Task WaitForStartingToComplete () await Manager.LocalPeerAnnounceAsync (); } - async ReusableTask CreateEmptyFiles () + async ReusableTask CreateOrTruncateFiles () { - 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); + foreach (TorrentFileInfo file in Manager.Files.Where (t => t.Priority != Priority.DoNotDownload)) { + var maybeLength = await DiskManager.GetLengthAsync (file).ConfigureAwait (false); + // If the file doesn't exist, create it. + if (!maybeLength.HasValue) + await DiskManager.CreateAsync (file, Settings.FileCreationOptions).ConfigureAwait (false); + + // Otherwise if the destination file is larger than it should be, truncate it + else if (maybeLength.Value > file.Length) + await DiskManager.SetLengthAsync (file, file.Length).ConfigureAwait (false); + + if (file.Length == 0) + file.BitField[0] = true; + } + + // Then check if any 'DoNotDownload' file overlaps with a file we are downloading. + var downloadingFiles = Manager.Files.Where (t => t.Priority != Priority.DoNotDownload).ToArray (); + foreach (var ignoredFile in Manager.Files.Where (t => t.Priority == Priority.DoNotDownload)) { + foreach (var downloading in downloadingFiles) { + if (ignoredFile.Overlaps (downloading)) + await DiskManager.CreateAsync (ignoredFile, Settings.FileCreationOptions); } } + + // After potentially creating or truncating files, refresh the state. + await Manager.RefreshAllFilesCorrectLengthAsync (); } async void SendAnnounces () @@ -206,6 +220,8 @@ async Task VerifyHashState () if (!Manager.HashChecked) return; + // Lightweight check - if any files are missing but we believe data should exist, reset the 'needs hashcheck' boolean + // so we force a hash check. 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/DiskManager.cs b/src/MonoTorrent.Client/MonoTorrent.Client/Managers/DiskManager.cs index b872db05d..2ad4a72df 100644 --- a/src/MonoTorrent.Client/MonoTorrent.Client/Managers/DiskManager.cs +++ b/src/MonoTorrent.Client/MonoTorrent.Client/Managers/DiskManager.cs @@ -720,5 +720,23 @@ internal async ReusableTask UpdateSettingsAsync (EngineSettings settings) await Cache.SetCapacityAsync (settings.DiskCacheBytes); } } + + internal async ReusableTask CreateAsync (ITorrentManagerFile file, FileCreationOptions fileCreationOptions) + { + await IOLoop; + return await Cache.Writer.CreateAsync (file, fileCreationOptions); + } + + internal async ReusableTask GetLengthAsync (ITorrentManagerFile file) + { + await IOLoop; + return await Cache.Writer.GetLengthAsync (file); + } + + internal async ReusableTask SetLengthAsync (ITorrentManagerFile file, long length) + { + await IOLoop; + return await Cache.Writer.SetLengthAsync (file, length); + } } } diff --git a/src/MonoTorrent.Client/MonoTorrent.Client/Managers/TorrentManager.cs b/src/MonoTorrent.Client/MonoTorrent.Client/Managers/TorrentManager.cs index 03e3334c3..9e76198ca 100644 --- a/src/MonoTorrent.Client/MonoTorrent.Client/Managers/TorrentManager.cs +++ b/src/MonoTorrent.Client/MonoTorrent.Client/Managers/TorrentManager.cs @@ -87,17 +87,44 @@ public async Task SetFilePriorityAsync (ITorrentManagerFile file, Priority prior if (!Files.Contains (file)) throw new ArgumentNullException (nameof (file), "The file is not part of this torrent"); - // No change + // No change - bail out if (priority == file.Priority) return; await ClientEngine.MainLoop; + if (Engine == null) + throw new InvalidOperationException ("This torrent manager has been removed from it's ClientEngine"); + // If the old priority, or new priority, is 'DoNotDownload' then the selector needs to be refreshed bool needsToUpdateSelector = file.Priority == Priority.DoNotDownload || priority == Priority.DoNotDownload; var oldPriority = file.Priority; + + if (oldPriority == Priority.DoNotDownload && !(await Engine.DiskManager.CheckFileExistsAsync (file))) { + // Always create the file the user requested to download + await Engine.DiskManager.CreateAsync (file, Engine.Settings.FileCreationOptions); + + if (file.Length == 0) + ((TorrentFileInfo) file).BitField[0] = await Engine.DiskManager.CheckFileExistsAsync (file); + } + + // Update the priority for the file itself now that we've successfully created it! ((TorrentFileInfo) file).Priority = priority; + if (oldPriority == Priority.DoNotDownload && file.Length > 0) { + // Look for any file which are still marked DoNotDownload but also overlap this file. + // We need to create those ones too because if there are three 400kB files and the + // piece length is 512kB, and the first file is set to 'DoNotDownload', then we still + // need to create it as we'll download the first 512kB under bittorrent v1. + foreach (var maybeCreateFile in Files.Where (t => t.Priority == Priority.DoNotDownload && t.Length > 0)) { + // If this file overlaps, create it! + if (maybeCreateFile.Overlaps(file) && !(await Engine.DiskManager.CheckFileExistsAsync (maybeCreateFile))) + await Engine.DiskManager.CreateAsync (maybeCreateFile, Engine.Settings.FileCreationOptions); + } + } +; + + // With the new priority, calculate which files we're actively downloading! if (needsToUpdateSelector) { // If we change the priority of a file we need to figure out which files are marked // as 'DoNotDownload' and which ones are downloadable. @@ -154,7 +181,7 @@ public async Task SetFilePriorityAsync (ITorrentManagerFile file, Priority prior /// all files have the correct length. If some files are marked as 'DoNotDownload' then the /// torrent will not be considered to be Complete until they are downloaded. /// - public bool Complete => Bitfield.AllTrue; + public bool Complete => Bitfield.AllTrue && AllFilesCorrectLength; internal bool Disposed { get; private set; } @@ -204,6 +231,7 @@ public async Task SetNeedsHashCheckAsync () internal void SetNeedsHashCheck () { HashChecked = false; + AllFilesCorrectLength = false; if (Engine != null && Engine.Settings.AutoSaveLoadFastResume) { var path = Engine.Settings.GetFastResumePath (InfoHashes); if (File.Exists (path)) @@ -220,6 +248,8 @@ internal void SetNeedsHashCheck () public bool HashChecked { get; private set; } + internal bool AllFilesCorrectLength { get; private set; } + /// /// The number of times a piece is downloaded, but is corrupt and fails the hashcheck and must be re-downloaded. /// @@ -583,6 +613,7 @@ internal async Task HashCheckAsync (bool autoStart, bool setStoppedModeWhenDone) } HashChecked = true; + if (autoStart) { await StartAsync (); } else if (setStoppedModeWhenDone) { @@ -680,8 +711,6 @@ internal void SetMetadata (Torrent torrent) var currentPath = File.Exists (downloadCompleteFullPath) ? downloadCompleteFullPath : downloadIncompleteFullPath; var torrentFileInfo = new TorrentFileInfo (file, currentPath); torrentFileInfo.UpdatePaths ((currentPath, downloadCompleteFullPath, downloadIncompleteFullPath)); - if (file.Length == 0) - torrentFileInfo.BitField[0] = true; return torrentFileInfo; }).Cast ().ToList ().AsReadOnly (); @@ -1061,7 +1090,18 @@ void CheckRegisteredAndDisposed () throw new InvalidOperationException ("The registered engine has been disposed"); } - public async Task LoadFastResumeAsync (FastResume data) + /// + /// Attempts to load the provided fastresume data. Several validations are performed during this, such as ensuring + /// files which have validated pieces actually exist on disk, and the length of those files is correct. If any validation + /// fails, the boolean will not be set to true, and will need + /// to be run to re-averify the file contents. + /// + /// + /// + public Task LoadFastResumeAsync (FastResume data) + => LoadFastResumeAsync (data, false); + + internal async Task LoadFastResumeAsync (FastResume data, bool skipStateCheckForTests) { if (data == null) throw new ArgumentNullException (nameof (data)); @@ -1069,18 +1109,39 @@ public async Task LoadFastResumeAsync (FastResume data) await ClientEngine.MainLoop; CheckMetadata (); - if (State != TorrentState.Stopped) + if (State != TorrentState.Stopped && !skipStateCheckForTests) throw new InvalidOperationException ("Can only load FastResume when the torrent is stopped"); if (InfoHashes != data.InfoHashes || Torrent!.PieceCount != data.Bitfield.Length) throw new ArgumentException ("The fast resume data does not match this torrent", "fastResumeData"); for (int i = 0; i < Torrent.PieceCount; i++) OnPieceHashed (i, data.Bitfield[i], i + 1, Torrent.PieceCount); + UnhashedPieces.From (data.UnhashedPieces); + await RefreshAllFilesCorrectLengthAsync (); HashChecked = true; } + internal async ReusableTask RefreshAllFilesCorrectLengthAsync () + { + var allFilesCorrectLength = true; + foreach (TorrentFileInfo file in Files) { + var maybeLength = await Engine!.DiskManager.GetLengthAsync (file); + + // Empty files aren't stored in fast resume data because it's as easy to just check if they exist on disk. + if (file.Length == 0) + file.BitField[0] = maybeLength.HasValue; + + // If any file doesn't exist, or any file is too large, indicate that something is wrong. + // If files exist but are too short, then we can assume everything is fine and the torrent just + // needs to be downloaded. + if (file.Priority != Priority.DoNotDownload && (!maybeLength.HasValue || maybeLength > file.Length)) + allFilesCorrectLength = false; + } + AllFilesCorrectLength = allFilesCorrectLength; + } + public async Task SaveFastResumeAsync () { await ClientEngine.MainLoop; diff --git a/src/MonoTorrent.Client/MonoTorrent.Client/Settings/EngineSettings.cs b/src/MonoTorrent.Client/MonoTorrent.Client/Settings/EngineSettings.cs index 812e04b0e..92867a418 100644 --- a/src/MonoTorrent.Client/MonoTorrent.Client/Settings/EngineSettings.cs +++ b/src/MonoTorrent.Client/MonoTorrent.Client/Settings/EngineSettings.cs @@ -153,6 +153,11 @@ public sealed class EngineSettings : IEquatable /// public FastResumeMode FastResumeMode { get; } = FastResumeMode.BestEffort; + /// + /// Sets the preferred approach to creating new files. + /// + public FileCreationOptions FileCreationOptions { get; } = FileCreationOptions.PreferSparse; + /// /// The list of HTTP(s) endpoints which the engine should bind to when a is set up /// to stream data from the torrent and is non-null. Should be of @@ -266,7 +271,8 @@ public EngineSettings () internal EngineSettings ( IList allowedEncryption, bool allowHaveSuppression, bool allowLocalPeerDiscovery, bool allowPortForwarding, bool autoSaveLoadDhtCache, bool autoSaveLoadFastResume, bool autoSaveLoadMagnetLinkMetadata, string cacheDirectory, - TimeSpan connectionTimeout, IPEndPoint? dhtEndPoint, int diskCacheBytes, CachePolicy diskCachePolicy, FastResumeMode fastResumeMode, Dictionary listenEndPoints, + TimeSpan connectionTimeout, IPEndPoint? dhtEndPoint, int diskCacheBytes, CachePolicy diskCachePolicy, FastResumeMode fastResumeMode, + FileCreationOptions fileCreationMode, Dictionary listenEndPoints, int maximumConnections, int maximumDiskReadRate, int maximumDiskWriteRate, int maximumDownloadRate, int maximumHalfOpenConnections, int maximumOpenFiles, int maximumUploadRate, IDictionary reportedListenEndPoints, bool usePartialFiles, TimeSpan webSeedConnectionTimeout, TimeSpan webSeedDelay, int webSeedSpeedTrigger, TimeSpan staleRequestTimeout, @@ -286,6 +292,7 @@ internal EngineSettings ( CacheDirectory = cacheDirectory; ConnectionTimeout = connectionTimeout; FastResumeMode = fastResumeMode; + FileCreationOptions = fileCreationMode; HttpStreamingPrefix = httpStreamingPrefix; ListenEndPoints = new ReadOnlyDictionary (new Dictionary (listenEndPoints)); MaximumConnections = maximumConnections; diff --git a/src/MonoTorrent.Client/MonoTorrent.Client/Settings/EngineSettingsBuilder.cs b/src/MonoTorrent.Client/MonoTorrent.Client/Settings/EngineSettingsBuilder.cs index 6163cec82..a15ae21c4 100644 --- a/src/MonoTorrent.Client/MonoTorrent.Client/Settings/EngineSettingsBuilder.cs +++ b/src/MonoTorrent.Client/MonoTorrent.Client/Settings/EngineSettingsBuilder.cs @@ -190,6 +190,11 @@ public int DiskCacheBytes { /// public FastResumeMode FastResumeMode { get; set; } + /// + /// Sets the preferred approach to creating new files. + /// + public FileCreationOptions FileCreationMode { get; set; } + /// /// The HTTP(s) prefix which the engine should bind to when a is set up /// to stream data from the torrent and is non-null. Should be of @@ -355,6 +360,7 @@ public EngineSettingsBuilder (EngineSettings settings) DiskCacheBytes = settings.DiskCacheBytes; DiskCachePolicy = settings.DiskCachePolicy; FastResumeMode = settings.FastResumeMode; + FileCreationMode = settings.FileCreationOptions; httpStreamingPrefix = settings.HttpStreamingPrefix; ListenEndPoints = new Dictionary (settings.ListenEndPoints); ReportedListenEndPoints = new Dictionary (settings.ReportedListenEndPoints); @@ -395,6 +401,7 @@ public EngineSettings ToSettings () diskCacheBytes: DiskCacheBytes, diskCachePolicy: DiskCachePolicy, fastResumeMode: FastResumeMode, + fileCreationMode: FileCreationMode, httpStreamingPrefix: HttpStreamingPrefix, listenEndPoints: ListenEndPoints, maximumConnections: MaximumConnections, diff --git a/src/MonoTorrent.Client/MonoTorrent.Client/Settings/Serializer.cs b/src/MonoTorrent.Client/MonoTorrent.Client/Settings/Serializer.cs index 4f74693d3..2bfd5e54b 100644 --- a/src/MonoTorrent.Client/MonoTorrent.Client/Settings/Serializer.cs +++ b/src/MonoTorrent.Client/MonoTorrent.Client/Settings/Serializer.cs @@ -77,6 +77,8 @@ static T Deserialize (BEncodedDictionary dict) property.SetValue (builder, new Uri (((BEncodedString) value).Text)); } else if (property.PropertyType == typeof (IPAddress)) { property.SetValue (builder, IPAddress.Parse (((BEncodedString) value).Text)); + } else if (property.PropertyType == typeof(FileCreationOptions)) { + property.SetValue (builder, Enum.Parse (typeof (FileCreationOptions), ((BEncodedString) value).Text)); } else if (property.PropertyType == typeof (IPEndPoint)) { var list = (BEncodedList) value; IPEndPoint? endPoint = null; @@ -115,6 +117,7 @@ static BEncodedDictionary Serialize (object builder) FastResumeMode value => new BEncodedString (value.ToString ()), CachePolicy value => new BEncodedString (value.ToString ()), Uri value => new BEncodedString (value.OriginalString), + FileCreationOptions value => new BEncodedString (value.ToString ()), null => null, Dictionary value => FromIPAddressDictionary(value), _ => throw new NotSupportedException ($"{property.Name} => type: ${property.PropertyType}"), diff --git a/src/MonoTorrent.PieceWriter/MonoTorrent.PieceWriter/DiskWriter.cs b/src/MonoTorrent.PieceWriter/MonoTorrent.PieceWriter/DiskWriter.cs index 88bf637c9..3d22df68b 100644 --- a/src/MonoTorrent.PieceWriter/MonoTorrent.PieceWriter/DiskWriter.cs +++ b/src/MonoTorrent.PieceWriter/MonoTorrent.PieceWriter/DiskWriter.cs @@ -117,6 +117,39 @@ async ReusableTask CloseAllAsync (AllStreams allStreams) allStreams.Streams.Clear (); } + public async ReusableTask CreateAsync (ITorrentManagerFile file, FileCreationOptions options) + { + await new EnsureThreadPool (); + + if (File.Exists (file.FullPath)) + return false; + + var parent = Path.GetDirectoryName (file.FullPath); + if (!string.IsNullOrEmpty (parent)) + Directory.CreateDirectory (parent); + + if (options == FileCreationOptions.PreferPreallocation) { +#if NETSTANDARD2_0 || NETSTANDARD2_1 || NET5_0 || NETCOREAPP3_0 || NET472 + if (!File.Exists (file.FullPath)) + using (var fs = new FileStream (file.FullPath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.ReadWrite | FileShare.Delete)) { + fs.SetLength (file.Length); + fs.Seek (file.Length - 1, SeekOrigin.Begin); + fs.Write (new byte[1]); + } +#else + File.OpenHandle (file.FullPath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.ReadWrite | FileShare.Delete, FileOptions.None, file.Length).Dispose (); +#endif + } else { + try { + NtfsSparseFile.CreateSparse (file.FullPath, file.Length); + } catch { + // who cares if we can't pre-allocate a sparse file. Try a regular file! + new FileStream (file.FullPath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.ReadWrite | FileShare.Delete).Dispose (); + } + } + return true; + } + public ReusableTask ExistsAsync (ITorrentManagerFile file) { if (file is null) @@ -142,6 +175,13 @@ public async ReusableTask FlushAsync (ITorrentManagerFile file) } } + public async ReusableTask GetLengthAsync (ITorrentManagerFile file) + { + await new EnsureThreadPool (); + var info = new FileInfo (file.FullPath); + return info.Exists ? info.Length : (long?) null; + } + public async ReusableTask MoveAsync (ITorrentManagerFile file, string newPath, bool overwrite) { ThrowIfNoSyncContext (); @@ -184,6 +224,18 @@ public async ReusableTask ReadAsync (ITorrentManagerFile file, long offset, } } + public async ReusableTask SetLengthAsync (ITorrentManagerFile file, long length) + { + await new EnsureThreadPool (); + var info = new FileInfo (file.FullPath); + if (!info.Exists) + return false; + + using (var fileStream = new FileStream (file.FullPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite, 1, FileOptions.None)) + fileStream.SetLength (file.Length); + return true; + } + public async ReusableTask WriteAsync (ITorrentManagerFile file, long offset, ReadOnlyMemory buffer) { ThrowIfNoSyncContext (); @@ -216,10 +268,8 @@ public ReusableTask SetMaximumOpenFilesAsync (int maximumOpenFiles) // in the method. If we already have a cached FileStream we won't need to swap threads before returning it. StreamData freshStreamData; ReusableSemaphore.Releaser freshStreamDataReleaser; - bool shouldTruncate = false; using (await allStreams.Locker.EnterAsync ()) { // We should check if the on-disk file needs truncation if this is the very first time we're opening it. - shouldTruncate = access.HasFlag (FileAccess.Write) && allStreams.Streams.Count == 0; foreach (var existing in allStreams.Streams) { if (existing.Locker.TryEnter (out ReusableSemaphore.Releaser r)) { if (((access & FileAccess.Write) != FileAccess.Write) || existing.Stream!.CanWrite) { @@ -245,9 +295,6 @@ public ReusableTask SetMaximumOpenFilesAsync (int maximumOpenFiles) if (!File.Exists (file.FullPath)) { if (Path.GetDirectoryName (file.FullPath) is string parentDirectory) Directory.CreateDirectory (parentDirectory); - } else if (shouldTruncate) { - // If this is the first Stream we're opening for this file and the file exists, ensure it's the correct length. - FileReaderWriterHelper.MaybeTruncate (file.FullPath, file.Length); } freshStreamData.Stream = new RandomFileReaderWriter (file.FullPath, file.Length, FileMode.OpenOrCreate, access, FileShare.ReadWrite | FileShare.Delete); return (freshStreamData.Stream, freshStreamDataReleaser); diff --git a/src/MonoTorrent.PieceWriter/MonoTorrent.PieceWriter/NtfsSparseFile.cs b/src/MonoTorrent.PieceWriter/MonoTorrent.PieceWriter/NtfsSparseFile.cs index 8c5f1278e..cef755d4f 100644 --- a/src/MonoTorrent.PieceWriter/MonoTorrent.PieceWriter/NtfsSparseFile.cs +++ b/src/MonoTorrent.PieceWriter/MonoTorrent.PieceWriter/NtfsSparseFile.cs @@ -36,7 +36,6 @@ namespace MonoTorrent.PieceWriter { -#if NETSTANDARD2_0 || NETSTANDARD2_1 || NET5_0 || NETCOREAPP3_0 || NET472 static class NtfsSparseFile { [StructLayout (LayoutKind.Sequential)] @@ -155,5 +154,4 @@ static extern bool GetVolumeInformationW ( uint nFileSystemNameSize ); } -#endif } diff --git a/src/MonoTorrent.PieceWriter/MonoTorrent.PieceWriter/RandomFileStream.cs b/src/MonoTorrent.PieceWriter/MonoTorrent.PieceWriter/RandomFileStream.cs index b04f768a0..9c8946347 100644 --- a/src/MonoTorrent.PieceWriter/MonoTorrent.PieceWriter/RandomFileStream.cs +++ b/src/MonoTorrent.PieceWriter/MonoTorrent.PieceWriter/RandomFileStream.cs @@ -20,17 +20,6 @@ interface IFileReaderWriter : IDisposable ReusableTask WriteAsync (ReadOnlyMemory buffer, long offset); } - static class FileReaderWriterHelper - { - public static void MaybeTruncate (string fullPath, long length) - { - if (new FileInfo (fullPath).Length > length) { - using (var fileStream = new FileStream (fullPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite, 1, FileOptions.None)) - fileStream.SetLength (length); - } - } - } - #if NETSTANDARD2_0 || NETSTANDARD2_1 || NET5_0 || NETCOREAPP3_0 || NET472 class RandomFileReaderWriter : IFileReaderWriter { diff --git a/src/MonoTorrent/MonoTorrent.PieceWriter/FileCreationOptions.cs b/src/MonoTorrent/MonoTorrent.PieceWriter/FileCreationOptions.cs new file mode 100644 index 000000000..0525218bb --- /dev/null +++ b/src/MonoTorrent/MonoTorrent.PieceWriter/FileCreationOptions.cs @@ -0,0 +1,46 @@ +// +// FileCreationOptions.cs +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2024 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + + +namespace MonoTorrent.PieceWriter +{ + public enum FileCreationOptions + { + /// + /// On filesystems where sparse files can be created, an attempt will be made to create a sparse file. + /// Otherwise an empty file will be created. + /// + PreferSparse, + /// + /// On filesystems which support preallocation the space required for the file will be reserved as soon + /// as the file is created. Otherwise, a best effort to pre-allocate will be made by writing 1 byte at + /// the end of the file. + /// + PreferPreallocation, + } +} diff --git a/src/MonoTorrent/MonoTorrent.PieceWriter/IPieceWriter.cs b/src/MonoTorrent/MonoTorrent.PieceWriter/IPieceWriter.cs index 469fefa06..f454b4601 100644 --- a/src/MonoTorrent/MonoTorrent.PieceWriter/IPieceWriter.cs +++ b/src/MonoTorrent/MonoTorrent.PieceWriter/IPieceWriter.cs @@ -38,13 +38,86 @@ public interface IPieceWriter : IDisposable int OpenFiles { get; } int MaximumOpenFiles { get; } + /// + /// Releases all resources associated with the specified file. + /// + /// + /// ReusableTask CloseAsync (ITorrentManagerFile file); + + /// + /// Returns false if the file already exists, otherwise creates the file and returns true. + /// + /// The file to create + /// Determines how new files will be created. + /// + ReusableTask CreateAsync (ITorrentManagerFile file, FileCreationOptions options); + + /// + /// Returns true if the file exists. + /// + /// + /// ReusableTask ExistsAsync (ITorrentManagerFile file); + + /// + /// Flush any cached data to the file. + /// + /// + /// ReusableTask FlushAsync (ITorrentManagerFile file); + + /// + /// Returns null if the specified file does not exist, otherwise returns the length of the file in bytes. + /// + /// + /// + ReusableTask GetLengthAsync (ITorrentManagerFile file); + + /// + /// Moves the specified file to the new location. Optionally overwrite any pre-existing files. + /// + /// + /// + /// + /// ReusableTask MoveAsync (ITorrentManagerFile file, string fullPath, bool overwrite); + + /// + /// Reads the specified amount of data from the specified file. + /// + /// + /// + /// + /// ReusableTask ReadAsync (ITorrentManagerFile file, long offset, Memory buffer); - ReusableTask WriteAsync (ITorrentManagerFile file, long offset, ReadOnlyMemory buffer); + + /// + /// Returns false and no action is taken if the file does not already exist. If the file does exist + /// it's length is set to the provided value. + /// + /// + /// + /// + ReusableTask SetLengthAsync (ITorrentManagerFile file, long length); + + /// + /// Optional limit to the maximum number of files this writer can have open concurrently. + /// + /// + /// ReusableTask SetMaximumOpenFilesAsync (int maximumOpenFiles); + + /// + /// Writes all data in the provided buffer to the specified file. Some implementatations may + /// have an internal cache, which means should + /// be invoked to guarantee the data is written to it's final destination. + /// + /// + /// + /// + /// + ReusableTask WriteAsync (ITorrentManagerFile file, long offset, ReadOnlyMemory buffer); } public static class PaddingAwareIPieceWriterExtensions diff --git a/src/MonoTorrent/MonoTorrent/ITorrentFileInfo.cs b/src/MonoTorrent/MonoTorrent/ITorrentFileInfo.cs index c74ce9058..fdc405c1e 100644 --- a/src/MonoTorrent/MonoTorrent/ITorrentFileInfo.cs +++ b/src/MonoTorrent/MonoTorrent/ITorrentFileInfo.cs @@ -61,5 +61,11 @@ public static class ITorrentFileInfoExtensions { public static long BytesDownloaded (this ITorrentManagerFile info) => (long) (info.BitField.PercentComplete * info.Length / 100.0); + + public static bool Overlaps (this ITorrentManagerFile self, ITorrentManagerFile file) + => self.Length > 0 && + file.Length > 0 && + self.StartPieceIndex <= file.EndPieceIndex && + file.StartPieceIndex <= self.EndPieceIndex; } } diff --git a/src/Samples/SampleClient/StressTest.cs b/src/Samples/SampleClient/StressTest.cs index 8b63eb87e..011d873c6 100644 --- a/src/Samples/SampleClient/StressTest.cs +++ b/src/Samples/SampleClient/StressTest.cs @@ -26,6 +26,11 @@ public ReusableTask CloseAsync (ITorrentManagerFile file) return ReusableTask.CompletedTask; } + public ReusableTask CreateAsync (ITorrentManagerFile file, FileCreationOptions options) + { + throw new NotImplementedException (); + } + public void Dispose () { } @@ -40,6 +45,11 @@ public ReusableTask FlushAsync (ITorrentManagerFile file) return ReusableTask.CompletedTask; } + public ReusableTask GetLengthAsync (ITorrentManagerFile file) + { + throw new NotImplementedException (); + } + public ReusableTask MoveAsync (ITorrentManagerFile file, string fullPath, bool overwrite) { return ReusableTask.CompletedTask; @@ -50,6 +60,11 @@ public ReusableTask ReadAsync (ITorrentManagerFile file, long offset, Memor return ReusableTask.FromResult (0); } + public ReusableTask SetLengthAsync (ITorrentManagerFile file, long length) + { + throw new NotImplementedException (); + } + public ReusableTask SetMaximumOpenFilesAsync (int maximumOpenFiles) { return ReusableTask.CompletedTask; diff --git a/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client.Modes/DownloadModeTests.cs b/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client.Modes/DownloadModeTests.cs index 01867f284..4a0f583cc 100644 --- a/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client.Modes/DownloadModeTests.cs +++ b/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client.Modes/DownloadModeTests.cs @@ -242,6 +242,10 @@ public async Task AnnounceWhenComplete () await TrackerManager.AddTrackerAsync (new Uri ("http://1.1.1.1")); var bitfield = new BitField (Manager.Bitfield); + // Create all the files. + foreach (var file in Manager.Files) + await Manager.Engine.DiskManager.CreateAsync (file, MonoTorrent.PieceWriter.FileCreationOptions.PreferSparse); + // Leecher at first bitfield.SetAll (true).Set (0, false); await Manager.LoadFastResumeAsync (new FastResume (Manager.InfoHashes, bitfield, new BitField (Manager.Torrent.PieceCount ()))); diff --git a/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client.Modes/HashingModeTests.cs b/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client.Modes/HashingModeTests.cs index 9545fee66..a5d55c589 100644 --- a/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client.Modes/HashingModeTests.cs +++ b/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client.Modes/HashingModeTests.cs @@ -65,6 +65,8 @@ public void Setup () Constants.BlockSize * 32, Constants.BlockSize * 2, Constants.BlockSize * 13, + 0, + 0, }; Manager = TestRig.CreateMultiFileManager (fileSizes, Constants.BlockSize * 2, writer: PieceWriter, baseDirectory: TempDirectory.Path); Manager.SetTrackerManager (TrackerManager); @@ -131,7 +133,7 @@ public async Task HashCheckAsync_DoNotTruncate () { await Manager.Engine.DiskManager.SetWriterAsync (Factories.Default.CreatePieceWriter (1)); - var file = Manager.Files[0]; + var file = Manager.Files.First (t => t.Length != 0); System.IO.Directory.CreateDirectory (System.IO.Path.GetDirectoryName (file.FullPath)); System.IO.File.WriteAllBytes (file.FullPath, new byte[file.Length + 1]); await Manager.HashCheckAsync (false); @@ -139,6 +141,50 @@ public async Task HashCheckAsync_DoNotTruncate () Assert.AreEqual (file.Length + 1, new System.IO.FileInfo (file.FullPath).Length); } + [Test] + public async Task HashCheckAsync_FilesAreCorrectLength () + { + await DiskManager.SetWriterAsync (Factories.Default.CreatePieceWriter (1)); + var mode = new HashingMode (Manager, DiskManager); + Manager.Mode = mode; + + // All files are 1 byte too long + foreach (var file in Manager.Files) { + System.IO.Directory.CreateDirectory (System.IO.Path.GetDirectoryName (file.FullPath)); + System.IO.File.WriteAllBytes (file.FullPath, new byte[file.Length + 1]); + } + + await mode.WaitForHashingToComplete (); + Assert.IsFalse (Manager.AllFilesCorrectLength); + + // All files are correctly sized and exist. + foreach (var file in Manager.Files) { + System.IO.Directory.CreateDirectory (System.IO.Path.GetDirectoryName (file.FullPath)); + System.IO.File.Delete (file.FullPath); + System.IO.File.WriteAllBytes (file.FullPath, new byte[file.Length]); + } + + await mode.WaitForHashingToComplete (); + Assert.IsTrue (Manager.AllFilesCorrectLength); + + // One zero length file is missing + var zeroLengthFile = Manager.Files.First (t => t.Length == 0); + System.IO.File.Delete (zeroLengthFile.FullPath); + + await mode.WaitForHashingToComplete (); + Assert.IsFalse (Manager.AllFilesCorrectLength); + + // One file is too large + System.IO.File.WriteAllBytes (zeroLengthFile.FullPath, new byte[1]); + await mode.WaitForHashingToComplete (); + Assert.IsFalse (Manager.AllFilesCorrectLength); + + // Back to normal! + System.IO.File.WriteAllBytes (zeroLengthFile.FullPath, new byte[0]); + await mode.WaitForHashingToComplete (); + Assert.IsTrue (Manager.AllFilesCorrectLength); + } + [Test] public async Task PauseResumeHashingMode () { @@ -146,7 +192,7 @@ public async Task PauseResumeHashingMode () var pieceHashed = new TaskCompletionSource (); var secondPieceHashed = new TaskCompletionSource (); - PieceWriter.FilesThatExist.AddRange (Manager.Files); + await PieceWriter.CreateAsync (Manager.Files); Manager.Engine.DiskManager.GetHashAsyncOverride = async (torrentdata, pieceIndex, dest) => { pieceTryHash.TrySetResult (null); @@ -225,12 +271,16 @@ public async Task SaveLoadFastResume () [Test] public async Task DoNotDownload_All () { + // No files exist! + Assert.AreEqual (0, PieceWriter.FilesWithLength.Count); + var bf = new BitField (Manager.Bitfield).SetAll (true); var unhashed = new BitField (bf).SetAll (false); await Manager.LoadFastResumeAsync (new FastResume (Manager.InfoHashes, bf, unhashed)); foreach (var f in Manager.Files) { - PieceWriter.FilesThatExist.Add (f); + if (f.Length > 0) + await PieceWriter.CreateAsync (f, MonoTorrent.PieceWriter.FileCreationOptions.PreferPreallocation); await Manager.SetFilePriorityAsync (f, Priority.DoNotDownload); ((TorrentFileInfo) f).BitField.SetAll (true); } @@ -245,6 +295,8 @@ public async Task DoNotDownload_All () // No piece should be marked as available, and no pieces should actually be hashchecked. Assert.IsTrue (Manager.Bitfield.AllFalse, "#2"); Assert.AreEqual (Manager.UnhashedPieces.TrueCount, Manager.UnhashedPieces.Length, "#3"); + + // Empty files will always have their bitfield set to true if they exist on disk. None should exist though foreach (var f in Manager.Files) Assert.IsTrue (f.BitField.AllFalse, "#4." + f.Path); } @@ -266,7 +318,7 @@ public async Task DoNotDownload_ThenDownload () await Manager.LoadFastResumeAsync (new FastResume (Manager.InfoHashes, bf, unhashed)); foreach (var f in Manager.Files) { - PieceWriter.FilesThatExist.Add (f); + await PieceWriter.CreateAsync (f, MonoTorrent.PieceWriter.FileCreationOptions.PreferPreallocation); await Manager.SetFilePriorityAsync (f, Priority.DoNotDownload); } @@ -330,7 +382,7 @@ public async Task StopWhileHashingPendingFiles () [Test] public async Task StopWhileHashingPaused () { - PieceWriter.FilesThatExist.AddRange (Manager.Files); + await PieceWriter.CreateAsync (Manager.Files); int getHashCount = 0; DiskManager.GetHashAsyncOverride = (manager, index, dest) => { @@ -362,7 +414,7 @@ public async Task DoNotDownload_OneFile () await Manager.LoadFastResumeAsync (new FastResume (Manager.InfoHashes, bf, unhashed)); foreach (var f in Manager.Files.Skip (1)) { - PieceWriter.FilesThatExist.Add (f); + await PieceWriter.CreateAsync (f, MonoTorrent.PieceWriter.FileCreationOptions.PreferPreallocation); await Manager.SetFilePriorityAsync (f, Priority.DoNotDownload); } @@ -384,14 +436,14 @@ public async Task DoNotDownload_OneFile () [Test] public async Task ReadZeroFromDisk () { - PieceWriter.FilesThatExist.AddRange (new[]{ - Manager.Files [0], - Manager.Files [2], - }); + var emptyFiles = Manager.Files.Where (t => t.Length == 0).ToArray (); + var nonEmptyFiles = Manager.Files.Where (t => t.Length != 0).ToArray (); + await PieceWriter.CreateAsync (new[] { nonEmptyFiles[0], nonEmptyFiles[2] }); + await PieceWriter.CreateAsync (emptyFiles); PieceWriter.DoNotReadFrom.AddRange (new[]{ - Manager.Files[0], - Manager.Files[3], + nonEmptyFiles [0], + nonEmptyFiles [3], }); var bf = new BitField (Manager.Torrent.PieceCount ()).SetAll (true); @@ -401,6 +453,9 @@ public async Task ReadZeroFromDisk () foreach (var file in Manager.Files) Assert.IsTrue (file.BitField.AllTrue, "#2." + file.Path); + // Remove zero length files so they no longer exist + foreach (var v in emptyFiles) + PieceWriter.FilesWithLength.Remove (v); var mode = new HashingMode (Manager, DiskManager); Manager.Mode = mode; await mode.WaitForHashingToComplete (); diff --git a/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client.Modes/InitialSeedingModeTest.cs b/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client.Modes/InitialSeedingModeTest.cs index 6ef17e190..a9939409a 100644 --- a/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client.Modes/InitialSeedingModeTest.cs +++ b/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client.Modes/InitialSeedingModeTest.cs @@ -43,6 +43,8 @@ InitialSeedingMode Mode { get { return Rig.Manager.Mode as InitialSeedingMode; } } + TestWriter PieceWriter { get; set; } + TestRig Rig { get; set; } @@ -50,14 +52,19 @@ TestRig Rig { [SetUp] public async Task Setup () { + PieceWriter = new TestWriter (); Rig = TestRig.CreateSingleFile (Constants.BlockSize * 20, Constants.BlockSize * 2); + await Rig.Engine.DiskManager.SetWriterAsync (PieceWriter); + + // create all the files + await PieceWriter.CreateAsync (Rig.Manager.Files); var bf = new BitField (Rig.Manager.Bitfield).SetAll (true); var unhashed = new BitField (bf).SetAll (false); await Rig.Manager.LoadFastResumeAsync (new FastResume (Rig.Manager.InfoHashes, bf, unhashed)); - Rig.Manager.Mode = new InitialSeedingMode (Rig.Manager, Rig.Engine.DiskManager, Rig.Engine.ConnectionManager, Rig.Engine.Settings); + } [TearDown] diff --git a/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client.Modes/NullWriter.cs b/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client.Modes/NullWriter.cs index a8b7b7be2..5ce830dc6 100644 --- a/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client.Modes/NullWriter.cs +++ b/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client.Modes/NullWriter.cs @@ -17,6 +17,11 @@ public ReusableTask CloseAsync (ITorrentManagerFile file) return ReusableTask.CompletedTask; } + public ReusableTask CreateAsync (ITorrentManagerFile file, FileCreationOptions options) + { + throw new NotImplementedException (); + } + public void Dispose () { } @@ -31,6 +36,11 @@ public ReusableTask FlushAsync (ITorrentManagerFile file) return ReusableTask.CompletedTask; } + public ReusableTask GetLengthAsync (ITorrentManagerFile file) + { + throw new NotImplementedException (); + } + public ReusableTask MoveAsync (ITorrentManagerFile file, string fullPath, bool overwrite) { return ReusableTask.CompletedTask; @@ -41,6 +51,11 @@ public ReusableTask ReadAsync (ITorrentManagerFile file, long offset, Memor return ReusableTask.FromResult (0); } + public ReusableTask SetLengthAsync (ITorrentManagerFile file, long length) + { + throw new NotImplementedException (); + } + public ReusableTask SetMaximumOpenFilesAsync (int maximumOpenFiles) { return ReusableTask.CompletedTask; @@ -50,5 +65,10 @@ public ReusableTask WriteAsync (ITorrentManagerFile file, long offset, ReadOnlyM { return ReusableTask.CompletedTask; } + + ReusableTask IPieceWriter.SetLengthAsync (ITorrentManagerFile file, long length) + { + throw new NotImplementedException (); + } } } diff --git a/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client.Modes/StartingModeTests.cs b/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client.Modes/StartingModeTests.cs index 8569154ef..f6007647d 100644 --- a/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client.Modes/StartingModeTests.cs +++ b/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client.Modes/StartingModeTests.cs @@ -167,7 +167,7 @@ public async Task FastResume_NoneExist () [Test] public async Task FastResume_SomeExist () { - PieceWriter.FilesThatExist.AddRange (new[]{ + await PieceWriter.CreateAsync (new[]{ Manager.Files [0], Manager.Files [2], }); diff --git a/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client/BlockingWriter.cs b/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client/BlockingWriter.cs index 7544588e4..9460bc62c 100644 --- a/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client/BlockingWriter.cs +++ b/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client/BlockingWriter.cs @@ -26,6 +26,11 @@ public async ReusableTask CloseAsync (ITorrentManagerFile file) await tcs.Task; } + public ReusableTask CreateAsync (ITorrentManagerFile file, FileCreationOptions options) + { + throw new NotImplementedException (); + } + public void Dispose () { } @@ -43,6 +48,12 @@ public async ReusableTask FlushAsync (ITorrentManagerFile file) Flushes.Add ((file, tcs)); await tcs.Task; } + + public ReusableTask GetLengthAsync (ITorrentManagerFile file) + { + throw new NotImplementedException (); + } + public async ReusableTask MoveAsync (ITorrentManagerFile file, string fullPath, bool overwrite) { var tcs = new ReusableTaskCompletionSource (); @@ -57,6 +68,11 @@ public async ReusableTask ReadAsync (ITorrentManagerFile file, long offset, return await tcs.Task; } + public ReusableTask SetLengthAsync (ITorrentManagerFile file, long length) + { + throw new NotImplementedException (); + } + public ReusableTask SetMaximumOpenFilesAsync (int maximumOpenFiles) { return ReusableTask.CompletedTask; diff --git a/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client/ClientEngineTests.cs b/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client/ClientEngineTests.cs index 37945d533..f4ca8cfec 100644 --- a/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client/ClientEngineTests.cs +++ b/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client/ClientEngineTests.cs @@ -521,6 +521,67 @@ public async Task StartAsyncAlwaysCreatesEmptyFiles () } } + [Test] + public async Task StartAsync_DoesNotCreateDoNotDownloadPriority () + { + using var writer = new TestWriter (); + var files = TorrentFile.Create (Constants.BlockSize * 4, 0, 1, 2, 3); + using var accessor = TempDir.Create (); + using var rig = TestRig.CreateMultiFile (files, Constants.BlockSize * 4, writer, baseDirectory: accessor.Path); + + foreach (var file in rig.Manager.Files) + await rig.Manager.SetFilePriorityAsync (file, Priority.DoNotDownload); + + await rig.Manager.StartAsync (); + + foreach (var file in rig.Manager.Files) + Assert.IsFalse (await writer.ExistsAsync (file)); + } + + [Test] + public async Task StartAsync_CreatesAllImplicatedFiles () + { + using var writer = new TestWriter (); + var files = TorrentFile.Create (Constants.BlockSize * 4, 0, 1, Constants.BlockSize * 4, 3); + using var accessor = TempDir.Create (); + using var rig = TestRig.CreateMultiFile (files, Constants.BlockSize * 4, writer, baseDirectory: accessor.Path); + + foreach (var file in rig.Manager.Files) + await rig.Manager.SetFilePriorityAsync (file, file.Length == 1 ? Priority.Normal : Priority.DoNotDownload); + + await rig.Manager.StartAsync (); + await rig.Manager.WaitForState (TorrentState.Downloading); + + Assert.IsFalse (await writer.ExistsAsync (rig.Manager.Files[0])); + Assert.IsTrue (await writer.ExistsAsync (rig.Manager.Files[1])); + Assert.IsTrue (await writer.ExistsAsync (rig.Manager.Files[2])); + Assert.IsFalse (await writer.ExistsAsync (rig.Manager.Files[3])); + } + + [Test] + public async Task StartAsync_SetPriorityCreatesAllImplicatedFiles () + { + using var writer = new TestWriter (); + var files = TorrentFile.Create (Constants.BlockSize * 4, 0, 1, Constants.BlockSize * 4, Constants.BlockSize * 4); + using var accessor = TempDir.Create (); + using var rig = TestRig.CreateMultiFile (files, Constants.BlockSize * 4, writer, baseDirectory: accessor.Path); + + foreach (var file in rig.Manager.Files) + await rig.Manager.SetFilePriorityAsync (file, Priority.DoNotDownload); + + await rig.Manager.StartAsync (); + + await rig.Manager.SetFilePriorityAsync (rig.Manager.Files[0], Priority.Normal); + Assert.IsTrue (await writer.ExistsAsync (rig.Manager.Files[0])); + Assert.IsTrue (rig.Manager.Files[0].BitField.AllTrue); + Assert.IsFalse (await writer.ExistsAsync (rig.Manager.Files[1])); + + await rig.Manager.SetFilePriorityAsync (rig.Manager.Files[1], Priority.Normal); + Assert.IsTrue (await writer.ExistsAsync (rig.Manager.Files[1])); + Assert.IsTrue (await writer.ExistsAsync (rig.Manager.Files[2])); + Assert.IsFalse (await writer.ExistsAsync (rig.Manager.Files[3])); + } + [Test] public async Task StopTest () { diff --git a/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client/DiskManagerExceptionTests.cs b/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client/DiskManagerExceptionTests.cs index f30bd62cb..3c6574d95 100644 --- a/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client/DiskManagerExceptionTests.cs +++ b/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client/DiskManagerExceptionTests.cs @@ -103,6 +103,21 @@ public ReusableTask SetMaximumOpenFilesAsync (int maximumOpenFiles) { return ReusableTask.CompletedTask; } + + public ReusableTask CreateAsync (ITorrentManagerFile file, FileCreationOptions options) + { + throw new NotImplementedException (); + } + + public ReusableTask GetLengthAsync (ITorrentManagerFile file) + { + throw new NotImplementedException (); + } + + public ReusableTask SetLengthAsync (ITorrentManagerFile file, long length) + { + throw new NotImplementedException (); + } } byte[] buffer; diff --git a/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client/DiskManagerTests.cs b/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client/DiskManagerTests.cs index e3b0f7a88..2dbf8000b 100644 --- a/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client/DiskManagerTests.cs +++ b/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client/DiskManagerTests.cs @@ -53,6 +53,11 @@ public ReusableTask CloseAsync (ITorrentManagerFile file) return ReusableTask.CompletedTask; } + public ReusableTask CreateAsync (ITorrentManagerFile file, FileCreationOptions options) + { + throw new NotImplementedException (); + } + public void Dispose () { } @@ -67,6 +72,11 @@ public ReusableTask FlushAsync (ITorrentManagerFile file) return ReusableTask.CompletedTask; } + public ReusableTask GetLengthAsync (ITorrentManagerFile file) + { + throw new NotImplementedException (); + } + public ReusableTask MoveAsync (ITorrentManagerFile file, string fullPath, bool overwrite) { return ReusableTask.CompletedTask; @@ -77,6 +87,11 @@ public virtual ReusableTask ReadAsync (ITorrentManagerFile file, long offse return ReusableTask.FromResult (0); } + public ReusableTask SetLengthAsync (ITorrentManagerFile file, long length) + { + throw new NotImplementedException (); + } + public ReusableTask SetMaximumOpenFilesAsync (int maximumOpenFiles) { return ReusableTask.CompletedTask; @@ -163,6 +178,21 @@ public ReusableTask SetMaximumOpenFilesAsync (int maximumOpenFiles) { return ReusableTask.CompletedTask; } + + public ReusableTask CreateAsync (ITorrentManagerFile file, FileCreationOptions options) + { + throw new NotImplementedException (); + } + + public ReusableTask GetLengthAsync (ITorrentManagerFile file) + { + throw new NotImplementedException (); + } + + public ReusableTask SetLengthAsync (ITorrentManagerFile file, long length) + { + throw new NotImplementedException (); + } } TestTorrentData fileData; diff --git a/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client/DiskManagerTests.v2.cs b/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client/DiskManagerTests.v2.cs index 887220391..5ec56e028 100644 --- a/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client/DiskManagerTests.v2.cs +++ b/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client/DiskManagerTests.v2.cs @@ -53,6 +53,11 @@ class ZeroWriter : IPieceWriter public ReusableTask CloseAsync (ITorrentManagerFile file) => ReusableTask.CompletedTask; + public ReusableTask CreateAsync (ITorrentManagerFile file, FileCreationOptions options) + { + throw new NotImplementedException (); + } + public void Dispose () { } @@ -63,6 +68,11 @@ public ReusableTask ExistsAsync (ITorrentManagerFile file) public ReusableTask FlushAsync (ITorrentManagerFile file) => ReusableTask.CompletedTask; + public ReusableTask GetLengthAsync (ITorrentManagerFile file) + { + throw new NotImplementedException (); + } + public ReusableTask MoveAsync (ITorrentManagerFile file, string fullPath, bool overwrite) => ReusableTask.CompletedTask; @@ -72,6 +82,11 @@ public ReusableTask ReadAsync (ITorrentManagerFile file, long offset, Memor return ReusableTask.FromResult (buffer.Length); } + public ReusableTask SetLengthAsync (ITorrentManagerFile file, long length) + { + throw new NotImplementedException (); + } + public ReusableTask SetMaximumOpenFilesAsync (int maximumOpenFiles) => ReusableTask.CompletedTask; diff --git a/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client/FastResumeTests.cs b/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client/FastResumeTests.cs index 1b530e70e..3760fe25d 100644 --- a/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client/FastResumeTests.cs +++ b/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client/FastResumeTests.cs @@ -212,7 +212,8 @@ public async Task DeleteBeforeHashing () Directory.CreateDirectory (Path.GetDirectoryName (path)); File.WriteAllBytes (path, new FastResume (torrent.InfoHashes, new BitField (torrent.PieceCount).SetAll (true), new BitField (torrent.PieceCount)).Encode ()); var manager = await engine.AddAsync (torrent, "savedir"); - testWriter.FilesThatExist = new System.Collections.Generic.List (manager.Files); + await testWriter.CreateAsync (manager.Files); + Assert.IsTrue (manager.HashChecked); manager.Engine.DiskManager.GetHashAsyncOverride = (torrent, pieceIndex, dest) => { first.SetResult (null); diff --git a/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client/TestRig.cs b/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client/TestRig.cs index e8160a027..aad308c4a 100644 --- a/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client/TestRig.cs +++ b/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Client/TestRig.cs @@ -51,8 +51,8 @@ namespace MonoTorrent.Client { public class TestWriter : IPieceWriter { - public List FilesThatExist = new List (); public List DoNotReadFrom = new List (); + public Dictionary FilesWithLength = new Dictionary (); public bool DontWrite; public byte? FillValue; @@ -108,7 +108,7 @@ public ReusableTask FlushAsync (ITorrentManagerFile file) public ReusableTask ExistsAsync (ITorrentManagerFile file) { - return ReusableTask.FromResult (FilesThatExist.Contains (file)); + return ReusableTask.FromResult (FilesWithLength.ContainsKey (file)); } public ReusableTask MoveAsync (ITorrentManagerFile file, string newPath, bool overwrite) @@ -120,6 +120,40 @@ public ReusableTask SetMaximumOpenFilesAsync (int maximumOpenFiles) { return ReusableTask.CompletedTask; } + + public async ReusableTask CreateAsync (IEnumerable files) + { + foreach (var file in files) + await CreateAsync (file, FileCreationOptions.PreferPreallocation); + return true; + } + + public ReusableTask CreateAsync (ITorrentManagerFile file, FileCreationOptions options) + { + if (FilesWithLength.ContainsKey (file)) + return ReusableTask.FromResult (false); + + FilesWithLength.Add (file, options == FileCreationOptions.PreferPreallocation ? file.Length : 0); + return ReusableTask.FromResult (true); + } + + public ReusableTask GetLengthAsync (ITorrentManagerFile file) + { + if (FilesWithLength.TryGetValue (file, out var length)) + return ReusableTask.FromResult (length); + return ReusableTask.FromResult (null); + } + + public ReusableTask SetLengthAsync (ITorrentManagerFile file, long length) + { + // If the file exists, change it's length + if (FilesWithLength.ContainsKey (file)) + FilesWithLength[file] = length; + + // This is successful only if the file existed beforehand. No action is taken + // if the file did not exist. + return ReusableTask.FromResult (FilesWithLength.ContainsKey (file)); + } } class CustomTrackerConnection : ITrackerConnection diff --git a/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Streaming/StreamProviderTests.cs b/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Streaming/StreamProviderTests.cs index 42dabe599..0d45f9107 100644 --- a/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Streaming/StreamProviderTests.cs +++ b/src/Tests/Tests.MonoTorrent.Client/MonoTorrent.Streaming/StreamProviderTests.cs @@ -71,7 +71,7 @@ public async Task CreateStream () { var manager = await Engine.AddStreamingAsync (Torrent, "test"); await manager.LoadFastResumeAsync (new FastResume (manager.InfoHashes, new BitField (manager.Torrent.PieceCount ()).SetAll (true), new BitField (manager.Torrent.PieceCount ()))); - PieceWriter.FilesThatExist.AddRange (manager.Files); + await PieceWriter.CreateAsync (manager.Files); await manager.StartAsync (); await manager.WaitForState (TorrentState.Seeding); @@ -87,7 +87,7 @@ public async Task CreateStream_Prebuffer () { var manager = await Engine.AddStreamingAsync (Torrent, "test"); await manager.LoadFastResumeAsync (new FastResume (manager.InfoHashes, new BitField (manager.Torrent.PieceCount ()).SetAll (true), new BitField (manager.Torrent.PieceCount ()))); - PieceWriter.FilesThatExist.AddRange (manager.Files); + await PieceWriter.CreateAsync (manager.Files); await manager.StartAsync (); await manager.WaitForState (TorrentState.Seeding); @@ -103,7 +103,7 @@ public async Task ReadPastEndOfStream () { var manager = await Engine.AddStreamingAsync (Torrent, "testDir"); await manager.LoadFastResumeAsync (new FastResume (manager.InfoHashes, new BitField (manager.Torrent.PieceCount ()).SetAll (true), new BitField (manager.Torrent.PieceCount ()))); - PieceWriter.FilesThatExist.AddRange (manager.Files); + await PieceWriter.CreateAsync (manager.Files); await manager.StartAsync (); await manager.WaitForState (TorrentState.Seeding); @@ -122,7 +122,7 @@ public async Task ReadLastByte () { var manager = await Engine.AddStreamingAsync (Torrent, "testDir"); await manager.LoadFastResumeAsync (new FastResume (manager.InfoHashes, new BitField (manager.Torrent.PieceCount ()).SetAll (true), new BitField (manager.Torrent.PieceCount ()))); - PieceWriter.FilesThatExist.AddRange (manager.Files); + await PieceWriter.CreateAsync (manager.Files); await manager.StartAsync (); await manager.WaitForState (TorrentState.Seeding); @@ -140,7 +140,7 @@ public async Task SeekBeforeStart () { var manager = await Engine.AddStreamingAsync (Torrent, "testDir"); await manager.LoadFastResumeAsync (new FastResume (manager.InfoHashes, new BitField (manager.Torrent.PieceCount ()).SetAll (true), new BitField (manager.Torrent.PieceCount ()))); - PieceWriter.FilesThatExist.AddRange (manager.Files); + await PieceWriter.CreateAsync (manager.Files); await manager.StartAsync (); await manager.WaitForState (TorrentState.Seeding); @@ -155,7 +155,7 @@ public async Task SeekToMiddle () { var manager = await Engine.AddStreamingAsync (Torrent, "testDir"); await manager.LoadFastResumeAsync (new FastResume (manager.InfoHashes, new BitField (manager.Torrent.PieceCount ()).SetAll (true), new BitField (manager.Torrent.PieceCount ()))); - PieceWriter.FilesThatExist.AddRange (manager.Files); + await PieceWriter.CreateAsync (manager.Files); await manager.StartAsync (); await manager.WaitForState (TorrentState.Seeding); @@ -169,7 +169,7 @@ public async Task SeekPastEnd () { var manager = await Engine.AddStreamingAsync (Torrent, "testDir"); await manager.LoadFastResumeAsync (new FastResume (manager.InfoHashes, new BitField (manager.Torrent.PieceCount ()).SetAll (true), new BitField (manager.Torrent.PieceCount ()))); - PieceWriter.FilesThatExist.AddRange (manager.Files); + await PieceWriter.CreateAsync (manager.Files); await manager.StartAsync (); await manager.WaitForState (TorrentState.Seeding); @@ -184,7 +184,7 @@ public async Task CreateStreamBeforeStart () { var manager = await Engine.AddStreamingAsync (Torrent, "testDir"); await manager.LoadFastResumeAsync (new FastResume (manager.InfoHashes, new BitField (manager.Torrent.PieceCount ()).SetAll (true), new BitField (manager.Torrent.PieceCount ()))); - PieceWriter.FilesThatExist.AddRange (manager.Files); + await PieceWriter.CreateAsync (manager.Files); Assert.ThrowsAsync (() => manager.StreamProvider.CreateHttpStreamAsync (manager.Files[0])); } @@ -194,7 +194,7 @@ public async Task CreateStreamTwice () { var manager = await Engine.AddStreamingAsync (Torrent, "testDir"); await manager.LoadFastResumeAsync (new FastResume (manager.InfoHashes, new BitField (manager.Torrent.PieceCount ()).SetAll (true), new BitField (manager.Torrent.PieceCount ()))); - PieceWriter.FilesThatExist.AddRange (manager.Files); + await PieceWriter.CreateAsync (manager.Files); await manager.StartAsync (); await manager.WaitForState (TorrentState.Seeding); diff --git a/src/Tests/Tests.MonoTorrent.IntegrationTests/IntegrationTests_FakePieceWriter.cs b/src/Tests/Tests.MonoTorrent.IntegrationTests/IntegrationTests_FakePieceWriter.cs index a5f514c5f..b65bc1d4b 100644 --- a/src/Tests/Tests.MonoTorrent.IntegrationTests/IntegrationTests_FakePieceWriter.cs +++ b/src/Tests/Tests.MonoTorrent.IntegrationTests/IntegrationTests_FakePieceWriter.cs @@ -106,6 +106,22 @@ public ReusableTask WriteAsync (ITorrentManagerFile file, long offset, ReadOnlyM return default; } + + public ReusableTask GetLengthAsync (ITorrentManagerFile file) + { + // pretend the file exists but is empty if leeching + return ReusableTask.FromResult (IsSeeder ? file.Length : 0); + } + + public ReusableTask SetLengthAsync (ITorrentManagerFile file, long length) + { + throw new NotImplementedException (); + } + + public ReusableTask CreateAsync (ITorrentManagerFile file, FileCreationOptions options) + { + throw new NotImplementedException (); + } } class CustomTorrentFileSource : ITorrentFileSource diff --git a/src/Tests/Tests.MonoTorrent.IntegrationTests/IntegrationTests_RealPieceWriter.cs b/src/Tests/Tests.MonoTorrent.IntegrationTests/IntegrationTests_RealPieceWriter.cs index 24be3e5e4..14c60d2b7 100644 --- a/src/Tests/Tests.MonoTorrent.IntegrationTests/IntegrationTests_RealPieceWriter.cs +++ b/src/Tests/Tests.MonoTorrent.IntegrationTests/IntegrationTests_RealPieceWriter.cs @@ -259,12 +259,20 @@ public async Task CreateAndDownloadTorrent (TorrentType torrentType, bool create leecherIsSeeding.TrySetException (e.TorrentManager.Error.Exception); }; - var seederManager = !useWebSeedDownload ? await StartTorrent (seederEngine, torrent, _seederDir.FullName, explitlyHashCheck, seederIsSeedingHandler) : null; - + TorrentManager seederManager; + TorrentManager leecherManager; var magnetLink = new MagnetLink (torrent.InfoHashes, "testing", torrent.AnnounceUrls.SelectMany (t => t).ToList (), null, torrent.Size); - var leecherManager = magnetLinkLeecher - ? await StartTorrent (leecherEngine, magnetLink, _leecherDir.FullName, explitlyHashCheck, leecherIsSeedingHandler) - : await StartTorrent (leecherEngine, torrent, _leecherDir.FullName, explitlyHashCheck, leecherIsSeedingHandler); + if (false && new Random ().Next (0, 100) % 2 == 1) { + seederManager = !useWebSeedDownload ? await StartTorrent (seederEngine, torrent, _seederDir.FullName, explitlyHashCheck, seederIsSeedingHandler) : null; + leecherManager = magnetLinkLeecher + ? await StartTorrent (leecherEngine, magnetLink, _leecherDir.FullName, explitlyHashCheck, leecherIsSeedingHandler) + : await StartTorrent (leecherEngine, torrent, _leecherDir.FullName, explitlyHashCheck, leecherIsSeedingHandler); + } else { + leecherManager = magnetLinkLeecher + ? await StartTorrent (leecherEngine, magnetLink, _leecherDir.FullName, explitlyHashCheck, leecherIsSeedingHandler) + : await StartTorrent (leecherEngine, torrent, _leecherDir.FullName, explitlyHashCheck, leecherIsSeedingHandler); + seederManager = !useWebSeedDownload ? await StartTorrent (seederEngine, torrent, _seederDir.FullName, explitlyHashCheck, seederIsSeedingHandler) : null; + } var timeout = new CancellationTokenSource (CancellationTimeout); timeout.Token.Register (() => { seederIsSeeding.TrySetCanceled (); }); @@ -319,6 +327,7 @@ private ClientEngine GetEngine (int port, Factories factories) DhtEndPoint = null, AllowPortForwarding = false, WebSeedDelay = TimeSpan.Zero, + AllowLocalPeerDiscovery = false, }; var engine = new ClientEngine (settingBuilder.ToSettings (), factories); return engine; @@ -418,6 +427,8 @@ private async Task StartTorrent (ClientEngine clientEngine, Torr await manager.HashCheckAsync (true); else await manager.StartAsync (); + + await Task.Delay (1000); return manager; } } diff --git a/src/Tests/Tests.MonoTorrent.PieceWriter/DiskWriterTests.cs b/src/Tests/Tests.MonoTorrent.PieceWriter/DiskWriterTests.cs index 666d5518f..dd07d0c86 100644 --- a/src/Tests/Tests.MonoTorrent.PieceWriter/DiskWriterTests.cs +++ b/src/Tests/Tests.MonoTorrent.PieceWriter/DiskWriterTests.cs @@ -123,10 +123,10 @@ public async Task TruncateLargeFile_ThenWrite () using (var file = new FileStream (TorrentFile.FullPath, FileMode.OpenOrCreate)) file.Write (new byte[TorrentFile.Length + 1]); - // This should implicitly truncate. + // File truncating only happens when starting a torrent in order to seed or leech it. It does not happen during hashchecking using var writer = new DiskWriter (); await writer.WriteAsync (TorrentFile, 0, new byte[12]); - Assert.AreEqual (TorrentFile.Length, new FileInfo (TorrentFile.FullPath).Length); + Assert.AreEqual (TorrentFile.Length + 1, new FileInfo (TorrentFile.FullPath).Length); } [Test] diff --git a/src/Tests/Tests.MonoTorrent.PieceWriter/MemoryCacheTests.cs b/src/Tests/Tests.MonoTorrent.PieceWriter/MemoryCacheTests.cs index 38bef6c7a..700ff6efd 100644 --- a/src/Tests/Tests.MonoTorrent.PieceWriter/MemoryCacheTests.cs +++ b/src/Tests/Tests.MonoTorrent.PieceWriter/MemoryCacheTests.cs @@ -58,6 +58,11 @@ public ReusableTask CloseAsync (ITorrentManagerFile file) return ReusableTask.CompletedTask; } + public ReusableTask CreateAsync (ITorrentManagerFile file, FileCreationOptions options) + { + throw new NotImplementedException (); + } + public void Dispose () { } @@ -74,6 +79,11 @@ public ReusableTask FlushAsync (ITorrentManagerFile file) return ReusableTask.CompletedTask; } + public ReusableTask GetLengthAsync (ITorrentManagerFile file) + { + throw new NotSupportedException (); + } + public ReusableTask MoveAsync (ITorrentManagerFile file, string fullPath, bool overwrite) { Moves.Add ((file, fullPath, overwrite)); @@ -95,6 +105,11 @@ public ReusableTask ReadAsync (ITorrentManagerFile file, long offset, Memor return ReusableTask.FromResult (0); } + public ReusableTask SetLengthAsync (ITorrentManagerFile file, long length) + { + throw new NotSupportedException (); + } + public ReusableTask SetMaximumOpenFilesAsync (int maximumOpenFiles) { return ReusableTask.CompletedTask; diff --git a/src/Tests/Tests.MonoTorrent.PieceWriter/NullWriter.cs b/src/Tests/Tests.MonoTorrent.PieceWriter/NullWriter.cs index 03c74512c..4efb2c449 100644 --- a/src/Tests/Tests.MonoTorrent.PieceWriter/NullWriter.cs +++ b/src/Tests/Tests.MonoTorrent.PieceWriter/NullWriter.cs @@ -43,6 +43,11 @@ public ReusableTask CloseAsync (ITorrentManagerFile file) return ReusableTask.CompletedTask; } + public ReusableTask CreateAsync (ITorrentManagerFile file, FileCreationOptions options) + { + throw new NotImplementedException (); + } + public void Dispose () { } @@ -57,6 +62,11 @@ public ReusableTask FlushAsync (ITorrentManagerFile file) return ReusableTask.CompletedTask; } + public ReusableTask GetLengthAsync (ITorrentManagerFile file) + { + return ReusableTask.FromResult (null); + } + public ReusableTask MoveAsync (ITorrentManagerFile file, string fullPath, bool overwrite) { return ReusableTask.CompletedTask; @@ -67,6 +77,11 @@ public ReusableTask ReadAsync (ITorrentManagerFile file, long offset, Memor return ReusableTask.FromResult (0); } + public ReusableTask SetLengthAsync (ITorrentManagerFile file, long length) + { + throw new NotImplementedException (); + } + public ReusableTask SetMaximumOpenFilesAsync (int maximumOpenFiles) { return ReusableTask.CompletedTask; @@ -76,5 +91,10 @@ public ReusableTask WriteAsync (ITorrentManagerFile file, long offset, ReadOnlyM { return ReusableTask.CompletedTask; } + + ReusableTask IPieceWriter.SetLengthAsync (ITorrentManagerFile file, long length) + { + throw new NotImplementedException (); + } } }