Merge 81318957df into 858bd7c95c
This commit is contained in:
commit
936562cf60
15 changed files with 1103 additions and 40 deletions
|
|
@ -11,6 +11,7 @@ public class DataContext(DbContextOptions options) : IdentityDbContext(options)
|
||||||
public DbSet<Download> Downloads { get; set; }
|
public DbSet<Download> Downloads { get; set; }
|
||||||
public DbSet<Setting> Settings { get; set; }
|
public DbSet<Setting> Settings { get; set; }
|
||||||
public DbSet<Torrent> Torrents { get; set; }
|
public DbSet<Torrent> Torrents { get; set; }
|
||||||
|
public DbSet<TorrentPayload> TorrentPayloads { get; set; }
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder builder)
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
{
|
{
|
||||||
|
|
@ -24,6 +25,12 @@ public class DataContext(DbContextOptions options) : IdentityDbContext(options)
|
||||||
})
|
})
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
|
builder.Entity<Torrent>()
|
||||||
|
.HasOne(m => m.Payload)
|
||||||
|
.WithOne(m => m.Torrent)
|
||||||
|
.HasForeignKey<TorrentPayload>(m => m.TorrentId)
|
||||||
|
.IsRequired(false);
|
||||||
|
|
||||||
var cascadeFKs = builder.Model.GetEntityTypes()
|
var cascadeFKs = builder.Model.GetEntityTypes()
|
||||||
.SelectMany(t => t.GetForeignKeys())
|
.SelectMany(t => t.GetForeignKeys())
|
||||||
.Where(fk => !fk.IsOwnership && fk.DeleteBehavior == DeleteBehavior.Cascade);
|
.Where(fk => !fk.IsOwnership && fk.DeleteBehavior == DeleteBehavior.Cascade);
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ public interface ITorrentData
|
||||||
Task<IList<Torrent>> Get();
|
Task<IList<Torrent>> Get();
|
||||||
Task<Torrent?> GetById(Guid torrentId);
|
Task<Torrent?> GetById(Guid torrentId);
|
||||||
Task<Torrent?> GetByHash(String hash);
|
Task<Torrent?> GetByHash(String hash);
|
||||||
|
Task<String?> GetPayloadContent(Guid torrentId);
|
||||||
|
|
||||||
Task<Torrent> Add(String? rdId,
|
Task<Torrent> Add(String? rdId,
|
||||||
String hash,
|
String hash,
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,9 @@ public class TorrentData(DataContext dataContext, ILogger<TorrentData>? logger =
|
||||||
{
|
{
|
||||||
public async Task<IList<Torrent>> Get()
|
public async Task<IList<Torrent>> Get()
|
||||||
{
|
{
|
||||||
var torrents = await dataContext.Torrents
|
var torrents = await BuildTorrentQuery(includePayload: false)
|
||||||
.AsNoTracking()
|
.OrderBy(m => m.Priority ?? 9999)
|
||||||
.AsSplitQuery()
|
.ToListAsync();
|
||||||
.Include(m => m.Downloads)
|
|
||||||
.OrderBy(m => m.Priority ?? 9999)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
return torrents.OrderBy(m => m.Priority ?? 9999)
|
return torrents.OrderBy(m => m.Priority ?? 9999)
|
||||||
.ThenBy(m => m.Added)
|
.ThenBy(m => m.Added)
|
||||||
|
|
@ -23,10 +20,8 @@ public class TorrentData(DataContext dataContext, ILogger<TorrentData>? logger =
|
||||||
|
|
||||||
public async Task<Torrent?> GetById(Guid torrentId)
|
public async Task<Torrent?> GetById(Guid torrentId)
|
||||||
{
|
{
|
||||||
var dbTorrent = await dataContext.Torrents
|
var dbTorrent = await BuildTorrentQuery(includePayload: true)
|
||||||
.AsNoTracking()
|
.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
|
||||||
.Include(m => m.Downloads)
|
|
||||||
.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
|
|
||||||
|
|
||||||
if (dbTorrent == null)
|
if (dbTorrent == null)
|
||||||
{
|
{
|
||||||
|
|
@ -45,10 +40,8 @@ public class TorrentData(DataContext dataContext, ILogger<TorrentData>? logger =
|
||||||
{
|
{
|
||||||
hash = hash.ToLower();
|
hash = hash.ToLower();
|
||||||
|
|
||||||
var dbTorrent = await dataContext.Torrents
|
var dbTorrent = await BuildTorrentQuery(includePayload: false)
|
||||||
.AsNoTracking()
|
.FirstOrDefaultAsync(m => m.Hash == hash);
|
||||||
.Include(m => m.Downloads)
|
|
||||||
.FirstOrDefaultAsync(m => m.Hash == hash);
|
|
||||||
|
|
||||||
if (dbTorrent == null)
|
if (dbTorrent == null)
|
||||||
{
|
{
|
||||||
|
|
@ -63,6 +56,15 @@ public class TorrentData(DataContext dataContext, ILogger<TorrentData>? logger =
|
||||||
return dbTorrent;
|
return dbTorrent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<String?> GetPayloadContent(Guid torrentId)
|
||||||
|
{
|
||||||
|
return await dataContext.TorrentPayloads
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(m => m.TorrentId == torrentId)
|
||||||
|
.Select(m => m.Content)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<Torrent> Add(String? rdId,
|
public async Task<Torrent> Add(String? rdId,
|
||||||
String hash,
|
String hash,
|
||||||
String? fileOrMagnetContents,
|
String? fileOrMagnetContents,
|
||||||
|
|
@ -88,7 +90,6 @@ public class TorrentData(DataContext dataContext, ILogger<TorrentData>? logger =
|
||||||
DownloadManualFiles = torrent.DownloadManualFiles,
|
DownloadManualFiles = torrent.DownloadManualFiles,
|
||||||
DownloadClient = downloadClient,
|
DownloadClient = downloadClient,
|
||||||
Type = downloadType,
|
Type = downloadType,
|
||||||
FileOrMagnet = fileOrMagnetContents,
|
|
||||||
IsFile = isFile,
|
IsFile = isFile,
|
||||||
Priority = torrent.Priority,
|
Priority = torrent.Priority,
|
||||||
TorrentRetryAttempts = torrent.TorrentRetryAttempts,
|
TorrentRetryAttempts = torrent.TorrentRetryAttempts,
|
||||||
|
|
@ -96,7 +97,13 @@ public class TorrentData(DataContext dataContext, ILogger<TorrentData>? logger =
|
||||||
DeleteOnError = torrent.DeleteOnError,
|
DeleteOnError = torrent.DeleteOnError,
|
||||||
Lifetime = torrent.Lifetime,
|
Lifetime = torrent.Lifetime,
|
||||||
RdStatus = torrent.RdStatus,
|
RdStatus = torrent.RdStatus,
|
||||||
RdName = torrent.RdName
|
RdName = torrent.RdName,
|
||||||
|
Payload = String.IsNullOrWhiteSpace(fileOrMagnetContents)
|
||||||
|
? null
|
||||||
|
: new()
|
||||||
|
{
|
||||||
|
Content = fileOrMagnetContents
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
await dataContext.Torrents.AddAsync(newTorrent);
|
await dataContext.Torrents.AddAsync(newTorrent);
|
||||||
|
|
@ -294,6 +301,10 @@ public class TorrentData(DataContext dataContext, ILogger<TorrentData>? logger =
|
||||||
.Where(m => m.TorrentId == torrentId)
|
.Where(m => m.TorrentId == torrentId)
|
||||||
.ExecuteDeleteAsync();
|
.ExecuteDeleteAsync();
|
||||||
|
|
||||||
|
await dataContext.TorrentPayloads
|
||||||
|
.Where(m => m.TorrentId == torrentId)
|
||||||
|
.ExecuteDeleteAsync();
|
||||||
|
|
||||||
var deletedTorrents = await dataContext.Torrents
|
var deletedTorrents = await dataContext.Torrents
|
||||||
.Where(m => m.TorrentId == torrentId)
|
.Where(m => m.TorrentId == torrentId)
|
||||||
.ExecuteDeleteAsync();
|
.ExecuteDeleteAsync();
|
||||||
|
|
@ -307,4 +318,20 @@ public class TorrentData(DataContext dataContext, ILogger<TorrentData>? logger =
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IQueryable<Torrent> BuildTorrentQuery(Boolean includePayload)
|
||||||
|
{
|
||||||
|
var query = dataContext.Torrents
|
||||||
|
.AsNoTracking()
|
||||||
|
.AsSplitQuery()
|
||||||
|
.Include(m => m.Downloads)
|
||||||
|
.AsQueryable();
|
||||||
|
|
||||||
|
if (includePayload)
|
||||||
|
{
|
||||||
|
query = query.Include(m => m.Payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
508
server/RdtClient.Data/Migrations/20260528163000_MoveSourcePayloadToPayloadTable.Designer.cs
generated
Normal file
508
server/RdtClient.Data/Migrations/20260528163000_MoveSourcePayloadToPayloadTable.Designer.cs
generated
Normal file
|
|
@ -0,0 +1,508 @@
|
||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using RdtClient.Data.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace RdtClient.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(DataContext))]
|
||||||
|
[Migration("20260528163000_MoveSourcePayloadToPayloadTable")]
|
||||||
|
partial class MoveSourcePayloadToPayloadTable
|
||||||
|
{
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "10.0.5");
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("RoleNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasDatabaseName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UserNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("DownloadId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("Added")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("Completed")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("DownloadFinished")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("DownloadQueued")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("DownloadStarted")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Error")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("FileName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Link")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("RemoteId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("RetryCount")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<Guid>("TorrentId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("UnpackingFinished")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("UnpackingQueued")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("UnpackingStarted")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("DownloadId");
|
||||||
|
|
||||||
|
b.HasIndex("TorrentId", "Path")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Downloads");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("RdtClient.Data.Models.Data.Setting", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("SettingId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("SettingId");
|
||||||
|
|
||||||
|
b.ToTable("Settings");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("TorrentId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("Added")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Category")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("ClientKind")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("Completed")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("DeleteOnError")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("DownloadAction")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("DownloadClient")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("DownloadManualFiles")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("DownloadMinSize")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("DownloadRetryAttempts")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Error")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ExcludeRegex")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("FilesSelected")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("FinishedAction")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("FinishedActionDelay")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Hash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("HostDownloadAction")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("IncludeRegex")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsFile")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Lifetime")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("Priority")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("RdAdded")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("RdEnded")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("RdFiles")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("RdHost")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("RdId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("RdName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<long?>("RdProgress")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<long?>("RdSeeders")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<long?>("RdSize")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<long?>("RdSpeed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<long?>("RdSplit")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("RdStatus")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("RdStatusRaw")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("Retry")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("RetryCount")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("TorrentRetryAttempts")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("TorrentId");
|
||||||
|
|
||||||
|
b.ToTable("Torrents");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("RdtClient.Data.Models.Data.TorrentPayload", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("TorrentId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Content")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("TorrentId");
|
||||||
|
|
||||||
|
b.ToTable("TorrentPayloads");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("RdtClient.Data.Models.Data.Torrent", "Torrent")
|
||||||
|
.WithMany("Downloads")
|
||||||
|
.HasForeignKey("TorrentId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Torrent");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("RdtClient.Data.Models.Data.TorrentPayload", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("RdtClient.Data.Models.Data.Torrent", "Torrent")
|
||||||
|
.WithOne("Payload")
|
||||||
|
.HasForeignKey("RdtClient.Data.Models.Data.TorrentPayload", "TorrentId")
|
||||||
|
.OnDelete(DeleteBehavior.NoAction)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Torrent");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Downloads");
|
||||||
|
b.Navigation("Payload");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace RdtClient.Data.Migrations;
|
||||||
|
|
||||||
|
public partial class MoveSourcePayloadToPayloadTable : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "TorrentPayloads",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
TorrentId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||||
|
Content = table.Column<string>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_TorrentPayloads", x => x.TorrentId);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_TorrentPayloads_Torrents_TorrentId",
|
||||||
|
column: x => x.TorrentId,
|
||||||
|
principalTable: "Torrents",
|
||||||
|
principalColumn: "TorrentId",
|
||||||
|
onDelete: ReferentialAction.NoAction);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.Sql("""
|
||||||
|
INSERT INTO TorrentPayloads (TorrentId, Content)
|
||||||
|
SELECT TorrentId, FileOrMagnet
|
||||||
|
FROM Torrents
|
||||||
|
WHERE FileOrMagnet IS NOT NULL;
|
||||||
|
""");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "FileOrMagnet",
|
||||||
|
table: "Torrents");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "FileOrMagnet",
|
||||||
|
table: "Torrents",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.Sql("""
|
||||||
|
UPDATE Torrents
|
||||||
|
SET FileOrMagnet = (
|
||||||
|
SELECT Content
|
||||||
|
FROM TorrentPayloads
|
||||||
|
WHERE TorrentPayloads.TorrentId = Torrents.TorrentId
|
||||||
|
)
|
||||||
|
WHERE TorrentId IN (
|
||||||
|
SELECT TorrentId
|
||||||
|
FROM TorrentPayloads
|
||||||
|
);
|
||||||
|
""");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "TorrentPayloads");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -324,9 +324,6 @@ namespace RdtClient.Data.Migrations
|
||||||
b.Property<string>("ExcludeRegex")
|
b.Property<string>("ExcludeRegex")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("FileOrMagnet")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("FilesSelected")
|
b.Property<DateTimeOffset?>("FilesSelected")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
|
@ -411,6 +408,20 @@ namespace RdtClient.Data.Migrations
|
||||||
b.ToTable("Torrents");
|
b.ToTable("Torrents");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("RdtClient.Data.Models.Data.TorrentPayload", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("TorrentId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Content")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("TorrentId");
|
||||||
|
|
||||||
|
b.ToTable("TorrentPayloads");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||||
|
|
@ -473,9 +484,21 @@ namespace RdtClient.Data.Migrations
|
||||||
b.Navigation("Torrent");
|
b.Navigation("Torrent");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("RdtClient.Data.Models.Data.TorrentPayload", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("RdtClient.Data.Models.Data.Torrent", "Torrent")
|
||||||
|
.WithOne("Payload")
|
||||||
|
.HasForeignKey("RdtClient.Data.Models.Data.TorrentPayload", "TorrentId")
|
||||||
|
.OnDelete(DeleteBehavior.NoAction)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Torrent");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b =>
|
modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("Downloads");
|
b.Navigation("Downloads");
|
||||||
|
b.Navigation("Payload");
|
||||||
});
|
});
|
||||||
#pragma warning restore 612, 618
|
#pragma warning restore 612, 618
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,6 @@ public class Torrent
|
||||||
public DateTimeOffset? Retry { get; set; }
|
public DateTimeOffset? Retry { get; set; }
|
||||||
|
|
||||||
public DownloadType Type { get; set; }
|
public DownloadType Type { get; set; }
|
||||||
public String? FileOrMagnet { get; set; }
|
|
||||||
public Boolean IsFile { get; set; }
|
public Boolean IsFile { get; set; }
|
||||||
|
|
||||||
public Int32? Priority { get; set; }
|
public Int32? Priority { get; set; }
|
||||||
|
|
@ -50,6 +49,9 @@ public class Torrent
|
||||||
[InverseProperty("Torrent")]
|
[InverseProperty("Torrent")]
|
||||||
public IList<Download> Downloads { get; set; } = [];
|
public IList<Download> Downloads { get; set; } = [];
|
||||||
|
|
||||||
|
[InverseProperty(nameof(TorrentPayload.Torrent))]
|
||||||
|
public TorrentPayload? Payload { get; set; }
|
||||||
|
|
||||||
public Provider? ClientKind { get; set; }
|
public Provider? ClientKind { get; set; }
|
||||||
public String? RdId { get; set; }
|
public String? RdId { get; set; }
|
||||||
public String? RdName { get; set; }
|
public String? RdName { get; set; }
|
||||||
|
|
|
||||||
15
server/RdtClient.Data/Models/Data/TorrentPayload.cs
Normal file
15
server/RdtClient.Data/Models/Data/TorrentPayload.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace RdtClient.Data.Models.Data;
|
||||||
|
|
||||||
|
public class TorrentPayload
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
[ForeignKey(nameof(Torrent))]
|
||||||
|
public Guid TorrentId { get; set; }
|
||||||
|
|
||||||
|
public String Content { get; set; } = null!;
|
||||||
|
|
||||||
|
public Torrent Torrent { get; set; } = null!;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using RdtClient.Data.Data;
|
||||||
|
using RdtClient.Data.Enums;
|
||||||
|
using RdtClient.Data.Models.Data;
|
||||||
|
|
||||||
|
namespace RdtClient.Service.Test.Regression;
|
||||||
|
|
||||||
|
public class TorrentPayloadDataTests : IAsyncLifetime
|
||||||
|
{
|
||||||
|
private readonly String _databasePath = Path.Combine(Path.GetTempPath(), $"rdt-client-payload-data-{Guid.NewGuid():N}.sqlite");
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Add_ShouldCreatePayloadRow_AndDetailReadShouldIncludePayload()
|
||||||
|
{
|
||||||
|
await using var context = CreateContext();
|
||||||
|
await context.Database.EnsureCreatedAsync();
|
||||||
|
|
||||||
|
var torrentData = new TorrentData(context);
|
||||||
|
var template = new Torrent
|
||||||
|
{
|
||||||
|
DownloadClient = DownloadClient.Bezzad,
|
||||||
|
HostDownloadAction = TorrentHostDownloadAction.DownloadAll,
|
||||||
|
DownloadAction = TorrentDownloadAction.DownloadAll,
|
||||||
|
FinishedAction = TorrentFinishedAction.None
|
||||||
|
};
|
||||||
|
|
||||||
|
var added = await torrentData.Add(null,
|
||||||
|
"hash-1",
|
||||||
|
"magnet:?xt=urn:btih:hash-1",
|
||||||
|
false,
|
||||||
|
DownloadType.Torrent,
|
||||||
|
DownloadClient.Bezzad,
|
||||||
|
template);
|
||||||
|
|
||||||
|
await using var verifyContext = CreateContext();
|
||||||
|
var payloadRow = await verifyContext.TorrentPayloads.SingleAsync(m => m.TorrentId == added.TorrentId);
|
||||||
|
Assert.Equal("magnet:?xt=urn:btih:hash-1", payloadRow.Content);
|
||||||
|
|
||||||
|
var detailTorrent = await new TorrentData(verifyContext).GetById(added.TorrentId);
|
||||||
|
Assert.NotNull(detailTorrent);
|
||||||
|
Assert.NotNull(detailTorrent!.Payload);
|
||||||
|
Assert.Equal("magnet:?xt=urn:btih:hash-1", detailTorrent.Payload!.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Get_ShouldNotMaterializePayloadNavigation()
|
||||||
|
{
|
||||||
|
await using var context = CreateContext();
|
||||||
|
await context.Database.EnsureCreatedAsync();
|
||||||
|
|
||||||
|
context.Torrents.Add(new Torrent
|
||||||
|
{
|
||||||
|
TorrentId = Guid.NewGuid(),
|
||||||
|
Hash = "hash-2",
|
||||||
|
Added = DateTimeOffset.UtcNow,
|
||||||
|
Type = DownloadType.Nzb,
|
||||||
|
IsFile = true,
|
||||||
|
Payload = new()
|
||||||
|
{
|
||||||
|
Content = Convert.ToBase64String(new Byte[1024])
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
await using var readContext = CreateContext();
|
||||||
|
var results = await new TorrentData(readContext).Get();
|
||||||
|
|
||||||
|
Assert.Single(results);
|
||||||
|
Assert.Null(results[0].Payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Delete_ShouldRemovePayloadRow()
|
||||||
|
{
|
||||||
|
var torrentId = Guid.NewGuid();
|
||||||
|
|
||||||
|
await using (var context = CreateContext())
|
||||||
|
{
|
||||||
|
await context.Database.EnsureCreatedAsync();
|
||||||
|
context.Torrents.Add(new Torrent
|
||||||
|
{
|
||||||
|
TorrentId = torrentId,
|
||||||
|
Hash = "hash-3",
|
||||||
|
Added = DateTimeOffset.UtcNow,
|
||||||
|
Type = DownloadType.Torrent,
|
||||||
|
Payload = new()
|
||||||
|
{
|
||||||
|
TorrentId = torrentId,
|
||||||
|
Content = "magnet:?xt=urn:btih:hash-3"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
await using (var deleteContext = CreateContext())
|
||||||
|
{
|
||||||
|
await new TorrentData(deleteContext).Delete(torrentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var verifyContext = CreateContext();
|
||||||
|
Assert.False(await verifyContext.Torrents.AnyAsync(m => m.TorrentId == torrentId));
|
||||||
|
Assert.False(await verifyContext.TorrentPayloads.AnyAsync(m => m.TorrentId == torrentId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task InitializeAsync()
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DisposeAsync()
|
||||||
|
{
|
||||||
|
SqliteConnection.ClearAllPools();
|
||||||
|
|
||||||
|
if (File.Exists(_databasePath))
|
||||||
|
{
|
||||||
|
File.Delete(_databasePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DataContext CreateContext()
|
||||||
|
{
|
||||||
|
var connectionString = new SqliteConnectionStringBuilder
|
||||||
|
{
|
||||||
|
DataSource = _databasePath,
|
||||||
|
ForeignKeys = true
|
||||||
|
}.ToString();
|
||||||
|
|
||||||
|
var options = new DbContextOptionsBuilder<DataContext>()
|
||||||
|
.UseSqlite(connectionString)
|
||||||
|
.Options;
|
||||||
|
|
||||||
|
return new(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,177 @@
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations.Operations;
|
||||||
|
using RdtClient.Data.Data;
|
||||||
|
using RdtClient.Data.Enums;
|
||||||
|
using RdtClient.Data.Models.Data;
|
||||||
|
|
||||||
|
namespace RdtClient.Service.Test.Regression;
|
||||||
|
|
||||||
|
public class TorrentPayloadMigrationTests : IAsyncLifetime
|
||||||
|
{
|
||||||
|
private readonly String _databasePath = Path.Combine(Path.GetTempPath(), $"rdt-client-payload-migration-{Guid.NewGuid():N}.sqlite");
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Migrate_ShouldBackfillPayloadTable_AndDropLegacyColumn()
|
||||||
|
{
|
||||||
|
await using var migrationContext = CreateContext();
|
||||||
|
var migrations = migrationContext.Database.GetMigrations().ToList();
|
||||||
|
|
||||||
|
Assert.True(migrations.Count >= 2);
|
||||||
|
|
||||||
|
var previousMigration = migrations[^2];
|
||||||
|
var migrator = migrationContext.GetService<IMigrator>();
|
||||||
|
|
||||||
|
await migrator.MigrateAsync(previousMigration);
|
||||||
|
|
||||||
|
var torrentId = Guid.NewGuid();
|
||||||
|
const String payload = "magnet:?xt=urn:btih:legacy-hash";
|
||||||
|
|
||||||
|
await migrationContext.Database.ExecuteSqlRawAsync(
|
||||||
|
"""
|
||||||
|
INSERT INTO Torrents (
|
||||||
|
TorrentId,
|
||||||
|
Hash,
|
||||||
|
DownloadAction,
|
||||||
|
FinishedAction,
|
||||||
|
FinishedActionDelay,
|
||||||
|
HostDownloadAction,
|
||||||
|
DownloadMinSize,
|
||||||
|
DownloadClient,
|
||||||
|
Added,
|
||||||
|
Type,
|
||||||
|
FileOrMagnet,
|
||||||
|
IsFile,
|
||||||
|
RetryCount,
|
||||||
|
DownloadRetryAttempts,
|
||||||
|
TorrentRetryAttempts,
|
||||||
|
DeleteOnError,
|
||||||
|
Lifetime,
|
||||||
|
RdName
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
{0},
|
||||||
|
{1},
|
||||||
|
{2},
|
||||||
|
{3},
|
||||||
|
{4},
|
||||||
|
{5},
|
||||||
|
{6},
|
||||||
|
{7},
|
||||||
|
{8},
|
||||||
|
{9},
|
||||||
|
{10},
|
||||||
|
{11},
|
||||||
|
{12},
|
||||||
|
{13},
|
||||||
|
{14},
|
||||||
|
{15},
|
||||||
|
{16},
|
||||||
|
{17}
|
||||||
|
);
|
||||||
|
""",
|
||||||
|
torrentId,
|
||||||
|
"legacy-hash",
|
||||||
|
(Int32)TorrentDownloadAction.DownloadAll,
|
||||||
|
(Int32)TorrentFinishedAction.None,
|
||||||
|
0,
|
||||||
|
(Int32)TorrentHostDownloadAction.DownloadAll,
|
||||||
|
0,
|
||||||
|
(Int32)DownloadClient.Bezzad,
|
||||||
|
DateTimeOffset.UtcNow,
|
||||||
|
(Int32)DownloadType.Torrent,
|
||||||
|
payload,
|
||||||
|
false,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
"Legacy Torrent");
|
||||||
|
|
||||||
|
var migrationsAssembly = migrationContext.GetService<IMigrationsAssembly>();
|
||||||
|
var modelDiffer = migrationContext.GetService<IMigrationsModelDiffer>();
|
||||||
|
var designTimeModel = migrationContext.GetService<IDesignTimeModel>();
|
||||||
|
var modelRuntimeInitializer = migrationContext.GetService<IModelRuntimeInitializer>();
|
||||||
|
var snapshotModel = migrationsAssembly.ModelSnapshot?.Model;
|
||||||
|
|
||||||
|
Assert.NotNull(snapshotModel);
|
||||||
|
|
||||||
|
var initializedSnapshotModel = modelRuntimeInitializer.Initialize(snapshotModel!);
|
||||||
|
|
||||||
|
var pendingOperations = modelDiffer.GetDifferences(initializedSnapshotModel.GetRelationalModel(), designTimeModel.Model.GetRelationalModel())
|
||||||
|
.Select(m => m switch
|
||||||
|
{
|
||||||
|
DropForeignKeyOperation drop => $"{drop.GetType().Name}:{drop.Table}.{drop.Name}",
|
||||||
|
AddForeignKeyOperation add => $"{add.GetType().Name}:{add.Table}.{add.Name}:delete={add.OnDelete}",
|
||||||
|
_ => m.GetType().Name
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
Assert.True(pendingOperations.Count == 0, $"Pending operations: {String.Join(", ", pendingOperations)}");
|
||||||
|
|
||||||
|
await migrator.MigrateAsync();
|
||||||
|
|
||||||
|
await using var verificationContext = CreateContext();
|
||||||
|
var torrentData = new TorrentData(verificationContext);
|
||||||
|
var torrent = await torrentData.GetById(torrentId);
|
||||||
|
|
||||||
|
Assert.NotNull(torrent);
|
||||||
|
Assert.NotNull(torrent!.Payload);
|
||||||
|
Assert.Equal(payload, torrent.Payload!.Content);
|
||||||
|
|
||||||
|
await using var command = verificationContext.Database.GetDbConnection().CreateCommand();
|
||||||
|
command.CommandText = "PRAGMA table_info('Torrents');";
|
||||||
|
|
||||||
|
if (command.Connection!.State != System.Data.ConnectionState.Open)
|
||||||
|
{
|
||||||
|
await command.Connection.OpenAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
var columns = new List<String>();
|
||||||
|
|
||||||
|
await using var reader = await command.ExecuteReaderAsync();
|
||||||
|
|
||||||
|
while (await reader.ReadAsync())
|
||||||
|
{
|
||||||
|
columns.Add(reader.GetString(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.DoesNotContain("FileOrMagnet", columns);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task InitializeAsync()
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DisposeAsync()
|
||||||
|
{
|
||||||
|
SqliteConnection.ClearAllPools();
|
||||||
|
|
||||||
|
if (File.Exists(_databasePath))
|
||||||
|
{
|
||||||
|
File.Delete(_databasePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DataContext CreateContext()
|
||||||
|
{
|
||||||
|
var connectionString = new SqliteConnectionStringBuilder
|
||||||
|
{
|
||||||
|
DataSource = _databasePath,
|
||||||
|
ForeignKeys = true
|
||||||
|
}.ToString();
|
||||||
|
|
||||||
|
var options = new DbContextOptionsBuilder<DataContext>()
|
||||||
|
.UseSqlite(connectionString)
|
||||||
|
.Options;
|
||||||
|
|
||||||
|
return new(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,7 +26,10 @@ public class TorrentRunnerTest
|
||||||
TorrentId = Guid.NewGuid(),
|
TorrentId = Guid.NewGuid(),
|
||||||
Hash = "hash-1",
|
Hash = "hash-1",
|
||||||
RdName = "Torrent 1",
|
RdName = "Torrent 1",
|
||||||
FileOrMagnet = "magnet:?xt=urn:btih:hash-1",
|
Payload = new()
|
||||||
|
{
|
||||||
|
Content = "magnet:?xt=urn:btih:hash-1"
|
||||||
|
},
|
||||||
Type = DownloadType.Torrent,
|
Type = DownloadType.Torrent,
|
||||||
RdStatus = TorrentStatus.Queued,
|
RdStatus = TorrentStatus.Queued,
|
||||||
DeleteOnError = 10,
|
DeleteOnError = 10,
|
||||||
|
|
|
||||||
|
|
@ -439,4 +439,82 @@ public class TorrentsTest
|
||||||
It.IsAny<Torrent>()),
|
It.IsAny<Torrent>()),
|
||||||
Times.Once);
|
Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RetryTorrent_ShouldRequeueUsingStoredPayload()
|
||||||
|
{
|
||||||
|
var mocks = new Mocks();
|
||||||
|
var magnetLink = "magnet:?xt=urn:btih:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&dn=RetryTorrent";
|
||||||
|
var originalTorrent = new Torrent
|
||||||
|
{
|
||||||
|
TorrentId = Guid.NewGuid(),
|
||||||
|
Hash = "legacy-hash",
|
||||||
|
Type = DownloadType.Torrent,
|
||||||
|
IsFile = false,
|
||||||
|
Retry = DateTimeOffset.UtcNow,
|
||||||
|
Payload = new()
|
||||||
|
{
|
||||||
|
Content = magnetLink
|
||||||
|
},
|
||||||
|
Downloads = [],
|
||||||
|
DownloadClient = DownloadClient.Bezzad
|
||||||
|
};
|
||||||
|
|
||||||
|
var requeuedTorrent = new Torrent
|
||||||
|
{
|
||||||
|
TorrentId = Guid.NewGuid()
|
||||||
|
};
|
||||||
|
|
||||||
|
mocks.TorrentDataMock.Setup(t => t.GetById(originalTorrent.TorrentId))
|
||||||
|
.ReturnsAsync(originalTorrent);
|
||||||
|
mocks.TorrentDataMock.Setup(t => t.UpdateComplete(It.IsAny<Guid>(),
|
||||||
|
It.IsAny<String?>(),
|
||||||
|
It.IsAny<DateTimeOffset?>(),
|
||||||
|
It.IsAny<Boolean>()))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
mocks.TorrentDataMock.Setup(t => t.UpdateRetry(It.IsAny<Guid>(),
|
||||||
|
It.IsAny<DateTimeOffset?>(),
|
||||||
|
It.IsAny<Int32>()))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
mocks.TorrentDataMock.Setup(t => t.Delete(originalTorrent.TorrentId))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
mocks.TorrentDataMock.Setup(t => t.GetByHash(It.IsAny<String>()))
|
||||||
|
.ReturnsAsync((Torrent?)null);
|
||||||
|
mocks.TorrentDataMock.Setup(t => t.Add(null,
|
||||||
|
It.IsAny<String>(),
|
||||||
|
magnetLink,
|
||||||
|
false,
|
||||||
|
DownloadType.Torrent,
|
||||||
|
originalTorrent.DownloadClient,
|
||||||
|
It.IsAny<Torrent>()))
|
||||||
|
.ReturnsAsync(requeuedTorrent);
|
||||||
|
mocks.EnricherMock.Setup(e => e.EnrichMagnetLink(magnetLink))
|
||||||
|
.ReturnsAsync(magnetLink);
|
||||||
|
|
||||||
|
var torrents = new TorrentsService(mocks.TorrentsLoggerMock.Object,
|
||||||
|
mocks.TorrentDataMock.Object,
|
||||||
|
mocks.DownloadsMock.Object,
|
||||||
|
mocks.ProcessFactoryMock.Object,
|
||||||
|
new MockFileSystem(),
|
||||||
|
mocks.EnricherMock.Object,
|
||||||
|
null!,
|
||||||
|
null!,
|
||||||
|
null!,
|
||||||
|
null!,
|
||||||
|
null!,
|
||||||
|
new TestSettings(),
|
||||||
|
new TorrentRunnerState());
|
||||||
|
|
||||||
|
await torrents.RetryTorrent(originalTorrent.TorrentId, 3);
|
||||||
|
|
||||||
|
mocks.TorrentDataMock.Verify(t => t.Add(null,
|
||||||
|
It.IsAny<String>(),
|
||||||
|
magnetLink,
|
||||||
|
false,
|
||||||
|
DownloadType.Torrent,
|
||||||
|
originalTorrent.DownloadClient,
|
||||||
|
It.IsAny<Torrent>()),
|
||||||
|
Times.Once);
|
||||||
|
mocks.TorrentDataMock.Verify(t => t.UpdateRetry(requeuedTorrent.TorrentId, null, 3), Times.Once);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ public static class TorrentDtoMapper
|
||||||
FilesSelected = torrent.FilesSelected,
|
FilesSelected = torrent.FilesSelected,
|
||||||
Completed = torrent.Completed,
|
Completed = torrent.Completed,
|
||||||
Type = torrent.Type,
|
Type = torrent.Type,
|
||||||
FileOrMagnet = includeFileOrMagnet ? torrent.FileOrMagnet : null,
|
FileOrMagnet = includeFileOrMagnet ? torrent.Payload?.Content : null,
|
||||||
IsFile = torrent.IsFile,
|
IsFile = torrent.IsFile,
|
||||||
Priority = torrent.Priority,
|
Priority = torrent.Priority,
|
||||||
RetryCount = torrent.RetryCount,
|
RetryCount = torrent.RetryCount,
|
||||||
|
|
|
||||||
|
|
@ -367,10 +367,8 @@ public class TorrentRunner(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process torrents in DebridQueue
|
// Process torrents in DebridQueue
|
||||||
var torrentsToAddToProvider = allTorrents
|
var torrentsToAddToProvider = allTorrents.Where(m => m.Completed == null && m.Error == null && m.RdId == null && m.RdAdded == null && m.RdStatus == TorrentStatus.Queued)
|
||||||
.Where(m => m.Completed == null && m.Error == null && m.RdId == null && m.RdAdded == null && m.FileOrMagnet != null &&
|
.ToList();
|
||||||
m.RdStatus == TorrentStatus.Queued)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (torrentsToAddToProvider.Count != 0)
|
if (torrentsToAddToProvider.Count != 0)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -314,7 +314,7 @@ public class Torrents(
|
||||||
|
|
||||||
private async Task CopyAddedTorrent(Torrent torrent)
|
private async Task CopyAddedTorrent(Torrent torrent)
|
||||||
{
|
{
|
||||||
if (String.IsNullOrWhiteSpace(settings.Current.General.CopyAddedTorrents) || String.IsNullOrWhiteSpace(torrent.FileOrMagnet) || String.IsNullOrWhiteSpace(torrent.RdName))
|
if (String.IsNullOrWhiteSpace(settings.Current.General.CopyAddedTorrents) || String.IsNullOrWhiteSpace(torrent.RdName))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -345,14 +345,21 @@ public class Torrents(
|
||||||
fileSystem.File.Delete(copyFileName);
|
fileSystem.File.Delete(copyFileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var payloadContent = await GetPayloadContent(torrent);
|
||||||
|
|
||||||
|
if (String.IsNullOrWhiteSpace(payloadContent))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (torrent.IsFile)
|
if (torrent.IsFile)
|
||||||
{
|
{
|
||||||
var bytes = Convert.FromBase64String(torrent.FileOrMagnet);
|
var bytes = Convert.FromBase64String(payloadContent);
|
||||||
await fileSystem.File.WriteAllBytesAsync(copyFileName, bytes);
|
await fileSystem.File.WriteAllBytesAsync(copyFileName, bytes);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await fileSystem.File.WriteAllTextAsync(copyFileName, torrent.FileOrMagnet);
|
await fileSystem.File.WriteAllTextAsync(copyFileName, payloadContent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
@ -366,7 +373,7 @@ public class Torrents(
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="torrent">The torrent from the database to upload to the debrid provider</param>
|
/// <param name="torrent">The torrent from the database to upload to the debrid provider</param>
|
||||||
/// <returns>Updated torrent</returns>
|
/// <returns>Updated torrent</returns>
|
||||||
/// <exception cref="Exception">When RdId is not null or FileOrMagnet is null.</exception>
|
/// <exception cref="Exception">When RdId is not null or the stored source payload is missing.</exception>
|
||||||
public async Task DequeueFromDebridQueue(Torrent torrent)
|
public async Task DequeueFromDebridQueue(Torrent torrent)
|
||||||
{
|
{
|
||||||
if (torrent.RdId != null)
|
if (torrent.RdId != null)
|
||||||
|
|
@ -374,7 +381,9 @@ public class Torrents(
|
||||||
throw new("Torrent already added to debrid provider, cannot dequeue");
|
throw new("Torrent already added to debrid provider, cannot dequeue");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (torrent.FileOrMagnet == null)
|
var payloadContent = await GetPayloadContent(torrent);
|
||||||
|
|
||||||
|
if (payloadContent == null)
|
||||||
{
|
{
|
||||||
throw new("Torrent has no torrent file or magnet link");
|
throw new("Torrent has no torrent file or magnet link");
|
||||||
}
|
}
|
||||||
|
|
@ -390,14 +399,14 @@ public class Torrents(
|
||||||
if (torrent.Type == DownloadType.Nzb)
|
if (torrent.Type == DownloadType.Nzb)
|
||||||
{
|
{
|
||||||
id = torrent.IsFile
|
id = torrent.IsFile
|
||||||
? await DebridClient.AddNzbFile(Convert.FromBase64String(torrent.FileOrMagnet), torrent.RdName)
|
? await DebridClient.AddNzbFile(Convert.FromBase64String(payloadContent), torrent.RdName)
|
||||||
: await DebridClient.AddNzbLink(torrent.FileOrMagnet);
|
: await DebridClient.AddNzbLink(payloadContent);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
id = torrent.IsFile
|
id = torrent.IsFile
|
||||||
? await DebridClient.AddTorrentFile(Convert.FromBase64String(torrent.FileOrMagnet))
|
? await DebridClient.AddTorrentFile(Convert.FromBase64String(payloadContent))
|
||||||
: await DebridClient.AddTorrentMagnet(torrent.FileOrMagnet);
|
: await DebridClient.AddTorrentMagnet(payloadContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
await torrentData.UpdateRdId(torrent, id);
|
await torrentData.UpdateRdId(torrent, id);
|
||||||
|
|
@ -795,7 +804,9 @@ public class Torrents(
|
||||||
|
|
||||||
await Delete(torrentId, true, true, true);
|
await Delete(torrentId, true, true, true);
|
||||||
|
|
||||||
if (String.IsNullOrWhiteSpace(torrent.FileOrMagnet))
|
var payloadContent = await GetPayloadContent(torrent);
|
||||||
|
|
||||||
|
if (String.IsNullOrWhiteSpace(payloadContent))
|
||||||
{
|
{
|
||||||
throw new($"Cannot re-add this torrent, original magnet or file not found");
|
throw new($"Cannot re-add this torrent, original magnet or file not found");
|
||||||
}
|
}
|
||||||
|
|
@ -806,26 +817,26 @@ public class Torrents(
|
||||||
{
|
{
|
||||||
if (torrent.IsFile)
|
if (torrent.IsFile)
|
||||||
{
|
{
|
||||||
var bytes = Convert.FromBase64String(torrent.FileOrMagnet!);
|
var bytes = Convert.FromBase64String(payloadContent);
|
||||||
|
|
||||||
newTorrent = await AddNzbFileToDebridQueue(bytes, torrent.RdName, torrent);
|
newTorrent = await AddNzbFileToDebridQueue(bytes, torrent.RdName, torrent);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
newTorrent = await AddNzbLinkToDebridQueue(torrent.FileOrMagnet!, torrent);
|
newTorrent = await AddNzbLinkToDebridQueue(payloadContent, torrent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (torrent.IsFile)
|
if (torrent.IsFile)
|
||||||
{
|
{
|
||||||
var bytes = Convert.FromBase64String(torrent.FileOrMagnet!);
|
var bytes = Convert.FromBase64String(payloadContent);
|
||||||
|
|
||||||
newTorrent = await AddFileToDebridQueue(bytes, torrent);
|
newTorrent = await AddFileToDebridQueue(bytes, torrent);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
newTorrent = await AddMagnetToDebridQueue(torrent.FileOrMagnet!, torrent);
|
newTorrent = await AddMagnetToDebridQueue(payloadContent, torrent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -975,6 +986,16 @@ public class Torrents(
|
||||||
return settingDownloadPath;
|
return settingDownloadPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<String?> GetPayloadContent(Torrent torrent)
|
||||||
|
{
|
||||||
|
if (!String.IsNullOrWhiteSpace(torrent.Payload?.Content))
|
||||||
|
{
|
||||||
|
return torrent.Payload.Content;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await torrentData.GetPayloadContent(torrent.TorrentId);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<Torrent> AddQueued(String infoHash,
|
private async Task<Torrent> AddQueued(String infoHash,
|
||||||
String fileOrMagnetContents,
|
String fileOrMagnetContents,
|
||||||
Boolean isFile,
|
Boolean isFile,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue