Build torrents with web seed

This commit is contained in:
Kieran 2023-03-04 16:44:13 +00:00
parent 0fd723847f
commit 8bb7064963
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
12 changed files with 114 additions and 29 deletions

View File

@ -47,6 +47,18 @@ public class DownloadController : Controller
var voidFile = await SetupDownload(gid);
if (voidFile == default) return;
if (id.EndsWith(".torrent"))
{
var t = await voidFile.Metadata!.MakeTorrent(
await _storage.Open(new(gid, Enumerable.Empty<RangeRequest>()), CancellationToken.None),
_settings.SiteUrl);
Response.Headers.ContentDisposition = $"inline; filename=\"{id}\"";
Response.ContentType = "application/x-bittorent";
await t.EncodeToAsync(Response.Body);
return;
}
var egressReq = new EgressRequest(gid, GetRanges(Request, (long)voidFile!.Metadata!.Size));
if (egressReq.Ranges.Count() > 1)
{
@ -116,6 +128,7 @@ public class DownloadController : Controller
var referer = Request.Headers.Referer.Count > 0 ? new Uri(Request.Headers.Referer.First()) : null;
var hasCorrectReferer = referer?.Host.Equals(_settings.SiteUrl.Host, StringComparison.InvariantCultureIgnoreCase) ??
false;
if (meta.VirusScan?.IsVirus == true && !hasCorrectReferer)
{
Response.StatusCode = (int)HttpStatusCode.Redirect;

View File

@ -4,6 +4,8 @@ using System.Text;
using Amazon;
using Amazon.Runtime;
using Amazon.S3;
using BencodeNET.Objects;
using BencodeNET.Torrents;
using VoidCat.Model.Exceptions;
using VoidCat.Model.User;
@ -272,4 +274,48 @@ public static class Extensions
public static bool HasGoogle(this VoidSettings settings)
=> settings.Google != null;
public static async Task<Torrent> MakeTorrent(this FileMeta meta, Stream fileStream, Uri baseAddress)
{
const int pieceSize = 16_384;
var webSeed = new UriBuilder(baseAddress)
{
Path = $"/d/{meta.Id.ToBase58()}"
};
async Task<byte[]> BuildPieces()
{
fileStream.Seek(0, SeekOrigin.Begin);
var hashes = new List<byte[]>();
var chunk = new byte[pieceSize];
for (var x = 0; x < (int)Math.Ceiling(meta.Size / (decimal)pieceSize); x++)
{
var rLen = await fileStream.ReadAsync(chunk, 0, chunk.Length);
hashes.Add(SHA1.HashData(chunk.AsSpan(0, rLen)));
}
return hashes.SelectMany(a => a).ToArray();
}
// build magnet link
var t = new Torrent()
{
File = new()
{
FileName = meta.Name,
FileSize = (long)meta.Size
},
Comment = meta.Description,
CreationDate = meta.Uploaded.UtcDateTime,
IsPrivate = false,
PieceSize = pieceSize,
Pieces = await BuildPieces(),
ExtraFields = new BDictionary
{
{"url-list", webSeed.ToString()}
}
};
return t;
}
}

View File

@ -80,6 +80,11 @@ public record FileMeta : IFileMeta
/// Encryption params as JSON string
/// </summary>
public string? EncryptionParams { get; set; }
/// <summary>
/// Magnet link for downloads
/// </summary>
public string? MagnetLink { get; set; }
}
/// <summary>

View File

@ -84,6 +84,12 @@ public class LocalDiskFileStore : StreamFileStore, IFileStore
}
}
if (payload.Segment == payload.TotalSegments)
{
var t = await vf.Metadata!.MakeTorrent(new FileStream(finalPath, FileMode.Open), _settings.SiteUrl);
vf.Metadata!.MagnetLink = t.GetMagnetLink();
}
return vf;
}

View File

@ -35,8 +35,8 @@ public class PostgresFileMetadataStore : IFileMetadataStore
await using var conn = await _connection.Get();
await conn.ExecuteAsync(
@"insert into
""Files""(""Id"", ""Name"", ""Size"", ""Uploaded"", ""Description"", ""MimeType"", ""Digest"", ""EditSecret"", ""Expires"", ""Storage"", ""EncryptionParams"")
values(:id, :name, :size, :uploaded, :description, :mimeType, :digest, :editSecret, :expires, :store, :encryptionParams)
""Files""(""Id"", ""Name"", ""Size"", ""Uploaded"", ""Description"", ""MimeType"", ""Digest"", ""EditSecret"", ""Expires"", ""Storage"", ""EncryptionParams"", ""MagnetLink"")
values(:id, :name, :size, :uploaded, :description, :mimeType, :digest, :editSecret, :expires, :store, :encryptionParams, :magnetLink)
on conflict (""Id"") do update set
""Name"" = :name,
""Size"" = :size,
@ -44,7 +44,8 @@ on conflict (""Id"") do update set
""MimeType"" = :mimeType,
""Expires"" = :expires,
""Storage"" = :store,
""EncryptionParams"" = :encryptionParams",
""EncryptionParams"" = :encryptionParams,
""MagnetLink"" = :magnetLink",
new
{
id,
@ -57,7 +58,8 @@ on conflict (""Id"") do update set
editSecret = obj.EditSecret,
expires = obj.Expires?.ToUniversalTime(),
store = obj.Storage,
encryptionParams = obj.EncryptionParams
encryptionParams = obj.EncryptionParams,
magnetLink = obj.MagnetLink,
});
}

View File

@ -0,0 +1,20 @@
using FluentMigrator;
namespace VoidCat.Services.Migrations.Database;
[Migration(20230304_1509)]
public class MagnetLink : Migration {
public override void Up()
{
Create.Column("MagnetLink")
.OnTable("Files")
.AsString()
.Nullable();
}
public override void Down()
{
Delete.Column("MagnetLink")
.FromTable("Files");
}
}

View File

@ -15,6 +15,7 @@
<ItemGroup>
<PackageReference Include="AWSSDK.S3" Version="3.7.9.30" />
<PackageReference Include="BencodeNET" Version="4.0.0" />
<PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="FFMpegCore" Version="4.8.0" />
<PackageReference Include="FluentMigrator" Version="3.3.2" />

View File

@ -9,7 +9,8 @@
},
"Settings": {
"CorsOrigins": [
"http://localhost:8001"
"http://localhost:8001",
"http://localhost:3000"
],
"VirusScanner": {
"ClamAV": {

View File

@ -7,6 +7,7 @@
},
"AllowedHosts": "*",
"Settings": {
"SiteUrl": "http://localhost:7195",
"DataDirectory": "./data"
}
}

View File

@ -1,3 +1,2 @@
BROWSER=none
HTTPS=true
API_HOST=https://localhost:7195
API_HOST=http://localhost:7195

View File

@ -2,7 +2,7 @@
"name": "spa",
"version": "0.1.0",
"private": true,
"proxy": "https://localhost:7195",
"proxy": "http://localhost:7195",
"dependencies": {
"@hcaptcha/react-hcaptcha": "^1.1.1",
"@reduxjs/toolkit": "^1.7.2",

View File

@ -273,15 +273,6 @@ export function FileUpload(props) {
return <div dangerouslySetInnerHTML={{__html: elm.outerHTML}}/>;
}
useEffect(() => {
let chromeVersion = getChromeVersion();
if (chromeVersion >= 105) {
//doStreamUpload().catch(console.error);
} else {
doXHRUpload().catch(console.error);
}
}, []);
return (
<div className="upload">
<div className="info">