From 30d53944255c0d4c94dff47955671f63232653c8 Mon Sep 17 00:00:00 2001 From: dyw770 Date: Mon, 18 Aug 2025 16:33:49 +0800 Subject: [PATCH] api: fix s3 multipart upload (#22) * api: fix s3 multipart upload use external s3 url * api: fix s3 multipart upload use external s3 url * api: fix CompleteMultipartUploadRequest can not deserialize * api: start multipart upload use s3 internal url * Update Notesnook.API/Models/PartETagWrapper.cs remove default constructor Co-authored-by: Abdullah Atta * api: remove default constructor Co-authored-by: Abdullah Atta * api: merge method call Co-authored-by: Abdullah Atta * api: revocation due to conflict * api: revocation due to conflict --------- Co-authored-by: Abdullah Atta --- Notesnook.API/Controllers/S3Controller.cs | 255 +++++++++--------- .../CompleteMultipartUploadRequestWrapper.cs | 30 +++ Notesnook.API/Models/PartETagWrapper.cs | 7 + 3 files changed, 165 insertions(+), 127 deletions(-) create mode 100644 Notesnook.API/Models/CompleteMultipartUploadRequestWrapper.cs create mode 100644 Notesnook.API/Models/PartETagWrapper.cs diff --git a/Notesnook.API/Controllers/S3Controller.cs b/Notesnook.API/Controllers/S3Controller.cs index 9db9762..6ba6c50 100644 --- a/Notesnook.API/Controllers/S3Controller.cs +++ b/Notesnook.API/Controllers/S3Controller.cs @@ -1,127 +1,128 @@ -/* -This file is part of the Notesnook Sync Server project (https://notesnook.com/) - -Copyright (C) 2023 Streetwriters (Private) Limited - -This program is free software: you can redistribute it and/or modify -it under the terms of the Affero GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -Affero GNU General Public License for more details. - -You should have received a copy of the Affero GNU General Public License -along with this program. If not, see . -*/ - -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Amazon.S3.Model; -using System.Threading.Tasks; -using System.Security.Claims; -using Notesnook.API.Interfaces; -using System; - -namespace Notesnook.API.Controllers -{ - [ApiController] - [Route("s3")] - [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] - public class S3Controller : ControllerBase - { - private IS3Service S3Service { get; set; } - public S3Controller(IS3Service s3Service) - { - S3Service = s3Service; - } - - [HttpPut] - [Authorize("Pro")] - public IActionResult Upload([FromQuery] string name) - { - var userId = this.User.FindFirstValue("sub"); - var url = S3Service.GetUploadObjectUrl(userId, name); - if (url == null) return BadRequest("Could not create signed url."); - return Ok(url); - } - - - [HttpGet("multipart")] - [Authorize("Pro")] - public async Task MultipartUpload([FromQuery] string name, [FromQuery] int parts, [FromQuery] string uploadId) - { - var userId = this.User.FindFirstValue("sub"); - try - { - var meta = await S3Service.StartMultipartUploadAsync(userId, name, parts, uploadId); - return Ok(meta); - } - catch (Exception ex) { return BadRequest(ex.Message); } - } - - [HttpDelete("multipart")] - [Authorize("Pro")] - public async Task AbortMultipartUpload([FromQuery] string name, [FromQuery] string uploadId) - { - var userId = this.User.FindFirstValue("sub"); - try - { - await S3Service.AbortMultipartUploadAsync(userId, name, uploadId); - return Ok(); - } - catch (Exception ex) { return BadRequest(ex.Message); } - } - - [HttpPost("multipart")] - [Authorize("Pro")] - public async Task CompleteMultipartUpload([FromBody] CompleteMultipartUploadRequest uploadRequest) - { - var userId = this.User.FindFirstValue("sub"); - try - { - await S3Service.CompleteMultipartUploadAsync(userId, uploadRequest); - return Ok(); - } - catch (Exception ex) { return BadRequest(ex.Message); } - } - - [HttpGet] - [Authorize("Sync")] - public IActionResult Download([FromQuery] string name) - { - var userId = this.User.FindFirstValue("sub"); - var url = S3Service.GetDownloadObjectUrl(userId, name); - if (url == null) return BadRequest("Could not create signed url."); - return Ok(url); - } - - [HttpHead] - [Authorize("Sync")] - public async Task Info([FromQuery] string name) - { - var userId = this.User.FindFirstValue("sub"); - var size = await S3Service.GetObjectSizeAsync(userId, name); - HttpContext.Response.Headers.ContentLength = size; - return Ok(); - } - - [HttpDelete] - [Authorize("Sync")] - public async Task DeleteAsync([FromQuery] string name) - { - try - { - var userId = this.User.FindFirstValue("sub"); - await S3Service.DeleteObjectAsync(userId, name); - return Ok(); - } - catch (Exception ex) - { - return BadRequest(ex.Message); - } - } - } -} +/* +This file is part of the Notesnook Sync Server project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the Affero GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +Affero GNU General Public License for more details. + +You should have received a copy of the Affero GNU General Public License +along with this program. If not, see . +*/ + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Amazon.S3.Model; +using System.Threading.Tasks; +using System.Security.Claims; +using Notesnook.API.Interfaces; +using System; +using Notesnook.API.Models; + +namespace Notesnook.API.Controllers +{ + [ApiController] + [Route("s3")] + [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] + public class S3Controller : ControllerBase + { + private IS3Service S3Service { get; set; } + public S3Controller(IS3Service s3Service) + { + S3Service = s3Service; + } + + [HttpPut] + [Authorize("Pro")] + public IActionResult Upload([FromQuery] string name) + { + var userId = this.User.FindFirstValue("sub"); + var url = S3Service.GetUploadObjectUrl(userId, name); + if (url == null) return BadRequest("Could not create signed url."); + return Ok(url); + } + + + [HttpGet("multipart")] + [Authorize("Pro")] + public async Task MultipartUpload([FromQuery] string name, [FromQuery] int parts, [FromQuery] string uploadId) + { + var userId = this.User.FindFirstValue("sub"); + try + { + var meta = await S3Service.StartMultipartUploadAsync(userId, name, parts, uploadId); + return Ok(meta); + } + catch (Exception ex) { return BadRequest(ex.Message); } + } + + [HttpDelete("multipart")] + [Authorize("Pro")] + public async Task AbortMultipartUpload([FromQuery] string name, [FromQuery] string uploadId) + { + var userId = this.User.FindFirstValue("sub"); + try + { + await S3Service.AbortMultipartUploadAsync(userId, name, uploadId); + return Ok(); + } + catch (Exception ex) { return BadRequest(ex.Message); } + } + + [HttpPost("multipart")] + [Authorize("Pro")] + public async Task CompleteMultipartUpload([FromBody] CompleteMultipartUploadRequestWrapper uploadRequestWrapper) + { + var userId = this.User.FindFirstValue("sub"); + try + { + await S3Service.CompleteMultipartUploadAsync(userId, uploadRequestWrapper.ToRequest()); + return Ok(); + } + catch (Exception ex) { return BadRequest(ex.Message); } + } + + [HttpGet] + [Authorize("Sync")] + public IActionResult Download([FromQuery] string name) + { + var userId = this.User.FindFirstValue("sub"); + var url = S3Service.GetDownloadObjectUrl(userId, name); + if (url == null) return BadRequest("Could not create signed url."); + return Ok(url); + } + + [HttpHead] + [Authorize("Sync")] + public async Task Info([FromQuery] string name) + { + var userId = this.User.FindFirstValue("sub"); + var size = await S3Service.GetObjectSizeAsync(userId, name); + HttpContext.Response.Headers.ContentLength = size; + return Ok(); + } + + [HttpDelete] + [Authorize("Sync")] + public async Task DeleteAsync([FromQuery] string name) + { + try + { + var userId = this.User.FindFirstValue("sub"); + await S3Service.DeleteObjectAsync(userId, name); + return Ok(); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + } +} \ No newline at end of file diff --git a/Notesnook.API/Models/CompleteMultipartUploadRequestWrapper.cs b/Notesnook.API/Models/CompleteMultipartUploadRequestWrapper.cs new file mode 100644 index 0000000..2fc9fd3 --- /dev/null +++ b/Notesnook.API/Models/CompleteMultipartUploadRequestWrapper.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using Amazon.S3.Model; + +namespace Notesnook.API.Models; + +public class CompleteMultipartUploadRequestWrapper +{ + public string Key { get; set; } + public List PartETags { get; set; } + public string UploadId { get; set; } + + public CompleteMultipartUploadRequest ToRequest() + { + CompleteMultipartUploadRequest completeMultipartUploadRequest = new CompleteMultipartUploadRequest(); + completeMultipartUploadRequest.Key = Key; + completeMultipartUploadRequest.UploadId = UploadId; + completeMultipartUploadRequest.PartETags = []; + foreach (var partETagWrapper in PartETags) + { + var partETag = new PartETag + { + PartNumber = partETagWrapper.PartNumber, + ETag = partETagWrapper.ETag + }; + completeMultipartUploadRequest.PartETags.Add(partETag); + } + + return completeMultipartUploadRequest; + } +} \ No newline at end of file diff --git a/Notesnook.API/Models/PartETagWrapper.cs b/Notesnook.API/Models/PartETagWrapper.cs new file mode 100644 index 0000000..46b4c9f --- /dev/null +++ b/Notesnook.API/Models/PartETagWrapper.cs @@ -0,0 +1,7 @@ +namespace Notesnook.API.Models; + +public class PartETagWrapper +{ + public int PartNumber { get; set; } + public string ETag { get; set; } +} \ No newline at end of file