diff --git a/Notesnook.API/Controllers/S3Controller.cs b/Notesnook.API/Controllers/S3Controller.cs index c124375..b88d140 100644 --- a/Notesnook.API/Controllers/S3Controller.cs +++ b/Notesnook.API/Controllers/S3Controller.cs @@ -32,6 +32,8 @@ using Streetwriters.Common; using Streetwriters.Common.Interfaces; using Notesnook.API.Models; using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.Extensions.Logging; +using MongoDB.Driver; namespace Notesnook.API.Controllers { @@ -39,38 +41,38 @@ namespace Notesnook.API.Controllers [Route("s3")] [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] [Authorize("Sync")] - public class S3Controller : ControllerBase + public class S3Controller(IS3Service s3Service, ISyncItemsRepositoryAccessor repositories, ILogger logger) : ControllerBase { - private ISyncItemsRepositoryAccessor Repositories { get; } - private IS3Service S3Service { get; set; } - public S3Controller(IS3Service s3Service, ISyncItemsRepositoryAccessor syncItemsRepositoryAccessor) - { - S3Service = s3Service; - Repositories = syncItemsRepositoryAccessor; - } - [HttpPut] public async Task Upload([FromQuery] string name) { - var userId = this.User.GetUserId(); - - var fileSize = HttpContext.Request.ContentLength ?? 0; - bool hasBody = fileSize > 0; - - if (!hasBody) + try { - return Ok(Request.GetEncodedUrl() + "&access_token=" + Request.Headers.Authorization.ToString().Replace("Bearer ", "")); + var userId = this.User.GetUserId(); + + var fileSize = HttpContext.Request.ContentLength ?? 0; + bool hasBody = fileSize > 0; + + if (!hasBody) + { + return Ok(Request.GetEncodedUrl() + "&access_token=" + Request.Headers.Authorization.ToString().Replace("Bearer ", "")); + } + + if (Constants.IS_SELF_HOSTED) await UploadFileAsync(userId, name, fileSize); + else await UploadFileWithChecksAsync(userId, name, fileSize); + + return Ok(); + } + catch (Exception ex) + { + logger.LogError(ex, "Error uploading attachment for user."); + return BadRequest(new { error = "Failed to upload attachment." }); } - - if (Constants.IS_SELF_HOSTED) await UploadFileAsync(userId, name, fileSize); - else await UploadFileWithChecksAsync(userId, name, fileSize); - - return Ok(); } private async Task UploadFileWithChecksAsync(string userId, string name, long fileSize) { - var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId == userId); + var userSettings = await repositories.UsersSettings.FindOneAsync((u) => u.UserId == userId); var subscriptionService = await WampServers.SubscriptionServer.GetServiceAsync(SubscriptionServerTopics.UserSubscriptionServiceTopic); var subscription = await subscriptionService.GetUserSubscriptionAsync(Clients.Notesnook.Id, userId) ?? throw new Exception("User subscription not found."); @@ -78,27 +80,29 @@ namespace Notesnook.API.Controllers if (StorageHelper.IsFileSizeExceeded(subscription, fileSize)) throw new Exception("Max file size exceeded."); - userSettings.StorageLimit ??= new Limit { Value = 0, UpdatedAt = 0 }; + userSettings.StorageLimit = StorageHelper.RolloverStorageLimit(userSettings.StorageLimit); if (StorageHelper.IsStorageLimitReached(subscription, userSettings.StorageLimit.Value + fileSize)) throw new Exception("Storage limit exceeded."); var uploadedFileSize = await UploadFileAsync(userId, name, fileSize); userSettings.StorageLimit.Value += uploadedFileSize; - userSettings.StorageLimit.UpdatedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - await Repositories.UsersSettings.UpsertAsync(userSettings, (u) => u.UserId == userId); + await repositories.UsersSettings.Collection.UpdateOneAsync( + Builders.Filter.Eq(u => u.UserId, userId), + Builders.Update.Set(u => u.StorageLimit, userSettings.StorageLimit) + ); // extra check in case user sets wrong ContentLength in the HTTP header if (uploadedFileSize != fileSize && StorageHelper.IsStorageLimitReached(subscription, userSettings.StorageLimit.Value)) { - await S3Service.DeleteObjectAsync(userId, name); + await s3Service.DeleteObjectAsync(userId, name); throw new Exception("Storage limit exceeded."); } } private async Task UploadFileAsync(string userId, string name, long fileSize) { - var url = S3Service.GetInternalUploadObjectUrl(userId, name) ?? throw new Exception("Could not create signed url."); + var url = s3Service.GetInternalUploadObjectUrl(userId, name) ?? throw new Exception("Could not create signed url."); var httpClient = new HttpClient(); var content = new StreamContent(HttpContext.Request.BodyReader.AsStream()); @@ -106,7 +110,7 @@ namespace Notesnook.API.Controllers var response = await httpClient.SendRequestAsync(url, null, HttpMethod.Put, content); if (!response.Success) throw new Exception(response.Content != null ? await response.Content.ReadAsStringAsync() : "Could not upload file."); - return await S3Service.GetObjectSizeAsync(userId, name); + return await s3Service.GetObjectSizeAsync(userId, name); } @@ -116,10 +120,14 @@ namespace Notesnook.API.Controllers var userId = this.User.GetUserId(); try { - var meta = await S3Service.StartMultipartUploadAsync(userId, name, parts, uploadId); + var meta = await s3Service.StartMultipartUploadAsync(userId, name, parts, uploadId); return Ok(meta); } - catch (Exception ex) { return BadRequest(ex.Message); } + catch (Exception ex) + { + logger.LogError(ex, "Error starting multipart upload for user."); + return BadRequest(new { error = "Failed to start multipart upload." }); + } } [HttpDelete("multipart")] @@ -128,10 +136,14 @@ namespace Notesnook.API.Controllers var userId = this.User.GetUserId(); try { - await S3Service.AbortMultipartUploadAsync(userId, name, uploadId); + await s3Service.AbortMultipartUploadAsync(userId, name, uploadId); return Ok(); } - catch (Exception ex) { return BadRequest(ex.Message); } + catch (Exception ex) + { + logger.LogError(ex, "Error aborting multipart upload for user."); + return BadRequest(new { error = "Failed to abort multipart upload." }); + } } [HttpPost("multipart")] @@ -140,10 +152,14 @@ namespace Notesnook.API.Controllers var userId = this.User.GetUserId(); try { - await S3Service.CompleteMultipartUploadAsync(userId, uploadRequestWrapper.ToRequest()); + await s3Service.CompleteMultipartUploadAsync(userId, uploadRequestWrapper.ToRequest()); return Ok(); } - catch (Exception ex) { return BadRequest(ex.Message); } + catch (Exception ex) + { + logger.LogError(ex, "Error completing multipart upload for user."); + return BadRequest(new { error = "Failed to complete multipart upload." }); + } } [HttpGet] @@ -152,20 +168,32 @@ namespace Notesnook.API.Controllers try { var userId = this.User.GetUserId(); - var url = await S3Service.GetDownloadObjectUrl(userId, name); + var url = await s3Service.GetDownloadObjectUrl(userId, name); if (url == null) return BadRequest("Could not create signed url."); return Ok(url); } - catch (Exception ex) { return BadRequest(ex.Message); } + catch (Exception ex) + { + logger.LogError(ex, "Error generating download url for user."); + return BadRequest(new { error = "Failed to get attachment url." }); + } } [HttpHead] public async Task Info([FromQuery] string name) { - var userId = this.User.GetUserId(); - var size = await S3Service.GetObjectSizeAsync(userId, name); - HttpContext.Response.Headers.ContentLength = size; - return Ok(); + try + { + var userId = this.User.GetUserId(); + var size = await s3Service.GetObjectSizeAsync(userId, name); + HttpContext.Response.Headers.ContentLength = size; + return Ok(); + } + catch (Exception ex) + { + logger.LogError(ex, "Error getting object info for user."); + return BadRequest(new { error = "Failed to get attachment info." }); + } } [HttpDelete] @@ -174,12 +202,13 @@ namespace Notesnook.API.Controllers try { var userId = this.User.GetUserId(); - await S3Service.DeleteObjectAsync(userId, name); + await s3Service.DeleteObjectAsync(userId, name); return Ok(); } catch (Exception ex) { - return BadRequest(ex.Message); + logger.LogError(ex, "Error deleting object for user."); + return BadRequest(new { error = "Failed to delete attachment." }); } } }