mirror of
https://github.com/wabbajack-tools/wabbajack.git
synced 2024-08-30 18:42:17 +00:00
Add nexus upload code
This commit is contained in:
parent
d0d2435661
commit
252ff675c6
@ -67,6 +67,7 @@ internal class Program
|
||||
services.AddSingleton<IVerb, SteamLogin>();
|
||||
services.AddSingleton<IVerb, SteamAppDumpInfo>();
|
||||
services.AddSingleton<IVerb, SteamDownloadFile>();
|
||||
services.AddSingleton<IVerb, UploadToNexus>();
|
||||
|
||||
services.AddSingleton<IUserInterventionHandler, UserInterventionHandler>();
|
||||
}).Build();
|
||||
|
47
Wabbajack.CLI/Verbs/UploadToNexus.cs
Normal file
47
Wabbajack.CLI/Verbs/UploadToNexus.cs
Normal file
@ -0,0 +1,47 @@
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Invocation;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Wabbajack.Common;
|
||||
using Wabbajack.DTOs.JsonConverters;
|
||||
using Wabbajack.Networking.NexusApi;
|
||||
using Wabbajack.Networking.NexusApi.DTOs;
|
||||
using Wabbajack.Paths;
|
||||
using Wabbajack.Paths.IO;
|
||||
|
||||
|
||||
namespace Wabbajack.CLI.Verbs;
|
||||
|
||||
public class UploadToNexus : IVerb
|
||||
{
|
||||
private readonly ILogger<UploadToNexus> _logger;
|
||||
private readonly NexusApi _client;
|
||||
private readonly DTOSerializer _dtos;
|
||||
|
||||
public UploadToNexus(ILogger<UploadToNexus> logger, NexusApi wjClient, DTOSerializer dtos)
|
||||
{
|
||||
_logger = logger;
|
||||
_client = wjClient;
|
||||
_dtos = dtos;
|
||||
}
|
||||
public Command MakeCommand()
|
||||
{
|
||||
var command = new Command("upload-to-nexus");
|
||||
command.Add(new Option<AbsolutePath>(new[] {"-d", "-definition"}, "Definition JSON file"));
|
||||
command.Description = "Uploads a file to the Nexus defined by the given .json definition file";
|
||||
command.Handler = CommandHandler.Create(Run);
|
||||
return command;
|
||||
}
|
||||
|
||||
public async Task<int> Run(AbsolutePath definition)
|
||||
{
|
||||
var d = await definition.FromJson<UploadDefinition>(_dtos);
|
||||
|
||||
await _client.UploadFile(d);
|
||||
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
120
Wabbajack.Networking.NexusApi/DTOs/UploadDefinition.cs
Normal file
120
Wabbajack.Networking.NexusApi/DTOs/UploadDefinition.cs
Normal file
@ -0,0 +1,120 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Web;
|
||||
using Wabbajack.DTOs;
|
||||
using Wabbajack.Paths;
|
||||
using Wabbajack.Paths.IO;
|
||||
|
||||
namespace Wabbajack.Networking.NexusApi.DTOs;
|
||||
|
||||
public enum Category : int
|
||||
{
|
||||
Main = 1,
|
||||
Updates = 2,
|
||||
Optional = 3,
|
||||
Old = 4,
|
||||
Misc = 5,
|
||||
Archives = 7
|
||||
}
|
||||
|
||||
public enum ChunkStatus
|
||||
{
|
||||
NoContent,
|
||||
Waiting,
|
||||
Done
|
||||
}
|
||||
|
||||
public class UploadDefinition
|
||||
{
|
||||
public const long ChunkSize = 5242880; // 5MB chunks
|
||||
public Game Game { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public long GameId => Game.MetaData().NexusGameId;
|
||||
|
||||
public string Name { get; set; }
|
||||
public AbsolutePath Path { get; set; }
|
||||
|
||||
public string Version { get; set; }
|
||||
|
||||
public string Category { get; set; }
|
||||
|
||||
public bool NewExisting { get; set; }
|
||||
|
||||
public long OldFileId { get; set; }
|
||||
|
||||
public bool RemoveOldVersion { get; set; }
|
||||
|
||||
public string BriefOverview { get; set; }
|
||||
|
||||
public string FileUUID { get; set; } = "";
|
||||
|
||||
public long FileSize => Path.Size();
|
||||
public long ModId { get; set; }
|
||||
|
||||
public long TotalChunks => (long) Math.Ceiling(FileSize / (double) ChunkSize);
|
||||
public string ResumableIdentifier => FileSize + "-" + Path.FileName.ToString().Replace(".", "").Replace(" ", "");
|
||||
public string ResumableRelativePath => HttpUtility.UrlEncode(Path.FileName.ToString());
|
||||
public bool SetAsMain { get; set; }
|
||||
|
||||
public IEnumerable<Chunk> Chunks()
|
||||
{
|
||||
|
||||
var size = FileSize;
|
||||
|
||||
if (size <= ChunkSize)
|
||||
{
|
||||
|
||||
yield return new Chunk
|
||||
{
|
||||
Index = 0,
|
||||
Offset = 0,
|
||||
Size = size
|
||||
};
|
||||
yield break;
|
||||
}
|
||||
|
||||
for (long block = 0; block * ChunkSize < size; block++) {
|
||||
yield return new Chunk
|
||||
{
|
||||
Index = block,
|
||||
Size = Math.Min(ChunkSize, size - block * ChunkSize),
|
||||
Offset = block * ChunkSize
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class Chunk
|
||||
{
|
||||
public long Index { get; set; }
|
||||
public long Size { get; set; }
|
||||
public long Offset { get; set; }
|
||||
}
|
||||
|
||||
public class ChunkStatusResult
|
||||
{
|
||||
[JsonPropertyName("filename")]
|
||||
public string Filename { get; set; }
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public bool Status { get; set; }
|
||||
|
||||
[JsonPropertyName("uuid")]
|
||||
public string UUID { get; set; }
|
||||
}
|
||||
|
||||
public class FileStatusResult
|
||||
{
|
||||
[JsonPropertyName("file_chunks_reassembled")]
|
||||
public bool FileChunksAssembled { get; set; }
|
||||
|
||||
[JsonPropertyName("s3_upload_complete")]
|
||||
public bool S3UploadComplete { get; set; }
|
||||
|
||||
[JsonPropertyName("virus_total_result")]
|
||||
public int VirusTotalStatus { get; set; }
|
||||
|
||||
}
|
@ -1,16 +1,22 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Wabbajack.Common;
|
||||
using Wabbajack.DTOs;
|
||||
using Wabbajack.DTOs.Logins;
|
||||
using Wabbajack.Networking.Http;
|
||||
using Wabbajack.Networking.Http.Interfaces;
|
||||
using Wabbajack.Networking.NexusApi.DTOs;
|
||||
using Wabbajack.Paths;
|
||||
using Wabbajack.Paths.IO;
|
||||
using Wabbajack.RateLimiter;
|
||||
using Wabbajack.Server.DTOs;
|
||||
|
||||
@ -165,4 +171,143 @@ public class NexusApi
|
||||
var msg = await GenerateMessage(HttpMethod.Get, Endpoints.Updates, game.MetaData().NexusName, "1m");
|
||||
return await Send<UpdateEntry[]>(msg, token);
|
||||
}
|
||||
|
||||
public async Task<ChunkStatus> ChunkStatus(UploadDefinition definition, Chunk chunk)
|
||||
{
|
||||
var msg = new HttpRequestMessage();
|
||||
msg.Method = HttpMethod.Get;
|
||||
|
||||
var query =
|
||||
$"resumableChunkNumber={chunk.Index + 1}&resumableCurrentChunkSize={chunk.Size}&resumableTotalSize={definition.FileSize}"
|
||||
+ $"&resumableType=&resumableIdentifier={definition.ResumableIdentifier}&resumableFilename={definition.ResumableRelativePath}"
|
||||
+ $"&resumableRelativePath={definition.ResumableRelativePath}&resumableTotalChunks={definition.Chunks().Count()}";
|
||||
|
||||
msg.RequestUri = new Uri($"https://upload.nexusmods.com/uploads/chunk?{query}");
|
||||
|
||||
using var result = await _client.SendAsync(msg);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
throw new HttpException(result);
|
||||
if (result.StatusCode == HttpStatusCode.NoContent)
|
||||
return DTOs.ChunkStatus.NoContent;
|
||||
|
||||
var status = await result.Content.ReadFromJsonAsync<ChunkStatusResult>();
|
||||
return status?.Status ?? false ? DTOs.ChunkStatus.Done : DTOs.ChunkStatus.Waiting;
|
||||
}
|
||||
|
||||
public async Task<ChunkStatusResult> UploadChunk(UploadDefinition d, Chunk chunk)
|
||||
{
|
||||
var form = new MultipartFormDataContent();
|
||||
form.Add(new StringContent((chunk.Index+1).ToString()), "resumableChunkNumber");
|
||||
form.Add(new StringContent(UploadDefinition.ChunkSize.ToString()), "resumableChunkSize");
|
||||
form.Add(new StringContent(chunk.Size.ToString()), "resumableCurrentChunkSize");
|
||||
form.Add(new StringContent(d.FileSize.ToString()), "resumableTotalSize");
|
||||
form.Add(new StringContent(""), "resumableType");
|
||||
form.Add(new StringContent(d.ResumableIdentifier), "resumableIdentifier");
|
||||
form.Add(new StringContent(d.ResumableRelativePath), "resumableFilename");
|
||||
form.Add(new StringContent(d.ResumableRelativePath), "resumableRelativePath");
|
||||
form.Add(new StringContent(d.Chunks().Count().ToString()), "resumableTotalChunks");
|
||||
|
||||
await using var ms = new MemoryStream();
|
||||
await using var fs = d.Path.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
fs.Position = chunk.Offset;
|
||||
await fs.CopyToLimitAsync(ms, (int)chunk.Size, CancellationToken.None);
|
||||
ms.Position = 0;
|
||||
|
||||
form.Add(new StreamContent(ms), "file", "blob");
|
||||
|
||||
var msg = new HttpRequestMessage(HttpMethod.Post, "https://upload.nexusmods.com/uploads/chunk");
|
||||
msg.Content = form;
|
||||
|
||||
var result = await _client.SendAsync(msg);
|
||||
if (result.StatusCode != HttpStatusCode.OK)
|
||||
throw new HttpException(result);
|
||||
|
||||
var response = await result.Content.ReadFromJsonAsync<ChunkStatusResult>(_jsonOptions);
|
||||
return response;
|
||||
}
|
||||
public async Task UploadFile(UploadDefinition d)
|
||||
{
|
||||
_logger.LogInformation("Checking Access");
|
||||
await CheckAccess();
|
||||
|
||||
_logger.LogInformation("Checking chunk status");
|
||||
|
||||
var numberOfChunks = d.Chunks().Count();
|
||||
var chunkStatus = new ChunkStatusResult();
|
||||
foreach (var chunk in d.Chunks())
|
||||
{
|
||||
var status = await ChunkStatus(d, chunk);
|
||||
_logger.LogInformation("({Index}/{MaxChunks}) Chunk status: {Status}", chunk.Index, numberOfChunks, status);
|
||||
if (status == DTOs.ChunkStatus.NoContent)
|
||||
{
|
||||
_logger.LogInformation("({Index}/{MaxChunks}) Uploading", chunk.Index, numberOfChunks);
|
||||
chunkStatus = await UploadChunk(d, chunk);
|
||||
}
|
||||
}
|
||||
|
||||
await WaitForFileStatus(chunkStatus);
|
||||
|
||||
await AddFile(d, chunkStatus);
|
||||
|
||||
}
|
||||
|
||||
private async Task CheckAccess()
|
||||
{
|
||||
var msg = new HttpRequestMessage(HttpMethod.Get, "https://www.nexusmods.com/users/myaccount");
|
||||
msg.AddCookies((await ApiKey.Get())!.Cookies);
|
||||
using var response = await _client.SendAsync(msg);
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
|
||||
if (body.Contains("You are not allowed to access this area!"))
|
||||
throw new HttpException(403, "Nexus Cookies are incorrect");
|
||||
}
|
||||
|
||||
private async Task AddFile(UploadDefinition d, ChunkStatusResult status)
|
||||
{
|
||||
_logger.LogInformation("Saving file update {Name} to {Game}:{ModId}", d.Path.FileName, d.Game, d.ModId);
|
||||
|
||||
var msg = new HttpRequestMessage(HttpMethod.Post,
|
||||
"https://www.nexusmods.com/Core/Libs/Common/Managers/Mods?AddFile");
|
||||
msg.Headers.Referrer =
|
||||
new Uri(
|
||||
$"https://www.nexusmods.com/{d.Game.MetaData().NexusName}/mods/edit/?id={d.ModId}&game_id={d.GameId}&step=files");
|
||||
|
||||
msg.AddCookies((await ApiKey.Get())!.Cookies);
|
||||
var form = new MultipartFormDataContent();
|
||||
form.Add(new StringContent(d.GameId.ToString()), "game_id");
|
||||
form.Add(new StringContent(d.Name), "name");
|
||||
form.Add(new StringContent(d.Version), "file-version");
|
||||
form.Add(new StringContent((d.RemoveOldVersion ? 1 : 0).ToString()), "update-version");
|
||||
form.Add(new StringContent(((int)Enum.Parse<Category>(d.Category, true)).ToString()), "category");
|
||||
form.Add(new StringContent((d.NewExisting ? 1 : 0).ToString()), "new-existing");
|
||||
form.Add(new StringContent(d.OldFileId.ToString()), "old_file_id");
|
||||
form.Add(new StringContent((d.RemoveOldVersion ? 1 : 0).ToString()), "remove-old-version");
|
||||
form.Add(new StringContent(d.BriefOverview), "brief-overview");
|
||||
form.Add(new StringContent((d.SetAsMain ? 1 : 0).ToString()), "set_as_main_nmm");
|
||||
form.Add(new StringContent(status.UUID), "file_uuid");
|
||||
form.Add(new StringContent(d.FileSize.ToString()), "file_size");
|
||||
form.Add(new StringContent(d.ModId.ToString()), "mod_id");
|
||||
form.Add(new StringContent(d.ModId.ToString()), "id");
|
||||
form.Add(new StringContent("save"), "action");
|
||||
form.Add(new StringContent(status.Filename), "uploaded_file");
|
||||
form.Add(new StringContent(d.Path.FileName.ToString()), "original_file");
|
||||
msg.Content = form;
|
||||
|
||||
using var result = await _client.SendAsync(msg);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
throw new HttpException(result);
|
||||
}
|
||||
|
||||
private async Task<FileStatusResult> WaitForFileStatus(ChunkStatusResult chunkStatus)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
_logger.LogInformation("Checking file status of {Uuid}", chunkStatus.UUID);
|
||||
var data = await _client.GetFromJsonAsync<FileStatusResult>(
|
||||
$"https://upload.nexusmods.com/uploads/check_status?id={chunkStatus.UUID}");
|
||||
if (data.FileChunksAssembled)
|
||||
return data;
|
||||
await Task.Delay(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user