api: add support for storage limits

This commit is contained in:
Abdullah Atta
2025-09-26 09:30:08 +05:00
committed by Abdullah Atta
parent b3dcdda697
commit 579e65b0be
6 changed files with 167 additions and 30 deletions
+47 -7
View File
@@ -28,9 +28,13 @@ using Amazon.Runtime;
using Amazon.S3;
using Amazon.S3.Model;
using Microsoft.Extensions.Options;
using Notesnook.API.Helpers;
using Notesnook.API.Interfaces;
using Notesnook.API.Models;
using Streetwriters.Common;
using Streetwriters.Common.Enums;
using Streetwriters.Common.Interfaces;
using Streetwriters.Common.Models;
namespace Notesnook.API.Services
{
@@ -45,6 +49,7 @@ namespace Notesnook.API.Services
private readonly string BUCKET_NAME = Constants.S3_BUCKET_NAME ?? "";
private readonly string INTERNAL_BUCKET_NAME = Constants.S3_INTERNAL_BUCKET_NAME ?? "";
private AmazonS3Client S3Client { get; }
private ISyncItemsRepositoryAccessor Repositories { get; }
// When running in a dockerized environment the sync server doesn't have access
// to the host's S3 Service URL. It can only talk to S3 server via its own internal
@@ -57,8 +62,9 @@ namespace Notesnook.API.Services
private AmazonS3Client S3InternalClient { get; }
private HttpClient httpClient = new HttpClient();
public S3Service()
public S3Service(ISyncItemsRepositoryAccessor syncItemsRepositoryAccessor)
{
Repositories = syncItemsRepositoryAccessor;
var config = new AmazonS3Config
{
#if (DEBUG || STAGING)
@@ -145,12 +151,6 @@ namespace Notesnook.API.Services
var request = new HttpRequestMessage(HttpMethod.Head, url);
var response = await httpClient.SendAsync(request);
const long MAX_SIZE = 513 * 1024 * 1024; // 512 MB
if (!Constants.IS_SELF_HOSTED && response.Content.Headers.ContentLength >= MAX_SIZE)
{
await this.DeleteObjectAsync(userId, name);
throw new Exception("File size exceeds the maximum allowed size.");
}
return response.Content.Headers.ContentLength ?? 0;
}
@@ -202,15 +202,55 @@ namespace Notesnook.API.Services
if (!IsSuccessStatusCode(((int)response.HttpStatusCode))) throw new Exception("Failed to abort multipart upload.");
}
private async Task<long> GetMultipartUploadSizeAsync(string userId, string key, string uploadId)
{
var objectName = GetFullObjectName(userId, key);
var parts = await GetS3Client(S3ClientMode.INTERNAL).ListPartsAsync(GetBucketName(S3ClientMode.INTERNAL), objectName, uploadId);
long totalSize = 0;
foreach (var part in parts.Parts)
{
totalSize += part.Size;
}
return totalSize;
}
public async Task CompleteMultipartUploadAsync(string userId, CompleteMultipartUploadRequest uploadRequest)
{
var objectName = GetFullObjectName(userId, uploadRequest.Key);
if (userId == null || objectName == null) throw new Exception("Could not abort multipart upload.");
var subscriptionService = await WampServers.SubscriptionServer.GetServiceAsync<IUserSubscriptionService>(SubscriptionServerTopics.UserSubscriptionServiceTopic);
var subscription = await subscriptionService.GetUserSubscriptionAsync(Clients.Notesnook.Id, userId);
long fileSize = await GetMultipartUploadSizeAsync(userId, uploadRequest.Key, uploadRequest.UploadId);
if (StorageHelper.IsFileSizeExceeded(subscription, fileSize))
{
await this.AbortMultipartUploadAsync(userId, uploadRequest.Key, uploadRequest.UploadId);
throw new Exception("Max file size exceeded.");
}
var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId == userId);
if (userSettings == null)
{
await this.AbortMultipartUploadAsync(userId, uploadRequest.Key, uploadRequest.UploadId);
throw new Exception("User settings not found.");
}
userSettings.StorageLimit ??= new Limit { Value = 0, UpdatedAt = 0 };
userSettings.StorageLimit.Value += fileSize;
if (StorageHelper.IsStorageLimitReached(subscription, userSettings.StorageLimit))
{
await this.AbortMultipartUploadAsync(userId, uploadRequest.Key, uploadRequest.UploadId);
throw new Exception("Storage limit reached.");
}
uploadRequest.Key = objectName;
uploadRequest.BucketName = GetBucketName(S3ClientMode.INTERNAL);
var response = await GetS3Client(S3ClientMode.INTERNAL).CompleteMultipartUploadAsync(uploadRequest);
if (!IsSuccessStatusCode(((int)response.HttpStatusCode))) throw new Exception("Failed to complete multipart upload.");
userSettings.StorageLimit.UpdatedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
await Repositories.UsersSettings.UpsertAsync(userSettings, (u) => u.UserId == userId);
}
private string? GetPresignedURL(string userId, string name, HttpVerb httpVerb, S3ClientMode mode = S3ClientMode.EXTERNAL)
+13
View File
@@ -23,6 +23,7 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Notesnook.API.Helpers;
using Notesnook.API.Interfaces;
using Notesnook.API.Models;
using Notesnook.API.Models.Responses;
@@ -71,6 +72,7 @@ namespace Notesnook.API.Services
await Repositories.UsersSettings.InsertAsync(new UserSettings
{
UserId = response.UserId,
StorageLimit = new Limit { UpdatedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), Value = 0 },
LastSynced = 0,
Salt = GetSalt()
});
@@ -119,6 +121,14 @@ namespace Notesnook.API.Services
}
var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId == user.UserId) ?? throw new Exception("User settings not found.");
// reset user's attachment limit every month
if (userSettings.StorageLimit == null || DateTimeOffset.UtcNow.Month > DateTimeOffset.FromUnixTimeMilliseconds(userSettings.StorageLimit.UpdatedAt).Month)
{
userSettings.StorageLimit ??= new Limit { UpdatedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), Value = 0 };
await Repositories.UsersSettings.UpsertAsync(userSettings, (u) => u.UserId == user.UserId);
}
return new UserResponse
{
UserId = user.UserId,
@@ -132,6 +142,8 @@ namespace Notesnook.API.Services
InboxKeys = userSettings.InboxKeys,
Salt = userSettings.Salt,
Subscription = subscription,
StorageUsed = userSettings.StorageLimit.Value,
TotalStorage = StorageHelper.GetStorageLimitForPlan(subscription),
Success = true,
StatusCode = 200
};
@@ -262,6 +274,7 @@ namespace Notesnook.API.Services
userSettings.AttachmentsKey = null;
userSettings.MonographPasswordsKey = null;
userSettings.VaultKey = null;
userSettings.InboxKeys = null;
userSettings.LastSynced = 0;
await Repositories.UsersSettings.UpsertAsync(userSettings, (s) => s.UserId == userId);