This commit is contained in:
452
server/AyaNova/Controllers/AttachmentController.cs
Normal file
452
server/AyaNova/Controllers/AttachmentController.cs
Normal file
@@ -0,0 +1,452 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AyaNova.Models;
|
||||
using AyaNova.Api.ControllerHelpers;
|
||||
using AyaNova.Util;
|
||||
using AyaNova.Biz;
|
||||
|
||||
|
||||
namespace AyaNova.Api.Controllers
|
||||
{
|
||||
|
||||
//FROM DOCS HERE:
|
||||
//https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads#uploading-large-files-with-streaming
|
||||
//https://github.com/aspnet/Docs/tree/74a44669d5e7039e2d4d2cb3f8b0c4ed742d1124/aspnetcore/mvc/models/file-uploads/sample/FileUploadSample
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Attachment controller
|
||||
/// </summary>
|
||||
[ApiVersion("8.0")]
|
||||
[Route("api/v{version:apiVersion}/[controller]")]
|
||||
[Produces("application/json")]
|
||||
[Authorize]
|
||||
public class AttachmentController : Controller
|
||||
{
|
||||
private readonly AyContext ct;
|
||||
private readonly ILogger<AttachmentController> log;
|
||||
private readonly ApiServerState serverState;
|
||||
//private static readonly FormOptions _defaultFormOptions = new FormOptions();
|
||||
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="dbcontext"></param>
|
||||
/// <param name="logger"></param>
|
||||
/// <param name="apiServerState"></param>
|
||||
public AttachmentController(AyContext dbcontext, ILogger<AttachmentController> logger, ApiServerState apiServerState)
|
||||
{
|
||||
ct = dbcontext;
|
||||
log = logger;
|
||||
serverState = apiServerState;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//TODO: Centralize this code somewhere else, it's going to be needed for backup as well
|
||||
//consider the 1 hour is this legit depending on client
|
||||
|
||||
/// <summary>
|
||||
/// Get download token
|
||||
/// A download token is good for 1 hour from issue
|
||||
/// </summary>
|
||||
/// <returns>Current download token for user</returns>
|
||||
[HttpGet("DownloadToken")]
|
||||
public async Task<IActionResult> GetDownloadToken()
|
||||
{
|
||||
if (!serverState.IsOpen)
|
||||
{
|
||||
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
|
||||
}
|
||||
|
||||
long lUserId = UserIdFromContext.Id(HttpContext.Items);
|
||||
var u = await ct.User.FirstOrDefaultAsync(a => a.Id == lUserId);
|
||||
if (u == null)
|
||||
return NotFound();
|
||||
else
|
||||
{
|
||||
|
||||
//Generate a download token and store it with the user account
|
||||
//users who are authenticated can get their token via download route
|
||||
Guid g = Guid.NewGuid();
|
||||
string dlkey = Convert.ToBase64String(g.ToByteArray());
|
||||
dlkey = dlkey.Replace("=", "");
|
||||
dlkey = dlkey.Replace("+", "");
|
||||
|
||||
//get expiry date for download token
|
||||
var exp = new DateTimeOffset(DateTime.Now.AddHours(1).ToUniversalTime(), TimeSpan.Zero);
|
||||
|
||||
u.DlKey = dlkey;
|
||||
u.DlKeyExpire = exp.DateTime;
|
||||
ct.User.Update(u);
|
||||
try
|
||||
{
|
||||
await ct.SaveChangesAsync();//triggering concurrency exception here
|
||||
}
|
||||
catch (Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException)
|
||||
{
|
||||
log.LogInformation("Auth retry dlkey");
|
||||
};
|
||||
|
||||
return Ok(new ApiOkResponse(new { dlkey = u.DlKey, expires = u.DlKeyExpire }));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Upload attachment file
|
||||
///
|
||||
/// Required roles: Same roles as object that file is being attached to
|
||||
///
|
||||
/// </summary>
|
||||
/// <returns>NameValue list of filenames and attachment id's</returns>
|
||||
[HttpPost]
|
||||
[DisableFormValueModelBinding]
|
||||
[RequestSizeLimit(10737418241)]//10737418240 = 10gb https://github.com/aspnet/Announcements/issues/267
|
||||
public async Task<IActionResult> Upload()
|
||||
{
|
||||
//Adapted from the example found here: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads#uploading-large-files-with-streaming
|
||||
|
||||
|
||||
|
||||
if (!serverState.IsOpen)
|
||||
{
|
||||
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
|
||||
}
|
||||
|
||||
var returnList = new List<NameIdItem>();
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
|
||||
{
|
||||
return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, "FileUploadAttempt", $"Expected a multipart request, but got {Request.ContentType}"));
|
||||
}
|
||||
|
||||
var uploadFormData = await ApiUploadProcessor.ProcessAttachmentUpload(HttpContext);
|
||||
|
||||
bool badRequest = false;
|
||||
string AttachToObjectType = string.Empty;
|
||||
string AttachToObjectId = string.Empty;
|
||||
string errorMessage = string.Empty;
|
||||
|
||||
if (!uploadFormData.FormFieldData.ContainsKey("AttachToObjectType") || !uploadFormData.FormFieldData.ContainsKey("AttachToObjectId"))
|
||||
{
|
||||
badRequest = true;
|
||||
errorMessage = "AttachToObjectType and / or AttachToObjectId are missing and are required";
|
||||
}
|
||||
if (!badRequest)
|
||||
{
|
||||
AttachToObjectType = uploadFormData.FormFieldData["AttachToObjectType"].ToString();
|
||||
AttachToObjectId = uploadFormData.FormFieldData["AttachToObjectId"].ToString();
|
||||
if (string.IsNullOrWhiteSpace(AttachToObjectType) || string.IsNullOrWhiteSpace(AttachToObjectId))
|
||||
{
|
||||
badRequest = true;
|
||||
errorMessage = "AttachToObjectType and / or AttachToObjectId are empty and are required";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//Get type and id object from post paramters
|
||||
AyaTypeId attachToObject = null;
|
||||
if (!badRequest)
|
||||
{
|
||||
attachToObject = new AyaTypeId(AttachToObjectType, AttachToObjectId);
|
||||
if (attachToObject.IsEmpty)
|
||||
{
|
||||
badRequest = true;
|
||||
errorMessage = "AttachToObjectType and / or AttachToObjectId are not valid and are required";
|
||||
}
|
||||
}
|
||||
|
||||
//Is it an attachable type of object?
|
||||
if (!badRequest)
|
||||
{
|
||||
if (!attachToObject.IsAttachable)
|
||||
{
|
||||
badRequest = true;
|
||||
errorMessage = attachToObject.ObjectType.ToString() + " - AttachToObjectType does not support attachments";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//does attach to object exist?
|
||||
if (!badRequest)
|
||||
{
|
||||
//check if object exists
|
||||
long attachToObjectOwnerId = attachToObject.OwnerId(ct);
|
||||
if (attachToObjectOwnerId == -1)
|
||||
{
|
||||
badRequest = true;
|
||||
errorMessage = "Invalid attach object";
|
||||
}
|
||||
else
|
||||
{
|
||||
// User needs modify rights to the object type in question
|
||||
if (!Authorized.IsAuthorizedToModify(HttpContext.Items, attachToObject.ObjectType, attachToObjectOwnerId))
|
||||
{
|
||||
//delete temp files
|
||||
DeleteTempFileUploadDueToBadRequest(uploadFormData);
|
||||
return StatusCode(401, new ApiNotAuthorizedResponse());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (badRequest)
|
||||
{
|
||||
//delete temp files
|
||||
DeleteTempFileUploadDueToBadRequest(uploadFormData);
|
||||
//return bad request
|
||||
return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_INVALID_VALUE, null, errorMessage));
|
||||
}
|
||||
|
||||
|
||||
//We have our files and a confirmed AyObject, ready to attach and save permanently
|
||||
if (uploadFormData.UploadedFiles.Count > 0)
|
||||
{
|
||||
foreach (UploadedFileInfo a in uploadFormData.UploadedFiles)
|
||||
{
|
||||
var v = FileUtil.storeFileAttachment(a.InitialUploadedPathName, a.MimeType, a.OriginalFileName, UserIdFromContext.Id(HttpContext.Items), attachToObject, ct);
|
||||
returnList.Add(new NameIdItem()
|
||||
{
|
||||
Name = v.DisplayFileName,
|
||||
Id = v.Id
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (InvalidDataException ex)
|
||||
{
|
||||
return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, "FileUploadAttempt", ex.Message));
|
||||
}
|
||||
|
||||
//Return the list of attachment ids and filenames
|
||||
return Ok(new ApiOkResponse(returnList));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Utility to delete files that were uploaded but couldn't be stored for some reason, called by Attach route
|
||||
/// </summary>
|
||||
/// <param name="uploadFormData"></param>
|
||||
private static void DeleteTempFileUploadDueToBadRequest(ApiUploadProcessor.ApiUploadedFilesResult uploadFormData)
|
||||
{
|
||||
if (uploadFormData.UploadedFiles.Count > 0)
|
||||
{
|
||||
foreach (UploadedFileInfo a in uploadFormData.UploadedFiles)
|
||||
{
|
||||
System.IO.File.Delete(a.InitialUploadedPathName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Delete Attachment
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns>Ok</returns>
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> DeleteAttachment([FromRoute] long id)
|
||||
{
|
||||
|
||||
if (!serverState.IsOpen)
|
||||
{
|
||||
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return BadRequest(new ApiErrorResponse(ModelState));
|
||||
}
|
||||
|
||||
var dbObj = await ct.FileAttachment.SingleOrDefaultAsync(m => m.Id == id);
|
||||
if (dbObj == null)
|
||||
{
|
||||
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
|
||||
}
|
||||
|
||||
|
||||
if (!Authorized.IsAuthorizedToDelete(HttpContext.Items, dbObj.AttachToObjectType, dbObj.OwnerId))
|
||||
{
|
||||
return StatusCode(401, new ApiNotAuthorizedResponse());
|
||||
}
|
||||
|
||||
//do the delete
|
||||
//this handles removing the file if there are no refs left and also the db record for the attachment
|
||||
FileUtil.deleteFileAttachment(dbObj, ct);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Download a file attachment
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="dlkey"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("download/{id}")]
|
||||
public async Task<IActionResult> Download([FromRoute] long id, [FromQuery] string dlkey)
|
||||
{
|
||||
//copied from Rockfish
|
||||
//https://dotnetcoretutorials.com/2017/03/12/uploading-files-asp-net-core/
|
||||
//https://stackoverflow.com/questions/45763149/asp-net-core-jwt-in-uri-query-parameter/45811270#45811270
|
||||
|
||||
|
||||
if (!serverState.IsOpen)
|
||||
{
|
||||
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
|
||||
}
|
||||
|
||||
|
||||
if (string.IsNullOrWhiteSpace(dlkey))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
//get user by key, if not found then reject
|
||||
//If user dlkeyexp has not expired then return file
|
||||
var dlkeyUser = await ct.User.SingleOrDefaultAsync(m => m.DlKey == dlkey);
|
||||
if (dlkeyUser == null)
|
||||
{
|
||||
return BadRequest(new ApiErrorResponse(ApiErrorCode.NOT_AUTHORIZED, "dlkey", "Download token not valid"));
|
||||
}
|
||||
|
||||
//Make sure the token provided is for the current user
|
||||
long lAuthenticatedUserId = UserIdFromContext.Id(HttpContext.Items);
|
||||
if (lAuthenticatedUserId != dlkeyUser.Id)
|
||||
{
|
||||
return BadRequest(new ApiErrorResponse(ApiErrorCode.NOT_AUTHORIZED, "dlkey", "Download token not valid"));
|
||||
}
|
||||
|
||||
|
||||
var utcNow = new DateTimeOffset(DateTime.Now.ToUniversalTime(), TimeSpan.Zero);
|
||||
if (dlkeyUser.DlKeyExpire < utcNow.DateTime)
|
||||
{
|
||||
return BadRequest(new ApiErrorResponse(ApiErrorCode.NOT_AUTHORIZED, "dlkey", "Download token has expired"));
|
||||
}
|
||||
|
||||
//Ok, user has a valid download key and it's not expired yet so get the attachment record
|
||||
var dbObj = await ct.FileAttachment.SingleOrDefaultAsync(m => m.Id == id);
|
||||
if (dbObj == null)
|
||||
{
|
||||
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
|
||||
}
|
||||
|
||||
//is this allowed?
|
||||
if (!Authorized.IsAuthorizedToRead(HttpContext.Items, dbObj.AttachToObjectType))
|
||||
{
|
||||
return StatusCode(401, new ApiNotAuthorizedResponse());
|
||||
}
|
||||
|
||||
//they are allowed, let's send the file
|
||||
string mimetype = dbObj.ContentType;
|
||||
var filePath = FileUtil.GetPermanentAttachmentFilePath(dbObj.StoredFileName);
|
||||
if (!System.IO.File.Exists(filePath))
|
||||
{
|
||||
//TODO: this should trigger some kind of notification to the ops people
|
||||
//and a red light on the dashboard
|
||||
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND, null, $"Physical file {dbObj.StoredFileName} not found despite attachment record, this file is missing"));
|
||||
}
|
||||
|
||||
return PhysicalFile(filePath, mimetype, dbObj.DisplayFileName);
|
||||
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
|
||||
}//eoc
|
||||
}//eons
|
||||
|
||||
|
||||
#region sample html form to work with this
|
||||
/*
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title></title>
|
||||
<script src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
|
||||
crossorigin="anonymous"></script>
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function () {
|
||||
$("#upload").click(function (evt) {
|
||||
var fileUpload = $("#files").get(0);
|
||||
var files = fileUpload.files;
|
||||
var data = new FormData();
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
data.append(files[i].name, files[i]);
|
||||
}
|
||||
|
||||
//attachment test
|
||||
data.append('AttachToObjectType','2');//object 2 is widget
|
||||
data.append('AttachToObjectId','200');//there should normally always be a widget with id 1
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "http://localhost:7575/api/v8.0/Attachment",
|
||||
headers: {
|
||||
Authorization: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOiIxNTIxNTY5ODE5IiwiZXhwIjoiMTUyNDE2MTgxOSIsImlzcyI6IkF5YU5vdmEiLCJpZCI6IjEifQ.4PzkzZNYK5mJkbfCHGQ2N2248atAvvcDgApoz65oIC0"
|
||||
},
|
||||
contentType: false,
|
||||
processData: false,
|
||||
data: data,
|
||||
success: function (message) {
|
||||
alert("upload successful!");
|
||||
console.log(message);
|
||||
},
|
||||
error: function (error) {
|
||||
console.log(error);
|
||||
alert("There was an error uploading files!");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<input type="file" id="files" name="files" multiple />
|
||||
<!-- <input type="file" accept=".zip,application/zip" id="files" name="files" multiple /> -->
|
||||
<input type="button" id="upload" value="Upload file(s)" />
|
||||
|
||||
|
||||
|
||||
|
||||
</form>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
*/
|
||||
#endregion
|
||||
Reference in New Issue
Block a user