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.IO;
using System.ComponentModel.DataAnnotations;
using AyaNova.Models;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Util;
using AyaNova.Biz;
using System.Linq;
using System.Collections.Generic;
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}/attachment")]
[Produces("application/json")]
public class AttachmentController : ControllerBase
{
private readonly AyContext ct;
private readonly ILogger log;
private readonly ApiServerState serverState;
///
///
///
///
///
///
public AttachmentController(AyContext dbcontext, ILogger logger, ApiServerState apiServerState)
{
ct = dbcontext;
log = logger;
serverState = apiServerState;
}
public class UpdateAttachmentInfo
{
[Required]
public uint Concurrency { get; set; }
[Required]
public string DisplayFileName { get; set; }
public string Notes { get; set; }
}
///
/// Update FileAttachment
/// (FileName and notes only)
///
///
///
/// list
[Authorize]
[HttpPut("{id}")]
public async Task PutAttachment([FromRoute] long id, [FromBody] UpdateAttachmentInfo inObj)
{
if (!serverState.IsOpen)
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
if (!ModelState.IsValid)
return BadRequest(new ApiErrorResponse(ModelState));
var dbObject = await ct.FileAttachment.SingleOrDefaultAsync(z => z.Id == id);
if (dbObject == null)
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
long UserId = UserIdFromContext.Id(HttpContext.Items);
if (!Authorized.HasModifyRole(HttpContext.Items, dbObject.AttachToAType))
{
return StatusCode(403, new ApiNotAuthorizedResponse());
}
try
{
string ChangeTextra = string.Empty;
if (dbObject.DisplayFileName != inObj.DisplayFileName)
{
ChangeTextra = $"\"{dbObject.DisplayFileName}\" => \"{inObj.DisplayFileName}\"";
}
if (dbObject.Notes != inObj.Notes)
{
if (!string.IsNullOrWhiteSpace(ChangeTextra))
ChangeTextra += ", ";
ChangeTextra += "Notes";
}
dbObject.DisplayFileName = inObj.DisplayFileName;
dbObject.Notes = inObj.Notes;
//Set "original" value of concurrency token to input token
//this will allow EF to check it out
ct.Entry(dbObject).OriginalValues["Concurrency"] = inObj.Concurrency;
//Log event and save context
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.AttachToObjectId, dbObject.AttachToAType, AyaEvent.AttachmentModified, ChangeTextra), ct);
//SEARCH INDEXING
var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationIdFromContext.Id(HttpContext.Items), id, AyaType.FileAttachment);
SearchParams.AddText(inObj.Notes).AddText(inObj.DisplayFileName);
await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams);
//--------------
}
catch (DbUpdateConcurrencyException)
{
return StatusCode(409, new ApiErrorResponse(ApiErrorCode.CONCURRENCY_CONFLICT));
}
//Normallyh wouldn't return a whole list but in this case the UI demands it because of reactivity issues
var ret = await GetFileListForObjectAsync(dbObject.AttachToAType, dbObject.AttachToObjectId);
return Ok(ApiOkResponse.Response(ret));
}
///
/// Get attachments for object type and id specified
///
/// Required Role: Read full object properties rights to object type specified
///
///
/// file attachment list for object
[Authorize]
[HttpGet("list")]
public async Task GetList([FromQuery] AyaType ayaType, [FromQuery] long ayaId)
{
if (serverState.IsClosed)
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
if (!Authorized.HasReadFullRole(HttpContext.Items, ayaType))
{
return StatusCode(403, new ApiNotAuthorizedResponse());
}
if (!ModelState.IsValid)
return BadRequest(new ApiErrorResponse(ModelState));
var ret = await GetFileListForObjectAsync(ayaType, ayaId);
return Ok(ApiOkResponse.Response(ret));
}
///
/// Get parent object type and id
/// for specified attachment id
///
///
///
[Authorize]
[HttpGet("parent/{id}")]
public async Task GetParent([FromRoute] long id)
{
if (serverState.IsClosed)
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
if (!Authorized.HasReadFullRole(HttpContext.Items, AyaType.FileAttachment))
return StatusCode(403, new ApiNotAuthorizedResponse());
if (!ModelState.IsValid)
return BadRequest(new ApiErrorResponse(ModelState));
var at = await ct.FileAttachment.AsNoTracking().Where(z => z.Id == id).FirstOrDefaultAsync();
if (at == null)
return NotFound();
return Ok(ApiOkResponse.Response(new { id = at.AttachToObjectId, type = at.AttachToAType }));
}
///
/// Upload attachment file
/// Max 10GiB total
/// Requires same Authorization roles as object that file is being attached to
///
///
/// NameValue list of filenames and attachment id's
[Authorize]
[HttpPost]
[DisableFormValueModelBinding]
[RequestSizeLimit(ServerBootConfig.MAX_ATTACHMENT_UPLOAD_BYTES)]
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();
object ret = null;
AyaTypeId attachToObject = null;
try
{
if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, null, $"Expected a multipart request, but got {Request.ContentType}"));
bool badRequest = false;
string AttachToAType = string.Empty;
string AttachToObjectId = string.Empty;
string errorMessage = string.Empty;
string Notes = string.Empty;
List FileData = new List();
var uploadFormData = await ApiUploadProcessor.ProcessUploadAsync(HttpContext);
if (!string.IsNullOrWhiteSpace(uploadFormData.Error))
{
badRequest = true;
errorMessage = uploadFormData.Error;
}
if (!badRequest
&& (!uploadFormData.FormFieldData.ContainsKey("FileData")
|| !uploadFormData.FormFieldData.ContainsKey("AttachToAType")
|| !uploadFormData.FormFieldData.ContainsKey("AttachToObjectId")))
{
badRequest = true;
errorMessage = "Missing one or more required FormFieldData values: AttachToAType, AttachToObjectId, FileData";
}
if (!badRequest)
{
AttachToAType = uploadFormData.FormFieldData["AttachToAType"].ToString();
AttachToObjectId = uploadFormData.FormFieldData["AttachToObjectId"].ToString();
if (uploadFormData.FormFieldData.ContainsKey("Notes"))
Notes = uploadFormData.FormFieldData["Notes"].ToString();
//fileData in JSON stringify format which contains the actual last modified dates etc
//"[{\"name\":\"Client.csv\",\"lastModified\":1582822079618},{\"name\":\"wmi4fu06nrs41.jpg\",\"lastModified\":1586900220990}]"
FileData = Newtonsoft.Json.JsonConvert.DeserializeObject>(uploadFormData.FormFieldData["FileData"].ToString());
if (string.IsNullOrWhiteSpace(AttachToAType) || string.IsNullOrWhiteSpace(AttachToObjectId))
{
badRequest = true;
errorMessage = "AttachToAType and / or AttachToObjectId are empty and are required";
}
}
//Get type and id object from post paramters
if (!badRequest)
{
attachToObject = new AyaTypeId(AttachToAType, AttachToObjectId);
if (attachToObject.IsEmpty)
{
badRequest = true;
errorMessage = "AttachToAType 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.AType.ToString() + " - AttachToAType does not support attachments";
}
}
//does attach to object exist?
if (!badRequest)
{
//check if object exists
if (!await BizObjectExistsInDatabase.ExistsAsync(attachToObject.AType, attachToObject.ObjectId, ct))
{
badRequest = true;
errorMessage = "Invalid attach object";
}
else
{
// User needs modify rights to the object type in question
if (!Authorized.HasModifyRole(HttpContext.Items, attachToObject.AType))
{
//delete temp files
ApiUploadProcessor.DeleteTempUploadFile(uploadFormData);
return StatusCode(403, new ApiNotAuthorizedResponse());
}
}
}
if (badRequest)
{
//delete temp files
ApiUploadProcessor.DeleteTempUploadFile(uploadFormData);
//file too large is most likely issue so in that case return this localized properly
if (errorMessage.Contains("413"))
{
var TransId = UserTranslationIdFromContext.Id(HttpContext.Items);
return BadRequest(new ApiErrorResponse(
ApiErrorCode.VALIDATION_LENGTH_EXCEEDED,
null,
"HTTP ERROR CODE 413 " + String.Format(await TranslationBiz.GetTranslationStaticAsync("AyaFileFileTooLarge", TransId, ct), FileUtil.GetBytesReadable(ServerBootConfig.MAX_ATTACHMENT_UPLOAD_BYTES))));
}
else//not too big, something else
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)
{
//Get the actual date from the separate filedata
//this is because the lastModified date is always empty in the form data files
DateTime theDate = DateTime.MinValue;
foreach (UploadFileData f in FileData)
{
if (f.name == a.OriginalFileName)
{
if (f.lastModified > 0)
{
theDate = DateTimeOffset.FromUnixTimeMilliseconds(f.lastModified).UtcDateTime;
}
}
}
if (theDate == DateTime.MinValue)
theDate = DateTime.UtcNow;
var v = await FileUtil.StoreFileAttachmentAsync(a.InitialUploadedPathName, a.MimeType, a.OriginalFileName, theDate, attachToObject, Notes, ct);
//EVENT LOG
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, attachToObject.ObjectId, attachToObject.AType, 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);
await Search.ProcessNewObjectKeywordsAsync(SearchParams);
}
}
ret = await GetFileListForObjectAsync(attachToObject.AType, attachToObject.ObjectId);
}
catch (InvalidDataException ex)
{
return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, null, ex.Message));
}
//Return the list of attachment ids and filenames
return Ok(ApiOkResponse.Response(ret));
}
// ///
// /// 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
[Authorize]
[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 dbObject = await ct.FileAttachment.SingleOrDefaultAsync(z => z.Id == id);
if (dbObject == null)
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
long UserId = UserIdFromContext.Id(HttpContext.Items);
if (!Authorized.HasDeleteRole(HttpContext.Items, dbObject.AttachToAType))
{
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(dbObject, ct);
//Event log process delete
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.AttachToObjectId, dbObject.AttachToAType, AyaEvent.AttachmentDelete, dbObject.DisplayFileName), ct);
//Delete search index
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, AyaType.FileAttachment, ct);
return NoContent();
}
///
/// Batch delete attachments
///
///
/// No content
[HttpPost("batch-delete")]
[Authorize]
public async Task PostBatchDelete([FromBody] List idList)
{
if (serverState.IsClosed)
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
if (!ModelState.IsValid)
return BadRequest(new ApiErrorResponse(ModelState));
if (!Authorized.HasModifyRole(HttpContext.Items, AyaType.FileAttachment))
return StatusCode(403, new ApiNotAuthorizedResponse());
long UserId = UserIdFromContext.Id(HttpContext.Items);
foreach (long id in idList)
{
var dbObject = await ct.FileAttachment.FirstOrDefaultAsync(z => z.Id == id);
if (dbObject == null)
continue;
//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(dbObject, ct);
//Event log process delete
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.AttachToObjectId, dbObject.AttachToAType, AyaEvent.AttachmentDelete, dbObject.DisplayFileName), ct);
//Delete search index
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, AyaType.FileAttachment, ct);
}
return NoContent();
}
///
/// Batch move attachments
///
///
/// No content
[HttpPost("batch-move")]
[Authorize]
public async Task PostBatchMove([FromBody] dtoBatchMove dt)
{
if (serverState.IsClosed)
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
if (!ModelState.IsValid)
return BadRequest(new ApiErrorResponse(ModelState));
if (!Authorized.HasModifyRole(HttpContext.Items, AyaType.FileAttachment))
return StatusCode(403, new ApiNotAuthorizedResponse());
if (!await BizObjectExistsInDatabase.ExistsAsync(dt.ToType, dt.ToId, ct))
return BadRequest(new ApiErrorResponse(ApiErrorCode.NOT_FOUND, null, "LT:ErrorAPI2010"));
long UserId = UserIdFromContext.Id(HttpContext.Items);
foreach (long id in dt.IdList)
{
var dbObject = await ct.FileAttachment.FirstOrDefaultAsync(z => z.Id == id);
if (dbObject == null)
continue;
//do the move
var msg = $"{dbObject.DisplayFileName} moved from {dbObject.AttachToAType}-{dbObject.AttachToObjectId} to {dt.ToType}-{dt.ToId} ";
dbObject.AttachToObjectId = dt.ToId;
dbObject.AttachToAType = dt.ToType;
await ct.SaveChangesAsync();
//Event log process move
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.AttachToObjectId, dbObject.AttachToAType, AyaEvent.AttachmentModified, msg), ct);
}
return NoContent();
}
public class dtoBatchMove
{
public List IdList { get; set; }
public AyaType ToType { get; set; }
public long ToId { get; set; }
}
///
/// Download a file attachment
///
///
/// download token
///
[HttpGet("download/{id}")]
public async Task DownloadAsync([FromRoute] long id, [FromQuery] string t)
{
//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));
var DownloadUser = await UserBiz.ValidateDownloadTokenAndReturnUserAsync(t, ct);
if (DownloadUser == null)
{
await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY);//DOS protection
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 dbObject = await ct.FileAttachment.SingleOrDefaultAsync(z => z.Id == id);
if (dbObject == null)
{
await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY);//fishing protection
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
//is this allowed?
if (!Authorized.HasReadFullRole(DownloadUser.Roles, dbObject.AttachToAType))
{
await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY);//DOS protection
return StatusCode(403, new ApiNotAuthorizedResponse());
}
//they are allowed, let's send the file
string mimetype = dbObject.ContentType;
var filePath = FileUtil.GetPermanentAttachmentFilePath(dbObject.StoredFileName);
if (!System.IO.File.Exists(filePath))
{
//TODO: this should reset the validity
var errText = $"Physical file {dbObject.StoredFileName} not found despite attachment record, this file is missing";
log.LogError(errText);
await NotifyEventHelper.AddOpsProblemEvent($"File attachment issue: {errText}");
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND, null, errText));
}
//Log
await EventLogProcessor.LogEventToDatabaseAsync(new Event(DownloadUser.Id, dbObject.AttachToObjectId, dbObject.AttachToAType, AyaEvent.AttachmentDownload, dbObject.DisplayFileName), ct);
return PhysicalFile(filePath, mimetype, dbObject.DisplayFileName);
}
////////////////////////////////////////////////////////////////////////////////////
async private Task