Skip to content

Commit

Permalink
Merge pull request #2250 from erri120/collection-download
Browse files Browse the repository at this point in the history
Download external collection files
  • Loading branch information
erri120 authored Nov 14, 2024
2 parents ec18f4d + 3841b32 commit effb8d8
Show file tree
Hide file tree
Showing 11 changed files with 282 additions and 58 deletions.
Original file line number Diff line number Diff line change
@@ -1,50 +1,51 @@
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NexusMods.Abstractions.Collections;
using NexusMods.Abstractions.Collections.Types;
using NexusMods.Abstractions.Jobs;
using NexusMods.Abstractions.Library.Models;
using NexusMods.Abstractions.MnemonicDB.Attributes;
using NexusMods.MnemonicDB.Abstractions;
using NexusMods.Networking.HttpDownloader;
using NexusMods.Paths;

namespace NexusMods.Collections;
namespace NexusMods.Networking.NexusWebApi;

public record DirectDownloadJob : HttpDownloadJob
/// <summary>
/// Job for external collection downloads.
/// </summary>
public record ExternalDownloadJob : HttpDownloadJob
{
/// <summary>
/// The expected MD5 hash value of the downloaded file.
/// </summary>
public required Md5HashValue ExpectedMd5 { get; init; }

/// <summary>
/// The user-friendly name of the file.
/// </summary>
public required string LogicalFileName { get; init; }

/// <summary>
/// Create a new download job for the given URL, the job will fail if the downloaded file does not
/// match the expected MD5 hash.
/// </summary>
public static IJobTask<DirectDownloadJob, AbsolutePath> Create(IServiceProvider provider, Uri uri,
public static IJobTask<ExternalDownloadJob, AbsolutePath> Create(IServiceProvider provider, Uri uri,
Md5HashValue expectedMd5, string logicalFileName)
{
var monitor = provider.GetRequiredService<IJobMonitor>();
var tempFileManager = provider.GetRequiredService<TemporaryFileManager>();
var job = new DirectDownloadJob
var job = new ExternalDownloadJob
{
Logger = provider.GetRequiredService<ILogger<DirectDownloadJob>>(),
Logger = provider.GetRequiredService<ILogger<ExternalDownloadJob>>(),
ExpectedMd5 = expectedMd5,
LogicalFileName = logicalFileName,
DownloadPageUri = uri,
Destination = tempFileManager.CreateFile(),
Uri = uri,
};
return monitor.Begin<DirectDownloadJob, AbsolutePath>(job);

return monitor.Begin<ExternalDownloadJob, AbsolutePath>(job);
}


Expand All @@ -59,7 +60,7 @@ public override async ValueTask AddMetadata(ITransaction tx, LibraryFile.New lib
if (md5Actual != ExpectedMd5)
throw new InvalidOperationException($"MD5 hash mismatch. Expected: {ExpectedMd5}, Actual: {md5Actual}");
}

tx.Add(libraryFile, DirectDownloadLibraryFile.Md5, ExpectedMd5);
tx.Add(libraryFile, DirectDownloadLibraryFile.LogicalFileName, LogicalFileName);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public partial class NexusModsLibrary
);

var collectionRevisionInfo = apiResult.Data?.CollectionRevision;
if (collectionRevisionInfo is null) throw new NotSupportedException($"API call returned no data for `{slug}` `{revisionNumber}`");
if (collectionRevisionInfo is null) throw new NotSupportedException($"API call returned no data for collection slug `{slug}` revision `{revisionNumber}`");

using var tx = _connection.BeginTransaction();
var db = _connection.Db;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,11 +187,11 @@ public async Task<IJobTask<NexusModsDownloadJob, AbsolutePath>> CreateDownloadJo
}

public async Task<IJobTask<NexusModsDownloadJob, AbsolutePath>> CreateDownloadJob(
AbsolutePath destination,
NexusModsFileMetadata.ReadOnly fileMetadata,
CancellationToken cancellationToken = default)
{
await using var tempPath = _temporaryFileManager.CreateFile();
return await CreateDownloadJob(tempPath, fileMetadata.Uid.GameId, fileMetadata.ModPage.Uid.ModId, fileMetadata.Uid.FileId, cancellationToken: cancellationToken);
return await CreateDownloadJob(destination, fileMetadata.Uid.GameId, fileMetadata.ModPage.Uid.ModId, fileMetadata.Uid.FileId, cancellationToken: cancellationToken);
}

