diff --git a/source/SimpleRedirects.Core/Enums/DataRecordProvider.cs b/source/SimpleRedirects.Core/Enums/DataRecordProvider.cs new file mode 100644 index 0000000..65c8517 --- /dev/null +++ b/source/SimpleRedirects.Core/Enums/DataRecordProvider.cs @@ -0,0 +1,7 @@ +namespace SimpleRedirects.Core.Enums; + +public enum DataRecordProvider +{ + Csv, + Excel +} \ No newline at end of file diff --git a/source/SimpleRedirects.Core/Extensions/FormFileExtensions.cs b/source/SimpleRedirects.Core/Extensions/FormFileExtensions.cs new file mode 100644 index 0000000..a740284 --- /dev/null +++ b/source/SimpleRedirects.Core/Extensions/FormFileExtensions.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Http; +using SimpleRedirects.Core.Enums; + +namespace SimpleRedirects.Core.Extensions; + +internal static class FormFileExtensions +{ + /// + /// Attempts to read a file's extension and translate this to a DataRecordProvider to be able to determine the correct service to use to handle the import file + /// + /// The file which will be used to determine the data record provider + /// + /// True and a DataRecordProvider as out var when filetype can be matched to one, false and null when the file's type cannot be mapped to a DataRecordProvider + internal static bool CanGetDataRecordProviderFromFile(this IFormFile file, out DataRecordProvider dataRecordProvider) + { + var fileType = System.IO.Path.GetExtension(file?.FileName); + switch (fileType) + { + case ".csv": + dataRecordProvider = DataRecordProvider.Csv; + return true; + case ".xlsx": + dataRecordProvider = DataRecordProvider.Excel; + return true; + default: + dataRecordProvider = DataRecordProvider.Csv; + return false; + } + } +} \ No newline at end of file diff --git a/source/SimpleRedirects.Core/Models/AddRedirectResponse.cs b/source/SimpleRedirects.Core/Models/AddRedirectResponse.cs index b95a028..b626d72 100644 --- a/source/SimpleRedirects.Core/Models/AddRedirectResponse.cs +++ b/source/SimpleRedirects.Core/Models/AddRedirectResponse.cs @@ -1,17 +1,10 @@ using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; namespace SimpleRedirects.Core.Models { - public class AddRedirectResponse + public class AddRedirectResponse : BaseResponse { [JsonProperty("newRedirect")] public Redirect NewRedirect { get; set; } - - [JsonProperty("success")] - public bool Success { get; set; } - - [JsonProperty("message")] - public string Message { get; set; } } } \ No newline at end of file diff --git a/source/SimpleRedirects.Core/Models/BaseResponse.cs b/source/SimpleRedirects.Core/Models/BaseResponse.cs new file mode 100644 index 0000000..35fe56e --- /dev/null +++ b/source/SimpleRedirects.Core/Models/BaseResponse.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace SimpleRedirects.Core.Models; + +public abstract class BaseResponse +{ + [JsonProperty("success")] + public bool Success { get; set; } + + [JsonProperty("message")] + public string Message { get; set; } +} \ No newline at end of file diff --git a/source/SimpleRedirects.Core/Models/DataRecordCollectionFile.cs b/source/SimpleRedirects.Core/Models/DataRecordCollectionFile.cs new file mode 100644 index 0000000..17cc100 --- /dev/null +++ b/source/SimpleRedirects.Core/Models/DataRecordCollectionFile.cs @@ -0,0 +1,23 @@ +using System; +using System.Globalization; +using Microsoft.AspNetCore.Mvc; +using SimpleRedirects.Core.Enums; + +namespace SimpleRedirects.Core.Models; + +public class DataRecordCollectionFile +{ + public DataRecordProvider DataRecordProvider { get; set; } + public byte[] File { get; set; } + public string ContentType => DataRecordProvider == DataRecordProvider.Csv ? "text/csv" : "application/vnd.ms-excel"; + public string FileExtension => DataRecordProvider == DataRecordProvider.Csv ? ".csv" : ".xlsx"; + public string FileName => $"SimpleRedirects-{DataRecordProvider}-Export-{DateTimeOffset.Now.ToString("dd-M-yyyy", CultureInfo.InvariantCulture)}{FileExtension}"; + + public DataRecordCollectionFile(DataRecordProvider dataRecordProvider, byte[] file) + { + DataRecordProvider = dataRecordProvider; + File = file; + } + + public FileContentResult AsFileContentResult() => new(File, ContentType) { FileDownloadName = FileName }; +} \ No newline at end of file diff --git a/source/SimpleRedirects.Core/Models/DeleteRedirectResponse.cs b/source/SimpleRedirects.Core/Models/DeleteRedirectResponse.cs index eaa6138..337f712 100644 --- a/source/SimpleRedirects.Core/Models/DeleteRedirectResponse.cs +++ b/source/SimpleRedirects.Core/Models/DeleteRedirectResponse.cs @@ -3,12 +3,7 @@ namespace SimpleRedirects.Core.Models { - public class DeleteRedirectResponse + public class DeleteRedirectResponse : BaseResponse { - [JsonProperty("success")] - public bool Success { get; set; } - - [JsonProperty("message")] - public string Message { get; set; } } } \ No newline at end of file diff --git a/source/SimpleRedirects.Core/Models/ImportRedirectsResponse.cs b/source/SimpleRedirects.Core/Models/ImportRedirectsResponse.cs new file mode 100644 index 0000000..fdd0ba2 --- /dev/null +++ b/source/SimpleRedirects.Core/Models/ImportRedirectsResponse.cs @@ -0,0 +1,45 @@ +using System; +using System.Linq; +using Newtonsoft.Json; + +namespace SimpleRedirects.Core.Models; + +public class ImportRedirectsResponse : BaseResponse +{ + [JsonProperty("addedRedirects")] + public int AddedRedirects { get; set; } + + [JsonProperty("updatedRedirects")] + public int UpdatedRedirects { get; set; } + + [JsonProperty("existingRedirects")] + public int ExistingRedirects { get; set; } + + [JsonProperty("errorRedirects")] + public Redirect[] ErrorRedirects { get; set; } + + public static ImportRedirectsResponse FromImport(int addedRedirects, int updatedRedirects, int existingRedirects, Redirect[] errorRedirects) + => new ImportRedirectsResponse + { + Success = !errorRedirects.Any(), + Message = $"Redirect import completed {(!errorRedirects.Any() ? "without errors" : $"with {errorRedirects.Length} error{FormatSingularOrPlural(errorRedirects.Length)}")},{(existingRedirects > 0 ? $" ignored {existingRedirects} redirect{FormatSingularOrPlural(existingRedirects)} because they already existed," : string.Empty)} added {addedRedirects} redirect{FormatSingularOrPlural(addedRedirects)}{(updatedRedirects > 0 ? $" and updated {updatedRedirects} redirect{FormatSingularOrPlural(updatedRedirects)}" : string.Empty)}.", + AddedRedirects = addedRedirects, + UpdatedRedirects = updatedRedirects, + ExistingRedirects = existingRedirects, + ErrorRedirects = errorRedirects + }; + + public static ImportRedirectsResponse EmptyImportRecordResponse(string message = "No valid redirects could be processed.") + => new ImportRedirectsResponse + { + Success = false, + Message = message, + AddedRedirects = 0, + UpdatedRedirects = 0, + ExistingRedirects = 0, + ErrorRedirects = Array.Empty() + }; + + private static string FormatSingularOrPlural(int length) + => length == 1 ? string.Empty : "s"; +} \ No newline at end of file diff --git a/source/SimpleRedirects.Core/Models/Redirect.cs b/source/SimpleRedirects.Core/Models/Redirect.cs index 7708738..9a11007 100644 --- a/source/SimpleRedirects.Core/Models/Redirect.cs +++ b/source/SimpleRedirects.Core/Models/Redirect.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Text.RegularExpressions; using System.Web; +using CsvHelper.Configuration.Attributes; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using NPoco; @@ -21,6 +22,7 @@ public class Redirect [Column("IsRegex")] [JsonProperty("isRegex")] + [Default("False")] public bool IsRegex { get; set; } [Column("OldUrl")] @@ -29,18 +31,22 @@ public class Redirect [Column("NewUrl")] [JsonProperty("newUrl")] + [Default("")] public string NewUrl { get; set; } [Column("RedirectCode")] [JsonProperty("redirectCode")] + [Default(301)] public int RedirectCode { get; set; } [Column("LastUpdated")] [JsonProperty("lastUpdated")] - public DateTime LastUpdated { get; set; } + [Default("")] + public DateTime? LastUpdated { get; set; } [Column("Notes")] [JsonProperty("notes")] + [Default("")] public string Notes { get; set; } public string GetNewUrl(Uri uri, bool preserveQueryString) diff --git a/source/SimpleRedirects.Core/Models/RedirectMap.cs b/source/SimpleRedirects.Core/Models/RedirectMap.cs new file mode 100644 index 0000000..7aefea0 --- /dev/null +++ b/source/SimpleRedirects.Core/Models/RedirectMap.cs @@ -0,0 +1,13 @@ +using System.Globalization; +using CsvHelper.Configuration; + +namespace SimpleRedirects.Core.Models; + +public sealed class RedirectMap : ClassMap +{ + public RedirectMap() + { + AutoMap(CultureInfo.InvariantCulture); + Map(m => m.Id).Ignore(); + } +} \ No newline at end of file diff --git a/source/SimpleRedirects.Core/Models/UpdateRedirectResponse.cs b/source/SimpleRedirects.Core/Models/UpdateRedirectResponse.cs index 4715f4e..7b859fb 100644 --- a/source/SimpleRedirects.Core/Models/UpdateRedirectResponse.cs +++ b/source/SimpleRedirects.Core/Models/UpdateRedirectResponse.cs @@ -3,15 +3,9 @@ namespace SimpleRedirects.Core.Models { - public class UpdateRedirectResponse + public class UpdateRedirectResponse : BaseResponse { [JsonProperty("updatedRedirect")] public Redirect UpdatedRedirect { get; set; } - - [JsonProperty("success")] - public bool Success { get; set; } - - [JsonProperty("message")] - public string Message { get; set; } } } \ No newline at end of file diff --git a/source/SimpleRedirects.Core/RedirectApiController.cs b/source/SimpleRedirects.Core/RedirectApiController.cs index e93dca5..fdcef5c 100644 --- a/source/SimpleRedirects.Core/RedirectApiController.cs +++ b/source/SimpleRedirects.Core/RedirectApiController.cs @@ -1,7 +1,11 @@ using System; using System.Collections.Generic; +using System.Linq; using Microsoft.AspNetCore.Mvc; +using SimpleRedirects.Core.Enums; +using SimpleRedirects.Core.Extensions; using SimpleRedirects.Core.Models; +using SimpleRedirects.Core.Services; using Umbraco.Cms.Web.BackOffice.Controllers; using Umbraco.Cms.Web.Common.Attributes; @@ -11,10 +15,12 @@ namespace SimpleRedirects.Core public class RedirectApiController : UmbracoAuthorizedApiController { private readonly RedirectRepository _redirectRepository; + private readonly ImportExportFactory _importExportFactory; - public RedirectApiController(RedirectRepository redirectRepository) + public RedirectApiController(RedirectRepository redirectRepository, ImportExportFactory importExportFactory) { _redirectRepository = redirectRepository; + _importExportFactory = importExportFactory; } /// @@ -36,18 +42,20 @@ public IEnumerable GetAll() public AddRedirectResponse Add(AddRedirectRequest request) { if (request == null) return new AddRedirectResponse() { Success = false, Message = "Request was empty" }; - if (!ModelState.IsValid) return new AddRedirectResponse() { Success = false, Message = "Missing required attributes" }; + if (!ModelState.IsValid) + return new AddRedirectResponse() { Success = false, Message = "Missing required attributes" }; try { - var redirect = _redirectRepository.AddRedirect(request.IsRegex, request.OldUrl, request.NewUrl, request.RedirectCode, request.Notes); + var redirect = _redirectRepository.AddRedirect(request.IsRegex, request.OldUrl, request.NewUrl, + request.RedirectCode, request.Notes); return new AddRedirectResponse() { Success = true, NewRedirect = redirect }; } - catch(Exception e) + catch (Exception e) { - return new AddRedirectResponse() { Success = false, Message = "There was an error adding the redirect : "+ e.Message }; + return new AddRedirectResponse() + { Success = false, Message = "There was an error adding the redirect : " + e.Message }; } - } /// @@ -58,9 +66,9 @@ public AddRedirectResponse Add(AddRedirectRequest request) [HttpPost] public UpdateRedirectResponse Update(UpdateRedirectRequest request) { - if (request == null) return new UpdateRedirectResponse() { Success = false, Message = "Request was empty" }; - if (!ModelState.IsValid) return new UpdateRedirectResponse() { Success = false, Message = "Missing required attributes" }; + if (!ModelState.IsValid) + return new UpdateRedirectResponse() { Success = false, Message = "Missing required attributes" }; try { @@ -69,7 +77,8 @@ public UpdateRedirectResponse Update(UpdateRedirectRequest request) } catch (Exception e) { - return new UpdateRedirectResponse() { Success = false, Message = "There was an error updating the redirect : "+e.Message }; + return new UpdateRedirectResponse() + { Success = false, Message = "There was an error updating the redirect : " + e.Message }; } } @@ -81,16 +90,38 @@ public UpdateRedirectResponse Update(UpdateRedirectRequest request) [HttpDelete] public DeleteRedirectResponse Delete(int id) { - if (id == 0) return new DeleteRedirectResponse() { Success = false, Message = "Invalid ID passed for redirect to delete" }; + if (id == 0) + return new DeleteRedirectResponse() + { Success = false, Message = "Invalid ID passed for redirect to delete" }; try { _redirectRepository.DeleteRedirect(id); return new DeleteRedirectResponse() { Success = true }; } - catch(Exception e) + catch (Exception e) { - return new DeleteRedirectResponse() { Success = false, Message = "There was an error deleting the redirect : " + e.Message }; + return new DeleteRedirectResponse() + { Success = false, Message = "There was an error deleting the redirect : " + e.Message }; + } + } + + /// + /// DELETE to delete all redirects + /// + /// Response object detailing success or failure + [HttpDelete] + public DeleteRedirectResponse DeleteAll() + { + try + { + _redirectRepository.DeleteAllRedirects(); + return new DeleteRedirectResponse() { Success = true }; + } + catch (Exception e) + { + return new DeleteRedirectResponse() + { Success = false, Message = "There was an error deleting the redirects : " + e.Message }; } } @@ -102,5 +133,34 @@ public void ClearCache() { _redirectRepository.ClearCache(); } + + /// + /// GET to export simple redirects to CSV + /// + [HttpGet] + public ActionResult ExportRedirects(DataRecordProvider dataRecordProvider) + { + var dataRecordCollectionFile = _importExportFactory.GetDataRecordProvider(dataRecordProvider) + .ExportDataRecordCollection(); + + return dataRecordCollectionFile.AsFileContentResult(); + } + + /// + /// Import redirects from CSV + /// + [HttpPost] + public ImportRedirectsResponse ImportRedirects(bool overwriteMatches) + { + var file = HttpContext.Request.Form.Files.Any() ? HttpContext.Request.Form.Files[0] : null; + if (file is null) return ImportRedirectsResponse.EmptyImportRecordResponse(); + if (!file.CanGetDataRecordProviderFromFile(out var provider)) + return ImportRedirectsResponse.EmptyImportRecordResponse( + "No redirects imported, provided file is not supported by the import process. Please provide a .csv or .xlsx file."); + + var response = _importExportFactory.GetDataRecordProvider(provider) + .ImportRedirectsFromCollection(file, overwriteMatches); + return response; + } } } \ No newline at end of file diff --git a/source/SimpleRedirects.Core/RedirectRepository.cs b/source/SimpleRedirects.Core/RedirectRepository.cs index 8651b53..4132998 100644 --- a/source/SimpleRedirects.Core/RedirectRepository.cs +++ b/source/SimpleRedirects.Core/RedirectRepository.cs @@ -1,9 +1,13 @@ using System; using System.Collections.Generic; +using System.Globalization; +using System.IO; using System.Linq; using System.Linq.Expressions; using System.Net; using System.Text.RegularExpressions; +using CsvHelper; +using Microsoft.AspNetCore.Http; using SimpleRedirects.Core.Extensions; using SimpleRedirects.Core.Models; using SimpleRedirects.Core.Utilities.Caching; @@ -179,6 +183,15 @@ public void DeleteRedirect(int id) ClearCache(); } + public void DeleteAllRedirects() + { + var allRedirects = GetAllRedirects(); + foreach (var redirect in allRedirects) + { + DeleteRedirect(redirect.Id); + } + } + /// /// Handles finding a redirect based on the oldUrl /// @@ -237,7 +250,7 @@ private Redirect FetchRedirectById(int id, bool fromCache = false) /// /// OldUrl of redirect to find /// Single redirect with matching OldUrl - private Redirect FetchRedirectByOldUrl(string oldUrl, bool fromCache = false) + public Redirect FetchRedirectByOldUrl(string oldUrl, bool fromCache = false) { oldUrl = CleanUrl(oldUrl); return fromCache @@ -302,6 +315,7 @@ private IEnumerable FetchRedirectsFromDbByQuery(Expression(); builder.Services.AddSingleton(); + builder.Services.AddScoped(); + builder.Services.AddScoped() + .AddScoped(s => s.GetService()); + builder.Services.AddScoped() + .AddScoped(s => s.GetService()); + builder.Services.Configure(builder.Config.GetSection( SimpleRedirectsOptions.Position)); - var onlyRedirectOn404 = builder.Config.GetSection(SimpleRedirectsOptions.Position)?.Get()?.OnlyRedirectOn404 ?? false; + var onlyRedirectOn404 = builder.Config.GetSection(SimpleRedirectsOptions.Position) + ?.Get()?.OnlyRedirectOn404 ?? false; - builder.Services.Configure(options => { + builder.Services.Configure(options => + { options.AddFilter(new UmbracoPipelineFilter( "SimpleRedirects", applicationBuilder => @@ -36,7 +45,8 @@ public void Compose(IUmbracoBuilder builder) if (!onlyRedirectOn404) applicationBuilder.UseMiddleware(); }, - applicationBuilder => { + applicationBuilder => + { if (onlyRedirectOn404) applicationBuilder.UseMiddleware(); }, diff --git a/source/SimpleRedirects.Core/Services/CsvImportExportService.cs b/source/SimpleRedirects.Core/Services/CsvImportExportService.cs new file mode 100644 index 0000000..fa8ce67 --- /dev/null +++ b/source/SimpleRedirects.Core/Services/CsvImportExportService.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using CsvHelper; +using Microsoft.AspNetCore.Http; +using SimpleRedirects.Core.Enums; +using SimpleRedirects.Core.Models; + +namespace SimpleRedirects.Core.Services; + +public class CsvImportExportService : IImportExportService +{ + private readonly RedirectRepository _redirectRepository; + + public CsvImportExportService(RedirectRepository redirectRepository) + { + _redirectRepository = redirectRepository; + } + + public DataRecordCollectionFile ExportDataRecordCollection() + { + var records = _redirectRepository.GetAllRedirects(); + using var memoryStream = new MemoryStream(); + using var streamWriter = new StreamWriter(memoryStream); + using var csvWriter = new CsvWriter(streamWriter, CultureInfo.InvariantCulture); + csvWriter.Context.RegisterClassMap(); + csvWriter.WriteHeader(); + csvWriter.NextRecord(); + csvWriter.WriteRecords(records); + csvWriter.Flush(); + streamWriter.Flush(); + memoryStream.Position = 0; + + return new DataRecordCollectionFile(DataRecordProvider.Csv, memoryStream.ToArray()); + } + + public ImportRedirectsResponse ImportRedirectsFromCollection(IFormFile file, bool overwriteMatches) + { + using var reader = new StreamReader(file.OpenReadStream()); + using var csv = new CsvReader(reader, CultureInfo.InvariantCulture); + csv.Context.RegisterClassMap(); + var records = csv.GetRecords().ToArray(); + + if (!records.Any()) return ImportRedirectsResponse.EmptyImportRecordResponse(); + var addedRedirects = 0; + var updatedRedirects = 0; + var existingRedirects = 0; + var errorList = new List(); + + foreach (var redirect in records) + { + if (_redirectRepository.FetchRedirectByOldUrl(redirect.OldUrl) is not { } existingRedirect) + { + try + { + _redirectRepository.AddRedirect(redirect.IsRegex, redirect.OldUrl, redirect.NewUrl, redirect.RedirectCode, redirect.Notes); + addedRedirects++; + } + catch (ArgumentException e) + { + redirect.Notes = e.Message; + errorList.Add(redirect); + } + } + else if(overwriteMatches && !existingRedirect.Equals(redirect)) + { + redirect.Id = existingRedirect.Id; + try + { + _redirectRepository.UpdateRedirect(redirect); + updatedRedirects++; + } + catch (ArgumentException e) + { + redirect.Notes = e.Message; + errorList.Add(redirect); + } + } + else + existingRedirects++; + } + + return ImportRedirectsResponse.FromImport(addedRedirects, updatedRedirects, existingRedirects, errorList.ToArray()); + } +} \ No newline at end of file diff --git a/source/SimpleRedirects.Core/Services/ExcelImportExportService.cs b/source/SimpleRedirects.Core/Services/ExcelImportExportService.cs new file mode 100644 index 0000000..42c2cd0 --- /dev/null +++ b/source/SimpleRedirects.Core/Services/ExcelImportExportService.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using CsvHelper; +using CsvHelper.Excel; +using Microsoft.AspNetCore.Http; +using SimpleRedirects.Core.Enums; +using SimpleRedirects.Core.Models; + +namespace SimpleRedirects.Core.Services; + +public class ExcelImportExportService : IImportExportService +{ + private readonly RedirectRepository _redirectRepository; + + public ExcelImportExportService(RedirectRepository redirectRepository) + { + _redirectRepository = redirectRepository; + } + + public DataRecordCollectionFile ExportDataRecordCollection() + { + var records = _redirectRepository.GetAllRedirects(); + using var memoryStream = new MemoryStream(); + using (var excelWriter = new ExcelWriter(memoryStream, "Redirect list", CultureInfo.InvariantCulture)) + { + excelWriter.Context.RegisterClassMap(); + excelWriter.WriteHeader(); + excelWriter.NextRecord(); + excelWriter.WriteRecords(records); + } + + return new DataRecordCollectionFile(DataRecordProvider.Excel, memoryStream.ToArray()); + } + + public ImportRedirectsResponse ImportRedirectsFromCollection(IFormFile file, bool overwriteMatches) + { + if (file.Length <= 0) return ImportRedirectsResponse.EmptyImportRecordResponse(); + using var memoryStream = new MemoryStream(); + file.CopyTo(memoryStream); + using var excelParser = new ExcelParser(memoryStream, CultureInfo.InvariantCulture); + using var csvReader = new CsvReader(excelParser); + csvReader.Context.RegisterClassMap(); + var records = csvReader.GetRecords().ToArray(); + if (!records.Any()) return ImportRedirectsResponse.EmptyImportRecordResponse(); + var addedRedirects = 0; + var updatedRedirects = 0; + var existingRedirects = 0; + var errorList = new List(); + + foreach (var redirect in records) + { + if (_redirectRepository.FetchRedirectByOldUrl(redirect.OldUrl) is not { } existingRedirect) + { + try + { + _redirectRepository.AddRedirect(redirect.IsRegex, redirect.OldUrl, redirect.NewUrl, redirect.RedirectCode, redirect.Notes); + addedRedirects++; + } + catch (ArgumentException e) + { + redirect.Notes = e.Message; + errorList.Add(redirect); + } + } + else if(overwriteMatches && !existingRedirect.Equals(redirect)) + { + redirect.Id = existingRedirect.Id; + try + { + _redirectRepository.UpdateRedirect(redirect); + updatedRedirects++; + } + catch (ArgumentException e) + { + redirect.Notes = e.Message; + errorList.Add(redirect); + } + } + else + existingRedirects++; + } + + return ImportRedirectsResponse.FromImport(addedRedirects, updatedRedirects, existingRedirects, errorList.ToArray()); + } +} \ No newline at end of file diff --git a/source/SimpleRedirects.Core/Services/IImportExportService.cs b/source/SimpleRedirects.Core/Services/IImportExportService.cs new file mode 100644 index 0000000..2c2dfb9 --- /dev/null +++ b/source/SimpleRedirects.Core/Services/IImportExportService.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Http; +using SimpleRedirects.Core.Models; + +namespace SimpleRedirects.Core.Services; + +public interface IImportExportService +{ + DataRecordCollectionFile ExportDataRecordCollection(); + ImportRedirectsResponse ImportRedirectsFromCollection(IFormFile file, bool overwriteMatches); +} \ No newline at end of file diff --git a/source/SimpleRedirects.Core/Services/ImportExportFactory.cs b/source/SimpleRedirects.Core/Services/ImportExportFactory.cs new file mode 100644 index 0000000..0702fb9 --- /dev/null +++ b/source/SimpleRedirects.Core/Services/ImportExportFactory.cs @@ -0,0 +1,19 @@ +using System; +using SimpleRedirects.Core.Enums; + +namespace SimpleRedirects.Core.Services; + +public class ImportExportFactory +{ + private readonly IServiceProvider _serviceProvider; + + public ImportExportFactory(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public IImportExportService GetDataRecordProvider(DataRecordProvider dataRecordProvider) + => dataRecordProvider == DataRecordProvider.Csv + ? (IImportExportService)_serviceProvider.GetService(typeof(CsvImportExportService)) + : (IImportExportService)_serviceProvider.GetService(typeof(ExcelImportExportService)); +} \ No newline at end of file diff --git a/source/SimpleRedirects.Core/SimpleRedirects.Core.csproj b/source/SimpleRedirects.Core/SimpleRedirects.Core.csproj index bbbf74c..27573ff 100644 --- a/source/SimpleRedirects.Core/SimpleRedirects.Core.csproj +++ b/source/SimpleRedirects.Core/SimpleRedirects.Core.csproj @@ -13,6 +13,8 @@ https://raw.githubusercontent.com/patrickdemooij9/SimpleRedirects/v9/main/package/simpleRedirectsLogo.png + + diff --git a/source/SimpleRedirects.Site/App_Plugins/SimpleRedirects/Lang/en.xml b/source/SimpleRedirects.Site/App_Plugins/SimpleRedirects/Lang/en.xml index b930345..33460f8 100644 --- a/source/SimpleRedirects.Site/App_Plugins/SimpleRedirects/Lang/en.xml +++ b/source/SimpleRedirects.Site/App_Plugins/SimpleRedirects/Lang/en.xml @@ -3,4 +3,8 @@ Redirects + + Export to CSV + Export to Excel (.xlsx) + diff --git a/source/SimpleRedirects.Site/App_Plugins/SimpleRedirects/app.html b/source/SimpleRedirects.Site/App_Plugins/SimpleRedirects/app.html index de7efbe..320a58f 100644 --- a/source/SimpleRedirects.Site/App_Plugins/SimpleRedirects/app.html +++ b/source/SimpleRedirects.Site/App_Plugins/SimpleRedirects/app.html @@ -9,13 +9,28 @@

Simple Redirects Manager

- - + +
+ +
+ + +
Cache Cleared!
- + + +
+ +
+ + +
+ +
+

Add, Update, and Delete redirects in the table below.

@@ -93,6 +108,8 @@

{{redirects.length}} rows

+ +