diff --git a/VoidCat/Controllers/DownloadController.cs b/VoidCat/Controllers/DownloadController.cs index 088852e..1b75044 100644 --- a/VoidCat/Controllers/DownloadController.cs +++ b/VoidCat/Controllers/DownloadController.cs @@ -47,7 +47,19 @@ public class DownloadController : Controller var voidFile = await SetupDownload(gid); if (voidFile == default) return; - var egressReq = new EgressRequest(gid, GetRanges(Request, (long) voidFile!.Metadata!.Size)); + if (id.EndsWith(".torrent")) + { + var t = await voidFile.Metadata!.MakeTorrent( + await _storage.Open(new(gid, Enumerable.Empty()), 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) { _logger.LogWarning("Multi-range request not supported!"); @@ -59,10 +71,10 @@ public class DownloadController : Controller } else if (egressReq.Ranges.Count() == 1) { - Response.StatusCode = (int) HttpStatusCode.PartialContent; + Response.StatusCode = (int)HttpStatusCode.PartialContent; if (egressReq.Ranges.Sum(a => a.Size) == 0) { - Response.StatusCode = (int) HttpStatusCode.RequestedRangeNotSatisfiable; + Response.StatusCode = (int)HttpStatusCode.RequestedRangeNotSatisfiable; return; } } @@ -80,7 +92,7 @@ public class DownloadController : Controller var preResult = await _storage.StartEgress(egressReq); if (preResult.Redirect != null) { - Response.StatusCode = (int) HttpStatusCode.Redirect; + Response.StatusCode = (int)HttpStatusCode.Redirect; Response.Headers.Location = preResult.Redirect.ToString(); Response.ContentLength = 0; return; @@ -107,7 +119,7 @@ public class DownloadController : Controller var orderId = Request.Headers.GetHeader("V-OrderId") ?? Request.Query["orderId"]; if (!await IsOrderPaid(orderId)) { - Response.StatusCode = (int) HttpStatusCode.PaymentRequired; + Response.StatusCode = (int)HttpStatusCode.PaymentRequired; return default; } } @@ -115,10 +127,11 @@ public class DownloadController : Controller // prevent hot-linking viruses 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; + false; + if (meta.VirusScan?.IsVirus == true && !hasCorrectReferer) { - Response.StatusCode = (int) HttpStatusCode.Redirect; + Response.StatusCode = (int)HttpStatusCode.Redirect; Response.Headers.Location = $"/{id.ToBase58()}"; return default; } @@ -159,4 +172,4 @@ public class DownloadController : Controller } } } -} \ No newline at end of file +} diff --git a/VoidCat/Model/Extensions.cs b/VoidCat/Model/Extensions.cs index 1156924..b72e24b 100644 --- a/VoidCat/Model/Extensions.cs +++ b/VoidCat/Model/Extensions.cs @@ -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 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 BuildPieces() + { + fileStream.Seek(0, SeekOrigin.Begin); + var hashes = new List(); + 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; + } } \ No newline at end of file diff --git a/VoidCat/Model/VoidFileMeta.cs b/VoidCat/Model/VoidFileMeta.cs index 753cdfd..fc622a0 100644 --- a/VoidCat/Model/VoidFileMeta.cs +++ b/VoidCat/Model/VoidFileMeta.cs @@ -80,6 +80,11 @@ public record FileMeta : IFileMeta /// Encryption params as JSON string /// public string? EncryptionParams { get; set; } + + /// + /// Magnet link for downloads + /// + public string? MagnetLink { get; set; } } /// diff --git a/VoidCat/Services/Files/LocalDiskFileStorage.cs b/VoidCat/Services/Files/LocalDiskFileStorage.cs index 2748727..8e56ebc 100644 --- a/VoidCat/Services/Files/LocalDiskFileStorage.cs +++ b/VoidCat/Services/Files/LocalDiskFileStorage.cs @@ -49,21 +49,21 @@ public class LocalDiskFileStore : StreamFileStore, IFileStore payload.IsAppend ? FileMode.Append : FileMode.Create, FileAccess.ReadWrite); var vf = await IngressToStream(fsTemp, payload, cts); - + if (payload.ShouldStripMetadata && payload.Segment == payload.TotalSegments) { fsTemp.Close(); var ext = Path.GetExtension(vf.Metadata!.Name); var srcPath = $"{finalPath}_orig{ext}"; File.Move(finalPath, srcPath); - + var dstPath = $"{finalPath}_dst{ext}"; var res = await _stripMetadata.TryCompressMedia(srcPath, dstPath, cts); if (res.Success) { File.Move(res.OutPath, finalPath); File.Delete(srcPath); - + // recompute metadata var fInfo = new FileInfo(finalPath); var hash = await SHA256.Create().ComputeHashAsync(fInfo.OpenRead(), cts); @@ -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; } diff --git a/VoidCat/Services/Files/PostgresFileMetadataStore.cs b/VoidCat/Services/Files/PostgresFileMetadataStore.cs index 8c4d93b..9876e0c 100644 --- a/VoidCat/Services/Files/PostgresFileMetadataStore.cs +++ b/VoidCat/Services/Files/PostgresFileMetadataStore.cs @@ -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, }); } diff --git a/VoidCat/Services/Migrations/Database/07-MagnetLink.cs b/VoidCat/Services/Migrations/Database/07-MagnetLink.cs new file mode 100644 index 0000000..3370b4b --- /dev/null +++ b/VoidCat/Services/Migrations/Database/07-MagnetLink.cs @@ -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"); + } +} diff --git a/VoidCat/VoidCat.csproj b/VoidCat/VoidCat.csproj index 1d2cf5b..fe47306 100644 --- a/VoidCat/VoidCat.csproj +++ b/VoidCat/VoidCat.csproj @@ -15,6 +15,7 @@ + diff --git a/VoidCat/appsettings.compose.json b/VoidCat/appsettings.compose.json index f5852d0..cf6f8cf 100644 --- a/VoidCat/appsettings.compose.json +++ b/VoidCat/appsettings.compose.json @@ -9,7 +9,8 @@ }, "Settings": { "CorsOrigins": [ - "http://localhost:8001" + "http://localhost:8001", + "http://localhost:3000" ], "VirusScanner": { "ClamAV": { diff --git a/VoidCat/appsettings.json b/VoidCat/appsettings.json index ac8144e..fd8cefa 100644 --- a/VoidCat/appsettings.json +++ b/VoidCat/appsettings.json @@ -7,6 +7,7 @@ }, "AllowedHosts": "*", "Settings": { + "SiteUrl": "http://localhost:7195", "DataDirectory": "./data" } } diff --git a/VoidCat/spa/.env b/VoidCat/spa/.env index 3ca0a11..5e26f88 100644 --- a/VoidCat/spa/.env +++ b/VoidCat/spa/.env @@ -1,3 +1,2 @@ -BROWSER=none -HTTPS=true -API_HOST=https://localhost:7195 \ No newline at end of file +BROWSER=none +API_HOST=http://localhost:7195 \ No newline at end of file diff --git a/VoidCat/spa/package.json b/VoidCat/spa/package.json index 708e12c..afd5c7f 100644 --- a/VoidCat/spa/package.json +++ b/VoidCat/spa/package.json @@ -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", diff --git a/VoidCat/spa/src/Components/FileUpload/FileUpload.js b/VoidCat/spa/src/Components/FileUpload/FileUpload.js index f331423..39fb482 100644 --- a/VoidCat/spa/src/Components/FileUpload/FileUpload.js +++ b/VoidCat/spa/src/Components/FileUpload/FileUpload.js @@ -273,15 +273,6 @@ export function FileUpload(props) { return
; } - useEffect(() => { - let chromeVersion = getChromeVersion(); - if (chromeVersion >= 105) { - //doStreamUpload().catch(console.error); - } else { - doXHRUpload().catch(console.error); - } - }, []); - return (