/// <summary>
Expand Down
1 change: 1 addition & 0 deletions src/NexusMods.App.UI/NexusMods.App.UI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
<ProjectReference Include="..\Networking\NexusMods.Networking.Downloaders\NexusMods.Networking.Downloaders.csproj" />
<ProjectReference Include="..\Networking\NexusMods.Networking.NexusWebApi\NexusMods.Networking.NexusWebApi.csproj" />
<ProjectReference Include="..\NexusMods.App.BuildInfo\NexusMods.App.BuildInfo.csproj" />
<ProjectReference Include="..\NexusMods.Collections\NexusMods.Collections.csproj" />
<ProjectReference Include="..\NexusMods.Icons\NexusMods.Icons.csproj" />
<ProjectReference Include="..\NexusMods.Media\NexusMods.Media.csproj" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,15 @@
using DynamicData;
using Microsoft.Extensions.DependencyInjection;
using NexusMods.Abstractions.Jobs;
using NexusMods.Abstractions.Library;
using NexusMods.Abstractions.NexusModsLibrary;
using NexusMods.Abstractions.NexusModsLibrary.Models;
using NexusMods.Abstractions.NexusWebApi;
using NexusMods.Abstractions.NexusWebApi.Types;
using NexusMods.Abstractions.Resources;
using NexusMods.Abstractions.Telemetry;
using NexusMods.App.UI.Controls;
using NexusMods.App.UI.Extensions;
using NexusMods.App.UI.Pages.LibraryPage;
using NexusMods.App.UI.Windows;
using NexusMods.App.UI.WorkspaceSystem;
using NexusMods.CrossPlatform.Process;
using NexusMods.Collections;
using NexusMods.MnemonicDB.Abstractions;
using NexusMods.Networking.NexusWebApi;
using NexusMods.Paths;
using R3;
using ReactiveUI;
Expand All @@ -30,12 +24,10 @@ public class CollectionDownloadViewModel : APageViewModel<ICollectionDownloadVie
{
private readonly CollectionRevisionMetadata.ReadOnly _revision;
private readonly CollectionMetadata.ReadOnly _collection;

private readonly IServiceProvider _serviceProvider;
private readonly NexusModsDataProvider _nexusModsDataProvider;
private readonly NexusModsLibrary _nexusModsLibrary;
private readonly ILibraryService _libraryService;
private readonly TemporaryFileManager _temporaryFileManager;
private readonly IOSInterop _osInterop;
private readonly ILoginManager _loginManager;
private readonly CollectionDownloader _collectionDownloader;

public CollectionDownloadTreeDataGridAdapter TreeDataGridAdapter { get; }

Expand All @@ -44,12 +36,9 @@ public CollectionDownloadViewModel(
IServiceProvider serviceProvider,
CollectionRevisionMetadata.ReadOnly revisionMetadata) : base(windowManager)
{
_serviceProvider = serviceProvider;
_nexusModsDataProvider = serviceProvider.GetRequiredService<NexusModsDataProvider>();
_nexusModsLibrary = serviceProvider.GetRequiredService<NexusModsLibrary>();
_libraryService = serviceProvider.GetRequiredService<ILibraryService>();
_temporaryFileManager = serviceProvider.GetRequiredService<TemporaryFileManager>();
_osInterop = serviceProvider.GetRequiredService<IOSInterop>();
_loginManager = serviceProvider.GetRequiredService<ILoginManager>();
_collectionDownloader = new CollectionDownloader(_serviceProvider);

var tileImagePipeline = ImagePipelines.GetCollectionTileImagePipeline(serviceProvider);
var backgroundImagePipeline = ImagePipelines.GetCollectionBackgroundImagePipeline(serviceProvider);
Expand Down Expand Up @@ -96,27 +85,19 @@ public CollectionDownloadViewModel(
.AddTo(disposables);
TreeDataGridAdapter.MessageSubject.SubscribeAwait(
onNextAsync: (message, cancellationToken) => DownloadOrOpenPage(message.Item.AsT0, cancellationToken),
onNextAsync: (message, cancellationToken) =>
{
return message.Item.Match(
f0: x => _collectionDownloader.Download(x, cancellationToken),
f1: x => _collectionDownloader.Download(x, cancellationToken)
);
},
awaitOperation: AwaitOperation.Parallel,
configureAwait: false
).AddTo(disposables);
});
}

private async ValueTask DownloadOrOpenPage(NexusModsFileMetadata.ReadOnly fileMetadata, CancellationToken cancellationToken)
{
if (_loginManager.IsPremium)
{
await using var tempPath = _temporaryFileManager.CreateFile();
var job = await _nexusModsLibrary.CreateDownloadJob(tempPath, fileMetadata.Uid.GameId, fileMetadata.ModPage.Uid.ModId, fileMetadata.Uid.FileId, cancellationToken: cancellationToken);
await _libraryService.AddDownload(job);
}
else
{
await _osInterop.OpenUrl(fileMetadata.GetUri(), logOutput: false, fireAndForget: true, cancellationToken: cancellationToken);
}
}

public string Name => _collection.Name;
public string Summary => _collection.Summary;
public int ModCount => _revision.Downloads.Count;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using NexusMods.Abstractions.Jobs;
using NexusMods.Abstractions.NexusModsLibrary.Models;
using NexusMods.App.UI.Controls;
using NexusMods.App.UI.Extensions;
using NexusMods.App.UI.Pages.LibraryPage;
using NexusMods.MnemonicDB.Abstractions;
using NexusMods.Paths;
using R3;

