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 /// /// Attachment controller /// [ApiController] [ApiVersion("8.0")] [Route("api/v{version:apiVersion}/[controller]")] [Produces("application/json")] [Authorize] public class AttachmentController : ControllerBase { private readonly AyContext ct; private readonly ILogger log; private readonly ApiServerState serverState; //private static readonly FormOptions _defaultFormOptions = new FormOptions(); /// /// /// /// /// /// public AttachmentController(AyContext dbcontext, ILogger logger, ApiServerState apiServerState) { ct = dbcontext; log = logger; serverState = apiServerState; } //Moved this functionality to authentication and expiry follows jwt token expiry // //LOOKAT: Centralize this code somewhere else, it's going to be needed for backup as well // //consider the 1 hour thing, is this legit depending on client? // /// // /// Get download token // /// A download token is good for 1 hour from issue // /// // /// Current download token for user // [HttpGet("DownloadToken")] // public async Task GetDownloadTokenAsync() // { // if (!serverState.IsOpen) // return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, 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(ApiOkResponse.Response(new { dlkey = u.DlKey, expires = u.DlKeyExpire }, true)); // } // } /// /// Upload attachment file /// /// Requires same Authorization roles as object that file is being attached to /// /// /// NameValue list of filenames and attachment id's [HttpPost] [DisableFormValueModelBinding] [RequestSizeLimit(10737418241)]//10737418240 = 10gb https://github.com/aspnet/Announcements/issues/267 public async Task UploadAsync() { //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(serverState.ApiErrorCode, null, serverState.Reason)); var returnList = new List(); 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.ProcessAttachmentUploadAsync(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.IsCoreBizObject) { badRequest = true; errorMessage = attachToObject.ObjectType.ToString() + " - AttachToObjectType does not support attachments"; } } //does attach to object exist? if (!badRequest) { //check if object exists if (!await BizObjectExistsInDatabase.ExistsAsync(attachToObject)) { badRequest = true; errorMessage = "Invalid attach object"; } else { // User needs modify rights to the object type in question if (!Authorized.HasModifyRole(HttpContext.Items, attachToObject.ObjectType)) { //delete temp files DeleteTempFileUploadDueToBadRequest(uploadFormData); return StatusCode(403, new ApiNotAuthorizedResponse()); } } } if (badRequest) { //delete temp files DeleteTempFileUploadDueToBadRequest(uploadFormData); //return bad request return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_INVALID_VALUE, null, errorMessage)); } long UserId = UserIdFromContext.Id(HttpContext.Items); //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 = await FileUtil.StoreFileAttachmentAsync(a.InitialUploadedPathName, a.MimeType, a.OriginalFileName, attachToObject, ct); returnList.Add(new NameIdItem() { Name = v.DisplayFileName, Id = v.Id }); //EVENT LOG await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, attachToObject.ObjectId, attachToObject.ObjectType, AyaEvent.AttachmentCreate, v.DisplayFileName), ct); //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationIdFromContext.Id(HttpContext.Items), v.Id, AyaType.FileAttachment); SearchParams.AddText(v.Notes).AddText(v.DisplayFileName).AddText(v.StoredFileName); await Search.ProcessNewObjectKeywordsAsync(SearchParams); } } } catch (InvalidDataException ex) { return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, "FileUploadAttempt", ex.Message)); } //Return the list of attachment ids and filenames return Ok(ApiOkResponse.Response(returnList, false)); } /// /// Utility to delete files that were uploaded but couldn't be stored for some reason, called by Attach route /// /// private static void DeleteTempFileUploadDueToBadRequest(ApiUploadProcessor.ApiUploadedFilesResult uploadFormData) { if (uploadFormData.UploadedFiles.Count > 0) { foreach (UploadedFileInfo a in uploadFormData.UploadedFiles) { System.IO.File.Delete(a.InitialUploadedPathName); } } } /// /// Delete Attachment /// /// /// Ok [HttpDelete("{id}")] public async Task DeleteAttachmentAsync([FromRoute] long id) { if (!serverState.IsOpen) return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, 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)); } long UserId = UserIdFromContext.Id(HttpContext.Items); if (!Authorized.HasDeleteRole(HttpContext.Items, dbObj.AttachToObjectType)) { return StatusCode(403, new ApiNotAuthorizedResponse()); } //do the delete //this handles removing the file if there are no refs left and also the db record for the attachment await FileUtil.DeleteFileAttachmentAsync(dbObj, ct); //Event log process delete await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObj.AttachToObjectId, dbObj.AttachToObjectType, AyaEvent.AttachmentDelete, dbObj.DisplayFileName), ct); //Delete search index await Search.ProcessDeletedObjectKeywordsAsync(dbObj.Id, AyaType.FileAttachment); return NoContent(); } /// /// Download a file attachment /// /// /// /// [HttpGet("download/{id}")] public async Task DownloadAsync([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(serverState.ApiErrorCode, 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) { //don't want to leak information so just say not found //return BadRequest(new ApiErrorResponse(ApiErrorCode.NOT_AUTHORIZED, "dlkey", "Download token not valid")); return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED)); } //Make sure the token provided is for the current user long UserId = UserIdFromContext.Id(HttpContext.Items); if (UserId != dlkeyUser.Id) { // return BadRequest(new ApiErrorResponse(ApiErrorCode.NOT_AUTHORIZED, "dlkey", "Download token not valid")); return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED)); } 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")); return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED)); } //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.HasReadFullRole(HttpContext.Items, dbObj.AttachToObjectType)) { return StatusCode(403, 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")); } //Log await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObj.AttachToObjectId, dbObj.AttachToObjectType, AyaEvent.AttachmentDownload, dbObj.DisplayFileName), ct); return PhysicalFile(filePath, mimetype, dbObj.DisplayFileName); } //////////////////////////////////////////////////////////////////////////////////// }//eoc }//eons #region sample html form to work with this /*
*/ #endregion