diff --git a/server/AyaNova/AyaNova.csproj b/server/AyaNova/AyaNova.csproj index 469a5ec3..08d34c58 100644 --- a/server/AyaNova/AyaNova.csproj +++ b/server/AyaNova/AyaNova.csproj @@ -4,8 +4,8 @@ true - 8.0.0-alpha.86 - 8.0.0.86 + 8.0.0-alpha.87 + 8.0.0.87 bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml 1591 diff --git a/server/AyaNova/Controllers/ExportController.cs b/server/AyaNova/Controllers/ExportController.cs new file mode 100644 index 00000000..cdaf6fbb --- /dev/null +++ b/server/AyaNova/Controllers/ExportController.cs @@ -0,0 +1,277 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Logging; +using Microsoft.EntityFrameworkCore; +using AyaNova.Models; +using AyaNova.Api.ControllerHelpers; +using AyaNova.Biz; +using AyaNova.Util; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace AyaNova.Api.Controllers +{ + [ApiController] + [ApiVersion("8.0")] + [Route("api/v{version:apiVersion}/export")] + [Produces("application/json")] + [Authorize] + public class ExportController : ControllerBase + { + private readonly AyContext ct; + private readonly ILogger log; + private readonly ApiServerState serverState; + + /// + /// ctor + /// + /// + /// + /// + public ExportController(AyContext dbcontext, ILogger logger, ApiServerState apiServerState) + { + ct = dbcontext; + log = logger; + serverState = apiServerState; + } + + + /// + /// Export to file + /// + /// Valid values are: "csv","json" + /// + + /// downloadable export file name + [HttpPost("render")] + public async Task RenderExport([FromRoute] string format, [FromBody] DataListSelection dataListSelection) + { + + if (!serverState.IsOpen) + return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); + if (!ModelState.IsValid) + return BadRequest(new ApiErrorResponse(ModelState)); + + if (dataListSelection.IsEmpty) + return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_REQUIRED, null, "DataListSelection is required")); + + if (!dataListSelection.ObjectType.HasAttribute(typeof(CoreBizObjectAttribute))) + return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, null, "Not a taggable object type")); + + if (!Authorized.HasReadFullRole(HttpContext.Items, dataListSelection.ObjectType)) + return StatusCode(403, new ApiNotAuthorizedResponse()); + + if (string.IsNullOrWhiteSpace(format)) + return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_REQUIRED, null, "format required")); + + if (format != "csv" && format != "json") + return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_INVALID_VALUE, null, "format not valid, must be 'csv' or 'json'")); + + await dataListSelection.RehydrateIdList(ct, UserRolesFromContext.Roles(HttpContext.Items), log); + if (dataListSelection.SelectedRowIds.Length == 0) + return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_REQUIRED, null, "List of ids")); + + + log.LogDebug($"Instantiating biz object handler for {dataListSelection.ObjectType}"); + var biz = BizObjectFactory.GetBizObject(dataListSelection.ObjectType, ct); + log.LogDebug($"Fetching data for {dataListSelection.SelectedRowIds.Length} {dataListSelection.ObjectType} items"); + var TheData = await ((IExportAbleObject)biz).GetExportData(dataListSelection.SelectedRowIds); + + string outputFileName = StringUtil.ReplaceLastOccurrence(FileUtil.NewRandomFileName, ".", "") + ".pdf"; + string outputFullPath = System.IO.Path.Combine(FileUtil.TemporaryFilesFolder, outputFileName); + log.LogDebug($"Calling render export data to file"); + //TODO: RENDER DATA TO FILE HERE await page.PdfAsync(outputFullPath, PdfOptions); + + log.LogDebug($"Completed, returning results"); + return Ok(ApiOkResponse.Response(outputFileName)); + + // var httpConnectionFeature = HttpContext.Features.Get(); + // var API_URL = $"http://127.0.0.1:{httpConnectionFeature.LocalPort}/api/v8/"; + // try + // { + // var result = await biz.RenderExport(ExportParam, API_URL); + + // if (string.IsNullOrWhiteSpace(result)) + // return BadRequest(new ApiErrorResponse(biz.Errors)); + // else + // return Ok(ApiOkResponse.Response(result)); + // } + // catch (System.Exception ex) + // { + // //The Javascript evaluation stack trace can be in the message making it long and internalized, + // //however the info is useful as it can indicate exactly which function failed etc so sending it all back is best + // return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, null, ex.Message)); + // } + } + + + /// + /// Download a rendered Export + /// + /// + /// download token + /// + [HttpGet("download/{fileName}")] + [AllowAnonymous] + public async Task DownloadAsync([FromRoute] string fileName, [FromQuery] string t) + { + if (!serverState.IsOpen) + return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); + + if (await UserBiz.ValidateDownloadTokenAndReturnUserAsync(t, ct) == null) + { + await Task.Delay(ServerBootConfig.FAILED_AUTH_DELAY);//DOS protection + return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED)); + } + + if (!FileUtil.TemporaryFileExists(fileName)) + { + await Task.Delay(ServerBootConfig.FAILED_AUTH_DELAY);//fishing protection + return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); + } + + var FilePath = FileUtil.GetFullPathForTemporaryFile(fileName); + return PhysicalFile(FilePath, "application/pdf"); + } + + + + /// + /// Download Export template + /// + /// Export id + /// download token + /// A single Export template as a file + [AllowAnonymous] + [HttpGet("export/{id}")] + public async Task DownloadTemplate([FromRoute] long id, [FromQuery] string t) + { + if (serverState.IsClosed) + return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); + + if (!ModelState.IsValid) + return BadRequest(new ApiErrorResponse(ModelState)); + + if (await UserBiz.ValidateDownloadTokenAndReturnUserAsync(t, ct) == null) + { + await Task.Delay(ServerBootConfig.FAILED_AUTH_DELAY);//DOS protection + return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED)); + } + + var o = await ct.Export.SingleOrDefaultAsync(z => z.Id == id); + //turn into correct format and then send as file + if (o == null) + { + return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); + } + var asText = Newtonsoft.Json.JsonConvert.SerializeObject( + o, + Newtonsoft.Json.Formatting.None, + new JsonSerializerSettings { ContractResolver = new AyaNova.Util.JsonUtil.ShouldSerializeContractResolver(new string[] { "Concurrency", "Id" }) }); + var bytes = System.Text.Encoding.UTF8.GetBytes(asText); + var file = new FileContentResult(bytes, "application/octet-stream"); + file.FileDownloadName = Util.FileUtil.StringToSafeFileName(o.Name) + ".ayrt"; + return file; + } + + + + + /// + /// Upload Reprot template export file + /// Max 15mb total + /// + /// Accepted + [Authorize] + [HttpPost("upload")] + [DisableFormValueModelBinding] + [RequestSizeLimit(15000000)]//currently the largest v7 export for a Export template is 828kb, I'm guessing 15mb is more than enough + 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)); + + + // AyaTypeId attachToObject = null; + ApiUploadProcessor.ApiUploadedFilesResult uploadFormData = null; + try + { + if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType)) + return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, null, $"Expected a multipart request, but got {Request.ContentType}")); + + //Save uploads to disk under temporary file names until we decide how to handle them + uploadFormData = await ApiUploadProcessor.ProcessUploadAsync(HttpContext); + + + + List FileData = new List(); + + if (!uploadFormData.FormFieldData.ContainsKey("FileData"))//only filedata is required + { + return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, null, "Missing required FormFieldData value: FileData")); + } + + //fileData in JSON stringify format + FileData = Newtonsoft.Json.JsonConvert.DeserializeObject>(uploadFormData.FormFieldData["FileData"].ToString()); + + //Instantiate the business object handler + ExportBiz biz = ExportBiz.GetBiz(ct, HttpContext); + + + //We have our files now can parse and insert into db + if (uploadFormData.UploadedFiles.Count > 0) + { + //deserialize each file and import + foreach (UploadedFileInfo a in uploadFormData.UploadedFiles) + { + JObject o = JObject.Parse(System.IO.File.ReadAllText(a.InitialUploadedPathName)); + if (!await biz.ImportAsync(o)) + { + //delete all the files temporarily uploaded and return bad request + DeleteTempUploadFile(uploadFormData); + return BadRequest(new ApiErrorResponse(biz.Errors)); + } + } + } + } + catch (System.IO.InvalidDataException ex) + { + return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, null, ex.Message)); + } + finally + { + //delete all the files temporarily uploaded and return bad request + + DeleteTempUploadFile(uploadFormData); + } + + //Return the list of attachment ids and filenames + return Accepted(); + } + + private static void DeleteTempUploadFile(ApiUploadProcessor.ApiUploadedFilesResult uploadFormData) + { + if (uploadFormData.UploadedFiles.Count > 0) + { + foreach (UploadedFileInfo a in uploadFormData.UploadedFiles) + { + System.IO.File.Delete(a.InitialUploadedPathName); + } + } + } + //----------------------------------------- + + + + + + }//eoc +}//eons \ No newline at end of file diff --git a/server/AyaNova/biz/IExportAbleObject.cs b/server/AyaNova/biz/IExportAbleObject.cs new file mode 100644 index 00000000..b5c81a01 --- /dev/null +++ b/server/AyaNova/biz/IExportAbleObject.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +namespace AyaNova.Biz +{ + /// + /// Interface for biz objects that support exporting + /// + internal interface IExportAbleObject + { + + //Get items indicated in id list in exportable format + //called by ExportBiz rendering code + Task GetExportData(long[] idList); + const int EXPORT_DATA_BATCH_SIZE = 100; + + } + +} \ No newline at end of file diff --git a/server/AyaNova/biz/WidgetBiz.cs b/server/AyaNova/biz/WidgetBiz.cs index bae11b1f..752a5fab 100644 --- a/server/AyaNova/biz/WidgetBiz.cs +++ b/server/AyaNova/biz/WidgetBiz.cs @@ -11,7 +11,7 @@ using System.Collections.Generic; namespace AyaNova.Biz { - internal class WidgetBiz : BizObject, IJobObject, ISearchAbleObject, IReportAbleObject + internal class WidgetBiz : BizObject, IJobObject, ISearchAbleObject, IReportAbleObject, IExportAbleObject { @@ -305,14 +305,22 @@ namespace AyaNova.Biz var orderedList = from id in batch join z in batchResults on id equals z.Id select z; foreach (Widget w in orderedList) { - var jo=JObject.FromObject(w); - jo["CustomFields"]=JObject.Parse((string)jo["CustomFields"]); + var jo = JObject.FromObject(w); + jo["CustomFields"] = JObject.Parse((string)jo["CustomFields"]); ReportData.Add(jo); } } return ReportData; } + //Get data for export + public async Task GetExportData(long[] idList) + { + //for now just re-use the report data code + //this may turn out to be the pattern for most biz object types but keeping it seperate allows for custom usage from time to time + return await GetReportData(idList); + } + //////////////////////////////////////////////////////////////////////////////////////////////// //JOB / OPERATIONS // @@ -389,6 +397,8 @@ namespace AyaNova.Biz + + ///////////////////////////////////////////////////////////////////// }//eoc diff --git a/server/AyaNova/util/AyaNovaVersion.cs b/server/AyaNova/util/AyaNovaVersion.cs index c06e3677..1fda47aa 100644 --- a/server/AyaNova/util/AyaNovaVersion.cs +++ b/server/AyaNova/util/AyaNovaVersion.cs @@ -5,7 +5,7 @@ namespace AyaNova.Util /// internal static class AyaNovaVersion { - public const string VersionString = "8.0.0-alpha.86"; + public const string VersionString = "8.0.0-alpha.87"; public const string FullNameAndVersion = "AyaNova server " + VersionString; }//eoc }//eons \ No newline at end of file