namespace NexusMods.App.UI.Pages.CollectionDownload;

public class ExternalDownloadItemModel : TreeDataGridItemModel<ILibraryItemModel, EntityId>,
ILibraryItemWithName,
ILibraryItemWithSize,
ILibraryItemWithDownloadAction
{
public ExternalDownloadItemModel(CollectionDownloadExternal.ReadOnly externalDownload)
{
DownloadableItem = new DownloadableItem(externalDownload);
FormattedSize = ItemSize.ToFormattedProperty();
DownloadItemCommand = ILibraryItemWithDownloadAction.CreateCommand(this);

// ReSharper disable once NotDisposedResource
var modelActivationDisposable = this.WhenActivated(static (self, disposables) =>
{
self.IsInLibraryObservable.CombineLatest(
source2: self.DownloadJobObservable.SelectMany(job => job.ObservableStatus.ToObservable()).Prepend(JobStatus.None),
resultSelector: static (a, b) => (a, b))
.ObserveOnUIThreadDispatcher()
.Subscribe(self, static (tuple, self) =>
{
var (inLibrary, status) = tuple;
self.DownloadState.Value = inLibrary ? JobStatus.Completed : status;
self.DownloadButtonText.Value = ILibraryItemWithDownloadAction.GetButtonText(status: self.DownloadState.Value);
}).AddTo(disposables);
});

_modelDisposable = Disposable.Combine(
modelActivationDisposable,
Name,
ItemSize,
FormattedSize,
DownloadItemCommand,
DownloadState,
DownloadButtonText
);
}

public required Observable<bool> IsInLibraryObservable { get; init; }
public required Observable<IJob> DownloadJobObservable { get; init; }

public BindableReactiveProperty<string> Name { get; } = new(value: "-");

public ReactiveProperty<Size> ItemSize { get; } = new();
public BindableReactiveProperty<string> FormattedSize { get; }

public DownloadableItem DownloadableItem { get; }

public ReactiveCommand<Unit, DownloadableItem> DownloadItemCommand { get; }

public BindableReactiveProperty<JobStatus> DownloadState { get; } = new();

public BindableReactiveProperty<string> DownloadButtonText { get; } = new(value: ILibraryItemWithDownloadAction.GetButtonText(status: JobStatus.None));

private bool _isDisposed;
private readonly IDisposable _modelDisposable;

protected override void Dispose(bool disposing)
{
if (!_isDisposed)
{
if (disposing)
{
_modelDisposable.Dispose();
}

_isDisposed = true;
}

base.Dispose(disposing);
}

public override string ToString() => $"External Download: {Name.Value}";
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using NexusMods.Abstractions.Jobs;
using NexusMods.Abstractions.MnemonicDB.Attributes;
using NexusMods.Abstractions.NexusModsLibrary;
using NexusMods.Abstractions.NexusModsLibrary.Models;
using NexusMods.App.UI.Controls;
using NexusMods.Paths;
using OneOf;
using R3;

Expand Down Expand Up @@ -66,9 +69,9 @@ public static string GetButtonText(int numInstalled, int numTotal, bool isExpand
}
}

public class DownloadableItem : OneOfBase<NexusModsFileMetadata.ReadOnly>
public class DownloadableItem : OneOfBase<CollectionDownloadNexusMods.ReadOnly, CollectionDownloadExternal.ReadOnly>
{
public DownloadableItem(OneOf<NexusModsFileMetadata.ReadOnly> input) : base(input) { }
public DownloadableItem(OneOf<CollectionDownloadNexusMods.ReadOnly, CollectionDownloadExternal.ReadOnly> input) : base(input) { }
}

public interface ILibraryItemWithDownloadAction : ILibraryItemWithAction
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using NexusMods.Abstractions.Jobs;
using NexusMods.Abstractions.NexusModsLibrary;
using NexusMods.Abstractions.NexusModsLibrary.Models;
using NexusMods.App.UI.Controls;
using NexusMods.App.UI.Extensions;
using NexusMods.MnemonicDB.Abstractions;
Expand All @@ -14,9 +14,9 @@ public class NexusModsFileMetadataLibraryItemModel : TreeDataGridItemModel<ILibr
ILibraryItemWithSize,
ILibraryItemWithDownloadAction
{
public NexusModsFileMetadataLibraryItemModel(NexusModsFileMetadata.ReadOnly fileMetadata)
public NexusModsFileMetadataLibraryItemModel(CollectionDownloadNexusMods.ReadOnly download)
{
DownloadableItem = new DownloadableItem(fileMetadata);
DownloadableItem = new DownloadableItem(download);
FormattedSize = ItemSize.ToFormattedProperty();
DownloadItemCommand = ILibraryItemWithDownloadAction.CreateCommand(this);

Expand Down
Loading

0 comments on commit effb8d8

Please sign in to comment.