This commit is contained in:
parent
3f373e6ca3
commit
992ea50aba
215
VoidCat/Controllers/BaseDownloadController.cs
Normal file
215
VoidCat/Controllers/BaseDownloadController.cs
Normal file
@ -0,0 +1,215 @@
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using VoidCat.Database;
|
||||
using VoidCat.Model;
|
||||
using VoidCat.Services.Abstractions;
|
||||
using VoidCat.Services.Files;
|
||||
|
||||
namespace VoidCat.Controllers;
|
||||
|
||||
public abstract class BaseDownloadController : Controller
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly VoidSettings _settings;
|
||||
private readonly FileInfoManager _fileInfo;
|
||||
private readonly IPaymentOrderStore _paymentOrders;
|
||||
private readonly IPaymentFactory _paymentFactory;
|
||||
private readonly FileStoreFactory _storage;
|
||||
|
||||
protected BaseDownloadController(VoidSettings settings, FileInfoManager fileInfo, IPaymentOrderStore paymentOrders,
|
||||
IPaymentFactory paymentFactory, ILogger logger, FileStoreFactory storage)
|
||||
{
|
||||
_settings = settings;
|
||||
_fileInfo = fileInfo;
|
||||
_paymentOrders = paymentOrders;
|
||||
_paymentFactory = paymentFactory;
|
||||
_logger = logger;
|
||||
_storage = storage;
|
||||
}
|
||||
|
||||
protected async Task SendResponse(string id, VoidFileResponse voidFile)
|
||||
{
|
||||
var gid = voidFile.Id;
|
||||
|
||||
if (id.EndsWith(".torrent"))
|
||||
{
|
||||
var t = await voidFile.Metadata.MakeTorrent(voidFile.Id,
|
||||
await _storage.Open(new(gid, Enumerable.Empty<RangeRequest>()), CancellationToken.None),
|
||||
_settings.SiteUrl, _settings.TorrentTrackers);
|
||||
|
||||
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!");
|
||||
// downgrade to full send
|
||||
egressReq = egressReq with
|
||||
{
|
||||
Ranges = Enumerable.Empty<RangeRequest>()
|
||||
};
|
||||
}
|
||||
else if (egressReq.Ranges.Count() == 1)
|
||||
{
|
||||
Response.StatusCode = (int)HttpStatusCode.PartialContent;
|
||||
if (egressReq.Ranges.Sum(a => a.Size) == 0)
|
||||
{
|
||||
Response.StatusCode = (int)HttpStatusCode.RequestedRangeNotSatisfiable;
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Response.Headers.AcceptRanges = "bytes";
|
||||
}
|
||||
|
||||
foreach (var range in egressReq.Ranges)
|
||||
{
|
||||
Response.Headers.Add("content-range", range.ToContentRange());
|
||||
Response.ContentLength = range.Size;
|
||||
}
|
||||
|
||||
var preResult = await _storage.StartEgress(egressReq);
|
||||
if (preResult.Redirect != null)
|
||||
{
|
||||
Response.StatusCode = (int)HttpStatusCode.Redirect;
|
||||
Response.Headers.Location = preResult.Redirect.ToString();
|
||||
Response.ContentLength = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
var cts = HttpContext.RequestAborted;
|
||||
await Response.StartAsync(cts);
|
||||
await _storage.Egress(egressReq, Response.Body, cts);
|
||||
await Response.CompleteAsync();
|
||||
}
|
||||
|
||||
protected async Task<VoidFileResponse?> SetupDownload(Guid id)
|
||||
{
|
||||
var meta = await _fileInfo.Get(id, false);
|
||||
if (meta == null)
|
||||
{
|
||||
Response.StatusCode = 404;
|
||||
return default;
|
||||
}
|
||||
|
||||
return await CheckDownload(meta);
|
||||
}
|
||||
|
||||
private async Task<VoidFileResponse?> CheckDownload(VoidFileResponse meta)
|
||||
{
|
||||
var origin = Request.Headers.Referer.FirstOrDefault() ?? Request.Headers.Origin.FirstOrDefault();
|
||||
if (!string.IsNullOrEmpty(origin) && Uri.TryCreate(origin, UriKind.RelativeOrAbsolute, out var u))
|
||||
{
|
||||
if (_settings.BlockedOrigins.Any(a => string.Equals(a, u.DnsSafeHost, StringComparison.InvariantCultureIgnoreCase)))
|
||||
{
|
||||
Response.StatusCode = (int)HttpStatusCode.Forbidden;
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
// check payment order
|
||||
if (meta.Payment != default && meta.Payment.Service != PaywallService.None && meta.Payment.Required)
|
||||
{
|
||||
var h402 = Request.Headers.FirstOrDefault(a => a.Key.Equals("Authorization", StringComparison.InvariantCultureIgnoreCase))
|
||||
.Value.FirstOrDefault(a => a?.StartsWith("L402") ?? false);
|
||||
|
||||
var orderId = Request.Headers.GetHeader("V-OrderId") ?? h402 ?? Request.Query["orderId"];
|
||||
if (!await IsOrderPaid(orderId!))
|
||||
{
|
||||
Response.Headers.CacheControl = "no-cache";
|
||||
Response.StatusCode = (int)HttpStatusCode.PaymentRequired;
|
||||
if (meta.Payment.Service is PaywallService.Strike or PaywallService.LnProxy)
|
||||
{
|
||||
var accept = Request.Headers.GetHeader("accept");
|
||||
if (accept == "L402")
|
||||
{
|
||||
var provider = await _paymentFactory.CreateProvider(meta.Payment.Service);
|
||||
var order = await provider.CreateOrder(meta.Payment!);
|
||||
if (order != default)
|
||||
{
|
||||
Response.Headers.Add("access-control-expose-headers", "www-authenticate");
|
||||
Response.Headers.Add("www-authenticate",
|
||||
$"L402 macaroon=\"{Convert.ToBase64String(order.Id.ToByteArray())}\", invoice=\"{order!.OrderLightning!.Invoice}\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
if (meta.VirusScan?.IsVirus == true && !hasCorrectReferer)
|
||||
{
|
||||
Response.StatusCode = (int)HttpStatusCode.Redirect;
|
||||
Response.Headers.Location = $"/{meta.Id.ToBase58()}";
|
||||
return default;
|
||||
}
|
||||
|
||||
Response.Headers.XFrameOptions = "SAMEORIGIN";
|
||||
Response.Headers.ContentDisposition = $"inline; filename=\"{meta?.Metadata?.Name}\"";
|
||||
Response.ContentType = meta?.Metadata?.MimeType ?? "application/octet-stream";
|
||||
|
||||
return meta;
|
||||
}
|
||||
|
||||
private async ValueTask<bool> IsOrderPaid(string? orderId)
|
||||
{
|
||||
if (orderId?.StartsWith("L402") ?? false)
|
||||
{
|
||||
orderId = new Guid(Convert.FromBase64String(orderId.Substring(5).Split(":")[0])).ToString();
|
||||
}
|
||||
|
||||
if (Guid.TryParse(orderId, out var oid))
|
||||
{
|
||||
var order = await _paymentOrders.Get(oid);
|
||||
if (order?.Status == PaywallOrderStatus.Paid)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (order?.Status is PaywallOrderStatus.Unpaid)
|
||||
{
|
||||
// check status
|
||||
var svc = await _paymentFactory.CreateProvider(order.Service);
|
||||
var status = await svc.GetOrderStatus(order.Id);
|
||||
if (status != default && status.Status != order.Status)
|
||||
{
|
||||
await _paymentOrders.UpdateStatus(order.Id, status.Status);
|
||||
}
|
||||
|
||||
if (status?.Status == PaywallOrderStatus.Paid)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private IEnumerable<RangeRequest> GetRanges(HttpRequest request, long totalSize)
|
||||
{
|
||||
foreach (var rangeHeader in request.Headers.Range)
|
||||
{
|
||||
if (string.IsNullOrEmpty(rangeHeader))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var h in RangeRequest.Parse(rangeHeader, totalSize))
|
||||
{
|
||||
yield return h;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,5 @@
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using VoidCat.Database;
|
||||
using VoidCat.Model;
|
||||
using VoidCat.Services.Abstractions;
|
||||
using VoidCat.Services.Files;
|
||||
@ -9,24 +7,12 @@ using VoidCat.Services.Files;
|
||||
namespace VoidCat.Controllers;
|
||||
|
||||
[Route("d")]
|
||||
public class DownloadController : Controller
|
||||
public class DownloadController : BaseDownloadController
|
||||
{
|
||||
private readonly VoidSettings _settings;
|
||||
private readonly FileStoreFactory _storage;
|
||||
private readonly FileInfoManager _fileInfo;
|
||||
private readonly IPaymentOrderStore _paymentOrders;
|
||||
private readonly IPaymentFactory _paymentFactory;
|
||||
private readonly ILogger<DownloadController> _logger;
|
||||
|
||||
public DownloadController(FileStoreFactory storage, ILogger<DownloadController> logger, FileInfoManager fileInfo,
|
||||
IPaymentOrderStore paymentOrderStore, VoidSettings settings, IPaymentFactory paymentFactory)
|
||||
: base(settings, fileInfo, paymentOrderStore, paymentFactory, logger, storage)
|
||||
{
|
||||
_storage = storage;
|
||||
_logger = logger;
|
||||
_fileInfo = fileInfo;
|
||||
_paymentOrders = paymentOrderStore;
|
||||
_settings = settings;
|
||||
_paymentFactory = paymentFactory;
|
||||
}
|
||||
|
||||
[HttpOptions]
|
||||
@ -52,180 +38,6 @@ public class DownloadController : Controller
|
||||
var voidFile = await SetupDownload(gid);
|
||||
if (voidFile == default) return;
|
||||
|
||||
if (id.EndsWith(".torrent"))
|
||||
{
|
||||
var t = await voidFile.Metadata.MakeTorrent(voidFile.Id,
|
||||
await _storage.Open(new(gid, Enumerable.Empty<RangeRequest>()), CancellationToken.None),
|
||||
_settings.SiteUrl, _settings.TorrentTrackers);
|
||||
|
||||
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!");
|
||||
// downgrade to full send
|
||||
egressReq = egressReq with
|
||||
{
|
||||
Ranges = Enumerable.Empty<RangeRequest>()
|
||||
};
|
||||
}
|
||||
else if (egressReq.Ranges.Count() == 1)
|
||||
{
|
||||
Response.StatusCode = (int)HttpStatusCode.PartialContent;
|
||||
if (egressReq.Ranges.Sum(a => a.Size) == 0)
|
||||
{
|
||||
Response.StatusCode = (int)HttpStatusCode.RequestedRangeNotSatisfiable;
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Response.Headers.AcceptRanges = "bytes";
|
||||
}
|
||||
|
||||
foreach (var range in egressReq.Ranges)
|
||||
{
|
||||
Response.Headers.Add("content-range", range.ToContentRange());
|
||||
Response.ContentLength = range.Size;
|
||||
}
|
||||
|
||||
var preResult = await _storage.StartEgress(egressReq);
|
||||
if (preResult.Redirect != null)
|
||||
{
|
||||
Response.StatusCode = (int)HttpStatusCode.Redirect;
|
||||
Response.Headers.Location = preResult.Redirect.ToString();
|
||||
Response.ContentLength = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
var cts = HttpContext.RequestAborted;
|
||||
await Response.StartAsync(cts);
|
||||
await _storage.Egress(egressReq, Response.Body, cts);
|
||||
await Response.CompleteAsync();
|
||||
}
|
||||
|
||||
private async Task<VoidFileResponse?> SetupDownload(Guid id)
|
||||
{
|
||||
var origin = Request.Headers.Referer.FirstOrDefault() ?? Request.Headers.Origin.FirstOrDefault();
|
||||
if (!string.IsNullOrEmpty(origin) && Uri.TryCreate(origin, UriKind.RelativeOrAbsolute, out var u))
|
||||
{
|
||||
if (_settings.BlockedOrigins.Any(a => string.Equals(a, u.DnsSafeHost, StringComparison.InvariantCultureIgnoreCase)))
|
||||
{
|
||||
Response.StatusCode = (int)HttpStatusCode.Forbidden;
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
var meta = await _fileInfo.Get(id, false);
|
||||
if (meta == null)
|
||||
{
|
||||
Response.StatusCode = 404;
|
||||
return default;
|
||||
}
|
||||
|
||||
// check payment order
|
||||
if (meta.Payment != default && meta.Payment.Service != PaywallService.None && meta.Payment.Required)
|
||||
{
|
||||
var h402 = Request.Headers.FirstOrDefault(a => a.Key.Equals("Authorization", StringComparison.InvariantCultureIgnoreCase))
|
||||
.Value.FirstOrDefault(a => a?.StartsWith("L402") ?? false);
|
||||
|
||||
var orderId = Request.Headers.GetHeader("V-OrderId") ?? h402 ?? Request.Query["orderId"];
|
||||
if (!await IsOrderPaid(orderId!))
|
||||
{
|
||||
Response.Headers.CacheControl = "no-cache";
|
||||
Response.StatusCode = (int)HttpStatusCode.PaymentRequired;
|
||||
if (meta.Payment.Service is PaywallService.Strike or PaywallService.LnProxy)
|
||||
{
|
||||
var accept = Request.Headers.GetHeader("accept");
|
||||
if (accept == "L402")
|
||||
{
|
||||
var provider = await _paymentFactory.CreateProvider(meta.Payment.Service);
|
||||
var order = await provider.CreateOrder(meta.Payment!);
|
||||
if (order != default)
|
||||
{
|
||||
Response.Headers.Add("access-control-expose-headers", "www-authenticate");
|
||||
Response.Headers.Add("www-authenticate",
|
||||
$"L402 macaroon=\"{Convert.ToBase64String(order.Id.ToByteArray())}\", invoice=\"{order!.OrderLightning!.Invoice}\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
if (meta.VirusScan?.IsVirus == true && !hasCorrectReferer)
|
||||
{
|
||||
Response.StatusCode = (int)HttpStatusCode.Redirect;
|
||||
Response.Headers.Location = $"/{id.ToBase58()}";
|
||||
return default;
|
||||
}
|
||||
|
||||
Response.Headers.XFrameOptions = "SAMEORIGIN";
|
||||
Response.Headers.ContentDisposition = $"inline; filename=\"{meta?.Metadata?.Name}\"";
|
||||
Response.ContentType = meta?.Metadata?.MimeType ?? "application/octet-stream";
|
||||
|
||||
return meta;
|
||||
}
|
||||
|
||||
private async ValueTask<bool> IsOrderPaid(string? orderId)
|
||||
{
|
||||
if (orderId?.StartsWith("L402") ?? false)
|
||||
{
|
||||
orderId = new Guid(Convert.FromBase64String(orderId.Substring(5).Split(":")[0])).ToString();
|
||||
}
|
||||
|
||||
if (Guid.TryParse(orderId, out var oid))
|
||||
{
|
||||
var order = await _paymentOrders.Get(oid);
|
||||
if (order?.Status == PaywallOrderStatus.Paid)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (order?.Status is PaywallOrderStatus.Unpaid)
|
||||
{
|
||||
// check status
|
||||
var svc = await _paymentFactory.CreateProvider(order.Service);
|
||||
var status = await svc.GetOrderStatus(order.Id);
|
||||
if (status != default && status.Status != order.Status)
|
||||
{
|
||||
await _paymentOrders.UpdateStatus(order.Id, status.Status);
|
||||
}
|
||||
|
||||
if (status?.Status == PaywallOrderStatus.Paid)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private IEnumerable<RangeRequest> GetRanges(HttpRequest request, long totalSize)
|
||||
{
|
||||
foreach (var rangeHeader in request.Headers.Range)
|
||||
{
|
||||
if (string.IsNullOrEmpty(rangeHeader))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var h in RangeRequest.Parse(rangeHeader, totalSize))
|
||||
{
|
||||
yield return h;
|
||||
}
|
||||
}
|
||||
await SendResponse(id, voidFile);
|
||||
}
|
||||
}
|
||||
|
232
VoidCat/Controllers/NostrController.cs
Normal file
232
VoidCat/Controllers/NostrController.cs
Normal file
@ -0,0 +1,232 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
using VoidCat.Model;
|
||||
using VoidCat.Services;
|
||||
using VoidCat.Services.Abstractions;
|
||||
using VoidCat.Services.Files;
|
||||
using VoidCat.Services.Users;
|
||||
using File = VoidCat.Database.File;
|
||||
|
||||
namespace VoidCat.Controllers;
|
||||
|
||||
[Route("nostr")]
|
||||
public class NostrController : BaseDownloadController
|
||||
{
|
||||
private readonly VoidSettings _settings;
|
||||
private readonly UserManager _userManager;
|
||||
private readonly FileStoreFactory _storeFactory;
|
||||
private readonly IFileMetadataStore _fileMetadata;
|
||||
private readonly IUserUploadsStore _userUploads;
|
||||
private readonly FileInfoManager _fileInfo;
|
||||
|
||||
public NostrController(VoidSettings settings, UserManager userManager, FileStoreFactory storeFactory, IFileMetadataStore fileMetadata,
|
||||
IUserUploadsStore userUploads, FileInfoManager fileInfo, IPaymentOrderStore paymentOrderStore, IPaymentFactory paymentFactory,
|
||||
ILogger<NostrController> logger)
|
||||
: base(settings, fileInfo, paymentOrderStore, paymentFactory, logger, storeFactory)
|
||||
{
|
||||
_settings = settings;
|
||||
_userManager = userManager;
|
||||
_storeFactory = storeFactory;
|
||||
_fileMetadata = fileMetadata;
|
||||
_userUploads = userUploads;
|
||||
_fileInfo = fileInfo;
|
||||
}
|
||||
|
||||
[HttpGet("/.well-known/nostr/nip96.json")]
|
||||
public IActionResult GetInfo()
|
||||
{
|
||||
var info = new Nip96Info
|
||||
{
|
||||
ApiUri = new Uri(_settings.SiteUrl, "/nostr"),
|
||||
Plans = new()
|
||||
{
|
||||
{
|
||||
"free", new Nip96Plan
|
||||
{
|
||||
Name = "Default",
|
||||
MaxUploadSize = (long?)_settings.UploadSegmentSize
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return Json(info);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[DisableRequestSizeLimit]
|
||||
[DisableFormValueModelBinding]
|
||||
[Authorize(AuthenticationSchemes = NostrAuth.Scheme, Policy = Policies.RequireNostr)]
|
||||
public async Task<IActionResult> Upload()
|
||||
{
|
||||
var pubkey = HttpContext.GetPubKey();
|
||||
if (string.IsNullOrEmpty(pubkey))
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var nostrUser = await _userManager.LoginOrRegister(pubkey);
|
||||
var file = Request.Form.Files.First();
|
||||
|
||||
var meta = new File
|
||||
{
|
||||
MimeType = file.ContentType,
|
||||
Name = file.FileName,
|
||||
Description = Request.Form.TryGetValue("alt", out var sd) ? sd.First() : default,
|
||||
Size = (ulong)file.Length,
|
||||
Storage = nostrUser.Storage
|
||||
};
|
||||
|
||||
var vf = await _storeFactory.Ingress(new(file.OpenReadStream(), meta, 1, 1, true), HttpContext.RequestAborted);
|
||||
|
||||
// save metadata
|
||||
await _fileMetadata.Add(vf);
|
||||
await _userUploads.AddFile(nostrUser.Id, vf.Id);
|
||||
|
||||
var ret = new Nip96UploadResult
|
||||
{
|
||||
FileHeader = new()
|
||||
{
|
||||
Tags = new()
|
||||
{
|
||||
new() {"url", new Uri(_settings.SiteUrl, $"/nostr/{vf.OriginalDigest}{Path.GetExtension(vf.Name)}").ToString()},
|
||||
new() {"ox", vf.OriginalDigest ?? "", _settings.SiteUrl.ToString()},
|
||||
new() {"x", vf.Digest ?? ""},
|
||||
new() {"m", vf.MimeType}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return Json(ret);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Json(new Nip96UploadResult()
|
||||
{
|
||||
Status = "error",
|
||||
Message = ex.Message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task GetFile([FromRoute] string id)
|
||||
{
|
||||
var digest = Path.GetFileNameWithoutExtension(id);
|
||||
var file = await _fileMetadata.GetHash(digest);
|
||||
if (file == default)
|
||||
{
|
||||
Response.StatusCode = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
var meta = await SetupDownload(file.Id);
|
||||
if (meta == default) return;
|
||||
|
||||
await SendResponse(id, meta);
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[Authorize(AuthenticationSchemes = NostrAuth.Scheme, Policy = Policies.RequireNostr)]
|
||||
public async Task<IActionResult> DeleteFile([FromRoute] string id)
|
||||
{
|
||||
var digest = Path.GetFileNameWithoutExtension(id);
|
||||
var file = await _fileMetadata.GetHash(digest);
|
||||
if (file == default)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var pubkey = HttpContext.GetPubKey();
|
||||
if (string.IsNullOrEmpty(pubkey))
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var nostrUser = await _userManager.LoginOrRegister(pubkey);
|
||||
var uploader = await _userUploads.Uploader(file.Id);
|
||||
if (uploader == default || uploader != nostrUser.Id)
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
await _fileInfo.Delete(file.Id);
|
||||
|
||||
return Json(new Nip96UploadResult());
|
||||
}
|
||||
}
|
||||
|
||||
public class Nip96Info
|
||||
{
|
||||
[JsonProperty("api_url")]
|
||||
public Uri ApiUri { get; init; } = null!;
|
||||
|
||||
[JsonProperty("download_url")]
|
||||
public Uri? DownloadUrl { get; init; }
|
||||
|
||||
[JsonProperty("delegated_to_url")]
|
||||
public Uri? DelegatedTo { get; init; }
|
||||
|
||||
[JsonProperty("supported_nips")]
|
||||
public List<int>? SupportedNips { get; init; }
|
||||
|
||||
[JsonProperty("tos_url")]
|
||||
public Uri? Tos { get; init; }
|
||||
|
||||
[JsonProperty("content_types")]
|
||||
public List<string>? ContentTypes { get; init; }
|
||||
|
||||
[JsonProperty("plans")]
|
||||
public Dictionary<string, Nip96Plan>? Plans { get; init; }
|
||||
}
|
||||
|
||||
public class Nip96Plan
|
||||
{
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; init; } = null!;
|
||||
|
||||
[JsonProperty("is_nip98_required")]
|
||||
public bool Nip98Required { get; init; } = true;
|
||||
|
||||
[JsonProperty("url")]
|
||||
public Uri? LandingPage { get; init; }
|
||||
|
||||
[JsonProperty("max_byte_size")]
|
||||
public long? MaxUploadSize { get; init; }
|
||||
|
||||
[JsonProperty("file_expiration")]
|
||||
public int[] FileExpiration { get; init; } = {0, 0};
|
||||
|
||||
[JsonProperty("media_transformations")]
|
||||
public Nip96MediaTransformations? MediaTransformations { get; init; }
|
||||
}
|
||||
|
||||
public class Nip96MediaTransformations
|
||||
{
|
||||
[JsonProperty("image")]
|
||||
public List<string>? Image { get; init; }
|
||||
}
|
||||
|
||||
public class Nip96UploadResult
|
||||
{
|
||||
[JsonProperty("status")]
|
||||
public string Status { get; init; } = "success";
|
||||
|
||||
[JsonProperty("message")]
|
||||
public string? Message { get; init; }
|
||||
|
||||
[JsonProperty("processing_url")]
|
||||
public Uri? ProcessingUrl { get; init; }
|
||||
|
||||
[JsonProperty("nip94_event")]
|
||||
public Nip94Info FileHeader { get; init; } = null!;
|
||||
}
|
||||
|
||||
public class Nip94Info
|
||||
{
|
||||
[JsonProperty("tags")]
|
||||
public List<List<string>> Tags { get; init; } = new();
|
||||
}
|
@ -59,8 +59,7 @@ namespace VoidCat.Controllers
|
||||
[HttpPost]
|
||||
[DisableRequestSizeLimit]
|
||||
[DisableFormValueModelBinding]
|
||||
[Authorize(AuthenticationSchemes = "Bearer,Nostr")]
|
||||
[AllowAnonymous]
|
||||
[Authorize(AuthenticationSchemes = "Bearer,Nostr", Policy = Policies.RequireNostr)]
|
||||
public async Task<IActionResult> UploadFile([FromQuery] bool cli = false)
|
||||
{
|
||||
try
|
||||
|
@ -34,6 +34,13 @@ public class FileConfiguration : IEntityTypeConfiguration<File>
|
||||
builder.Property(a => a.EncryptionParams);
|
||||
builder.Property(a => a.MagnetLink);
|
||||
|
||||
builder.Property(a => a.OriginalDigest);
|
||||
|
||||
builder.Property(a => a.MediaDimensions);
|
||||
|
||||
builder.HasIndex(a => a.Uploaded);
|
||||
|
||||
builder.HasIndex(a => a.Digest);
|
||||
builder.HasIndex(a => a.OriginalDigest);
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,8 @@ public record File
|
||||
public string Storage { get; set; } = "local-disk";
|
||||
public string? EncryptionParams { get; set; }
|
||||
public string? MagnetLink { get; set; }
|
||||
public string? OriginalDigest { get; init; }
|
||||
public string? MediaDimensions { get; init; }
|
||||
|
||||
public Paywall? Paywall { get; init; }
|
||||
}
|
||||
|
491
VoidCat/Migrations/20231120132852_Nip96.Designer.cs
generated
Normal file
491
VoidCat/Migrations/20231120132852_Nip96.Designer.cs
generated
Normal file
@ -0,0 +1,491 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using VoidCat.Services;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace VoidCat.Migrations
|
||||
{
|
||||
[DbContext(typeof(VoidContext))]
|
||||
[Migration("20231120132852_Nip96")]
|
||||
partial class Nip96
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "7.0.5")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("VoidCat.Database.ApiKey", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("Expiry")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Token")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("ApiKey", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VoidCat.Database.EmailVerification", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("Code")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("Expires")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("EmailVerification", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VoidCat.Database.File", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Digest")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("EditSecret")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("EncryptionParams")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime?>("Expires")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("MagnetLink")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("MediaDimensions")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("text")
|
||||
.HasDefaultValue("application/octet-stream");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("OriginalDigest")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<decimal>("Size")
|
||||
.HasColumnType("numeric(20,0)");
|
||||
|
||||
b.Property<string>("Storage")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("text")
|
||||
.HasDefaultValue("local-disk");
|
||||
|
||||
b.Property<DateTime>("Uploaded")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Digest");
|
||||
|
||||
b.HasIndex("OriginalDigest");
|
||||
|
||||
b.HasIndex("Uploaded");
|
||||
|
||||
b.ToTable("Files", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VoidCat.Database.Paywall", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal>("Amount")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<byte>("Currency")
|
||||
.HasColumnType("smallint");
|
||||
|
||||
b.Property<Guid>("FileId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("Required")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<int>("Service")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Upstream")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FileId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Payment", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VoidCat.Database.PaywallOrder", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal>("Amount")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<byte>("Currency")
|
||||
.HasColumnType("smallint");
|
||||
|
||||
b.Property<Guid>("FileId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("Service")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<byte>("Status")
|
||||
.HasColumnType("smallint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FileId");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.ToTable("PaymentOrder", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VoidCat.Database.PaywallOrderLightning", b =>
|
||||
{
|
||||
b.Property<Guid>("OrderId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("Expire")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Invoice")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("OrderId");
|
||||
|
||||
b.ToTable("PaymentOrderLightning", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VoidCat.Database.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("AuthType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("text")
|
||||
.HasDefaultValue("void user");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Flags")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("LastLogin")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Storage")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("text")
|
||||
.HasDefaultValue("local-disk");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Email");
|
||||
|
||||
b.ToTable("Users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VoidCat.Database.UserAuthToken", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("AccessToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("Expires")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("IdToken")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Provider")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("RefreshToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Scope")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("TokenType")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("UsersAuthToken", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VoidCat.Database.UserFile", b =>
|
||||
{
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("FileId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("UserId", "FileId");
|
||||
|
||||
b.HasIndex("FileId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("UserFiles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VoidCat.Database.UserRole", b =>
|
||||
{
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Role")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserId", "Role");
|
||||
|
||||
b.ToTable("UserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VoidCat.Database.VirusScanResult", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("FileId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Names")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("ScanTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Scanner")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<decimal>("Score")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FileId");
|
||||
|
||||
b.ToTable("VirusScanResult", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VoidCat.Database.ApiKey", b =>
|
||||
{
|
||||
b.HasOne("VoidCat.Database.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VoidCat.Database.EmailVerification", b =>
|
||||
{
|
||||
b.HasOne("VoidCat.Database.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VoidCat.Database.Paywall", b =>
|
||||
{
|
||||
b.HasOne("VoidCat.Database.File", "File")
|
||||
.WithOne("Paywall")
|
||||
.HasForeignKey("VoidCat.Database.Paywall", "FileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("File");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VoidCat.Database.PaywallOrder", b =>
|
||||
{
|
||||
b.HasOne("VoidCat.Database.File", "File")
|
||||
.WithMany()
|
||||
.HasForeignKey("FileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("File");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VoidCat.Database.PaywallOrderLightning", b =>
|
||||
{
|
||||
b.HasOne("VoidCat.Database.PaywallOrder", "Order")
|
||||
.WithOne("OrderLightning")
|
||||
.HasForeignKey("VoidCat.Database.PaywallOrderLightning", "OrderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Order");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VoidCat.Database.UserAuthToken", b =>
|
||||
{
|
||||
b.HasOne("VoidCat.Database.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VoidCat.Database.UserFile", b =>
|
||||
{
|
||||
b.HasOne("VoidCat.Database.File", "File")
|
||||
.WithOne()
|
||||
.HasForeignKey("VoidCat.Database.UserFile", "FileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("VoidCat.Database.User", "User")
|
||||
.WithMany("UserFiles")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("File");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VoidCat.Database.UserRole", b =>
|
||||
{
|
||||
b.HasOne("VoidCat.Database.User", "User")
|
||||
.WithMany("Roles")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VoidCat.Database.VirusScanResult", b =>
|
||||
{
|
||||
b.HasOne("VoidCat.Database.File", "File")
|
||||
.WithMany()
|
||||
.HasForeignKey("FileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("File");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VoidCat.Database.File", b =>
|
||||
{
|
||||
b.Navigation("Paywall");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VoidCat.Database.PaywallOrder", b =>
|
||||
{
|
||||
b.Navigation("OrderLightning");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VoidCat.Database.User", b =>
|
||||
{
|
||||
b.Navigation("Roles");
|
||||
|
||||
b.Navigation("UserFiles");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
56
VoidCat/Migrations/20231120132852_Nip96.cs
Normal file
56
VoidCat/Migrations/20231120132852_Nip96.cs
Normal file
@ -0,0 +1,56 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace VoidCat.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Nip96 : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "MediaDimensions",
|
||||
table: "Files",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "OriginalDigest",
|
||||
table: "Files",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Files_Digest",
|
||||
table: "Files",
|
||||
column: "Digest");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Files_OriginalDigest",
|
||||
table: "Files",
|
||||
column: "OriginalDigest");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Files_Digest",
|
||||
table: "Files");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Files_OriginalDigest",
|
||||
table: "Files");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MediaDimensions",
|
||||
table: "Files");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "OriginalDigest",
|
||||
table: "Files");
|
||||
}
|
||||
}
|
||||
}
|
@ -94,6 +94,9 @@ namespace VoidCat.Migrations
|
||||
b.Property<string>("MagnetLink")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("MediaDimensions")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
@ -103,6 +106,9 @@ namespace VoidCat.Migrations
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("OriginalDigest")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<decimal>("Size")
|
||||
.HasColumnType("numeric(20,0)");
|
||||
|
||||
@ -117,6 +123,10 @@ namespace VoidCat.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Digest");
|
||||
|
||||
b.HasIndex("OriginalDigest");
|
||||
|
||||
b.HasIndex("Uploaded");
|
||||
|
||||
b.ToTable("Files", (string)null);
|
||||
|
@ -15,6 +15,13 @@ public interface IFileMetadataStore
|
||||
/// <returns></returns>
|
||||
ValueTask<File?> Get(Guid id);
|
||||
|
||||
/// <summary>
|
||||
/// Get metadata for a single file by its hash
|
||||
/// </summary>
|
||||
/// <param name="digest"></param>
|
||||
/// <returns></returns>
|
||||
ValueTask<File?> GetHash(string digest);
|
||||
|
||||
/// <summary>
|
||||
/// Get metadata for multiple files
|
||||
/// </summary>
|
||||
|
@ -27,6 +27,11 @@ public class LocalDiskFileMetadataStore : IFileMetadataStore
|
||||
return GetMeta<Database.File>(id);
|
||||
}
|
||||
|
||||
public ValueTask<Database.File?> GetHash(string digest)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<IReadOnlyList<Database.File>> Get(Guid[] ids)
|
||||
{
|
||||
|
@ -50,6 +50,8 @@ public class LocalDiskFileStore : StreamFileStore, IFileStore
|
||||
|
||||
if (payload.ShouldStripMetadata && payload.Segment == payload.TotalSegments)
|
||||
{
|
||||
fsTemp.Seek(0, SeekOrigin.Begin);
|
||||
var originalHash = await SHA256.Create().ComputeHashAsync(fsTemp, cts);
|
||||
fsTemp.Close();
|
||||
var ext = Path.GetExtension(vf.Name);
|
||||
var srcPath = $"{finalPath}_orig{ext}";
|
||||
@ -60,6 +62,7 @@ public class LocalDiskFileStore : StreamFileStore, IFileStore
|
||||
if (res.Success)
|
||||
{
|
||||
File.Move(res.OutPath, finalPath);
|
||||
|
||||
File.Delete(srcPath);
|
||||
|
||||
// recompute metadata
|
||||
@ -69,7 +72,8 @@ public class LocalDiskFileStore : StreamFileStore, IFileStore
|
||||
{
|
||||
Size = (ulong)fInfo.Length,
|
||||
Digest = hash.ToHex(),
|
||||
MimeType = res.MimeType ?? vf.MimeType
|
||||
MimeType = res.MimeType ?? vf.MimeType,
|
||||
OriginalDigest = originalHash.ToHex()
|
||||
};
|
||||
}
|
||||
else
|
||||
|
@ -1,10 +1,10 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using VoidCat.Model;
|
||||
using VoidCat.Services.Abstractions;
|
||||
using File = VoidCat.Database.File;
|
||||
|
||||
namespace VoidCat.Services.Files;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class PostgresFileMetadataStore : IFileMetadataStore
|
||||
{
|
||||
private readonly VoidContext _db;
|
||||
@ -18,8 +18,7 @@ public class PostgresFileMetadataStore : IFileMetadataStore
|
||||
|
||||
public string? Key => "postgres";
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<Database.File?> Get(Guid id)
|
||||
public async ValueTask<File?> Get(Guid id)
|
||||
{
|
||||
return await _db.Files
|
||||
.AsNoTracking()
|
||||
@ -27,13 +26,20 @@ public class PostgresFileMetadataStore : IFileMetadataStore
|
||||
.SingleOrDefaultAsync(a => a.Id == id);
|
||||
}
|
||||
|
||||
public async ValueTask Add(Database.File f)
|
||||
public async ValueTask<File?> GetHash(string digest)
|
||||
{
|
||||
return await _db.Files
|
||||
.AsNoTracking()
|
||||
.Include(a => a.Paywall)
|
||||
.SingleOrDefaultAsync(a => a.Digest == digest || a.OriginalDigest == digest);
|
||||
}
|
||||
|
||||
public async ValueTask Add(File f)
|
||||
{
|
||||
_db.Files.Add(f);
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask Delete(Guid id)
|
||||
{
|
||||
await _db.Files
|
||||
@ -41,8 +47,7 @@ public class PostgresFileMetadataStore : IFileMetadataStore
|
||||
.ExecuteDeleteAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<IReadOnlyList<Database.File>> Get(Guid[] ids)
|
||||
public async ValueTask<IReadOnlyList<File>> Get(Guid[] ids)
|
||||
{
|
||||
return await _db.Files
|
||||
.Include(a => a.Paywall)
|
||||
@ -50,8 +55,7 @@ public class PostgresFileMetadataStore : IFileMetadataStore
|
||||
.ToArrayAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask Update(Guid id, Database.File obj)
|
||||
public async ValueTask Update(Guid id, File obj)
|
||||
{
|
||||
var existing = await _db.Files.FindAsync(id);
|
||||
if (existing == default)
|
||||
@ -63,10 +67,9 @@ public class PostgresFileMetadataStore : IFileMetadataStore
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<PagedResult<Database.File>> ListFiles(PagedRequest request)
|
||||
public async ValueTask<PagedResult<File>> ListFiles(PagedRequest request)
|
||||
{
|
||||
IQueryable<Database.File> MakeQuery(VoidContext db)
|
||||
IQueryable<File> MakeQuery(VoidContext db)
|
||||
{
|
||||
var q = db.Files.AsNoTracking().AsQueryable();
|
||||
switch (request.SortBy, request.SortOrder)
|
||||
@ -100,7 +103,7 @@ public class PostgresFileMetadataStore : IFileMetadataStore
|
||||
return q.Skip(request.Page * request.PageSize).Take(request.PageSize);
|
||||
}
|
||||
|
||||
async IAsyncEnumerable<Database.File> Enumerate()
|
||||
async IAsyncEnumerable<File> Enumerate()
|
||||
{
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<VoidContext>();
|
||||
@ -121,7 +124,6 @@ public class PostgresFileMetadataStore : IFileMetadataStore
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<IFileMetadataStore.StoreStats> Stats()
|
||||
{
|
||||
var size = await _db.Files
|
||||
|
@ -2,6 +2,7 @@
|
||||
using Newtonsoft.Json;
|
||||
using VoidCat.Model;
|
||||
using VoidCat.Services.Abstractions;
|
||||
using File = VoidCat.Database.File;
|
||||
|
||||
namespace VoidCat.Services.Files;
|
||||
|
||||
@ -41,6 +42,11 @@ public class S3FileMetadataStore : IFileMetadataStore
|
||||
return default;
|
||||
}
|
||||
|
||||
public ValueTask<File?> GetHash(string digest)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<IReadOnlyList<Database.File>> Get(Guid[] ids)
|
||||
{
|
||||
|
@ -112,7 +112,7 @@ public static class VoidStartup
|
||||
|
||||
if (voidSettings.CorsOrigins.Count > 0)
|
||||
{
|
||||
p.WithOrigins(voidSettings.CorsOrigins.Select(a => a.OriginalString).ToArray())
|
||||
p.WithOrigins(voidSettings.CorsOrigins.Select(a => a.ToString()).ToArray())
|
||||
.AllowCredentials();
|
||||
}
|
||||
else
|
||||
|
@ -8,11 +8,8 @@
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Settings": {
|
||||
"SiteUrl": "https://localhost:7195",
|
||||
"SiteUrl": "http://localhost:7195",
|
||||
"DataDirectory": "./data",
|
||||
"CorsOrigins": [
|
||||
"http://localhost:3000",
|
||||
"http://localhost:8080"
|
||||
]
|
||||
"CorsOrigins": []
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user