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