diff --git a/frontend/src/Activity/History/HistoryRow.js b/frontend/src/Activity/History/HistoryRow.js index 620f87997..f3ad1d9c9 100644 --- a/frontend/src/Activity/History/HistoryRow.js +++ b/frontend/src/Activity/History/HistoryRow.js @@ -128,7 +128,7 @@ class HistoryRow extends Component { ); } - if (name === 'episodeTitle') { + if (name === 'episodes.title') { return ( { @@ -206,7 +206,7 @@ class QueueRow extends Component { ); } - if (name === 'episode.airDateUtc') { + if (name === 'episodes.airDateUtc') { if (episode) { return ( @@ -99,7 +98,6 @@ class SecuritySettings extends Component { @@ -114,7 +112,6 @@ class SecuritySettings extends Component { diff --git a/frontend/src/Store/Actions/Settings/qualityDefinitions.js b/frontend/src/Store/Actions/Settings/qualityDefinitions.js index ef5d0a757..c8202f02a 100644 --- a/frontend/src/Store/Actions/Settings/qualityDefinitions.js +++ b/frontend/src/Store/Actions/Settings/qualityDefinitions.js @@ -78,7 +78,9 @@ export default { const promise = createAjaxRequest({ method: 'PUT', url: '/qualityDefinition/update', - data: JSON.stringify(upatedDefinitions) + data: JSON.stringify(upatedDefinitions), + contentType: 'application/json', + dataType: 'json' }).request; promise.done((data) => { diff --git a/frontend/src/Store/Actions/addSeriesActions.js b/frontend/src/Store/Actions/addSeriesActions.js index 0b7c160e3..c912abb2b 100644 --- a/frontend/src/Store/Actions/addSeriesActions.js +++ b/frontend/src/Store/Actions/addSeriesActions.js @@ -125,6 +125,7 @@ export const actionHandlers = handleThunks({ const promise = createAjaxRequest({ url: '/series', method: 'POST', + dataType: 'json', contentType: 'application/json', data: JSON.stringify(newSeries) }).request; diff --git a/frontend/src/Store/Actions/blocklistActions.js b/frontend/src/Store/Actions/blocklistActions.js index 0cc1a6d3a..088d8989e 100644 --- a/frontend/src/Store/Actions/blocklistActions.js +++ b/frontend/src/Store/Actions/blocklistActions.js @@ -151,6 +151,7 @@ export const actionHandlers = handleThunks({ url: '/blocklist/bulk', method: 'DELETE', dataType: 'json', + contentType: 'application/json', data: JSON.stringify({ ids }) }).request; diff --git a/frontend/src/Store/Actions/commandActions.js b/frontend/src/Store/Actions/commandActions.js index 63c5e2a3d..945273da2 100644 --- a/frontend/src/Store/Actions/commandActions.js +++ b/frontend/src/Store/Actions/commandActions.js @@ -145,7 +145,8 @@ export function executeCommandHelper(payload, dispatch) { const promise = createAjaxRequest({ url: '/command', method: 'POST', - data: JSON.stringify(requestPayload) + data: JSON.stringify(requestPayload), + dataType: 'json' }).request; return promise.then((data) => { diff --git a/frontend/src/Store/Actions/historyActions.js b/frontend/src/Store/Actions/historyActions.js index 9fc10e1b7..20cf5d1c1 100644 --- a/frontend/src/Store/Actions/historyActions.js +++ b/frontend/src/Store/Actions/historyActions.js @@ -47,7 +47,7 @@ export const defaultState = { isVisible: true }, { - name: 'episodeTitle', + name: 'episodes.title', label: 'Episode Title', isVisible: true }, diff --git a/frontend/src/Store/Actions/queueActions.js b/frontend/src/Store/Actions/queueActions.js index cfca4bbe5..4df97769d 100644 --- a/frontend/src/Store/Actions/queueActions.js +++ b/frontend/src/Store/Actions/queueActions.js @@ -75,13 +75,13 @@ export const defaultState = { isVisible: true }, { - name: 'episode.title', + name: 'episodes.title', label: 'Episode Title', isSortable: true, isVisible: true }, { - name: 'episode.airDateUtc', + name: 'episodes.airDateUtc', label: 'Episode Air Date', isSortable: true, isVisible: false @@ -406,6 +406,7 @@ export const actionHandlers = handleThunks({ url: `/queue/bulk?removeFromClient=${remove}&blocklist=${blocklist}`, method: 'DELETE', dataType: 'json', + contentType: 'application/json', data: JSON.stringify({ ids }) }).request; diff --git a/frontend/src/Store/Actions/releaseActions.js b/frontend/src/Store/Actions/releaseActions.js index 14861fc62..8317d9285 100644 --- a/frontend/src/Store/Actions/releaseActions.js +++ b/frontend/src/Store/Actions/releaseActions.js @@ -279,6 +279,7 @@ export const actionHandlers = handleThunks({ const promise = createAjaxRequest({ url: '/release', method: 'POST', + dataType: 'json', contentType: 'application/json', data: JSON.stringify(payload) }).request; diff --git a/frontend/src/Store/Actions/seriesHistoryActions.js b/frontend/src/Store/Actions/seriesHistoryActions.js index 6c8b02d32..59c7f9d8e 100644 --- a/frontend/src/Store/Actions/seriesHistoryActions.js +++ b/frontend/src/Store/Actions/seriesHistoryActions.js @@ -78,11 +78,9 @@ export const actionHandlers = handleThunks({ } = payload; const promise = createAjaxRequest({ - url: '/history/failed', + url: `/history/failed/${historyId}`, method: 'POST', - data: { - id: historyId - } + dataType: 'json' }).request; promise.done(() => { diff --git a/frontend/src/Store/Actions/tagActions.js b/frontend/src/Store/Actions/tagActions.js index 5389b1a6b..f74c95067 100644 --- a/frontend/src/Store/Actions/tagActions.js +++ b/frontend/src/Store/Actions/tagActions.js @@ -53,7 +53,8 @@ export const actionHandlers = handleThunks({ const promise = createAjaxRequest({ url: '/tag', method: 'POST', - data: JSON.stringify(payload.tag) + data: JSON.stringify(payload.tag), + dataType: 'json' }).request; promise.done((data) => { diff --git a/frontend/src/Store/Actions/wantedActions.js b/frontend/src/Store/Actions/wantedActions.js index 3d1d19cc5..69e8bc249 100644 --- a/frontend/src/Store/Actions/wantedActions.js +++ b/frontend/src/Store/Actions/wantedActions.js @@ -21,7 +21,7 @@ export const defaultState = { isFetching: false, isPopulated: false, pageSize: 20, - sortKey: 'airDateUtc', + sortKey: 'episodes.airDateUtc', sortDirection: sortDirections.DESCENDING, error: null, items: [], @@ -39,12 +39,12 @@ export const defaultState = { isVisible: true }, { - name: 'episodeTitle', + name: 'episodes.title', label: 'Episode Title', isVisible: true }, { - name: 'airDateUtc', + name: 'episodes.airDateUtc', label: 'Air Date', isSortable: true, isVisible: true @@ -94,7 +94,7 @@ export const defaultState = { isFetching: false, isPopulated: false, pageSize: 20, - sortKey: 'airDateUtc', + sortKey: 'episodes.airDateUtc', sortDirection: sortDirections.DESCENDING, items: [], @@ -111,12 +111,12 @@ export const defaultState = { isVisible: true }, { - name: 'episodeTitle', + name: 'episodes.episodeTitle', label: 'Episode Title', isVisible: true }, { - name: 'airDateUtc', + name: 'episodes.airDateUtc', label: 'Air Date', isSortable: true, isVisible: true diff --git a/frontend/src/Utilities/createAjaxRequest.js b/frontend/src/Utilities/createAjaxRequest.js index 1350a4ef9..17e46ac3c 100644 --- a/frontend/src/Utilities/createAjaxRequest.js +++ b/frontend/src/Utilities/createAjaxRequest.js @@ -7,18 +7,6 @@ function isRelative(ajaxOptions) { return !absUrlRegex.test(ajaxOptions.url); } -function moveBodyToQuery(ajaxOptions) { - if (ajaxOptions.data && ajaxOptions.type === 'DELETE') { - if (ajaxOptions.url.contains('?')) { - ajaxOptions.url += '&'; - } else { - ajaxOptions.url += '?'; - } - ajaxOptions.url += $.param(ajaxOptions.data); - delete ajaxOptions.data; - } -} - function addRootUrl(ajaxOptions) { ajaxOptions.url = apiRoot + ajaxOptions.url; } @@ -32,7 +20,7 @@ function addContentType(ajaxOptions) { if ( ajaxOptions.contentType == null && ajaxOptions.dataType === 'json' && - (ajaxOptions.method === 'PUT' || ajaxOptions.method === 'POST')) { + (ajaxOptions.method === 'PUT' || ajaxOptions.method === 'POST' || ajaxOptions.method === 'DELETE')) { ajaxOptions.contentType = 'application/json'; } } @@ -49,10 +37,9 @@ export default function createAjaxRequest(originalAjaxOptions) { } } - const ajaxOptions = { dataType: 'json', ...originalAjaxOptions }; + const ajaxOptions = { ...originalAjaxOptions }; if (isRelative(ajaxOptions)) { - moveBodyToQuery(ajaxOptions); addRootUrl(ajaxOptions); addApiKey(ajaxOptions); addContentType(ajaxOptions); diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js index de0b0e161..3fadf89c5 100644 --- a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js @@ -83,7 +83,7 @@ function CutoffUnmetRow(props) { ); } - if (name === 'episodeTitle') { + if (name === 'episodes.title') { return ( ("{ \"ignored\": null, \"required\": null }"); + var resource = STJson.Deserialize("{ \"ignored\": null, \"required\": null }"); var model = resource.ToModel(); @@ -28,7 +28,7 @@ namespace NzbDrone.Api.Test.v3.ReleaseProfiles [Test] public void should_deserialize_releaseprofile_v3_ignored_string() { - var resource = Json.Deserialize("{ \"ignored\": \"testa,testb\", \"required\": \"testc,testd\" }"); + var resource = STJson.Deserialize("{ \"ignored\": \"testa,testb\", \"required\": \"testc,testd\" }"); var model = resource.ToModel(); @@ -39,7 +39,7 @@ namespace NzbDrone.Api.Test.v3.ReleaseProfiles [Test] public void should_deserialize_releaseprofile_v3_ignored_string_array() { - var resource = Json.Deserialize("{ \"ignored\": [ \"testa\", \"testb\" ], \"required\": [ \"testc\", \"testd\" ] }"); + var resource = STJson.Deserialize("{ \"ignored\": [ \"testa\", \"testb\" ], \"required\": [ \"testc\", \"testd\" ] }"); var model = resource.ToModel(); @@ -50,7 +50,7 @@ namespace NzbDrone.Api.Test.v3.ReleaseProfiles [Test] public void should_throw_with_bad_releaseprofile_v3_ignored_type() { - var resource = Json.Deserialize("{ \"ignored\": {} }"); + var resource = STJson.Deserialize("{ \"ignored\": {} }"); Assert.Throws(() => resource.ToModel()); } diff --git a/src/NzbDrone.Common.Test/InstrumentationTests/SentryTargetFixture.cs b/src/NzbDrone.Common.Test/InstrumentationTests/SentryTargetFixture.cs index 75c6f0b52..7392c3b85 100644 --- a/src/NzbDrone.Common.Test/InstrumentationTests/SentryTargetFixture.cs +++ b/src/NzbDrone.Common.Test/InstrumentationTests/SentryTargetFixture.cs @@ -20,8 +20,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests private static Exception[] FilteredExceptions = new Exception[] { - new UnauthorizedAccessException(), - new TinyIoC.TinyIoCResolutionException(typeof(string)) + new UnauthorizedAccessException() }; [SetUp] diff --git a/src/NzbDrone.Common.Test/ServiceFactoryFixture.cs b/src/NzbDrone.Common.Test/ServiceFactoryFixture.cs index 9dfdde396..51b39189e 100644 --- a/src/NzbDrone.Common.Test/ServiceFactoryFixture.cs +++ b/src/NzbDrone.Common.Test/ServiceFactoryFixture.cs @@ -1,8 +1,13 @@ using System.Linq; +using DryIoc; +using DryIoc.Microsoft.DependencyInjection; using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; +using NzbDrone.Common.Composition.Extensions; using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Core.Datastore; +using NzbDrone.Common.Instrumentation.Extensions; +using NzbDrone.Core.Datastore.Extensions; using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Messaging.Events; using NzbDrone.Host; @@ -16,12 +21,16 @@ namespace NzbDrone.Common.Test [Test] public void event_handlers_should_be_unique() { - var container = MainAppContainerBuilder.BuildContainer(new StartupContext()); - container.Register(new MainDatabase(null)); - container.Register(new LogDatabase(null)); - container.Resolve().Register(); + var container = new Container(rules => rules.WithNzbDroneRules()) + .AddNzbDroneLogger() + .AutoAddServices(Bootstrap.ASSEMBLIES) + .AddDummyDatabase() + .AddStartupContext(new StartupContext("first", "second")) + .GetServiceProvider(); - Mocker.SetConstant(container); + container.GetRequiredService().Register(); + + Mocker.SetConstant(container); var handlers = Subject.BuildAll>() .Select(c => c.GetType().FullName); diff --git a/src/NzbDrone.Common/Composition/AssemblyLoader.cs b/src/NzbDrone.Common/Composition/AssemblyLoader.cs new file mode 100644 index 000000000..2d01967a0 --- /dev/null +++ b/src/NzbDrone.Common/Composition/AssemblyLoader.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Runtime.Loader; +using NzbDrone.Common.EnvironmentInfo; + +namespace NzbDrone.Common.Composition +{ + public class AssemblyLoader + { + static AssemblyLoader() + { + AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(ContainerResolveEventHandler); + } + + public static IEnumerable Load(IEnumerable assemblyNames) + { + var toLoad = assemblyNames.ToList(); + toLoad.Add("Sonarr.Common"); + toLoad.Add(OsInfo.IsWindows ? "Sonarr.Windows" : "Sonarr.Mono"); + + var toRegisterResolver = new List { "System.Data.SQLite" }; + toRegisterResolver.AddRange(assemblyNames.Intersect(new[] { "Sonarr.Core" })); + RegisterNativeResolver(toRegisterResolver); + + var startupPath = AppDomain.CurrentDomain.BaseDirectory; + + return toLoad.Select(x => + AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.Combine(startupPath, $"{x}.dll"))); + } + + private static Assembly ContainerResolveEventHandler(object sender, ResolveEventArgs args) + { + var resolver = new AssemblyDependencyResolver(args.RequestingAssembly.Location); + var assemblyPath = resolver.ResolveAssemblyToPath(new AssemblyName(args.Name)); + + if (assemblyPath == null) + { + return null; + } + + return AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath); + } + + public static void RegisterNativeResolver(IEnumerable assemblyNames) + { + foreach (var name in assemblyNames) + { + // This ensures we look for sqlite3 using libsqlite3.so.0 on Linux and not libsqlite3.so which + // is less likely to exist. + var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath( + Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"{name}.dll")); + + try + { + NativeLibrary.SetDllImportResolver(assembly, LoadNativeLib); + } + catch (InvalidOperationException) + { + // This can only be set once per assembly + // Catch required for NzbDrone.Host tests + } + } + } + + private static IntPtr LoadNativeLib(string libraryName, Assembly assembly, DllImportSearchPath? dllImportSearchPath) + { + var mappedName = libraryName; + if (OsInfo.IsLinux) + { + if (libraryName == "sqlite3") + { + mappedName = "libsqlite3.so.0"; + } + else if (libraryName == "mediainfo") + { + mappedName = "libmediainfo.so.0"; + } + } + + return NativeLibrary.Load(mappedName, assembly, dllImportSearchPath); + } + } +} diff --git a/src/NzbDrone.Common/Composition/Container.cs b/src/NzbDrone.Common/Composition/Container.cs deleted file mode 100644 index 6ae1e4bfa..000000000 --- a/src/NzbDrone.Common/Composition/Container.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using TinyIoC; - -namespace NzbDrone.Common.Composition -{ - public class Container : IContainer - { - private readonly TinyIoCContainer _container; - private readonly List _loadedTypes; - - public Container(TinyIoCContainer container, List loadedTypes) - { - _container = container; - _loadedTypes = loadedTypes; - _container.Register(this); - } - - public void Register() - where TImplementation : class, TService - where TService : class - { - _container.Register(); - } - - public void Register(T instance) - where T : class - { - _container.Register(instance); - } - - public T Resolve() - where T : class - { - return _container.Resolve(); - } - - public object Resolve(Type type) - { - return _container.Resolve(type); - } - - public void Register(Type serviceType, Type implementationType) - { - _container.Register(serviceType, implementationType); - } - - public void Register(Func factory) - where TService : class - { - _container.Register((c, n) => factory(this)); - } - - public void RegisterSingleton(Type service, Type implementation) - { - var factory = CreateSingletonImplementationFactory(implementation); - - // For Resolve and ResolveAll - _container.Register(service, factory); - - // For ctor(IEnumerable) - var enumerableType = typeof(IEnumerable<>).MakeGenericType(service); - _container.Register(enumerableType, (c, p) => - { - var instance = factory(c, p); - var result = Array.CreateInstance(service, 1); - result.SetValue(instance, 0); - return result; - }); - } - - public IEnumerable ResolveAll() - where T : class - { - return _container.ResolveAll(); - } - - public void RegisterAllAsSingleton(Type service, IEnumerable implementationList) - { - foreach (var implementation in implementationList) - { - var factory = CreateSingletonImplementationFactory(implementation); - - // For ResolveAll and ctor(IEnumerable) - _container.Register(service, factory, implementation.FullName); - } - } - - private Func CreateSingletonImplementationFactory(Type implementation) - { - const string singleImplPrefix = "singleImpl_"; - - _container.Register(implementation, implementation, singleImplPrefix + implementation.FullName).AsSingleton(); - - return (c, p) => _container.Resolve(implementation, singleImplPrefix + implementation.FullName); - } - - public bool IsTypeRegistered(Type type) - { - return _container.CanResolve(type); - } - - public IEnumerable GetImplementations(Type contractType) - { - return _loadedTypes - .Where(implementation => - contractType.IsAssignableFrom(implementation) && - !implementation.IsInterface && - !implementation.IsAbstract); - } - } -} diff --git a/src/NzbDrone.Common/Composition/ContainerBuilderBase.cs b/src/NzbDrone.Common/Composition/ContainerBuilderBase.cs deleted file mode 100644 index c3ce47940..000000000 --- a/src/NzbDrone.Common/Composition/ContainerBuilderBase.cs +++ /dev/null @@ -1,136 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Runtime.Loader; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Common.Messaging; -using TinyIoC; - -namespace NzbDrone.Common.Composition -{ - public abstract class ContainerBuilderBase - { - private readonly List _loadedTypes; - - protected IContainer Container { get; } - - protected ContainerBuilderBase(IStartupContext args, List assemblies) - { - _loadedTypes = new List(); - - assemblies.Add(OsInfo.IsWindows ? "Sonarr.Windows" : "Sonarr.Mono"); - assemblies.Add("Sonarr.Common"); - - var startupPath = AppDomain.CurrentDomain.BaseDirectory; - - foreach (var assemblyName in assemblies) - { - _loadedTypes.AddRange(AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.Combine(startupPath, $"{assemblyName}.dll")).GetExportedTypes()); - } - - var toRegisterResolver = new List { "System.Data.SQLite" }; - toRegisterResolver.AddRange(assemblies.Intersect(new[] { "Sonarr.Core" })); - RegisterNativeResolver(toRegisterResolver); - AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(ContainerResolveEventHandler); - - Container = new Container(new TinyIoCContainer(), _loadedTypes); - AutoRegisterInterfaces(); - Container.Register(args); - } - - private static Assembly ContainerResolveEventHandler(object sender, ResolveEventArgs args) - { - var resolver = new AssemblyDependencyResolver(args.RequestingAssembly.Location); - var assemblyPath = resolver.ResolveAssemblyToPath(new AssemblyName(args.Name)); - - if (assemblyPath == null) - { - return null; - } - - return AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath); - } - - public static void RegisterNativeResolver(IEnumerable assemblyNames) - { - // This ensures we look for sqlite3 using libsqlite3.so.0 on Linux and not libsqlite3.so which - // is less likely to exist. - foreach (var name in assemblyNames) - { - var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath( - Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"{name}.dll")); - - try - { - NativeLibrary.SetDllImportResolver(assembly, LoadNativeLib); - } - catch (InvalidOperationException) - { - // This can only be set once per assembly - // Catch required for NzbDrone.Host tests - } - } - } - - private static IntPtr LoadNativeLib(string libraryName, Assembly assembly, DllImportSearchPath? dllImportSearchPath) - { - var mappedName = libraryName; - if (OsInfo.IsLinux) - { - if (libraryName == "sqlite3") - { - mappedName = "libsqlite3.so.0"; - } - else if (libraryName == "mediainfo") - { - mappedName = "libmediainfo.so.0"; - } - } - - return NativeLibrary.Load(mappedName, assembly, dllImportSearchPath); - } - - private void AutoRegisterInterfaces() - { - var loadedInterfaces = _loadedTypes.Where(t => t.IsInterface).ToList(); - var implementedInterfaces = _loadedTypes.SelectMany(t => t.GetInterfaces()); - - var contracts = loadedInterfaces.Union(implementedInterfaces).Where(c => !c.IsGenericTypeDefinition && !string.IsNullOrWhiteSpace(c.FullName)) - .Where(c => !c.FullName.StartsWith("System")) - .Except(new List { typeof(IMessage), typeof(IEvent), typeof(IContainer) }).Distinct().OrderBy(c => c.FullName); - - foreach (var contract in contracts) - { - AutoRegisterImplementations(contract); - } - } - - protected void AutoRegisterImplementations() - { - AutoRegisterImplementations(typeof(TContract)); - } - - private void AutoRegisterImplementations(Type contractType) - { - var implementations = Container.GetImplementations(contractType).Where(c => !c.IsGenericTypeDefinition).ToList(); - - if (implementations.Count == 0) - { - return; - } - - if (implementations.Count == 1) - { - var impl = implementations.Single(); - Container.RegisterSingleton(contractType, impl); - } - else - { - Container.RegisterAllAsSingleton(contractType, implementations); - } - } - } -} diff --git a/src/NzbDrone.Common/Composition/Extensions.cs b/src/NzbDrone.Common/Composition/Extensions.cs new file mode 100644 index 000000000..52ebd9040 --- /dev/null +++ b/src/NzbDrone.Common/Composition/Extensions.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Linq; +using DryIoc; +using NzbDrone.Common.EnvironmentInfo; + +namespace NzbDrone.Common.Composition.Extensions +{ + public static class ServiceCollectionExtensions + { + public static Rules WithNzbDroneRules(this Rules rules) + { + return rules.WithMicrosoftDependencyInjectionRules() + .WithAutoConcreteTypeResolution() + .WithDefaultReuse(Reuse.Singleton); + } + + public static IContainer AddStartupContext(this IContainer container, StartupContext context) + { + container.RegisterInstance(context, ifAlreadyRegistered: IfAlreadyRegistered.Replace); + return container; + } + + public static IContainer AutoAddServices(this IContainer container, List assemblyNames) + { + var assemblies = AssemblyLoader.Load(assemblyNames); + + container.RegisterMany(assemblies, + serviceTypeCondition: type => type.IsInterface && !string.IsNullOrWhiteSpace(type.FullName) && !type.FullName.StartsWith("System"), + reuse: Reuse.Singleton); + + container.RegisterMany(assemblies, + serviceTypeCondition: type => !type.IsInterface && !string.IsNullOrWhiteSpace(type.FullName) && !type.FullName.StartsWith("System"), + reuse: Reuse.Transient); + + var knownTypes = new KnownTypes(assemblies.SelectMany(x => x.GetTypes()).ToList()); + container.RegisterInstance(knownTypes); + + return container; + } + } +} diff --git a/src/NzbDrone.Common/Composition/IContainer.cs b/src/NzbDrone.Common/Composition/IContainer.cs deleted file mode 100644 index 9e7402017..000000000 --- a/src/NzbDrone.Common/Composition/IContainer.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace NzbDrone.Common.Composition -{ - public interface IContainer - { - void Register(T instance) - where T : class; - - void Register() - where TImplementation : class, TService - where TService : class; - T Resolve() - where T : class; - object Resolve(Type type); - void Register(Type serviceType, Type implementationType); - void Register(Func factory) - where TService : class; - void RegisterSingleton(Type service, Type implementation); - IEnumerable ResolveAll() - where T : class; - void RegisterAllAsSingleton(Type registrationType, IEnumerable implementationList); - bool IsTypeRegistered(Type type); - - IEnumerable GetImplementations(Type contractType); - } -} diff --git a/src/NzbDrone.Common/Composition/KnownTypes.cs b/src/NzbDrone.Common/Composition/KnownTypes.cs new file mode 100644 index 000000000..3f816633f --- /dev/null +++ b/src/NzbDrone.Common/Composition/KnownTypes.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace NzbDrone.Common.Composition +{ + public class KnownTypes + { + private List _knownTypes; + + // So unity can resolve for tests + public KnownTypes() + : this(new List()) + { + } + + public KnownTypes(List loadedTypes) + { + _knownTypes = loadedTypes; + } + + public IEnumerable GetImplementations(Type contractType) + { + return _knownTypes + .Where(implementation => + contractType.IsAssignableFrom(implementation) && + !implementation.IsInterface && + !implementation.IsAbstract); + } + } +} diff --git a/src/NzbDrone.Common/Extensions/PathExtensions.cs b/src/NzbDrone.Common/Extensions/PathExtensions.cs index e46311f39..949d12217 100644 --- a/src/NzbDrone.Common/Extensions/PathExtensions.cs +++ b/src/NzbDrone.Common/Extensions/PathExtensions.cs @@ -286,6 +286,11 @@ namespace NzbDrone.Common.Extensions return appFolderInfo.AppDataFolder; } + public static string GetDataProtectionPath(this IAppFolderInfo appFolderInfo) + { + return Path.Combine(GetAppDataPath(appFolderInfo), "asp"); + } + public static string GetLogFolder(this IAppFolderInfo appFolderInfo) { return Path.Combine(GetAppDataPath(appFolderInfo), "logs"); diff --git a/src/NzbDrone.Common/Instrumentation/Extensions/CompositionExtensions.cs b/src/NzbDrone.Common/Instrumentation/Extensions/CompositionExtensions.cs new file mode 100644 index 000000000..10b7d4af4 --- /dev/null +++ b/src/NzbDrone.Common/Instrumentation/Extensions/CompositionExtensions.cs @@ -0,0 +1,14 @@ +using DryIoc; +using NLog; + +namespace NzbDrone.Common.Instrumentation.Extensions +{ + public static class CompositionExtensions + { + public static IContainer AddNzbDroneLogger(this IContainer container) + { + container.Register(Made.Of(() => LogManager.GetLogger(Arg.Index(0)), r => r.Parent.ImplementationType.Name.ToString()), reuse: Reuse.Transient); + return container; + } + } +} diff --git a/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs b/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs index 585d5a4af..02fd38370 100644 --- a/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs +++ b/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs @@ -42,10 +42,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry "UnauthorizedAccessException", // Filter out people stuck in boot loops - "CorruptDatabaseException", - - // This also filters some people in boot loops - "TinyIoCResolutionException" + "CorruptDatabaseException" }; public static readonly List FilteredExceptionMessages = new List diff --git a/src/NzbDrone.Common/Serializer/System.Text.Json/STJson.cs b/src/NzbDrone.Common/Serializer/System.Text.Json/STJson.cs index 48b98cf6d..6b28a17ac 100644 --- a/src/NzbDrone.Common/Serializer/System.Text.Json/STJson.cs +++ b/src/NzbDrone.Common/Serializer/System.Text.Json/STJson.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Text.Json; using System.Text.Json.Serialization; +using System.Threading.Tasks; namespace NzbDrone.Common.Serializer { @@ -15,23 +16,25 @@ namespace NzbDrone.Common.Serializer public static JsonSerializerOptions GetSerializerSettings() { - var serializerSettings = new JsonSerializerOptions - { - AllowTrailingCommas = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - PropertyNameCaseInsensitive = true, - DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true - }; + var settings = new JsonSerializerOptions(); + ApplySerializerSettings(settings); + return settings; + } + + public static void ApplySerializerSettings(JsonSerializerOptions serializerSettings) + { + serializerSettings.AllowTrailingCommas = true; + serializerSettings.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + serializerSettings.PropertyNameCaseInsensitive = true; + serializerSettings.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase; + serializerSettings.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + serializerSettings.WriteIndented = true; serializerSettings.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, true)); serializerSettings.Converters.Add(new STJVersionConverter()); serializerSettings.Converters.Add(new STJHttpUriConverter()); serializerSettings.Converters.Add(new STJTimeSpanConverter()); serializerSettings.Converters.Add(new STJUtcConverter()); - - return serializerSettings; } public static T Deserialize(string json) @@ -84,5 +87,15 @@ namespace NzbDrone.Common.Serializer JsonSerializer.Serialize(writer, (object)model, options); } } + + public static Task SerializeAsync(TModel model, Stream outputStream, JsonSerializerOptions options = null) + { + if (options == null) + { + options = SerializerSettings; + } + + return JsonSerializer.SerializeAsync(outputStream, (object)model, options); + } } } diff --git a/src/NzbDrone.Common/ServiceFactory.cs b/src/NzbDrone.Common/ServiceFactory.cs index 4f7e4bb0b..e8a0b0dbd 100644 --- a/src/NzbDrone.Common/ServiceFactory.cs +++ b/src/NzbDrone.Common/ServiceFactory.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using NzbDrone.Common.Composition; +using Microsoft.Extensions.DependencyInjection; namespace NzbDrone.Common { @@ -17,9 +17,9 @@ namespace NzbDrone.Common public class ServiceFactory : IServiceFactory { - private readonly IContainer _container; + private readonly System.IServiceProvider _container; - public ServiceFactory(IContainer container) + public ServiceFactory(System.IServiceProvider container) { _container = container; } @@ -27,23 +27,23 @@ namespace NzbDrone.Common public T Build() where T : class { - return _container.Resolve(); + return _container.GetRequiredService(); } public IEnumerable BuildAll() where T : class { - return _container.ResolveAll().GroupBy(c => c.GetType().FullName).Select(g => g.First()); + return _container.GetServices().GroupBy(c => c.GetType().FullName).Select(g => g.First()); } public object Build(Type contract) { - return _container.Resolve(contract); + return _container.GetRequiredService(contract); } public IEnumerable GetImplementations(Type contract) { - return _container.GetImplementations(contract); + return _container.GetServices(contract).Select(x => x.GetType()); } } } diff --git a/src/NzbDrone.Common/Sonarr.Common.csproj b/src/NzbDrone.Common/Sonarr.Common.csproj index b9c8ca4fd..20c9a45dc 100644 --- a/src/NzbDrone.Common/Sonarr.Common.csproj +++ b/src/NzbDrone.Common/Sonarr.Common.csproj @@ -4,10 +4,13 @@ ISMUSL + + + diff --git a/src/NzbDrone.Common/TinyIoC.cs b/src/NzbDrone.Common/TinyIoC.cs deleted file mode 100644 index 97ebe1600..000000000 --- a/src/NzbDrone.Common/TinyIoC.cs +++ /dev/null @@ -1,3780 +0,0 @@ -//=============================================================================== -// TinyIoC -// -// An easy to use, hassle free, Inversion of Control Container for small projects -// and beginners alike. -// -// https://github.com/grumpydev/TinyIoC -//=============================================================================== -// Copyright © Steven Robbins. All rights reserved. -// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY -// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT -// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND -// FITNESS FOR A PARTICULAR PURPOSE. -//=============================================================================== - -#pragma warning disable SX1101, SA1108, SA1119, SA1124, SA1200, SA1208, SA1314, SA1403, SA1503, SA1514, SA1515, SA1519, SX1309 - -#region Preprocessor Directives -// Uncomment this line if you want the container to automatically -// register the TinyMessenger messenger/event aggregator -//#define TINYMESSENGER - -// Preprocessor directives for enabling/disabling functionality -// depending on platform features. If the platform has an appropriate -// #DEFINE then these should be set automatically below. - -#define EXPRESSIONS // Platform supports System.Linq.Expressions -#define COMPILED_EXPRESSIONS // Platform supports compiling expressions -#define APPDOMAIN_GETASSEMBLIES // Platform supports getting all assemblies from the AppDomain object -#define UNBOUND_GENERICS_GETCONSTRUCTORS // Platform supports GetConstructors on unbound generic types -#define GETPARAMETERS_OPEN_GENERICS // Platform supports GetParameters on open generics -#define RESOLVE_OPEN_GENERICS // Platform supports resolving open generics -#define READER_WRITER_LOCK_SLIM // Platform supports ReaderWriterLockSlim - -//// NETFX_CORE -//#if NETFX_CORE -//#endif - -// CompactFramework / Windows Phone 7 -// By default does not support System.Linq.Expressions. -// AppDomain object does not support enumerating all assemblies in the app domain. -#if PocketPC || WINDOWS_PHONE -#undef EXPRESSIONS -#undef COMPILED_EXPRESSIONS -#undef APPDOMAIN_GETASSEMBLIES -#undef UNBOUND_GENERICS_GETCONSTRUCTORS -#endif - -// PocketPC has a bizarre limitation on enumerating parameters on unbound generic methods. -// We need to use a slower workaround in that case. -#if PocketPC -#undef GETPARAMETERS_OPEN_GENERICS -#undef RESOLVE_OPEN_GENERICS -#undef READER_WRITER_LOCK_SLIM -#endif - -#if SILVERLIGHT -#undef APPDOMAIN_GETASSEMBLIES -#endif - -#if NETFX_CORE -#undef APPDOMAIN_GETASSEMBLIES -#undef RESOLVE_OPEN_GENERICS -#endif - -#if COMPILED_EXPRESSIONS -#define USE_OBJECT_CONSTRUCTOR -#endif - -#endregion -namespace TinyIoC -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Reflection; - -#if EXPRESSIONS - using System.Linq.Expressions; - using NLog; - using System.Threading; - -#endif - -#if NETFX_CORE - using System.Threading.Tasks; - using Windows.Storage.Search; - using Windows.Storage; - using Windows.UI.Xaml.Shapes; -#endif - - #region SafeDictionary -#if READER_WRITER_LOCK_SLIM - public class SafeDictionary : IDisposable - { - private readonly ReaderWriterLockSlim _padlock = new ReaderWriterLockSlim(); - private readonly Dictionary _Dictionary = new Dictionary(); - - public TValue this[TKey key] - { - set - { - _padlock.EnterWriteLock(); - - try - { - TValue current; - if (_Dictionary.TryGetValue(key, out current)) - { - var disposable = current as IDisposable; - - if (disposable != null) - disposable.Dispose(); - } - - _Dictionary[key] = value; - } - finally - { - _padlock.ExitWriteLock(); - } - } - } - - public bool TryGetValue(TKey key, out TValue value) - { - _padlock.EnterReadLock(); - try - { - return _Dictionary.TryGetValue(key, out value); - } - finally - { - _padlock.ExitReadLock(); - } - } - - public bool Remove(TKey key) - { - _padlock.EnterWriteLock(); - try - { - return _Dictionary.Remove(key); - } - finally - { - _padlock.ExitWriteLock(); - } - } - - public void Clear() - { - _padlock.EnterWriteLock(); - try - { - _Dictionary.Clear(); - } - finally - { - _padlock.ExitWriteLock(); - } - } - - public IEnumerable Keys - { - get - { - _padlock.EnterReadLock(); - try - { - return new List(_Dictionary.Keys); - } - finally - { - _padlock.ExitReadLock(); - } - } - } - - #region IDisposable Members - - public void Dispose() - { - _padlock.EnterWriteLock(); - - try - { - var disposableItems = from item in _Dictionary.Values - where item is IDisposable - select item as IDisposable; - - foreach (var item in disposableItems) - { - item.Dispose(); - } - } - finally - { - _padlock.ExitWriteLock(); - } - - GC.SuppressFinalize(this); - } - - #endregion - } -#else - public class SafeDictionary : IDisposable - { - private readonly object _Padlock = new object(); - private readonly Dictionary _Dictionary = new Dictionary(); - - public TValue this[TKey key] - { - set - { - lock (_Padlock) - { - TValue current; - if (_Dictionary.TryGetValue(key, out current)) - { - var disposable = current as IDisposable; - - if (disposable != null) - disposable.Dispose(); - } - - _Dictionary[key] = value; - } - } - } - - public bool TryGetValue(TKey key, out TValue value) - { - lock (_Padlock) - { - return _Dictionary.TryGetValue(key, out value); - } - } - - public bool Remove(TKey key) - { - lock (_Padlock) - { - return _Dictionary.Remove(key); - } - } - - public void Clear() - { - lock (_Padlock) - { - _Dictionary.Clear(); - } - } - - public IEnumerable Keys - { - get - { - return _Dictionary.Keys; - } - } - #region IDisposable Members - - public void Dispose() - { - lock (_Padlock) - { - var disposableItems = from item in _Dictionary.Values - where item is IDisposable - select item as IDisposable; - - foreach (var item in disposableItems) - { - item.Dispose(); - } - } - - GC.SuppressFinalize(this); - } - - #endregion - } -#endif - #endregion - - #region Extensions - public static class AssemblyExtensions - { - public static Type[] SafeGetTypes(this Assembly assembly) - { - Type[] assemblies; - - try - { - assemblies = assembly.GetTypes(); - } - catch (System.IO.FileNotFoundException) - { - assemblies = Array.Empty(); - } - catch (NotSupportedException) - { - assemblies = Array.Empty(); - } -#if !NETFX_CORE - catch (ReflectionTypeLoadException e) - { - assemblies = e.Types.Where(t => t != null).ToArray(); - } -#endif - return assemblies; - } - } - - public static class TypeExtensions - { - private static SafeDictionary _genericMethodCache; - - static TypeExtensions() - { - _genericMethodCache = new SafeDictionary(); - } - - //#if NETFX_CORE - // /// - // /// Gets a generic method from a type given the method name, generic types and parameter types - // /// - // /// Source type - // /// Name of the method - // /// Generic types to use to make the method generic - // /// Method parameters - // /// MethodInfo or null if no matches found - // /// - // /// - // public static MethodInfo GetGenericMethod(this Type sourceType, string methodName, Type[] genericTypes, Type[] parameterTypes) - // { - // MethodInfo method; - // var cacheKey = new GenericMethodCacheKey(sourceType, methodName, genericTypes, parameterTypes); - - // // Shouldn't need any additional locking - // // we don't care if we do the method info generation - // // more than once before it gets cached. - // if (!_genericMethodCache.TryGetValue(cacheKey, out method)) - // { - // method = GetMethod(sourceType, methodName, genericTypes, parameterTypes); - // _genericMethodCache[cacheKey] = method; - // } - - // return method; - // } - //#else - /// - /// Gets a generic method from a type given the method name, binding flags, generic types and parameter types - /// - /// Source type - /// Binding flags - /// Name of the method - /// Generic types to use to make the method generic - /// Method parameters - /// MethodInfo or null if no matches found - /// - /// - public static MethodInfo GetGenericMethod(this Type sourceType, BindingFlags bindingFlags, string methodName, Type[] genericTypes, Type[] parameterTypes) - { - MethodInfo method; - var cacheKey = new GenericMethodCacheKey(sourceType, methodName, genericTypes, parameterTypes); - - // Shouldn't need any additional locking - // we don't care if we do the method info generation - // more than once before it gets cached. - if (!_genericMethodCache.TryGetValue(cacheKey, out method)) - { - method = GetMethod(sourceType, bindingFlags, methodName, genericTypes, parameterTypes); - _genericMethodCache[cacheKey] = method; - } - - return method; - } - - //#endif - -#if NETFX_CORE - private static MethodInfo GetMethod(Type sourceType, BindingFlags flags, string methodName, Type[] genericTypes, Type[] parameterTypes) - { - var methods = - sourceType.GetMethods(flags).Where( - mi => string.Equals(methodName, mi.Name, StringComparison.Ordinal)).Where( - mi => mi.ContainsGenericParameters).Where(mi => mi.GetGenericArguments().Length == genericTypes.Length). - Where(mi => mi.GetParameters().Length == parameterTypes.Length).Select( - mi => mi.MakeGenericMethod(genericTypes)).Where( - mi => mi.GetParameters().Select(pi => pi.ParameterType).SequenceEqual(parameterTypes)).ToList(); - - if (methods.Count > 1) - { - throw new AmbiguousMatchException(); - } - - return methods.FirstOrDefault(); - } -#else - private static MethodInfo GetMethod(Type sourceType, BindingFlags bindingFlags, string methodName, Type[] genericTypes, Type[] parameterTypes) - { -#if GETPARAMETERS_OPEN_GENERICS - var methods = - sourceType.GetMethods(bindingFlags).Where( - mi => string.Equals(methodName, mi.Name, StringComparison.Ordinal)).Where( - mi => mi.ContainsGenericParameters).Where(mi => mi.GetGenericArguments().Length == genericTypes.Length). - Where(mi => mi.GetParameters().Length == parameterTypes.Length).Select( - mi => mi.MakeGenericMethod(genericTypes)).Where( - mi => mi.GetParameters().Select(pi => pi.ParameterType).SequenceEqual(parameterTypes)).ToList(); -#else - var validMethods = from method in sourceType.GetMethods(bindingFlags) - where method.Name == methodName - where method.IsGenericMethod - where method.GetGenericArguments().Length == genericTypes.Length - let genericMethod = method.MakeGenericMethod(genericTypes) - where genericMethod.GetParameters().Count() == parameterTypes.Length - where genericMethod.GetParameters().Select(pi => pi.ParameterType).SequenceEqual(parameterTypes) - select genericMethod; - - var methods = validMethods.ToList(); -#endif - if (methods.Count > 1) - { - throw new AmbiguousMatchException(); - } - - return methods.FirstOrDefault(); - } -#endif - - private sealed class GenericMethodCacheKey - { - private readonly Type _sourceType; - - private readonly string _methodName; - - private readonly Type[] _genericTypes; - - private readonly Type[] _parameterTypes; - - private readonly int _hashCode; - - public GenericMethodCacheKey(Type sourceType, string methodName, Type[] genericTypes, Type[] parameterTypes) - { - _sourceType = sourceType; - _methodName = methodName; - _genericTypes = genericTypes; - _parameterTypes = parameterTypes; - _hashCode = GenerateHashCode(); - } - - public override bool Equals(object obj) - { - var cacheKey = obj as GenericMethodCacheKey; - if (cacheKey == null) - return false; - - if (_sourceType != cacheKey._sourceType) - return false; - - if (!string.Equals(_methodName, cacheKey._methodName, StringComparison.Ordinal)) - return false; - - if (_genericTypes.Length != cacheKey._genericTypes.Length) - return false; - - if (_parameterTypes.Length != cacheKey._parameterTypes.Length) - return false; - - for (int i = 0; i < _genericTypes.Length; ++i) - { - if (_genericTypes[i] != cacheKey._genericTypes[i]) - return false; - } - - for (int i = 0; i < _parameterTypes.Length; ++i) - { - if (_parameterTypes[i] != cacheKey._parameterTypes[i]) - return false; - } - - return true; - } - - public override int GetHashCode() - { - return _hashCode; - } - - private int GenerateHashCode() - { - unchecked - { - var result = _sourceType.GetHashCode(); - - result = (result * 397) ^ _methodName.GetHashCode(); - - for (int i = 0; i < _genericTypes.Length; ++i) - { - result = (result * 397) ^ _genericTypes[i].GetHashCode(); - } - - for (int i = 0; i < _parameterTypes.Length; ++i) - { - result = (result * 397) ^ _parameterTypes[i].GetHashCode(); - } - - return result; - } - } - } - } - - // @mbrit - 2012-05-22 - shim for ForEach call on List... -#if NETFX_CORE - internal static class ListExtender - { - internal static void ForEach(this List list, Action callback) - { - foreach (T obj in list) - callback(obj); - } - } -#endif - - #endregion - - #region TinyIoC Exception Types - public class TinyIoCResolutionException : Exception - { - private const string ERROR_TEXT = "Unable to resolve type: {0}"; - - public TinyIoCResolutionException(Type type) - : base(string.Format(ERROR_TEXT, type.FullName)) - { - } - - public TinyIoCResolutionException(Type type, Exception innerException) - : base(string.Format(ERROR_TEXT, type.FullName), innerException) - { - } - } - - public class TinyIoCRegistrationTypeException : Exception - { - private const string REGISTER_ERROR_TEXT = "Cannot register type {0} - abstract classes or interfaces are not valid implementation types for {1}."; - - public TinyIoCRegistrationTypeException(Type type, string factory) - : base(string.Format(REGISTER_ERROR_TEXT, type.FullName, factory)) - { - } - - public TinyIoCRegistrationTypeException(Type type, string factory, Exception innerException) - : base(string.Format(REGISTER_ERROR_TEXT, type.FullName, factory), innerException) - { - } - } - - public class TinyIoCRegistrationException : Exception - { - private const string CONVERT_ERROR_TEXT = "Cannot convert current registration of {0} to {1}"; - private const string GENERIC_CONSTRAINT_ERROR_TEXT = "Type {1} is not valid for a registration of type {0}"; - - public TinyIoCRegistrationException(Type type, string method) - : base(string.Format(CONVERT_ERROR_TEXT, type.FullName, method)) - { - } - - public TinyIoCRegistrationException(Type type, string method, Exception innerException) - : base(string.Format(CONVERT_ERROR_TEXT, type.FullName, method), innerException) - { - } - - public TinyIoCRegistrationException(Type registerType, Type implementationType) - : base(string.Format(GENERIC_CONSTRAINT_ERROR_TEXT, registerType.FullName, implementationType.FullName)) - { - } - - public TinyIoCRegistrationException(Type registerType, Type implementationType, Exception innerException) - : base(string.Format(GENERIC_CONSTRAINT_ERROR_TEXT, registerType.FullName, implementationType.FullName), innerException) - { - } - } - - public class TinyIoCWeakReferenceException : Exception - { - private const string ERROR_TEXT = "Unable to instantiate {0} - referenced object has been reclaimed"; - - public TinyIoCWeakReferenceException(Type type) - : base(string.Format(ERROR_TEXT, type.FullName)) - { - } - - public TinyIoCWeakReferenceException(Type type, Exception innerException) - : base(string.Format(ERROR_TEXT, type.FullName), innerException) - { - } - } - - public class TinyIoCConstructorResolutionException : Exception - { - private const string ERROR_TEXT = "Unable to resolve constructor for {0} using provided Expression."; - - public TinyIoCConstructorResolutionException(Type type) - : base(string.Format(ERROR_TEXT, type.FullName)) - { - } - - public TinyIoCConstructorResolutionException(Type type, Exception innerException) - : base(string.Format(ERROR_TEXT, type.FullName), innerException) - { - } - - public TinyIoCConstructorResolutionException(string message, Exception innerException) - : base(message, innerException) - { - } - - public TinyIoCConstructorResolutionException(string message) - : base(message) - { - } - } - - public class TinyIoCAutoRegistrationException : Exception - { - private const string ERROR_TEXT = "Duplicate implementation of type {0} found ({1})."; - - public TinyIoCAutoRegistrationException(Type registerType, IEnumerable types) - : base(string.Format(ERROR_TEXT, registerType, GetTypesString(types))) - { - } - - public TinyIoCAutoRegistrationException(Type registerType, IEnumerable types, Exception innerException) - : base(string.Format(ERROR_TEXT, registerType, GetTypesString(types)), innerException) - { - } - - private static string GetTypesString(IEnumerable types) - { - var typeNames = from type in types - select type.FullName; - - return string.Join(",", typeNames.ToArray()); - } - } - #endregion - - #region Public Setup / Settings Classes - /// - /// Name/Value pairs for specifying "user" parameters when resolving - /// - public sealed class NamedParameterOverloads : Dictionary - { - public static NamedParameterOverloads FromIDictionary(IDictionary data) - { - return data as NamedParameterOverloads ?? new NamedParameterOverloads(data); - } - - public NamedParameterOverloads() - { - } - - public NamedParameterOverloads(IDictionary data) - : base(data) - { - } - - private static readonly NamedParameterOverloads _Default = new NamedParameterOverloads(); - - public static NamedParameterOverloads Default => _Default; - } - - public enum UnregisteredResolutionActions - { - /// - /// Attempt to resolve type, even if the type isn't registered. - /// - /// Registered types/options will always take precedence. - /// - AttemptResolve, - - /// - /// Fail resolution if type not explicitly registered - /// - Fail, - - /// - /// Attempt to resolve unregistered type if requested type is generic - /// and no registration exists for the specific generic parameters used. - /// - /// Registered types/options will always take precedence. - /// - GenericsOnly - } - - public enum NamedResolutionFailureActions - { - AttemptUnnamedResolution, - Fail - } - - public enum DuplicateImplementationActions - { - RegisterSingle, - RegisterMultiple, - Fail - } - - /// - /// Resolution settings - /// - public sealed class ResolveOptions - { - private static readonly ResolveOptions _Default = new ResolveOptions(); - private static readonly ResolveOptions _FailUnregisteredAndNameNotFound = new ResolveOptions() { NamedResolutionFailureAction = NamedResolutionFailureActions.Fail, UnregisteredResolutionAction = UnregisteredResolutionActions.Fail }; - private static readonly ResolveOptions _FailUnregisteredOnly = new ResolveOptions() { NamedResolutionFailureAction = NamedResolutionFailureActions.AttemptUnnamedResolution, UnregisteredResolutionAction = UnregisteredResolutionActions.Fail }; - private static readonly ResolveOptions _FailNameNotFoundOnly = new ResolveOptions() { NamedResolutionFailureAction = NamedResolutionFailureActions.Fail, UnregisteredResolutionAction = UnregisteredResolutionActions.AttemptResolve }; - - private UnregisteredResolutionActions _UnregisteredResolutionAction = UnregisteredResolutionActions.AttemptResolve; - public UnregisteredResolutionActions UnregisteredResolutionAction - { - get { return _UnregisteredResolutionAction; } - set { _UnregisteredResolutionAction = value; } - } - - private NamedResolutionFailureActions _NamedResolutionFailureAction = NamedResolutionFailureActions.Fail; - public NamedResolutionFailureActions NamedResolutionFailureAction - { - get { return _NamedResolutionFailureAction; } - set { _NamedResolutionFailureAction = value; } - } - - /// - /// Gets the default options (attempt resolution of unregistered types, fail on named resolution if name not found) - /// - public static ResolveOptions Default => _Default; - - /// - /// Preconfigured option for attempting resolution of unregistered types and failing on named resolution if name not found - /// - public static ResolveOptions FailNameNotFoundOnly => _FailNameNotFoundOnly; - - /// - /// Preconfigured option for failing on resolving unregistered types and on named resolution if name not found - /// - public static ResolveOptions FailUnregisteredAndNameNotFound => _FailUnregisteredAndNameNotFound; - - /// - /// Preconfigured option for failing on resolving unregistered types, but attempting unnamed resolution if name not found - /// - public static ResolveOptions FailUnregisteredOnly => _FailUnregisteredOnly; - } - #endregion - - public sealed partial class TinyIoCContainer : IDisposable - { - #region Fake NETFX_CORE Classes -#if NETFX_CORE - private sealed class MethodAccessException : Exception - { - } - - private sealed class AppDomain - { - public static AppDomain CurrentDomain { get; private set; } - - static AppDomain() - { - CurrentDomain = new AppDomain(); - } - - // @mbrit - 2012-05-30 - in WinRT, this should be done async... - public async Task> GetAssembliesAsync() - { - var folder = Windows.ApplicationModel.Package.Current.InstalledLocation; - - List assemblies = new List(); - - var files = await folder.GetFilesAsync(); - - foreach (StorageFile file in files) - { - if (file.FileType == ".dll" || file.FileType == ".exe") - { - AssemblyName name = new AssemblyName() { Name = System.IO.Path.GetFileNameWithoutExtension(file.Name) }; - try - { - var asm = Assembly.Load(name); - assemblies.Add(asm); - } - catch - { - // ignore exceptions here... - } - } - } - - return assemblies; - } - } -#endif - #endregion - - #region "Fluent" API - /// - /// Registration options for "fluent" API - /// - public sealed class RegisterOptions - { - private TinyIoCContainer _Container; - private TypeRegistration _Registration; - - public RegisterOptions(TinyIoCContainer container, TypeRegistration registration) - { - _Container = container; - _Registration = registration; - } - - /// - /// Make registration a singleton (single instance) if possible - /// - /// RegisterOptions - /// - public RegisterOptions AsSingleton() - { - var currentFactory = _Container.GetCurrentFactory(_Registration); - - if (currentFactory == null) - throw new TinyIoCRegistrationException(_Registration.Type, "singleton"); - - return _Container.AddUpdateRegistration(_Registration, currentFactory.SingletonVariant); - } - - /// - /// Make registration multi-instance if possible - /// - /// RegisterOptions - /// - public RegisterOptions AsMultiInstance() - { - var currentFactory = _Container.GetCurrentFactory(_Registration); - - if (currentFactory == null) - throw new TinyIoCRegistrationException(_Registration.Type, "multi-instance"); - - return _Container.AddUpdateRegistration(_Registration, currentFactory.MultiInstanceVariant); - } - - /// - /// Make registration hold a weak reference if possible - /// - /// RegisterOptions - /// - public RegisterOptions WithWeakReference() - { - var currentFactory = _Container.GetCurrentFactory(_Registration); - - if (currentFactory == null) - throw new TinyIoCRegistrationException(_Registration.Type, "weak reference"); - - return _Container.AddUpdateRegistration(_Registration, currentFactory.WeakReferenceVariant); - } - - /// - /// Make registration hold a strong reference if possible - /// - /// RegisterOptions - /// - public RegisterOptions WithStrongReference() - { - var currentFactory = _Container.GetCurrentFactory(_Registration); - - if (currentFactory == null) - throw new TinyIoCRegistrationException(_Registration.Type, "strong reference"); - - return _Container.AddUpdateRegistration(_Registration, currentFactory.StrongReferenceVariant); - } - -#if EXPRESSIONS - public RegisterOptions UsingConstructor(Expression> constructor) - { - var lambda = constructor as LambdaExpression; - if (lambda == null) - throw new TinyIoCConstructorResolutionException(typeof(RegisterType)); - - var newExpression = lambda.Body as NewExpression; - if (newExpression == null) - throw new TinyIoCConstructorResolutionException(typeof(RegisterType)); - - var constructorInfo = newExpression.Constructor; - if (constructorInfo == null) - throw new TinyIoCConstructorResolutionException(typeof(RegisterType)); - - var currentFactory = _Container.GetCurrentFactory(_Registration); - if (currentFactory == null) - throw new TinyIoCConstructorResolutionException(typeof(RegisterType)); - - currentFactory.SetConstructor(constructorInfo); - - return this; - } -#endif - /// - /// Switches to a custom lifetime manager factory if possible. - /// - /// Usually used for RegisterOptions "To*" extension methods such as the ASP.Net per-request one. - /// - /// RegisterOptions instance - /// Custom lifetime manager - /// Error string to display if switch fails - /// RegisterOptions - public static RegisterOptions ToCustomLifetimeManager(RegisterOptions instance, ITinyIoCObjectLifetimeProvider lifetimeProvider, string errorString) - { - if (instance == null) - throw new ArgumentNullException("instance", "instance is null."); - - if (lifetimeProvider == null) - throw new ArgumentNullException("lifetimeProvider", "lifetimeProvider is null."); - - if (string.IsNullOrEmpty(errorString)) - throw new ArgumentException("errorString is null or empty.", "errorString"); - - var currentFactory = instance._Container.GetCurrentFactory(instance._Registration); - - if (currentFactory == null) - throw new TinyIoCRegistrationException(instance._Registration.Type, errorString); - - return instance._Container.AddUpdateRegistration(instance._Registration, currentFactory.GetCustomObjectLifetimeVariant(lifetimeProvider, errorString)); - } - } - - /// - /// Registration options for "fluent" API when registering multiple implementations - /// - public sealed class MultiRegisterOptions - { - private IEnumerable _RegisterOptions; - - /// - /// Initializes a new instance of the MultiRegisterOptions class. - /// - /// Registration options - public MultiRegisterOptions(IEnumerable registerOptions) - { - _RegisterOptions = registerOptions; - } - - /// - /// Make registration a singleton (single instance) if possible - /// - /// RegisterOptions - /// - public MultiRegisterOptions AsSingleton() - { - _RegisterOptions = ExecuteOnAllRegisterOptions(ro => ro.AsSingleton()); - return this; - } - - /// - /// Make registration multi-instance if possible - /// - /// MultiRegisterOptions - /// - public MultiRegisterOptions AsMultiInstance() - { - _RegisterOptions = ExecuteOnAllRegisterOptions(ro => ro.AsMultiInstance()); - return this; - } - - private IEnumerable ExecuteOnAllRegisterOptions(Func action) - { - var newRegisterOptions = new List(); - - foreach (var registerOption in _RegisterOptions) - { - newRegisterOptions.Add(action(registerOption)); - } - - return newRegisterOptions; - } - } - #endregion - - #region Public API - #region Child Containers - public TinyIoCContainer GetChildContainer() - { - return new TinyIoCContainer(this); - } - #endregion - - #region Registration - /// - /// Attempt to automatically register all non-generic classes and interfaces in the current app domain. - /// - /// If more than one class implements an interface then only one implementation will be registered - /// although no error will be thrown. - /// - public void AutoRegister() - { -#if APPDOMAIN_GETASSEMBLIES - AutoRegisterInternal(AppDomain.CurrentDomain.GetAssemblies().Where(a => !IsIgnoredAssembly(a)), DuplicateImplementationActions.RegisterSingle, null); -#else - AutoRegisterInternal(new Assembly[] {this.GetType().Assembly()}, true, null); -#endif - } - - /// - /// Attempt to automatically register all non-generic classes and interfaces in the current app domain. - /// Types will only be registered if they pass the supplied registration predicate. - /// - /// If more than one class implements an interface then only one implementation will be registered - /// although no error will be thrown. - /// - /// Predicate to determine if a particular type should be registered - public void AutoRegister(Func registrationPredicate) - { -#if APPDOMAIN_GETASSEMBLIES - AutoRegisterInternal(AppDomain.CurrentDomain.GetAssemblies().Where(a => !IsIgnoredAssembly(a)), DuplicateImplementationActions.RegisterSingle, registrationPredicate); -#else - AutoRegisterInternal(new Assembly[] { this.GetType().Assembly()}, true, registrationPredicate); -#endif - } - - /// - /// Attempt to automatically register all non-generic classes and interfaces in the current app domain. - /// - /// What action to take when encountering duplicate implementations of an interface/base class. - /// - public void AutoRegister(DuplicateImplementationActions duplicateAction) - { -#if APPDOMAIN_GETASSEMBLIES - AutoRegisterInternal(AppDomain.CurrentDomain.GetAssemblies().Where(a => !IsIgnoredAssembly(a)), duplicateAction, null); -#else - AutoRegisterInternal(new Assembly[] { this.GetType().Assembly() }, ignoreDuplicateImplementations, null); -#endif - } - - /// - /// Attempt to automatically register all non-generic classes and interfaces in the current app domain. - /// Types will only be registered if they pass the supplied registration predicate. - /// - /// What action to take when encountering duplicate implementations of an interface/base class. - /// Predicate to determine if a particular type should be registered - /// - public void AutoRegister(DuplicateImplementationActions duplicateAction, Func registrationPredicate) - { -#if APPDOMAIN_GETASSEMBLIES - AutoRegisterInternal(AppDomain.CurrentDomain.GetAssemblies().Where(a => !IsIgnoredAssembly(a)), duplicateAction, registrationPredicate); -#else - AutoRegisterInternal(new Assembly[] { this.GetType().Assembly() }, ignoreDuplicateImplementations, registrationPredicate); -#endif - } - - /// - /// Attempt to automatically register all non-generic classes and interfaces in the specified assemblies - /// - /// If more than one class implements an interface then only one implementation will be registered - /// although no error will be thrown. - /// - /// Assemblies to process - public void AutoRegister(IEnumerable assemblies) - { - AutoRegisterInternal(assemblies, DuplicateImplementationActions.RegisterSingle, null); - } - - /// - /// Attempt to automatically register all non-generic classes and interfaces in the specified assemblies - /// Types will only be registered if they pass the supplied registration predicate. - /// - /// If more than one class implements an interface then only one implementation will be registered - /// although no error will be thrown. - /// - /// Assemblies to process - /// Predicate to determine if a particular type should be registered - public void AutoRegister(IEnumerable assemblies, Func registrationPredicate) - { - AutoRegisterInternal(assemblies, DuplicateImplementationActions.RegisterSingle, registrationPredicate); - } - - /// - /// Attempt to automatically register all non-generic classes and interfaces in the specified assemblies - /// - /// Assemblies to process - /// What action to take when encountering duplicate implementations of an interface/base class. - /// - public void AutoRegister(IEnumerable assemblies, DuplicateImplementationActions duplicateAction) - { - AutoRegisterInternal(assemblies, duplicateAction, null); - } - - /// - /// Attempt to automatically register all non-generic classes and interfaces in the specified assemblies - /// Types will only be registered if they pass the supplied registration predicate. - /// - /// Assemblies to process - /// What action to take when encountering duplicate implementations of an interface/base class. - /// Predicate to determine if a particular type should be registered - /// - public void AutoRegister(IEnumerable assemblies, DuplicateImplementationActions duplicateAction, Func registrationPredicate) - { - AutoRegisterInternal(assemblies, duplicateAction, registrationPredicate); - } - - /// - /// Creates/replaces a container class registration with default options. - /// - /// Type to register - /// RegisterOptions for fluent API - public RegisterOptions Register(Type registerType) - { - return RegisterInternal(registerType, string.Empty, GetDefaultObjectFactory(registerType, registerType)); - } - - /// - /// Creates/replaces a named container class registration with default options. - /// - /// Type to register - /// Name of registration - /// RegisterOptions for fluent API - public RegisterOptions Register(Type registerType, string name) - { - return RegisterInternal(registerType, name, GetDefaultObjectFactory(registerType, registerType)); - } - - /// - /// Creates/replaces a container class registration with a given implementation and default options. - /// - /// Type to register - /// Type to instantiate that implements RegisterType - /// RegisterOptions for fluent API - public RegisterOptions Register(Type registerType, Type registerImplementation) - { - return this.RegisterInternal(registerType, string.Empty, GetDefaultObjectFactory(registerType, registerImplementation)); - } - - /// - /// Creates/replaces a named container class registration with a given implementation and default options. - /// - /// Type to register - /// Type to instantiate that implements RegisterType - /// Name of registration - /// RegisterOptions for fluent API - public RegisterOptions Register(Type registerType, Type registerImplementation, string name) - { - return this.RegisterInternal(registerType, name, GetDefaultObjectFactory(registerType, registerImplementation)); - } - - /// - /// Creates/replaces a container class registration with a specific, strong referenced, instance. - /// - /// Type to register - /// Instance of RegisterType to register - /// RegisterOptions for fluent API - public RegisterOptions Register(Type registerType, object instance) - { - return RegisterInternal(registerType, string.Empty, new InstanceFactory(registerType, registerType, instance)); - } - - /// - /// Creates/replaces a named container class registration with a specific, strong referenced, instance. - /// - /// Type to register - /// Instance of RegisterType to register - /// Name of registration - /// RegisterOptions for fluent API - public RegisterOptions Register(Type registerType, object instance, string name) - { - return RegisterInternal(registerType, name, new InstanceFactory(registerType, registerType, instance)); - } - - /// - /// Creates/replaces a container class registration with a specific, strong referenced, instance. - /// - /// Type to register - /// Type of instance to register that implements RegisterType - /// Instance of RegisterImplementation to register - /// RegisterOptions for fluent API - public RegisterOptions Register(Type registerType, Type registerImplementation, object instance) - { - return RegisterInternal(registerType, string.Empty, new InstanceFactory(registerType, registerImplementation, instance)); - } - - /// - /// Creates/replaces a named container class registration with a specific, strong referenced, instance. - /// - /// Type to register - /// Type of instance to register that implements RegisterType - /// Instance of RegisterImplementation to register - /// Name of registration - /// RegisterOptions for fluent API - public RegisterOptions Register(Type registerType, Type registerImplementation, object instance, string name) - { - return RegisterInternal(registerType, name, new InstanceFactory(registerType, registerImplementation, instance)); - } - - /// - /// Creates/replaces a container class registration with a user specified factory - /// - /// Type to register - /// Factory/lambda that returns an instance of RegisterType - /// RegisterOptions for fluent API - public RegisterOptions Register(Type registerType, Func factory) - { - return RegisterInternal(registerType, string.Empty, new DelegateFactory(registerType, factory)); - } - - /// - /// Creates/replaces a container class registration with a user specified factory - /// - /// Type to register - /// Factory/lambda that returns an instance of RegisterType - /// Name of registation - /// RegisterOptions for fluent API - public RegisterOptions Register(Type registerType, Func factory, string name) - { - return RegisterInternal(registerType, name, new DelegateFactory(registerType, factory)); - } - - /// - /// Creates/replaces a container class registration with default options. - /// - /// Type to register - /// RegisterOptions for fluent API - public RegisterOptions Register() - where RegisterType : class - { - return this.Register(typeof(RegisterType)); - } - - /// - /// Creates/replaces a named container class registration with default options. - /// - /// Type to register - /// Name of registration - /// RegisterOptions for fluent API - public RegisterOptions Register(string name) - where RegisterType : class - { - return this.Register(typeof(RegisterType), name); - } - - /// - /// Creates/replaces a container class registration with a given implementation and default options. - /// - /// Type to register - /// Type to instantiate that implements RegisterType - /// RegisterOptions for fluent API - public RegisterOptions Register() - where RegisterType : class - where RegisterImplementation : class, RegisterType - { - return this.Register(typeof(RegisterType), typeof(RegisterImplementation)); - } - - /// - /// Creates/replaces a named container class registration with a given implementation and default options. - /// - /// Type to register - /// Type to instantiate that implements RegisterType - /// Name of registration - /// RegisterOptions for fluent API - public RegisterOptions Register(string name) - where RegisterType : class - where RegisterImplementation : class, RegisterType - { - return this.Register(typeof(RegisterType), typeof(RegisterImplementation), name); - } - - /// - /// Creates/replaces a container class registration with a specific, strong referenced, instance. - /// - /// Type to register - /// Instance of RegisterType to register - /// RegisterOptions for fluent API - public RegisterOptions Register(RegisterType instance) - where RegisterType : class - { - return this.Register(typeof(RegisterType), instance); - } - - /// - /// Creates/replaces a named container class registration with a specific, strong referenced, instance. - /// - /// Type to register - /// Instance of RegisterType to register - /// Name of registration - /// RegisterOptions for fluent API - public RegisterOptions Register(RegisterType instance, string name) - where RegisterType : class - { - return this.Register(typeof(RegisterType), instance, name); - } - - /// - /// Creates/replaces a container class registration with a specific, strong referenced, instance. - /// - /// Type to register - /// Type of instance to register that implements RegisterType - /// Instance of RegisterImplementation to register - /// RegisterOptions for fluent API - public RegisterOptions Register(RegisterImplementation instance) - where RegisterType : class - where RegisterImplementation : class, RegisterType - { - return this.Register(typeof(RegisterType), typeof(RegisterImplementation), instance); - } - - /// - /// Creates/replaces a named container class registration with a specific, strong referenced, instance. - /// - /// Type to register - /// Type of instance to register that implements RegisterType - /// Instance of RegisterImplementation to register - /// Name of registration - /// RegisterOptions for fluent API - public RegisterOptions Register(RegisterImplementation instance, string name) - where RegisterType : class - where RegisterImplementation : class, RegisterType - { - return this.Register(typeof(RegisterType), typeof(RegisterImplementation), instance, name); - } - - /// - /// Creates/replaces a container class registration with a user specified factory - /// - /// Type to register - /// Factory/lambda that returns an instance of RegisterType - /// RegisterOptions for fluent API - public RegisterOptions Register(Func factory) - where RegisterType : class - { - if (factory == null) - { - throw new ArgumentNullException("factory"); - } - - return this.Register(typeof(RegisterType), (c, o) => factory(c, o)); - } - - /// - /// Creates/replaces a named container class registration with a user specified factory - /// - /// Type to register - /// Factory/lambda that returns an instance of RegisterType - /// Name of registation - /// RegisterOptions for fluent API - public RegisterOptions Register(Func factory, string name) - where RegisterType : class - { - if (factory == null) - { - throw new ArgumentNullException("factory"); - } - - return this.Register(typeof(RegisterType), (c, o) => factory(c, o), name); - } - - /// - /// Register multiple implementations of a type. - /// - /// Internally this registers each implementation using the full name of the class as its registration name. - /// - /// Type that each implementation implements - /// Types that implement RegisterType - /// MultiRegisterOptions for the fluent API - public MultiRegisterOptions RegisterMultiple(IEnumerable implementationTypes) - { - return RegisterMultiple(typeof(RegisterType), implementationTypes); - } - - /// - /// Register multiple implementations of a type. - /// - /// Internally this registers each implementation using the full name of the class as its registration name. - /// - /// Type that each implementation implements - /// Types that implement RegisterType - /// MultiRegisterOptions for the fluent API - public MultiRegisterOptions RegisterMultiple(Type registrationType, IEnumerable implementationTypes) - { - if (implementationTypes == null) - throw new ArgumentNullException("types", "types is null."); - - foreach (var type in implementationTypes) - //#if NETFX_CORE - // if (!registrationType.GetTypeInfo().IsAssignableFrom(type.GetTypeInfo())) - //#else - if (!registrationType.IsAssignableFrom(type)) - - //#endif - throw new ArgumentException(string.Format("types: The type {0} is not assignable from {1}", registrationType.FullName, type.FullName)); - - if (implementationTypes.Count() != implementationTypes.Distinct().Count()) - { - var queryForDuplicatedTypes = from i in implementationTypes - group i by i - into j - where j.Count() > 1 - select j.Key.FullName; - - var fullNamesOfDuplicatedTypes = string.Join(",\n", queryForDuplicatedTypes.ToArray()); - var multipleRegMessage = string.Format("types: The same implementation type cannot be specified multiple times for {0}\n\n{1}", registrationType.FullName, fullNamesOfDuplicatedTypes); - throw new ArgumentException(multipleRegMessage); - } - - var registerOptions = new List(); - - foreach (var type in implementationTypes) - { - registerOptions.Add(Register(registrationType, type, type.FullName)); - } - - return new MultiRegisterOptions(registerOptions); - } - #endregion - - #region Resolution - /// - /// Attempts to resolve a type using default options. - /// - /// Type to resolve - /// Instance of type - /// Unable to resolve the type. - public object Resolve(Type resolveType) - { - return ResolveInternal(new TypeRegistration(resolveType), NamedParameterOverloads.Default, ResolveOptions.Default); - } - - /// - /// Attempts to resolve a type using specified options. - /// - /// Type to resolve - /// Resolution options - /// Instance of type - /// Unable to resolve the type. - public object Resolve(Type resolveType, ResolveOptions options) - { - return ResolveInternal(new TypeRegistration(resolveType), NamedParameterOverloads.Default, options); - } - - /// - /// Attempts to resolve a type using default options and the supplied name. - /// - /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). - /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. - /// - /// Type to resolve - /// Name of registration - /// Instance of type - /// Unable to resolve the type. - public object Resolve(Type resolveType, string name) - { - return ResolveInternal(new TypeRegistration(resolveType, name), NamedParameterOverloads.Default, ResolveOptions.Default); - } - - /// - /// Attempts to resolve a type using supplied options and name. - /// - /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). - /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. - /// - /// Type to resolve - /// Name of registration - /// Resolution options - /// Instance of type - /// Unable to resolve the type. - public object Resolve(Type resolveType, string name, ResolveOptions options) - { - return ResolveInternal(new TypeRegistration(resolveType, name), NamedParameterOverloads.Default, options); - } - - /// - /// Attempts to resolve a type using default options and the supplied constructor parameters. - /// - /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). - /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. - /// - /// Type to resolve - /// User specified constructor parameters - /// Instance of type - /// Unable to resolve the type. - public object Resolve(Type resolveType, NamedParameterOverloads parameters) - { - return ResolveInternal(new TypeRegistration(resolveType), parameters, ResolveOptions.Default); - } - - /// - /// Attempts to resolve a type using specified options and the supplied constructor parameters. - /// - /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). - /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. - /// - /// Type to resolve - /// User specified constructor parameters - /// Resolution options - /// Instance of type - /// Unable to resolve the type. - public object Resolve(Type resolveType, NamedParameterOverloads parameters, ResolveOptions options) - { - return ResolveInternal(new TypeRegistration(resolveType), parameters, options); - } - - /// - /// Attempts to resolve a type using default options and the supplied constructor parameters and name. - /// - /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). - /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. - /// - /// Type to resolve - /// User specified constructor parameters - /// Name of registration - /// Instance of type - /// Unable to resolve the type. - public object Resolve(Type resolveType, string name, NamedParameterOverloads parameters) - { - return ResolveInternal(new TypeRegistration(resolveType, name), parameters, ResolveOptions.Default); - } - - /// - /// Attempts to resolve a named type using specified options and the supplied constructor parameters. - /// - /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). - /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. - /// - /// Type to resolve - /// Name of registration - /// User specified constructor parameters - /// Resolution options - /// Instance of type - /// Unable to resolve the type. - public object Resolve(Type resolveType, string name, NamedParameterOverloads parameters, ResolveOptions options) - { - return ResolveInternal(new TypeRegistration(resolveType, name), parameters, options); - } - - /// - /// Attempts to resolve a type using default options. - /// - /// Type to resolve - /// Instance of type - /// Unable to resolve the type. - public ResolveType Resolve() - where ResolveType : class - { - return (ResolveType)Resolve(typeof(ResolveType)); - } - - /// - /// Attempts to resolve a type using specified options. - /// - /// Type to resolve - /// Resolution options - /// Instance of type - /// Unable to resolve the type. - public ResolveType Resolve(ResolveOptions options) - where ResolveType : class - { - return (ResolveType)Resolve(typeof(ResolveType), options); - } - - /// - /// Attempts to resolve a type using default options and the supplied name. - /// - /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). - /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. - /// - /// Type to resolve - /// Name of registration - /// Instance of type - /// Unable to resolve the type. - public ResolveType Resolve(string name) - where ResolveType : class - { - return (ResolveType)Resolve(typeof(ResolveType), name); - } - - /// - /// Attempts to resolve a type using supplied options and name. - /// - /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). - /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. - /// - /// Type to resolve - /// Name of registration - /// Resolution options - /// Instance of type - /// Unable to resolve the type. - public ResolveType Resolve(string name, ResolveOptions options) - where ResolveType : class - { - return (ResolveType)Resolve(typeof(ResolveType), name, options); - } - - /// - /// Attempts to resolve a type using default options and the supplied constructor parameters. - /// - /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). - /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. - /// - /// Type to resolve - /// User specified constructor parameters - /// Instance of type - /// Unable to resolve the type. - public ResolveType Resolve(NamedParameterOverloads parameters) - where ResolveType : class - { - return (ResolveType)Resolve(typeof(ResolveType), parameters); - } - - /// - /// Attempts to resolve a type using specified options and the supplied constructor parameters. - /// - /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). - /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. - /// - /// Type to resolve - /// User specified constructor parameters - /// Resolution options - /// Instance of type - /// Unable to resolve the type. - public ResolveType Resolve(NamedParameterOverloads parameters, ResolveOptions options) - where ResolveType : class - { - return (ResolveType)Resolve(typeof(ResolveType), parameters, options); - } - - /// - /// Attempts to resolve a type using default options and the supplied constructor parameters and name. - /// - /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). - /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. - /// - /// Type to resolve - /// User specified constructor parameters - /// Name of registration - /// Instance of type - /// Unable to resolve the type. - public ResolveType Resolve(string name, NamedParameterOverloads parameters) - where ResolveType : class - { - return (ResolveType)Resolve(typeof(ResolveType), name, parameters); - } - - /// - /// Attempts to resolve a named type using specified options and the supplied constructor parameters. - /// - /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). - /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. - /// - /// Type to resolve - /// Name of registration - /// User specified constructor parameters - /// Resolution options - /// Instance of type - /// Unable to resolve the type. - public ResolveType Resolve(string name, NamedParameterOverloads parameters, ResolveOptions options) - where ResolveType : class - { - return (ResolveType)Resolve(typeof(ResolveType), name, parameters, options); - } - - /// - /// Attempts to predict whether a given type can be resolved with default options. - /// - /// Note: Resolution may still fail if user defined factory registations fail to construct objects when called. - /// - /// Type to resolve - /// Name of registration - /// Bool indicating whether the type can be resolved - public bool CanResolve(Type resolveType) - { - return CanResolveInternal(new TypeRegistration(resolveType), NamedParameterOverloads.Default, ResolveOptions.Default); - } - - /// - /// Attempts to predict whether a given named type can be resolved with default options. - /// - /// Note: Resolution may still fail if user defined factory registations fail to construct objects when called. - /// - /// Type to resolve - /// Bool indicating whether the type can be resolved - private bool CanResolve(Type resolveType, string name) - { - return CanResolveInternal(new TypeRegistration(resolveType, name), NamedParameterOverloads.Default, ResolveOptions.Default); - } - - /// - /// Attempts to predict whether a given type can be resolved with the specified options. - /// - /// Note: Resolution may still fail if user defined factory registations fail to construct objects when called. - /// - /// Type to resolve - /// Name of registration - /// Resolution options - /// Bool indicating whether the type can be resolved - public bool CanResolve(Type resolveType, ResolveOptions options) - { - return CanResolveInternal(new TypeRegistration(resolveType), NamedParameterOverloads.Default, options); - } - - /// - /// Attempts to predict whether a given named type can be resolved with the specified options. - /// - /// Note: Resolution may still fail if user defined factory registations fail to construct objects when called. - /// - /// Type to resolve - /// Name of registration - /// Resolution options - /// Bool indicating whether the type can be resolved - public bool CanResolve(Type resolveType, string name, ResolveOptions options) - { - return CanResolveInternal(new TypeRegistration(resolveType, name), NamedParameterOverloads.Default, options); - } - - /// - /// Attempts to predict whether a given type can be resolved with the supplied constructor parameters and default options. - /// - /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). - /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. - /// - /// Note: Resolution may still fail if user defined factory registations fail to construct objects when called. - /// - /// Type to resolve - /// User supplied named parameter overloads - /// Bool indicating whether the type can be resolved - public bool CanResolve(Type resolveType, NamedParameterOverloads parameters) - { - return CanResolveInternal(new TypeRegistration(resolveType), parameters, ResolveOptions.Default); - } - - /// - /// Attempts to predict whether a given named type can be resolved with the supplied constructor parameters and default options. - /// - /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). - /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. - /// - /// Note: Resolution may still fail if user defined factory registations fail to construct objects when called. - /// - /// Type to resolve - /// Name of registration - /// User supplied named parameter overloads - /// Bool indicating whether the type can be resolved - public bool CanResolve(Type resolveType, string name, NamedParameterOverloads parameters) - { - return CanResolveInternal(new TypeRegistration(resolveType, name), parameters, ResolveOptions.Default); - } - - /// - /// Attempts to predict whether a given type can be resolved with the supplied constructor parameters options. - /// - /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). - /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. - /// - /// Note: Resolution may still fail if user defined factory registations fail to construct objects when called. - /// - /// Type to resolve - /// User supplied named parameter overloads - /// Resolution options - /// Bool indicating whether the type can be resolved - public bool CanResolve(Type resolveType, NamedParameterOverloads parameters, ResolveOptions options) - { - return CanResolveInternal(new TypeRegistration(resolveType), parameters, options); - } - - /// - /// Attempts to predict whether a given named type can be resolved with the supplied constructor parameters options. - /// - /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). - /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. - /// - /// Note: Resolution may still fail if user defined factory registations fail to construct objects when called. - /// - /// Type to resolve - /// Name of registration - /// User supplied named parameter overloads - /// Resolution options - /// Bool indicating whether the type can be resolved - public bool CanResolve(Type resolveType, string name, NamedParameterOverloads parameters, ResolveOptions options) - { - return CanResolveInternal(new TypeRegistration(resolveType, name), parameters, options); - } - - /// - /// Attempts to predict whether a given type can be resolved with default options. - /// - /// Note: Resolution may still fail if user defined factory registations fail to construct objects when called. - /// - /// Type to resolve - /// Name of registration - /// Bool indicating whether the type can be resolved - public bool CanResolve() - where ResolveType : class - { - return CanResolve(typeof(ResolveType)); - } - - /// - /// Attempts to predict whether a given named type can be resolved with default options. - /// - /// Note: Resolution may still fail if user defined factory registations fail to construct objects when called. - /// - /// Type to resolve - /// Bool indicating whether the type can be resolved - public bool CanResolve(string name) - where ResolveType : class - { - return CanResolve(typeof(ResolveType), name); - } - - /// - /// Attempts to predict whether a given type can be resolved with the specified options. - /// - /// Note: Resolution may still fail if user defined factory registations fail to construct objects when called. - /// - /// Type to resolve - /// Name of registration - /// Resolution options - /// Bool indicating whether the type can be resolved - public bool CanResolve(ResolveOptions options) - where ResolveType : class - { - return CanResolve(typeof(ResolveType), options); - } - - /// - /// Attempts to predict whether a given named type can be resolved with the specified options. - /// - /// Note: Resolution may still fail if user defined factory registations fail to construct objects when called. - /// - /// Type to resolve - /// Name of registration - /// Resolution options - /// Bool indicating whether the type can be resolved - public bool CanResolve(string name, ResolveOptions options) - where ResolveType : class - { - return CanResolve(typeof(ResolveType), name, options); - } - - /// - /// Attempts to predict whether a given type can be resolved with the supplied constructor parameters and default options. - /// - /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). - /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. - /// - /// Note: Resolution may still fail if user defined factory registations fail to construct objects when called. - /// - /// Type to resolve - /// User supplied named parameter overloads - /// Bool indicating whether the type can be resolved - public bool CanResolve(NamedParameterOverloads parameters) - where ResolveType : class - { - return CanResolve(typeof(ResolveType), parameters); - } - - /// - /// Attempts to predict whether a given named type can be resolved with the supplied constructor parameters and default options. - /// - /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). - /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. - /// - /// Note: Resolution may still fail if user defined factory registations fail to construct objects when called. - /// - /// Type to resolve - /// Name of registration - /// User supplied named parameter overloads - /// Bool indicating whether the type can be resolved - public bool CanResolve(string name, NamedParameterOverloads parameters) - where ResolveType : class - { - return CanResolve(typeof(ResolveType), name, parameters); - } - - /// - /// Attempts to predict whether a given type can be resolved with the supplied constructor parameters options. - /// - /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). - /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. - /// - /// Note: Resolution may still fail if user defined factory registations fail to construct objects when called. - /// - /// Type to resolve - /// User supplied named parameter overloads - /// Resolution options - /// Bool indicating whether the type can be resolved - public bool CanResolve(NamedParameterOverloads parameters, ResolveOptions options) - where ResolveType : class - { - return CanResolve(typeof(ResolveType), parameters, options); - } - - /// - /// Attempts to predict whether a given named type can be resolved with the supplied constructor parameters options. - /// - /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). - /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. - /// - /// Note: Resolution may still fail if user defined factory registations fail to construct objects when called. - /// - /// Type to resolve - /// Name of registration - /// User supplied named parameter overloads - /// Resolution options - /// Bool indicating whether the type can be resolved - public bool CanResolve(string name, NamedParameterOverloads parameters, ResolveOptions options) - where ResolveType : class - { - return CanResolve(typeof(ResolveType), name, parameters, options); - } - - /// - /// Attemps to resolve a type using the default options - /// - /// Type to resolve - /// Resolved type or default if resolve fails - /// True if resolved sucessfully, false otherwise - public bool TryResolve(Type resolveType, out object resolvedType) - { - try - { - resolvedType = Resolve(resolveType); - return true; - } - catch (TinyIoCResolutionException) - { - resolvedType = null; - return false; - } - } - - /// - /// Attemps to resolve a type using the given options - /// - /// Type to resolve - /// Resolution options - /// Resolved type or default if resolve fails - /// True if resolved sucessfully, false otherwise - public bool TryResolve(Type resolveType, ResolveOptions options, out object resolvedType) - { - try - { - resolvedType = Resolve(resolveType, options); - return true; - } - catch (TinyIoCResolutionException) - { - resolvedType = null; - return false; - } - } - - /// - /// Attemps to resolve a type using the default options and given name - /// - /// Type to resolve - /// Name of registration - /// Resolved type or default if resolve fails - /// True if resolved sucessfully, false otherwise - public bool TryResolve(Type resolveType, string name, out object resolvedType) - { - try - { - resolvedType = Resolve(resolveType, name); - return true; - } - catch (TinyIoCResolutionException) - { - resolvedType = null; - return false; - } - } - - /// - /// Attemps to resolve a type using the given options and name - /// - /// Type to resolve - /// Name of registration - /// Resolution options - /// Resolved type or default if resolve fails - /// True if resolved sucessfully, false otherwise - public bool TryResolve(Type resolveType, string name, ResolveOptions options, out object resolvedType) - { - try - { - resolvedType = Resolve(resolveType, name, options); - return true; - } - catch (TinyIoCResolutionException) - { - resolvedType = null; - return false; - } - } - - /// - /// Attemps to resolve a type using the default options and supplied constructor parameters - /// - /// Type to resolve - /// User specified constructor parameters - /// Resolved type or default if resolve fails - /// True if resolved sucessfully, false otherwise - public bool TryResolve(Type resolveType, NamedParameterOverloads parameters, out object resolvedType) - { - try - { - resolvedType = Resolve(resolveType, parameters); - return true; - } - catch (TinyIoCResolutionException) - { - resolvedType = null; - return false; - } - } - - /// - /// Attemps to resolve a type using the default options and supplied name and constructor parameters - /// - /// Type to resolve - /// Name of registration - /// User specified constructor parameters - /// Resolved type or default if resolve fails - /// True if resolved sucessfully, false otherwise - public bool TryResolve(Type resolveType, string name, NamedParameterOverloads parameters, out object resolvedType) - { - try - { - resolvedType = Resolve(resolveType, name, parameters); - return true; - } - catch (TinyIoCResolutionException) - { - resolvedType = null; - return false; - } - } - - /// - /// Attemps to resolve a type using the supplied options and constructor parameters - /// - /// Type to resolve - /// Name of registration - /// User specified constructor parameters - /// Resolution options - /// Resolved type or default if resolve fails - /// True if resolved sucessfully, false otherwise - public bool TryResolve(Type resolveType, NamedParameterOverloads parameters, ResolveOptions options, out object resolvedType) - { - try - { - resolvedType = Resolve(resolveType, parameters, options); - return true; - } - catch (TinyIoCResolutionException) - { - resolvedType = null; - return false; - } - } - - /// - /// Attemps to resolve a type using the supplied name, options and constructor parameters - /// - /// Type to resolve - /// Name of registration - /// User specified constructor parameters - /// Resolution options - /// Resolved type or default if resolve fails - /// True if resolved sucessfully, false otherwise - public bool TryResolve(Type resolveType, string name, NamedParameterOverloads parameters, ResolveOptions options, out object resolvedType) - { - try - { - resolvedType = Resolve(resolveType, name, parameters, options); - return true; - } - catch (TinyIoCResolutionException) - { - resolvedType = null; - return false; - } - } - - /// - /// Attemps to resolve a type using the default options - /// - /// Type to resolve - /// Resolved type or default if resolve fails - /// True if resolved sucessfully, false otherwise - public bool TryResolve(out ResolveType resolvedType) - where ResolveType : class - { - try - { - resolvedType = Resolve(); - return true; - } - catch (TinyIoCResolutionException) - { - resolvedType = default(ResolveType); - return false; - } - } - - /// - /// Attemps to resolve a type using the given options - /// - /// Type to resolve - /// Resolution options - /// Resolved type or default if resolve fails - /// True if resolved sucessfully, false otherwise - public bool TryResolve(ResolveOptions options, out ResolveType resolvedType) - where ResolveType : class - { - try - { - resolvedType = Resolve(options); - return true; - } - catch (TinyIoCResolutionException) - { - resolvedType = default(ResolveType); - return false; - } - } - - /// - /// Attemps to resolve a type using the default options and given name - /// - /// Type to resolve - /// Name of registration - /// Resolved type or default if resolve fails - /// True if resolved sucessfully, false otherwise - public bool TryResolve(string name, out ResolveType resolvedType) - where ResolveType : class - { - try - { - resolvedType = Resolve(name); - return true; - } - catch (TinyIoCResolutionException) - { - resolvedType = default(ResolveType); - return false; - } - } - - /// - /// Attemps to resolve a type using the given options and name - /// - /// Type to resolve - /// Name of registration - /// Resolution options - /// Resolved type or default if resolve fails - /// True if resolved sucessfully, false otherwise - public bool TryResolve(string name, ResolveOptions options, out ResolveType resolvedType) - where ResolveType : class - { - try - { - resolvedType = Resolve(name, options); - return true; - } - catch (TinyIoCResolutionException) - { - resolvedType = default(ResolveType); - return false; - } - } - - /// - /// Attemps to resolve a type using the default options and supplied constructor parameters - /// - /// Type to resolve - /// User specified constructor parameters - /// Resolved type or default if resolve fails - /// True if resolved sucessfully, false otherwise - public bool TryResolve(NamedParameterOverloads parameters, out ResolveType resolvedType) - where ResolveType : class - { - try - { - resolvedType = Resolve(parameters); - return true; - } - catch (TinyIoCResolutionException) - { - resolvedType = default(ResolveType); - return false; - } - } - - /// - /// Attemps to resolve a type using the default options and supplied name and constructor parameters - /// - /// Type to resolve - /// Name of registration - /// User specified constructor parameters - /// Resolved type or default if resolve fails - /// True if resolved sucessfully, false otherwise - public bool TryResolve(string name, NamedParameterOverloads parameters, out ResolveType resolvedType) - where ResolveType : class - { - try - { - resolvedType = Resolve(name, parameters); - return true; - } - catch (TinyIoCResolutionException) - { - resolvedType = default(ResolveType); - return false; - } - } - - /// - /// Attemps to resolve a type using the supplied options and constructor parameters - /// - /// Type to resolve - /// Name of registration - /// User specified constructor parameters - /// Resolution options - /// Resolved type or default if resolve fails - /// True if resolved sucessfully, false otherwise - public bool TryResolve(NamedParameterOverloads parameters, ResolveOptions options, out ResolveType resolvedType) - where ResolveType : class - { - try - { - resolvedType = Resolve(parameters, options); - return true; - } - catch (TinyIoCResolutionException) - { - resolvedType = default(ResolveType); - return false; - } - } - - /// - /// Attemps to resolve a type using the supplied name, options and constructor parameters - /// - /// Type to resolve - /// Name of registration - /// User specified constructor parameters - /// Resolution options - /// Resolved type or default if resolve fails - /// True if resolved sucessfully, false otherwise - public bool TryResolve(string name, NamedParameterOverloads parameters, ResolveOptions options, out ResolveType resolvedType) - where ResolveType : class - { - try - { - resolvedType = Resolve(name, parameters, options); - return true; - } - catch (TinyIoCResolutionException) - { - resolvedType = default(ResolveType); - return false; - } - } - - /// - /// Returns all registrations of a type - /// - /// Type to resolveAll - /// Whether to include un-named (default) registrations - /// IEnumerable - public IEnumerable ResolveAll(Type resolveType, bool includeUnnamed) - { - return ResolveAllInternal(resolveType, includeUnnamed); - } - - /// - /// Returns all registrations of a type, both named and unnamed - /// - /// Type to resolveAll - /// IEnumerable - public IEnumerable ResolveAll(Type resolveType) - { - return ResolveAll(resolveType, false); - } - - /// - /// Returns all registrations of a type - /// - /// Type to resolveAll - /// Whether to include un-named (default) registrations - /// IEnumerable - public IEnumerable ResolveAll(bool includeUnnamed) - where ResolveType : class - { - return this.ResolveAll(typeof(ResolveType), includeUnnamed).Cast(); - } - - /// - /// Returns all registrations of a type, both named and unnamed - /// - /// Type to resolveAll - /// Whether to include un-named (default) registrations - /// IEnumerable - public IEnumerable ResolveAll() - where ResolveType : class - { - return ResolveAll(true); - } - - /// - /// Attempts to resolve all public property dependencies on the given object. - /// - /// Object to "build up" - public void BuildUp(object input) - { - BuildUpInternal(input, ResolveOptions.Default); - } - - /// - /// Attempts to resolve all public property dependencies on the given object using the given resolve options. - /// - /// Object to "build up" - /// Resolve options to use - public void BuildUp(object input, ResolveOptions resolveOptions) - { - BuildUpInternal(input, resolveOptions); - } - #endregion - #endregion - - #region Object Factories - /// - /// Provides custom lifetime management for ASP.Net per-request lifetimes etc. - /// - public interface ITinyIoCObjectLifetimeProvider - { - /// - /// Gets the stored object if it exists, or null if not - /// - /// Object instance or null - object GetObject(); - - /// - /// Store the object - /// - /// Object to store - void SetObject(object value); - - /// - /// Release the object - /// - void ReleaseObject(); - } - - private abstract class ObjectFactoryBase - { - /// - /// Whether to assume this factory sucessfully constructs its objects - /// - /// Generally set to true for delegate style factories as CanResolve cannot delve - /// into the delegates they contain. - /// - public virtual bool AssumeConstruction => false; - - /// - /// The type the factory instantiates - /// - public abstract Type CreatesType { get; } - - /// - /// Constructor to use, if specified - /// - public ConstructorInfo Constructor { get; protected set; } - - /// - /// Create the type - /// - /// Type user requested to be resolved - /// Container that requested the creation - /// Any user parameters passed - /// - /// - public abstract object GetObject(Type requestedType, TinyIoCContainer container, NamedParameterOverloads parameters, ResolveOptions options); - - public virtual ObjectFactoryBase SingletonVariant - { - get - { - throw new TinyIoCRegistrationException(this.GetType(), "singleton"); - } - } - - public virtual ObjectFactoryBase MultiInstanceVariant - { - get - { - throw new TinyIoCRegistrationException(this.GetType(), "multi-instance"); - } - } - - public virtual ObjectFactoryBase StrongReferenceVariant - { - get - { - throw new TinyIoCRegistrationException(this.GetType(), "strong reference"); - } - } - - public virtual ObjectFactoryBase WeakReferenceVariant - { - get - { - throw new TinyIoCRegistrationException(this.GetType(), "weak reference"); - } - } - - public virtual ObjectFactoryBase GetCustomObjectLifetimeVariant(ITinyIoCObjectLifetimeProvider lifetimeProvider, string errorString) - { - throw new TinyIoCRegistrationException(this.GetType(), errorString); - } - - public virtual void SetConstructor(ConstructorInfo constructor) - { - Constructor = constructor; - } - - public virtual ObjectFactoryBase GetFactoryForChildContainer(Type type, TinyIoCContainer parent, TinyIoCContainer child) - { - return this; - } - } - - /// - /// IObjectFactory that creates new instances of types for each resolution - /// - private class MultiInstanceFactory : ObjectFactoryBase - { - private readonly Type registerType; - private readonly Type registerImplementation; - public override Type CreatesType => this.registerImplementation; - - public MultiInstanceFactory(Type registerType, Type registerImplementation) - { - //#if NETFX_CORE - // if (registerImplementation.GetTypeInfo().IsAbstract() || registerImplementation.GetTypeInfo().IsInterface()) - // throw new TinyIoCRegistrationTypeException(registerImplementation, "MultiInstanceFactory"); - //#else - if (registerImplementation.IsAbstract() || registerImplementation.IsInterface()) - throw new TinyIoCRegistrationTypeException(registerImplementation, "MultiInstanceFactory"); - - //#endif - if (!IsValidAssignment(registerType, registerImplementation)) - throw new TinyIoCRegistrationTypeException(registerImplementation, "MultiInstanceFactory"); - - this.registerType = registerType; - this.registerImplementation = registerImplementation; - } - - public override object GetObject(Type requestedType, TinyIoCContainer container, NamedParameterOverloads parameters, ResolveOptions options) - { - try - { - return container.ConstructType(requestedType, this.registerImplementation, Constructor, parameters, options); - } - catch (TinyIoCResolutionException ex) - { - throw new TinyIoCResolutionException(this.registerType, ex); - } - } - - public override ObjectFactoryBase SingletonVariant => new SingletonFactory(this.registerType, this.registerImplementation); - - public override ObjectFactoryBase GetCustomObjectLifetimeVariant(ITinyIoCObjectLifetimeProvider lifetimeProvider, string errorString) - { - return new CustomObjectLifetimeFactory(this.registerType, this.registerImplementation, lifetimeProvider, errorString); - } - - public override ObjectFactoryBase MultiInstanceVariant => this; - } - - /// - /// IObjectFactory that invokes a specified delegate to construct the object - /// - private class DelegateFactory : ObjectFactoryBase - { - private readonly Type registerType; - - private Func _factory; - - public override bool AssumeConstruction => true; - - public override Type CreatesType => this.registerType; - - public override object GetObject(Type requestedType, TinyIoCContainer container, NamedParameterOverloads parameters, ResolveOptions options) - { - try - { - return _factory.Invoke(container, parameters); - } - catch (Exception ex) - { - throw new TinyIoCResolutionException(this.registerType, ex); - } - } - - public DelegateFactory(Type registerType, Func factory) - { - if (factory == null) - throw new ArgumentNullException("factory"); - - _factory = factory; - - this.registerType = registerType; - } - - public override ObjectFactoryBase WeakReferenceVariant => new WeakDelegateFactory(this.registerType, _factory); - - public override ObjectFactoryBase StrongReferenceVariant => this; - - public override void SetConstructor(ConstructorInfo constructor) - { - throw new TinyIoCConstructorResolutionException("Constructor selection is not possible for delegate factory registrations"); - } - } - - /// - /// IObjectFactory that invokes a specified delegate to construct the object - /// Holds the delegate using a weak reference - /// - private class WeakDelegateFactory : ObjectFactoryBase - { - private readonly Type registerType; - - private WeakReference _factory; - - public override bool AssumeConstruction => true; - - public override Type CreatesType => this.registerType; - - public override object GetObject(Type requestedType, TinyIoCContainer container, NamedParameterOverloads parameters, ResolveOptions options) - { - var factory = _factory.Target as Func; - - if (factory == null) - throw new TinyIoCWeakReferenceException(this.registerType); - - try - { - return factory.Invoke(container, parameters); - } - catch (Exception ex) - { - throw new TinyIoCResolutionException(this.registerType, ex); - } - } - - public WeakDelegateFactory(Type registerType, Func factory) - { - if (factory == null) - throw new ArgumentNullException("factory"); - - _factory = new WeakReference(factory); - - this.registerType = registerType; - } - - public override ObjectFactoryBase StrongReferenceVariant - { - get - { - var factory = _factory.Target as Func; - - if (factory == null) - throw new TinyIoCWeakReferenceException(this.registerType); - - return new DelegateFactory(this.registerType, factory); - } - } - - public override ObjectFactoryBase WeakReferenceVariant => this; - - public override void SetConstructor(ConstructorInfo constructor) - { - throw new TinyIoCConstructorResolutionException("Constructor selection is not possible for delegate factory registrations"); - } - } - - /// - /// Stores an particular instance to return for a type - /// - private class InstanceFactory : ObjectFactoryBase, IDisposable - { - private readonly Type registerType; - private readonly Type registerImplementation; - private object _instance; - - public override bool AssumeConstruction => true; - - public InstanceFactory(Type registerType, Type registerImplementation, object instance) - { - if (!IsValidAssignment(registerType, registerImplementation)) - throw new TinyIoCRegistrationTypeException(registerImplementation, "InstanceFactory"); - - this.registerType = registerType; - this.registerImplementation = registerImplementation; - _instance = instance; - } - - public override Type CreatesType => this.registerImplementation; - - public override object GetObject(Type requestedType, TinyIoCContainer container, NamedParameterOverloads parameters, ResolveOptions options) - { - return _instance; - } - - public override ObjectFactoryBase MultiInstanceVariant => new MultiInstanceFactory(this.registerType, this.registerImplementation); - - public override ObjectFactoryBase WeakReferenceVariant => new WeakInstanceFactory(this.registerType, this.registerImplementation, this._instance); - - public override ObjectFactoryBase StrongReferenceVariant => this; - - public override void SetConstructor(ConstructorInfo constructor) - { - throw new TinyIoCConstructorResolutionException("Constructor selection is not possible for instance factory registrations"); - } - - public void Dispose() - { - if (_instance is IDisposable disposable) - disposable.Dispose(); - } - } - - /// - /// Stores an particular instance to return for a type - /// - /// Stores the instance with a weak reference - /// - private class WeakInstanceFactory : ObjectFactoryBase, IDisposable - { - private readonly Type registerType; - private readonly Type registerImplementation; - private readonly WeakReference _instance; - - public WeakInstanceFactory(Type registerType, Type registerImplementation, object instance) - { - if (!IsValidAssignment(registerType, registerImplementation)) - throw new TinyIoCRegistrationTypeException(registerImplementation, "WeakInstanceFactory"); - - this.registerType = registerType; - this.registerImplementation = registerImplementation; - _instance = new WeakReference(instance); - } - - public override Type CreatesType => this.registerImplementation; - - public override object GetObject(Type requestedType, TinyIoCContainer container, NamedParameterOverloads parameters, ResolveOptions options) - { - var instance = _instance.Target; - - if (instance == null) - throw new TinyIoCWeakReferenceException(this.registerType); - - return instance; - } - - public override ObjectFactoryBase MultiInstanceVariant => new MultiInstanceFactory(this.registerType, this.registerImplementation); - - public override ObjectFactoryBase WeakReferenceVariant => this; - - public override ObjectFactoryBase StrongReferenceVariant - { - get - { - var instance = _instance.Target; - - if (instance == null) - throw new TinyIoCWeakReferenceException(this.registerType); - - return new InstanceFactory(this.registerType, this.registerImplementation, instance); - } - } - - public override void SetConstructor(ConstructorInfo constructor) - { - throw new TinyIoCConstructorResolutionException("Constructor selection is not possible for instance factory registrations"); - } - - public void Dispose() - { - if (_instance.Target is IDisposable disposable) - disposable.Dispose(); - } - } - - /// - /// A factory that lazy instantiates a type and always returns the same instance - /// - private class SingletonFactory : ObjectFactoryBase, IDisposable - { - private readonly Type registerType; - private readonly Type registerImplementation; - private readonly object SingletonLock = new object(); - private object _Current; - - public SingletonFactory(Type registerType, Type registerImplementation) - { - //#if NETFX_CORE - // if (registerImplementation.GetTypeInfo().IsAbstract() || registerImplementation.GetTypeInfo().IsInterface()) - //#else - if (registerImplementation.IsAbstract() || registerImplementation.IsInterface()) - - //#endif - throw new TinyIoCRegistrationTypeException(registerImplementation, "SingletonFactory"); - - if (!IsValidAssignment(registerType, registerImplementation)) - throw new TinyIoCRegistrationTypeException(registerImplementation, "SingletonFactory"); - - this.registerType = registerType; - this.registerImplementation = registerImplementation; - } - - public override Type CreatesType => this.registerImplementation; - - public override object GetObject(Type requestedType, TinyIoCContainer container, NamedParameterOverloads parameters, ResolveOptions options) - { - if (parameters.Count != 0) - throw new ArgumentException("Cannot specify parameters for singleton types"); - - lock (SingletonLock) - if (_Current == null) - _Current = container.ConstructType(requestedType, this.registerImplementation, Constructor, options); - - return _Current; - } - - public override ObjectFactoryBase SingletonVariant => this; - - public override ObjectFactoryBase GetCustomObjectLifetimeVariant(ITinyIoCObjectLifetimeProvider lifetimeProvider, string errorString) - { - return new CustomObjectLifetimeFactory(this.registerType, this.registerImplementation, lifetimeProvider, errorString); - } - - public override ObjectFactoryBase MultiInstanceVariant => new MultiInstanceFactory(this.registerType, this.registerImplementation); - - public override ObjectFactoryBase GetFactoryForChildContainer(Type type, TinyIoCContainer parent, TinyIoCContainer child) - { - // We make sure that the singleton is constructed before the child container takes the factory. - // Otherwise the results would vary depending on whether or not the parent container had resolved - // the type before the child container does. - GetObject(type, parent, NamedParameterOverloads.Default, ResolveOptions.Default); - return this; - } - - public void Dispose() - { - if (this._Current == null) - return; - - if (this._Current is IDisposable disposable) - disposable.Dispose(); - } - } - - /// - /// A factory that offloads lifetime to an external lifetime provider - /// - private class CustomObjectLifetimeFactory : ObjectFactoryBase, IDisposable - { - private readonly object SingletonLock = new object(); - private readonly Type registerType; - private readonly Type registerImplementation; - private readonly ITinyIoCObjectLifetimeProvider _LifetimeProvider; - - public CustomObjectLifetimeFactory(Type registerType, Type registerImplementation, ITinyIoCObjectLifetimeProvider lifetimeProvider, string errorMessage) - { - if (lifetimeProvider == null) - throw new ArgumentNullException("lifetimeProvider", "lifetimeProvider is null."); - - if (!IsValidAssignment(registerType, registerImplementation)) - throw new TinyIoCRegistrationTypeException(registerImplementation, "SingletonFactory"); - - //#if NETFX_CORE - // if (registerImplementation.GetTypeInfo().IsAbstract() || registerImplementation.GetTypeInfo().IsInterface()) - //#else - if (registerImplementation.IsAbstract() || registerImplementation.IsInterface()) - - //#endif - throw new TinyIoCRegistrationTypeException(registerImplementation, errorMessage); - - this.registerType = registerType; - this.registerImplementation = registerImplementation; - _LifetimeProvider = lifetimeProvider; - } - - public override Type CreatesType => this.registerImplementation; - - public override object GetObject(Type requestedType, TinyIoCContainer container, NamedParameterOverloads parameters, ResolveOptions options) - { - object current; - - lock (SingletonLock) - { - current = _LifetimeProvider.GetObject(); - if (current == null) - { - current = container.ConstructType(requestedType, this.registerImplementation, Constructor, options); - _LifetimeProvider.SetObject(current); - } - } - - return current; - } - - public override ObjectFactoryBase SingletonVariant - { - get - { - _LifetimeProvider.ReleaseObject(); - return new SingletonFactory(this.registerType, this.registerImplementation); - } - } - - public override ObjectFactoryBase MultiInstanceVariant - { - get - { - _LifetimeProvider.ReleaseObject(); - return new MultiInstanceFactory(this.registerType, this.registerImplementation); - } - } - - public override ObjectFactoryBase GetCustomObjectLifetimeVariant(ITinyIoCObjectLifetimeProvider lifetimeProvider, string errorString) - { - _LifetimeProvider.ReleaseObject(); - return new CustomObjectLifetimeFactory(this.registerType, this.registerImplementation, lifetimeProvider, errorString); - } - - public override ObjectFactoryBase GetFactoryForChildContainer(Type type, TinyIoCContainer parent, TinyIoCContainer child) - { - // We make sure that the singleton is constructed before the child container takes the factory. - // Otherwise the results would vary depending on whether or not the parent container had resolved - // the type before the child container does. - GetObject(type, parent, NamedParameterOverloads.Default, ResolveOptions.Default); - return this; - } - - public void Dispose() - { - _LifetimeProvider.ReleaseObject(); - } - } - #endregion - - #region Singleton Container - private static readonly TinyIoCContainer _Current = new TinyIoCContainer(); - - static TinyIoCContainer() - { - } - - /// - /// Lazy created Singleton instance of the container for simple scenarios - /// - public static TinyIoCContainer Current => _Current; - - #endregion - - #region Type Registrations - public sealed class TypeRegistration - { - private int _hashCode; - - public Type Type { get; private set; } - public string Name { get; private set; } - - public TypeRegistration(Type type) - : this(type, string.Empty) - { - } - - public TypeRegistration(Type type, string name) - { - Type = type; - Name = name; - - _hashCode = string.Concat(Type.FullName, "|", Name).GetHashCode(); - } - - public override bool Equals(object obj) - { - var typeRegistration = obj as TypeRegistration; - - if (typeRegistration == null) - return false; - - if (Type != typeRegistration.Type) - return false; - - if (string.Compare(Name, typeRegistration.Name, StringComparison.Ordinal) != 0) - return false; - - return true; - } - - public override int GetHashCode() - { - return _hashCode; - } - } - - private readonly SafeDictionary _RegisteredTypes; -#if USE_OBJECT_CONSTRUCTOR - private delegate object ObjectConstructor(params object[] parameters); - private static readonly SafeDictionary _ObjectConstructorCache = new SafeDictionary(); -#endif - #endregion - - #region Constructors - public TinyIoCContainer() - { - _RegisteredTypes = new SafeDictionary(); - - RegisterDefaultTypes(); - } - - private TinyIoCContainer _Parent; - private TinyIoCContainer(TinyIoCContainer parent) - : this() - { - _Parent = parent; - } - #endregion - - #region Internal Methods - private readonly object _AutoRegisterLock = new object(); - private void AutoRegisterInternal(IEnumerable assemblies, DuplicateImplementationActions duplicateAction, Func registrationPredicate) - { - lock (_AutoRegisterLock) - { - var types = assemblies.SelectMany(a => a.SafeGetTypes()).Where(t => !IsIgnoredType(t, registrationPredicate)).ToList(); - - var concreteTypes = from type in types - where type.IsClass() && (type.IsAbstract() == false) && (type != this.GetType() && (type.DeclaringType != this.GetType()) && (!type.IsGenericTypeDefinition())) - select type; - - foreach (var type in concreteTypes) - { - try - { - RegisterInternal(type, string.Empty, GetDefaultObjectFactory(type, type)); - } - catch (MethodAccessException) - { - // Ignore methods we can't access - added for Silverlight - } - } - - var abstractInterfaceTypes = from type in types - where ((type.IsInterface() || type.IsAbstract()) && (type.DeclaringType != this.GetType()) && (!type.IsGenericTypeDefinition())) - select type; - - foreach (var type in abstractInterfaceTypes) - { - var localType = type; - var implementations = from implementationType in concreteTypes - where localType.IsAssignableFrom(implementationType) - select implementationType; - - if (implementations.Count() > 1) - { - if (duplicateAction == DuplicateImplementationActions.Fail) - throw new TinyIoCAutoRegistrationException(type, implementations); - - if (duplicateAction == DuplicateImplementationActions.RegisterMultiple) - { - RegisterMultiple(type, implementations); - } - } - - var firstImplementation = implementations.FirstOrDefault(); - if (firstImplementation != null) - { - try - { - RegisterInternal(type, string.Empty, GetDefaultObjectFactory(type, firstImplementation)); - } - catch (MethodAccessException) - { - // Ignore methods we can't access - added for Silverlight - } - } - } - } - } - - private bool IsIgnoredAssembly(Assembly assembly) - { - // TODO - find a better way to remove "system" assemblies from the auto registration - var ignoreChecks = new List>() - { - asm => asm.FullName.StartsWith("Microsoft.", StringComparison.Ordinal), - asm => asm.FullName.StartsWith("System.", StringComparison.Ordinal), - asm => asm.FullName.StartsWith("System,", StringComparison.Ordinal), - asm => asm.FullName.StartsWith("CR_ExtUnitTest", StringComparison.Ordinal), - asm => asm.FullName.StartsWith("mscorlib,", StringComparison.Ordinal), - asm => asm.FullName.StartsWith("CR_VSTest", StringComparison.Ordinal), - asm => asm.FullName.StartsWith("DevExpress.CodeRush", StringComparison.Ordinal), - }; - - foreach (var check in ignoreChecks) - { - if (check(assembly)) - return true; - } - - return false; - } - - private bool IsIgnoredType(Type type, Func registrationPredicate) - { - // TODO - find a better way to remove "system" types from the auto registration - var ignoreChecks = new List>() - { - t => t.FullName.StartsWith("System.", StringComparison.Ordinal), - t => t.FullName.StartsWith("Microsoft.", StringComparison.Ordinal), - t => t.IsPrimitive(), -#if !UNBOUND_GENERICS_GETCONSTRUCTORS - t => t.IsGenericTypeDefinition(), -#endif - t => (t.GetConstructors(BindingFlags.Instance | BindingFlags.Public).Length == 0) && !(t.IsInterface() || t.IsAbstract()), - }; - - if (registrationPredicate != null) - { - ignoreChecks.Add(t => !registrationPredicate(t)); - } - - foreach (var check in ignoreChecks) - { - if (check(type)) - return true; - } - - return false; - } - - private void RegisterDefaultTypes() - { - Register(this); - -#if TINYMESSENGER - // Only register the TinyMessenger singleton if we are the root container - if (_Parent == null) - Register(); -#endif - } - - private ObjectFactoryBase GetCurrentFactory(TypeRegistration registration) - { - ObjectFactoryBase current = null; - - _RegisteredTypes.TryGetValue(registration, out current); - - return current; - } - - private RegisterOptions RegisterInternal(Type registerType, string name, ObjectFactoryBase factory) - { - var typeRegistration = new TypeRegistration(registerType, name); - - return AddUpdateRegistration(typeRegistration, factory); - } - - private RegisterOptions AddUpdateRegistration(TypeRegistration typeRegistration, ObjectFactoryBase factory) - { - _RegisteredTypes[typeRegistration] = factory; - - return new RegisterOptions(this, typeRegistration); - } - - private void RemoveRegistration(TypeRegistration typeRegistration) - { - _RegisteredTypes.Remove(typeRegistration); - } - - private ObjectFactoryBase GetDefaultObjectFactory(Type registerType, Type registerImplementation) - { - //#if NETFX_CORE - // if (registerType.GetTypeInfo().IsInterface() || registerType.GetTypeInfo().IsAbstract()) - //#else - if (registerType.IsInterface() || registerType.IsAbstract()) - - //#endif - return new SingletonFactory(registerType, registerImplementation); - - return new MultiInstanceFactory(registerType, registerImplementation); - } - - private bool CanResolveInternal(TypeRegistration registration, NamedParameterOverloads parameters, ResolveOptions options) - { - if (parameters == null) - throw new ArgumentNullException("parameters"); - - Type checkType = registration.Type; - string name = registration.Name; - - ObjectFactoryBase factory; - if (_RegisteredTypes.TryGetValue(new TypeRegistration(checkType, name), out factory)) - { - if (factory.AssumeConstruction) - return true; - - if (factory.Constructor == null) - return (GetBestConstructor(factory.CreatesType, parameters, options) != null) ? true : false; - else - return CanConstruct(factory.Constructor, parameters, options); - } - - // Fail if requesting named resolution and settings set to fail if unresolved - // Or bubble up if we have a parent - if (!string.IsNullOrEmpty(name) && options.NamedResolutionFailureAction == NamedResolutionFailureActions.Fail) - return (_Parent != null) ? _Parent.CanResolveInternal(registration, parameters, options) : false; - - // Attemped unnamed fallback container resolution if relevant and requested - if (!string.IsNullOrEmpty(name) && options.NamedResolutionFailureAction == NamedResolutionFailureActions.AttemptUnnamedResolution) - { - if (_RegisteredTypes.TryGetValue(new TypeRegistration(checkType), out factory)) - { - if (factory.AssumeConstruction) - return true; - - return (GetBestConstructor(factory.CreatesType, parameters, options) != null) ? true : false; - } - } - - // Check if type is an automatic lazy factory request - if (IsAutomaticLazyFactoryRequest(checkType)) - return true; - - // Check if type is an IEnumerable - if (IsIEnumerableRequest(registration.Type)) - return true; - - // Attempt unregistered construction if possible and requested - // If we cant', bubble if we have a parent - if ((options.UnregisteredResolutionAction == UnregisteredResolutionActions.AttemptResolve) || (checkType.IsGenericType() && options.UnregisteredResolutionAction == UnregisteredResolutionActions.GenericsOnly)) - return (GetBestConstructor(checkType, parameters, options) != null) ? true : (_Parent != null) ? _Parent.CanResolveInternal(registration, parameters, options) : false; - - // Bubble resolution up the container tree if we have a parent - if (_Parent != null) - return _Parent.CanResolveInternal(registration, parameters, options); - - return false; - } - - private bool IsIEnumerableRequest(Type type) - { - if (!type.IsGenericType()) - return false; - - Type genericType = type.GetGenericTypeDefinition(); - - if (genericType == typeof(IEnumerable<>)) - return true; - - return false; - } - - private bool IsAutomaticLazyFactoryRequest(Type type) - { - if (!type.IsGenericType()) - return false; - - Type genericType = type.GetGenericTypeDefinition(); - - // Just a func - if (genericType == typeof(Func<>)) - return true; - - // 2 parameter func with string as first parameter (name) - //#if NETFX_CORE - // if ((genericType == typeof(Func<,>) && type.GetTypeInfo().GenericTypeArguments[0] == typeof(string))) - //#else - if ((genericType == typeof(Func<,>) && type.GetGenericArguments()[0] == typeof(string))) - - //#endif - return true; - - // 3 parameter func with string as first parameter (name) and IDictionary as second (parameters) - //#if NETFX_CORE - // if ((genericType == typeof(Func<,,>) && type.GetTypeInfo().GenericTypeArguments[0] == typeof(string) && type.GetTypeInfo().GenericTypeArguments[1] == typeof(IDictionary))) - //#else - if ((genericType == typeof(Func<,,>) && type.GetGenericArguments()[0] == typeof(string) && type.GetGenericArguments()[1] == typeof(IDictionary))) - - //#endif - return true; - - return false; - } - - private ObjectFactoryBase GetParentObjectFactory(TypeRegistration registration) - { - if (_Parent == null) - return null; - - ObjectFactoryBase factory; - if (_Parent._RegisteredTypes.TryGetValue(registration, out factory)) - { - return factory.GetFactoryForChildContainer(registration.Type, _Parent, this); - } - - return _Parent.GetParentObjectFactory(registration); - } - - private object ResolveInternal(TypeRegistration registration, NamedParameterOverloads parameters, ResolveOptions options) - { - ObjectFactoryBase factory; - - // Attempt container resolution - if (_RegisteredTypes.TryGetValue(registration, out factory)) - { - try - { - return factory.GetObject(registration.Type, this, parameters, options); - } - catch (TinyIoCResolutionException) - { - throw; - } - catch (Exception ex) - { - throw new TinyIoCResolutionException(registration.Type, ex); - } - } - -#if RESOLVE_OPEN_GENERICS - // Attempt container resolution of open generic - if (registration.Type.IsGenericType()) - { - var openTypeRegistration = new TypeRegistration(registration.Type.GetGenericTypeDefinition(), - registration.Name); - - if (_RegisteredTypes.TryGetValue(openTypeRegistration, out factory)) - { - try - { - return factory.GetObject(registration.Type, this, parameters, options); - } - catch (TinyIoCResolutionException) - { - throw; - } - catch (Exception ex) - { - throw new TinyIoCResolutionException(registration.Type, ex); - } - } - } -#endif - - // Attempt to get a factory from parent if we can - var bubbledObjectFactory = GetParentObjectFactory(registration); - if (bubbledObjectFactory != null) - { - try - { - return bubbledObjectFactory.GetObject(registration.Type, this, parameters, options); - } - catch (TinyIoCResolutionException) - { - throw; - } - catch (Exception ex) - { - throw new TinyIoCResolutionException(registration.Type, ex); - } - } - - // Fail if requesting named resolution and settings set to fail if unresolved - if (!string.IsNullOrEmpty(registration.Name) && options.NamedResolutionFailureAction == NamedResolutionFailureActions.Fail) - throw new TinyIoCResolutionException(registration.Type); - - // Attemped unnamed fallback container resolution if relevant and requested - if (!string.IsNullOrEmpty(registration.Name) && options.NamedResolutionFailureAction == NamedResolutionFailureActions.AttemptUnnamedResolution) - { - if (_RegisteredTypes.TryGetValue(new TypeRegistration(registration.Type, string.Empty), out factory)) - { - try - { - return factory.GetObject(registration.Type, this, parameters, options); - } - catch (TinyIoCResolutionException) - { - throw; - } - catch (Exception ex) - { - throw new TinyIoCResolutionException(registration.Type, ex); - } - } - } - -#if EXPRESSIONS - // Attempt to construct an automatic lazy factory if possible - if (IsAutomaticLazyFactoryRequest(registration.Type)) - return GetLazyAutomaticFactoryRequest(registration.Type); -#endif - if (IsIEnumerableRequest(registration.Type)) - return GetIEnumerableRequest(registration.Type); - - // Attempt unregistered construction if possible and requested - if ((options.UnregisteredResolutionAction == UnregisteredResolutionActions.AttemptResolve) || (registration.Type.IsGenericType() && options.UnregisteredResolutionAction == UnregisteredResolutionActions.GenericsOnly)) - { - if (!registration.Type.IsAbstract() && !registration.Type.IsInterface()) - return ConstructType(null, registration.Type, parameters, options); - } - - // Unable to resolve - throw - throw new TinyIoCResolutionException(registration.Type); - } - -#if EXPRESSIONS - private object GetLazyAutomaticFactoryRequest(Type type) - { - if (!type.IsGenericType()) - return null; - - Type genericType = type.GetGenericTypeDefinition(); - //#if NETFX_CORE - // Type[] genericArguments = type.GetTypeInfo().GenericTypeArguments.ToArray(); - //#else - Type[] genericArguments = type.GetGenericArguments(); - //#endif - - // Just a func - if (genericType == typeof(Func<>)) - { - Type returnType = genericArguments[0]; - - //#if NETFX_CORE - // MethodInfo resolveMethod = typeof(TinyIoCContainer).GetTypeInfo().GetDeclaredMethods("Resolve").First(mi => !mi.GetParameters().Any()); - //#else - MethodInfo resolveMethod = typeof(TinyIoCContainer).GetMethod("Resolve", Array.Empty()); - - //#endif - resolveMethod = resolveMethod.MakeGenericMethod(returnType); - - var resolveCall = Expression.Call(Expression.Constant(this), resolveMethod); - - var resolveLambda = Expression.Lambda(resolveCall).Compile(); - - return resolveLambda; - } - - // 2 parameter func with string as first parameter (name) - if ((genericType == typeof(Func<,>)) && (genericArguments[0] == typeof(string))) - { - Type returnType = genericArguments[1]; - - //#if NETFX_CORE - // MethodInfo resolveMethod = typeof(TinyIoCContainer).GetTypeInfo().GetDeclaredMethods("Resolve").First(mi => mi.GetParameters().Length == 1 && mi.GetParameters()[0].GetType() == typeof(String)); - //#else - MethodInfo resolveMethod = typeof(TinyIoCContainer).GetMethod("Resolve", new Type[] { typeof(string) }); - //#endif - resolveMethod = resolveMethod.MakeGenericMethod(returnType); - - ParameterExpression[] resolveParameters = new ParameterExpression[] { Expression.Parameter(typeof(string), "name") }; - var resolveCall = Expression.Call(Expression.Constant(this), resolveMethod, resolveParameters); - - var resolveLambda = Expression.Lambda(resolveCall, resolveParameters).Compile(); - - return resolveLambda; - } - - // 3 parameter func with string as first parameter (name) and IDictionary as second (parameters) - //#if NETFX_CORE - // if ((genericType == typeof(Func<,,>) && type.GenericTypeArguments[0] == typeof(string) && type.GenericTypeArguments[1] == typeof(IDictionary))) - //#else - if ((genericType == typeof(Func<,,>) && type.GetGenericArguments()[0] == typeof(string) && type.GetGenericArguments()[1] == typeof(IDictionary))) - //#endif - { - Type returnType = genericArguments[2]; - - var name = Expression.Parameter(typeof(string), "name"); - var parameters = Expression.Parameter(typeof(IDictionary), "parameters"); - - //#if NETFX_CORE - // MethodInfo resolveMethod = typeof(TinyIoCContainer).GetTypeInfo().GetDeclaredMethods("Resolve").First(mi => mi.GetParameters().Length == 2 && mi.GetParameters()[0].GetType() == typeof(String) && mi.GetParameters()[1].GetType() == typeof(NamedParameterOverloads)); - //#else - MethodInfo resolveMethod = typeof(TinyIoCContainer).GetMethod("Resolve", new Type[] { typeof(string), typeof(NamedParameterOverloads) }); - //#endif - resolveMethod = resolveMethod.MakeGenericMethod(returnType); - - var resolveCall = Expression.Call(Expression.Constant(this), resolveMethod, name, Expression.Call(typeof(NamedParameterOverloads), "FromIDictionary", null, parameters)); - - var resolveLambda = Expression.Lambda(resolveCall, name, parameters).Compile(); - - return resolveLambda; - } - - throw new TinyIoCResolutionException(type); - } -#endif - private object GetIEnumerableRequest(Type type) - { - //#if NETFX_CORE - // var genericResolveAllMethod = this.GetType().GetGenericMethod("ResolveAll", type.GenericTypeArguments, new[] { typeof(bool) }); - //#else - var genericResolveAllMethod = this.GetType().GetGenericMethod(BindingFlags.Public | BindingFlags.Instance, "ResolveAll", type.GetGenericArguments(), new[] { typeof(bool) }); - //#endif - - return genericResolveAllMethod.Invoke(this, new object[] { false }); - } - - private bool CanConstruct(ConstructorInfo ctor, NamedParameterOverloads parameters, ResolveOptions options) - { - if (parameters == null) - throw new ArgumentNullException("parameters"); - - foreach (var parameter in ctor.GetParameters()) - { - if (string.IsNullOrEmpty(parameter.Name)) - return false; - - var isParameterOverload = parameters.ContainsKey(parameter.Name); - - //#if NETFX_CORE - // if (parameter.ParameterType.GetTypeInfo().IsPrimitive && !isParameterOverload) - //#else - if (parameter.ParameterType.IsPrimitive() && !isParameterOverload) - - //#endif - return false; - - if (!isParameterOverload && !CanResolveInternal(new TypeRegistration(parameter.ParameterType), NamedParameterOverloads.Default, options)) - return false; - } - - return true; - } - - private ConstructorInfo GetBestConstructor(Type type, NamedParameterOverloads parameters, ResolveOptions options) - { - if (parameters == null) - throw new ArgumentNullException("parameters"); - - //#if NETFX_CORE - // if (type.GetTypeInfo().IsValueType) - //#else - if (type.IsValueType()) - - //#endif - return null; - - // Get constructors in reverse order based on the number of parameters - // i.e. be as "greedy" as possible so we satify the most amount of dependencies possible - var ctors = this.GetTypeConstructors(type); - - foreach (var ctor in ctors) - { - if (this.CanConstruct(ctor, parameters, options)) - return ctor; - } - - return null; - } - - private IEnumerable GetTypeConstructors(Type type) - { - //#if NETFX_CORE - // return type.GetTypeInfo().DeclaredConstructors.OrderByDescending(ctor => ctor.GetParameters().Count()); - //#else - return type.GetConstructors().OrderByDescending(ctor => ctor.GetParameters().Length); - - //#endif - } - - private object ConstructType(Type requestedType, Type implementationType, ResolveOptions options) - { - return ConstructType(requestedType, implementationType, null, NamedParameterOverloads.Default, options); - } - - private object ConstructType(Type requestedType, Type implementationType, ConstructorInfo constructor, ResolveOptions options) - { - return ConstructType(requestedType, implementationType, constructor, NamedParameterOverloads.Default, options); - } - - private object ConstructType(Type requestedType, Type implementationType, NamedParameterOverloads parameters, ResolveOptions options) - { - return ConstructType(requestedType, implementationType, null, parameters, options); - } - - private object ConstructType(Type requestedType, Type implementationType, ConstructorInfo constructor, NamedParameterOverloads parameters, ResolveOptions options) - { - var typeToConstruct = implementationType; - -#if RESOLVE_OPEN_GENERICS - if (implementationType.IsGenericTypeDefinition()) - { - if (requestedType == null || !requestedType.IsGenericType() || !requestedType.GetGenericArguments().Any()) - throw new TinyIoCResolutionException(typeToConstruct); - - typeToConstruct = typeToConstruct.MakeGenericType(requestedType.GetGenericArguments()); - } -#endif - if (constructor == null) - { - // Try and get the best constructor that we can construct - // if we can't construct any then get the constructor - // with the least number of parameters so we can throw a meaningful - // resolve exception - constructor = GetBestConstructor(typeToConstruct, parameters, options) ?? GetTypeConstructors(typeToConstruct).LastOrDefault(); - } - - if (constructor == null) - throw new TinyIoCResolutionException(typeToConstruct); - - var ctorParams = constructor.GetParameters(); - object[] args = new object[ctorParams.Length]; - - for (int parameterIndex = 0; parameterIndex < ctorParams.Length; parameterIndex++) - { - var currentParam = ctorParams[parameterIndex]; - - try - { - if (ctorParams[parameterIndex].ParameterType == typeof(Logger)) - { - args[parameterIndex] = LogManager.GetLogger(implementationType.Name); - } - else - { - args[parameterIndex] = parameters.ContainsKey(currentParam.Name) ? - parameters[currentParam.Name] : - ResolveInternal( - new TypeRegistration(currentParam.ParameterType), - NamedParameterOverloads.Default, - options); - } - } - catch (TinyIoCResolutionException ex) - { - // If a constructor parameter can't be resolved - // it will throw, so wrap it and throw that this can't - // be resolved. - throw new TinyIoCResolutionException(typeToConstruct, ex); - } - catch (Exception ex) - { - throw new TinyIoCResolutionException(typeToConstruct, ex); - } - } - - try - { -#if USE_OBJECT_CONSTRUCTOR - var constructionDelegate = CreateObjectConstructionDelegateWithCache(constructor); - return constructionDelegate.Invoke(args); -#else - return constructor.Invoke(args); -#endif - } - catch (Exception ex) - { - throw new TinyIoCResolutionException(typeToConstruct, ex); - } - } - -#if USE_OBJECT_CONSTRUCTOR - private static ObjectConstructor CreateObjectConstructionDelegateWithCache(ConstructorInfo constructor) - { - ObjectConstructor objectConstructor; - if (_ObjectConstructorCache.TryGetValue(constructor, out objectConstructor)) - return objectConstructor; - - // We could lock the cache here, but there's no real side - // effect to two threads creating the same ObjectConstructor - // at the same time, compared to the cost of a lock for - // every creation. - var constructorParams = constructor.GetParameters(); - var lambdaParams = Expression.Parameter(typeof(object[]), "parameters"); - var newParams = new Expression[constructorParams.Length]; - - for (int i = 0; i < constructorParams.Length; i++) - { - var paramsParameter = Expression.ArrayIndex(lambdaParams, Expression.Constant(i)); - - newParams[i] = Expression.Convert(paramsParameter, constructorParams[i].ParameterType); - } - - var newExpression = Expression.New(constructor, newParams); - - var constructionLambda = Expression.Lambda(typeof(ObjectConstructor), newExpression, lambdaParams); - - objectConstructor = (ObjectConstructor)constructionLambda.Compile(); - - _ObjectConstructorCache[constructor] = objectConstructor; - return objectConstructor; - } -#endif - - private void BuildUpInternal(object input, ResolveOptions resolveOptions) - { - //#if NETFX_CORE - // var properties = from property in input.GetType().GetTypeInfo().DeclaredProperties - // where (property.GetMethod != null) && (property.SetMethod != null) && !property.PropertyType.GetTypeInfo().IsValueType - // select property; - //#else - var properties = from property in input.GetType().GetProperties() - where (property.GetGetMethod() != null) && (property.GetSetMethod() != null) && !property.PropertyType.IsValueType() - select property; - //#endif - - foreach (var property in properties) - { - if (property.GetValue(input, null) == null) - { - try - { - property.SetValue(input, ResolveInternal(new TypeRegistration(property.PropertyType), NamedParameterOverloads.Default, resolveOptions), null); - } - catch (TinyIoCResolutionException) - { - // Catch any resolution errors and ignore them - } - } - } - } - - private IEnumerable GetParentRegistrationsForType(Type resolveType) - { - if (_Parent == null) - return Array.Empty(); - - var registrations = _Parent._RegisteredTypes.Keys.Where(tr => tr.Type == resolveType); - - return registrations.Concat(_Parent.GetParentRegistrationsForType(resolveType)); - } - - private IEnumerable ResolveAllInternal(Type resolveType, bool includeUnnamed) - { - var registrations = _RegisteredTypes.Keys.Where(tr => tr.Type == resolveType).Concat(GetParentRegistrationsForType(resolveType)); - - if (!includeUnnamed) - registrations = registrations.Where(tr => !string.IsNullOrEmpty(tr.Name)); - - return registrations.Select(registration => this.ResolveInternal(registration, NamedParameterOverloads.Default, ResolveOptions.Default)); - } - - private static bool IsValidAssignment(Type registerType, Type registerImplementation) - { - //#if NETFX_CORE - // var registerTypeDef = registerType.GetTypeInfo(); - // var registerImplementationDef = registerImplementation.GetTypeInfo(); - - // if (!registerTypeDef.IsGenericTypeDefinition) - // { - // if (!registerTypeDef.IsAssignableFrom(registerImplementationDef)) - // return false; - // } - // else - // { - // if (registerTypeDef.IsInterface()) - // { - // if (!registerImplementationDef.ImplementedInterfaces.Any(t => t.GetTypeInfo().Name == registerTypeDef.Name)) - // return false; - // } - // else if (registerTypeDef.IsAbstract() && registerImplementationDef.BaseType() != registerType) - // { - // return false; - // } - // } - //#else - if (!registerType.IsGenericTypeDefinition()) - { - if (!registerType.IsAssignableFrom(registerImplementation)) - return false; - } - else - { - if (registerType.IsInterface()) - { - if (!registerImplementation.FindInterfaces((t, o) => t.Name == registerType.Name, null).Any()) - return false; - } - else if (registerType.IsAbstract() && registerImplementation.BaseType() != registerType) - { - return false; - } - } - - //#endif - return true; - } - - #endregion - - #region IDisposable Members - private bool disposed = false; - public void Dispose() - { - if (!disposed) - { - disposed = true; - - _RegisteredTypes.Dispose(); - - GC.SuppressFinalize(this); - } - } - - #endregion - } -} - -// reverse shim for WinRT SR changes... -#if !NETFX_CORE -namespace System.Reflection -{ - public static class ReverseTypeExtender - { - public static bool IsClass(this Type type) - { - return type.IsClass; - } - - public static bool IsAbstract(this Type type) - { - return type.IsAbstract; - } - - public static bool IsInterface(this Type type) - { - return type.IsInterface; - } - - public static bool IsPrimitive(this Type type) - { - return type.IsPrimitive; - } - - public static bool IsValueType(this Type type) - { - return type.IsValueType; - } - - public static bool IsGenericType(this Type type) - { - return type.IsGenericType; - } - - public static bool IsGenericParameter(this Type type) - { - return type.IsGenericParameter; - } - - public static bool IsGenericTypeDefinition(this Type type) - { - return type.IsGenericTypeDefinition; - } - - public static Type BaseType(this Type type) - { - return type.BaseType; - } - - public static Assembly Assembly(this Type type) - { - return type.Assembly; - } - } -} -#endif diff --git a/src/NzbDrone.Console/ConsoleAlerts.cs b/src/NzbDrone.Console/ConsoleAlerts.cs deleted file mode 100644 index 04893b18a..000000000 --- a/src/NzbDrone.Console/ConsoleAlerts.cs +++ /dev/null @@ -1,15 +0,0 @@ -using NzbDrone.Host; - -namespace NzbDrone.Console -{ - public class ConsoleAlerts : IUserAlert - { - public void Alert(string message) - { - System.Console.WriteLine(); - System.Console.WriteLine(message); - System.Console.WriteLine("Press enter to continue"); - System.Console.ReadLine(); - } - } -} diff --git a/src/NzbDrone.Console/ConsoleApp.cs b/src/NzbDrone.Console/ConsoleApp.cs index 5730710df..8c09bc9a1 100644 --- a/src/NzbDrone.Console/ConsoleApp.cs +++ b/src/NzbDrone.Console/ConsoleApp.cs @@ -1,5 +1,9 @@ using System; +using System.IO; using System.Net.Sockets; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; using NLog; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Exceptions; @@ -14,7 +18,7 @@ namespace NzbDrone.Console { private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(ConsoleApp)); - private enum ExitCodes : int + private enum ExitCodes { Normal = 0, UnknownFailure = 1, @@ -40,7 +44,7 @@ namespace NzbDrone.Console throw; } - Bootstrap.Start(startupArgs, new ConsoleAlerts()); + Bootstrap.Start(args); } catch (SonarrStartupException ex) { @@ -56,6 +60,20 @@ namespace NzbDrone.Console Logger.Fatal(ex.Message + ". This can happen if another instance of Sonarr is already running another application is using the same port (default: 8989) or the user has insufficient permissions"); Exit(ExitCodes.RecoverableFailure, startupArgs); } + catch (IOException ex) + { + if (ex.InnerException is AddressInUseException) + { + System.Console.WriteLine(""); + System.Console.WriteLine(""); + Logger.Fatal(ex.Message + " This can happen if another instance of Sonarr is already running another application is using the same port (default: 8989) or the user has insufficient permissions"); + Exit(ExitCodes.RecoverableFailure, startupArgs); + } + else + { + throw; + } + } catch (RemoteAccessException ex) { System.Console.WriteLine(""); diff --git a/src/NzbDrone.Core.Test/Framework/MigrationTest.cs b/src/NzbDrone.Core.Test/Framework/MigrationTest.cs index fb5ffbc6d..9ac581a83 100644 --- a/src/NzbDrone.Core.Test/Framework/MigrationTest.cs +++ b/src/NzbDrone.Core.Test/Framework/MigrationTest.cs @@ -2,6 +2,7 @@ using System; using System.Data; using FluentMigrator; using Microsoft.Extensions.Logging; +using NLog.Extensions.Logging; using NUnit.Framework; using NzbDrone.Core.Datastore.Migration.Framework; @@ -32,7 +33,7 @@ namespace NzbDrone.Core.Test.Framework protected override void SetupLogging() { - Mocker.SetConstant(Mocker.Resolve()); + Mocker.SetConstant(Mocker.Resolve()); } private ITestDatabase WithMigrationAction(Action beforeMigration = null) diff --git a/src/NzbDrone.Core/Datastore/DbFactory.cs b/src/NzbDrone.Core/Datastore/DbFactory.cs index d0511ea21..b24fbae85 100644 --- a/src/NzbDrone.Core/Datastore/DbFactory.cs +++ b/src/NzbDrone.Core/Datastore/DbFactory.cs @@ -1,7 +1,6 @@ using System; using System.Data.SQLite; using NLog; -using NzbDrone.Common.Composition; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Exceptions; @@ -40,17 +39,6 @@ namespace NzbDrone.Core.Datastore Environment.SetEnvironmentVariable("No_SQLiteFunctions", "true"); } - public static void RegisterDatabase(IContainer container) - { - var mainDb = new MainDatabase(container.Resolve().Create()); - - container.Register(mainDb); - - var logDb = new LogDatabase(container.Resolve().Create(MigrationType.Log)); - - container.Register(logDb); - } - public DbFactory(IMigrationController migrationController, IConnectionStringFactory connectionStringFactory, IDiskProvider diskProvider, diff --git a/src/NzbDrone.Core/Datastore/Extensions/CompositionExtensions.cs b/src/NzbDrone.Core/Datastore/Extensions/CompositionExtensions.cs new file mode 100644 index 000000000..c5e31f92c --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Extensions/CompositionExtensions.cs @@ -0,0 +1,24 @@ +using DryIoc; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Extensions +{ + public static class CompositionExtensions + { + public static IContainer AddDatabase(this IContainer container) + { + container.RegisterDelegate(f => new MainDatabase(f.Create()), Reuse.Singleton); + container.RegisterDelegate(f => new LogDatabase(f.Create(MigrationType.Log)), Reuse.Singleton); + + return container; + } + + public static IContainer AddDummyDatabase(this IContainer container) + { + container.RegisterInstance(new MainDatabase(null)); + container.RegisterInstance(new LogDatabase(null)); + + return container; + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationController.cs b/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationController.cs index e49b7b493..2a788e380 100644 --- a/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationController.cs +++ b/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationController.cs @@ -7,6 +7,7 @@ using FluentMigrator.Runner.Processors; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using NLog; +using NLog.Extensions.Logging; namespace NzbDrone.Core.Datastore.Migration.Framework { @@ -34,7 +35,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework _logger.Info("*** Migrating {0} ***", connectionString); var serviceProvider = new ServiceCollection() - .AddLogging(lb => lb.AddProvider(_migrationLoggerProvider)) + .AddLogging(b => b.AddNLog()) .AddFluentMigratorCore() .ConfigureRunner( builder => builder diff --git a/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationLogger.cs b/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationLogger.cs deleted file mode 100644 index 47ff934e1..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationLogger.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using FluentMigrator.Runner; -using FluentMigrator.Runner.Logging; -using NLog; - -namespace NzbDrone.Core.Datastore.Migration.Framework -{ - public class MigrationLogger : FluentMigratorLogger - { - private readonly Logger _logger; - - public MigrationLogger(Logger logger, - FluentMigratorLoggerOptions options) - : base(options) - { - _logger = logger; - } - - protected override void WriteHeading(string message) - { - _logger.Info("*** {0} ***", message); - } - - protected override void WriteSay(string message) - { - _logger.Debug(message); - } - - protected override void WriteEmphasize(string message) - { - _logger.Warn(message); - } - - protected override void WriteSql(string sql) - { - _logger.Debug(sql); - } - - protected override void WriteEmptySql() - { - _logger.Debug(@"No SQL statement executed."); - } - - protected override void WriteElapsedTime(TimeSpan timeSpan) - { - _logger.Debug("Took: {0}", timeSpan); - } - - protected override void WriteError(string message) - { - _logger.Error(message); - } - - protected override void WriteError(Exception exception) - { - _logger.Error(exception); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationLoggerProvider.cs b/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationLoggerProvider.cs deleted file mode 100644 index 23d2dafe0..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationLoggerProvider.cs +++ /dev/null @@ -1,26 +0,0 @@ -using FluentMigrator.Runner; -using Microsoft.Extensions.Logging; -using NLog; -using ILogger = Microsoft.Extensions.Logging.ILogger; - -namespace NzbDrone.Core.Datastore.Migration.Framework -{ - public class MigrationLoggerProvider : ILoggerProvider - { - private readonly Logger _logger; - - public MigrationLoggerProvider(Logger logger) - { - _logger = logger; - } - - public ILogger CreateLogger(string categoryName) - { - return new MigrationLogger(_logger, new FluentMigratorLoggerOptions() { ShowElapsedTime = true, ShowSql = true }); - } - - public void Dispose() - { - } - } -} diff --git a/src/NzbDrone.Core/Download/DownloadClientFactory.cs b/src/NzbDrone.Core/Download/DownloadClientFactory.cs index 8ba7be505..2d77e434f 100644 --- a/src/NzbDrone.Core/Download/DownloadClientFactory.cs +++ b/src/NzbDrone.Core/Download/DownloadClientFactory.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using FluentValidation.Results; using NLog; -using NzbDrone.Common.Composition; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.ThingiProvider; @@ -22,7 +21,7 @@ namespace NzbDrone.Core.Download public DownloadClientFactory(IDownloadClientStatusService downloadClientStatusService, IDownloadClientRepository providerRepository, IEnumerable providers, - IContainer container, + IServiceProvider container, IEventAggregator eventAggregator, Logger logger) : base(providerRepository, providers, container, eventAggregator, logger) diff --git a/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs b/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs index 0bb4a4a4b..c47ff8058 100644 --- a/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs +++ b/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs @@ -19,7 +19,7 @@ namespace NzbDrone.Core.Extras public ExistingExtraFileService(IDiskProvider diskProvider, IDiskScanService diskScanService, - List existingExtraFileImporters, + IEnumerable existingExtraFileImporters, Logger logger) { _diskProvider = diskProvider; diff --git a/src/NzbDrone.Core/Extras/Metadata/MetadataFactory.cs b/src/NzbDrone.Core/Extras/Metadata/MetadataFactory.cs index b52d8a18c..5202d89ef 100644 --- a/src/NzbDrone.Core/Extras/Metadata/MetadataFactory.cs +++ b/src/NzbDrone.Core/Extras/Metadata/MetadataFactory.cs @@ -17,7 +17,7 @@ namespace NzbDrone.Core.Extras.Metadata { private readonly IMetadataRepository _providerRepository; - public MetadataFactory(IMetadataRepository providerRepository, IEnumerable providers, IContainer container, IEventAggregator eventAggregator, Logger logger) + public MetadataFactory(IMetadataRepository providerRepository, IEnumerable providers, IServiceProvider container, IEventAggregator eventAggregator, Logger logger) : base(providerRepository, providers, container, eventAggregator, logger) { _providerRepository = providerRepository; diff --git a/src/NzbDrone.Core/ImportLists/ImportListFactory.cs b/src/NzbDrone.Core/ImportLists/ImportListFactory.cs index 79165d275..aed9ad44d 100644 --- a/src/NzbDrone.Core/ImportLists/ImportListFactory.cs +++ b/src/NzbDrone.Core/ImportLists/ImportListFactory.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using FluentValidation.Results; @@ -21,7 +22,7 @@ namespace NzbDrone.Core.ImportLists public ImportListFactory(IImportListStatusService importListStatusService, IImportListRepository providerRepository, IEnumerable providers, - IContainer container, + IServiceProvider container, IEventAggregator eventAggregator, Logger logger) : base(providerRepository, providers, container, eventAggregator, logger) diff --git a/src/NzbDrone.Core/Indexers/IndexerFactory.cs b/src/NzbDrone.Core/Indexers/IndexerFactory.cs index 902de54b9..08b260609 100644 --- a/src/NzbDrone.Core/Indexers/IndexerFactory.cs +++ b/src/NzbDrone.Core/Indexers/IndexerFactory.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using FluentValidation.Results; @@ -23,7 +24,7 @@ namespace NzbDrone.Core.Indexers public IndexerFactory(IIndexerStatusService indexerStatusService, IIndexerRepository providerRepository, IEnumerable providers, - IContainer container, + IServiceProvider container, IEventAggregator eventAggregator, Logger logger) : base(providerRepository, providers, container, eventAggregator, logger) diff --git a/src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs b/src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs index 7e038561c..97c7b3ca5 100644 --- a/src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs +++ b/src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs @@ -5,6 +5,7 @@ using System.Net; using System.Threading; using NLog; using NzbDrone.Common; +using NzbDrone.Common.Composition; using NzbDrone.Common.EnsureThat; using NzbDrone.Common.Serializer; using NzbDrone.Core.Exceptions; @@ -36,17 +37,18 @@ namespace NzbDrone.Core.Messaging.Commands public class CommandQueueManager : IManageCommandQueue, IHandle { private readonly ICommandRepository _repo; - private readonly IServiceFactory _serviceFactory; + private readonly KnownTypes _knownTypes; private readonly Logger _logger; private readonly CommandQueue _commandQueue; public CommandQueueManager(ICommandRepository repo, IServiceFactory serviceFactory, + KnownTypes knownTypes, Logger logger) { _repo = repo; - _serviceFactory = serviceFactory; + _knownTypes = knownTypes; _logger = logger; _commandQueue = new CommandQueue(); @@ -232,9 +234,8 @@ namespace NzbDrone.Core.Messaging.Commands private dynamic GetCommand(string commandName) { commandName = commandName.Split('.').Last(); - - var commandType = _serviceFactory.GetImplementations(typeof(Command)) - .Single(c => c.Name.Equals(commandName, StringComparison.InvariantCultureIgnoreCase)); + var commands = _knownTypes.GetImplementations(typeof(Command)); + var commandType = commands.Single(c => c.Name.Equals(commandName, StringComparison.InvariantCultureIgnoreCase)); return Json.Deserialize("{}", commandType); } diff --git a/src/NzbDrone.Core/Notifications/NotificationFactory.cs b/src/NzbDrone.Core/Notifications/NotificationFactory.cs index 6e526637c..115557a3b 100644 --- a/src/NzbDrone.Core/Notifications/NotificationFactory.cs +++ b/src/NzbDrone.Core/Notifications/NotificationFactory.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Common.Composition; @@ -22,7 +23,7 @@ namespace NzbDrone.Core.Notifications public class NotificationFactory : ProviderFactory, INotificationFactory { - public NotificationFactory(INotificationRepository providerRepository, IEnumerable providers, IContainer container, IEventAggregator eventAggregator, Logger logger) + public NotificationFactory(INotificationRepository providerRepository, IEnumerable providers, IServiceProvider container, IEventAggregator eventAggregator, Logger logger) : base(providerRepository, providers, container, eventAggregator, logger) { } diff --git a/src/NzbDrone.Core/Sonarr.Core.csproj b/src/NzbDrone.Core/Sonarr.Core.csproj index c7129a7c1..3eb5abff7 100644 --- a/src/NzbDrone.Core/Sonarr.Core.csproj +++ b/src/NzbDrone.Core/Sonarr.Core.csproj @@ -6,13 +6,15 @@ + + + - diff --git a/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs b/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs index a8af11ca1..5648def45 100644 --- a/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs +++ b/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using FluentValidation.Results; +using Microsoft.Extensions.DependencyInjection; using NLog; using NzbDrone.Common.Composition; using NzbDrone.Core.Lifecycle; @@ -15,7 +16,7 @@ namespace NzbDrone.Core.ThingiProvider where TProvider : IProvider { private readonly IProviderRepository _providerRepository; - private readonly IContainer _container; + private readonly IServiceProvider _container; private readonly IEventAggregator _eventAggregator; private readonly Logger _logger; @@ -23,7 +24,7 @@ namespace NzbDrone.Core.ThingiProvider protected ProviderFactory(IProviderRepository providerRepository, IEnumerable providers, - IContainer container, + IServiceProvider container, IEventAggregator eventAggregator, Logger logger) { @@ -129,7 +130,7 @@ namespace NzbDrone.Core.ThingiProvider public TProvider GetInstance(TProviderDefinition definition) { var type = GetImplementation(definition); - var instance = (TProvider)_container.Resolve(type); + var instance = (TProvider)_container.GetRequiredService(type); instance.Definition = definition; SetProviderCharacteristics(instance, definition); return instance; diff --git a/src/NzbDrone.Core/Validation/Paths/RecycleBinValidator.cs b/src/NzbDrone.Core/Validation/Paths/RecycleBinValidator.cs new file mode 100644 index 000000000..a8a714fe6 --- /dev/null +++ b/src/NzbDrone.Core/Validation/Paths/RecycleBinValidator.cs @@ -0,0 +1,44 @@ +using FluentValidation.Validators; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; + +namespace NzbDrone.Core.Validation.Paths +{ + public class RecycleBinValidator : PropertyValidator + { + private readonly IConfigService _configService; + + public RecycleBinValidator(IConfigService configService) + : base("Path is {relationship} configured recycle bin folder") + { + _configService = configService; + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var recycleBin = _configService.RecycleBin; + var folder = context.PropertyValue.ToString(); + + if (context.PropertyValue == null || recycleBin.IsNullOrWhiteSpace()) + { + return true; + } + + if (recycleBin.PathEquals(folder)) + { + context.MessageFormatter.AppendArgument("relationship", "set to"); + + return false; + } + + if (recycleBin.IsParentPath(folder)) + { + context.MessageFormatter.AppendArgument("relationship", "child of"); + + return false; + } + + return true; + } + } +} diff --git a/src/NzbDrone.Core/Validation/Paths/RootFolderAncestorValidator.cs b/src/NzbDrone.Core/Validation/Paths/RootFolderAncestorValidator.cs new file mode 100644 index 000000000..19f0800b5 --- /dev/null +++ b/src/NzbDrone.Core/Validation/Paths/RootFolderAncestorValidator.cs @@ -0,0 +1,28 @@ +using System.Linq; +using FluentValidation.Validators; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.RootFolders; + +namespace NzbDrone.Core.Validation.Paths +{ + public class RootFolderAncestorValidator : PropertyValidator + { + private readonly IRootFolderService _rootFolderService; + + public RootFolderAncestorValidator(IRootFolderService rootFolderService) + : base("Path is an ancestor of an existing root folder") + { + _rootFolderService = rootFolderService; + } + + protected override bool IsValid(PropertyValidatorContext context) + { + if (context.PropertyValue == null) + { + return true; + } + + return !_rootFolderService.All().Any(s => context.PropertyValue.ToString().IsParentPath(s.Path)); + } + } +} diff --git a/src/NzbDrone.Host.Test/ContainerFixture.cs b/src/NzbDrone.Host.Test/ContainerFixture.cs index 9078bf48f..3debf33e6 100644 --- a/src/NzbDrone.Host.Test/ContainerFixture.cs +++ b/src/NzbDrone.Host.Test/ContainerFixture.cs @@ -1,12 +1,16 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; +using DryIoc; +using DryIoc.Microsoft.DependencyInjection; using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; using Moq; using NUnit.Framework; using NzbDrone.Common; -using NzbDrone.Common.Composition; +using NzbDrone.Common.Composition.Extensions; using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Core.Datastore; +using NzbDrone.Common.Instrumentation.Extensions; +using NzbDrone.Core.Datastore.Extensions; using NzbDrone.Core.Download; using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.Indexers; @@ -17,44 +21,50 @@ using NzbDrone.Core.Messaging.Events; using NzbDrone.Host; using NzbDrone.SignalR; using NzbDrone.Test.Common; +using IServiceProvider = System.IServiceProvider; namespace NzbDrone.App.Test { [TestFixture] public class ContainerFixture : TestBase { - private IContainer _container; + private IServiceProvider _container; [SetUp] public void SetUp() { var args = new StartupContext("first", "second"); - _container = MainAppContainerBuilder.BuildContainer(args); - - _container.Register(new MainDatabase(null)); - // set up a dummy broadcaster to allow tests to resolve var mockBroadcaster = new Mock(); - _container.Register(mockBroadcaster.Object); + + var container = new Container(rules => rules.WithNzbDroneRules()) + .AutoAddServices(Bootstrap.ASSEMBLIES) + .AddNzbDroneLogger() + .AddDummyDatabase() + .AddStartupContext(args); + + container.RegisterInstance(mockBroadcaster.Object); + + _container = container.GetServiceProvider(); } [Test] public void should_be_able_to_resolve_indexers() { - _container.Resolve>().Should().NotBeEmpty(); + _container.GetRequiredService>().Should().NotBeEmpty(); } [Test] public void should_be_able_to_resolve_downloadclients() { - _container.Resolve>().Should().NotBeEmpty(); + _container.GetRequiredService>().Should().NotBeEmpty(); } [Test] public void container_should_inject_itself() { - var factory = _container.Resolve(); + var factory = _container.GetRequiredService(); factory.Build().Should().NotBeNull(); } @@ -64,7 +74,7 @@ namespace NzbDrone.App.Test { var genericExecutor = typeof(IExecute<>).MakeGenericType(typeof(RssSyncCommand)); - var executor = _container.Resolve(genericExecutor); + var executor = _container.GetRequiredService(genericExecutor); executor.Should().NotBeNull(); executor.Should().BeAssignableTo>(); @@ -73,17 +83,17 @@ namespace NzbDrone.App.Test [Test] public void should_return_same_instance_via_resolve_and_resolveall() { - var first = (DownloadMonitoringService)_container.Resolve>(); - var second = _container.ResolveAll>().OfType().Single(); + var first = (DownloadMonitoringService)_container.GetRequiredService>(); + var second = _container.GetServices>().OfType().Single(); first.Should().BeSameAs(second); } [Test] - public void should_return_same_instance_of_singletons_by_same_interface() + public void should_return_same_instance_of_singletons_by_different_same_interface() { - var first = _container.ResolveAll>().OfType().Single(); - var second = _container.ResolveAll>().OfType().Single(); + var first = _container.GetServices>().OfType().Single(); + var second = _container.GetServices>().OfType().Single(); first.Should().BeSameAs(second); } @@ -91,8 +101,8 @@ namespace NzbDrone.App.Test [Test] public void should_return_same_instance_of_singletons_by_different_interfaces() { - var first = _container.ResolveAll>().OfType().Single(); - var second = (DownloadMonitoringService)_container.Resolve>(); + var first = _container.GetServices>().OfType().Single(); + var second = (DownloadMonitoringService)_container.GetRequiredService>(); first.Should().BeSameAs(second); } diff --git a/src/NzbDrone.Host.Test/RouterTest.cs b/src/NzbDrone.Host.Test/RouterTest.cs index 52793f31b..70c5ecfc4 100644 --- a/src/NzbDrone.Host.Test/RouterTest.cs +++ b/src/NzbDrone.Host.Test/RouterTest.cs @@ -1,4 +1,3 @@ -using System.ServiceProcess; using Moq; using NUnit.Framework; using NzbDrone.Common; @@ -10,7 +9,7 @@ using NzbDrone.Test.Common; namespace NzbDrone.App.Test { [TestFixture] - public class RouterTest : TestBase + public class RouterTest : TestBase { [SetUp] public void Setup() @@ -49,33 +48,6 @@ namespace NzbDrone.App.Test serviceProviderMock.Verify(c => c.Uninstall(ServiceProvider.SERVICE_NAME), Times.Once()); } - [Test] - public void Route_should_call_console_service_when_application_mode_is_console() - { - Mocker.GetMock().SetupGet(c => c.IsUserInteractive).Returns(true); - - Subject.Route(ApplicationModes.Interactive); - - Mocker.GetMock().Verify(c => c.Start(), Times.Once()); - } - - [Test] - public void Route_should_call_service_start_when_run_in_service_mode() - { - var envMock = Mocker.GetMock(); - var serviceProvider = Mocker.GetMock(); - - envMock.SetupGet(c => c.IsUserInteractive).Returns(false); - - serviceProvider.Setup(c => c.Run(It.IsAny())); - serviceProvider.Setup(c => c.ServiceExist(It.IsAny())).Returns(true); - serviceProvider.Setup(c => c.GetStatus(It.IsAny())).Returns(ServiceControllerStatus.StartPending); - - Subject.Route(ApplicationModes.Service); - - serviceProvider.Verify(c => c.Run(It.IsAny()), Times.Once()); - } - [Test] public void show_error_on_install_if_service_already_exist() { diff --git a/src/NzbDrone.Host/WebHost/AccessControl/RemoteAccessAdapter.cs b/src/NzbDrone.Host/AccessControl/RemoteAccessAdapter.cs similarity index 90% rename from src/NzbDrone.Host/WebHost/AccessControl/RemoteAccessAdapter.cs rename to src/NzbDrone.Host/AccessControl/RemoteAccessAdapter.cs index 518f2584f..183ddc8e1 100644 --- a/src/NzbDrone.Host/WebHost/AccessControl/RemoteAccessAdapter.cs +++ b/src/NzbDrone.Host/AccessControl/RemoteAccessAdapter.cs @@ -2,6 +2,11 @@ using NzbDrone.Common.EnvironmentInfo; namespace NzbDrone.Host.AccessControl { + public interface IRemoteAccessAdapter + { + void MakeAccessible(bool passive); + } + public class RemoteAccessAdapter : IRemoteAccessAdapter { private readonly IRuntimeInfo _runtimeInfo; diff --git a/src/NzbDrone.Host/AppLifetime.cs b/src/NzbDrone.Host/AppLifetime.cs new file mode 100644 index 000000000..b9ae84a38 --- /dev/null +++ b/src/NzbDrone.Host/AppLifetime.cs @@ -0,0 +1,120 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using NLog; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Processes; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Lifecycle; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Host; + +namespace NzbDrone.Host +{ + public class AppLifetime : IHostedService, IHandle + { + private readonly IHostApplicationLifetime _appLifetime; + private readonly IConfigFileProvider _configFileProvider; + private readonly IRuntimeInfo _runtimeInfo; + private readonly IStartupContext _startupContext; + private readonly IBrowserService _browserService; + private readonly IProcessProvider _processProvider; + private readonly IEventAggregator _eventAggregator; + private readonly IUtilityModeRouter _utilityModeRouter; + private readonly Logger _logger; + + public AppLifetime(IHostApplicationLifetime appLifetime, + IConfigFileProvider configFileProvider, + IRuntimeInfo runtimeInfo, + IStartupContext startupContext, + IBrowserService browserService, + IProcessProvider processProvider, + IEventAggregator eventAggregator, + IUtilityModeRouter utilityModeRouter, + Logger logger) + { + _appLifetime = appLifetime; + _configFileProvider = configFileProvider; + _runtimeInfo = runtimeInfo; + _startupContext = startupContext; + _browserService = browserService; + _processProvider = processProvider; + _eventAggregator = eventAggregator; + _utilityModeRouter = utilityModeRouter; + _logger = logger; + + appLifetime.ApplicationStarted.Register(OnAppStarted); + appLifetime.ApplicationStopped.Register(OnAppStopped); + } + + public Task StartAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + private void OnAppStarted() + { + _runtimeInfo.IsExiting = false; + + if (!_startupContext.Flags.Contains(StartupContext.NO_BROWSER) + && _configFileProvider.LaunchBrowser) + { + _browserService.LaunchWebUI(); + } + + _eventAggregator.PublishEvent(new ApplicationStartedEvent()); + } + + private void OnAppStopped() + { + if (_runtimeInfo.RestartPending) + { + var restartArgs = GetRestartArgs(); + + _logger.Info("Attempting restart with arguments: {0}", restartArgs); + _processProvider.SpawnNewProcess(_runtimeInfo.ExecutingApplication, restartArgs); + } + } + + private void Shutdown() + { + _logger.Info("Attempting to stop application."); + _logger.Info("Application has finished stop routine."); + _runtimeInfo.IsExiting = true; + _appLifetime.StopApplication(); + } + + private string GetRestartArgs() + { + var args = _startupContext.PreservedArguments; + + args += " /restart"; + + if (!args.Contains("/nobrowser")) + { + args += " /nobrowser"; + } + + return args; + } + + public void Handle(ApplicationShutdownRequested message) + { + if (!_runtimeInfo.IsWindowsService) + { + if (message.Restarting) + { + _runtimeInfo.RestartPending = true; + } + + LogManager.Configuration = null; + Shutdown(); + } + } + } +} diff --git a/src/NzbDrone.Host/ApplicationServer.cs b/src/NzbDrone.Host/ApplicationServer.cs deleted file mode 100644 index 3a39765f3..000000000 --- a/src/NzbDrone.Host/ApplicationServer.cs +++ /dev/null @@ -1,136 +0,0 @@ -using System; -using System.ServiceProcess; -using NLog; -using NzbDrone.Common.Composition; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Lifecycle; -using NzbDrone.Core.Messaging.Events; - -namespace NzbDrone.Host -{ - public interface INzbDroneServiceFactory - { - ServiceBase Build(); - } - - public interface INzbDroneConsoleFactory - { - void Start(); - void Shutdown(); - } - - public class NzbDroneServiceFactory : ServiceBase, INzbDroneServiceFactory - { - private readonly INzbDroneConsoleFactory _consoleFactory; - - public NzbDroneServiceFactory(INzbDroneConsoleFactory consoleFactory) - { - _consoleFactory = consoleFactory; - } - - protected override void OnStart(string[] args) - { - _consoleFactory.Start(); - } - - protected override void OnStop() - { - _consoleFactory.Shutdown(); - } - - public ServiceBase Build() - { - return this; - } - } - - public class DummyNzbDroneServiceFactory : INzbDroneServiceFactory - { - public ServiceBase Build() - { - return null; - } - } - - public class NzbDroneConsoleFactory : INzbDroneConsoleFactory, IHandle - { - private readonly IConfigFileProvider _configFileProvider; - private readonly IRuntimeInfo _runtimeInfo; - private readonly IHostController _hostController; - private readonly IStartupContext _startupContext; - private readonly IBrowserService _browserService; - private readonly IContainer _container; - private readonly Logger _logger; - - // private CancelHandler _cancelHandler; - public NzbDroneConsoleFactory(IConfigFileProvider configFileProvider, - IHostController hostController, - IRuntimeInfo runtimeInfo, - IStartupContext startupContext, - IBrowserService browserService, - IContainer container, - Logger logger) - { - _configFileProvider = configFileProvider; - _hostController = hostController; - _runtimeInfo = runtimeInfo; - _startupContext = startupContext; - _browserService = browserService; - _container = container; - _logger = logger; - } - - public void Start() - { - if (OsInfo.IsNotWindows) - { - //Console.CancelKeyPress += (sender, eventArgs) => eventArgs.Cancel = true; - //_cancelHandler = new CancelHandler(); - } - - _runtimeInfo.IsExiting = false; - DbFactory.RegisterDatabase(_container); - - _container.Resolve().PublishEvent(new ApplicationStartingEvent()); - - if (_runtimeInfo.IsExiting) - { - return; - } - - _hostController.StartServer(); - - if (!_startupContext.Flags.Contains(StartupContext.NO_BROWSER) - && _configFileProvider.LaunchBrowser) - { - _browserService.LaunchWebUI(); - } - - _container.Resolve().PublishEvent(new ApplicationStartedEvent()); - } - - public void Shutdown() - { - _logger.Info("Attempting to stop application."); - _hostController.StopServer(); - _logger.Info("Application has finished stop routine."); - _runtimeInfo.IsExiting = true; - } - - public void Handle(ApplicationShutdownRequested message) - { - if (!_runtimeInfo.IsWindowsService) - { - if (message.Restarting) - { - _runtimeInfo.RestartPending = true; - } - - LogManager.Configuration = null; - Shutdown(); - } - } - } -} diff --git a/src/NzbDrone.Host/Bootstrap.cs b/src/NzbDrone.Host/Bootstrap.cs index 0dac43281..c32f8085b 100644 --- a/src/NzbDrone.Host/Bootstrap.cs +++ b/src/NzbDrone.Host/Bootstrap.cs @@ -1,115 +1,160 @@ using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; using System.Reflection; -using System.Threading; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using DryIoc; +using DryIoc.Microsoft.DependencyInjection; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.WindowsServices; using NLog; -using NzbDrone.Common.Composition; +using NzbDrone.Common.Composition.Extensions; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Exceptions; +using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation; -using NzbDrone.Common.Processes; +using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Core.Configuration; -using NzbDrone.Core.Instrumentation; +using NzbDrone.Core.Datastore.Extensions; +using NzbDrone.Host; namespace NzbDrone.Host { public static class Bootstrap { private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(Bootstrap)); - private static IContainer _container; - public static void Start(StartupContext startupContext, IUserAlert userAlert, Action startCallback = null) + public static readonly List ASSEMBLIES = new List + { + "Sonarr.Host", + "Sonarr.Core", + "Sonarr.SignalR", + "Sonarr.Api.V3", + "Sonarr.Http" + }; + + public static void Start(string[] args, Action trayCallback = null) { try { - Logger.Info("Starting Sonarr - {0} - Version {1}", Assembly.GetCallingAssembly().Location, Assembly.GetExecutingAssembly().GetName().Version); + Logger.Info("Starting Sonarr - {0} - Version {1}", + Process.GetCurrentProcess().MainModule.FileName, + Assembly.GetExecutingAssembly().GetName().Version); - if (!PlatformValidation.IsValidate(userAlert)) - { - throw new TerminateApplicationException("Missing system requirements"); - } + var startupContext = new StartupContext(args); - LongPathSupport.Enable(); - - _container = MainAppContainerBuilder.BuildContainer(startupContext); - _container.Resolve().Initialize(); - _container.Resolve().Register(); - _container.Resolve().Write(); + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); var appMode = GetApplicationMode(startupContext); - Start(appMode, startupContext); + switch (appMode) + { + case ApplicationModes.Service: + { + Logger.Debug("Service selected"); - if (startCallback != null) - { - startCallback(_container); - } - else - { - SpinToExit(appMode); + CreateConsoleHostBuilder(args, startupContext).UseWindowsService().Build().Run(); + break; + } + + case ApplicationModes.Interactive: + { + Logger.Debug(trayCallback != null ? "Tray selected" : "Console selected"); + var builder = CreateConsoleHostBuilder(args, startupContext); + + if (trayCallback != null) + { + trayCallback(builder); + } + + builder.Build().Run(); + break; + } + + // Utility mode + default: + { + new Container(rules => rules.WithNzbDroneRules()) + .AutoAddServices(ASSEMBLIES) + .AddNzbDroneLogger() + .AddStartupContext(startupContext) + .Resolve() + .Route(appMode); + break; + } } } catch (InvalidConfigFileException ex) { throw new SonarrStartupException(ex); } - catch (TerminateApplicationException ex) + catch (TerminateApplicationException e) { - Logger.Info(ex.Message); + Logger.Info(e.Message); LogManager.Configuration = null; } } - private static void Start(ApplicationModes applicationModes, StartupContext startupContext) + public static IHostBuilder CreateConsoleHostBuilder(string[] args, StartupContext context) { - _container.Resolve().Reconfigure(); + var config = GetConfiguration(context); - if (!IsInUtilityMode(applicationModes)) + var bindAddress = config.GetValue(nameof(ConfigFileProvider.BindAddress), "*"); + var port = config.GetValue(nameof(ConfigFileProvider.Port), 8989); + var sslPort = config.GetValue(nameof(ConfigFileProvider.SslPort), 9898); + var enableSsl = config.GetValue(nameof(ConfigFileProvider.EnableSsl), false); + var sslCertPath = config.GetValue(nameof(ConfigFileProvider.SslCertPath)); + var sslCertPassword = config.GetValue(nameof(ConfigFileProvider.SslCertPassword)); + + var urls = new List { BuildUrl("http", bindAddress, port) }; + + if (enableSsl && sslCertPath.IsNotNullOrWhiteSpace()) { - if (startupContext.Flags.Contains(StartupContext.RESTART)) + urls.Add(BuildUrl("https", bindAddress, sslPort)); + } + + return new HostBuilder() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseServiceProviderFactory(new DryIocServiceProviderFactory(new Container(rules => rules.WithNzbDroneRules()))) + .ConfigureContainer(c => { - Thread.Sleep(2000); - } - - EnsureSingleInstance(applicationModes == ApplicationModes.Service, startupContext); - } - - _container.Resolve().Route(applicationModes); + c.AutoAddServices(Bootstrap.ASSEMBLIES) + .AddNzbDroneLogger() + .AddDatabase() + .AddStartupContext(context); + }) + .ConfigureWebHost(builder => + { + builder.UseConfiguration(config); + builder.UseUrls(urls.ToArray()); + builder.UseKestrel(options => + { + if (enableSsl && sslCertPath.IsNotNullOrWhiteSpace()) + { + options.ConfigureHttpsDefaults(configureOptions => + { + configureOptions.ServerCertificate = ValidateSslCertificate(sslCertPath, sslCertPassword); + }); + } + }); + builder.ConfigureKestrel(serverOptions => + { + serverOptions.AllowSynchronousIO = true; + serverOptions.Limits.MaxRequestBodySize = null; + }); + builder.UseStartup(); + }); } - private static void SpinToExit(ApplicationModes applicationModes) - { - if (IsInUtilityMode(applicationModes)) - { - return; - } - - _container.Resolve().Spin(); - } - - private static void EnsureSingleInstance(bool isService, IStartupContext startupContext) - { - var instancePolicy = _container.Resolve(); - - if (startupContext.Flags.Contains(StartupContext.TERMINATE)) - { - instancePolicy.KillAllOtherInstance(); - } - else if (startupContext.Args.ContainsKey(StartupContext.APPDATA)) - { - instancePolicy.WarnIfAlreadyRunning(); - } - else if (isService) - { - instancePolicy.KillAllOtherInstance(); - } - else - { - instancePolicy.PreventStartIfAlreadyRunning(); - } - } - - private static ApplicationModes GetApplicationMode(IStartupContext startupContext) + public static ApplicationModes GetApplicationMode(IStartupContext startupContext) { if (startupContext.Help) { @@ -131,7 +176,7 @@ namespace NzbDrone.Host return ApplicationModes.UninstallService; } - if (_container.Resolve().IsWindowsService) + if (OsInfo.IsWindows && WindowsServiceHelpers.IsWindowsService()) { return ApplicationModes.Service; } @@ -139,23 +184,40 @@ namespace NzbDrone.Host return ApplicationModes.Interactive; } - private static bool IsInUtilityMode(ApplicationModes applicationMode) + private static IConfiguration GetConfiguration(StartupContext context) { - switch (applicationMode) - { - case ApplicationModes.InstallService: - case ApplicationModes.UninstallService: - case ApplicationModes.RegisterUrl: - case ApplicationModes.Help: - { - return true; - } + var appFolder = new AppFolderInfo(context); + return new ConfigurationBuilder() + .AddXmlFile(appFolder.GetConfigPath(), optional: true, reloadOnChange: false) + .AddInMemoryCollection(new List> { new ("dataProtectionFolder", appFolder.GetDataProtectionPath()) }) + .Build(); + } - default: - { - return false; - } + private static string BuildUrl(string scheme, string bindAddress, int port) + { + return $"{scheme}://{bindAddress}:{port}"; + } + + private static X509Certificate2 ValidateSslCertificate(string cert, string password) + { + X509Certificate2 certificate; + + try + { + certificate = new X509Certificate2(cert, password, X509KeyStorageFlags.DefaultKeySet); } + catch (CryptographicException ex) + { + if (ex.HResult == 0x2 || ex.HResult == 0x2006D080) + { + throw new SonarrStartupException(ex, + $"The SSL certificate file {cert} does not exist"); + } + + throw new SonarrStartupException(ex); + } + + return certificate; } } } diff --git a/src/NzbDrone.Host/IHostController.cs b/src/NzbDrone.Host/IHostController.cs deleted file mode 100644 index 858b785ad..000000000 --- a/src/NzbDrone.Host/IHostController.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace NzbDrone.Host -{ - public interface IHostController - { - void StartServer(); - void StopServer(); - } -} diff --git a/src/NzbDrone.Host/IRemoteAccessAdapter.cs b/src/NzbDrone.Host/IRemoteAccessAdapter.cs deleted file mode 100644 index 7021411a9..000000000 --- a/src/NzbDrone.Host/IRemoteAccessAdapter.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace NzbDrone.Host.AccessControl -{ - public interface IRemoteAccessAdapter - { - void MakeAccessible(bool passive); - } -} diff --git a/src/NzbDrone.Host/IUserAlert.cs b/src/NzbDrone.Host/IUserAlert.cs deleted file mode 100644 index 3be8f7e63..000000000 --- a/src/NzbDrone.Host/IUserAlert.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace NzbDrone.Host -{ - public interface IUserAlert - { - void Alert(string message); - } -} diff --git a/src/NzbDrone.Host/MainAppContainerBuilder.cs b/src/NzbDrone.Host/MainAppContainerBuilder.cs deleted file mode 100644 index ffb632519..000000000 --- a/src/NzbDrone.Host/MainAppContainerBuilder.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Collections.Generic; -using Nancy.Bootstrapper; -using NzbDrone.Common.Composition; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.SignalR; -using Sonarr.Http; - -namespace NzbDrone.Host -{ - public class MainAppContainerBuilder : ContainerBuilderBase - { - public static IContainer BuildContainer(StartupContext args) - { - var assemblies = new List - { - "Sonarr.Host", - "Sonarr.Core", - "Sonarr.SignalR", - "Sonarr.Api.V3", - "Sonarr.Http" - }; - - return new MainAppContainerBuilder(args, assemblies).Container; - } - - private MainAppContainerBuilder(StartupContext args, List assemblies) - : base(args, assemblies) - { - AutoRegisterImplementations(); - - Container.Register(); - - if (OsInfo.IsWindows) - { - Container.Register(); - } - else - { - Container.Register(); - } - } - } -} diff --git a/src/NzbDrone.Host/PlatformValidation.cs b/src/NzbDrone.Host/PlatformValidation.cs deleted file mode 100644 index affa0e991..000000000 --- a/src/NzbDrone.Host/PlatformValidation.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Diagnostics; -using System.Reflection; -using NLog; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Common.Instrumentation; - -namespace NzbDrone.Host -{ - public static class PlatformValidation - { - private const string DOWNLOAD_LINK = "http://www.microsoft.com/en-us/download/details.aspx?id=42643"; - private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(PlatformValidation)); - - public static bool IsValidate(IUserAlert userAlert) - { - if (OsInfo.IsNotWindows) - { - return true; - } - - if (!IsAssemblyAvailable("System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")) - { - userAlert.Alert("It looks like you don't have the correct version of .NET Framework installed. You will now be directed the download page."); - - try - { - Process.Start(DOWNLOAD_LINK); - } - catch (Exception) - { - userAlert.Alert("Oops. Couldn't start your browser. Please visit http://www.microsoft.com/net to download the latest version of .NET Framework"); - } - - return false; - } - - return true; - } - - private static bool IsAssemblyAvailable(string assemblyString) - { - try - { - Assembly.Load(assemblyString); - return true; - } - catch (Exception e) - { - Logger.Warn(e, "Couldn't load {0}", assemblyString); - return false; - } - } - } -} diff --git a/src/NzbDrone.Host/Sonarr.Host.csproj b/src/NzbDrone.Host/Sonarr.Host.csproj index 199846894..0e5540d5d 100644 --- a/src/NzbDrone.Host/Sonarr.Host.csproj +++ b/src/NzbDrone.Host/Sonarr.Host.csproj @@ -7,6 +7,9 @@ + + + diff --git a/src/NzbDrone.Host/SpinService.cs b/src/NzbDrone.Host/SpinService.cs deleted file mode 100644 index aaa8b9ec5..000000000 --- a/src/NzbDrone.Host/SpinService.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System.IO; -using System.Threading; -using NLog; -using NzbDrone.Common.Disk; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Common.Processes; - -namespace NzbDrone.Host -{ - public interface IWaitForExit - { - void Spin(); - } - - public class SpinService : IWaitForExit - { - private readonly IRuntimeInfo _runtimeInfo; - private readonly IProcessProvider _processProvider; - private readonly IDiskProvider _diskProvider; - private readonly IStartupContext _startupContext; - private readonly Logger _logger; - - public SpinService(IRuntimeInfo runtimeInfo, IProcessProvider processProvider, IDiskProvider diskProvider, IStartupContext startupContext, Logger logger) - { - _runtimeInfo = runtimeInfo; - _processProvider = processProvider; - _diskProvider = diskProvider; - _startupContext = startupContext; - _logger = logger; - } - - public void Spin() - { - while (!_runtimeInfo.IsExiting) - { - Thread.Sleep(1000); - } - - _logger.Debug("Wait loop was terminated."); - - if (_runtimeInfo.RestartPending) - { - var restartArgs = GetRestartArgs(); - - var path = _runtimeInfo.ExecutingApplication; - var installationFolder = Path.GetDirectoryName(path); - - _logger.Info("Attempting restart with arguments: {0} {1}", path, restartArgs); - - if (OsInfo.IsOsx) - { - if (installationFolder.EndsWith(".app/Contents/MacOS/bin")) - { - // New MacOS App stores Sonarr binaries in MacOS/bin and has a shim in MacOS - // Run the app bundle instead of the binary - path = Path.GetDirectoryName(installationFolder); - path = Path.GetDirectoryName(path); - path = Path.GetDirectoryName(path); - } - else if (installationFolder.EndsWith(".app/Contents/MacOS")) - { - // Old MacOS App stores Sonarr binaries in MacOS together with shell script - // Run the app bundle instead - path = Path.GetDirectoryName(installationFolder); - path = Path.GetDirectoryName(path); - } - } - - _processProvider.SpawnNewProcess(path, restartArgs); - } - } - - private string GetRestartArgs() - { - var args = _startupContext.PreservedArguments; - - args += " /restart"; - - if (!args.Contains("/nobrowser")) - { - args += " /nobrowser"; - } - - return args; - } - } -} diff --git a/src/NzbDrone.Host/Startup.cs b/src/NzbDrone.Host/Startup.cs new file mode 100644 index 000000000..0c7cf6547 --- /dev/null +++ b/src/NzbDrone.Host/Startup.cs @@ -0,0 +1,217 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using NLog.Extensions.Logging; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Instrumentation; +using NzbDrone.Common.Processes; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Instrumentation; +using NzbDrone.Host; +using NzbDrone.Host.AccessControl; +using NzbDrone.Http.Authentication; +using NzbDrone.SignalR; +using Sonarr.Api.V3.System; +using Sonarr.Http; +using Sonarr.Http.Authentication; +using Sonarr.Http.ErrorManagement; +using Sonarr.Http.Frontend; +using Sonarr.Http.Middleware; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; + +namespace NzbDrone.Host +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + services.AddLogging(b => + { + b.ClearProviders(); + b.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace); + b.AddFilter("Microsoft.AspNetCore", Microsoft.Extensions.Logging.LogLevel.Warning); + b.AddFilter("Sonarr.Http.Authentication", LogLevel.Information); + b.AddFilter("Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager", LogLevel.Error); + b.AddNLog(); + }); + + services.Configure(options => + { + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; + options.KnownNetworks.Clear(); + options.KnownProxies.Clear(); + }); + + services.AddRouting(options => options.LowercaseUrls = true); + + services.AddResponseCompression(); + + services.AddCors(options => + { + options.AddPolicy(VersionedApiControllerAttribute.API_CORS_POLICY, + builder => + builder.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader()); + + options.AddPolicy("AllowGet", + builder => + builder.AllowAnyOrigin() + .WithMethods("GET", "OPTIONS") + .AllowAnyHeader()); + }); + + services + .AddControllers(options => + { + options.ReturnHttpNotAcceptable = true; + }) + .AddApplicationPart(typeof(SystemController).Assembly) + .AddApplicationPart(typeof(StaticResourceController).Assembly) + .AddJsonOptions(options => + { + STJson.ApplySerializerSettings(options.JsonSerializerOptions); + }) + .AddControllersAsServices(); + + services + .AddSignalR() + .AddJsonProtocol(options => + { + options.PayloadSerializerOptions = STJson.GetSerializerSettings(); + }); + + services.AddDataProtection() + .PersistKeysToFileSystem(new DirectoryInfo(Configuration["dataProtectionFolder"])); + + services.AddSingleton(); + services.AddAuthorization(options => + { + options.AddPolicy("SignalR", policy => + { + policy.AuthenticationSchemes.Add("SignalR"); + policy.RequireAuthenticatedUser(); + }); + + // Require auth on everything except those marked [AllowAnonymous] + options.FallbackPolicy = new AuthorizationPolicyBuilder("API") + .RequireAuthenticatedUser() + .Build(); + }); + + services.AddAppAuthentication(); + } + + public void Configure(IApplicationBuilder app, + IStartupContext startupContext, + Lazy mainDatabaseFactory, + Lazy logDatabaseFactory, + DatabaseTarget dbTarget, + ISingleInstancePolicy singleInstancePolicy, + InitializeLogger initializeLogger, + ReconfigureLogging reconfigureLogging, + IAppFolderFactory appFolderFactory, + IProvidePidFile pidFileProvider, + IConfigFileProvider configFileProvider, + IRuntimeInfo runtimeInfo, + IFirewallAdapter firewallAdapter, + SonarrErrorPipeline errorHandler) + { + initializeLogger.Initialize(); + appFolderFactory.Register(); + pidFileProvider.Write(); + + reconfigureLogging.Reconfigure(); + + EnsureSingleInstance(false, startupContext, singleInstancePolicy); + + // instantiate the databases to initialize/migrate them + _ = mainDatabaseFactory.Value; + _ = logDatabaseFactory.Value; + + dbTarget.Register(); + + if (OsInfo.IsNotWindows) + { + Console.CancelKeyPress += (sender, eventArgs) => NLog.LogManager.Configuration = null; + } + + if (OsInfo.IsWindows && runtimeInfo.IsAdmin) + { + firewallAdapter.MakeAccessible(); + } + + app.UseForwardedHeaders(); + app.UseMiddleware(); + app.UsePathBase(new PathString(configFileProvider.UrlBase)); + app.UseExceptionHandler(new ExceptionHandlerOptions + { + AllowStatusCode404Response = true, + ExceptionHandler = errorHandler.HandleException + }); + + app.UseRouting(); + app.UseCors(); + app.UseAuthentication(); + app.UseAuthorization(); + app.UseResponseCompression(); + app.Properties["host.AppName"] = BuildInfo.AppName; + + app.UseMiddleware(); + app.UseMiddleware(configFileProvider.UrlBase); + app.UseMiddleware(); + app.UseMiddleware(); + app.UseMiddleware(new List { "/api/v3/command" }); + + app.UseWebSockets(); + + app.UseEndpoints(x => + { + x.MapHub("/signalr/messages").RequireAuthorization("SignalR"); + x.MapControllers(); + }); + } + + private void EnsureSingleInstance(bool isService, IStartupContext startupContext, ISingleInstancePolicy instancePolicy) + { + if (startupContext.Flags.Contains(StartupContext.NO_SINGLE_INSTANCE_CHECK)) + { + return; + } + + if (startupContext.Flags.Contains(StartupContext.TERMINATE)) + { + instancePolicy.KillAllOtherInstance(); + } + else if (startupContext.Args.ContainsKey(StartupContext.APPDATA)) + { + instancePolicy.WarnIfAlreadyRunning(); + } + else if (isService) + { + instancePolicy.KillAllOtherInstance(); + } + else + { + instancePolicy.PreventStartIfAlreadyRunning(); + } + } + } +} diff --git a/src/NzbDrone.Host/Router.cs b/src/NzbDrone.Host/UtilityModeRouter.cs similarity index 76% rename from src/NzbDrone.Host/Router.cs rename to src/NzbDrone.Host/UtilityModeRouter.cs index 09fd7fca5..8b94712cd 100644 --- a/src/NzbDrone.Host/Router.cs +++ b/src/NzbDrone.Host/UtilityModeRouter.cs @@ -7,10 +7,13 @@ using IServiceProvider = NzbDrone.Common.IServiceProvider; namespace NzbDrone.Host { - public class Router + public interface IUtilityModeRouter + { + void Route(ApplicationModes applicationModes); + } + + public class UtilityModeRouter : IUtilityModeRouter { - private readonly INzbDroneConsoleFactory _nzbDroneConsoleFactory; - private readonly INzbDroneServiceFactory _nzbDroneServiceFactory; private readonly IServiceProvider _serviceProvider; private readonly IConsoleService _consoleService; private readonly IRuntimeInfo _runtimeInfo; @@ -18,17 +21,13 @@ namespace NzbDrone.Host private readonly IRemoteAccessAdapter _remoteAccessAdapter; private readonly Logger _logger; - public Router(INzbDroneConsoleFactory nzbDroneConsoleFactory, - INzbDroneServiceFactory nzbDroneServiceFactory, - IServiceProvider serviceProvider, + public UtilityModeRouter(IServiceProvider serviceProvider, IConsoleService consoleService, IRuntimeInfo runtimeInfo, IProcessProvider processProvider, IRemoteAccessAdapter remoteAccessAdapter, Logger logger) { - _nzbDroneConsoleFactory = nzbDroneConsoleFactory; - _nzbDroneServiceFactory = nzbDroneServiceFactory; _serviceProvider = serviceProvider; _consoleService = consoleService; _runtimeInfo = runtimeInfo; @@ -43,22 +42,6 @@ namespace NzbDrone.Host switch (applicationModes) { - case ApplicationModes.Service: - { - _logger.Debug("Service selected"); - - _serviceProvider.Run(_nzbDroneServiceFactory.Build()); - - break; - } - - case ApplicationModes.Interactive: - { - _logger.Debug(_runtimeInfo.IsWindowsTray ? "Tray selected" : "Console selected"); - _nzbDroneConsoleFactory.Start(); - break; - } - case ApplicationModes.InstallService: { _logger.Debug("Install Service selected"); diff --git a/src/NzbDrone.Host/WebHost/Middleware/IAspNetCoreMiddleware.cs b/src/NzbDrone.Host/WebHost/Middleware/IAspNetCoreMiddleware.cs deleted file mode 100644 index 8121fd19b..000000000 --- a/src/NzbDrone.Host/WebHost/Middleware/IAspNetCoreMiddleware.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Microsoft.AspNetCore.Builder; - -namespace NzbDrone.Host.Middleware -{ - public interface IAspNetCoreMiddleware - { - int Order { get; } - void Attach(IApplicationBuilder appBuilder); - } -} diff --git a/src/NzbDrone.Host/WebHost/Middleware/NancyMiddleware.cs b/src/NzbDrone.Host/WebHost/Middleware/NancyMiddleware.cs deleted file mode 100644 index 489bea524..000000000 --- a/src/NzbDrone.Host/WebHost/Middleware/NancyMiddleware.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Nancy.Bootstrapper; -using Nancy.Owin; - -namespace NzbDrone.Host.Middleware -{ - public class NancyMiddleware : IAspNetCoreMiddleware - { - private readonly INancyBootstrapper _nancyBootstrapper; - - public int Order => 2; - - public NancyMiddleware(INancyBootstrapper nancyBootstrapper) - { - _nancyBootstrapper = nancyBootstrapper; - } - - public void Attach(IApplicationBuilder appBuilder) - { - var options = new NancyOptions - { - Bootstrapper = _nancyBootstrapper, - PerformPassThrough = context => context.Request.Path.StartsWith("/signalr") - }; - - appBuilder.UseOwin(x => x.UseNancy(options)); - } - } -} diff --git a/src/NzbDrone.Host/WebHost/Middleware/SignalRMiddleware.cs b/src/NzbDrone.Host/WebHost/Middleware/SignalRMiddleware.cs deleted file mode 100644 index 671c5631b..000000000 --- a/src/NzbDrone.Host/WebHost/Middleware/SignalRMiddleware.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.SignalR; -using Microsoft.Extensions.DependencyInjection; -using NLog; -using NzbDrone.Common.Composition; -using NzbDrone.Core.Configuration; -using NzbDrone.SignalR; - -namespace NzbDrone.Host.Middleware -{ - public class SignalRMiddleware : IAspNetCoreMiddleware - { - private readonly IContainer _container; - private readonly Logger _logger; - private static string API_KEY; - private static string URL_BASE; - public int Order => 1; - - public SignalRMiddleware(IContainer container, - IConfigFileProvider configFileProvider, - Logger logger) - { - _container = container; - _logger = logger; - API_KEY = configFileProvider.ApiKey; - URL_BASE = configFileProvider.UrlBase; - } - - public void Attach(IApplicationBuilder appBuilder) - { - appBuilder.UseWebSockets(); - - appBuilder.Use(async (context, next) => - { - if (context.Request.Path.StartsWithSegments("/signalr") && - !context.Request.Path.Value.EndsWith("/negotiate")) - { - if (!context.Request.Query.ContainsKey("access_token") || - context.Request.Query["access_token"] != API_KEY) - { - context.Response.StatusCode = 401; - await context.Response.WriteAsync("Unauthorized"); - return; - } - } - - try - { - await next(); - } - catch (OperationCanceledException e) - { - // Demote the exception to trace logging so users don't worry (as much). - _logger.Trace(e); - } - }); - - appBuilder.UseEndpoints(x => - { - x.MapHub(URL_BASE + "/signalr/messages"); - }); - - // This is a side effect of haing multiple IoC containers, TinyIoC and whatever - // Kestrel/SignalR is using. Ideally we'd have one IoC container, but that's non-trivial with TinyIoC - // TODO: Use a single IoC container if supported for TinyIoC or if we switch to another system (ie Autofac). - - var hubContext = appBuilder.ApplicationServices.GetService>(); - _container.Register(hubContext); - } - } -} diff --git a/src/NzbDrone.Host/WebHost/WebHostController.cs b/src/NzbDrone.Host/WebHost/WebHostController.cs deleted file mode 100644 index 015e3cd2a..000000000 --- a/src/NzbDrone.Host/WebHost/WebHostController.cs +++ /dev/null @@ -1,157 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using NLog; -using NLog.Extensions.Logging; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Common.Exceptions; -using NzbDrone.Common.Extensions; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Configuration; -using NzbDrone.Host.AccessControl; -using NzbDrone.Host.Middleware; -using LogLevel = Microsoft.Extensions.Logging.LogLevel; - -namespace NzbDrone.Host -{ - public class WebHostController : IHostController - { - private readonly IRuntimeInfo _runtimeInfo; - private readonly IConfigFileProvider _configFileProvider; - private readonly IFirewallAdapter _firewallAdapter; - private readonly IEnumerable _middlewares; - private readonly Logger _logger; - private IWebHost _host; - - public WebHostController(IRuntimeInfo runtimeInfo, - IConfigFileProvider configFileProvider, - IFirewallAdapter firewallAdapter, - IEnumerable middlewares, - Logger logger) - { - _runtimeInfo = runtimeInfo; - _configFileProvider = configFileProvider; - _firewallAdapter = firewallAdapter; - _middlewares = middlewares; - _logger = logger; - } - - public void StartServer() - { - if (OsInfo.IsWindows) - { - if (_runtimeInfo.IsAdmin) - { - _firewallAdapter.MakeAccessible(); - } - } - - var bindAddress = _configFileProvider.BindAddress; - var enableSsl = _configFileProvider.EnableSsl; - var sslCertPath = _configFileProvider.SslCertPath; - - var urls = new List(); - - urls.Add(BuildUrl("http", bindAddress, _configFileProvider.Port)); - - if (enableSsl && sslCertPath.IsNotNullOrWhiteSpace()) - { - urls.Add(BuildUrl("https", bindAddress, _configFileProvider.SslPort)); - } - - _host = new WebHostBuilder() - .UseUrls(urls.ToArray()) - .UseKestrel(options => - { - if (enableSsl && sslCertPath.IsNotNullOrWhiteSpace()) - { - options.ConfigureHttpsDefaults(configureOptions => - { - X509Certificate2 certificate; - - try - { - certificate = new X509Certificate2(sslCertPath, _configFileProvider.SslCertPassword, X509KeyStorageFlags.DefaultKeySet); - } - catch (CryptographicException ex) - { - if (ex.HResult == 0x2 || ex.HResult == 0x2006D080) - { - throw new SonarrStartupException(ex, $"The SSL certificate file {sslCertPath} does not exist"); - } - - throw new SonarrStartupException(ex); - } - - configureOptions.ServerCertificate = certificate; - }); - } - }) - .ConfigureKestrel(serverOptions => - { - serverOptions.AllowSynchronousIO = true; - serverOptions.Limits.MaxRequestBodySize = null; - }) - .ConfigureLogging(logging => - { - logging.AddProvider(new NLogLoggerProvider()); - logging.SetMinimumLevel(LogLevel.Warning); - }) - .ConfigureServices(services => - { - services - .AddSignalR() - .AddJsonProtocol(options => - { - options.PayloadSerializerOptions = STJson.GetSerializerSettings(); - }); - }) - .Configure(app => - { - app.UseRouting(); - app.Properties["host.AppName"] = BuildInfo.AppName; - app.UsePathBase(_configFileProvider.UrlBase); - - foreach (var middleWare in _middlewares.OrderBy(c => c.Order)) - { - _logger.Debug("Attaching {0} to host", middleWare.GetType().Name); - middleWare.Attach(app); - } - }) - .UseContentRoot(Directory.GetCurrentDirectory()) - .Build(); - - _logger.Info("Listening on the following URLs:"); - - foreach (var url in urls) - { - _logger.Info(" {0}", url); - } - - _host.Start(); - } - - public async void StopServer() - { - _logger.Info("Attempting to stop OWIN host"); - - await _host.StopAsync(TimeSpan.FromSeconds(5)); - _host.Dispose(); - _host = null; - - _logger.Info("Host has stopped"); - } - - private string BuildUrl(string scheme, string bindAddress, int port) - { - return $"{scheme}://{bindAddress}:{port}"; - } - } -} diff --git a/src/NzbDrone.Integration.Test/ApiTests/CommandFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/CommandFixture.cs index f1ff009bb..4f40de1ae 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/CommandFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/CommandFixture.cs @@ -5,6 +5,7 @@ using NzbDrone.Integration.Test.Client; namespace NzbDrone.Integration.Test.ApiTests { [TestFixture] + [Ignore("Not ready to be used on this branch")] public class CommandFixture : IntegrationTest { [Test] diff --git a/src/NzbDrone.Integration.Test/Client/ClientBase.cs b/src/NzbDrone.Integration.Test/Client/ClientBase.cs index 2304cbec8..8e2091f94 100644 --- a/src/NzbDrone.Integration.Test/Client/ClientBase.cs +++ b/src/NzbDrone.Integration.Test/Client/ClientBase.cs @@ -51,7 +51,7 @@ namespace NzbDrone.Integration.Test.Client throw response.ErrorException; } - AssertDisableCache(response.Headers); + AssertDisableCache(response); response.ErrorMessage.Should().BeNullOrWhiteSpace(); @@ -68,13 +68,14 @@ namespace NzbDrone.Integration.Test.Client return Json.Deserialize(content); } - private static void AssertDisableCache(IList headers) + private static void AssertDisableCache(IRestResponse response) { // cache control header gets reordered on net core + var headers = response.Headers; ((string)headers.Single(c => c.Name == "Cache-Control").Value).Split(',').Select(x => x.Trim()) - .Should().BeEquivalentTo("no-store, must-revalidate, no-cache, max-age=0".Split(',').Select(x => x.Trim())); + .Should().BeEquivalentTo("no-store, no-cache".Split(',').Select(x => x.Trim())); headers.Single(c => c.Name == "Pragma").Value.Should().Be("no-cache"); - headers.Single(c => c.Name == "Expires").Value.Should().Be("0"); + headers.Single(c => c.Name == "Expires").Value.Should().Be("-1"); } } diff --git a/src/NzbDrone.Integration.Test/CorsFixture.cs b/src/NzbDrone.Integration.Test/CorsFixture.cs index 252996dc1..068b08296 100644 --- a/src/NzbDrone.Integration.Test/CorsFixture.cs +++ b/src/NzbDrone.Integration.Test/CorsFixture.cs @@ -11,6 +11,7 @@ namespace NzbDrone.Integration.Test private RestRequest BuildGet(string route = "series") { var request = new RestRequest(route, Method.GET); + request.AddHeader("Origin", "http://a.different.domain"); request.AddHeader(AccessControlHeaders.RequestMethod, "POST"); return request; @@ -19,6 +20,8 @@ namespace NzbDrone.Integration.Test private RestRequest BuildOptions(string route = "series") { var request = new RestRequest(route, Method.OPTIONS); + request.AddHeader("Origin", "http://a.different.domain"); + request.AddHeader(AccessControlHeaders.RequestMethod, "POST"); return request; } diff --git a/src/NzbDrone.Integration.Test/IndexHtmlFixture.cs b/src/NzbDrone.Integration.Test/IndexHtmlFixture.cs index 8bd1a26fc..f78c0627e 100644 --- a/src/NzbDrone.Integration.Test/IndexHtmlFixture.cs +++ b/src/NzbDrone.Integration.Test/IndexHtmlFixture.cs @@ -1,4 +1,5 @@ -using System.Net; +using System.Linq; +using System.Net; using FluentAssertions; using NUnit.Framework; @@ -13,5 +14,19 @@ namespace NzbDrone.Integration.Test var text = new WebClient().DownloadString(RootUrl); text.Should().NotBeNullOrWhiteSpace(); } + + [Test] + public void index_should_not_be_cached() + { + var client = new WebClient(); + _ = client.DownloadString(RootUrl); + + var headers = client.ResponseHeaders; + + headers.Get("Cache-Control").Split(',').Select(x => x.Trim()) + .Should().BeEquivalentTo("no-store, no-cache".Split(',').Select(x => x.Trim())); + headers.Get("Pragma").Should().Be("no-cache"); + headers.Get("Expires").Should().Be("-1"); + } } } diff --git a/src/NzbDrone.Integration.Test/IntegrationTestBase.cs b/src/NzbDrone.Integration.Test/IntegrationTestBase.cs index 5f4e99c56..1042b4156 100644 --- a/src/NzbDrone.Integration.Test/IntegrationTestBase.cs +++ b/src/NzbDrone.Integration.Test/IntegrationTestBase.cs @@ -176,7 +176,7 @@ namespace NzbDrone.Integration.Test protected async Task ConnectSignalR() { _signalRReceived = new List(); - _signalrConnection = new HubConnectionBuilder().WithUrl("http://localhost:7878/signalr/messages").Build(); + _signalrConnection = new HubConnectionBuilder().WithUrl("http://localhost:8989/signalr/messages").Build(); var cts = new CancellationTokenSource(); diff --git a/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs b/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs index 6b3a1a102..67a07a1dd 100644 --- a/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs +++ b/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs @@ -148,11 +148,12 @@ namespace NzbDrone.Test.Common.AutoMoq _container = container; container.RegisterInstance(this); - RegisterPlatformLibrary(container); - _registeredMocks = new Dictionary(); + + RegisterPlatformLibrary(container); AddTheAutoMockingContainerExtensionToTheContainer(container); - ContainerBuilderBase.RegisterNativeResolver(new[] { "System.Data.SQLite", "Sonarr.Core" }); + + AssemblyLoader.RegisterNativeResolver(new[] { "System.Data.SQLite", "Sonarr.Core" }); } private static void AddTheAutoMockingContainerExtensionToTheContainer(IUnityContainer container) diff --git a/src/NzbDrone.Update/Sonarr.Update.csproj b/src/NzbDrone.Update/Sonarr.Update.csproj index c52c9e477..dd7fc25f9 100644 --- a/src/NzbDrone.Update/Sonarr.Update.csproj +++ b/src/NzbDrone.Update/Sonarr.Update.csproj @@ -4,6 +4,8 @@ net6.0 + + diff --git a/src/NzbDrone.Update/UpdateApp.cs b/src/NzbDrone.Update/UpdateApp.cs index 9c835241e..b3360a92d 100644 --- a/src/NzbDrone.Update/UpdateApp.cs +++ b/src/NzbDrone.Update/UpdateApp.cs @@ -1,11 +1,14 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; +using DryIoc; using NLog; -using NzbDrone.Common.Composition; +using NzbDrone.Common.Composition.Extensions; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation; +using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Common.Processes; using NzbDrone.Update.UpdateEngine; @@ -18,8 +21,6 @@ namespace NzbDrone.Update private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(UpdateApp)); - private static IContainer _container; - public UpdateApp(IInstallUpdateService installUpdateService, IProcessProvider processProvider) { _installUpdateService = installUpdateService; @@ -30,14 +31,18 @@ namespace NzbDrone.Update { try { - var startupContext = new StartupContext(args); - NzbDroneLogger.Register(startupContext, true, true); + var startupArgument = new StartupContext(args); + NzbDroneLogger.Register(startupArgument, true, true); Logger.Info("Starting Sonarr Update Client"); - _container = UpdateContainerBuilder.Build(startupContext); - _container.Resolve().Initialize(); - _container.Resolve().Start(args); + var container = new Container(rules => rules.WithNzbDroneRules()) + .AutoAddServices(new List { "Sonarr.Update" }) + .AddNzbDroneLogger() + .AddStartupContext(startupArgument); + + container.Resolve().Initialize(); + container.Resolve().Start(args); Logger.Info("Update completed successfully"); } @@ -59,17 +64,17 @@ namespace NzbDrone.Update { if (args == null || !args.Any()) { - throw new ArgumentOutOfRangeException(nameof(args), "args must be specified"); + throw new ArgumentOutOfRangeException("args", "args must be specified"); } var startupContext = new UpdateStartupContext - { - ProcessId = ParseProcessId(args[0]) - }; + { + ProcessId = ParseProcessId(args[0]) + }; if (OsInfo.IsNotWindows) { - switch (args.Count()) + switch (args.Length) { case 1: return startupContext; @@ -98,7 +103,7 @@ namespace NzbDrone.Update int id; if (!int.TryParse(arg, out id) || id <= 0) { - throw new ArgumentOutOfRangeException(nameof(arg), "Invalid process ID"); + throw new ArgumentOutOfRangeException("arg", "Invalid process ID"); } Logger.Debug("NzbDrone process ID: {0}", id); diff --git a/src/NzbDrone.Update/UpdateContainerBuilder.cs b/src/NzbDrone.Update/UpdateContainerBuilder.cs deleted file mode 100644 index 8573f871d..000000000 --- a/src/NzbDrone.Update/UpdateContainerBuilder.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Common.Composition; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Common.Http.Dispatchers; - -namespace NzbDrone.Update -{ - public class UpdateContainerBuilder : ContainerBuilderBase - { - private UpdateContainerBuilder(IStartupContext startupContext, List assemblies) - : base(startupContext, assemblies) - { - } - - public static IContainer Build(IStartupContext startupContext) - { - var assemblies = new List - { - "Sonarr.Update" - }; - - return new UpdateContainerBuilder(startupContext, assemblies).Container; - } - } -} diff --git a/src/NzbDrone/MessageBoxUserAlert.cs b/src/NzbDrone/MessageBoxUserAlert.cs deleted file mode 100644 index b95eec3de..000000000 --- a/src/NzbDrone/MessageBoxUserAlert.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Windows.Forms; -using NzbDrone.Host; - -namespace NzbDrone -{ - public class MessageBoxUserAlert : IUserAlert - { - public void Alert(string message) - { - MessageBox.Show(text: message, buttons: MessageBoxButtons.OK, icon: MessageBoxIcon.Warning, caption: "NzbDrone"); - } - } -} diff --git a/src/NzbDrone/SysTray/SysTrayApp.cs b/src/NzbDrone/SysTray/SysTrayApp.cs index 38979724f..4493099c7 100644 --- a/src/NzbDrone/SysTray/SysTrayApp.cs +++ b/src/NzbDrone/SysTray/SysTrayApp.cs @@ -1,6 +1,9 @@ using System; using System.ComponentModel; +using System.Threading; +using System.Threading.Tasks; using System.Windows.Forms; +using Microsoft.Extensions.Hosting; using NLog; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Processes; @@ -8,12 +11,7 @@ using NzbDrone.Host; namespace NzbDrone.SysTray { - public interface ISystemTrayApp - { - void Start(); - } - - public class SystemTrayApp : Form, ISystemTrayApp + public class SystemTrayApp : Form, IHostedService { private readonly IBrowserService _browserService; private readonly IRuntimeInfo _runtimeInfo; @@ -34,8 +32,12 @@ namespace NzbDrone.SysTray Application.ThreadException += OnThreadException; Application.ApplicationExit += OnApplicationExit; + Application.SetHighDpiMode(HighDpiMode.PerMonitor); + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + _trayMenu.Items.Add(new ToolStripMenuItem("Launch Browser", null, LaunchBrowser)); - _trayMenu.Items.Add(new ToolStripMenuItem("-")); + _trayMenu.Items.Add(new ToolStripSeparator()); _trayMenu.Items.Add(new ToolStripMenuItem("Exit", null, OnExit)); _trayIcon.Text = string.Format("Sonarr - {0}", BuildInfo.Version); @@ -48,6 +50,20 @@ namespace NzbDrone.SysTray Application.Run(this); } + public Task StartAsync(CancellationToken cancellationToken) + { + var thread = new Thread(Start); + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + protected override void OnClosing(CancelEventArgs e) { DisposeTrayIcon(); diff --git a/src/NzbDrone/WindowsApp.cs b/src/NzbDrone/WindowsApp.cs index 076c27a68..a2d99a4e0 100644 --- a/src/NzbDrone/WindowsApp.cs +++ b/src/NzbDrone/WindowsApp.cs @@ -1,5 +1,7 @@ using System; using System.Windows.Forms; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using NLog; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Instrumentation; @@ -20,11 +22,9 @@ namespace NzbDrone NzbDroneLogger.Register(startupArgs, false, true); - Bootstrap.Start(startupArgs, new MessageBoxUserAlert(), container => + Bootstrap.Start(args, e => { - container.Register(); - var trayApp = container.Resolve(); - trayApp.Start(); + e.ConfigureServices((_, s) => s.AddSingleton()); }); } catch (Exception e) diff --git a/src/Sonarr.Api.V3/Blacklist/BlacklistModule.cs b/src/Sonarr.Api.V3/Blacklist/BlacklistModule.cs deleted file mode 100644 index 9eb18d15d..000000000 --- a/src/Sonarr.Api.V3/Blacklist/BlacklistModule.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Blacklist has been deprecated for blocklist. -using NzbDrone.Core.Blocklisting; -using NzbDrone.Core.Datastore; -using Sonarr.Api.V3.Blocklist; -using Sonarr.Http; -using Sonarr.Http.Extensions; - -namespace Sonarr.Api.V3.Blacklist -{ - public class BlacklistModule : SonarrRestModule - { - private readonly BlocklistService _blocklistService; - - public BlacklistModule(BlocklistService blocklistService) - { - _blocklistService = blocklistService; - GetResourcePaged = Blocklist; - DeleteResource = DeleteBlockList; - - Delete("/bulk", x => Remove()); - } - - private PagingResource Blocklist(PagingResource pagingResource) - { - var pagingSpec = pagingResource.MapToPagingSpec("date", SortDirection.Descending); - - return ApplyToPage(_blocklistService.Paged, pagingSpec, BlocklistResourceMapper.MapToResource); - } - - private void DeleteBlockList(int id) - { - _blocklistService.Delete(id); - } - - private object Remove() - { - var resource = Request.Body.FromJson(); - - _blocklistService.Delete(resource.Ids); - - return new object(); - } - } -} diff --git a/src/Sonarr.Api.V3/Blocklist/BlocklistController.cs b/src/Sonarr.Api.V3/Blocklist/BlocklistController.cs new file mode 100644 index 000000000..c3a049916 --- /dev/null +++ b/src/Sonarr.Api.V3/Blocklist/BlocklistController.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Blocklisting; +using NzbDrone.Core.Datastore; +using Sonarr.Http; +using Sonarr.Http.Extensions; +using Sonarr.Http.REST.Attributes; + +namespace Sonarr.Api.V3.Blocklist +{ + [V3ApiController] + public class BlocklistController : Controller + { + private readonly IBlocklistService _blocklistService; + + public BlocklistController(IBlocklistService blocklistService) + { + _blocklistService = blocklistService; + } + + [HttpGet] + public PagingResource GetBlocklist() + { + var pagingResource = Request.ReadPagingResourceFromRequest(); + var pagingSpec = pagingResource.MapToPagingSpec("date", SortDirection.Descending); + + return pagingSpec.ApplyToPage(_blocklistService.Paged, model => BlocklistResourceMapper.MapToResource(model)); + } + + [RestDeleteById] + public void DeleteBlocklist(int id) + { + _blocklistService.Delete(id); + } + + [HttpDelete("bulk")] + public object Remove([FromBody] BlocklistBulkResource resource) + { + _blocklistService.Delete(resource.Ids); + + return new { }; + } + } +} diff --git a/src/Sonarr.Api.V3/Blocklist/BlocklistModule.cs b/src/Sonarr.Api.V3/Blocklist/BlocklistModule.cs deleted file mode 100644 index ed0d28528..000000000 --- a/src/Sonarr.Api.V3/Blocklist/BlocklistModule.cs +++ /dev/null @@ -1,42 +0,0 @@ -using NzbDrone.Core.Blocklisting; -using NzbDrone.Core.Datastore; -using Sonarr.Http; -using Sonarr.Http.Extensions; - -namespace Sonarr.Api.V3.Blocklist -{ - public class BlocklistModule : SonarrRestModule - { - private readonly BlocklistService _blocklistService; - - public BlocklistModule(BlocklistService blocklistService) - { - _blocklistService = blocklistService; - GetResourcePaged = Blocklist; - DeleteResource = DeleteBlockList; - - Delete("/bulk", x => Remove()); - } - - private PagingResource Blocklist(PagingResource pagingResource) - { - var pagingSpec = pagingResource.MapToPagingSpec("date", SortDirection.Descending); - - return ApplyToPage(_blocklistService.Paged, pagingSpec, BlocklistResourceMapper.MapToResource); - } - - private void DeleteBlockList(int id) - { - _blocklistService.Delete(id); - } - - private object Remove() - { - var resource = Request.Body.FromJson(); - - _blocklistService.Delete(resource.Ids); - - return new object(); - } - } -} diff --git a/src/Sonarr.Api.V3/Calendar/CalendarController.cs b/src/Sonarr.Api.V3/Calendar/CalendarController.cs new file mode 100644 index 000000000..988a1d8df --- /dev/null +++ b/src/Sonarr.Api.V3/Calendar/CalendarController.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.DecisionEngine.Specifications; +using NzbDrone.Core.Tv; +using NzbDrone.SignalR; +using Sonarr.Api.V3.Episodes; +using Sonarr.Http; + +namespace Sonarr.Api.V3.Calendar +{ + [V3ApiController] + public class CalendarController : EpisodeControllerWithSignalR + { + public CalendarController(IBroadcastSignalRMessage signalR, + IEpisodeService episodeService, + ISeriesService seriesService, + IUpgradableSpecification qualityUpgradableSpecification) + : base(episodeService, seriesService, qualityUpgradableSpecification, signalR) + { + } + + [HttpGet] + public List GetCalendar(DateTime? start, DateTime? end, bool unmonitored = false, bool includeSeries = false, bool includeEpisodeFile = false, bool includeEpisodeImages = false) + { + var startUse = start ?? DateTime.Today; + var endUse = end ?? DateTime.Today.AddDays(2); + + var resources = MapToResource(_episodeService.EpisodesBetweenDates(startUse, endUse, unmonitored), includeSeries, includeEpisodeFile, includeEpisodeImages); + + return resources.OrderBy(e => e.AirDateUtc).ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/Calendar/CalendarFeedModule.cs b/src/Sonarr.Api.V3/Calendar/CalendarFeedController.cs similarity index 54% rename from src/Sonarr.Api.V3/Calendar/CalendarFeedModule.cs rename to src/Sonarr.Api.V3/Calendar/CalendarFeedController.cs index 5f68c3981..957dbb81a 100644 --- a/src/Sonarr.Api.V3/Calendar/CalendarFeedModule.cs +++ b/src/Sonarr.Api.V3/Calendar/CalendarFeedController.cs @@ -5,65 +5,42 @@ using Ical.Net; using Ical.Net.CalendarComponents; using Ical.Net.DataTypes; using Ical.Net.Serialization; -using Nancy; -using Nancy.Responses; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.Tags; using NzbDrone.Core.Tv; -using Sonarr.Http.Extensions; +using Sonarr.Http; namespace Sonarr.Api.V3.Calendar { - public class CalendarFeedModule : SonarrV3FeedModule + [V3FeedController("calendar")] + public class CalendarFeedController : Controller { private readonly IEpisodeService _episodeService; + private readonly ISeriesService _seriesService; private readonly ITagService _tagService; - public CalendarFeedModule(IEpisodeService episodeService, ITagService tagService) - : base("calendar") + public CalendarFeedController(IEpisodeService episodeService, ISeriesService seriesService, ITagService tagService) { _episodeService = episodeService; + _seriesService = seriesService; _tagService = tagService; - - Get("/Sonarr.ics", options => GetCalendarFeed()); } - private object GetCalendarFeed() + [HttpGet("Sonarr.ics")] + public IActionResult GetCalendarFeed(int pastDays = 7, int futureDays = 28, string tagList = "", bool unmonitored = false, bool premieresOnly = false, bool asAllDay = false) { - var pastDays = 7; - var futureDays = 28; var start = DateTime.Today.AddDays(-pastDays); var end = DateTime.Today.AddDays(futureDays); - var unmonitored = Request.GetBooleanQueryParameter("unmonitored"); - - // There was a typo, recognize both the correct 'premieresOnly' and mistyped 'premiersOnly' boolean for background compat. - var premieresOnly = Request.GetBooleanQueryParameter("premieresOnly") || Request.GetBooleanQueryParameter("premiersOnly"); - var asAllDay = Request.GetBooleanQueryParameter("asAllDay"); var tags = new List(); - var queryPastDays = Request.Query.PastDays; - var queryFutureDays = Request.Query.FutureDays; - var queryTags = Request.Query.Tags; - - if (queryPastDays.HasValue) + if (tagList.IsNotNullOrWhiteSpace()) { - pastDays = int.Parse(queryPastDays.Value); - start = DateTime.Today.AddDays(-pastDays); - } - - if (queryFutureDays.HasValue) - { - futureDays = int.Parse(queryFutureDays.Value); - end = DateTime.Today.AddDays(futureDays); - } - - if (queryTags.HasValue) - { - var tagInput = (string)queryTags.Value.ToString(); - tags.AddRange(tagInput.Split(',').Select(_tagService.GetTag).Select(t => t.Id)); + tags.AddRange(tagList.Split(',').Select(_tagService.GetTag).Select(t => t.Id)); } var episodes = _episodeService.EpisodesBetweenDates(start, end, unmonitored); + var allSeries = _seriesService.GetAllSeries(); var calendar = new Ical.Net.Calendar { ProductId = "-//sonarr.tv//Sonarr//EN" @@ -75,12 +52,14 @@ namespace Sonarr.Api.V3.Calendar foreach (var episode in episodes.OrderBy(v => v.AirDateUtc.Value)) { + var series = allSeries.SingleOrDefault(s => s.Id == episode.SeriesId); + if (premieresOnly && (episode.SeasonNumber == 0 || episode.EpisodeNumber != 1)) { continue; } - if (tags.Any() && tags.None(episode.Series.Tags.Contains)) + if (tags.Any() && tags.None(series.Tags.Contains)) { continue; } @@ -89,7 +68,7 @@ namespace Sonarr.Api.V3.Calendar occurrence.Uid = "NzbDrone_episode_" + episode.Id; occurrence.Status = episode.HasFile ? EventStatus.Confirmed : EventStatus.Tentative; occurrence.Description = episode.Overview; - occurrence.Categories = new List() { episode.Series.Network }; + occurrence.Categories = new List() { series.Network }; if (asAllDay) { @@ -98,16 +77,16 @@ namespace Sonarr.Api.V3.Calendar else { occurrence.Start = new CalDateTime(episode.AirDateUtc.Value) { HasTime = true }; - occurrence.End = new CalDateTime(episode.AirDateUtc.Value.AddMinutes(episode.Series.Runtime)) { HasTime = true }; + occurrence.End = new CalDateTime(episode.AirDateUtc.Value.AddMinutes(series.Runtime)) { HasTime = true }; } - switch (episode.Series.SeriesType) + switch (series.SeriesType) { case SeriesTypes.Daily: - occurrence.Summary = $"{episode.Series.Title} - {episode.Title}"; + occurrence.Summary = $"{series.Title} - {episode.Title}"; break; default: - occurrence.Summary = $"{episode.Series.Title} - {episode.SeasonNumber}x{episode.EpisodeNumber:00} - {episode.Title}"; + occurrence.Summary = $"{series.Title} - {episode.SeasonNumber}x{episode.EpisodeNumber:00} - {episode.Title}"; break; } } @@ -115,7 +94,7 @@ namespace Sonarr.Api.V3.Calendar var serializer = (IStringSerializer)new SerializerFactory().Build(calendar.GetType(), new SerializationContext()); var icalendar = serializer.SerializeToString(calendar); - return new TextResponse(icalendar, "text/calendar"); + return Content(icalendar, "text/calendar"); } } } diff --git a/src/Sonarr.Api.V3/Calendar/CalendarModule.cs b/src/Sonarr.Api.V3/Calendar/CalendarModule.cs deleted file mode 100644 index 09017ba60..000000000 --- a/src/Sonarr.Api.V3/Calendar/CalendarModule.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NzbDrone.Core.DecisionEngine; -using NzbDrone.Core.DecisionEngine.Specifications; -using NzbDrone.Core.Tv; -using NzbDrone.SignalR; -using Sonarr.Api.V3.Episodes; -using Sonarr.Http.Extensions; - -namespace Sonarr.Api.V3.Calendar -{ - public class CalendarModule : EpisodeModuleWithSignalR - { - public CalendarModule(IEpisodeService episodeService, - ISeriesService seriesService, - IUpgradableSpecification ugradableSpecification, - IBroadcastSignalRMessage signalRBroadcaster) - : base(episodeService, seriesService, ugradableSpecification, signalRBroadcaster, "calendar") - { - GetResourceAll = GetCalendar; - } - - private List GetCalendar() - { - var start = DateTime.Today; - var end = DateTime.Today.AddDays(2); - var includeUnmonitored = Request.GetBooleanQueryParameter("unmonitored"); - var includeSeries = Request.GetBooleanQueryParameter("includeSeries"); - var includeEpisodeFile = Request.GetBooleanQueryParameter("includeEpisodeFile"); - var includeEpisodeImages = Request.GetBooleanQueryParameter("includeEpisodeImages"); - - var queryStart = Request.Query.Start; - var queryEnd = Request.Query.End; - - if (queryStart.HasValue) - { - start = DateTime.Parse(queryStart.Value); - } - - if (queryEnd.HasValue) - { - end = DateTime.Parse(queryEnd.Value); - } - - var resources = MapToResource(_episodeService.EpisodesBetweenDates(start, end, includeUnmonitored), includeSeries, includeEpisodeFile, includeEpisodeImages); - - return resources.OrderBy(e => e.AirDateUtc).ToList(); - } - } -} diff --git a/src/Sonarr.Api.V3/Commands/CommandModule.cs b/src/Sonarr.Api.V3/Commands/CommandController.cs similarity index 62% rename from src/Sonarr.Api.V3/Commands/CommandModule.cs rename to src/Sonarr.Api.V3/Commands/CommandController.cs index 3c960d816..5112162f5 100644 --- a/src/Sonarr.Api.V3/Commands/CommandModule.cs +++ b/src/Sonarr.Api.V3/Commands/CommandController.cs @@ -1,7 +1,11 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Common; +using NzbDrone.Common.Composition; +using NzbDrone.Common.Serializer; using NzbDrone.Common.TPL; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Messaging.Commands; @@ -9,63 +13,68 @@ using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.ProgressMessaging; using NzbDrone.SignalR; using Sonarr.Http; -using Sonarr.Http.Extensions; +using Sonarr.Http.REST; +using Sonarr.Http.REST.Attributes; using Sonarr.Http.Validation; namespace Sonarr.Api.V3.Commands { - public class CommandModule : SonarrRestModuleWithSignalR, IHandle + [V3ApiController] + public class CommandController : RestControllerWithSignalR, IHandle { private readonly IManageCommandQueue _commandQueueManager; - private readonly IServiceFactory _serviceFactory; + private readonly KnownTypes _knownTypes; private readonly Debouncer _debouncer; private readonly Dictionary _pendingUpdates; private readonly CommandPriorityComparer _commandPriorityComparer = new CommandPriorityComparer(); - public CommandModule(IManageCommandQueue commandQueueManager, + public CommandController(IManageCommandQueue commandQueueManager, IBroadcastSignalRMessage signalRBroadcaster, - IServiceFactory serviceFactory) + KnownTypes knownTypes) : base(signalRBroadcaster) { _commandQueueManager = commandQueueManager; - _serviceFactory = serviceFactory; + _knownTypes = knownTypes; _debouncer = new Debouncer(SendUpdates, TimeSpan.FromSeconds(0.1)); _pendingUpdates = new Dictionary(); - GetResourceById = GetCommand; - CreateResource = StartCommand; - GetResourceAll = GetStartedCommands; - DeleteResource = CancelCommand; - PostValidator.RuleFor(c => c.Name).NotBlank(); } - private CommandResource GetCommand(int id) + protected override CommandResource GetResourceById(int id) { return _commandQueueManager.Get(id).ToResource(); } - private int StartCommand(CommandResource commandResource) + [RestPostById] + public ActionResult StartCommand(CommandResource commandResource) { var commandType = - _serviceFactory.GetImplementations(typeof(Command)) + _knownTypes.GetImplementations(typeof(Command)) .Single(c => c.Name.Replace("Command", "") .Equals(commandResource.Name, StringComparison.InvariantCultureIgnoreCase)); - dynamic command = Request.Body.FromJson(commandType); - command.Trigger = CommandTrigger.Manual; - command.SuppressMessages = !command.SendUpdatesToClient; - command.SendUpdatesToClient = true; + Request.Body.Seek(0, SeekOrigin.Begin); + using (var reader = new StreamReader(Request.Body)) + { + var body = reader.ReadToEnd(); - command.ClientUserAgent = Request.Headers.UserAgent; + dynamic command = STJson.Deserialize(body, commandType); - var trackedCommand = _commandQueueManager.Push(command, CommandPriority.Normal, CommandTrigger.Manual); - return trackedCommand.Id; + command.Trigger = CommandTrigger.Manual; + command.SuppressMessages = !command.SendUpdatesToClient; + command.SendUpdatesToClient = true; + command.ClientUserAgent = Request.Headers["UserAgent"]; + + var trackedCommand = _commandQueueManager.Push(command, CommandPriority.Normal, CommandTrigger.Manual); + return Created(trackedCommand.Id); + } } - private List GetStartedCommands() + [HttpGet] + public List GetStartedCommands() { return _commandQueueManager.All() .OrderBy(c => c.Status, _commandPriorityComparer) @@ -73,11 +82,13 @@ namespace Sonarr.Api.V3.Commands .ToResource(); } - private void CancelCommand(int id) + [RestDeleteById] + public void CancelCommand(int id) { _commandQueueManager.Cancel(id); } + [NonAction] public void Handle(CommandUpdatedEvent message) { if (message.Command.Body.SendUpdatesToClient) diff --git a/src/Sonarr.Api.V3/Config/ConfigController.cs b/src/Sonarr.Api.V3/Config/ConfigController.cs new file mode 100644 index 000000000..52a77cbe1 --- /dev/null +++ b/src/Sonarr.Api.V3/Config/ConfigController.cs @@ -0,0 +1,48 @@ +using System.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Configuration; +using Sonarr.Http.REST; +using Sonarr.Http.REST.Attributes; + +namespace Sonarr.Api.V3.Config +{ + public abstract class ConfigController : RestController + where TResource : RestResource, new() + { + private readonly IConfigService _configService; + + protected ConfigController(IConfigService configService) + { + _configService = configService; + } + + protected override TResource GetResourceById(int id) + { + return GetConfig(); + } + + [HttpGet] + public TResource GetConfig() + { + var resource = ToResource(_configService); + resource.Id = 1; + + return resource; + } + + [RestPutById] + public ActionResult SaveConfig(TResource resource) + { + var dictionary = resource.GetType() + .GetProperties(BindingFlags.Instance | BindingFlags.Public) + .ToDictionary(prop => prop.Name, prop => prop.GetValue(resource, null)); + + _configService.SaveConfigDictionary(dictionary); + + return Accepted(resource.Id); + } + + protected abstract TResource ToResource(IConfigService model); + } +} diff --git a/src/Sonarr.Api.V3/Config/DownloadClientConfigModule.cs b/src/Sonarr.Api.V3/Config/DownloadClientConfigController.cs similarity index 50% rename from src/Sonarr.Api.V3/Config/DownloadClientConfigModule.cs rename to src/Sonarr.Api.V3/Config/DownloadClientConfigController.cs index 242e3971a..7cb3d170b 100644 --- a/src/Sonarr.Api.V3/Config/DownloadClientConfigModule.cs +++ b/src/Sonarr.Api.V3/Config/DownloadClientConfigController.cs @@ -1,10 +1,12 @@ -using NzbDrone.Core.Configuration; +using NzbDrone.Core.Configuration; +using Sonarr.Http; namespace Sonarr.Api.V3.Config { - public class DownloadClientConfigModule : SonarrConfigModule + [V3ApiController("config/downloadclient")] + public class DownloadClientConfigController : ConfigController { - public DownloadClientConfigModule(IConfigService configService) + public DownloadClientConfigController(IConfigService configService) : base(configService) { } diff --git a/src/Sonarr.Api.V3/Config/HostConfigModule.cs b/src/Sonarr.Api.V3/Config/HostConfigController.cs similarity index 83% rename from src/Sonarr.Api.V3/Config/HostConfigModule.cs rename to src/Sonarr.Api.V3/Config/HostConfigController.cs index c72646a6c..121b76bde 100644 --- a/src/Sonarr.Api.V3/Config/HostConfigModule.cs +++ b/src/Sonarr.Api.V3/Config/HostConfigController.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Reflection; using System.Security.Cryptography.X509Certificates; using FluentValidation; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.Authentication; using NzbDrone.Core.Configuration; @@ -10,29 +11,27 @@ using NzbDrone.Core.Update; using NzbDrone.Core.Validation; using NzbDrone.Core.Validation.Paths; using Sonarr.Http; +using Sonarr.Http.REST; +using Sonarr.Http.REST.Attributes; namespace Sonarr.Api.V3.Config { - public class HostConfigModule : SonarrRestModule + [V3ApiController("config/host")] + public class HostConfigController : RestController { private readonly IConfigFileProvider _configFileProvider; private readonly IConfigService _configService; private readonly IUserService _userService; - public HostConfigModule(IConfigFileProvider configFileProvider, - IConfigService configService, - IUserService userService, - FileExistsValidator fileExistsValidator) - : base("/config/host") + public HostConfigController(IConfigFileProvider configFileProvider, + IConfigService configService, + IUserService userService, + FileExistsValidator fileExistsValidator) { _configFileProvider = configFileProvider; _configService = configService; _userService = userService; - GetResourceSingle = GetHostConfig; - GetResourceById = GetHostConfig; - UpdateResource = SaveHostConfig; - SharedValidator.RuleFor(c => c.BindAddress) .ValidIp4Address() .NotListenAllIp4Address() @@ -80,7 +79,13 @@ namespace Sonarr.Api.V3.Config return cert != null; } - private HostConfigResource GetHostConfig() + protected override HostConfigResource GetResourceById(int id) + { + return GetHostConfig(); + } + + [HttpGet] + public HostConfigResource GetHostConfig() { var resource = _configFileProvider.ToResource(_configService); resource.Id = 1; @@ -95,12 +100,8 @@ namespace Sonarr.Api.V3.Config return resource; } - private HostConfigResource GetHostConfig(int id) - { - return GetHostConfig(); - } - - private void SaveHostConfig(HostConfigResource resource) + [RestPutById] + public ActionResult SaveHostConfig(HostConfigResource resource) { var dictionary = resource.GetType() .GetProperties(BindingFlags.Instance | BindingFlags.Public) @@ -113,6 +114,8 @@ namespace Sonarr.Api.V3.Config { _userService.Upsert(resource.Username, resource.Password); } + + return Accepted(resource.Id); } } } diff --git a/src/Sonarr.Api.V3/Config/IndexerConfigModule.cs b/src/Sonarr.Api.V3/Config/IndexerConfigController.cs similarity index 73% rename from src/Sonarr.Api.V3/Config/IndexerConfigModule.cs rename to src/Sonarr.Api.V3/Config/IndexerConfigController.cs index 7251c9d78..fe4717757 100644 --- a/src/Sonarr.Api.V3/Config/IndexerConfigModule.cs +++ b/src/Sonarr.Api.V3/Config/IndexerConfigController.cs @@ -1,12 +1,14 @@ -using FluentValidation; +using FluentValidation; using NzbDrone.Core.Configuration; +using Sonarr.Http; using Sonarr.Http.Validation; namespace Sonarr.Api.V3.Config { - public class IndexerConfigModule : SonarrConfigModule + [V3ApiController("config/indexer")] + public class IndexerConfigController : ConfigController { - public IndexerConfigModule(IConfigService configService) + public IndexerConfigController(IConfigService configService) : base(configService) { SharedValidator.RuleFor(c => c.MinimumAge) diff --git a/src/Sonarr.Api.V3/Config/MediaManagementConfigController.cs b/src/Sonarr.Api.V3/Config/MediaManagementConfigController.cs new file mode 100644 index 000000000..91e2b17bd --- /dev/null +++ b/src/Sonarr.Api.V3/Config/MediaManagementConfigController.cs @@ -0,0 +1,45 @@ +using FluentValidation; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Validation; +using NzbDrone.Core.Validation.Paths; +using Sonarr.Http; + +namespace Sonarr.Api.V3.Config +{ + [V3ApiController("config/mediamanagement")] + public class MediaManagementConfigController : ConfigController + { + public MediaManagementConfigController(IConfigService configService, + PathExistsValidator pathExistsValidator, + FolderChmodValidator folderChmodValidator, + FolderWritableValidator folderWritableValidator, + SeriesPathValidator seriesPathValidator, + StartupFolderValidator startupFolderValidator, + SystemFolderValidator systemFolderValidator, + RootFolderAncestorValidator rootFolderAncestorValidator, + RootFolderValidator rootFolderValidator) + : base(configService) + { + SharedValidator.RuleFor(c => c.RecycleBinCleanupDays).GreaterThanOrEqualTo(0); + SharedValidator.RuleFor(c => c.ChmodFolder).SetValidator(folderChmodValidator).When(c => !string.IsNullOrEmpty(c.ChmodFolder) && (OsInfo.IsLinux || OsInfo.IsOsx)); + + SharedValidator.RuleFor(c => c.RecycleBin).IsValidPath() + .SetValidator(folderWritableValidator) + .SetValidator(rootFolderValidator) + .SetValidator(pathExistsValidator) + .SetValidator(rootFolderAncestorValidator) + .SetValidator(startupFolderValidator) + .SetValidator(systemFolderValidator) + .SetValidator(seriesPathValidator) + .When(c => !string.IsNullOrWhiteSpace(c.RecycleBin)); + + SharedValidator.RuleFor(c => c.MinimumFreeSpaceWhenImporting).GreaterThanOrEqualTo(100); + } + + protected override MediaManagementConfigResource ToResource(IConfigService model) + { + return MediaManagementConfigResourceMapper.ToResource(model); + } + } +} diff --git a/src/Sonarr.Api.V3/Config/MediaManagementConfigModule.cs b/src/Sonarr.Api.V3/Config/MediaManagementConfigModule.cs deleted file mode 100644 index fbbe2459c..000000000 --- a/src/Sonarr.Api.V3/Config/MediaManagementConfigModule.cs +++ /dev/null @@ -1,25 +0,0 @@ -using FluentValidation; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.Validation; -using NzbDrone.Core.Validation.Paths; - -namespace Sonarr.Api.V3.Config -{ - public class MediaManagementConfigModule : SonarrConfigModule - { - public MediaManagementConfigModule(IConfigService configService, PathExistsValidator pathExistsValidator, FolderChmodValidator folderChmodValidator) - : base(configService) - { - SharedValidator.RuleFor(c => c.RecycleBinCleanupDays).GreaterThanOrEqualTo(0); - SharedValidator.RuleFor(c => c.ChmodFolder).SetValidator(folderChmodValidator).When(c => !string.IsNullOrEmpty(c.ChmodFolder) && OsInfo.IsNotWindows); - SharedValidator.RuleFor(c => c.RecycleBin).IsValidPath().SetValidator(pathExistsValidator).When(c => !string.IsNullOrWhiteSpace(c.RecycleBin)); - SharedValidator.RuleFor(c => c.MinimumFreeSpaceWhenImporting).GreaterThanOrEqualTo(100); - } - - protected override MediaManagementConfigResource ToResource(IConfigService model) - { - return MediaManagementConfigResourceMapper.ToResource(model); - } - } -} diff --git a/src/Sonarr.Api.V3/Config/NamingConfigModule.cs b/src/Sonarr.Api.V3/Config/NamingConfigController.cs similarity index 86% rename from src/Sonarr.Api.V3/Config/NamingConfigModule.cs rename to src/Sonarr.Api.V3/Config/NamingConfigController.cs index 2863b829b..137f24769 100644 --- a/src/Sonarr.Api.V3/Config/NamingConfigModule.cs +++ b/src/Sonarr.Api.V3/Config/NamingConfigController.cs @@ -1,36 +1,33 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using FluentValidation; using FluentValidation.Results; -using Nancy.ModelBinding; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.Organizer; using Sonarr.Http; +using Sonarr.Http.REST; +using Sonarr.Http.REST.Attributes; namespace Sonarr.Api.V3.Config { - public class NamingConfigModule : SonarrRestModule + [V3ApiController("config/naming")] + public class NamingConfigController : RestController { private readonly INamingConfigService _namingConfigService; private readonly IFilenameSampleService _filenameSampleService; private readonly IFilenameValidationService _filenameValidationService; private readonly IBuildFileNames _filenameBuilder; - public NamingConfigModule(INamingConfigService namingConfigService, - IFilenameSampleService filenameSampleService, - IFilenameValidationService filenameValidationService, - IBuildFileNames filenameBuilder) - : base("config/naming") + public NamingConfigController(INamingConfigService namingConfigService, + IFilenameSampleService filenameSampleService, + IFilenameValidationService filenameValidationService, + IBuildFileNames filenameBuilder) { _namingConfigService = namingConfigService; _filenameSampleService = filenameSampleService; _filenameValidationService = filenameValidationService; _filenameBuilder = filenameBuilder; - GetResourceSingle = GetNamingConfig; - GetResourceById = GetNamingConfig; - UpdateResource = UpdateNamingConfig; - - Get("/examples", x => GetExamples(this.Bind())); SharedValidator.RuleFor(c => c.MultiEpisodeStyle).InclusiveBetween(0, 5); SharedValidator.RuleFor(c => c.StandardEpisodeFormat).ValidEpisodeFormat(); @@ -41,15 +38,13 @@ namespace Sonarr.Api.V3.Config SharedValidator.RuleFor(c => c.SpecialsFolderFormat).ValidSpecialsFolderFormat(); } - private void UpdateNamingConfig(NamingConfigResource resource) + protected override NamingConfigResource GetResourceById(int id) { - var nameSpec = resource.ToModel(); - ValidateFormatResult(nameSpec); - - _namingConfigService.Save(nameSpec); + return GetNamingConfig(); } - private NamingConfigResource GetNamingConfig() + [HttpGet] + public NamingConfigResource GetNamingConfig() { var nameSpec = _namingConfigService.GetConfig(); var resource = nameSpec.ToResource(); @@ -63,12 +58,19 @@ namespace Sonarr.Api.V3.Config return resource; } - private NamingConfigResource GetNamingConfig(int id) + [RestPutById] + public ActionResult UpdateNamingConfig(NamingConfigResource resource) { - return GetNamingConfig(); + var nameSpec = resource.ToModel(); + ValidateFormatResult(nameSpec); + + _namingConfigService.Save(nameSpec); + + return Accepted(resource.Id); } - private object GetExamples(NamingConfigResource config) + [HttpGet("examples")] + public object GetExamples([FromQuery]NamingConfigResource config) { if (config.Id == 0) { diff --git a/src/Sonarr.Api.V3/Config/SonarrConfigModule.cs b/src/Sonarr.Api.V3/Config/SonarrConfigModule.cs deleted file mode 100644 index ae7a000dc..000000000 --- a/src/Sonarr.Api.V3/Config/SonarrConfigModule.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Linq; -using System.Reflection; -using NzbDrone.Core.Configuration; -using Sonarr.Http; -using Sonarr.Http.REST; - -namespace Sonarr.Api.V3.Config -{ - public abstract class SonarrConfigModule : SonarrRestModule - where TResource : RestResource, new() - { - private readonly IConfigService _configService; - - protected SonarrConfigModule(IConfigService configService) - : this(new TResource().ResourceName.Replace("config", ""), configService) - { - } - - protected SonarrConfigModule(string resource, IConfigService configService) - : base("config/" + resource.Trim('/')) - { - _configService = configService; - - GetResourceSingle = GetConfig; - GetResourceById = GetConfig; - UpdateResource = SaveConfig; - } - - private TResource GetConfig() - { - var resource = ToResource(_configService); - resource.Id = 1; - - return resource; - } - - protected abstract TResource ToResource(IConfigService model); - - private TResource GetConfig(int id) - { - return GetConfig(); - } - - private void SaveConfig(TResource resource) - { - var dictionary = resource.GetType() - .GetProperties(BindingFlags.Instance | BindingFlags.Public) - .ToDictionary(prop => prop.Name, prop => prop.GetValue(resource, null)); - - _configService.SaveConfigDictionary(dictionary); - } - } -} diff --git a/src/Sonarr.Api.V3/Config/UiConfigModule.cs b/src/Sonarr.Api.V3/Config/UiConfigController.cs similarity index 53% rename from src/Sonarr.Api.V3/Config/UiConfigModule.cs rename to src/Sonarr.Api.V3/Config/UiConfigController.cs index 86306f22a..ea04e9765 100644 --- a/src/Sonarr.Api.V3/Config/UiConfigModule.cs +++ b/src/Sonarr.Api.V3/Config/UiConfigController.cs @@ -1,10 +1,12 @@ -using NzbDrone.Core.Configuration; +using NzbDrone.Core.Configuration; +using Sonarr.Http; namespace Sonarr.Api.V3.Config { - public class UiConfigModule : SonarrConfigModule + [V3ApiController("config/ui")] + public class UiConfigController : ConfigController { - public UiConfigModule(IConfigService configService) + public UiConfigController(IConfigService configService) : base(configService) { } diff --git a/src/Sonarr.Api.V3/CustomFilters/CustomFilterController.cs b/src/Sonarr.Api.V3/CustomFilters/CustomFilterController.cs new file mode 100644 index 000000000..59bf82174 --- /dev/null +++ b/src/Sonarr.Api.V3/CustomFilters/CustomFilterController.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.CustomFilters; +using Sonarr.Http; +using Sonarr.Http.REST; +using Sonarr.Http.REST.Attributes; + +namespace Sonarr.Api.V3.CustomFilters +{ + [V3ApiController] + public class CustomFilterController : RestController + { + private readonly ICustomFilterService _customFilterService; + + public CustomFilterController(ICustomFilterService customFilterService) + { + _customFilterService = customFilterService; + } + + protected override CustomFilterResource GetResourceById(int id) + { + return _customFilterService.Get(id).ToResource(); + } + + [HttpGet] + public List GetCustomFilters() + { + return _customFilterService.All().ToResource(); + } + + [RestPostById] + public ActionResult AddCustomFilter(CustomFilterResource resource) + { + var customFilter = _customFilterService.Add(resource.ToModel()); + + return Created(customFilter.Id); + } + + [RestPutById] + public ActionResult UpdateCustomFilter(CustomFilterResource resource) + { + _customFilterService.Update(resource.ToModel()); + return Accepted(resource.Id); + } + + [RestDeleteById] + public void DeleteCustomResource(int id) + { + _customFilterService.Delete(id); + } + } +} diff --git a/src/Sonarr.Api.V3/CustomFilters/CustomFilterModule.cs b/src/Sonarr.Api.V3/CustomFilters/CustomFilterModule.cs deleted file mode 100644 index 473b4628c..000000000 --- a/src/Sonarr.Api.V3/CustomFilters/CustomFilterModule.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Core.CustomFilters; -using Sonarr.Http; - -namespace Sonarr.Api.V3.CustomFilters -{ - public class CustomFilterModule : SonarrRestModule - { - private readonly ICustomFilterService _customFilterService; - - public CustomFilterModule(ICustomFilterService customFilterService) - { - _customFilterService = customFilterService; - - GetResourceById = GetCustomFilter; - GetResourceAll = GetCustomFilters; - CreateResource = AddCustomFilter; - UpdateResource = UpdateCustomFilter; - DeleteResource = DeleteCustomResource; - } - - private CustomFilterResource GetCustomFilter(int id) - { - return _customFilterService.Get(id).ToResource(); - } - - private List GetCustomFilters() - { - return _customFilterService.All().ToResource(); - } - - private int AddCustomFilter(CustomFilterResource resource) - { - var customFilter = _customFilterService.Add(resource.ToModel()); - - return customFilter.Id; - } - - private void UpdateCustomFilter(CustomFilterResource resource) - { - _customFilterService.Update(resource.ToModel()); - } - - private void DeleteCustomResource(int id) - { - _customFilterService.Delete(id); - } - } -} diff --git a/src/Sonarr.Api.V3/DiskSpace/DiskSpaceModule.cs b/src/Sonarr.Api.V3/DiskSpace/DiskSpaceController.cs similarity index 62% rename from src/Sonarr.Api.V3/DiskSpace/DiskSpaceModule.cs rename to src/Sonarr.Api.V3/DiskSpace/DiskSpaceController.cs index adf256a7c..44ce2dd8e 100644 --- a/src/Sonarr.Api.V3/DiskSpace/DiskSpaceModule.cs +++ b/src/Sonarr.Api.V3/DiskSpace/DiskSpaceController.cs @@ -1,20 +1,21 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.DiskSpace; using Sonarr.Http; namespace Sonarr.Api.V3.DiskSpace { - public class DiskSpaceModule : SonarrRestModule + [V3ApiController("diskspace")] + public class DiskSpaceController : Controller { private readonly IDiskSpaceService _diskSpaceService; - public DiskSpaceModule(IDiskSpaceService diskSpaceService) - : base("diskspace") + public DiskSpaceController(IDiskSpaceService diskSpaceService) { _diskSpaceService = diskSpaceService; - GetResourceAll = GetFreeSpace; } + [HttpGet] public List GetFreeSpace() { return _diskSpaceService.GetFreeSpace().ConvertAll(DiskSpaceResourceMapper.MapToResource); diff --git a/src/Sonarr.Api.V3/DownloadClient/DownloadClientController.cs b/src/Sonarr.Api.V3/DownloadClient/DownloadClientController.cs new file mode 100644 index 000000000..b50db0d78 --- /dev/null +++ b/src/Sonarr.Api.V3/DownloadClient/DownloadClientController.cs @@ -0,0 +1,16 @@ +using NzbDrone.Core.Download; +using Sonarr.Http; + +namespace Sonarr.Api.V3.DownloadClient +{ + [V3ApiController] + public class DownloadClientController : ProviderControllerBase + { + public static readonly DownloadClientResourceMapper ResourceMapper = new DownloadClientResourceMapper(); + + public DownloadClientController(IDownloadClientFactory downloadClientFactory) + : base(downloadClientFactory, "downloadclient", ResourceMapper) + { + } + } +} diff --git a/src/Sonarr.Api.V3/DownloadClient/DownloadClientModule.cs b/src/Sonarr.Api.V3/DownloadClient/DownloadClientModule.cs deleted file mode 100644 index 4575a2bd4..000000000 --- a/src/Sonarr.Api.V3/DownloadClient/DownloadClientModule.cs +++ /dev/null @@ -1,14 +0,0 @@ -using NzbDrone.Core.Download; - -namespace Sonarr.Api.V3.DownloadClient -{ - public class DownloadClientModule : ProviderModuleBase - { - public static readonly DownloadClientResourceMapper ResourceMapper = new DownloadClientResourceMapper(); - - public DownloadClientModule(IDownloadClientFactory downloadClientFactory) - : base(downloadClientFactory, "downloadclient", ResourceMapper) - { - } - } -} diff --git a/src/Sonarr.Api.V3/DownloadClient/DownloadClientResource.cs b/src/Sonarr.Api.V3/DownloadClient/DownloadClientResource.cs index 31e200f0b..85372aedb 100644 --- a/src/Sonarr.Api.V3/DownloadClient/DownloadClientResource.cs +++ b/src/Sonarr.Api.V3/DownloadClient/DownloadClientResource.cs @@ -3,7 +3,7 @@ using NzbDrone.Core.Indexers; namespace Sonarr.Api.V3.DownloadClient { - public class DownloadClientResource : ProviderResource + public class DownloadClientResource : ProviderResource { public bool Enable { get; set; } public DownloadProtocol Protocol { get; set; } diff --git a/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileModule.cs b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileController.cs similarity index 70% rename from src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileModule.cs rename to src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileController.cs index 2111c0d8f..2532ed14f 100644 --- a/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileModule.cs +++ b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileController.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using Nancy; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Exceptions; @@ -12,12 +12,14 @@ using NzbDrone.Core.Parser; using NzbDrone.Core.Tv; using NzbDrone.SignalR; using Sonarr.Http; -using Sonarr.Http.Extensions; +using Sonarr.Http.REST; +using Sonarr.Http.REST.Attributes; using BadRequestException = Sonarr.Http.REST.BadRequestException; namespace Sonarr.Api.V3.EpisodeFiles { - public class EpisodeFileModule : SonarrRestModuleWithSignalR, + [V3ApiController] + public class EpisodeFileController : RestControllerWithSignalR, IHandle, IHandle { @@ -26,7 +28,7 @@ namespace Sonarr.Api.V3.EpisodeFiles private readonly ISeriesService _seriesService; private readonly IUpgradableSpecification _upgradableSpecification; - public EpisodeFileModule(IBroadcastSignalRMessage signalRBroadcaster, + public EpisodeFileController(IBroadcastSignalRMessage signalRBroadcaster, IMediaFileService mediaFileService, IDeleteMediaFiles mediaFileDeletionService, ISeriesService seriesService, @@ -37,18 +39,9 @@ namespace Sonarr.Api.V3.EpisodeFiles _mediaFileDeletionService = mediaFileDeletionService; _seriesService = seriesService; _upgradableSpecification = upgradableSpecification; - - GetResourceById = GetEpisodeFile; - GetResourceAll = GetEpisodeFiles; - UpdateResource = SetQuality; - DeleteResource = DeleteEpisodeFile; - - Put("/editor", episodeFiles => SetPropertiesEditor()); - Put("/bulk", episodeFiles => SetPropertiesBulk()); - Delete("/bulk", episodeFiles => DeleteEpisodeFiles()); } - private EpisodeFileResource GetEpisodeFile(int id) + protected override EpisodeFileResource GetResourceById(int id) { var episodeFile = _mediaFileService.Get(id); var series = _seriesService.GetSeries(episodeFile.SeriesId); @@ -56,31 +49,22 @@ namespace Sonarr.Api.V3.EpisodeFiles return episodeFile.ToResource(series, _upgradableSpecification); } - private List GetEpisodeFiles() + [HttpGet] + public List GetEpisodeFiles(int? seriesId, [FromQuery] List episodeFileIds) { - var seriesIdQuery = Request.Query.SeriesId; - var episodeFileIdsQuery = Request.Query.EpisodeFileIds; - - if (!seriesIdQuery.HasValue && !episodeFileIdsQuery.HasValue) + if (!seriesId.HasValue && !episodeFileIds.Any()) { throw new BadRequestException("seriesId or episodeFileIds must be provided"); } - if (seriesIdQuery.HasValue) + if (seriesId.HasValue) { - int seriesId = Convert.ToInt32(seriesIdQuery.Value); - var series = _seriesService.GetSeries(seriesId); + var series = _seriesService.GetSeries(seriesId.Value); - return _mediaFileService.GetFilesBySeries(seriesId).ConvertAll(f => f.ToResource(series, _upgradableSpecification)); + return _mediaFileService.GetFilesBySeries(seriesId.Value).ConvertAll(f => f.ToResource(series, _upgradableSpecification)); } else { - string episodeFileIdsValue = episodeFileIdsQuery.Value.ToString(); - - var episodeFileIds = episodeFileIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(e => Convert.ToInt32(e)) - .ToList(); - var episodeFiles = _mediaFileService.Get(episodeFileIds); return episodeFiles.GroupBy(e => e.SeriesId) @@ -90,7 +74,8 @@ namespace Sonarr.Api.V3.EpisodeFiles } } - private void SetQuality(EpisodeFileResource episodeFileResource) + [RestPutById] + public ActionResult SetQuality(EpisodeFileResource episodeFileResource) { var episodeFile = _mediaFileService.Get(episodeFileResource.Id); episodeFile.Quality = episodeFileResource.Quality; @@ -106,12 +91,12 @@ namespace Sonarr.Api.V3.EpisodeFiles } _mediaFileService.Update(episodeFile); + return Accepted(episodeFile.Id); } - // Deprecated: Use SetPropertiesBulk instead - private object SetPropertiesEditor() + [HttpPut("editor")] + public object SetQuality([FromBody] EpisodeFileListResource resource) { - var resource = Request.Body.FromJson(); var episodeFiles = _mediaFileService.GetFiles(resource.EpisodeFileIds); foreach (var episodeFile in episodeFiles) @@ -141,17 +126,46 @@ namespace Sonarr.Api.V3.EpisodeFiles var series = _seriesService.GetSeries(episodeFiles.First().SeriesId); - return ResponseWithCode(episodeFiles.ConvertAll(f => f.ToResource(series, _upgradableSpecification)), HttpStatusCode.Accepted); + return Accepted(episodeFiles.ConvertAll(f => f.ToResource(series, _upgradableSpecification))); } - private object SetPropertiesBulk() + [RestDeleteById] + public void DeleteEpisodeFile(int id) { - var resource = Request.Body.FromJson>(); - var episodeFiles = _mediaFileService.GetFiles(resource.Select(r => r.Id)); + var episodeFile = _mediaFileService.Get(id); + + if (episodeFile == null) + { + throw new NzbDroneClientException(global::System.Net.HttpStatusCode.NotFound, "Episode file not found"); + } + + var series = _seriesService.GetSeries(episodeFile.SeriesId); + + _mediaFileDeletionService.DeleteEpisodeFile(series, episodeFile); + } + + [HttpDelete("bulk")] + public object DeleteEpisodeFiles([FromBody] EpisodeFileListResource resource) + { + var episodeFiles = _mediaFileService.GetFiles(resource.EpisodeFileIds); + var series = _seriesService.GetSeries(episodeFiles.First().SeriesId); foreach (var episodeFile in episodeFiles) { - var resourceEpisodeFile = resource.Single(r => r.Id == episodeFile.Id); + _mediaFileDeletionService.DeleteEpisodeFile(series, episodeFile); + } + + return new { }; + } + + [HttpPut("bulk")] + public object SetPropertiesBulk([FromBody] List resources) + { + var episodeFiles = _mediaFileService.GetFiles(resources.Select(r => r.Id)); + + foreach (var episodeFile in episodeFiles) + { + var resourceEpisodeFile = resources.Single(r => r.Id == episodeFile.Id); if (resourceEpisodeFile.Language != null) { @@ -175,45 +189,17 @@ namespace Sonarr.Api.V3.EpisodeFiles } _mediaFileService.Update(episodeFiles); - var series = _seriesService.GetSeries(episodeFiles.First().SeriesId); - - return ResponseWithCode(episodeFiles.ConvertAll(f => f.ToResource(series, _upgradableSpecification)), HttpStatusCode.Accepted); - } - - private void DeleteEpisodeFile(int id) - { - var episodeFile = _mediaFileService.Get(id); - - if (episodeFile == null) - { - throw new NzbDroneClientException(global::System.Net.HttpStatusCode.NotFound, "Episode file not found"); - } - - var series = _seriesService.GetSeries(episodeFile.SeriesId); - - _mediaFileDeletionService.DeleteEpisodeFile(series, episodeFile); - } - - private object DeleteEpisodeFiles() - { - var resource = Request.Body.FromJson(); - var episodeFiles = _mediaFileService.GetFiles(resource.EpisodeFileIds); - var series = _seriesService.GetSeries(episodeFiles.First().SeriesId); - - foreach (var episodeFile in episodeFiles) - { - _mediaFileDeletionService.DeleteEpisodeFile(series, episodeFile); - } - - return new object(); + return Accepted(episodeFiles.ConvertAll(f => f.ToResource(series, _upgradableSpecification))); } + [NonAction] public void Handle(EpisodeFileAddedEvent message) { BroadcastResourceChange(ModelAction.Updated, message.EpisodeFile.Id); } + [NonAction] public void Handle(EpisodeFileDeletedEvent message) { BroadcastResourceChange(ModelAction.Deleted, message.EpisodeFile.Id); diff --git a/src/Sonarr.Api.V3/Episodes/EpisodeController.cs b/src/Sonarr.Api.V3/Episodes/EpisodeController.cs new file mode 100644 index 000000000..cc13a34b6 --- /dev/null +++ b/src/Sonarr.Api.V3/Episodes/EpisodeController.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.DecisionEngine.Specifications; +using NzbDrone.Core.Tv; +using NzbDrone.SignalR; +using Sonarr.Http; +using Sonarr.Http.Extensions; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Episodes +{ + [V3ApiController] + public class EpisodeController : EpisodeControllerWithSignalR + { + public EpisodeController(ISeriesService seriesService, + IEpisodeService episodeService, + IUpgradableSpecification upgradableSpecification, + IBroadcastSignalRMessage signalRBroadcaster) + : base(episodeService, seriesService, upgradableSpecification, signalRBroadcaster) + { + } + + [HttpGet] + public List GetEpisodes(int? seriesId, int? seasonNumber, [FromQuery]List episodeIds, int? episodeFileId, bool includeImages = false) + { + if (seriesId.HasValue) + { + if (seasonNumber.HasValue) + { + return MapToResource(_episodeService.GetEpisodesBySeason(seriesId.Value, seasonNumber.Value), false, false, includeImages); + } + + return MapToResource(_episodeService.GetEpisodeBySeries(seriesId.Value), false, false, includeImages); + } + else if (episodeIds.Any()) + { + return MapToResource(_episodeService.GetEpisodes(episodeIds), false, false, includeImages); + } + else if (episodeFileId.HasValue) + { + return MapToResource(_episodeService.GetEpisodesByFileId(episodeFileId.Value), false, false, includeImages); + } + + throw new BadRequestException("seriesId or episodeIds must be provided"); + } + + [HttpPut("{id}")] + public IActionResult SetEpisodeMonitored([FromBody] EpisodeResource resource, [FromRoute] int id) + { + _episodeService.SetEpisodeMonitored(id, resource.Monitored); + + resource = MapToResource(_episodeService.GetEpisode(id), false, false, false); + + return Accepted(resource); + } + + [HttpPut("monitor")] + public IActionResult SetEpisodesMonitored([FromBody] EpisodesMonitoredResource resource) + { + var includeImages = Request.GetBooleanQueryParameter("includeImages", false); + + if (resource.EpisodeIds.Count == 1) + { + _episodeService.SetEpisodeMonitored(resource.EpisodeIds.First(), resource.Monitored); + } + else + { + _episodeService.SetMonitored(resource.EpisodeIds, resource.Monitored); + } + + var resources = MapToResource(_episodeService.GetEpisodes(resource.EpisodeIds), false, false, includeImages); + + return Accepted(resources); + } + } +} diff --git a/src/Sonarr.Api.V3/Episodes/EpisodeModuleWithSignalR.cs b/src/Sonarr.Api.V3/Episodes/EpisodeControllerWithSignalR.cs similarity index 82% rename from src/Sonarr.Api.V3/Episodes/EpisodeModuleWithSignalR.cs rename to src/Sonarr.Api.V3/Episodes/EpisodeControllerWithSignalR.cs index b51c609b7..4447811d3 100644 --- a/src/Sonarr.Api.V3/Episodes/EpisodeModuleWithSignalR.cs +++ b/src/Sonarr.Api.V3/Episodes/EpisodeControllerWithSignalR.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; -using NzbDrone.Common.Extensions; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Datastore.Events; -using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Download; using NzbDrone.Core.MediaFiles.Events; @@ -10,20 +9,20 @@ using NzbDrone.Core.Tv; using NzbDrone.SignalR; using Sonarr.Api.V3.EpisodeFiles; using Sonarr.Api.V3.Series; -using Sonarr.Http; +using Sonarr.Http.REST; namespace Sonarr.Api.V3.Episodes { - public abstract class EpisodeModuleWithSignalR : SonarrRestModuleWithSignalR, - IHandle, - IHandle, - IHandle + public abstract class EpisodeControllerWithSignalR : RestControllerWithSignalR, + IHandle, + IHandle, + IHandle { protected readonly IEpisodeService _episodeService; protected readonly ISeriesService _seriesService; protected readonly IUpgradableSpecification _upgradableSpecification; - protected EpisodeModuleWithSignalR(IEpisodeService episodeService, + protected EpisodeControllerWithSignalR(IEpisodeService episodeService, ISeriesService seriesService, IUpgradableSpecification upgradableSpecification, IBroadcastSignalRMessage signalRBroadcaster) @@ -32,38 +31,27 @@ namespace Sonarr.Api.V3.Episodes _episodeService = episodeService; _seriesService = seriesService; _upgradableSpecification = upgradableSpecification; - - GetResourceById = GetEpisode; } - protected EpisodeModuleWithSignalR(IEpisodeService episodeService, + protected EpisodeControllerWithSignalR(IEpisodeService episodeService, ISeriesService seriesService, IUpgradableSpecification upgradableSpecification, IBroadcastSignalRMessage signalRBroadcaster, string resource) - : base(signalRBroadcaster, resource) + : base(signalRBroadcaster) { _episodeService = episodeService; _seriesService = seriesService; _upgradableSpecification = upgradableSpecification; - - GetResourceById = GetEpisode; } - protected EpisodeResource GetEpisode(int id) + protected override EpisodeResource GetResourceById(int id) { var episode = _episodeService.GetEpisode(id); var resource = MapToResource(episode, true, true, true); return resource; } - protected override EpisodeResource GetResourceByIdForBroadcast(int id) - { - var episode = _episodeService.GetEpisode(id); - var resource = MapToResource(episode, false, false, false); - return resource; - } - protected EpisodeResource MapToResource(Episode episode, bool includeSeries, bool includeEpisodeFile, bool includeImages) { var resource = episode.ToResource(); @@ -126,6 +114,7 @@ namespace Sonarr.Api.V3.Episodes return result; } + [NonAction] public void Handle(EpisodeGrabbedEvent message) { foreach (var episode in message.Episode.Episodes) @@ -137,6 +126,7 @@ namespace Sonarr.Api.V3.Episodes } } + [NonAction] public void Handle(EpisodeImportedEvent message) { foreach (var episode in message.EpisodeInfo.Episodes) @@ -145,6 +135,7 @@ namespace Sonarr.Api.V3.Episodes } } + [NonAction] public void Handle(EpisodeFileDeletedEvent message) { foreach (var episode in message.EpisodeFile.Episodes.Value) diff --git a/src/Sonarr.Api.V3/Episodes/EpisodeModule.cs b/src/Sonarr.Api.V3/Episodes/EpisodeModule.cs deleted file mode 100644 index 53bf48925..000000000 --- a/src/Sonarr.Api.V3/Episodes/EpisodeModule.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Nancy; -using NzbDrone.Core.DecisionEngine; -using NzbDrone.Core.DecisionEngine.Specifications; -using NzbDrone.Core.Tv; -using NzbDrone.SignalR; -using Sonarr.Http.Extensions; -using Sonarr.Http.REST; - -namespace Sonarr.Api.V3.Episodes -{ - public class EpisodeModule : EpisodeModuleWithSignalR - { - public EpisodeModule(ISeriesService seriesService, - IEpisodeService episodeService, - IUpgradableSpecification upgradableSpecification, - IBroadcastSignalRMessage signalRBroadcaster) - : base(episodeService, seriesService, upgradableSpecification, signalRBroadcaster) - { - GetResourceAll = GetEpisodes; - Put(@"/(?[\d]{1,10})", x => SetEpisodeMonitored(x.Id)); - Put("/monitor", x => SetEpisodesMonitored()); - } - - private List GetEpisodes() - { - var seriesIdQuery = Request.Query.SeriesId; - var episodeIdsQuery = Request.Query.EpisodeIds; - var episodeFileIdQuery = Request.Query.EpisodeFileId; - var includeImages = Request.GetBooleanQueryParameter("includeImages", false); - - if (seriesIdQuery.HasValue) - { - int seriesId = Convert.ToInt32(seriesIdQuery.Value); - var seasonNumber = Request.Query.SeasonNumber.HasValue ? (int)Request.Query.SeasonNumber : (int?)null; - - if (seasonNumber.HasValue) - { - return MapToResource(_episodeService.GetEpisodesBySeason(seriesId, seasonNumber.Value), false, false, includeImages); - } - - return MapToResource(_episodeService.GetEpisodeBySeries(seriesId), false, false, includeImages); - } - else if (episodeIdsQuery.HasValue) - { - string episodeIdsValue = episodeIdsQuery.Value.ToString(); - - var episodeIds = episodeIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(e => Convert.ToInt32(e)) - .ToList(); - - return MapToResource(_episodeService.GetEpisodes(episodeIds), false, false, includeImages); - } - else if (episodeFileIdQuery.HasValue) - { - int episodeFileId = Convert.ToInt32(episodeFileIdQuery.Value); - - return MapToResource(_episodeService.GetEpisodesByFileId(episodeFileId), false, false, includeImages); - } - - throw new BadRequestException("seriesId or episodeIds must be provided"); - } - - private object SetEpisodeMonitored(int id) - { - var resource = Request.Body.FromJson(); - _episodeService.SetEpisodeMonitored(id, resource.Monitored); - - resource = MapToResource(_episodeService.GetEpisode(id), false, false, false); - - return ResponseWithCode(resource, HttpStatusCode.Accepted); - } - - private object SetEpisodesMonitored() - { - var includeImages = Request.GetBooleanQueryParameter("includeImages", false); - var resource = Request.Body.FromJson(); - - if (resource.EpisodeIds.Count == 1) - { - _episodeService.SetEpisodeMonitored(resource.EpisodeIds.First(), resource.Monitored); - } - else - { - _episodeService.SetMonitored(resource.EpisodeIds, resource.Monitored); - } - - var resources = MapToResource(_episodeService.GetEpisodes(resource.EpisodeIds), false, false, includeImages); - - return ResponseWithCode(resources, HttpStatusCode.Accepted); - } - } -} diff --git a/src/Sonarr.Api.V3/Episodes/RenameEpisodeController.cs b/src/Sonarr.Api.V3/Episodes/RenameEpisodeController.cs new file mode 100644 index 000000000..2195aaa8f --- /dev/null +++ b/src/Sonarr.Api.V3/Episodes/RenameEpisodeController.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.MediaFiles; +using Sonarr.Http; + +namespace Sonarr.Api.V3.Episodes +{ + [V3ApiController("rename")] + public class RenameEpisodeController : Controller + { + private readonly IRenameEpisodeFileService _renameEpisodeFileService; + + public RenameEpisodeController(IRenameEpisodeFileService renameEpisodeFileService) + { + _renameEpisodeFileService = renameEpisodeFileService; + } + + [HttpGet] + public List GetEpisodes(int seriesId, int? seasonNumber) + { + if (seasonNumber.HasValue) + { + return _renameEpisodeFileService.GetRenamePreviews(seriesId, seasonNumber.Value).ToResource(); + } + + return _renameEpisodeFileService.GetRenamePreviews(seriesId).ToResource(); + } + } +} diff --git a/src/Sonarr.Api.V3/Episodes/RenameEpisodeModule.cs b/src/Sonarr.Api.V3/Episodes/RenameEpisodeModule.cs deleted file mode 100644 index d0ed7da23..000000000 --- a/src/Sonarr.Api.V3/Episodes/RenameEpisodeModule.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Core.MediaFiles; -using Sonarr.Http; -using Sonarr.Http.REST; - -namespace Sonarr.Api.V3.Episodes -{ - public class RenameEpisodeModule : SonarrRestModule - { - private readonly IRenameEpisodeFileService _renameEpisodeFileService; - - public RenameEpisodeModule(IRenameEpisodeFileService renameEpisodeFileService) - : base("rename") - { - _renameEpisodeFileService = renameEpisodeFileService; - - GetResourceAll = GetEpisodes; - } - - private List GetEpisodes() - { - int seriesId; - - if (Request.Query.SeriesId.HasValue) - { - seriesId = (int)Request.Query.SeriesId; - } - else - { - throw new BadRequestException("seriesId is missing"); - } - - if (Request.Query.SeasonNumber.HasValue) - { - var seasonNumber = (int)Request.Query.SeasonNumber; - return _renameEpisodeFileService.GetRenamePreviews(seriesId, seasonNumber).ToResource(); - } - - return _renameEpisodeFileService.GetRenamePreviews(seriesId).ToResource(); - } - } -} diff --git a/src/Sonarr.Api.V3/FileSystem/FileSystemModule.cs b/src/Sonarr.Api.V3/FileSystem/FileSystemController.cs similarity index 55% rename from src/Sonarr.Api.V3/FileSystem/FileSystemModule.cs rename to src/Sonarr.Api.V3/FileSystem/FileSystemController.cs index 29303568b..9fdf5847f 100644 --- a/src/Sonarr.Api.V3/FileSystem/FileSystemModule.cs +++ b/src/Sonarr.Api.V3/FileSystem/FileSystemController.cs @@ -1,47 +1,39 @@ using System; using System.IO; using System.Linq; -using Nancy; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.MediaFiles; -using Sonarr.Http.Extensions; +using Sonarr.Http; namespace Sonarr.Api.V3.FileSystem { - public class FileSystemModule : SonarrV3Module + [V3ApiController] + public class FileSystemController : Controller { private readonly IFileSystemLookupService _fileSystemLookupService; private readonly IDiskProvider _diskProvider; private readonly IDiskScanService _diskScanService; - public FileSystemModule(IFileSystemLookupService fileSystemLookupService, + public FileSystemController(IFileSystemLookupService fileSystemLookupService, IDiskProvider diskProvider, IDiskScanService diskScanService) - : base("/filesystem") { _fileSystemLookupService = fileSystemLookupService; _diskProvider = diskProvider; _diskScanService = diskScanService; - Get("/", x => GetContents()); - Get("/type", x => GetEntityType()); - Get("/mediafiles", x => GetMediaFiles()); } - private object GetContents() + [HttpGet] + public IActionResult GetContents(string path, bool includeFiles = false, bool allowFoldersWithoutTrailingSlashes = false) { - var pathQuery = Request.Query.path; - var includeFiles = Request.GetBooleanQueryParameter("includeFiles"); - var allowFoldersWithoutTrailingSlashes = Request.GetBooleanQueryParameter("allowFoldersWithoutTrailingSlashes"); - - return _fileSystemLookupService.LookupContents((string)pathQuery.Value, includeFiles, allowFoldersWithoutTrailingSlashes); + return Ok(_fileSystemLookupService.LookupContents(path, includeFiles, allowFoldersWithoutTrailingSlashes)); } - private object GetEntityType() + [HttpGet("type")] + public object GetEntityType(string path) { - var pathQuery = Request.Query.path; - var path = (string)pathQuery.Value; - if (_diskProvider.FileExists(path)) { return new { type = "file" }; @@ -51,14 +43,12 @@ namespace Sonarr.Api.V3.FileSystem return new { type = "folder" }; } - private object GetMediaFiles() + [HttpGet("mediafiles")] + public object GetMediaFiles(string path) { - var pathQuery = Request.Query.path; - var path = (string)pathQuery.Value; - if (!_diskProvider.FolderExists(path)) { - return new string[0]; + return Array.Empty(); } return _diskScanService.GetVideoFiles(path).Select(f => new diff --git a/src/Sonarr.Api.V3/Health/HealthModule.cs b/src/Sonarr.Api.V3/Health/HealthController.cs similarity index 54% rename from src/Sonarr.Api.V3/Health/HealthModule.cs rename to src/Sonarr.Api.V3/Health/HealthController.cs index 841e31d57..d3400d05f 100644 --- a/src/Sonarr.Api.V3/Health/HealthModule.cs +++ b/src/Sonarr.Api.V3/Health/HealthController.cs @@ -1,29 +1,39 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.HealthCheck; using NzbDrone.Core.Messaging.Events; using NzbDrone.SignalR; using Sonarr.Http; +using Sonarr.Http.REST; namespace Sonarr.Api.V3.Health { - public class HealthModule : SonarrRestModuleWithSignalR, + [V3ApiController] + public class HealthController : RestControllerWithSignalR, IHandle { private readonly IHealthCheckService _healthCheckService; - public HealthModule(IBroadcastSignalRMessage signalRBroadcaster, IHealthCheckService healthCheckService) + public HealthController(IBroadcastSignalRMessage signalRBroadcaster, IHealthCheckService healthCheckService) : base(signalRBroadcaster) { _healthCheckService = healthCheckService; - GetResourceAll = GetHealth; } - private List GetHealth() + protected override HealthResource GetResourceById(int id) + { + throw new NotImplementedException(); + } + + [HttpGet] + public List GetHealth() { return _healthCheckService.Results().ToResource(); } + [NonAction] public void Handle(HealthCheckCompleteEvent message) { BroadcastResourceChange(ModelAction.Sync); diff --git a/src/Sonarr.Api.V3/History/HistoryModule.cs b/src/Sonarr.Api.V3/History/HistoryController.cs similarity index 53% rename from src/Sonarr.Api.V3/History/HistoryModule.cs rename to src/Sonarr.Api.V3/History/HistoryController.cs index 548ced0c5..31e72864c 100644 --- a/src/Sonarr.Api.V3/History/HistoryModule.cs +++ b/src/Sonarr.Api.V3/History/HistoryController.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Datastore; using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Download; @@ -9,29 +10,23 @@ using Sonarr.Api.V3.Episodes; using Sonarr.Api.V3.Series; using Sonarr.Http; using Sonarr.Http.Extensions; -using Sonarr.Http.REST; namespace Sonarr.Api.V3.History { - public class HistoryModule : SonarrRestModule + [V3ApiController] + public class HistoryController : Controller { private readonly IHistoryService _historyService; private readonly IUpgradableSpecification _upgradableSpecification; private readonly IFailedDownloadService _failedDownloadService; - public HistoryModule(IHistoryService historyService, + public HistoryController(IHistoryService historyService, IUpgradableSpecification upgradableSpecification, IFailedDownloadService failedDownloadService) { _historyService = historyService; _upgradableSpecification = upgradableSpecification; _failedDownloadService = failedDownloadService; - GetResourcePaged = GetHistory; - - Get("/since", x => GetHistorySince()); - Get("/series", x => GetSeriesHistory()); - Post("/failed", x => MarkAsFailed()); - Post(@"/failed/(?[\d]{1,10})", x => MarkAsFailed((int)x.Id)); } protected HistoryResource MapToResource(EpisodeHistory model, bool includeSeries, bool includeEpisode) @@ -57,11 +52,11 @@ namespace Sonarr.Api.V3.History return resource; } - private PagingResource GetHistory(PagingResource pagingResource) + [HttpGet] + public PagingResource GetHistory(bool includeSeries, bool includeEpisode) { + var pagingResource = Request.ReadPagingResourceFromRequest(); var pagingSpec = pagingResource.MapToPagingSpec("date", SortDirection.Descending); - var includeSeries = Request.GetBooleanQueryParameter("includeSeries"); - var includeEpisode = Request.GetBooleanQueryParameter("includeEpisode"); var eventTypeFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "eventType"); var episodeIdFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "episodeId"); @@ -85,76 +80,31 @@ namespace Sonarr.Api.V3.History pagingSpec.FilterExpressions.Add(h => h.DownloadId == downloadId); } - return ApplyToPage(_historyService.Paged, pagingSpec, h => MapToResource(h, includeSeries, includeEpisode)); + return pagingSpec.ApplyToPage(_historyService.Paged, h => MapToResource(h, includeSeries, includeEpisode)); } - private List GetHistorySince() + [HttpGet("since")] + public List GetHistorySince(DateTime date, EpisodeHistoryEventType? eventType = null, bool includeSeries = false, bool includeEpisode = false) { - var queryDate = Request.Query.Date; - var queryEventType = Request.Query.EventType; - - if (!queryDate.HasValue) - { - throw new BadRequestException("date is missing"); - } - - DateTime date = DateTime.Parse(queryDate.Value); - EpisodeHistoryEventType? eventType = null; - var includeSeries = Request.GetBooleanQueryParameter("includeSeries"); - var includeEpisode = Request.GetBooleanQueryParameter("includeEpisode"); - - if (queryEventType.HasValue) - { - eventType = (EpisodeHistoryEventType)Convert.ToInt32(queryEventType.Value); - } - return _historyService.Since(date, eventType).Select(h => MapToResource(h, includeSeries, includeEpisode)).ToList(); } - private List GetSeriesHistory() + [HttpGet("series")] + public List GetSeriesHistory(int seriesId, int? seasonNumber, EpisodeHistoryEventType? eventType = null, bool includeSeries = false, bool includeEpisode = false) { - var querySeriesId = Request.Query.SeriesId; - var querySeasonNumber = Request.Query.SeasonNumber; - var queryEventType = Request.Query.EventType; - - if (!querySeriesId.HasValue) + if (seasonNumber.HasValue) { - throw new BadRequestException("seriesId is missing"); - } - - int seriesId = Convert.ToInt32(querySeriesId.Value); - EpisodeHistoryEventType? eventType = null; - var includeSeries = Request.GetBooleanQueryParameter("includeSeries"); - var includeEpisode = Request.GetBooleanQueryParameter("includeEpisode"); - - if (queryEventType.HasValue) - { - eventType = (EpisodeHistoryEventType)Convert.ToInt32(queryEventType.Value); - } - - if (querySeasonNumber.HasValue) - { - int seasonNumber = Convert.ToInt32(querySeasonNumber.Value); - - return _historyService.GetBySeason(seriesId, seasonNumber, eventType).Select(h => MapToResource(h, includeSeries, includeEpisode)).ToList(); + return _historyService.GetBySeason(seriesId, seasonNumber.Value, eventType).Select(h => MapToResource(h, includeSeries, includeEpisode)).ToList(); } return _historyService.GetBySeries(seriesId, eventType).Select(h => MapToResource(h, includeSeries, includeEpisode)).ToList(); } - // v4 TODO: Getting the ID from the form is atypical, consider removing. - private object MarkAsFailed() - { - var id = (int)Request.Form.Id; - - return MarkAsFailed(id); - } - - private object MarkAsFailed(int id) + [HttpPost("failed/{id}")] + public object MarkAsFailed([FromRoute] int id) { _failedDownloadService.MarkAsFailed(id); - - return new object(); + return new { }; } } } diff --git a/src/Sonarr.Api.V3/ImportLists/ImportListModule.cs b/src/Sonarr.Api.V3/ImportLists/ImportListController.cs similarity index 70% rename from src/Sonarr.Api.V3/ImportLists/ImportListModule.cs rename to src/Sonarr.Api.V3/ImportLists/ImportListController.cs index e3f06128b..21c4fccd1 100644 --- a/src/Sonarr.Api.V3/ImportLists/ImportListModule.cs +++ b/src/Sonarr.Api.V3/ImportLists/ImportListController.cs @@ -1,16 +1,16 @@ using NzbDrone.Core.ImportLists; using NzbDrone.Core.Validation; using NzbDrone.Core.Validation.Paths; +using Sonarr.Http; namespace Sonarr.Api.V3.ImportLists { - public class ImportListModule : ProviderModuleBase + [V3ApiController] + public class ImportListController : ProviderControllerBase { public static readonly ImportListResourceMapper ResourceMapper = new ImportListResourceMapper(); - public ImportListModule(ImportListFactory importListFactory, - ProfileExistsValidator profileExistsValidator, - LanguageProfileExistsValidator languageProfileExistsValidator) + public ImportListController(IImportListFactory importListFactory, ProfileExistsValidator profileExistsValidator, LanguageProfileExistsValidator languageProfileExistsValidator) : base(importListFactory, "importlist", ResourceMapper) { Http.Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.QualityProfileId)); diff --git a/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs b/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs new file mode 100644 index 000000000..e4e483b6e --- /dev/null +++ b/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.ImportLists.Exclusions; +using NzbDrone.Core.Validation; +using Sonarr.Http; +using Sonarr.Http.REST; +using Sonarr.Http.REST.Attributes; + +namespace Sonarr.Api.V3.ImportLists +{ + [V3ApiController] + public class ImportListExclusionController : RestController + { + private readonly IImportListExclusionService _importListExclusionService; + + public ImportListExclusionController(IImportListExclusionService importListExclusionService, + ImportListExclusionExistsValidator importListExclusionExistsValidator) + { + _importListExclusionService = importListExclusionService; + + SharedValidator.RuleFor(c => c.TvdbId).NotEmpty().SetValidator(importListExclusionExistsValidator); + SharedValidator.RuleFor(c => c.Title).NotEmpty(); + } + + protected override ImportListExclusionResource GetResourceById(int id) + { + return _importListExclusionService.Get(id).ToResource(); + } + + [HttpGet] + public List GetImportListExclusions() + { + return _importListExclusionService.All().ToResource(); + } + + [RestPostById] + public ActionResult AddImportListExclusion(ImportListExclusionResource resource) + { + var importListExclusion = _importListExclusionService.Add(resource.ToModel()); + + return Created(importListExclusion.Id); + } + + [RestPutById] + public ActionResult UpdateImportListExclusion(ImportListExclusionResource resource) + { + _importListExclusionService.Update(resource.ToModel()); + return Accepted(resource.Id); + } + + [RestDeleteById] + public void DeleteImportListExclusionResource(int id) + { + _importListExclusionService.Delete(id); + } + } +} diff --git a/src/Sonarr.Api.V3/ImportLists/ImportListExclusionModule.cs b/src/Sonarr.Api.V3/ImportLists/ImportListExclusionModule.cs deleted file mode 100644 index e88e9d83c..000000000 --- a/src/Sonarr.Api.V3/ImportLists/ImportListExclusionModule.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Collections.Generic; -using FluentValidation; -using NzbDrone.Core.ImportLists.Exclusions; -using NzbDrone.Core.Validation; -using Sonarr.Http; - -namespace Sonarr.Api.V3.ImportLists -{ - public class ImportListExclusionModule : SonarrRestModule - { - private readonly IImportListExclusionService _importListExclusionService; - - public ImportListExclusionModule(IImportListExclusionService importListExclusionService, - ImportListExclusionExistsValidator importListExclusionExistsValidator) - { - _importListExclusionService = importListExclusionService; - - GetResourceById = GetImportListExclusion; - GetResourceAll = GetImportListExclusions; - CreateResource = AddImportListExclusion; - UpdateResource = UpdateImportListExclusion; - DeleteResource = DeleteImportListExclusionResource; - - SharedValidator.RuleFor(c => c.TvdbId).NotEmpty().SetValidator(importListExclusionExistsValidator); - SharedValidator.RuleFor(c => c.Title).NotEmpty(); - } - - private ImportListExclusionResource GetImportListExclusion(int id) - { - return _importListExclusionService.Get(id).ToResource(); - } - - private List GetImportListExclusions() - { - return _importListExclusionService.All().ToResource(); - } - - private int AddImportListExclusion(ImportListExclusionResource resource) - { - var customFilter = _importListExclusionService.Add(resource.ToModel()); - - return customFilter.Id; - } - - private void UpdateImportListExclusion(ImportListExclusionResource resource) - { - _importListExclusionService.Update(resource.ToModel()); - } - - private void DeleteImportListExclusionResource(int id) - { - _importListExclusionService.Delete(id); - } - } -} diff --git a/src/Sonarr.Api.V3/ImportLists/ImportListResource.cs b/src/Sonarr.Api.V3/ImportLists/ImportListResource.cs index bfb65275e..7f033e0c3 100644 --- a/src/Sonarr.Api.V3/ImportLists/ImportListResource.cs +++ b/src/Sonarr.Api.V3/ImportLists/ImportListResource.cs @@ -3,7 +3,7 @@ using NzbDrone.Core.Tv; namespace Sonarr.Api.V3.ImportLists { - public class ImportListResource : ProviderResource + public class ImportListResource : ProviderResource { public bool EnableAutomaticAdd { get; set; } public MonitorTypes ShouldMonitor { get; set; } diff --git a/src/Sonarr.Api.V3/Indexers/IndexerController.cs b/src/Sonarr.Api.V3/Indexers/IndexerController.cs new file mode 100644 index 000000000..e7cf33a49 --- /dev/null +++ b/src/Sonarr.Api.V3/Indexers/IndexerController.cs @@ -0,0 +1,16 @@ +using NzbDrone.Core.Indexers; +using Sonarr.Http; + +namespace Sonarr.Api.V3.Indexers +{ + [V3ApiController] + public class IndexerController : ProviderControllerBase + { + public static readonly IndexerResourceMapper ResourceMapper = new IndexerResourceMapper(); + + public IndexerController(IndexerFactory indexerFactory) + : base(indexerFactory, "indexer", ResourceMapper) + { + } + } +} diff --git a/src/Sonarr.Api.V3/Indexers/IndexerModule.cs b/src/Sonarr.Api.V3/Indexers/IndexerModule.cs deleted file mode 100644 index 54ed1a778..000000000 --- a/src/Sonarr.Api.V3/Indexers/IndexerModule.cs +++ /dev/null @@ -1,14 +0,0 @@ -using NzbDrone.Core.Indexers; - -namespace Sonarr.Api.V3.Indexers -{ - public class IndexerModule : ProviderModuleBase - { - public static readonly IndexerResourceMapper ResourceMapper = new IndexerResourceMapper(); - - public IndexerModule(IndexerFactory indexerFactory) - : base(indexerFactory, "indexer", ResourceMapper) - { - } - } -} diff --git a/src/Sonarr.Api.V3/Indexers/IndexerResource.cs b/src/Sonarr.Api.V3/Indexers/IndexerResource.cs index decb25806..9816e08db 100644 --- a/src/Sonarr.Api.V3/Indexers/IndexerResource.cs +++ b/src/Sonarr.Api.V3/Indexers/IndexerResource.cs @@ -2,7 +2,7 @@ using NzbDrone.Core.Indexers; namespace Sonarr.Api.V3.Indexers { - public class IndexerResource : ProviderResource + public class IndexerResource : ProviderResource { public bool EnableRss { get; set; } public bool EnableAutomaticSearch { get; set; } diff --git a/src/Sonarr.Api.V3/Indexers/ReleaseModule.cs b/src/Sonarr.Api.V3/Indexers/ReleaseController.cs similarity index 92% rename from src/Sonarr.Api.V3/Indexers/ReleaseModule.cs rename to src/Sonarr.Api.V3/Indexers/ReleaseController.cs index abce3b4a7..43783ad21 100644 --- a/src/Sonarr.Api.V3/Indexers/ReleaseModule.cs +++ b/src/Sonarr.Api.V3/Indexers/ReleaseController.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using FluentValidation; -using Nancy; +using Microsoft.AspNetCore.Mvc; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; @@ -16,11 +16,13 @@ using NzbDrone.Core.Profiles.Languages; using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Tv; using NzbDrone.Core.Validation; +using Sonarr.Http; using HttpStatusCode = System.Net.HttpStatusCode; namespace Sonarr.Api.V3.Indexers { - public class ReleaseModule : ReleaseModuleBase + [V3ApiController] + public class ReleaseController : ReleaseControllerBase { private readonly IFetchAndParseRss _rssFetcherAndParser; private readonly ISearchForReleases _releaseSearchService; @@ -34,7 +36,7 @@ namespace Sonarr.Api.V3.Indexers private readonly ICached _remoteEpisodeCache; - public ReleaseModule(IFetchAndParseRss rssFetcherAndParser, + public ReleaseController(IFetchAndParseRss rssFetcherAndParser, ISearchForReleases releaseSearchService, IMakeDownloadDecision downloadDecisionMaker, IPrioritizeDownloadDecision prioritizeDownloadDecision, @@ -61,13 +63,11 @@ namespace Sonarr.Api.V3.Indexers PostValidator.RuleFor(s => s.IndexerId).ValidId(); PostValidator.RuleFor(s => s.Guid).NotEmpty(); - GetResourceAll = GetReleases; - Post("/", x => DownloadRelease(ReadResourceFromRequest())); - _remoteEpisodeCache = cacheManager.GetCache(GetType(), "remoteEpisodes"); } - private object DownloadRelease(ReleaseResource release) + [HttpPost] + public object DownloadRelease(ReleaseResource release) { var remoteEpisode = _remoteEpisodeCache.Find(GetCacheKey(release)); @@ -137,16 +137,17 @@ namespace Sonarr.Api.V3.Indexers return release; } - private List GetReleases() + [HttpGet] + public List GetReleases(int? seriesId, int? episodeId, int? seasonNumber) { - if (Request.Query.episodeId.HasValue) + if (episodeId.HasValue) { - return GetEpisodeReleases(Request.Query.episodeId); + return GetEpisodeReleases(episodeId.Value); } - if (Request.Query.seriesId.HasValue && Request.Query.seasonNumber.HasValue) + if (seriesId.HasValue && seasonNumber.HasValue) { - return GetSeasonReleases(Request.Query.seriesId, Request.Query.seasonNumber); + return GetSeasonReleases(seriesId.Value, seasonNumber.Value); } return GetRss(); diff --git a/src/Sonarr.Api.V3/Indexers/ReleaseModuleBase.cs b/src/Sonarr.Api.V3/Indexers/ReleaseControllerBase.cs similarity index 78% rename from src/Sonarr.Api.V3/Indexers/ReleaseModuleBase.cs rename to src/Sonarr.Api.V3/Indexers/ReleaseControllerBase.cs index 7c1eb1907..2c6e601a2 100644 --- a/src/Sonarr.Api.V3/Indexers/ReleaseModuleBase.cs +++ b/src/Sonarr.Api.V3/Indexers/ReleaseControllerBase.cs @@ -1,23 +1,29 @@ +using System; using System.Collections.Generic; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Profiles.Languages; using NzbDrone.Core.Profiles.Qualities; -using Sonarr.Http; +using Sonarr.Http.REST; namespace Sonarr.Api.V3.Indexers { - public abstract class ReleaseModuleBase : SonarrRestModule + public abstract class ReleaseControllerBase : RestController { private readonly LanguageProfile _languageProfile; private readonly QualityProfile _qualityProfile; - public ReleaseModuleBase(ILanguageProfileService languageProfileService, - IQualityProfileService qualityProfileService) + public ReleaseControllerBase(ILanguageProfileService languageProfileService, + IQualityProfileService qualityProfileService) { _languageProfile = languageProfileService.GetDefaultProfile(string.Empty); _qualityProfile = qualityProfileService.GetDefaultProfile(string.Empty); } + protected override ReleaseResource GetResourceById(int id) + { + throw new NotImplementedException(); + } + protected virtual List MapDecisions(IEnumerable decisions) { var result = new List(); diff --git a/src/Sonarr.Api.V3/Indexers/ReleasePushModule.cs b/src/Sonarr.Api.V3/Indexers/ReleasePushController.cs similarity index 89% rename from src/Sonarr.Api.V3/Indexers/ReleasePushModule.cs rename to src/Sonarr.Api.V3/Indexers/ReleasePushController.cs index f832c7412..cdd10959a 100644 --- a/src/Sonarr.Api.V3/Indexers/ReleasePushModule.cs +++ b/src/Sonarr.Api.V3/Indexers/ReleasePushController.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using FluentValidation; using FluentValidation.Results; +using Microsoft.AspNetCore.Mvc; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore; @@ -11,17 +12,19 @@ using NzbDrone.Core.Indexers; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Profiles.Languages; using NzbDrone.Core.Profiles.Qualities; +using Sonarr.Http; namespace Sonarr.Api.V3.Indexers { - public class ReleasePushModule : ReleaseModuleBase + [V3ApiController("release/push")] + public class ReleasePushController : ReleaseControllerBase { private readonly IMakeDownloadDecision _downloadDecisionMaker; private readonly IProcessDownloadDecisions _downloadDecisionProcessor; private readonly IIndexerFactory _indexerFactory; private readonly Logger _logger; - public ReleasePushModule(IMakeDownloadDecision downloadDecisionMaker, + public ReleasePushController(IMakeDownloadDecision downloadDecisionMaker, IProcessDownloadDecisions downloadDecisionProcessor, IIndexerFactory indexerFactory, ILanguageProfileService languageProfileService, @@ -38,14 +41,15 @@ namespace Sonarr.Api.V3.Indexers PostValidator.RuleFor(s => s.DownloadUrl).NotEmpty(); PostValidator.RuleFor(s => s.Protocol).NotEmpty(); PostValidator.RuleFor(s => s.PublishDate).NotEmpty(); - - Post("/push", x => ProcessRelease(ReadResourceFromRequest())); } - private object ProcessRelease(ReleaseResource release) + [HttpPost] + public ActionResult> Create(ReleaseResource release) { _logger.Info("Release pushed: {0} - {1}", release.Title, release.DownloadUrl); + ValidateResource(release); + var info = release.ToModel(); info.Guid = "PUSH-" + info.DownloadUrl; @@ -90,7 +94,7 @@ namespace Sonarr.Api.V3.Indexers } catch (ModelNotFoundException) { - _logger.Debug("Push Release {0} not associated with known indexer {0}.", release.Title, release.IndexerId); + _logger.Debug("Push Release {0} not associated with known indexer {1}.", release.Title, release.IndexerId); release.IndexerId = 0; } } diff --git a/src/Sonarr.Api.V3/Logs/LogModule.cs b/src/Sonarr.Api.V3/Logs/LogController.cs similarity index 82% rename from src/Sonarr.Api.V3/Logs/LogModule.cs rename to src/Sonarr.Api.V3/Logs/LogController.cs index c0800a8ca..6398bf070 100644 --- a/src/Sonarr.Api.V3/Logs/LogModule.cs +++ b/src/Sonarr.Api.V3/Logs/LogController.cs @@ -1,21 +1,25 @@ using System.Linq; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Instrumentation; using Sonarr.Http; +using Sonarr.Http.Extensions; namespace Sonarr.Api.V3.Logs { - public class LogModule : SonarrRestModule + [V3ApiController] + public class LogController : Controller { private readonly ILogService _logService; - public LogModule(ILogService logService) + public LogController(ILogService logService) { _logService = logService; - GetResourcePaged = GetLogs; } - private PagingResource GetLogs(PagingResource pagingResource) + [HttpGet] + public PagingResource GetLogs() { + var pagingResource = Request.ReadPagingResourceFromRequest(); var pageSpec = pagingResource.MapToPagingSpec(); if (pageSpec.SortKey == "time") @@ -50,7 +54,7 @@ namespace Sonarr.Api.V3.Logs } } - var response = ApplyToPage(_logService.Paged, pageSpec, LogResourceMapper.ToResource); + var response = pageSpec.ApplyToPage(_logService.Paged, LogResourceMapper.ToResource); if (pageSpec.SortKey == "id") { diff --git a/src/Sonarr.Api.V3/Logs/LogFileModule.cs b/src/Sonarr.Api.V3/Logs/LogFileController.cs similarity index 83% rename from src/Sonarr.Api.V3/Logs/LogFileModule.cs rename to src/Sonarr.Api.V3/Logs/LogFileController.cs index 83441c792..eb3b78ece 100644 --- a/src/Sonarr.Api.V3/Logs/LogFileModule.cs +++ b/src/Sonarr.Api.V3/Logs/LogFileController.cs @@ -1,18 +1,20 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; +using Sonarr.Http; namespace Sonarr.Api.V3.Logs { - public class LogFileModule : LogFileModuleBase + [V3ApiController("log/file")] + public class LogFileController : LogFileControllerBase { private readonly IAppFolderInfo _appFolderInfo; private readonly IDiskProvider _diskProvider; - public LogFileModule(IAppFolderInfo appFolderInfo, + public LogFileController(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, IConfigFileProvider configFileProvider) : base(diskProvider, configFileProvider, "") diff --git a/src/Sonarr.Api.V3/Logs/LogFileModuleBase.cs b/src/Sonarr.Api.V3/Logs/LogFileControllerBase.cs similarity index 65% rename from src/Sonarr.Api.V3/Logs/LogFileModuleBase.cs rename to src/Sonarr.Api.V3/Logs/LogFileControllerBase.cs index 95ae0dd23..915c11dfe 100644 --- a/src/Sonarr.Api.V3/Logs/LogFileModuleBase.cs +++ b/src/Sonarr.Api.V3/Logs/LogFileControllerBase.cs @@ -1,34 +1,32 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; -using Nancy; -using Nancy.Responses; +using Microsoft.AspNetCore.Mvc; +using NLog; using NzbDrone.Common.Disk; using NzbDrone.Core.Configuration; -using Sonarr.Http; namespace Sonarr.Api.V3.Logs { - public abstract class LogFileModuleBase : SonarrRestModule + public abstract class LogFileControllerBase : Controller { protected const string LOGFILE_ROUTE = @"/(?[-.a-zA-Z0-9]+?\.txt)"; + protected string _resource; private readonly IDiskProvider _diskProvider; private readonly IConfigFileProvider _configFileProvider; - public LogFileModuleBase(IDiskProvider diskProvider, + public LogFileControllerBase(IDiskProvider diskProvider, IConfigFileProvider configFileProvider, - string route) - : base("log/file" + route) + string resource) { _diskProvider = diskProvider; _configFileProvider = configFileProvider; - GetResourceAll = GetLogFilesResponse; - - Get(LOGFILE_ROUTE, options => GetLogFileResponse(options.filename)); + _resource = resource; } - private List GetLogFilesResponse() + [HttpGet] + public List GetLogFilesResponse() { var result = new List(); @@ -44,7 +42,7 @@ namespace Sonarr.Api.V3.Logs Id = i + 1, Filename = filename, LastWriteTime = _diskProvider.FileGetLastWrite(file), - ContentsUrl = string.Format("{0}/api/v3/{1}/{2}", _configFileProvider.UrlBase, Resource, filename), + ContentsUrl = string.Format("{0}/api/v1/{1}/{2}", _configFileProvider.UrlBase, _resource, filename), DownloadUrl = string.Format("{0}/{1}/{2}", _configFileProvider.UrlBase, DownloadUrlRoot, filename) }); } @@ -52,18 +50,19 @@ namespace Sonarr.Api.V3.Logs return result.OrderByDescending(l => l.LastWriteTime).ToList(); } - private object GetLogFileResponse(string filename) + [HttpGet(@"{filename:regex([[-.a-zA-Z0-9]]+?\.txt)}")] + public IActionResult GetLogFileResponse(string filename) { + LogManager.Flush(); + var filePath = GetLogFilePath(filename); if (!_diskProvider.FileExists(filePath)) { - return new NotFoundResponse(); + return NotFound(); } - var data = _diskProvider.ReadAllText(filePath); - - return new TextResponse(data); + return PhysicalFile(filePath, "text/plain"); } protected abstract IEnumerable GetLogFiles(); diff --git a/src/Sonarr.Api.V3/Logs/UpdateLogFileModule.cs b/src/Sonarr.Api.V3/Logs/UpdateLogFileController.cs similarity index 83% rename from src/Sonarr.Api.V3/Logs/UpdateLogFileModule.cs rename to src/Sonarr.Api.V3/Logs/UpdateLogFileController.cs index a9537f85b..4f6e8a928 100644 --- a/src/Sonarr.Api.V3/Logs/UpdateLogFileModule.cs +++ b/src/Sonarr.Api.V3/Logs/UpdateLogFileController.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; @@ -6,18 +6,20 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; +using Sonarr.Http; namespace Sonarr.Api.V3.Logs { - public class UpdateLogFileModule : LogFileModuleBase + [V3ApiController("log/file/update")] + public class UpdateLogFileController : LogFileControllerBase { private readonly IAppFolderInfo _appFolderInfo; private readonly IDiskProvider _diskProvider; - public UpdateLogFileModule(IAppFolderInfo appFolderInfo, + public UpdateLogFileController(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, IConfigFileProvider configFileProvider) - : base(diskProvider, configFileProvider, "/update") + : base(diskProvider, configFileProvider, "update") { _appFolderInfo = appFolderInfo; _diskProvider = diskProvider; diff --git a/src/Sonarr.Api.V3/ManualImport/ManualImportModule.cs b/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs similarity index 73% rename from src/Sonarr.Api.V3/ManualImport/ManualImportModule.cs rename to src/Sonarr.Api.V3/ManualImport/ManualImportController.cs index cd23cfab8..da89be25e 100644 --- a/src/Sonarr.Api.V3/ManualImport/ManualImportModule.cs +++ b/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs @@ -1,36 +1,28 @@ using System.Collections.Generic; using System.Linq; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.Languages; using NzbDrone.Core.MediaFiles.EpisodeImport.Manual; using NzbDrone.Core.Qualities; using Sonarr.Api.V3.Episodes; using Sonarr.Http; -using Sonarr.Http.Extensions; namespace Sonarr.Api.V3.ManualImport { - public class ManualImportModule : SonarrRestModule + [V3ApiController] + public class ManualImportController : Controller { private readonly IManualImportService _manualImportService; - public ManualImportModule(IManualImportService manualImportService) - : base("/manualimport") + public ManualImportController(IManualImportService manualImportService) { _manualImportService = manualImportService; - - GetResourceAll = GetMediaFiles; - Post("/", x => ReprocessItems()); } - private List GetMediaFiles() + [HttpGet] + public List GetMediaFiles(string folder, string downloadId, int? seriesId, int? seasonNumber, bool filterExistingFiles = true) { - var folder = (string)Request.Query.folder; - var downloadId = (string)Request.Query.downloadId; - var filterExistingFiles = Request.GetBooleanQueryParameter("filterExistingFiles", true); - var seriesId = Request.GetNullableIntegerQueryParameter("seriesId", null); - var seasonNumber = Request.GetNullableIntegerQueryParameter("seasonNumber", null); - if (seriesId.HasValue) { return _manualImportService.GetMediaFiles(seriesId.Value, seasonNumber).ToResource().Select(AddQualityWeight).ToList(); @@ -39,10 +31,9 @@ namespace Sonarr.Api.V3.ManualImport return _manualImportService.GetMediaFiles(folder, downloadId, seriesId, filterExistingFiles).ToResource().Select(AddQualityWeight).ToList(); } - private object ReprocessItems() + [HttpPost] + public object ReprocessItems([FromBody] List items) { - var items = Request.Body.FromJson>(); - foreach (var item in items) { var processedItem = _manualImportService.ReprocessItem(item.Path, item.DownloadId, item.SeriesId, item.SeasonNumber, item.EpisodeIds ?? new List(), item.ReleaseGroup, item.Quality, item.Language); diff --git a/src/Sonarr.Api.V3/MediaCovers/MediaCoverModule.cs b/src/Sonarr.Api.V3/MediaCovers/MediaCoverController.cs similarity index 57% rename from src/Sonarr.Api.V3/MediaCovers/MediaCoverModule.cs rename to src/Sonarr.Api.V3/MediaCovers/MediaCoverController.cs index efe4848ea..aa7655cb6 100644 --- a/src/Sonarr.Api.V3/MediaCovers/MediaCoverModule.cs +++ b/src/Sonarr.Api.V3/MediaCovers/MediaCoverController.cs @@ -1,32 +1,32 @@ using System.IO; using System.Text.RegularExpressions; -using Nancy; -using Nancy.Responses; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.StaticFiles; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; +using Sonarr.Http; namespace Sonarr.Api.V3.MediaCovers { - public class MediaCoverModule : SonarrV3Module + [V3ApiController] + public class MediaCoverController : Controller { - private const string MEDIA_COVER_ROUTE = @"/(?\d+)/(?(.+)\.(jpg|png|gif))"; - private static readonly Regex RegexResizedImage = new Regex(@"-\d+\.jpg$", RegexOptions.Compiled | RegexOptions.IgnoreCase); private readonly IAppFolderInfo _appFolderInfo; private readonly IDiskProvider _diskProvider; + private readonly IContentTypeProvider _mimeTypeProvider; - public MediaCoverModule(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider) - : base("MediaCover") + public MediaCoverController(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider) { _appFolderInfo = appFolderInfo; _diskProvider = diskProvider; - - Get(MEDIA_COVER_ROUTE, options => GetMediaCover(options.seriesId, options.filename)); + _mimeTypeProvider = new FileExtensionContentTypeProvider(); } - private object GetMediaCover(int seriesId, string filename) + [HttpGet(@"{seriesId:int}/{filename:regex((.+)\.(jpg|png|gif))}")] + public IActionResult GetMediaCover(int seriesId, string filename) { var filePath = Path.Combine(_appFolderInfo.GetAppDataPath(), "MediaCover", seriesId.ToString(), filename); @@ -37,13 +37,23 @@ namespace Sonarr.Api.V3.MediaCovers var basefilePath = RegexResizedImage.Replace(filePath, ".jpg"); if (basefilePath == filePath || !_diskProvider.FileExists(basefilePath)) { - return new NotFoundResponse(); + return NotFound(); } filePath = basefilePath; } - return new StreamResponse(() => File.OpenRead(filePath), MimeTypes.GetMimeType(filePath)); + return PhysicalFile(filePath, GetContentType(filePath)); + } + + private string GetContentType(string filePath) + { + if (!_mimeTypeProvider.TryGetContentType(filePath, out var contentType)) + { + contentType = "application/octet-stream"; + } + + return contentType; } } } diff --git a/src/Sonarr.Api.V3/Metadata/MetadataController.cs b/src/Sonarr.Api.V3/Metadata/MetadataController.cs new file mode 100644 index 000000000..b906465b0 --- /dev/null +++ b/src/Sonarr.Api.V3/Metadata/MetadataController.cs @@ -0,0 +1,16 @@ +using NzbDrone.Core.Extras.Metadata; +using Sonarr.Http; + +namespace Sonarr.Api.V3.Metadata +{ + [V3ApiController] + public class MetadataController : ProviderControllerBase + { + public static readonly MetadataResourceMapper ResourceMapper = new MetadataResourceMapper(); + + public MetadataController(IMetadataFactory metadataFactory) + : base(metadataFactory, "metadata", ResourceMapper) + { + } + } +} diff --git a/src/Sonarr.Api.V3/Metadata/MetadataModule.cs b/src/Sonarr.Api.V3/Metadata/MetadataModule.cs deleted file mode 100644 index a2ec54177..000000000 --- a/src/Sonarr.Api.V3/Metadata/MetadataModule.cs +++ /dev/null @@ -1,14 +0,0 @@ -using NzbDrone.Core.Extras.Metadata; - -namespace Sonarr.Api.V3.Metadata -{ - public class MetadataModule : ProviderModuleBase - { - public static readonly MetadataResourceMapper ResourceMapper = new MetadataResourceMapper(); - - public MetadataModule(IMetadataFactory metadataFactory) - : base(metadataFactory, "metadata", ResourceMapper) - { - } - } -} diff --git a/src/Sonarr.Api.V3/Metadata/MetadataResource.cs b/src/Sonarr.Api.V3/Metadata/MetadataResource.cs index eaaec0eb0..8c9177ee3 100644 --- a/src/Sonarr.Api.V3/Metadata/MetadataResource.cs +++ b/src/Sonarr.Api.V3/Metadata/MetadataResource.cs @@ -2,7 +2,7 @@ namespace Sonarr.Api.V3.Metadata { - public class MetadataResource : ProviderResource + public class MetadataResource : ProviderResource { public bool Enable { get; set; } } diff --git a/src/Sonarr.Api.V3/Notifications/NotificationController.cs b/src/Sonarr.Api.V3/Notifications/NotificationController.cs new file mode 100644 index 000000000..53d507ab0 --- /dev/null +++ b/src/Sonarr.Api.V3/Notifications/NotificationController.cs @@ -0,0 +1,16 @@ +using NzbDrone.Core.Notifications; +using Sonarr.Http; + +namespace Sonarr.Api.V3.Notifications +{ + [V3ApiController] + public class NotificationController : ProviderControllerBase + { + public static readonly NotificationResourceMapper ResourceMapper = new NotificationResourceMapper(); + + public NotificationController(NotificationFactory notificationFactory) + : base(notificationFactory, "notification", ResourceMapper) + { + } + } +} diff --git a/src/Sonarr.Api.V3/Notifications/NotificationModule.cs b/src/Sonarr.Api.V3/Notifications/NotificationModule.cs deleted file mode 100644 index e6ed91b0c..000000000 --- a/src/Sonarr.Api.V3/Notifications/NotificationModule.cs +++ /dev/null @@ -1,14 +0,0 @@ -using NzbDrone.Core.Notifications; - -namespace Sonarr.Api.V3.Notifications -{ - public class NotificationModule : ProviderModuleBase - { - public static readonly NotificationResourceMapper ResourceMapper = new NotificationResourceMapper(); - - public NotificationModule(NotificationFactory notificationFactory) - : base(notificationFactory, "notification", ResourceMapper) - { - } - } -} diff --git a/src/Sonarr.Api.V3/Notifications/NotificationResource.cs b/src/Sonarr.Api.V3/Notifications/NotificationResource.cs index fae38ac7a..8845cf126 100644 --- a/src/Sonarr.Api.V3/Notifications/NotificationResource.cs +++ b/src/Sonarr.Api.V3/Notifications/NotificationResource.cs @@ -2,7 +2,7 @@ namespace Sonarr.Api.V3.Notifications { - public class NotificationResource : ProviderResource + public class NotificationResource : ProviderResource { public string Link { get; set; } public bool OnGrab { get; set; } diff --git a/src/Sonarr.Api.V3/Parse/ParseModule.cs b/src/Sonarr.Api.V3/Parse/ParseController.cs similarity index 71% rename from src/Sonarr.Api.V3/Parse/ParseModule.cs rename to src/Sonarr.Api.V3/Parse/ParseController.cs index b036bf759..2683d49e0 100644 --- a/src/Sonarr.Api.V3/Parse/ParseModule.cs +++ b/src/Sonarr.Api.V3/Parse/ParseController.cs @@ -1,31 +1,28 @@ -using NzbDrone.Common.Extensions; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Parser; using Sonarr.Api.V3.Episodes; using Sonarr.Api.V3.Series; using Sonarr.Http; -using Sonarr.Http.REST; namespace Sonarr.Api.V3.Parse { - public class ParseModule : SonarrRestModule + [V3ApiController] + public class ParseController : Controller { private readonly IParsingService _parsingService; - public ParseModule(IParsingService parsingService) + public ParseController(IParsingService parsingService) { _parsingService = parsingService; - - GetResourceSingle = Parse; } - private ParseResource Parse() + [HttpGet] + public ParseResource Parse(string title, string path) { - var title = Request.Query.Title.Value as string; - var path = Request.Query.Path.Value as string; - - if (path.IsNullOrWhiteSpace() && title.IsNullOrWhiteSpace()) + if (title.IsNullOrWhiteSpace()) { - throw new BadRequestException("title or path is missing"); + return null; } var parsedEpisodeInfo = path.IsNotNullOrWhiteSpace() ? Parser.ParsePath(path) : Parser.ParseTitle(title); diff --git a/src/Sonarr.Api.V3/Profiles/Delay/DelayProfileModule.cs b/src/Sonarr.Api.V3/Profiles/Delay/DelayProfileController.cs similarity index 62% rename from src/Sonarr.Api.V3/Profiles/Delay/DelayProfileModule.cs rename to src/Sonarr.Api.V3/Profiles/Delay/DelayProfileController.cs index 007d46220..efa003868 100644 --- a/src/Sonarr.Api.V3/Profiles/Delay/DelayProfileModule.cs +++ b/src/Sonarr.Api.V3/Profiles/Delay/DelayProfileController.cs @@ -1,29 +1,23 @@ -using System; using System.Collections.Generic; using FluentValidation; -using Nancy; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Profiles.Delay; using Sonarr.Http; using Sonarr.Http.REST; +using Sonarr.Http.REST.Attributes; using Sonarr.Http.Validation; namespace Sonarr.Api.V3.Profiles.Delay { - public class DelayProfileModule : SonarrRestModule + [V3ApiController] + public class DelayProfileController : RestController { private readonly IDelayProfileService _delayProfileService; - public DelayProfileModule(IDelayProfileService delayProfileService, DelayProfileTagInUseValidator tagInUseValidator) + public DelayProfileController(IDelayProfileService delayProfileService, DelayProfileTagInUseValidator tagInUseValidator) { _delayProfileService = delayProfileService; - GetResourceAll = GetAll; - GetResourceById = GetById; - UpdateResource = Update; - CreateResource = Create; - DeleteResource = DeleteProfile; - Put(@"/reorder/(?[\d]{1,10})", options => Reorder(options.Id)); - SharedValidator.RuleFor(d => d.Tags).NotEmpty().When(d => d.Id != 1); SharedValidator.RuleFor(d => d.Tags).EmptyCollection().When(d => d.Id == 1); SharedValidator.RuleFor(d => d.Tags).SetValidator(tagInUseValidator); @@ -39,15 +33,17 @@ namespace Sonarr.Api.V3.Profiles.Delay }); } - private int Create(DelayProfileResource resource) + [RestPostById] + public ActionResult Create(DelayProfileResource resource) { var model = resource.ToModel(); model = _delayProfileService.Add(model); - return model.Id; + return Created(model.Id); } - private void DeleteProfile(int id) + [RestDeleteById] + public void DeleteProfile(int id) { if (id == 1) { @@ -57,30 +53,31 @@ namespace Sonarr.Api.V3.Profiles.Delay _delayProfileService.Delete(id); } - private void Update(DelayProfileResource resource) + [RestPutById] + public ActionResult Update(DelayProfileResource resource) { var model = resource.ToModel(); _delayProfileService.Update(model); + return Accepted(model.Id); } - private DelayProfileResource GetById(int id) + protected override DelayProfileResource GetResourceById(int id) { return _delayProfileService.Get(id).ToResource(); } - private List GetAll() + [HttpGet] + public List GetAll() { return _delayProfileService.All().ToResource(); } - private object Reorder(int id) + [HttpPut("reorder/{id}")] + public List Reorder([FromRoute] int id, int? after) { ValidateId(id); - var afterIdQuery = Request.Query.After; - int? afterId = afterIdQuery.HasValue ? Convert.ToInt32(afterIdQuery.Value) : null; - - return _delayProfileService.Reorder(id, afterId).ToResource(); + return _delayProfileService.Reorder(id, after).ToResource(); } } } diff --git a/src/Sonarr.Api.V3/Profiles/Language/LanguageProfileController.cs b/src/Sonarr.Api.V3/Profiles/Language/LanguageProfileController.cs new file mode 100644 index 000000000..ed031e216 --- /dev/null +++ b/src/Sonarr.Api.V3/Profiles/Language/LanguageProfileController.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Profiles.Languages; +using Sonarr.Http; +using Sonarr.Http.REST; +using Sonarr.Http.REST.Attributes; + +namespace Sonarr.Api.V3.Profiles.Language +{ + [V3ApiController] + public class LanguageProfileController : RestController + { + private readonly ILanguageProfileService _profileService; + + public LanguageProfileController(ILanguageProfileService profileService) + { + _profileService = profileService; + SharedValidator.RuleFor(c => c.Name).NotEmpty(); + SharedValidator.RuleFor(c => c.Cutoff).NotNull(); + SharedValidator.RuleFor(c => c.Languages).MustHaveAllowedLanguage(); + } + + [RestPostById] + public ActionResult Create(LanguageProfileResource resource) + { + var model = resource.ToModel(); + model = _profileService.Add(model); + return Created(model.Id); + } + + [RestDeleteById] + public void DeleteProfile(int id) + { + _profileService.Delete(id); + } + + [RestPutById] + public ActionResult Update(LanguageProfileResource resource) + { + var model = resource.ToModel(); + + _profileService.Update(model); + + return Accepted(model.Id); + } + + protected override LanguageProfileResource GetResourceById(int id) + { + return _profileService.Get(id).ToResource(); + } + + [HttpGet] + public List GetAll() + { + return _profileService.All().ToResource(); + } + } +} diff --git a/src/Sonarr.Api.V3/Profiles/Language/LanguageProfileModule.cs b/src/Sonarr.Api.V3/Profiles/Language/LanguageProfileModule.cs deleted file mode 100644 index fd25e342c..000000000 --- a/src/Sonarr.Api.V3/Profiles/Language/LanguageProfileModule.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Collections.Generic; -using FluentValidation; -using NzbDrone.Core.Profiles.Languages; -using Sonarr.Http; - -namespace Sonarr.Api.V3.Profiles.Language -{ - public class LanguageProfileModule : SonarrRestModule - { - private readonly ILanguageProfileService _profileService; - - public LanguageProfileModule(ILanguageProfileService profileService) - { - _profileService = profileService; - SharedValidator.RuleFor(c => c.Name).NotEmpty(); - SharedValidator.RuleFor(c => c.Cutoff).NotNull(); - SharedValidator.RuleFor(c => c.Languages).MustHaveAllowedLanguage(); - - GetResourceAll = GetAll; - GetResourceById = GetById; - UpdateResource = Update; - CreateResource = Create; - DeleteResource = DeleteProfile; - } - - private int Create(LanguageProfileResource resource) - { - var model = resource.ToModel(); - model = _profileService.Add(model); - return model.Id; - } - - private void DeleteProfile(int id) - { - _profileService.Delete(id); - } - - private void Update(LanguageProfileResource resource) - { - var model = resource.ToModel(); - - _profileService.Update(model); - } - - private LanguageProfileResource GetById(int id) - { - return _profileService.Get(id).ToResource(); - } - - private List GetAll() - { - var profiles = _profileService.All().ToResource(); - - return profiles; - } - } -} diff --git a/src/Sonarr.Api.V3/Profiles/Language/LanguageProfileSchemaController.cs b/src/Sonarr.Api.V3/Profiles/Language/LanguageProfileSchemaController.cs new file mode 100644 index 000000000..fabb7696a --- /dev/null +++ b/src/Sonarr.Api.V3/Profiles/Language/LanguageProfileSchemaController.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Profiles.Languages; +using Sonarr.Http; + +namespace Sonarr.Api.V3.Profiles.Language +{ + [V3ApiController("languageprofile/schema")] + public class LanguageProfileSchemaController : Controller + { + private readonly ILanguageProfileService _profileService; + + public LanguageProfileSchemaController(ILanguageProfileService profileService) + { + _profileService = profileService; + } + + [HttpGet] + public LanguageProfileResource GetSchema() + { + var qualityProfile = _profileService.GetDefaultProfile(string.Empty); + + return qualityProfile.ToResource(); + } + } +} diff --git a/src/Sonarr.Api.V3/Profiles/Language/LanguageProfileSchemaModule.cs b/src/Sonarr.Api.V3/Profiles/Language/LanguageProfileSchemaModule.cs deleted file mode 100644 index b92058a29..000000000 --- a/src/Sonarr.Api.V3/Profiles/Language/LanguageProfileSchemaModule.cs +++ /dev/null @@ -1,23 +0,0 @@ -using NzbDrone.Core.Profiles.Languages; -using Sonarr.Http; - -namespace Sonarr.Api.V3.Profiles.Language -{ - public class LanguageProfileSchemaModule : SonarrRestModule - { - private readonly LanguageProfileService _languageProfileService; - - public LanguageProfileSchemaModule(LanguageProfileService languageProfileService) - : base("/languageprofile/schema") - { - _languageProfileService = languageProfileService; - GetResourceSingle = GetAll; - } - - private LanguageProfileResource GetAll() - { - var profile = _languageProfileService.GetDefaultProfile(string.Empty); - return profile.ToResource(); - } - } -} diff --git a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileController.cs b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileController.cs new file mode 100644 index 000000000..cf4950f5b --- /dev/null +++ b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileController.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Profiles.Qualities; +using Sonarr.Http; +using Sonarr.Http.REST; +using Sonarr.Http.REST.Attributes; + +namespace Sonarr.Api.V3.Profiles.Quality +{ + [V3ApiController] + public class QualityProfileController : RestController + { + private readonly IQualityProfileService _profileService; + + public QualityProfileController(IQualityProfileService profileService) + { + _profileService = profileService; + SharedValidator.RuleFor(c => c.Name).NotEmpty(); + SharedValidator.RuleFor(c => c.Cutoff).ValidCutoff(); + SharedValidator.RuleFor(c => c.Items).ValidItems(); + } + + [RestPostById] + public ActionResult Create(QualityProfileResource resource) + { + var model = resource.ToModel(); + model = _profileService.Add(model); + return Created(model.Id); + } + + [RestDeleteById] + public void DeleteProfile(int id) + { + _profileService.Delete(id); + } + + [RestPutById] + public ActionResult Update(QualityProfileResource resource) + { + var model = resource.ToModel(); + + _profileService.Update(model); + + return Accepted(model.Id); + } + + protected override QualityProfileResource GetResourceById(int id) + { + return _profileService.Get(id).ToResource(); + } + + [HttpGet] + public List GetAll() + { + return _profileService.All().ToResource(); + } + } +} diff --git a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileModule.cs b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileModule.cs deleted file mode 100644 index 4617c5d1a..000000000 --- a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileModule.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Collections.Generic; -using FluentValidation; -using NzbDrone.Core.Profiles.Qualities; -using Sonarr.Http; - -namespace Sonarr.Api.V3.Profiles.Quality -{ - public class ProfileModule : SonarrRestModule - { - private readonly IQualityProfileService _qualityProfileService; - - public ProfileModule(IQualityProfileService qualityProfileService) - { - _qualityProfileService = qualityProfileService; - SharedValidator.RuleFor(c => c.Name).NotEmpty(); - SharedValidator.RuleFor(c => c.Cutoff).ValidCutoff(); - SharedValidator.RuleFor(c => c.Items).ValidItems(); - - GetResourceAll = GetAll; - GetResourceById = GetById; - UpdateResource = Update; - CreateResource = Create; - DeleteResource = DeleteProfile; - } - - private int Create(QualityProfileResource resource) - { - var model = resource.ToModel(); - model = _qualityProfileService.Add(model); - return model.Id; - } - - private void DeleteProfile(int id) - { - _qualityProfileService.Delete(id); - } - - private void Update(QualityProfileResource resource) - { - var model = resource.ToModel(); - - _qualityProfileService.Update(model); - } - - private QualityProfileResource GetById(int id) - { - return _qualityProfileService.Get(id).ToResource(); - } - - private List GetAll() - { - return _qualityProfileService.All().ToResource(); - } - } -} diff --git a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileSchemaController.cs b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileSchemaController.cs new file mode 100644 index 000000000..b43cd5b9d --- /dev/null +++ b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileSchemaController.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Profiles.Qualities; +using Sonarr.Http; + +namespace Sonarr.Api.V3.Profiles.Quality +{ + [V3ApiController("qualityprofile/schema")] + public class QualityProfileSchemaController : Controller + { + private readonly IQualityProfileService _profileService; + + public QualityProfileSchemaController(IQualityProfileService profileService) + { + _profileService = profileService; + } + + [HttpGet] + public QualityProfileResource GetSchema() + { + var qualityProfile = _profileService.GetDefaultProfile(string.Empty); + + return qualityProfile.ToResource(); + } + } +} diff --git a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileSchemaModule.cs b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileSchemaModule.cs deleted file mode 100644 index 98379e09f..000000000 --- a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileSchemaModule.cs +++ /dev/null @@ -1,24 +0,0 @@ -using NzbDrone.Core.Profiles.Qualities; -using Sonarr.Http; - -namespace Sonarr.Api.V3.Profiles.Quality -{ - public class QualityProfileSchemaModule : SonarrRestModule - { - private readonly IQualityProfileService _qualityProfileService; - - public QualityProfileSchemaModule(IQualityProfileService qualityProfileService) - : base("/qualityprofile/schema") - { - _qualityProfileService = qualityProfileService; - GetResourceSingle = GetSchema; - } - - private QualityProfileResource GetSchema() - { - var qualityProfile = _qualityProfileService.GetDefaultProfile(string.Empty); - - return qualityProfile.ToResource(); - } - } -} diff --git a/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileModule.cs b/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileController.cs similarity index 51% rename from src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileModule.cs rename to src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileController.cs index d4af789b9..fd5ca9b57 100644 --- a/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileModule.cs +++ b/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileController.cs @@ -1,29 +1,27 @@ using System.Collections.Generic; using System.Linq; using FluentValidation; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.Indexers; using NzbDrone.Core.Profiles.Releases; using Sonarr.Http; +using Sonarr.Http.REST; +using Sonarr.Http.REST.Attributes; namespace Sonarr.Api.V3.Profiles.Release { - public class ReleaseProfileModule : SonarrRestModule + [V3ApiController] + public class ReleaseProfileController : RestController { - private readonly IReleaseProfileService _releaseProfileService; + private readonly IReleaseProfileService _profileService; private readonly IIndexerFactory _indexerFactory; - public ReleaseProfileModule(IReleaseProfileService releaseProfileService, IIndexerFactory indexerFactory) + public ReleaseProfileController(IReleaseProfileService profileService, IIndexerFactory indexerFactory) { - _releaseProfileService = releaseProfileService; + _profileService = profileService; _indexerFactory = indexerFactory; - GetResourceById = GetReleaseProfile; - GetResourceAll = GetAll; - CreateResource = Create; - UpdateResource = Update; - DeleteResource = DeleteReleaseProfile; - SharedValidator.RuleFor(d => d).Custom((restriction, context) => { if (restriction.MapIgnored().Empty() && restriction.MapRequired().Empty() && restriction.Preferred.Empty()) @@ -43,29 +41,39 @@ namespace Sonarr.Api.V3.Profiles.Release }); } - private ReleaseProfileResource GetReleaseProfile(int id) + [RestPostById] + public ActionResult Create(ReleaseProfileResource resource) { - return _releaseProfileService.Get(id).ToResource(); + var model = resource.ToModel(); + model = _profileService.Add(model); + return Created(model.Id); } - private List GetAll() + [RestDeleteById] + public void DeleteProfile(int id) { - return _releaseProfileService.All().ToResource(); + _profileService.Delete(id); } - private int Create(ReleaseProfileResource resource) + [RestPutById] + public ActionResult Update(ReleaseProfileResource resource) { - return _releaseProfileService.Add(resource.ToModel()).Id; + var model = resource.ToModel(); + + _profileService.Update(model); + + return Accepted(model.Id); } - private void Update(ReleaseProfileResource resource) + protected override ReleaseProfileResource GetResourceById(int id) { - _releaseProfileService.Update(resource.ToModel()); + return _profileService.Get(id).ToResource(); } - private void DeleteReleaseProfile(int id) + [HttpGet] + public List GetAll() { - _releaseProfileService.Delete(id); + return _profileService.All().ToResource(); } } } diff --git a/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileResource.cs b/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileResource.cs index 795378ef9..b6bd95a0d 100644 --- a/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileResource.cs +++ b/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileResource.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using Newtonsoft.Json.Linq; +using System.Text.Json; using NzbDrone.Core.Profiles.Releases; using Sonarr.Http.REST; @@ -91,9 +91,17 @@ namespace Sonarr.Api.V3.Profiles.Release return list; } - if (resource is JArray jarray) + if (resource is JsonElement array) { - return jarray.ToObject>(); + if (array.ValueKind == JsonValueKind.String) + { + return array.GetString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); + } + + if (array.ValueKind == JsonValueKind.Array) + { + return JsonSerializer.Deserialize>(array); + } } if (resource is string str) diff --git a/src/Sonarr.Api.V3/ProviderModuleBase.cs b/src/Sonarr.Api.V3/ProviderControllerBase.cs similarity index 70% rename from src/Sonarr.Api.V3/ProviderModuleBase.cs rename to src/Sonarr.Api.V3/ProviderControllerBase.cs index 3b1eec76c..0c63d3016 100644 --- a/src/Sonarr.Api.V3/ProviderModuleBase.cs +++ b/src/Sonarr.Api.V3/ProviderControllerBase.cs @@ -2,40 +2,29 @@ using System.Collections.Generic; using System.Linq; using FluentValidation; using FluentValidation.Results; -using Nancy; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Serializer; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; -using Sonarr.Http; using Sonarr.Http.Extensions; +using Sonarr.Http.REST; +using Sonarr.Http.REST.Attributes; namespace Sonarr.Api.V3 { - public abstract class ProviderModuleBase : SonarrRestModule + public abstract class ProviderControllerBase : RestController where TProviderDefinition : ProviderDefinition, new() where TProvider : IProvider - where TProviderResource : ProviderResource, new() + where TProviderResource : ProviderResource, new() { private readonly IProviderFactory _providerFactory; private readonly ProviderResourceMapper _resourceMapper; - protected ProviderModuleBase(IProviderFactory providerFactory, string resource, ProviderResourceMapper resourceMapper) - : base(resource) + protected ProviderControllerBase(IProviderFactory providerFactory, string resource, ProviderResourceMapper resourceMapper) { _providerFactory = providerFactory; _resourceMapper = resourceMapper; - Get("schema", x => GetTemplates()); - Post("test", x => Test(ReadResourceFromRequest(true))); - Post("testall", x => TestAll()); - Post("action/{action}", x => RequestAction(x.action, ReadResourceFromRequest(true, true))); - - GetResourceAll = GetAll; - GetResourceById = GetProviderById; - CreateResource = CreateProvider; - UpdateResource = UpdateProvider; - DeleteResource = DeleteProvider; - SharedValidator.RuleFor(c => c.Name).NotEmpty(); SharedValidator.RuleFor(c => c.Name).Must((v, c) => !_providerFactory.All().Any(p => p.Name == c && p.Id != v.Id)).WithMessage("Should be unique"); SharedValidator.RuleFor(c => c.Implementation).NotEmpty(); @@ -44,7 +33,7 @@ namespace Sonarr.Api.V3 PostValidator.RuleFor(c => c.Fields).NotNull(); } - private TProviderResource GetProviderById(int id) + protected override TProviderResource GetResourceById(int id) { var definition = _providerFactory.Get(id); _providerFactory.SetProviderCharacteristics(definition); @@ -52,7 +41,8 @@ namespace Sonarr.Api.V3 return _resourceMapper.ToResource(definition); } - private List GetAll() + [HttpGet] + public List GetAll() { var providerDefinitions = _providerFactory.All().OrderBy(p => p.ImplementationName); @@ -68,7 +58,8 @@ namespace Sonarr.Api.V3 return result.OrderBy(p => p.Name).ToList(); } - private int CreateProvider(TProviderResource providerResource) + [RestPostById] + public ActionResult CreateProvider(TProviderResource providerResource) { var providerDefinition = GetDefinition(providerResource, true, false, false); @@ -79,10 +70,11 @@ namespace Sonarr.Api.V3 providerDefinition = _providerFactory.Create(providerDefinition); - return providerDefinition.Id; + return Created(providerDefinition.Id); } - private void UpdateProvider(TProviderResource providerResource) + [RestPutById] + public ActionResult UpdateProvider(TProviderResource providerResource) { var providerDefinition = GetDefinition(providerResource, true, false, false); var forceSave = Request.GetBooleanQueryParameter("forceSave"); @@ -94,6 +86,8 @@ namespace Sonarr.Api.V3 } _providerFactory.Update(providerDefinition); + + return Accepted(providerResource.Id); } private TProviderDefinition GetDefinition(TProviderResource providerResource, bool validate, bool includeWarnings, bool forceValidate) @@ -108,28 +102,29 @@ namespace Sonarr.Api.V3 return definition; } - private void DeleteProvider(int id) + [RestDeleteById] + public object DeleteProvider(int id) { _providerFactory.Delete(id); + + return new { }; } - private object GetTemplates() + [HttpGet("schema")] + public List GetTemplates() { var defaultDefinitions = _providerFactory.GetDefaultDefinitions().OrderBy(p => p.ImplementationName).ToList(); - var result = new List(defaultDefinitions.Count()); + var result = new List(defaultDefinitions.Count); foreach (var providerDefinition in defaultDefinitions) { var providerResource = _resourceMapper.ToResource(providerDefinition); var presetDefinitions = _providerFactory.GetPresetDefinitions(providerDefinition); - providerResource.Presets = presetDefinitions.Select(v => - { - var presetResource = _resourceMapper.ToResource(v); - - return presetResource as ProviderResource; - }).ToList(); + providerResource.Presets = presetDefinitions + .Select(v => _resourceMapper.ToResource(v)) + .ToList(); result.Add(providerResource); } @@ -137,7 +132,9 @@ namespace Sonarr.Api.V3 return result; } - private object Test(TProviderResource providerResource) + [SkipValidation(true, false)] + [HttpPost("test")] + public object Test([FromBody] TProviderResource providerResource) { var providerDefinition = GetDefinition(providerResource, true, true, true); @@ -146,7 +143,8 @@ namespace Sonarr.Api.V3 return "{}"; } - private object TestAll() + [HttpPost("testall")] + public IActionResult TestAll() { var providerDefinitions = _providerFactory.All() .Where(c => c.Settings.Validate().IsValid && c.Enable) @@ -161,25 +159,26 @@ namespace Sonarr.Api.V3 validationFailures.AddRange(_providerFactory.Test(definition).Errors); result.Add(new ProviderTestAllResult - { - Id = definition.Id, - ValidationFailures = validationFailures + { + Id = definition.Id, + ValidationFailures = validationFailures }); } - return ResponseWithCode(result, result.Any(c => !c.IsValid) ? HttpStatusCode.BadRequest : HttpStatusCode.OK); + return result.Any(c => !c.IsValid) ? BadRequest(result) : Ok(result); } - private object RequestAction(string action, TProviderResource providerResource) + [SkipValidation] + [HttpPost("action/{name}")] + public IActionResult RequestAction(string name, [FromBody] TProviderResource resource) { - var providerDefinition = GetDefinition(providerResource, false, false, false); + var providerDefinition = GetDefinition(resource, false, false, false); - var query = ((IDictionary)Request.Query.ToDictionary()).ToDictionary(k => k.Key, k => k.Value.ToString()); + var query = Request.Query.ToDictionary(x => x.Key, x => x.Value.ToString()); - var data = _providerFactory.RequestAction(providerDefinition, action, query); - Response resp = data.ToJson(); - resp.ContentType = "application/json"; - return resp; + var data = _providerFactory.RequestAction(providerDefinition, name, query); + + return Content(data.ToJson(), "application/json"); } private void Validate(TProviderDefinition definition, bool includeWarnings) diff --git a/src/Sonarr.Api.V3/ProviderResource.cs b/src/Sonarr.Api.V3/ProviderResource.cs index d7f9672b0..774796272 100644 --- a/src/Sonarr.Api.V3/ProviderResource.cs +++ b/src/Sonarr.Api.V3/ProviderResource.cs @@ -6,7 +6,7 @@ using Sonarr.Http.REST; namespace Sonarr.Api.V3 { - public class ProviderResource : RestResource + public class ProviderResource : RestResource { public string Name { get; set; } public List Fields { get; set; } @@ -17,11 +17,11 @@ namespace Sonarr.Api.V3 public ProviderMessage Message { get; set; } public HashSet Tags { get; set; } - public List Presets { get; set; } + public List Presets { get; set; } } public class ProviderResourceMapper - where TProviderResource : ProviderResource, new() + where TProviderResource : ProviderResource, new() where TProviderDefinition : ProviderDefinition, new() { public virtual TProviderResource ToResource(TProviderDefinition definition) diff --git a/src/Sonarr.Api.V3/Qualities/QualityDefinitionModule.cs b/src/Sonarr.Api.V3/Qualities/QualityDefinitionController.cs similarity index 53% rename from src/Sonarr.Api.V3/Qualities/QualityDefinitionModule.cs rename to src/Sonarr.Api.V3/Qualities/QualityDefinitionController.cs index e6cec4c1b..fc10f2860 100644 --- a/src/Sonarr.Api.V3/Qualities/QualityDefinitionModule.cs +++ b/src/Sonarr.Api.V3/Qualities/QualityDefinitionController.cs @@ -1,58 +1,58 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using Nancy; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Qualities; using NzbDrone.SignalR; using Sonarr.Http; -using Sonarr.Http.Extensions; +using Sonarr.Http.REST; +using Sonarr.Http.REST.Attributes; namespace Sonarr.Api.V3.Qualities { - public class QualityDefinitionModule : SonarrRestModuleWithSignalR, IHandle + [V3ApiController] + public class QualityDefinitionController : RestControllerWithSignalR, IHandle { private readonly IQualityDefinitionService _qualityDefinitionService; - public QualityDefinitionModule(IQualityDefinitionService qualityDefinitionService, IBroadcastSignalRMessage signalRBroadcaster) + public QualityDefinitionController(IQualityDefinitionService qualityDefinitionService, IBroadcastSignalRMessage signalRBroadcaster) : base(signalRBroadcaster) { _qualityDefinitionService = qualityDefinitionService; - - GetResourceAll = GetAll; - GetResourceById = GetById; - UpdateResource = Update; - Put("/update", d => UpdateMany()); } - private void Update(QualityDefinitionResource resource) + [RestPutById] + public ActionResult Update(QualityDefinitionResource resource) { var model = resource.ToModel(); _qualityDefinitionService.Update(model); + return Accepted(model.Id); } - private QualityDefinitionResource GetById(int id) + protected override QualityDefinitionResource GetResourceById(int id) { return _qualityDefinitionService.GetById(id).ToResource(); } - private List GetAll() + [HttpGet] + public List GetAll() { return _qualityDefinitionService.All().ToResource(); } - private object UpdateMany() + [HttpPut("update")] + public object UpdateMany([FromBody] List resource) { //Read from request - var qualityDefinitions = Request.Body.FromJson>() + var qualityDefinitions = resource .ToModel() .ToList(); _qualityDefinitionService.UpdateMany(qualityDefinitions); - return ResponseWithCode(_qualityDefinitionService.All() - .ToResource(), - HttpStatusCode.Accepted); + return Accepted(_qualityDefinitionService.All() + .ToResource()); } public void Handle(CommandExecutedEvent message) diff --git a/src/Sonarr.Api.V3/Queue/QueueActionController.cs b/src/Sonarr.Api.V3/Queue/QueueActionController.cs new file mode 100644 index 000000000..849fc2b39 --- /dev/null +++ b/src/Sonarr.Api.V3/Queue/QueueActionController.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Download; +using NzbDrone.Core.Download.Pending; +using Sonarr.Http; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Queue +{ + [V3ApiController("queue")] + public class QueueActionController : Controller + { + private readonly IPendingReleaseService _pendingReleaseService; + private readonly IDownloadService _downloadService; + + public QueueActionController(IPendingReleaseService pendingReleaseService, + IDownloadService downloadService) + { + _pendingReleaseService = pendingReleaseService; + _downloadService = downloadService; + } + + [HttpPost("grab/{id:int}")] + public object Grab(int id) + { + var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id); + + if (pendingRelease == null) + { + throw new NotFoundException(); + } + + _downloadService.DownloadReport(pendingRelease.RemoteEpisode); + + return new { }; + } + + [HttpPost("grab/bulk")] + public object Grab([FromBody] QueueBulkResource resource) + { + foreach (var id in resource.Ids) + { + var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id); + + if (pendingRelease == null) + { + throw new NotFoundException(); + } + + _downloadService.DownloadReport(pendingRelease.RemoteEpisode); + } + + return new { }; + } + } +} diff --git a/src/Sonarr.Api.V3/Queue/QueueActionModule.cs b/src/Sonarr.Api.V3/Queue/QueueActionModule.cs deleted file mode 100644 index ed6ed6e8a..000000000 --- a/src/Sonarr.Api.V3/Queue/QueueActionModule.cs +++ /dev/null @@ -1,197 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Nancy; -using NzbDrone.Core.Blocklisting; -using NzbDrone.Core.Download; -using NzbDrone.Core.Download.Pending; -using NzbDrone.Core.Download.TrackedDownloads; -using NzbDrone.Core.Queue; -using Sonarr.Http; -using Sonarr.Http.Extensions; -using Sonarr.Http.REST; - -namespace Sonarr.Api.V3.Queue -{ - public class QueueActionModule : SonarrRestModule - { - private readonly IQueueService _queueService; - private readonly ITrackedDownloadService _trackedDownloadService; - private readonly IFailedDownloadService _failedDownloadService; - private readonly IIgnoredDownloadService _ignoredDownloadService; - private readonly IProvideDownloadClient _downloadClientProvider; - private readonly IPendingReleaseService _pendingReleaseService; - private readonly IDownloadService _downloadService; - private readonly IBlocklistService _blocklistService; - - public QueueActionModule(IQueueService queueService, - ITrackedDownloadService trackedDownloadService, - IFailedDownloadService failedDownloadService, - IIgnoredDownloadService ignoredDownloadService, - IProvideDownloadClient downloadClientProvider, - IPendingReleaseService pendingReleaseService, - IDownloadService downloadService, - IBlocklistService blocklistService) - { - _queueService = queueService; - _trackedDownloadService = trackedDownloadService; - _failedDownloadService = failedDownloadService; - _ignoredDownloadService = ignoredDownloadService; - _downloadClientProvider = downloadClientProvider; - _pendingReleaseService = pendingReleaseService; - _downloadService = downloadService; - _blocklistService = blocklistService; - - Post(@"/grab/(?[\d]{1,10})", x => Grab((int)x.Id)); - Post("/grab/bulk", x => Grab()); - - Delete(@"/(?[\d]{1,10})", x => Remove((int)x.Id)); - Delete("/bulk", x => Remove()); - } - - private object Grab(int id) - { - var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id); - - if (pendingRelease == null) - { - throw new NotFoundException(); - } - - _downloadService.DownloadReport(pendingRelease.RemoteEpisode); - - return new object(); - } - - private object Grab() - { - var resource = Request.Body.FromJson(); - - foreach (var id in resource.Ids) - { - var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id); - - if (pendingRelease == null) - { - throw new NotFoundException(); - } - - _downloadService.DownloadReport(pendingRelease.RemoteEpisode); - } - - return new object(); - } - - private object Remove(int id) - { - var removeFromClient = Request.GetBooleanQueryParameter("removeFromClient", true); - - // blacklist maintained for backwards compatability, UI uses blocklist. - var blocklist = Request.GetBooleanQueryParameter("blocklist") ? Request.GetBooleanQueryParameter("blocklist") : Request.GetBooleanQueryParameter("blacklist"); - - var trackedDownload = Remove(id, removeFromClient, blocklist); - - if (trackedDownload != null) - { - _trackedDownloadService.StopTracking(trackedDownload.DownloadItem.DownloadId); - } - - return new object(); - } - - private object Remove() - { - var removeFromClient = Request.GetBooleanQueryParameter("removeFromClient", true); - - // blacklist maintained for backwards compatability, UI uses blocklist. - var blocklist = Request.GetBooleanQueryParameter("blocklist") ? Request.GetBooleanQueryParameter("blocklist") : Request.GetBooleanQueryParameter("blacklist"); - - var resource = Request.Body.FromJson(); - var trackedDownloadIds = new List(); - - foreach (var id in resource.Ids) - { - var trackedDownload = Remove(id, removeFromClient, blocklist); - - if (trackedDownload != null) - { - trackedDownloadIds.Add(trackedDownload.DownloadItem.DownloadId); - } - } - - _trackedDownloadService.StopTracking(trackedDownloadIds); - - return new object(); - } - - private TrackedDownload Remove(int id, bool removeFromClient, bool blocklist) - { - var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id); - - if (pendingRelease != null) - { - if (blocklist) - { - _blocklistService.Block(pendingRelease.RemoteEpisode, "Pending release manually blocklisted"); - } - - _pendingReleaseService.RemovePendingQueueItems(pendingRelease.Id); - - return null; - } - - var trackedDownload = GetTrackedDownload(id); - - if (trackedDownload == null) - { - throw new NotFoundException(); - } - - if (removeFromClient) - { - var downloadClient = _downloadClientProvider.Get(trackedDownload.DownloadClient); - - if (downloadClient == null) - { - throw new BadRequestException(); - } - - downloadClient.RemoveItem(trackedDownload.DownloadItem, true); - } - - if (blocklist) - { - _failedDownloadService.MarkAsFailed(trackedDownload.DownloadItem.DownloadId); - } - - if (!removeFromClient && !blocklist) - { - if (!_ignoredDownloadService.IgnoreDownload(trackedDownload)) - { - return null; - } - } - - return trackedDownload; - } - - private TrackedDownload GetTrackedDownload(int queueId) - { - var queueItem = _queueService.Find(queueId); - - if (queueItem == null) - { - throw new NotFoundException(); - } - - var trackedDownload = _trackedDownloadService.Find(queueItem.DownloadId); - - if (trackedDownload == null) - { - throw new NotFoundException(); - } - - return trackedDownload; - } - } -} diff --git a/src/Sonarr.Api.V3/Queue/QueueModule.cs b/src/Sonarr.Api.V3/Queue/QueueController.cs similarity index 57% rename from src/Sonarr.Api.V3/Queue/QueueModule.cs rename to src/Sonarr.Api.V3/Queue/QueueController.cs index a20b2d6c7..072d82292 100644 --- a/src/Sonarr.Api.V3/Queue/QueueModule.cs +++ b/src/Sonarr.Api.V3/Queue/QueueController.cs @@ -1,9 +1,14 @@ using System; +using System.Collections.Generic; using System.Linq; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Blocklisting; using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Download; using NzbDrone.Core.Download.Pending; +using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.Languages; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Profiles.Languages; @@ -13,10 +18,13 @@ using NzbDrone.Core.Queue; using NzbDrone.SignalR; using Sonarr.Http; using Sonarr.Http.Extensions; +using Sonarr.Http.REST; +using Sonarr.Http.REST.Attributes; namespace Sonarr.Api.V3.Queue { - public class QueueModule : SonarrRestModuleWithSignalR, + [V3ApiController] + public class QueueController : RestControllerWithSignalR, IHandle, IHandle { private readonly IQueueService _queueService; @@ -24,30 +32,79 @@ namespace Sonarr.Api.V3.Queue private readonly LanguageComparer _languageComparer; private readonly QualityModelComparer _qualityComparer; + private readonly ITrackedDownloadService _trackedDownloadService; + private readonly IFailedDownloadService _failedDownloadService; + private readonly IIgnoredDownloadService _ignoredDownloadService; + private readonly IProvideDownloadClient _downloadClientProvider; + private readonly IBlocklistService _blocklistService; - public QueueModule(IBroadcastSignalRMessage broadcastSignalRMessage, + public QueueController(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService, + IQualityProfileService qualityProfileService, ILanguageProfileService languageProfileService, - IQualityProfileService qualityProfileService) + ITrackedDownloadService trackedDownloadService, + IFailedDownloadService failedDownloadService, + IIgnoredDownloadService ignoredDownloadService, + IProvideDownloadClient downloadClientProvider, + IBlocklistService blocklistService) : base(broadcastSignalRMessage) { _queueService = queueService; _pendingReleaseService = pendingReleaseService; - GetResourcePaged = GetQueue; + _trackedDownloadService = trackedDownloadService; + _failedDownloadService = failedDownloadService; + _ignoredDownloadService = ignoredDownloadService; + _downloadClientProvider = downloadClientProvider; + _blocklistService = blocklistService; - _languageComparer = new LanguageComparer(languageProfileService.GetDefaultProfile(string.Empty)); _qualityComparer = new QualityModelComparer(qualityProfileService.GetDefaultProfile(string.Empty)); + _languageComparer = new LanguageComparer(languageProfileService.GetDefaultProfile(string.Empty)); } - private PagingResource GetQueue(PagingResource pagingResource) + protected override QueueResource GetResourceById(int id) { - var pagingSpec = pagingResource.MapToPagingSpec("timeleft", SortDirection.Ascending); - var includeUnknownSeriesItems = Request.GetBooleanQueryParameter("includeUnknownSeriesItems"); - var includeSeries = Request.GetBooleanQueryParameter("includeSeries"); - var includeEpisode = Request.GetBooleanQueryParameter("includeEpisode"); + throw new NotImplementedException(); + } - return ApplyToPage((spec) => GetQueue(spec, includeUnknownSeriesItems), pagingSpec, (q) => MapToResource(q, includeSeries, includeEpisode)); + [RestDeleteById] + public void RemoveAction(int id, bool removeFromClient = true, bool blocklist = false) + { + var trackedDownload = Remove(id, removeFromClient, blocklist); + + if (trackedDownload != null) + { + _trackedDownloadService.StopTracking(trackedDownload.DownloadItem.DownloadId); + } + } + + [HttpDelete("bulk")] + public object RemoveMany([FromBody] QueueBulkResource resource, [FromQuery] bool removeFromClient = true, [FromQuery] bool blocklist = false) + { + var trackedDownloadIds = new List(); + + foreach (var id in resource.Ids) + { + var trackedDownload = Remove(id, removeFromClient, blocklist); + + if (trackedDownload != null) + { + trackedDownloadIds.Add(trackedDownload.DownloadItem.DownloadId); + } + } + + _trackedDownloadService.StopTracking(trackedDownloadIds); + + return new { }; + } + + [HttpGet] + public PagingResource GetQueue(bool includeUnknownSeriesItems = false, bool includeSeries = false, bool includeEpisode = false) + { + var pagingResource = Request.ReadPagingResourceFromRequest(); + var pagingSpec = pagingResource.MapToPagingSpec("timeleft", SortDirection.Ascending); + + return pagingSpec.ApplyToPage((spec) => GetQueue(spec, includeUnknownSeriesItems), (q) => MapToResource(q, includeSeries, includeEpisode)); } private PagingSpec GetQueue(PagingSpec pagingSpec, bool includeUnknownSeriesItems) @@ -61,14 +118,7 @@ namespace Sonarr.Api.V3.Queue var fullQueue = filteredQueue.Concat(pending).ToList(); IOrderedEnumerable ordered; - if (pagingSpec.SortKey == "episode") - { - ordered = ascending - ? fullQueue.OrderBy(q => q.Episode?.SeasonNumber).ThenBy(q => q.Episode?.EpisodeNumber) - : fullQueue.OrderByDescending(q => q.Episode?.SeasonNumber) - .ThenByDescending(q => q.Episode?.EpisodeNumber); - } - else if (pagingSpec.SortKey == "timeleft") + if (pagingSpec.SortKey == "timeleft") { ordered = ascending ? fullQueue.OrderBy(q => q.Timeleft, new TimeleftComparer()) @@ -99,18 +149,18 @@ namespace Sonarr.Api.V3.Queue ? fullQueue.OrderBy(q => q.DownloadClient, StringComparer.InvariantCultureIgnoreCase) : fullQueue.OrderByDescending(q => q.DownloadClient, StringComparer.InvariantCultureIgnoreCase); } - else if (pagingSpec.SortKey == "language") - { - ordered = ascending - ? fullQueue.OrderBy(q => q.Language, _languageComparer) - : fullQueue.OrderByDescending(q => q.Language, _languageComparer); - } else if (pagingSpec.SortKey == "quality") { ordered = ascending ? fullQueue.OrderBy(q => q.Quality, _qualityComparer) : fullQueue.OrderByDescending(q => q.Quality, _qualityComparer); } + else if (pagingSpec.SortKey == "language") + { + ordered = ascending + ? fullQueue.OrderBy(q => q.Language, _languageComparer) + : fullQueue.OrderByDescending(q => q.Language, _languageComparer); + } else { ordered = ascending ? fullQueue.OrderBy(orderByFunc) : fullQueue.OrderByDescending(orderByFunc); @@ -142,9 +192,9 @@ namespace Sonarr.Api.V3.Queue return q => q.Title; case "episode": return q => q.Episode; - case "episode.airDateUtc": + case "episodes.airDateUtc": return q => q.Episode?.AirDateUtc ?? DateTime.MinValue; - case "episode.title": + case "episodes.title": return q => q.Episode?.Title ?? string.Empty; case "language": return q => q.Language; @@ -158,16 +208,84 @@ namespace Sonarr.Api.V3.Queue } } + private TrackedDownload Remove(int id, bool removeFromClient, bool blocklist) + { + var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id); + + if (pendingRelease != null) + { + _blocklistService.Block(pendingRelease.RemoteEpisode, "Pending release manually blocklisted"); + _pendingReleaseService.RemovePendingQueueItems(pendingRelease.Id); + + return null; + } + + var trackedDownload = GetTrackedDownload(id); + + if (trackedDownload == null) + { + throw new NotFoundException(); + } + + if (removeFromClient) + { + var downloadClient = _downloadClientProvider.Get(trackedDownload.DownloadClient); + + if (downloadClient == null) + { + throw new BadRequestException(); + } + + downloadClient.RemoveItem(trackedDownload.DownloadItem, true); + } + + if (blocklist) + { + _failedDownloadService.MarkAsFailed(trackedDownload.DownloadItem.DownloadId); + } + + if (!removeFromClient && !blocklist) + { + if (!_ignoredDownloadService.IgnoreDownload(trackedDownload)) + { + return null; + } + } + + return trackedDownload; + } + + private TrackedDownload GetTrackedDownload(int queueId) + { + var queueItem = _queueService.Find(queueId); + + if (queueItem == null) + { + throw new NotFoundException(); + } + + var trackedDownload = _trackedDownloadService.Find(queueItem.DownloadId); + + if (trackedDownload == null) + { + throw new NotFoundException(); + } + + return trackedDownload; + } + private QueueResource MapToResource(NzbDrone.Core.Queue.Queue queueItem, bool includeSeries, bool includeEpisode) { return queueItem.ToResource(includeSeries, includeEpisode); } + [NonAction] public void Handle(QueueUpdatedEvent message) { BroadcastResourceChange(ModelAction.Sync); } + [NonAction] public void Handle(PendingReleasesUpdatedEvent message) { BroadcastResourceChange(ModelAction.Sync); diff --git a/src/Sonarr.Api.V3/Queue/QueueDetailsModule.cs b/src/Sonarr.Api.V3/Queue/QueueDetailsController.cs similarity index 52% rename from src/Sonarr.Api.V3/Queue/QueueDetailsModule.cs rename to src/Sonarr.Api.V3/Queue/QueueDetailsController.cs index 1d5a4f118..5d478cc25 100644 --- a/src/Sonarr.Api.V3/Queue/QueueDetailsModule.cs +++ b/src/Sonarr.Api.V3/Queue/QueueDetailsController.cs @@ -1,65 +1,63 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Queue; using NzbDrone.SignalR; using Sonarr.Http; -using Sonarr.Http.Extensions; +using Sonarr.Http.REST; namespace Sonarr.Api.V3.Queue { - public class QueueDetailsModule : SonarrRestModuleWithSignalR, + [V3ApiController("queue/details")] + public class QueueDetailsController : RestControllerWithSignalR, IHandle, IHandle { private readonly IQueueService _queueService; private readonly IPendingReleaseService _pendingReleaseService; - public QueueDetailsModule(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService) - : base(broadcastSignalRMessage, "queue/details") + public QueueDetailsController(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService) + : base(broadcastSignalRMessage) { _queueService = queueService; _pendingReleaseService = pendingReleaseService; - GetResourceAll = GetQueue; } - private List GetQueue() + protected override QueueResource GetResourceById(int id) + { + throw new NotImplementedException(); + } + + [HttpGet] + public List GetQueue(int? seriesId, [FromQuery]List episodeIds, bool includeSeries = false, bool includeEpisode = false) { - var includeSeries = Request.GetBooleanQueryParameter("includeSeries"); - var includeEpisode = Request.GetBooleanQueryParameter("includeEpisode", true); var queue = _queueService.GetQueue(); var pending = _pendingReleaseService.GetPendingQueue(); var fullQueue = queue.Concat(pending); - var seriesIdQuery = Request.Query.SeriesId; - var episodeIdsQuery = Request.Query.EpisodeIds; - - if (seriesIdQuery.HasValue) + if (seriesId.HasValue) { - return fullQueue.Where(q => q.Series?.Id == (int)seriesIdQuery).ToResource(includeSeries, includeEpisode); + return fullQueue.Where(q => q.Series?.Id == seriesId).ToResource(includeSeries, includeEpisode); } - if (episodeIdsQuery.HasValue) + if (episodeIds.Any()) { - string episodeIdsValue = episodeIdsQuery.Value.ToString(); - - var episodeIds = episodeIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(e => Convert.ToInt32(e)) - .ToList(); - return fullQueue.Where(q => q.Episode != null && episodeIds.Contains(q.Episode.Id)).ToResource(includeSeries, includeEpisode); } return fullQueue.ToResource(includeSeries, includeEpisode); } + [NonAction] public void Handle(QueueUpdatedEvent message) { BroadcastResourceChange(ModelAction.Sync); } + [NonAction] public void Handle(PendingReleasesUpdatedEvent message) { BroadcastResourceChange(ModelAction.Sync); diff --git a/src/Sonarr.Api.V3/Queue/QueueStatusModule.cs b/src/Sonarr.Api.V3/Queue/QueueStatusController.cs similarity index 77% rename from src/Sonarr.Api.V3/Queue/QueueStatusModule.cs rename to src/Sonarr.Api.V3/Queue/QueueStatusController.cs index df599b18b..90bee4e6a 100644 --- a/src/Sonarr.Api.V3/Queue/QueueStatusModule.cs +++ b/src/Sonarr.Api.V3/Queue/QueueStatusController.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Linq; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.TPL; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Download.Pending; @@ -8,33 +9,34 @@ using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Queue; using NzbDrone.SignalR; using Sonarr.Http; +using Sonarr.Http.REST; namespace Sonarr.Api.V3.Queue { - public class QueueStatusModule : SonarrRestModuleWithSignalR, + [V3ApiController("queue/status")] + public class QueueStatusController : RestControllerWithSignalR, IHandle, IHandle { private readonly IQueueService _queueService; private readonly IPendingReleaseService _pendingReleaseService; private readonly Debouncer _broadcastDebounce; - public QueueStatusModule(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService) - : base(broadcastSignalRMessage, "queue/status") + public QueueStatusController(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService) + : base(broadcastSignalRMessage) { _queueService = queueService; _pendingReleaseService = pendingReleaseService; _broadcastDebounce = new Debouncer(BroadcastChange, TimeSpan.FromSeconds(5)); - - Get("/", x => GetQueueStatusResponse()); } - private object GetQueueStatusResponse() + protected override QueueStatusResource GetResourceById(int id) { - return GetQueueStatus(); + throw new NotImplementedException(); } - private QueueStatusResource GetQueueStatus() + [HttpGet] + public QueueStatusResource GetQueueStatus() { _broadcastDebounce.Pause(); @@ -62,11 +64,13 @@ namespace Sonarr.Api.V3.Queue BroadcastResourceChange(ModelAction.Updated, GetQueueStatus()); } + [NonAction] public void Handle(QueueUpdatedEvent message) { _broadcastDebounce.Execute(); } + [NonAction] public void Handle(PendingReleasesUpdatedEvent message) { _broadcastDebounce.Execute(); diff --git a/src/Sonarr.Api.V3/RemotePathMappings/RemotePathMappingModule.cs b/src/Sonarr.Api.V3/RemotePathMappings/RemotePathMappingController.cs similarity index 60% rename from src/Sonarr.Api.V3/RemotePathMappings/RemotePathMappingModule.cs rename to src/Sonarr.Api.V3/RemotePathMappings/RemotePathMappingController.cs index 953283b9a..6f5011f0d 100644 --- a/src/Sonarr.Api.V3/RemotePathMappings/RemotePathMappingModule.cs +++ b/src/Sonarr.Api.V3/RemotePathMappings/RemotePathMappingController.cs @@ -1,27 +1,25 @@ -using System.Collections.Generic; +using System.Collections.Generic; using FluentValidation; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.Validation.Paths; using Sonarr.Http; +using Sonarr.Http.REST; +using Sonarr.Http.REST.Attributes; namespace Sonarr.Api.V3.RemotePathMappings { - public class RemotePathMappingModule : SonarrRestModule + [V3ApiController] + public class RemotePathMappingController : RestController { private readonly IRemotePathMappingService _remotePathMappingService; - public RemotePathMappingModule(IRemotePathMappingService remotePathMappingService, + public RemotePathMappingController(IRemotePathMappingService remotePathMappingService, PathExistsValidator pathExistsValidator, MappedNetworkDriveValidator mappedNetworkDriveValidator) { _remotePathMappingService = remotePathMappingService; - GetResourceAll = GetMappings; - GetResourceById = GetMappingById; - CreateResource = CreateMapping; - DeleteResource = DeleteMapping; - UpdateResource = UpdateMapping; - SharedValidator.RuleFor(c => c.Host) .NotEmpty(); @@ -36,33 +34,37 @@ namespace Sonarr.Api.V3.RemotePathMappings .SetValidator(pathExistsValidator); } - private RemotePathMappingResource GetMappingById(int id) + protected override RemotePathMappingResource GetResourceById(int id) { return _remotePathMappingService.Get(id).ToResource(); } - private int CreateMapping(RemotePathMappingResource resource) + [RestPostById] + public ActionResult CreateMapping(RemotePathMappingResource resource) { var model = resource.ToModel(); - return _remotePathMappingService.Add(model).Id; + return Created(_remotePathMappingService.Add(model).Id); } - private List GetMappings() + [HttpGet] + public List GetMappings() { return _remotePathMappingService.All().ToResource(); } - private void DeleteMapping(int id) + [RestDeleteById] + public void DeleteMapping(int id) { _remotePathMappingService.Remove(id); } - private void UpdateMapping(RemotePathMappingResource resource) + [RestPutById] + public ActionResult UpdateMapping(RemotePathMappingResource resource) { var mapping = resource.ToModel(); - _remotePathMappingService.Update(mapping); + return Accepted(_remotePathMappingService.Update(mapping)); } } } diff --git a/src/Sonarr.Api.V3/RootFolders/RootFolderModule.cs b/src/Sonarr.Api.V3/RootFolders/RootFolderController.cs similarity index 65% rename from src/Sonarr.Api.V3/RootFolders/RootFolderModule.cs rename to src/Sonarr.Api.V3/RootFolders/RootFolderController.cs index 54dff8599..1ebb80a7c 100644 --- a/src/Sonarr.Api.V3/RootFolders/RootFolderModule.cs +++ b/src/Sonarr.Api.V3/RootFolders/RootFolderController.cs @@ -1,66 +1,69 @@ using System.Collections.Generic; -using System.Threading; using FluentValidation; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.RootFolders; using NzbDrone.Core.Validation.Paths; using NzbDrone.SignalR; using Sonarr.Http; using Sonarr.Http.Extensions; +using Sonarr.Http.REST; +using Sonarr.Http.REST.Attributes; namespace Sonarr.Api.V3.RootFolders { - public class RootFolderModule : SonarrRestModuleWithSignalR + [V3ApiController] + public class RootFolderController : RestControllerWithSignalR { private readonly IRootFolderService _rootFolderService; - public RootFolderModule(IRootFolderService rootFolderService, + public RootFolderController(IRootFolderService rootFolderService, IBroadcastSignalRMessage signalRBroadcaster, RootFolderValidator rootFolderValidator, PathExistsValidator pathExistsValidator, MappedNetworkDriveValidator mappedNetworkDriveValidator, + RecycleBinValidator recycleBinValidator, StartupFolderValidator startupFolderValidator, SystemFolderValidator systemFolderValidator, FolderWritableValidator folderWritableValidator) - : base(signalRBroadcaster) + : base(signalRBroadcaster) { _rootFolderService = rootFolderService; - GetResourceAll = GetRootFolders; - GetResourceById = GetRootFolder; - CreateResource = CreateRootFolder; - DeleteResource = DeleteFolder; - SharedValidator.RuleFor(c => c.Path) .Cascade(CascadeMode.StopOnFirstFailure) .IsValidPath() .SetValidator(rootFolderValidator) .SetValidator(mappedNetworkDriveValidator) .SetValidator(startupFolderValidator) + .SetValidator(recycleBinValidator) .SetValidator(pathExistsValidator) .SetValidator(systemFolderValidator) .SetValidator(folderWritableValidator); } - private RootFolderResource GetRootFolder(int id) + protected override RootFolderResource GetResourceById(int id) { - var timeout = Context?.Request?.GetBooleanQueryParameter("timeout", true) ?? true; + var timeout = Request?.GetBooleanQueryParameter("timeout", true) ?? true; return _rootFolderService.Get(id, timeout).ToResource(); } - private int CreateRootFolder(RootFolderResource rootFolderResource) + [RestPostById] + public ActionResult CreateRootFolder(RootFolderResource rootFolderResource) { var model = rootFolderResource.ToModel(); - return _rootFolderService.Add(model).Id; + return Created(_rootFolderService.Add(model).Id); } - private List GetRootFolders() + [HttpGet] + public List GetRootFolders() { return _rootFolderService.AllWithUnmappedFolders().ToResource(); } - private void DeleteFolder(int id) + [RestDeleteById] + public void DeleteFolder(int id) { _rootFolderService.Remove(id); } diff --git a/src/Sonarr.Api.V3/SeasonPass/SeasonPassModule.cs b/src/Sonarr.Api.V3/SeasonPass/SeasonPassController.cs similarity index 60% rename from src/Sonarr.Api.V3/SeasonPass/SeasonPassModule.cs rename to src/Sonarr.Api.V3/SeasonPass/SeasonPassController.cs index f35b2e6f9..42e91bd60 100644 --- a/src/Sonarr.Api.V3/SeasonPass/SeasonPassModule.cs +++ b/src/Sonarr.Api.V3/SeasonPass/SeasonPassController.cs @@ -1,30 +1,28 @@ -using System.Linq; -using Nancy; +using System.Linq; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Tv; -using Sonarr.Http.Extensions; +using Sonarr.Http; namespace Sonarr.Api.V3.SeasonPass { - public class SeasonPassModule : SonarrV3Module + [V3ApiController] + public class SeasonPassController : Controller { private readonly ISeriesService _seriesService; private readonly IEpisodeMonitoredService _episodeMonitoredService; - public SeasonPassModule(ISeriesService seriesService, IEpisodeMonitoredService episodeMonitoredService) - : base("/seasonpass") + public SeasonPassController(ISeriesService seriesService, IEpisodeMonitoredService episodeMonitoredService) { _seriesService = seriesService; _episodeMonitoredService = episodeMonitoredService; - Post("/", series => UpdateAll()); } - private object UpdateAll() + [HttpPost] + public IActionResult UpdateAll(SeasonPassResource resource) { - //Read from request - var request = Request.Body.FromJson(); - var seriesToUpdate = _seriesService.GetSeries(request.Series.Select(s => s.Id)); + var seriesToUpdate = _seriesService.GetSeries(resource.Series.Select(s => s.Id)); - foreach (var s in request.Series) + foreach (var s in resource.Series) { var series = seriesToUpdate.Single(c => c.Id == s.Id); @@ -46,15 +44,15 @@ namespace Sonarr.Api.V3.SeasonPass } } - if (request.MonitoringOptions != null && request.MonitoringOptions.Monitor == MonitorTypes.None) + if (resource.MonitoringOptions != null && resource.MonitoringOptions.Monitor == MonitorTypes.None) { series.Monitored = false; } - _episodeMonitoredService.SetEpisodeMonitoredStatus(series, request.MonitoringOptions); + _episodeMonitoredService.SetEpisodeMonitoredStatus(series, resource.MonitoringOptions); } - return ResponseWithCode("ok", HttpStatusCode.Accepted); + return Accepted(); } } } diff --git a/src/Sonarr.Api.V3/Series/SeriesModule.cs b/src/Sonarr.Api.V3/Series/SeriesController.cs similarity index 89% rename from src/Sonarr.Api.V3/Series/SeriesModule.cs rename to src/Sonarr.Api.V3/Series/SeriesController.cs index 9433f973f..e4982540a 100644 --- a/src/Sonarr.Api.V3/Series/SeriesModule.cs +++ b/src/Sonarr.Api.V3/Series/SeriesController.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using FluentValidation; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.DataAugmentation.Scene; using NzbDrone.Core.Datastore.Events; @@ -19,10 +20,13 @@ using NzbDrone.Core.Validation.Paths; using NzbDrone.SignalR; using Sonarr.Http; using Sonarr.Http.Extensions; +using Sonarr.Http.REST; +using Sonarr.Http.REST.Attributes; namespace Sonarr.Api.V3.Series { - public class SeriesModule : SonarrRestModuleWithSignalR, + [V3ApiController] + public class SeriesController : RestControllerWithSignalR, IHandle, IHandle, IHandle, @@ -39,7 +43,7 @@ namespace Sonarr.Api.V3.Series private readonly IManageCommandQueue _commandQueueManager; private readonly IRootFolderService _rootFolderService; - public SeriesModule(IBroadcastSignalRMessage signalRBroadcaster, + public SeriesController(IBroadcastSignalRMessage signalRBroadcaster, ISeriesService seriesService, IAddSeriesService addSeriesService, ISeriesStatisticsService seriesStatisticsService, @@ -67,12 +71,6 @@ namespace Sonarr.Api.V3.Series _commandQueueManager = commandQueueManager; _rootFolderService = rootFolderService; - GetResourceAll = AllSeries; - GetResourceById = GetSeries; - CreateResource = AddSeries; - UpdateResource = UpdateSeries; - DeleteResource = DeleteSeries; - Http.Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.QualityProfileId)); SharedValidator.RuleFor(s => s.Path) @@ -99,16 +97,15 @@ namespace Sonarr.Api.V3.Series PutValidator.RuleFor(s => s.Path).IsValidPath(); } - private List AllSeries() + [HttpGet] + public List AllSeries(int? tvdbId, bool includeSeasonImages = false) { - var tvdbId = Request.GetIntegerQueryParameter("tvdbId"); - var includeSeasonImages = Request.GetBooleanQueryParameter("includeSeasonImages"); var seriesStats = _seriesStatisticsService.SeriesStatistics(); var seriesResources = new List(); - if (tvdbId > 0) + if (tvdbId.HasValue) { - seriesResources.AddIfNotNull(_seriesService.FindByTvdbId(tvdbId).ToResource(includeSeasonImages)); + seriesResources.AddIfNotNull(_seriesService.FindByTvdbId(tvdbId.Value).ToResource(includeSeasonImages)); } else { @@ -123,22 +120,24 @@ namespace Sonarr.Api.V3.Series return seriesResources; } - private SeriesResource GetSeries(int id) + protected override SeriesResource GetResourceById(int id) { - var includeSeasonImages = Context != null && Request.GetBooleanQueryParameter("includeSeasonImages"); var series = _seriesService.GetSeries(id); - return GetSeriesResource(series, includeSeasonImages); + // Parse IncludeImages and use it + return GetSeriesResource(series, false); } - private int AddSeries(SeriesResource seriesResource) + [RestPostById] + public ActionResult AddSeries(SeriesResource seriesResource) { var series = _addSeriesService.AddSeries(seriesResource.ToModel()); - return series.Id; + return Created(series.Id); } - private void UpdateSeries(SeriesResource seriesResource) + [RestPutById] + public ActionResult UpdateSeries(SeriesResource seriesResource) { var moveFiles = Request.GetBooleanQueryParameter("moveFiles"); var series = _seriesService.GetSeries(seriesResource.Id); @@ -162,9 +161,12 @@ namespace Sonarr.Api.V3.Series _seriesService.UpdateSeries(model); BroadcastResourceChange(ModelAction.Updated, seriesResource); + + return Accepted(seriesResource.Id); } - private void DeleteSeries(int id) + [RestDeleteById] + public void DeleteSeries(int id) { var deleteFiles = Request.GetBooleanQueryParameter("deleteFiles"); var addImportListExclusion = Request.GetBooleanQueryParameter("addImportListExclusion"); @@ -255,11 +257,13 @@ namespace Sonarr.Api.V3.Series resource.RootFolderPath = _rootFolderService.GetBestRootFolderPath(resource.Path); } + [NonAction] public void Handle(EpisodeImportedEvent message) { BroadcastResourceChange(ModelAction.Updated, message.ImportedEpisode.SeriesId); } + [NonAction] public void Handle(EpisodeFileDeletedEvent message) { if (message.Reason == DeleteMediaFileReason.Upgrade) @@ -270,28 +274,33 @@ namespace Sonarr.Api.V3.Series BroadcastResourceChange(ModelAction.Updated, message.EpisodeFile.SeriesId); } + [NonAction] public void Handle(SeriesUpdatedEvent message) { BroadcastResourceChange(ModelAction.Updated, message.Series.Id); } + [NonAction] public void Handle(SeriesEditedEvent message) { - var resource = GetResourceByIdForBroadcast(message.Series.Id); + var resource = message.Series.ToResource(); resource.EpisodesChanged = message.EpisodesChanged; BroadcastResourceChange(ModelAction.Updated, resource); } + [NonAction] public void Handle(SeriesDeletedEvent message) { BroadcastResourceChange(ModelAction.Deleted, message.Series.ToResource()); } + [NonAction] public void Handle(SeriesRenamedEvent message) { BroadcastResourceChange(ModelAction.Updated, message.Series.Id); } + [NonAction] public void Handle(MediaCoversUpdatedEvent message) { if (message.Updated) diff --git a/src/Sonarr.Api.V3/Series/SeriesEditorModule.cs b/src/Sonarr.Api.V3/Series/SeriesEditorController.cs similarity index 80% rename from src/Sonarr.Api.V3/Series/SeriesEditorModule.cs rename to src/Sonarr.Api.V3/Series/SeriesEditorController.cs index 318907ac9..fbb8fd350 100644 --- a/src/Sonarr.Api.V3/Series/SeriesEditorModule.cs +++ b/src/Sonarr.Api.V3/Series/SeriesEditorController.cs @@ -1,31 +1,29 @@ using System.Collections.Generic; using System.Linq; -using Nancy; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Tv; using NzbDrone.Core.Tv.Commands; -using Sonarr.Http.Extensions; +using Sonarr.Http; namespace Sonarr.Api.V3.Series { - public class SeriesEditorModule : SonarrV3Module + [V3ApiController("series/editor")] + public class SeriesEditorController : Controller { private readonly ISeriesService _seriesService; private readonly IManageCommandQueue _commandQueueManager; - public SeriesEditorModule(ISeriesService seriesService, IManageCommandQueue commandQueueManager) - : base("/series/editor") + public SeriesEditorController(ISeriesService seriesService, IManageCommandQueue commandQueueManager) { _seriesService = seriesService; _commandQueueManager = commandQueueManager; - Put("/", series => SaveAll()); - Delete("/", series => DeleteSeries()); } - private object SaveAll() + [HttpPut] + public object SaveAll([FromBody] SeriesEditorResource resource) { - var resource = Request.Body.FromJson(); var seriesToUpdate = _seriesService.GetSeries(resource.SeriesIds); var seriesToMove = new List(); @@ -95,21 +93,18 @@ namespace Sonarr.Api.V3.Series }); } - return ResponseWithCode(_seriesService.UpdateSeries(seriesToUpdate, !resource.MoveFiles) - .ToResource(), - HttpStatusCode.Accepted); + return Accepted(_seriesService.UpdateSeries(seriesToUpdate, !resource.MoveFiles).ToResource()); } - private object DeleteSeries() + [HttpDelete] + public object DeleteSeries([FromBody] SeriesEditorResource resource) { - var resource = Request.Body.FromJson(); - foreach (var seriesId in resource.SeriesIds) { _seriesService.DeleteSeries(seriesId, resource.DeleteFiles, resource.AddImportListExclusion); } - return new object(); + return new { }; } } } diff --git a/src/Sonarr.Api.V3/Series/SeriesImportController.cs b/src/Sonarr.Api.V3/Series/SeriesImportController.cs new file mode 100644 index 000000000..e5529b898 --- /dev/null +++ b/src/Sonarr.Api.V3/Series/SeriesImportController.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Tv; +using Sonarr.Http; + +namespace Sonarr.Api.V3.Series +{ + [V3ApiController("series/import")] + public class SeriesImportController : Controller + { + private readonly IAddSeriesService _addSeriesService; + + public SeriesImportController(IAddSeriesService addSeriesService) + { + _addSeriesService = addSeriesService; + } + + [HttpPost] + public object Import([FromBody] List resource) + { + var newSeries = resource.ToModel(); + + return _addSeriesService.AddSeries(newSeries).ToResource(); + } + } +} diff --git a/src/Sonarr.Api.V3/Series/SeriesImportModule.cs b/src/Sonarr.Api.V3/Series/SeriesImportModule.cs deleted file mode 100644 index f46b6a24e..000000000 --- a/src/Sonarr.Api.V3/Series/SeriesImportModule.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Generic; -using Nancy; -using NzbDrone.Core.Tv; -using Sonarr.Http; -using Sonarr.Http.Extensions; - -namespace Sonarr.Api.V3.Series -{ - public class SeriesImportModule : SonarrRestModule - { - private readonly IAddSeriesService _addSeriesService; - - public SeriesImportModule(IAddSeriesService addSeriesService) - : base("/series/import") - { - _addSeriesService = addSeriesService; - Post("/", x => Import()); - } - - private object Import() - { - var resource = Request.Body.FromJson>(); - var newSeries = resource.ToModel(); - - return _addSeriesService.AddSeries(newSeries).ToResource(); - } - } -} diff --git a/src/Sonarr.Api.V3/Series/SeriesLookupModule.cs b/src/Sonarr.Api.V3/Series/SeriesLookupController.cs similarity index 77% rename from src/Sonarr.Api.V3/Series/SeriesLookupModule.cs rename to src/Sonarr.Api.V3/Series/SeriesLookupController.cs index 9f1fd43a5..a914bb9e6 100644 --- a/src/Sonarr.Api.V3/Series/SeriesLookupModule.cs +++ b/src/Sonarr.Api.V3/Series/SeriesLookupController.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Linq; -using Nancy; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MetadataSource; using NzbDrone.Core.Organizer; @@ -9,24 +9,24 @@ using Sonarr.Http; namespace Sonarr.Api.V3.Series { - public class SeriesLookupModule : SonarrRestModule + [V3ApiController("series/lookup")] + public class SeriesLookupController : Controller { private readonly ISearchForNewSeries _searchProxy; private readonly IBuildFileNames _fileNameBuilder; private readonly IMapCoversToLocal _coverMapper; - public SeriesLookupModule(ISearchForNewSeries searchProxy, IBuildFileNames fileNameBuilder, IMapCoversToLocal coverMapper) - : base("/series/lookup") + public SeriesLookupController(ISearchForNewSeries searchProxy, IBuildFileNames fileNameBuilder, IMapCoversToLocal coverMapper) { _searchProxy = searchProxy; _fileNameBuilder = fileNameBuilder; _coverMapper = coverMapper; - Get("/", x => Search()); } - private object Search() + [HttpGet] + public object Search([FromQuery] string term) { - var tvDbResults = _searchProxy.SearchForNewSeries((string)Request.Query.term); + var tvDbResults = _searchProxy.SearchForNewSeries(term); return MapToResource(tvDbResults); } diff --git a/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj b/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj index f4e7fa651..2859afdad 100644 --- a/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj +++ b/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj @@ -5,9 +5,6 @@ - - - diff --git a/src/Sonarr.Api.V3/SonarrV3FeedModule.cs b/src/Sonarr.Api.V3/SonarrV3FeedModule.cs deleted file mode 100644 index a90c408e7..000000000 --- a/src/Sonarr.Api.V3/SonarrV3FeedModule.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Sonarr.Http; - -namespace Sonarr.Api.V3 -{ - public abstract class SonarrV3FeedModule : SonarrModule - { - protected SonarrV3FeedModule(string resource) - : base("/feed/v3/" + resource.Trim('/')) - { - } - } -} diff --git a/src/Sonarr.Api.V3/SonarrV3Module.cs b/src/Sonarr.Api.V3/SonarrV3Module.cs deleted file mode 100644 index 6dd14ba15..000000000 --- a/src/Sonarr.Api.V3/SonarrV3Module.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Sonarr.Http; - -namespace Sonarr.Api.V3 -{ - public abstract class SonarrV3Module : SonarrModule - { - protected SonarrV3Module(string resource) - : base("/api/v3/" + resource.Trim('/')) - { - } - } -} diff --git a/src/Sonarr.Api.V3/System/Backup/BackupModule.cs b/src/Sonarr.Api.V3/System/Backup/BackupController.cs similarity index 70% rename from src/Sonarr.Api.V3/System/Backup/BackupModule.cs rename to src/Sonarr.Api.V3/System/Backup/BackupController.cs index f6d84bc90..e20a732c5 100644 --- a/src/Sonarr.Api.V3/System/Backup/BackupModule.cs +++ b/src/Sonarr.Api.V3/System/Backup/BackupController.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Crypto; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; @@ -8,10 +9,12 @@ using NzbDrone.Common.Extensions; using NzbDrone.Core.Backup; using Sonarr.Http; using Sonarr.Http.REST; +using Sonarr.Http.REST.Attributes; namespace Sonarr.Api.V3.System.Backup { - public class BackupModule : SonarrRestModule + [V3ApiController("system/backup")] + public class BackupController : Controller { private readonly IBackupService _backupService; private readonly IAppFolderInfo _appFolderInfo; @@ -19,39 +22,35 @@ namespace Sonarr.Api.V3.System.Backup private static readonly List ValidExtensions = new List { ".zip", ".db", ".xml" }; - public BackupModule(IBackupService backupService, + public BackupController(IBackupService backupService, IAppFolderInfo appFolderInfo, IDiskProvider diskProvider) - : base("system/backup") { _backupService = backupService; _appFolderInfo = appFolderInfo; _diskProvider = diskProvider; - GetResourceAll = GetBackupFiles; - DeleteResource = DeleteBackup; - - Post(@"/restore/(?[\d]{1,10})", x => Restore((int)x.Id)); - Post("/restore/upload", x => UploadAndRestore()); } + [HttpGet] public List GetBackupFiles() { var backups = _backupService.GetBackups(); return backups.Select(b => new BackupResource - { - Id = GetBackupId(b), - Name = b.Name, - Path = $"/backup/{b.Type.ToString().ToLower()}/{b.Name}", - Type = b.Type, - Size = b.Size, - Time = b.Time - }) + { + Id = GetBackupId(b), + Name = b.Name, + Path = $"/backup/{b.Type.ToString().ToLower()}/{b.Name}", + Size = b.Size, + Type = b.Type, + Time = b.Time + }) .OrderByDescending(b => b.Time) .ToList(); } - private void DeleteBackup(int id) + [RestDeleteById] + public void DeleteBackup(int id) { var backup = GetBackup(id); var path = GetBackupPath(backup); @@ -64,6 +63,7 @@ namespace Sonarr.Api.V3.System.Backup _diskProvider.DeleteFile(path); } + [HttpPost("restore/{id:int}")] public object Restore(int id) { var backup = GetBackup(id); @@ -78,14 +78,15 @@ namespace Sonarr.Api.V3.System.Backup _backupService.Restore(path); return new - { - RestartRequired = true - }; + { + RestartRequired = true + }; } + [HttpPost("restore/upload")] public object UploadAndRestore() { - var files = Context.Request.Files.ToList(); + var files = Request.Form.Files; if (files.Empty()) { @@ -93,7 +94,7 @@ namespace Sonarr.Api.V3.System.Backup } var file = files.First(); - var extension = Path.GetExtension(file.Name); + var extension = Path.GetExtension(file.FileName); if (!ValidExtensions.Contains(extension)) { @@ -102,16 +103,16 @@ namespace Sonarr.Api.V3.System.Backup var path = Path.Combine(_appFolderInfo.TempFolder, $"sonarr_backup_restore{extension}"); - _diskProvider.SaveStream(file.Value, path); + _diskProvider.SaveStream(file.OpenReadStream(), path); _backupService.Restore(path); // Cleanup restored file _diskProvider.DeleteFile(path); return new - { - RestartRequired = true - }; + { + RestartRequired = true + }; } private string GetBackupPath(NzbDrone.Core.Backup.Backup backup) diff --git a/src/Sonarr.Api.V3/System/SystemController.cs b/src/Sonarr.Api.V3/System/SystemController.cs new file mode 100644 index 000000000..a5b20f4b4 --- /dev/null +++ b/src/Sonarr.Api.V3/System/SystemController.cs @@ -0,0 +1,125 @@ +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Internal; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Lifecycle; +using Sonarr.Http; +using Sonarr.Http.Validation; + +namespace Sonarr.Api.V3.System +{ + [V3ApiController] + public class SystemController : Controller + { + private readonly IAppFolderInfo _appFolderInfo; + private readonly IRuntimeInfo _runtimeInfo; + private readonly IPlatformInfo _platformInfo; + private readonly IOsInfo _osInfo; + private readonly IConfigFileProvider _configFileProvider; + private readonly IMainDatabase _database; + private readonly ILifecycleService _lifecycleService; + private readonly IDeploymentInfoProvider _deploymentInfoProvider; + private readonly EndpointDataSource _endpointData; + private readonly DfaGraphWriter _graphWriter; + private readonly DuplicateEndpointDetector _detector; + + public SystemController(IAppFolderInfo appFolderInfo, + IRuntimeInfo runtimeInfo, + IPlatformInfo platformInfo, + IOsInfo osInfo, + IConfigFileProvider configFileProvider, + IMainDatabase database, + ILifecycleService lifecycleService, + IDeploymentInfoProvider deploymentInfoProvider, + EndpointDataSource endpoints, + DfaGraphWriter graphWriter, + DuplicateEndpointDetector detector) + { + _appFolderInfo = appFolderInfo; + _runtimeInfo = runtimeInfo; + _platformInfo = platformInfo; + _osInfo = osInfo; + _configFileProvider = configFileProvider; + _database = database; + _lifecycleService = lifecycleService; + _deploymentInfoProvider = deploymentInfoProvider; + _endpointData = endpoints; + _graphWriter = graphWriter; + _detector = detector; + } + + [HttpGet("status")] + public object GetStatus() + { + return new + { + AppName = BuildInfo.AppName, + InstanceName = _configFileProvider.InstanceName, + Version = BuildInfo.Version.ToString(), + BuildTime = BuildInfo.BuildDateTime, + IsDebug = BuildInfo.IsDebug, + IsProduction = RuntimeInfo.IsProduction, + IsAdmin = _runtimeInfo.IsAdmin, + IsUserInteractive = RuntimeInfo.IsUserInteractive, + StartupPath = _appFolderInfo.StartUpFolder, + AppData = _appFolderInfo.GetAppDataPath(), + OsName = _osInfo.Name, + OsVersion = _osInfo.Version, + IsNetCore = PlatformInfo.IsNetCore, + IsLinux = OsInfo.IsLinux, + IsOsx = OsInfo.IsOsx, + IsWindows = OsInfo.IsWindows, + IsDocker = _osInfo.IsDocker, + Mode = _runtimeInfo.Mode, + Branch = _configFileProvider.Branch, + Authentication = _configFileProvider.AuthenticationMethod, + SqliteVersion = _database.Version, + MigrationVersion = _database.Migration, + UrlBase = _configFileProvider.UrlBase, + RuntimeVersion = _platformInfo.Version, + RuntimeName = PlatformInfo.Platform, + StartTime = _runtimeInfo.StartTime, + PackageVersion = _deploymentInfoProvider.PackageVersion, + PackageAuthor = _deploymentInfoProvider.PackageAuthor, + PackageUpdateMechanism = _deploymentInfoProvider.PackageUpdateMechanism, + PackageUpdateMechanismMessage = _deploymentInfoProvider.PackageUpdateMechanismMessage + }; + } + + [HttpGet("routes")] + public IActionResult GetRoutes() + { + using (var sw = new StringWriter()) + { + _graphWriter.Write(_endpointData, sw); + var graph = sw.ToString(); + return Content(graph, "text/plain"); + } + } + + [HttpGet("routes/duplicate")] + public object DuplicateRoutes() + { + return _detector.GetDuplicateEndpoints(_endpointData); + } + + [HttpPost("shutdown")] + public object Shutdown() + { + Task.Factory.StartNew(() => _lifecycleService.Shutdown()); + return new { ShuttingDown = true }; + } + + [HttpPost("restart")] + public object Restart() + { + Task.Factory.StartNew(() => _lifecycleService.Restart()); + return new { Restarting = true }; + } + } +} diff --git a/src/Sonarr.Api.V3/System/SystemModule.cs b/src/Sonarr.Api.V3/System/SystemModule.cs deleted file mode 100644 index 085493ed4..000000000 --- a/src/Sonarr.Api.V3/System/SystemModule.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.Threading.Tasks; -using Nancy.Routing; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Lifecycle; - -namespace Sonarr.Api.V3.System -{ - public class SystemModule : SonarrV3Module - { - private readonly IAppFolderInfo _appFolderInfo; - private readonly IRuntimeInfo _runtimeInfo; - private readonly IPlatformInfo _platformInfo; - private readonly IOsInfo _osInfo; - private readonly IRouteCacheProvider _routeCacheProvider; - private readonly IConfigFileProvider _configFileProvider; - private readonly IMainDatabase _database; - private readonly ILifecycleService _lifecycleService; - private readonly IDeploymentInfoProvider _deploymentInfoProvider; - - public SystemModule(IAppFolderInfo appFolderInfo, - IRuntimeInfo runtimeInfo, - IPlatformInfo platformInfo, - IOsInfo osInfo, - IRouteCacheProvider routeCacheProvider, - IConfigFileProvider configFileProvider, - IMainDatabase database, - ILifecycleService lifecycleService, - IDeploymentInfoProvider deploymentInfoProvider) - : base("system") - { - _appFolderInfo = appFolderInfo; - _runtimeInfo = runtimeInfo; - _platformInfo = platformInfo; - _osInfo = osInfo; - _routeCacheProvider = routeCacheProvider; - _configFileProvider = configFileProvider; - _database = database; - _lifecycleService = lifecycleService; - _deploymentInfoProvider = deploymentInfoProvider; - Get("/status", x => GetStatus()); - Get("/routes", x => GetRoutes()); - Post("/shutdown", x => Shutdown()); - Post("/restart", x => Restart()); - } - - private object GetStatus() - { - return new - { - AppName = BuildInfo.AppName, - InstanceName = _configFileProvider.InstanceName, - Version = BuildInfo.Version.ToString(), - BuildTime = BuildInfo.BuildDateTime, - IsDebug = BuildInfo.IsDebug, - IsProduction = RuntimeInfo.IsProduction, - IsAdmin = _runtimeInfo.IsAdmin, - IsUserInteractive = RuntimeInfo.IsUserInteractive, - StartupPath = _appFolderInfo.StartUpFolder, - AppData = _appFolderInfo.GetAppDataPath(), - OsName = _osInfo.Name, - OsVersion = _osInfo.Version, - IsNetCore = PlatformInfo.IsNetCore, - IsLinux = OsInfo.IsLinux, - IsOsx = OsInfo.IsOsx, - IsWindows = OsInfo.IsWindows, - IsDocker = _osInfo.IsDocker, - Mode = _runtimeInfo.Mode, - Branch = _configFileProvider.Branch, - Authentication = _configFileProvider.AuthenticationMethod, - SqliteVersion = _database.Version, - UrlBase = _configFileProvider.UrlBase, - RuntimeVersion = _platformInfo.Version, - RuntimeName = PlatformInfo.Platform, - StartTime = _runtimeInfo.StartTime, - PackageVersion = _deploymentInfoProvider.PackageVersion, - PackageAuthor = _deploymentInfoProvider.PackageAuthor, - PackageUpdateMechanism = _deploymentInfoProvider.PackageUpdateMechanism, - PackageUpdateMechanismMessage = _deploymentInfoProvider.PackageUpdateMechanismMessage - }; - } - - private object GetRoutes() - { - return _routeCacheProvider.GetCache().Values; - } - - private object Shutdown() - { - Task.Factory.StartNew(() => _lifecycleService.Shutdown()); - return new { ShuttingDown = true }; - } - - private object Restart() - { - Task.Factory.StartNew(() => _lifecycleService.Restart()); - return new { Restarting = true }; - } - } -} diff --git a/src/Sonarr.Api.V3/System/Tasks/TaskModule.cs b/src/Sonarr.Api.V3/System/Tasks/TaskController.cs similarity index 58% rename from src/Sonarr.Api.V3/System/Tasks/TaskModule.cs rename to src/Sonarr.Api.V3/System/Tasks/TaskController.cs index 1081feb62..2ea015e9e 100644 --- a/src/Sonarr.Api.V3/System/Tasks/TaskModule.cs +++ b/src/Sonarr.Api.V3/System/Tasks/TaskController.cs @@ -1,28 +1,29 @@ using System.Collections.Generic; using System.Linq; -using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Jobs; using NzbDrone.Core.Messaging.Events; using NzbDrone.SignalR; using Sonarr.Http; +using Sonarr.Http.REST; namespace Sonarr.Api.V3.System.Tasks { - public class TaskModule : SonarrRestModuleWithSignalR, IHandle + [V3ApiController("system/task")] + public class TaskController : RestControllerWithSignalR, IHandle { private readonly ITaskManager _taskManager; - public TaskModule(ITaskManager taskManager, IBroadcastSignalRMessage broadcastSignalRMessage) - : base(broadcastSignalRMessage, "system/task") + public TaskController(ITaskManager taskManager, IBroadcastSignalRMessage broadcastSignalRMessage) + : base(broadcastSignalRMessage) { _taskManager = taskManager; - GetResourceAll = GetAll; - GetResourceById = GetTask; } - private List GetAll() + [HttpGet] + public List GetAll() { return _taskManager.GetAll() .Select(ConvertToResource) @@ -30,7 +31,7 @@ namespace Sonarr.Api.V3.System.Tasks .ToList(); } - private TaskResource GetTask(int id) + protected override TaskResource GetResourceById(int id) { var task = _taskManager.GetAll() .SingleOrDefault(t => t.Id == id); @@ -48,16 +49,17 @@ namespace Sonarr.Api.V3.System.Tasks var taskName = scheduledTask.TypeName.Split('.').Last().Replace("Command", ""); return new TaskResource - { - Id = scheduledTask.Id, - Name = taskName.SplitCamelCase(), - TaskName = taskName, - Interval = scheduledTask.Interval, - LastExecution = scheduledTask.LastExecution, - NextExecution = scheduledTask.LastExecution.AddMinutes(scheduledTask.Interval) - }; + { + Id = scheduledTask.Id, + Name = taskName.SplitCamelCase(), + TaskName = taskName, + Interval = scheduledTask.Interval, + LastExecution = scheduledTask.LastExecution, + NextExecution = scheduledTask.LastExecution.AddMinutes(scheduledTask.Interval) + }; } + [NonAction] public void Handle(CommandExecutedEvent message) { BroadcastResourceChange(ModelAction.Sync); diff --git a/src/Sonarr.Api.V3/Tags/TagController.cs b/src/Sonarr.Api.V3/Tags/TagController.cs new file mode 100644 index 000000000..b267c0d13 --- /dev/null +++ b/src/Sonarr.Api.V3/Tags/TagController.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Tags; +using NzbDrone.SignalR; +using Sonarr.Http; +using Sonarr.Http.REST; +using Sonarr.Http.REST.Attributes; + +namespace Sonarr.Api.V3.Tags +{ + [V3ApiController] + public class TagController : RestControllerWithSignalR, IHandle + { + private readonly ITagService _tagService; + + public TagController(IBroadcastSignalRMessage signalRBroadcaster, + ITagService tagService) + : base(signalRBroadcaster) + { + _tagService = tagService; + } + + protected override TagResource GetResourceById(int id) + { + return _tagService.GetTag(id).ToResource(); + } + + [HttpGet] + public List GetAll() + { + return _tagService.All().ToResource(); + } + + [RestPostById] + public ActionResult Create(TagResource resource) + { + return Created(_tagService.Add(resource.ToModel()).Id); + } + + [RestPutById] + public ActionResult Update(TagResource resource) + { + _tagService.Update(resource.ToModel()); + return Accepted(resource.Id); + } + + [RestDeleteById] + public void DeleteTag(int id) + { + _tagService.Delete(id); + } + + [NonAction] + public void Handle(TagsUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Sync); + } + } +} diff --git a/src/Sonarr.Api.V3/Tags/TagDetailsModule.cs b/src/Sonarr.Api.V3/Tags/TagDetailsController.cs similarity index 51% rename from src/Sonarr.Api.V3/Tags/TagDetailsModule.cs rename to src/Sonarr.Api.V3/Tags/TagDetailsController.cs index 16a1bc639..733801090 100644 --- a/src/Sonarr.Api.V3/Tags/TagDetailsModule.cs +++ b/src/Sonarr.Api.V3/Tags/TagDetailsController.cs @@ -1,28 +1,28 @@ using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Tags; using Sonarr.Http; +using Sonarr.Http.REST; namespace Sonarr.Api.V3.Tags { - public class TagDetailsModule : SonarrRestModule + [V3ApiController("tag/detail")] + public class TagDetailsController : RestController { private readonly ITagService _tagService; - public TagDetailsModule(ITagService tagService) - : base("/tag/detail") + public TagDetailsController(ITagService tagService) { _tagService = tagService; - - GetResourceById = GetTagDetails; - GetResourceAll = GetAll; } - private TagDetailsResource GetTagDetails(int id) + protected override TagDetailsResource GetResourceById(int id) { return _tagService.Details(id).ToResource(); } - private List GetAll() + [HttpGet] + public List GetAll() { return _tagService.Details().ToResource(); } diff --git a/src/Sonarr.Api.V3/Tags/TagModule.cs b/src/Sonarr.Api.V3/Tags/TagModule.cs deleted file mode 100644 index a0b315d7e..000000000 --- a/src/Sonarr.Api.V3/Tags/TagModule.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Core.Datastore.Events; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Tags; -using NzbDrone.SignalR; -using Sonarr.Http; - -namespace Sonarr.Api.V3.Tags -{ - public class TagModule : SonarrRestModuleWithSignalR, IHandle - { - private readonly ITagService _tagService; - - public TagModule(IBroadcastSignalRMessage signalRBroadcaster, - ITagService tagService) - : base(signalRBroadcaster) - { - _tagService = tagService; - - GetResourceById = GetTag; - GetResourceAll = GetAll; - CreateResource = Create; - UpdateResource = Update; - DeleteResource = DeleteTag; - } - - private TagResource GetTag(int id) - { - return _tagService.GetTag(id).ToResource(); - } - - private List GetAll() - { - return _tagService.All().ToResource(); - } - - private int Create(TagResource resource) - { - return _tagService.Add(resource.ToModel()).Id; - } - - private void Update(TagResource resource) - { - _tagService.Update(resource.ToModel()); - } - - private void DeleteTag(int id) - { - _tagService.Delete(id); - } - - public void Handle(TagsUpdatedEvent message) - { - BroadcastResourceChange(ModelAction.Sync); - } - } -} diff --git a/src/Sonarr.Api.V3/Update/UpdateModule.cs b/src/Sonarr.Api.V3/Update/UpdateController.cs similarity index 84% rename from src/Sonarr.Api.V3/Update/UpdateModule.cs rename to src/Sonarr.Api.V3/Update/UpdateController.cs index b83c120f1..e1028a6a5 100644 --- a/src/Sonarr.Api.V3/Update/UpdateModule.cs +++ b/src/Sonarr.Api.V3/Update/UpdateController.cs @@ -1,5 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; using NzbDrone.Core.Update; @@ -8,19 +9,20 @@ using Sonarr.Http; namespace Sonarr.Api.V3.Update { - public class UpdateModule : SonarrRestModule + [V3ApiController] + public class UpdateController : Controller { private readonly IRecentUpdateProvider _recentUpdateProvider; private readonly IUpdateHistoryService _updateHistoryService; - public UpdateModule(IRecentUpdateProvider recentUpdateProvider, IUpdateHistoryService updateHistoryService) + public UpdateController(IRecentUpdateProvider recentUpdateProvider, IUpdateHistoryService updateHistoryService) { _recentUpdateProvider = recentUpdateProvider; _updateHistoryService = updateHistoryService; - GetResourceAll = GetRecentUpdates; } - private List GetRecentUpdates() + [HttpGet] + public List GetRecentUpdates() { var resources = _recentUpdateProvider.GetRecentUpdatePackages() .OrderByDescending(u => u.Version) diff --git a/src/Sonarr.Api.V3/Wanted/CutoffModule.cs b/src/Sonarr.Api.V3/Wanted/CutoffController.cs similarity index 65% rename from src/Sonarr.Api.V3/Wanted/CutoffModule.cs rename to src/Sonarr.Api.V3/Wanted/CutoffController.cs index d496e8ff3..d50418692 100644 --- a/src/Sonarr.Api.V3/Wanted/CutoffModule.cs +++ b/src/Sonarr.Api.V3/Wanted/CutoffController.cs @@ -1,6 +1,6 @@ using System.Linq; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Datastore; -using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Tv; using NzbDrone.SignalR; @@ -10,23 +10,25 @@ using Sonarr.Http.Extensions; namespace Sonarr.Api.V3.Wanted { - public class CutoffModule : EpisodeModuleWithSignalR + [V3ApiController("wanted/cutoff")] + public class CutoffController : EpisodeControllerWithSignalR { private readonly IEpisodeCutoffService _episodeCutoffService; - public CutoffModule(IEpisodeCutoffService episodeCutoffService, + public CutoffController(IEpisodeCutoffService episodeCutoffService, IEpisodeService episodeService, ISeriesService seriesService, IUpgradableSpecification upgradableSpecification, IBroadcastSignalRMessage signalRBroadcaster) - : base(episodeService, seriesService, upgradableSpecification, signalRBroadcaster, "wanted/cutoff") + : base(episodeService, seriesService, upgradableSpecification, signalRBroadcaster) { _episodeCutoffService = episodeCutoffService; - GetResourcePaged = GetCutoffUnmetEpisodes; } - private PagingResource GetCutoffUnmetEpisodes(PagingResource pagingResource) + [HttpGet] + public PagingResource GetCutoffUnmetEpisodes(bool includeSeries = false, bool includeEpisodeFile = false, bool includeImages = false) { + var pagingResource = Request.ReadPagingResourceFromRequest(); var pagingSpec = new PagingSpec { Page = pagingResource.Page, @@ -35,9 +37,6 @@ namespace Sonarr.Api.V3.Wanted SortDirection = pagingResource.SortDirection }; - var includeSeries = Request.GetBooleanQueryParameter("includeSeries"); - var includeEpisodeFile = Request.GetBooleanQueryParameter("includeEpisodeFile"); - var includeImages = Request.GetBooleanQueryParameter("includeImages"); var filter = pagingResource.Filters.FirstOrDefault(f => f.Key == "monitored"); if (filter != null && filter.Value == "false") @@ -49,7 +48,7 @@ namespace Sonarr.Api.V3.Wanted pagingSpec.FilterExpressions.Add(v => v.Monitored == true && v.Series.Monitored == true); } - var resource = ApplyToPage(_episodeCutoffService.EpisodesWhereCutoffUnmet, pagingSpec, v => MapToResource(v, includeSeries, includeEpisodeFile, includeImages)); + var resource = pagingSpec.ApplyToPage(_episodeCutoffService.EpisodesWhereCutoffUnmet, v => MapToResource(v, includeSeries, includeEpisodeFile, includeImages)); return resource; } diff --git a/src/Sonarr.Api.V3/Wanted/MissingModule.cs b/src/Sonarr.Api.V3/Wanted/MissingController.cs similarity index 55% rename from src/Sonarr.Api.V3/Wanted/MissingModule.cs rename to src/Sonarr.Api.V3/Wanted/MissingController.cs index b8313adb1..e1df1b603 100644 --- a/src/Sonarr.Api.V3/Wanted/MissingModule.cs +++ b/src/Sonarr.Api.V3/Wanted/MissingController.cs @@ -1,4 +1,5 @@ using System.Linq; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Datastore; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine.Specifications; @@ -10,22 +11,29 @@ using Sonarr.Http.Extensions; namespace Sonarr.Api.V3.Wanted { - public class MissingModule : EpisodeModuleWithSignalR + [V3ApiController("wanted/missing")] + public class MissingController : EpisodeControllerWithSignalR { - public MissingModule(IEpisodeService episodeService, + public MissingController(IEpisodeService episodeService, ISeriesService seriesService, IUpgradableSpecification upgradableSpecification, IBroadcastSignalRMessage signalRBroadcaster) - : base(episodeService, seriesService, upgradableSpecification, signalRBroadcaster, "wanted/missing") + : base(episodeService, seriesService, upgradableSpecification, signalRBroadcaster) { - GetResourcePaged = GetMissingEpisodes; } - private PagingResource GetMissingEpisodes(PagingResource pagingResource) + [HttpGet] + public PagingResource GetMissingEpisodes(bool includeSeries = false, bool includeImages = false) { - var pagingSpec = pagingResource.MapToPagingSpec("airDateUtc", SortDirection.Descending); - var includeSeries = Request.GetBooleanQueryParameter("includeSeries"); - var includeImages = Request.GetBooleanQueryParameter("includeImages"); + var pagingResource = Request.ReadPagingResourceFromRequest(); + var pagingSpec = new PagingSpec + { + Page = pagingResource.Page, + PageSize = pagingResource.PageSize, + SortKey = pagingResource.SortKey, + SortDirection = pagingResource.SortDirection + }; + var monitoredFilter = pagingResource.Filters.FirstOrDefault(f => f.Key == "monitored"); if (monitoredFilter != null && monitoredFilter.Value == "false") @@ -37,7 +45,7 @@ namespace Sonarr.Api.V3.Wanted pagingSpec.FilterExpressions.Add(v => v.Monitored == true && v.Series.Monitored == true); } - var resource = ApplyToPage(_episodeService.EpisodesWithoutFiles, pagingSpec, v => MapToResource(v, includeSeries, false, includeImages)); + var resource = pagingSpec.ApplyToPage(_episodeService.EpisodesWithoutFiles, v => MapToResource(v, includeSeries, false, includeImages)); return resource; } diff --git a/src/Sonarr.Http/Authentication/ApiKeyAuthenticationHandler.cs b/src/Sonarr.Http/Authentication/ApiKeyAuthenticationHandler.cs new file mode 100644 index 000000000..248390b92 --- /dev/null +++ b/src/Sonarr.Http/Authentication/ApiKeyAuthenticationHandler.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NzbDrone.Core.Configuration; + +namespace Sonarr.Http.Authentication +{ + public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions + { + public const string DefaultScheme = "API Key"; + public string Scheme => DefaultScheme; + public string AuthenticationType = DefaultScheme; + + public string HeaderName { get; set; } + public string QueryName { get; set; } + } + + public class ApiKeyAuthenticationHandler : AuthenticationHandler + { + private readonly string _apiKey; + + public ApiKeyAuthenticationHandler(IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock, + IConfigFileProvider config) + : base(options, logger, encoder, clock) + { + _apiKey = config.ApiKey; + } + + private string ParseApiKey() + { + // Try query parameter + if (Request.Query.TryGetValue(Options.QueryName, out var value)) + { + return value.FirstOrDefault(); + } + + // No ApiKey query parameter found try headers + if (Request.Headers.TryGetValue(Options.HeaderName, out var headerValue)) + { + return headerValue.FirstOrDefault(); + } + + return Request.Headers["Authorization"].FirstOrDefault()?.Replace("Bearer ", ""); + } + + protected override Task HandleAuthenticateAsync() + { + var providedApiKey = ParseApiKey(); + + if (string.IsNullOrWhiteSpace(providedApiKey)) + { + return Task.FromResult(AuthenticateResult.NoResult()); + } + + if (_apiKey == providedApiKey) + { + var claims = new List + { + new Claim("ApiKey", "true") + }; + + var identity = new ClaimsIdentity(claims, Options.AuthenticationType); + var identities = new List { identity }; + var principal = new ClaimsPrincipal(identities); + var ticket = new AuthenticationTicket(principal, Options.Scheme); + + return Task.FromResult(AuthenticateResult.Success(ticket)); + } + + return Task.FromResult(AuthenticateResult.NoResult()); + } + + protected override Task HandleChallengeAsync(AuthenticationProperties properties) + { + Response.StatusCode = 401; + return Task.CompletedTask; + } + + protected override Task HandleForbiddenAsync(AuthenticationProperties properties) + { + Response.StatusCode = 403; + return Task.CompletedTask; + } + } +} diff --git a/src/Sonarr.Http/Authentication/AuthenticationBuilderExtensions.cs b/src/Sonarr.Http/Authentication/AuthenticationBuilderExtensions.cs new file mode 100644 index 000000000..f270a70aa --- /dev/null +++ b/src/Sonarr.Http/Authentication/AuthenticationBuilderExtensions.cs @@ -0,0 +1,49 @@ +using System; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.DependencyInjection; +using NzbDrone.Core.Authentication; + +namespace Sonarr.Http.Authentication +{ + public static class AuthenticationBuilderExtensions + { + public static AuthenticationBuilder AddApiKey(this AuthenticationBuilder authenticationBuilder, string name, Action options) + { + return authenticationBuilder.AddScheme(name, options); + } + + public static AuthenticationBuilder AddBasic(this AuthenticationBuilder authenticationBuilder, string name) + { + return authenticationBuilder.AddScheme(name, options => { }); + } + + public static AuthenticationBuilder AddNone(this AuthenticationBuilder authenticationBuilder, string name) + { + return authenticationBuilder.AddScheme(name, options => { }); + } + + public static AuthenticationBuilder AddAppAuthentication(this IServiceCollection services) + { + return services.AddAuthentication() + .AddNone(AuthenticationType.None.ToString()) + .AddBasic(AuthenticationType.Basic.ToString()) + .AddCookie(AuthenticationType.Forms.ToString(), options => + { + options.Cookie.Name = "SonarrAuth"; + options.AccessDeniedPath = "/login?loginFailed=true"; + options.LoginPath = "/login"; + options.ExpireTimeSpan = TimeSpan.FromDays(7); + }) + .AddApiKey("API", options => + { + options.HeaderName = "X-Api-Key"; + options.QueryName = "apikey"; + }) + .AddApiKey("SignalR", options => + { + options.HeaderName = "X-Api-Key"; + options.QueryName = "access_token"; + }); + } + } +} diff --git a/src/Sonarr.Http/Authentication/AuthenticationController.cs b/src/Sonarr.Http/Authentication/AuthenticationController.cs new file mode 100644 index 000000000..79edc7567 --- /dev/null +++ b/src/Sonarr.Http/Authentication/AuthenticationController.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Authentication; +using NzbDrone.Core.Configuration; + +namespace Sonarr.Http.Authentication +{ + [AllowAnonymous] + [ApiController] + public class AuthenticationController : Controller + { + private readonly IAuthenticationService _authService; + private readonly IConfigFileProvider _configFileProvider; + + public AuthenticationController(IAuthenticationService authService, IConfigFileProvider configFileProvider) + { + _authService = authService; + _configFileProvider = configFileProvider; + } + + [HttpPost("login")] + public async Task Login([FromForm] LoginResource resource, [FromQuery] string returnUrl = null) + { + var user = _authService.Login(HttpContext.Request, resource.Username, resource.Password); + + if (user == null) + { + return Redirect($"~/login?returnUrl={returnUrl}&loginFailed=true"); + } + + var claims = new List + { + new Claim("user", user.Username), + new Claim("identifier", user.Identifier.ToString()), + new Claim("AuthType", AuthenticationType.Forms.ToString()) + }; + + var authProperties = new AuthenticationProperties + { + IsPersistent = resource.RememberMe == "on" + }; + + await HttpContext.SignInAsync(AuthenticationType.Forms.ToString(), new ClaimsPrincipal(new ClaimsIdentity(claims, "Cookies", "user", "identifier")), authProperties); + + return Redirect(_configFileProvider.UrlBase + "/"); + } + + [HttpGet("logout")] + public async Task Logout() + { + _authService.Logout(HttpContext); + await HttpContext.SignOutAsync(AuthenticationType.Forms.ToString()); + return Redirect(_configFileProvider.UrlBase + "/"); + } + } +} diff --git a/src/Sonarr.Http/Authentication/AuthenticationModule.cs b/src/Sonarr.Http/Authentication/AuthenticationModule.cs deleted file mode 100644 index 36c5db387..000000000 --- a/src/Sonarr.Http/Authentication/AuthenticationModule.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using Nancy; -using Nancy.Authentication.Forms; -using Nancy.Extensions; -using Nancy.ModelBinding; -using NzbDrone.Core.Configuration; - -namespace Sonarr.Http.Authentication -{ - public class AuthenticationModule : NancyModule - { - private readonly IAuthenticationService _authService; - private readonly IConfigFileProvider _configFileProvider; - - public AuthenticationModule(IAuthenticationService authService, IConfigFileProvider configFileProvider) - { - _authService = authService; - _configFileProvider = configFileProvider; - Post("/login", x => Login(this.Bind())); - Get("/logout", x => Logout()); - } - - private Response Login(LoginResource resource) - { - var user = _authService.Login(Context, resource.Username, resource.Password); - - if (user == null) - { - var returnUrl = (string)Request.Query.returnUrl; - return Context.GetRedirect($"~/login?returnUrl={returnUrl}&loginFailed=true"); - } - - DateTime? expiry = null; - - if (resource.RememberMe) - { - expiry = DateTime.UtcNow.AddDays(7); - } - - return this.LoginAndRedirect(user.Identifier, expiry, _configFileProvider.UrlBase + "/"); - } - - private Response Logout() - { - _authService.Logout(Context); - - return this.LogoutAndRedirect(_configFileProvider.UrlBase + "/"); - } - } -} diff --git a/src/Sonarr.Http/Authentication/AuthenticationService.cs b/src/Sonarr.Http/Authentication/AuthenticationService.cs index 8d6003155..374994aaa 100644 --- a/src/Sonarr.Http/Authentication/AuthenticationService.cs +++ b/src/Sonarr.Http/Authentication/AuthenticationService.cs @@ -1,28 +1,16 @@ -using System; -using System.Linq; -using System.Net; -using System.Security.Claims; -using System.Security.Principal; -using Nancy; -using Nancy.Authentication.Basic; -using Nancy.Authentication.Forms; -using Nancy.Routing.Trie.Nodes; +using Microsoft.AspNetCore.Http; using NLog; -using NzbDrone.Common.Extensions; using NzbDrone.Core.Authentication; using NzbDrone.Core.Configuration; using Sonarr.Http.Extensions; namespace Sonarr.Http.Authentication { - public interface IAuthenticationService : IUserValidator, IUserMapper + public interface IAuthenticationService { - void SetContext(NancyContext context); - - void LogUnauthorized(NancyContext context); - User Login(NancyContext context, string username, string password); - void Logout(NancyContext context); - bool IsAuthenticated(NancyContext context); + void LogUnauthorized(HttpRequest context); + User Login(HttpRequest request, string username, string password); + void Logout(HttpContext context); } public class AuthenticationService : IAuthenticationService @@ -34,9 +22,6 @@ namespace Sonarr.Http.Authentication private static string API_KEY; private static AuthenticationType AUTH_METHOD; - [ThreadStatic] - private static NancyContext _context; - public AuthenticationService(IConfigFileProvider configFileProvider, IUserService userService) { _userService = userService; @@ -44,13 +29,7 @@ namespace Sonarr.Http.Authentication AUTH_METHOD = configFileProvider.AuthenticationMethod; } - public void SetContext(NancyContext context) - { - // Validate and GetUserIdentifier don't have access to the NancyContext so get it from the pipeline earlier - _context = context; - } - - public User Login(NancyContext context, string username, string password) + public User Login(HttpRequest request, string username, string password) { if (AUTH_METHOD == AuthenticationType.None) { @@ -61,189 +40,50 @@ namespace Sonarr.Http.Authentication if (user != null) { - LogSuccess(context, username); + LogSuccess(request, username); return user; } - LogFailure(context, username); + LogFailure(request, username); return null; } - public void Logout(NancyContext context) + public void Logout(HttpContext context) { if (AUTH_METHOD == AuthenticationType.None) { return; } - if (context.CurrentUser != null) + if (context.User != null) { - LogLogout(context, context.CurrentUser.Identity.Name); + LogLogout(context.Request, context.User.Identity.Name); } } - public ClaimsPrincipal Validate(string username, string password) + public void LogUnauthorized(HttpRequest context) { - if (AUTH_METHOD == AuthenticationType.None) - { - return new ClaimsPrincipal(new GenericIdentity(AnonymousUser)); - } - - var user = _userService.FindUser(username, password); - - if (user != null) - { - if (AUTH_METHOD != AuthenticationType.Basic) - { - // Don't log success for basic auth - LogSuccess(_context, username); - } - - return new ClaimsPrincipal(new GenericIdentity(user.Username)); - } - - LogFailure(_context, username); - - return null; + _authLogger.Info("Auth-Unauthorized ip {0} url '{1}'", context.GetRemoteIP(), context.Path); } - public ClaimsPrincipal GetUserFromIdentifier(Guid identifier, NancyContext context) - { - if (AUTH_METHOD == AuthenticationType.None) - { - return new ClaimsPrincipal(new GenericIdentity(AnonymousUser)); - } - - var user = _userService.FindUser(identifier); - - if (user != null) - { - return new ClaimsPrincipal(new GenericIdentity(user.Username)); - } - - LogInvalidated(_context); - - return null; - } - - public bool IsAuthenticated(NancyContext context) - { - var apiKey = GetApiKey(context); - - if (context.Request.IsApiRequest()) - { - return ValidApiKey(apiKey); - } - - if (AUTH_METHOD == AuthenticationType.None) - { - return true; - } - - if (context.Request.IsFeedRequest()) - { - if (ValidUser(context) || ValidApiKey(apiKey)) - { - return true; - } - - return false; - } - - if (context.Request.IsLoginRequest()) - { - return true; - } - - if (context.Request.IsContentRequest()) - { - return true; - } - - if (context.Request.IsBundledJsRequest()) - { - return true; - } - - if (context.Request.IsFavIconRequest()) - { - return true; - } - - if (context.Request.IsPingRequest()) - { - return true; - } - - if (ValidUser(context)) - { - return true; - } - - return false; - } - - private bool ValidUser(NancyContext context) - { - if (context.CurrentUser != null) - { - return true; - } - - return false; - } - - private bool ValidApiKey(string apiKey) - { - if (API_KEY.Equals(apiKey)) - { - return true; - } - - return false; - } - - private string GetApiKey(NancyContext context) - { - var apiKeyHeader = context.Request.Headers["X-Api-Key"].FirstOrDefault(); - var apiKeyQueryString = context.Request.Query["ApiKey"]; - - if (!apiKeyHeader.IsNullOrWhiteSpace()) - { - return apiKeyHeader; - } - - if (apiKeyQueryString.HasValue) - { - return apiKeyQueryString.Value; - } - - return context.Request.Headers.Authorization; - } - - public void LogUnauthorized(NancyContext context) - { - _authLogger.Info("Auth-Unauthorized ip {0} url '{1}'", context.GetRemoteIP(), context.Request.Url.ToString()); - } - - private void LogInvalidated(NancyContext context) + private void LogInvalidated(HttpRequest context) { _authLogger.Info("Auth-Invalidated ip {0}", context.GetRemoteIP()); } - private void LogFailure(NancyContext context, string username) + private void LogFailure(HttpRequest context, string username) { _authLogger.Warn("Auth-Failure ip {0} username '{1}'", context.GetRemoteIP(), username); } - private void LogSuccess(NancyContext context, string username) + private void LogSuccess(HttpRequest context, string username) { _authLogger.Info("Auth-Success ip {0} username '{1}'", context.GetRemoteIP(), username); } - private void LogLogout(NancyContext context, string username) + private void LogLogout(HttpRequest context, string username) { _authLogger.Info("Auth-Logout ip {0} username '{1}'", context.GetRemoteIP(), username); } diff --git a/src/Sonarr.Http/Authentication/BasicAuthenticationHandler.cs b/src/Sonarr.Http/Authentication/BasicAuthenticationHandler.cs new file mode 100644 index 000000000..41dc5aad2 --- /dev/null +++ b/src/Sonarr.Http/Authentication/BasicAuthenticationHandler.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Text; +using System.Text.Encodings.Web; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Authentication; + +namespace Sonarr.Http.Authentication +{ + public class BasicAuthenticationHandler : AuthenticationHandler + { + private readonly IAuthenticationService _authService; + + public BasicAuthenticationHandler(IAuthenticationService authService, + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock) + : base(options, logger, encoder, clock) + { + _authService = authService; + } + + protected override Task HandleAuthenticateAsync() + { + if (!Request.Headers.ContainsKey("Authorization")) + { + return Task.FromResult(AuthenticateResult.Fail("Authorization header missing.")); + } + + // Get authorization key + var authorizationHeader = Request.Headers["Authorization"].ToString(); + var authHeaderRegex = new Regex(@"Basic (.*)"); + + if (!authHeaderRegex.IsMatch(authorizationHeader)) + { + return Task.FromResult(AuthenticateResult.Fail("Authorization code not formatted properly.")); + } + + var authBase64 = Encoding.UTF8.GetString(Convert.FromBase64String(authHeaderRegex.Replace(authorizationHeader, "$1"))); + var authSplit = authBase64.Split(':', 2); + var authUsername = authSplit[0]; + var authPassword = authSplit.Length > 1 ? authSplit[1] : throw new Exception("Unable to get password"); + + var user = _authService.Login(Request, authUsername, authPassword); + + if (user == null) + { + return Task.FromResult(AuthenticateResult.Fail("The username or password is not correct.")); + } + + var claims = new List + { + new Claim("user", user.Username), + new Claim("identifier", user.Identifier.ToString()), + new Claim("AuthType", AuthenticationType.Basic.ToString()) + }; + + var identity = new ClaimsIdentity(claims, "Basic", "user", "identifier"); + var principal = new ClaimsPrincipal(identity); + var ticket = new AuthenticationTicket(principal, "Basic"); + + return Task.FromResult(AuthenticateResult.Success(ticket)); + } + + protected override Task HandleChallengeAsync(AuthenticationProperties properties) + { + Response.Headers.Add("WWW-Authenticate", $"Basic realm=\"{BuildInfo.AppName}\""); + Response.StatusCode = 401; + return Task.CompletedTask; + } + + protected override Task HandleForbiddenAsync(AuthenticationProperties properties) + { + Response.StatusCode = 403; + return Task.CompletedTask; + } + } +} diff --git a/src/Sonarr.Http/Authentication/EnableAuthInNancy.cs b/src/Sonarr.Http/Authentication/EnableAuthInNancy.cs deleted file mode 100644 index a4e60911f..000000000 --- a/src/Sonarr.Http/Authentication/EnableAuthInNancy.cs +++ /dev/null @@ -1,141 +0,0 @@ -using System; -using System.Text; -using Nancy; -using Nancy.Authentication.Basic; -using Nancy.Authentication.Forms; -using Nancy.Bootstrapper; -using Nancy.Cryptography; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Authentication; -using NzbDrone.Core.Configuration; -using Sonarr.Http.Extensions; -using Sonarr.Http.Extensions.Pipelines; - -namespace Sonarr.Http.Authentication -{ - public class EnableAuthInNancy : IRegisterNancyPipeline - { - private readonly IAuthenticationService _authenticationService; - private readonly IConfigService _configService; - private readonly IConfigFileProvider _configFileProvider; - private FormsAuthenticationConfiguration _formsAuthConfig; - - public EnableAuthInNancy(IAuthenticationService authenticationService, - IConfigService configService, - IConfigFileProvider configFileProvider) - { - _authenticationService = authenticationService; - _configService = configService; - _configFileProvider = configFileProvider; - } - - public int Order => 10; - - public void Register(IPipelines pipelines) - { - if (_configFileProvider.AuthenticationMethod == AuthenticationType.Forms) - { - RegisterFormsAuth(pipelines); - pipelines.AfterRequest.AddItemToEndOfPipeline((Action)SlidingAuthenticationForFormsAuth); - } - else if (_configFileProvider.AuthenticationMethod == AuthenticationType.Basic) - { - pipelines.EnableBasicAuthentication(new BasicAuthenticationConfiguration(_authenticationService, "Sonarr")); - pipelines.BeforeRequest.AddItemToStartOfPipeline(CaptureContext); - } - - pipelines.BeforeRequest.AddItemToEndOfPipeline((Func)RequiresAuthentication); - pipelines.AfterRequest.AddItemToEndOfPipeline((Action)RemoveLoginHooksForApiCalls); - } - - private Response CaptureContext(NancyContext context) - { - _authenticationService.SetContext(context); - - return null; - } - - private Response RequiresAuthentication(NancyContext context) - { - Response response = null; - - if (!_authenticationService.IsAuthenticated(context)) - { - _authenticationService.LogUnauthorized(context); - response = new Response { StatusCode = HttpStatusCode.Unauthorized }; - } - - return response; - } - - private void RegisterFormsAuth(IPipelines pipelines) - { - FormsAuthentication.FormsAuthenticationCookieName = "SonarrAuth"; - - var cryptographyConfiguration = new CryptographyConfiguration( - new AesEncryptionProvider(new PassphraseKeyGenerator(_configService.RijndaelPassphrase, Encoding.ASCII.GetBytes(_configService.RijndaelSalt))), - new DefaultHmacProvider(new PassphraseKeyGenerator(_configService.HmacPassphrase, Encoding.ASCII.GetBytes(_configService.HmacSalt)))); - - _formsAuthConfig = new FormsAuthenticationConfiguration - { - RedirectUrl = _configFileProvider.UrlBase + "/login", - UserMapper = _authenticationService, - Path = GetCookiePath(), - CryptographyConfiguration = cryptographyConfiguration - }; - - FormsAuthentication.Enable(pipelines, _formsAuthConfig); - } - - private void RemoveLoginHooksForApiCalls(NancyContext context) - { - if (context.Request.IsApiRequest()) - { - if ((context.Response.StatusCode == HttpStatusCode.SeeOther && - context.Response.Headers["Location"].StartsWith($"{_configFileProvider.UrlBase}/login", StringComparison.InvariantCultureIgnoreCase)) || - context.Response.StatusCode == HttpStatusCode.Unauthorized) - { - context.Response = new { Error = "Unauthorized" }.AsResponse(context, HttpStatusCode.Unauthorized); - } - } - } - - private void SlidingAuthenticationForFormsAuth(NancyContext context) - { - if (context.CurrentUser == null) - { - return; - } - - var formsAuthCookieName = FormsAuthentication.FormsAuthenticationCookieName; - - if (!context.Request.Path.Equals("/logout") && - context.Request.Cookies.ContainsKey(formsAuthCookieName)) - { - var formsAuthCookieValue = context.Request.Cookies[formsAuthCookieName]; - - if (FormsAuthentication.DecryptAndValidateAuthenticationCookie(formsAuthCookieValue, _formsAuthConfig).IsNotNullOrWhiteSpace()) - { - var formsAuthCookie = new SonarrNancyCookie(formsAuthCookieName, formsAuthCookieValue, true, false, DateTime.UtcNow.AddDays(7)) - { - Path = GetCookiePath() - }; - - context.Response.WithCookie(formsAuthCookie); - } - } - } - - private string GetCookiePath() - { - var urlBase = _configFileProvider.UrlBase; - - if (urlBase.IsNullOrWhiteSpace()) - { - return "/"; - } - - return urlBase; - } - } -} diff --git a/src/Sonarr.Http/Authentication/LoginResource.cs b/src/Sonarr.Http/Authentication/LoginResource.cs index 0a725541c..d71ef233f 100644 --- a/src/Sonarr.Http/Authentication/LoginResource.cs +++ b/src/Sonarr.Http/Authentication/LoginResource.cs @@ -4,6 +4,6 @@ { public string Username { get; set; } public string Password { get; set; } - public bool RememberMe { get; set; } + public string RememberMe { get; set; } } } diff --git a/src/Sonarr.Http/Authentication/NoAuthenticationHandler.cs b/src/Sonarr.Http/Authentication/NoAuthenticationHandler.cs new file mode 100644 index 000000000..da1c8b04b --- /dev/null +++ b/src/Sonarr.Http/Authentication/NoAuthenticationHandler.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NzbDrone.Core.Authentication; + +namespace Sonarr.Http.Authentication +{ + public class NoAuthenticationHandler : AuthenticationHandler + { + public NoAuthenticationHandler(IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock) + : base(options, logger, encoder, clock) + { + } + + protected override Task HandleAuthenticateAsync() + { + var claims = new List + { + new Claim("user", "Anonymous"), + new Claim("AuthType", AuthenticationType.None.ToString()) + }; + + var identity = new ClaimsIdentity(claims, "NoAuth", "user", "identifier"); + var principal = new ClaimsPrincipal(identity); + var ticket = new AuthenticationTicket(principal, "NoAuth"); + + return Task.FromResult(AuthenticateResult.Success(ticket)); + } + } +} diff --git a/src/Sonarr.Http/Authentication/SonarrNancyCookie.cs b/src/Sonarr.Http/Authentication/SonarrNancyCookie.cs deleted file mode 100644 index b160e69a2..000000000 --- a/src/Sonarr.Http/Authentication/SonarrNancyCookie.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using Nancy.Cookies; - -namespace Sonarr.Http.Authentication -{ - public class SonarrNancyCookie : NancyCookie - { - public SonarrNancyCookie(string name, string value) - : base(name, value) - { - } - - public SonarrNancyCookie(string name, string value, DateTime expires) - : base(name, value, expires) - { - } - - public SonarrNancyCookie(string name, string value, bool httpOnly) - : base(name, value, httpOnly) - { - } - - public SonarrNancyCookie(string name, string value, bool httpOnly, bool secure) - : base(name, value, httpOnly, secure) - { - } - - public SonarrNancyCookie(string name, string value, bool httpOnly, bool secure, DateTime? expires) - : base(name, value, httpOnly, secure, expires) - { - } - - public override string ToString() - { - return base.ToString() + "; SameSite=Lax"; - } - } -} diff --git a/src/Sonarr.Http/Authentication/UiAuthorizationPolicyProvider.cs b/src/Sonarr.Http/Authentication/UiAuthorizationPolicyProvider.cs new file mode 100644 index 000000000..a5295a99f --- /dev/null +++ b/src/Sonarr.Http/Authentication/UiAuthorizationPolicyProvider.cs @@ -0,0 +1,39 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Options; +using NzbDrone.Core.Configuration; + +namespace NzbDrone.Http.Authentication +{ + public class UiAuthorizationPolicyProvider : IAuthorizationPolicyProvider + { + private const string POLICY_NAME = "UI"; + private readonly IConfigFileProvider _config; + + public DefaultAuthorizationPolicyProvider FallbackPolicyProvider { get; } + + public UiAuthorizationPolicyProvider(IOptions options, + IConfigFileProvider config) + { + FallbackPolicyProvider = new DefaultAuthorizationPolicyProvider(options); + _config = config; + } + + public Task GetDefaultPolicyAsync() => FallbackPolicyProvider.GetDefaultPolicyAsync(); + + public Task GetFallbackPolicyAsync() => FallbackPolicyProvider.GetFallbackPolicyAsync(); + + public Task GetPolicyAsync(string policyName) + { + if (policyName.Equals(POLICY_NAME, StringComparison.OrdinalIgnoreCase)) + { + var policy = new AuthorizationPolicyBuilder(_config.AuthenticationMethod.ToString()) + .RequireAuthenticatedUser(); + return Task.FromResult(policy.Build()); + } + + return FallbackPolicyProvider.GetPolicyAsync(policyName); + } + } +} diff --git a/src/Sonarr.Http/ByteArrayResponse.cs b/src/Sonarr.Http/ByteArrayResponse.cs deleted file mode 100644 index acfe36730..000000000 --- a/src/Sonarr.Http/ByteArrayResponse.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.IO; -using Nancy; - -namespace Sonarr.Http -{ - public class ByteArrayResponse : Response - { - public ByteArrayResponse(byte[] body, string contentType) - { - ContentType = contentType; - - Contents = stream => - { - using (var writer = new BinaryWriter(stream)) - { - writer.Write(body); - } - }; - } - } -} diff --git a/src/Sonarr.Http/ErrorManagement/ErrorHandler.cs b/src/Sonarr.Http/ErrorManagement/ErrorHandler.cs deleted file mode 100644 index 7cc962f78..000000000 --- a/src/Sonarr.Http/ErrorManagement/ErrorHandler.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Nancy; -using Nancy.ErrorHandling; -using Sonarr.Http.Extensions; - -namespace Sonarr.Http.ErrorManagement -{ - public class ErrorHandler : IStatusCodeHandler - { - public bool HandlesStatusCode(HttpStatusCode statusCode, NancyContext context) - { - return true; - } - - public void Handle(HttpStatusCode statusCode, NancyContext context) - { - if (statusCode == HttpStatusCode.SeeOther || statusCode == HttpStatusCode.OK) - { - return; - } - - if (statusCode == HttpStatusCode.Continue) - { - context.Response = new Response { StatusCode = statusCode }; - return; - } - - if (statusCode == HttpStatusCode.Unauthorized) - { - return; - } - - if (context.Response.ContentType == "text/html" || context.Response.ContentType == "text/plain") - { - context.Response = new ErrorModel - { - Message = statusCode.ToString() - }.AsResponse(context, statusCode); - } - } - } -} diff --git a/src/Sonarr.Http/ErrorManagement/ErrorModel.cs b/src/Sonarr.Http/ErrorManagement/ErrorModel.cs index 22eddb873..0726cdc18 100644 --- a/src/Sonarr.Http/ErrorManagement/ErrorModel.cs +++ b/src/Sonarr.Http/ErrorManagement/ErrorModel.cs @@ -1,4 +1,8 @@ -using Sonarr.Http.Exceptions; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using NzbDrone.Common.Serializer; +using Sonarr.Http.Exceptions; namespace Sonarr.Http.ErrorManagement { @@ -17,5 +21,12 @@ namespace Sonarr.Http.ErrorManagement public ErrorModel() { } + + public Task WriteToResponse(HttpResponse response, HttpStatusCode statusCode = HttpStatusCode.InternalServerError) + { + response.StatusCode = (int)statusCode; + response.ContentType = "application/json"; + return STJson.SerializeAsync(this, response.Body); + } } } diff --git a/src/Sonarr.Http/ErrorManagement/SonarrErrorPipeline.cs b/src/Sonarr.Http/ErrorManagement/SonarrErrorPipeline.cs index 65edb1833..32786e725 100644 --- a/src/Sonarr.Http/ErrorManagement/SonarrErrorPipeline.cs +++ b/src/Sonarr.Http/ErrorManagement/SonarrErrorPipeline.cs @@ -1,13 +1,14 @@ -using System; using System.Data.SQLite; +using System.Net; +using System.Threading.Tasks; using FluentValidation; -using Nancy; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http; using NLog; +using NzbDrone.Common.Serializer; using NzbDrone.Core.Datastore; using NzbDrone.Core.Exceptions; using Sonarr.Http.Exceptions; -using Sonarr.Http.Extensions; -using HttpStatusCode = Nancy.HttpStatusCode; namespace Sonarr.Http.ErrorManagement { @@ -20,73 +21,67 @@ namespace Sonarr.Http.ErrorManagement _logger = logger; } - public Response HandleException(NancyContext context, Exception exception) + public async Task HandleException(HttpContext context) { _logger.Trace("Handling Exception"); + var response = context.Response; + var exceptionHandlerPathFeature = context.Features.Get(); + var exception = exceptionHandlerPathFeature?.Error; + + var statusCode = HttpStatusCode.InternalServerError; + var errorModel = new ErrorModel + { + Message = exception?.Message, + Description = exception?.ToString() + }; + if (exception is ApiException apiException) { - _logger.Warn(apiException, "API Error"); - return apiException.ToErrorResponse(context); - } + _logger.Warn(apiException, "API Error:\n{0}", apiException.Message); - if (exception is ValidationException validationException) + errorModel = new ErrorModel(apiException); + statusCode = apiException.StatusCode; + } + else if (exception is ValidationException validationException) { _logger.Warn("Invalid request {0}", validationException.Message); - return validationException.Errors.AsResponse(context, HttpStatusCode.BadRequest); + response.StatusCode = (int)HttpStatusCode.BadRequest; + response.ContentType = "application/json"; + await response.WriteAsync(STJson.ToJson(validationException.Errors)); + return; } - - if (exception is NzbDroneClientException clientException) + else if (exception is NzbDroneClientException clientException) { - return new ErrorModel - { - Message = exception.Message, - Description = exception.ToString() - }.AsResponse(context, (HttpStatusCode)clientException.StatusCode); + statusCode = clientException.StatusCode; } - - if (exception is ModelNotFoundException notFoundException) + else if (exception is ModelNotFoundException) { - return new ErrorModel - { - Message = exception.Message, - Description = exception.ToString() - }.AsResponse(context, HttpStatusCode.NotFound); + statusCode = HttpStatusCode.NotFound; } - - if (exception is ModelConflictException conflictException) + else if (exception is ModelConflictException) { - return new ErrorModel - { - Message = exception.Message, - Description = exception.ToString() - }.AsResponse(context, HttpStatusCode.Conflict); + statusCode = HttpStatusCode.Conflict; } - - if (exception is SQLiteException sqLiteException) + else if (exception is SQLiteException sqLiteException) { if (context.Request.Method == "PUT" || context.Request.Method == "POST") { if (sqLiteException.Message.Contains("constraint failed")) { - return new ErrorModel - { - Message = exception.Message, - }.AsResponse(context, HttpStatusCode.Conflict); + statusCode = HttpStatusCode.Conflict; } } _logger.Error(sqLiteException, "[{0} {1}]", context.Request.Method, context.Request.Path); } - - _logger.Fatal(exception, "Request Failed. {0} {1}", context.Request.Method, context.Request.Path); - - return new ErrorModel + else { - Message = exception.Message, - Description = exception.ToString() - }.AsResponse(context, HttpStatusCode.InternalServerError); + _logger.Fatal(exception, "Request Failed. {0} {1}", context.Request.Method, context.Request.Path); + } + + await errorModel.WriteToResponse(response, statusCode); } } } diff --git a/src/Sonarr.Http/Exceptions/ApiException.cs b/src/Sonarr.Http/Exceptions/ApiException.cs index 1e84dcd8b..52e231970 100644 --- a/src/Sonarr.Http/Exceptions/ApiException.cs +++ b/src/Sonarr.Http/Exceptions/ApiException.cs @@ -1,8 +1,5 @@ using System; -using Nancy; -using Nancy.Responses; -using Sonarr.Http.ErrorManagement; -using Sonarr.Http.Extensions; +using System.Net; namespace Sonarr.Http.Exceptions { @@ -19,11 +16,6 @@ namespace Sonarr.Http.Exceptions Content = content; } - public JsonResponse ToErrorResponse(NancyContext context) - { - return new ErrorModel(this).AsResponse(context, StatusCode); - } - private static string GetMessage(HttpStatusCode statusCode, object content) { var result = statusCode.ToString(); diff --git a/src/Sonarr.Http/Extensions/NancyJsonSerializer.cs b/src/Sonarr.Http/Extensions/NancyJsonSerializer.cs deleted file mode 100644 index 66604b2a2..000000000 --- a/src/Sonarr.Http/Extensions/NancyJsonSerializer.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Text.Json; -using Nancy; -using Nancy.Responses.Negotiation; -using NzbDrone.Common.Serializer; - -namespace Sonarr.Http.Extensions -{ - public class NancyJsonSerializer : ISerializer - { - protected readonly JsonSerializerOptions _serializerSettings; - - public NancyJsonSerializer() - { - _serializerSettings = STJson.GetSerializerSettings(); - } - - public bool CanSerialize(MediaRange contentType) - { - return contentType == "application/json"; - } - - public void Serialize(MediaRange contentType, TModel model, Stream outputStream) - { - STJson.Serialize(model, outputStream, _serializerSettings); - } - - public IEnumerable Extensions { get; private set; } - } -} diff --git a/src/Sonarr.Http/Extensions/Pipelines/CacheHeaderPipeline.cs b/src/Sonarr.Http/Extensions/Pipelines/CacheHeaderPipeline.cs deleted file mode 100644 index 70a627fe0..000000000 --- a/src/Sonarr.Http/Extensions/Pipelines/CacheHeaderPipeline.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using Nancy; -using Nancy.Bootstrapper; -using Sonarr.Http.Frontend; - -namespace Sonarr.Http.Extensions.Pipelines -{ - public class CacheHeaderPipeline : IRegisterNancyPipeline - { - private readonly ICacheableSpecification _cacheableSpecification; - - public CacheHeaderPipeline(ICacheableSpecification cacheableSpecification) - { - _cacheableSpecification = cacheableSpecification; - } - - public int Order => 0; - - public void Register(IPipelines pipelines) - { - pipelines.AfterRequest.AddItemToStartOfPipeline((Action)Handle); - } - - private void Handle(NancyContext context) - { - if (context.Request.Method == "OPTIONS") - { - return; - } - - if (_cacheableSpecification.IsCacheable(context)) - { - context.Response.Headers.EnableCache(); - } - else - { - context.Response.Headers.DisableCache(); - } - } - } -} diff --git a/src/Sonarr.Http/Extensions/Pipelines/CorsPipeline.cs b/src/Sonarr.Http/Extensions/Pipelines/CorsPipeline.cs deleted file mode 100644 index 5b64f9c2c..000000000 --- a/src/Sonarr.Http/Extensions/Pipelines/CorsPipeline.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.Linq; -using Nancy; -using Nancy.Bootstrapper; -using NzbDrone.Common.Extensions; - -namespace Sonarr.Http.Extensions.Pipelines -{ - public class CorsPipeline : IRegisterNancyPipeline - { - public int Order => 0; - - public void Register(IPipelines pipelines) - { - pipelines.BeforeRequest.AddItemToEndOfPipeline(HandleRequest); - pipelines.AfterRequest.AddItemToEndOfPipeline(HandleResponse); - } - - private Response HandleRequest(NancyContext context) - { - if (context == null || context.Request.Method != "OPTIONS") - { - return null; - } - - var response = new Response() - .WithStatusCode(HttpStatusCode.OK) - .WithContentType(""); - ApplyResponseHeaders(response, context.Request); - return response; - } - - private void HandleResponse(NancyContext context) - { - if (context == null || context.Response.Headers.ContainsKey(AccessControlHeaders.AllowOrigin)) - { - return; - } - - ApplyResponseHeaders(context.Response, context.Request); - } - - private static void ApplyResponseHeaders(Response response, Request request) - { - if (request.IsApiRequest()) - { - // Allow Cross-Origin access to the API since it's protected with the apikey, and nothing else. - ApplyCorsResponseHeaders(response, request, "*", "GET, OPTIONS, PATCH, POST, PUT, DELETE"); - } - else if (request.IsSharedContentRequest()) - { - // Allow Cross-Origin access to specific shared content such as mediacovers and images. - ApplyCorsResponseHeaders(response, request, "*", "GET, OPTIONS"); - } - - // Disallow Cross-Origin access for any other route. - } - - private static void ApplyCorsResponseHeaders(Response response, Request request, string allowOrigin, string allowedMethods) - { - response.Headers.Add(AccessControlHeaders.AllowOrigin, allowOrigin); - - if (request.Method == "OPTIONS") - { - if (response.Headers.ContainsKey("Allow")) - { - allowedMethods = response.Headers["Allow"]; - } - - response.Headers.Add(AccessControlHeaders.AllowMethods, allowedMethods); - - if (request.Headers[AccessControlHeaders.RequestHeaders].Any()) - { - var requestedHeaders = request.Headers[AccessControlHeaders.RequestHeaders].Join(", "); - - response.Headers.Add(AccessControlHeaders.AllowHeaders, requestedHeaders); - } - } - } - } -} diff --git a/src/Sonarr.Http/Extensions/Pipelines/GZipPipeline.cs b/src/Sonarr.Http/Extensions/Pipelines/GZipPipeline.cs deleted file mode 100644 index 68c6dc39d..000000000 --- a/src/Sonarr.Http/Extensions/Pipelines/GZipPipeline.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System; -using System.IO; -using System.IO.Compression; -using System.Linq; -using Nancy; -using Nancy.Bootstrapper; -using NLog; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Common.Extensions; - -namespace Sonarr.Http.Extensions.Pipelines -{ - public class GzipCompressionPipeline : IRegisterNancyPipeline - { - private readonly Logger _logger; - - public int Order => 0; - - private readonly Action, Stream> _writeGZipStream; - - public GzipCompressionPipeline(Logger logger) - { - _logger = logger; - - // On Mono GZipStream/DeflateStream leaks memory if an exception is thrown, use an intermediate buffer in that case. - _writeGZipStream = (Action, Stream>)WriteGZipStream; - } - - public void Register(IPipelines pipelines) - { - pipelines.AfterRequest.AddItemToEndOfPipeline(CompressResponse); - } - - private void CompressResponse(NancyContext context) - { - var request = context.Request; - var response = context.Response; - - try - { - if ( - response.Contents != Response.NoBody - && !response.ContentType.Contains("image") - && !response.ContentType.Contains("font") - && request.Headers.AcceptEncoding.Any(x => x.Contains("gzip")) - && !AlreadyGzipEncoded(response) - && !ContentLengthIsTooSmall(response)) - { - var contents = response.Contents; - - response.Headers["Content-Encoding"] = "gzip"; - response.Contents = responseStream => _writeGZipStream(contents, responseStream); - } - } - catch (Exception ex) - { - _logger.Error(ex, "Unable to gzip response"); - throw; - } - } - - private static void WriteGZipStreamMono(Action innerContent, Stream targetStream) - { - using (var membuffer = new MemoryStream()) - { - WriteGZipStream(innerContent, membuffer); - membuffer.Position = 0; - membuffer.CopyTo(targetStream); - } - } - - private static void WriteGZipStream(Action innerContent, Stream targetStream) - { - using (var gzip = new GZipStream(targetStream, CompressionMode.Compress, true)) - using (var buffered = new BufferedStream(gzip, 8192)) - { - innerContent.Invoke(buffered); - } - } - - private static bool ContentLengthIsTooSmall(Response response) - { - var contentLength = response.Headers.TryGetValue("Content-Length", out var value) ? value : null; - - if (contentLength != null && long.Parse(contentLength) < 1024) - { - return true; - } - - return false; - } - - private static bool AlreadyGzipEncoded(Response response) - { - var contentEncoding = response.Headers.TryGetValue("Content-Encoding", out var value) ? value : null; - - if (contentEncoding == "gzip") - { - return true; - } - - return false; - } - } -} diff --git a/src/Sonarr.Http/Extensions/Pipelines/IRegisterNancyPipeline.cs b/src/Sonarr.Http/Extensions/Pipelines/IRegisterNancyPipeline.cs deleted file mode 100644 index 7853075a5..000000000 --- a/src/Sonarr.Http/Extensions/Pipelines/IRegisterNancyPipeline.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Nancy.Bootstrapper; - -namespace Sonarr.Http.Extensions.Pipelines -{ - public interface IRegisterNancyPipeline - { - int Order { get; } - - void Register(IPipelines pipelines); - } -} diff --git a/src/Sonarr.Http/Extensions/Pipelines/IfModifiedPipeline.cs b/src/Sonarr.Http/Extensions/Pipelines/IfModifiedPipeline.cs deleted file mode 100644 index 7c7e8ed80..000000000 --- a/src/Sonarr.Http/Extensions/Pipelines/IfModifiedPipeline.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using Nancy; -using Nancy.Bootstrapper; -using Sonarr.Http.Frontend; - -namespace Sonarr.Http.Extensions.Pipelines -{ - public class IfModifiedPipeline : IRegisterNancyPipeline - { - private readonly ICacheableSpecification _cacheableSpecification; - - public IfModifiedPipeline(ICacheableSpecification cacheableSpecification) - { - _cacheableSpecification = cacheableSpecification; - } - - public int Order => 0; - - public void Register(IPipelines pipelines) - { - pipelines.BeforeRequest.AddItemToStartOfPipeline((Func)Handle); - } - - private Response Handle(NancyContext context) - { - if (_cacheableSpecification.IsCacheable(context) && context.Request.Headers.IfModifiedSince.HasValue) - { - var response = new Response { ContentType = MimeTypes.GetMimeType(context.Request.Path), StatusCode = HttpStatusCode.NotModified }; - response.Headers.EnableCache(); - return response; - } - - return null; - } - } -} diff --git a/src/Sonarr.Http/Extensions/Pipelines/RequestLoggingPipeline.cs b/src/Sonarr.Http/Extensions/Pipelines/RequestLoggingPipeline.cs deleted file mode 100644 index 1f51de296..000000000 --- a/src/Sonarr.Http/Extensions/Pipelines/RequestLoggingPipeline.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.Threading; -using Nancy; -using Nancy.Bootstrapper; -using NLog; -using NzbDrone.Common.Extensions; -using Sonarr.Http.ErrorManagement; - -namespace Sonarr.Http.Extensions.Pipelines -{ - public class RequestLoggingPipeline : IRegisterNancyPipeline - { - private static readonly Logger _loggerHttp = LogManager.GetLogger("Http"); - private static readonly Logger _loggerApi = LogManager.GetLogger("Api"); - - private static int _requestSequenceID; - - private readonly SonarrErrorPipeline _errorPipeline; - - public RequestLoggingPipeline(SonarrErrorPipeline errorPipeline) - { - _errorPipeline = errorPipeline; - } - - public int Order => 100; - - public void Register(IPipelines pipelines) - { - pipelines.BeforeRequest.AddItemToStartOfPipeline(LogStart); - pipelines.AfterRequest.AddItemToEndOfPipeline(LogEnd); - pipelines.OnError.AddItemToEndOfPipeline(LogError); - } - - private Response LogStart(NancyContext context) - { - var id = Interlocked.Increment(ref _requestSequenceID); - - context.Items["ApiRequestSequenceID"] = id; - context.Items["ApiRequestStartTime"] = DateTime.UtcNow; - - var reqPath = GetRequestPathAndQuery(context.Request); - - _loggerHttp.Trace("Req: {0} [{1}] {2} (from {3})", id, context.Request.Method, reqPath, GetOrigin(context)); - - return null; - } - - private void LogEnd(NancyContext context) - { - var id = (int)context.Items["ApiRequestSequenceID"]; - var startTime = (DateTime)context.Items["ApiRequestStartTime"]; - - var endTime = DateTime.UtcNow; - var duration = endTime - startTime; - - var reqPath = GetRequestPathAndQuery(context.Request); - - _loggerHttp.Trace("Res: {0} [{1}] {2}: {3}.{4} ({5} ms)", id, context.Request.Method, reqPath, (int)context.Response.StatusCode, context.Response.StatusCode, (int)duration.TotalMilliseconds); - - if (context.Request.IsApiRequest()) - { - _loggerApi.Debug("[{0}] {1}: {2}.{3} ({4} ms)", context.Request.Method, reqPath, (int)context.Response.StatusCode, context.Response.StatusCode, (int)duration.TotalMilliseconds); - } - } - - private Response LogError(NancyContext context, Exception exception) - { - var response = _errorPipeline.HandleException(context, exception); - - context.Response = response; - - LogEnd(context); - - context.Response = null; - - return response; - } - - private static string GetRequestPathAndQuery(Request request) - { - if (request.Url.Query.IsNotNullOrWhiteSpace()) - { - return string.Concat(request.Url.Path, request.Url.Query); - } - else - { - return request.Url.Path; - } - } - - private static string GetOrigin(NancyContext context) - { - if (context.Request.Headers.UserAgent.IsNullOrWhiteSpace()) - { - return context.GetRemoteIP(); - } - else - { - return $"{context.GetRemoteIP()} {context.Request.Headers.UserAgent}"; - } - } - } -} diff --git a/src/Sonarr.Http/Extensions/Pipelines/SetCookieHeaderPipeline.cs b/src/Sonarr.Http/Extensions/Pipelines/SetCookieHeaderPipeline.cs deleted file mode 100644 index e78f18d35..000000000 --- a/src/Sonarr.Http/Extensions/Pipelines/SetCookieHeaderPipeline.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Linq; -using Nancy; -using Nancy.Bootstrapper; - -namespace Sonarr.Http.Extensions.Pipelines -{ - public class SetCookieHeaderPipeline : IRegisterNancyPipeline - { - public int Order => 99; - - public void Register(IPipelines pipelines) - { - pipelines.AfterRequest.AddItemToEndOfPipeline((Action)Handle); - } - - private void Handle(NancyContext context) - { - if (context.Request.IsContentRequest() || context.Request.IsBundledJsRequest()) - { - var authCookie = context.Response.Cookies.FirstOrDefault(c => c.Name == "SonarrAuth"); - - if (authCookie != null) - { - context.Response.Cookies.Remove(authCookie); - } - } - } - } -} diff --git a/src/Sonarr.Http/Extensions/Pipelines/SonarrVersionPipeline.cs b/src/Sonarr.Http/Extensions/Pipelines/SonarrVersionPipeline.cs deleted file mode 100644 index 600c11a43..000000000 --- a/src/Sonarr.Http/Extensions/Pipelines/SonarrVersionPipeline.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using Nancy; -using Nancy.Bootstrapper; -using NzbDrone.Common.EnvironmentInfo; - -namespace Sonarr.Http.Extensions.Pipelines -{ - public class SonarrVersionPipeline : IRegisterNancyPipeline - { - public int Order => 0; - - public void Register(IPipelines pipelines) - { - pipelines.AfterRequest.AddItemToStartOfPipeline((Action)Handle); - } - - private void Handle(NancyContext context) - { - if (!context.Response.Headers.ContainsKey("X-Application-Version")) - { - context.Response.Headers.Add("X-Application-Version", BuildInfo.Version.ToString()); - } - } - } -} diff --git a/src/Sonarr.Http/Extensions/Pipelines/UrlBasePipeline.cs b/src/Sonarr.Http/Extensions/Pipelines/UrlBasePipeline.cs deleted file mode 100644 index 73623cd43..000000000 --- a/src/Sonarr.Http/Extensions/Pipelines/UrlBasePipeline.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using Nancy; -using Nancy.Bootstrapper; -using Nancy.Responses; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Configuration; - -namespace Sonarr.Http.Extensions.Pipelines -{ - public class UrlBasePipeline : IRegisterNancyPipeline - { - private readonly string _urlBase; - - public UrlBasePipeline(IConfigFileProvider configFileProvider) - { - _urlBase = configFileProvider.UrlBase; - } - - public int Order => 99; - - public void Register(IPipelines pipelines) - { - if (_urlBase.IsNotNullOrWhiteSpace()) - { - pipelines.BeforeRequest.AddItemToStartOfPipeline((Func)Handle); - } - } - - private Response Handle(NancyContext context) - { - var basePath = context.Request.Url.BasePath; - - if (basePath.IsNullOrWhiteSpace()) - { - return new RedirectResponse($"{_urlBase}{context.Request.Path}{context.Request.Url.Query}"); - } - - if (_urlBase != basePath) - { - return new NotFoundResponse(); - } - - return null; - } - } -} diff --git a/src/Sonarr.Http/Extensions/ReqResExtensions.cs b/src/Sonarr.Http/Extensions/ReqResExtensions.cs deleted file mode 100644 index 1cce0e46d..000000000 --- a/src/Sonarr.Http/Extensions/ReqResExtensions.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Nancy; -using Nancy.Responses; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Common.Serializer; - -namespace Sonarr.Http.Extensions -{ - public static class ReqResExtensions - { - private static readonly NancyJsonSerializer NancySerializer = new NancyJsonSerializer(); - private static readonly string Expires = DateTime.UtcNow.AddYears(1).ToString("r"); - - public static readonly string LastModified = BuildInfo.BuildDateTime.ToString("r"); - - public static T FromJson(this Stream body) - where T : class, new() - { - return FromJson(body, typeof(T)); - } - - public static T FromJson(this Stream body, Type type) - { - return (T)FromJson(body, type); - } - - public static object FromJson(this Stream body, Type type) - { - body.Position = 0; - return STJson.Deserialize(body, type); - } - - public static JsonResponse AsResponse(this TModel model, NancyContext context, HttpStatusCode statusCode = HttpStatusCode.OK) - { - var response = new JsonResponse(model, NancySerializer, context.Environment) { StatusCode = statusCode }; - response.Headers.DisableCache(); - - return response; - } - - public static IDictionary DisableCache(this IDictionary headers) - { - headers["Cache-Control"] = "no-cache, no-store, must-revalidate, max-age=0"; - headers["Pragma"] = "no-cache"; - headers["Expires"] = "0"; - - return headers; - } - - public static IDictionary EnableCache(this IDictionary headers) - { - headers["Cache-Control"] = "max-age=31536000, public"; - headers["Expires"] = Expires; - headers["Last-Modified"] = LastModified; - headers["Age"] = "193266"; - - return headers; - } - } -} diff --git a/src/Sonarr.Http/Extensions/RequestExtensions.cs b/src/Sonarr.Http/Extensions/RequestExtensions.cs index cdddbda3f..f87f3d0b9 100644 --- a/src/Sonarr.Http/Extensions/RequestExtensions.cs +++ b/src/Sonarr.Http/Extensions/RequestExtensions.cs @@ -1,135 +1,185 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Net; -using Nancy; +using Microsoft.AspNetCore.Http; +using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Exceptions; namespace Sonarr.Http.Extensions { public static class RequestExtensions { - public static bool IsApiRequest(this Request request) + // See src/Readarr.Api.V1/Queue/QueueModule.cs + private static readonly HashSet VALID_SORT_KEYS = new HashSet(StringComparer.OrdinalIgnoreCase) { - return request.Path.CleanRequestPath().StartsWith("/api/", StringComparison.InvariantCultureIgnoreCase); + "series.sortname", //Workaround authors table properties not being added on isValidSortKey call + "timeleft", + "estimatedCompletionTime", + "protocol", + "episode", + "indexer", + "downloadClient", + "quality", + "status", + "title", + "progress" + }; + + private static readonly HashSet EXCLUDED_KEYS = new HashSet(StringComparer.InvariantCultureIgnoreCase) + { + "page", + "pageSize", + "sortKey", + "sortDirection", + "filterKey", + "filterValue", + }; + + public static bool IsApiRequest(this HttpRequest request) + { + return request.Path.StartsWithSegments("/api", StringComparison.InvariantCultureIgnoreCase); } - public static bool IsFeedRequest(this Request request) + public static bool IsFavIconRequest(this HttpRequest request) { - return request.Path.CleanRequestPath().StartsWith("/feed/", StringComparison.InvariantCultureIgnoreCase); + return request.Path.Equals("/favicon.ico", StringComparison.InvariantCultureIgnoreCase); } - public static bool IsPingRequest(this Request request) - { - return request.Path.CleanRequestPath().StartsWith("/ping", StringComparison.InvariantCultureIgnoreCase); - } - - public static bool IsSignalRRequest(this Request request) - { - return request.Path.CleanRequestPath().StartsWith("/signalr/", StringComparison.InvariantCultureIgnoreCase); - } - - public static bool IsLocalRequest(this Request request) - { - return request.UserHostAddress.Equals("localhost") || - request.UserHostAddress.Equals("127.0.0.1") || - request.UserHostAddress.Equals("::1"); - } - - public static bool IsLoginRequest(this Request request) - { - return request.Path.CleanRequestPath().Equals("/login", StringComparison.InvariantCultureIgnoreCase); - } - - public static bool IsContentRequest(this Request request) - { - return request.Path.CleanRequestPath().StartsWith("/Content/", StringComparison.InvariantCultureIgnoreCase); - } - - public static bool IsBundledJsRequest(this Request request) - { - return !request.Path.CleanRequestPath().EqualsIgnoreCase("/initialize.js") && request.Path.EndsWith(".js", StringComparison.InvariantCultureIgnoreCase); - } - - public static bool IsFavIconRequest(this Request request) - { - return request.Path.CleanRequestPath().EqualsIgnoreCase("/favicon.ico"); - } - - public static bool IsSharedContentRequest(this Request request) - { - return request.Path.CleanRequestPath().StartsWith("/MediaCover/", StringComparison.InvariantCultureIgnoreCase) || - request.Path.CleanRequestPath().StartsWith("/Content/Images/", StringComparison.InvariantCultureIgnoreCase); - } - - public static bool GetBooleanQueryParameter(this Request request, string parameter, bool defaultValue = false) + public static bool GetBooleanQueryParameter(this HttpRequest request, string parameter, bool defaultValue = false) { var parameterValue = request.Query[parameter]; - if (parameterValue.HasValue) + if (parameterValue.Any()) { - return bool.Parse(parameterValue.Value); + return bool.Parse(parameterValue.ToString()); } return defaultValue; } - public static int GetIntegerQueryParameter(this Request request, string parameter, int defaultValue = 0) + public static PagingResource ReadPagingResourceFromRequest(this HttpRequest request) { - var parameterValue = request.Query[parameter]; - - if (parameterValue.HasValue) + if (!int.TryParse(request.Query["PageSize"].ToString(), out var pageSize)) { - return int.Parse(parameterValue.Value); + pageSize = 10; } - return defaultValue; - } - - public static int? GetNullableIntegerQueryParameter(this Request request, string parameter, int? defaultValue = null) - { - var parameterValue = request.Query[parameter]; - - if (parameterValue.HasValue) + if (!int.TryParse(request.Query["Page"].ToString(), out var page)) { - return int.Parse(parameterValue.Value); + page = 1; } - return defaultValue; + var pagingResource = new PagingResource + { + PageSize = pageSize, + Page = page, + Filters = new List() + }; + + if (request.Query["SortKey"].Any()) + { + var sortKey = request.Query["SortKey"].ToString(); + + if (!VALID_SORT_KEYS.Contains(sortKey) && + !TableMapping.Mapper.IsValidSortKey(sortKey)) + { + throw new BadRequestException($"Invalid sort key {sortKey}"); + } + + pagingResource.SortKey = sortKey; + + if (request.Query["SortDirection"].Any()) + { + pagingResource.SortDirection = request.Query["SortDirection"].ToString() + .Equals("ascending", StringComparison.InvariantCultureIgnoreCase) + ? SortDirection.Ascending + : SortDirection.Descending; + } + } + + // For backwards compatibility with v2 + if (request.Query["FilterKey"].Any()) + { + var filter = new PagingResourceFilter + { + Key = request.Query["FilterKey"].ToString() + }; + + if (request.Query["FilterValue"].Any()) + { + filter.Value = request.Query["FilterValue"].ToString(); + } + + pagingResource.Filters.Add(filter); + } + + // v3 uses filters in key=value format + foreach (var pair in request.Query) + { + if (EXCLUDED_KEYS.Contains(pair.Key)) + { + continue; + } + + pagingResource.Filters.Add(new PagingResourceFilter + { + Key = pair.Key, + Value = pair.Value.ToString() + }); + } + + return pagingResource; } - public static string GetRemoteIP(this NancyContext context) + public static PagingResource ApplyToPage(this PagingSpec pagingSpec, Func, PagingSpec> function, Converter mapper) { - if (context == null || context.Request == null) + pagingSpec = function(pagingSpec); + + return new PagingResource + { + Page = pagingSpec.Page, + PageSize = pagingSpec.PageSize, + SortDirection = pagingSpec.SortDirection, + SortKey = pagingSpec.SortKey, + TotalRecords = pagingSpec.TotalRecords, + Records = pagingSpec.Records.ConvertAll(mapper) + }; + } + + public static string GetRemoteIP(this HttpContext context) + { + return context?.Request?.GetRemoteIP() ?? "Unknown"; + } + + public static string GetRemoteIP(this HttpRequest request) + { + if (request == null) { return "Unknown"; } - var remoteAddress = context.Request.UserHostAddress; - - IPAddress.TryParse(remoteAddress, out IPAddress remoteIP); - - if (remoteIP == null) - { - return remoteAddress; - } + var remoteIP = request.HttpContext.Connection.RemoteIpAddress; if (remoteIP.IsIPv4MappedToIPv6) { remoteIP = remoteIP.MapToIPv4(); } - remoteAddress = remoteIP.ToString(); + var remoteAddress = remoteIP.ToString(); // Only check if forwarded by a local network reverse proxy if (remoteIP.IsLocalAddress()) { - var realIPHeader = context.Request.Headers["X-Real-IP"]; + var realIPHeader = request.Headers["X-Real-IP"]; if (realIPHeader.Any()) { return realIPHeader.First().ToString(); } - var forwardedForHeader = context.Request.Headers["X-Forwarded-For"]; + var forwardedForHeader = request.Headers["X-Forwarded-For"]; if (forwardedForHeader.Any()) { // Get the first address that was forwarded by a local IP to prevent remote clients faking another proxy @@ -153,12 +203,18 @@ namespace Sonarr.Http.Extensions return remoteAddress; } - private static string CleanRequestPath(this string path) + public static void DisableCache(this IHeaderDictionary headers) { - // When running under mono the path is not stripped of extraneous leading slashes which can break our IXRequest - // path detection, this will remove all leading slashes and replace them with a single slash. + headers.Remove("Last-Modified"); + headers["Cache-Control"] = "no-cache, no-store"; + headers["Expires"] = "-1"; + headers["Pragma"] = "no-cache"; + } - return $"/{path.TrimStart('/')}"; + public static void EnableCache(this IHeaderDictionary headers) + { + headers["Cache-Control"] = "max-age=31536000, public"; + headers["Last-Modified"] = BuildInfo.BuildDateTime.ToString("r"); } } } diff --git a/src/Sonarr.Http/Frontend/CacheableSpecification.cs b/src/Sonarr.Http/Frontend/CacheableSpecification.cs deleted file mode 100644 index ffee8cf82..000000000 --- a/src/Sonarr.Http/Frontend/CacheableSpecification.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using Nancy; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Common.Extensions; - -namespace Sonarr.Http.Frontend -{ - public interface ICacheableSpecification - { - bool IsCacheable(NancyContext context); - } - - public class CacheableSpecification : ICacheableSpecification - { - public bool IsCacheable(NancyContext context) - { - if (!RuntimeInfo.IsProduction) - { - return false; - } - - if (((DynamicDictionary)context.Request.Query).ContainsKey("h")) - { - return true; - } - - if (context.Request.Path.StartsWith("/api", StringComparison.CurrentCultureIgnoreCase)) - { - if (context.Request.Path.ContainsIgnoreCase("/MediaCover")) - { - return true; - } - - return false; - } - - if (context.Request.Path.StartsWith("/signalr", StringComparison.CurrentCultureIgnoreCase)) - { - return false; - } - - if (context.Request.Path.EndsWith("index.js")) - { - return false; - } - - if (context.Request.Path.EndsWith("initialize.js")) - { - return false; - } - - if (context.Request.Path.StartsWith("/feed", StringComparison.CurrentCultureIgnoreCase)) - { - return false; - } - - if (context.Request.Path.StartsWith("/log", StringComparison.CurrentCultureIgnoreCase) && - context.Request.Path.EndsWith(".txt", StringComparison.CurrentCultureIgnoreCase)) - { - return false; - } - - if (context.Response != null) - { - if (context.Response.ContentType.Contains("text/html")) - { - return false; - } - } - - return true; - } - } -} diff --git a/src/Sonarr.Http/Frontend/InitializeJsModule.cs b/src/Sonarr.Http/Frontend/InitializeJsController.cs similarity index 69% rename from src/Sonarr.Http/Frontend/InitializeJsModule.cs rename to src/Sonarr.Http/Frontend/InitializeJsController.cs index fe9d8e9e1..df9e4d545 100644 --- a/src/Sonarr.Http/Frontend/InitializeJsModule.cs +++ b/src/Sonarr.Http/Frontend/InitializeJsController.cs @@ -1,7 +1,6 @@ -using System.IO; using System.Text; -using Nancy; -using Nancy.Responses; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Common; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Analytics; @@ -9,7 +8,9 @@ using NzbDrone.Core.Configuration; namespace Sonarr.Http.Frontend { - public class InitializeJsModule : NancyModule + [Authorize(Policy = "UI")] + [ApiController] + public class InitializeJsController : Controller { private readonly IConfigFileProvider _configFileProvider; private readonly IAnalyticsService _analyticsService; @@ -18,35 +19,20 @@ namespace Sonarr.Http.Frontend private static string _urlBase; private string _generatedContent; - public InitializeJsModule(IConfigFileProvider configFileProvider, - IAnalyticsService analyticsService) + public InitializeJsController(IConfigFileProvider configFileProvider, + IAnalyticsService analyticsService) { _configFileProvider = configFileProvider; _analyticsService = analyticsService; _apiKey = configFileProvider.ApiKey; _urlBase = configFileProvider.UrlBase; - - Get("/initialize.js", x => Index()); } - private Response Index() + [HttpGet("/initialize.js")] + public IActionResult Index() { - // TODO: Move away from window.Sonarr and prefetch the information returned here when starting the UI - return new StreamResponse(GetContentStream, "application/javascript"); - } - - private Stream GetContentStream() - { - var text = GetContent(); - - var stream = new MemoryStream(); - var writer = new StreamWriter(stream); - writer.Write(text); - writer.Flush(); - stream.Position = 0; - - return stream; + return Content(GetContent(), "application/javascript"); } private string GetContent() diff --git a/src/Sonarr.Http/Frontend/Mappers/HtmlMapperBase.cs b/src/Sonarr.Http/Frontend/Mappers/HtmlMapperBase.cs index 378a1185c..f593edebd 100644 --- a/src/Sonarr.Http/Frontend/Mappers/HtmlMapperBase.cs +++ b/src/Sonarr.Http/Frontend/Mappers/HtmlMapperBase.cs @@ -1,8 +1,6 @@ using System; -using System.Text; +using System.IO; using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Nancy; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; @@ -12,13 +10,13 @@ namespace Sonarr.Http.Frontend.Mappers public abstract class HtmlMapperBase : StaticResourceMapperBase { private readonly IDiskProvider _diskProvider; - private readonly Func _cacheBreakProviderFactory; + private readonly Lazy _cacheBreakProviderFactory; private static readonly Regex ReplaceRegex = new Regex(@"(?:(?href|src)=\"")(?.*?(?css|js|png|ico|ics|svg|json))(?:\"")(?:\s(?data-no-hash))?", RegexOptions.Compiled | RegexOptions.IgnoreCase); private string _generatedContent; protected HtmlMapperBase(IDiskProvider diskProvider, - Func cacheBreakProviderFactory, + Lazy cacheBreakProviderFactory, Logger logger) : base(diskProvider, logger) { @@ -29,19 +27,16 @@ namespace Sonarr.Http.Frontend.Mappers protected string HtmlPath; protected string UrlBase; - protected override Task GetContent(string filePath) + protected override Stream GetContentStream(string filePath) { var text = GetHtmlText(); - var data = Encoding.UTF8.GetBytes(text); - return Task.FromResult(data); - } - public async override Task GetResponse(string resourceUrl) - { - var response = await base.GetResponse(resourceUrl); - response.Headers["X-UA-Compatible"] = "IE=edge"; - - return response; + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write(text); + writer.Flush(); + stream.Position = 0; + return stream; } protected string GetHtmlText() @@ -52,7 +47,7 @@ namespace Sonarr.Http.Frontend.Mappers } var text = _diskProvider.ReadAllText(HtmlPath); - var cacheBreakProvider = _cacheBreakProviderFactory(); + var cacheBreakProvider = _cacheBreakProviderFactory.Value; text = ReplaceRegex.Replace(text, match => { diff --git a/src/Sonarr.Http/Frontend/Mappers/IMapHttpRequestsToDisk.cs b/src/Sonarr.Http/Frontend/Mappers/IMapHttpRequestsToDisk.cs index 86dad7188..7b3021f8f 100644 --- a/src/Sonarr.Http/Frontend/Mappers/IMapHttpRequestsToDisk.cs +++ b/src/Sonarr.Http/Frontend/Mappers/IMapHttpRequestsToDisk.cs @@ -1,5 +1,4 @@ -using System.Threading.Tasks; -using Nancy; +using Microsoft.AspNetCore.Mvc; namespace Sonarr.Http.Frontend.Mappers { @@ -7,6 +6,6 @@ namespace Sonarr.Http.Frontend.Mappers { string Map(string resourceUrl); bool CanHandle(string resourceUrl); - Task GetResponse(string resourceUrl); + IActionResult GetResponse(string resourceUrl); } } diff --git a/src/Sonarr.Http/Frontend/Mappers/IndexHtmlMapper.cs b/src/Sonarr.Http/Frontend/Mappers/IndexHtmlMapper.cs index fde71dd1b..79d5d326f 100644 --- a/src/Sonarr.Http/Frontend/Mappers/IndexHtmlMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/IndexHtmlMapper.cs @@ -14,7 +14,7 @@ namespace Sonarr.Http.Frontend.Mappers public IndexHtmlMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, IConfigFileProvider configFileProvider, - Func cacheBreakProviderFactory, + Lazy cacheBreakProviderFactory, Logger logger) : base(diskProvider, cacheBreakProviderFactory, logger) { diff --git a/src/Sonarr.Http/Frontend/Mappers/LogFileMapper.cs b/src/Sonarr.Http/Frontend/Mappers/LogFileMapper.cs index 2b418f981..269e0243d 100644 --- a/src/Sonarr.Http/Frontend/Mappers/LogFileMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/LogFileMapper.cs @@ -6,11 +6,11 @@ using NzbDrone.Common.Extensions; namespace Sonarr.Http.Frontend.Mappers { - public class UpdateLogFileMapper : StaticResourceMapperBase + public class LogFileMapper : StaticResourceMapperBase { private readonly IAppFolderInfo _appFolderInfo; - public UpdateLogFileMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, Logger logger) + public LogFileMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, Logger logger) : base(diskProvider, logger) { _appFolderInfo = appFolderInfo; @@ -21,12 +21,12 @@ namespace Sonarr.Http.Frontend.Mappers var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar); path = Path.GetFileName(path); - return Path.Combine(_appFolderInfo.GetUpdateLogFolder(), path); + return Path.Combine(_appFolderInfo.GetLogFolder(), path); } public override bool CanHandle(string resourceUrl) { - return resourceUrl.StartsWith("/updatelogfile/") && resourceUrl.EndsWith(".txt"); + return resourceUrl.StartsWith("/logfile/") && resourceUrl.EndsWith(".txt"); } } } diff --git a/src/Sonarr.Http/Frontend/Mappers/LoginHtmlMapper.cs b/src/Sonarr.Http/Frontend/Mappers/LoginHtmlMapper.cs index ea11e0b09..58f73db91 100644 --- a/src/Sonarr.Http/Frontend/Mappers/LoginHtmlMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/LoginHtmlMapper.cs @@ -11,7 +11,7 @@ namespace Sonarr.Http.Frontend.Mappers { public LoginHtmlMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, - Func cacheBreakProviderFactory, + Lazy cacheBreakProviderFactory, IConfigFileProvider configFileProvider, Logger logger) : base(diskProvider, cacheBreakProviderFactory, logger) diff --git a/src/Sonarr.Http/Frontend/Mappers/MediaCoverProxyMapper.cs b/src/Sonarr.Http/Frontend/Mappers/MediaCoverProxyMapper.cs index 89e557ec4..ece8c0609 100644 --- a/src/Sonarr.Http/Frontend/Mappers/MediaCoverProxyMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/MediaCoverProxyMapper.cs @@ -1,7 +1,8 @@ using System; +using System.Net; using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Nancy; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.StaticFiles; using NzbDrone.Core.MediaCover; namespace Sonarr.Http.Frontend.Mappers @@ -11,10 +12,12 @@ namespace Sonarr.Http.Frontend.Mappers private readonly Regex _regex = new Regex(@"/MediaCoverProxy/(?\w+)/(?(.+)\.(jpg|png|gif))"); private readonly IMediaCoverProxy _mediaCoverProxy; + private readonly IContentTypeProvider _mimeTypeProvider; public MediaCoverProxyMapper(IMediaCoverProxy mediaCoverProxy) { _mediaCoverProxy = mediaCoverProxy; + _mimeTypeProvider = new FileExtensionContentTypeProvider(); } public string Map(string resourceUrl) @@ -27,13 +30,13 @@ namespace Sonarr.Http.Frontend.Mappers return resourceUrl.StartsWith("/MediaCoverProxy/", StringComparison.InvariantCultureIgnoreCase); } - public Task GetResponse(string resourceUrl) + public IActionResult GetResponse(string resourceUrl) { var match = _regex.Match(resourceUrl); if (!match.Success) { - return Task.FromResult(new NotFoundResponse()); + return new StatusCodeResult((int)HttpStatusCode.NotFound); } var hash = match.Groups["hash"].Value; @@ -41,7 +44,12 @@ namespace Sonarr.Http.Frontend.Mappers var imageData = _mediaCoverProxy.GetImage(hash); - return Task.FromResult(new ByteArrayResponse(imageData, MimeTypes.GetMimeType(filename))); + if (!_mimeTypeProvider.TryGetContentType(filename, out var contentType)) + { + contentType = "application/octet-stream"; + } + + return new FileContentResult(imageData, contentType); } } } diff --git a/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs b/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs index c004cfd94..40df35e99 100644 --- a/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs +++ b/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs @@ -1,7 +1,7 @@ using System; using System.IO; -using System.Threading.Tasks; -using Nancy; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.StaticFiles; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; @@ -13,14 +13,14 @@ namespace Sonarr.Http.Frontend.Mappers private readonly IDiskProvider _diskProvider; private readonly Logger _logger; private readonly StringComparison _caseSensitive; - - private static readonly NotFoundResponse NotFoundResponse = new NotFoundResponse(); + private readonly IContentTypeProvider _mimeTypeProvider; protected StaticResourceMapperBase(IDiskProvider diskProvider, Logger logger) { _diskProvider = diskProvider; _logger = logger; + _mimeTypeProvider = new FileExtensionContentTypeProvider(); _caseSensitive = RuntimeInfo.IsProduction ? DiskProviderBase.PathStringComparison : StringComparison.OrdinalIgnoreCase; } @@ -28,33 +28,28 @@ namespace Sonarr.Http.Frontend.Mappers public abstract bool CanHandle(string resourceUrl); - public async virtual Task GetResponse(string resourceUrl) + public IActionResult GetResponse(string resourceUrl) { var filePath = Map(resourceUrl); if (_diskProvider.FileExists(filePath, _caseSensitive)) { - var data = await GetContent(filePath).ConfigureAwait(false); + if (!_mimeTypeProvider.TryGetContentType(filePath, out var contentType)) + { + contentType = "application/octet-stream"; + } - return new ByteArrayResponse(data, MimeTypes.GetMimeType(filePath)); + return new FileStreamResult(GetContentStream(filePath), contentType); } _logger.Warn("File {0} not found", filePath); - return NotFoundResponse; + return null; } - protected async virtual Task GetContent(string filePath) + protected virtual Stream GetContentStream(string filePath) { - using (var output = new MemoryStream()) - { - using (var file = File.OpenRead(filePath)) - { - await file.CopyToAsync(output).ConfigureAwait(false); - } - - return output.ToArray(); - } + return File.OpenRead(filePath); } } } diff --git a/src/Sonarr.Http/Frontend/Mappers/UpdateLogFileMapper.cs b/src/Sonarr.Http/Frontend/Mappers/UpdateLogFileMapper.cs index 269e0243d..2b418f981 100644 --- a/src/Sonarr.Http/Frontend/Mappers/UpdateLogFileMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/UpdateLogFileMapper.cs @@ -6,11 +6,11 @@ using NzbDrone.Common.Extensions; namespace Sonarr.Http.Frontend.Mappers { - public class LogFileMapper : StaticResourceMapperBase + public class UpdateLogFileMapper : StaticResourceMapperBase { private readonly IAppFolderInfo _appFolderInfo; - public LogFileMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, Logger logger) + public UpdateLogFileMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, Logger logger) : base(diskProvider, logger) { _appFolderInfo = appFolderInfo; @@ -21,12 +21,12 @@ namespace Sonarr.Http.Frontend.Mappers var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar); path = Path.GetFileName(path); - return Path.Combine(_appFolderInfo.GetLogFolder(), path); + return Path.Combine(_appFolderInfo.GetUpdateLogFolder(), path); } public override bool CanHandle(string resourceUrl) { - return resourceUrl.StartsWith("/logfile/") && resourceUrl.EndsWith(".txt"); + return resourceUrl.StartsWith("/updatelogfile/") && resourceUrl.EndsWith(".txt"); } } } diff --git a/src/Sonarr.Http/Frontend/StaticResourceController.cs b/src/Sonarr.Http/Frontend/StaticResourceController.cs new file mode 100644 index 000000000..d09604992 --- /dev/null +++ b/src/Sonarr.Http/Frontend/StaticResourceController.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Mvc; +using NLog; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; +using Sonarr.Http.Extensions; +using Sonarr.Http.Frontend.Mappers; + +namespace Sonarr.Http.Frontend +{ + [Authorize(Policy="UI")] + [ApiController] + public class StaticResourceController : Controller + { + private readonly IEnumerable _requestMappers; + private readonly Logger _logger; + + public StaticResourceController(IEnumerable requestMappers, + Logger logger) + { + _requestMappers = requestMappers; + _logger = logger; + } + + [AllowAnonymous] + [HttpGet("login")] + public IActionResult LoginPage() + { + return MapResource("login"); + } + + [EnableCors("AllowGet")] + [AllowAnonymous] + [HttpGet("/content/{**path:regex(^(?!api/).*)}")] + public IActionResult IndexContent([FromRoute] string path) + { + return MapResource("Content/" + path); + } + + [HttpGet("")] + [HttpGet("/{**path:regex(^(?!(api|feed)/).*)}")] + public IActionResult Index([FromRoute] string path) + { + return MapResource(path); + } + + private IActionResult MapResource(string path) + { + path = "/" + (path ?? ""); + + var mapper = _requestMappers.SingleOrDefault(m => m.CanHandle(path)); + + if (mapper != null) + { + var result = mapper.GetResponse(path); + + if (result != null) + { + if ((result as FileResult)?.ContentType == "text/html") + { + Response.Headers.DisableCache(); + } + + return result; + } + + return NotFound(); + } + + _logger.Warn("Couldn't find handler for {0}", path); + + return NotFound(); + } + } +} diff --git a/src/Sonarr.Http/Frontend/StaticResourceModule.cs b/src/Sonarr.Http/Frontend/StaticResourceModule.cs deleted file mode 100644 index 4069e168a..000000000 --- a/src/Sonarr.Http/Frontend/StaticResourceModule.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Nancy; -using NLog; -using Sonarr.Http.Frontend.Mappers; - -namespace Sonarr.Http.Frontend -{ - public class StaticResourceModule : NancyModule - { - private readonly IEnumerable _requestMappers; - private readonly Logger _logger; - - public StaticResourceModule(IEnumerable requestMappers, Logger logger) - { - _requestMappers = requestMappers; - _logger = logger; - - Get("/{resource*}", async (x, ct) => await Index()); - Get("/", async (x, ct) => await Index()); - } - - private async Task Index() - { - var path = Request.Url.Path; - - if ( - string.IsNullOrWhiteSpace(path) || - path.StartsWith("/api", StringComparison.CurrentCultureIgnoreCase) || - path.StartsWith("/signalr", StringComparison.CurrentCultureIgnoreCase)) - { - return new NotFoundResponse(); - } - - var mapper = _requestMappers.SingleOrDefault(m => m.CanHandle(path)); - - if (mapper != null) - { - return await mapper.GetResponse(path); - } - - _logger.Warn("Couldn't find handler for {0}", path); - - return new NotFoundResponse(); - } - } -} diff --git a/src/Sonarr.Http/Middleware/BufferingMiddleware.cs b/src/Sonarr.Http/Middleware/BufferingMiddleware.cs new file mode 100644 index 000000000..e82d157a0 --- /dev/null +++ b/src/Sonarr.Http/Middleware/BufferingMiddleware.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Sonarr.Http.Middleware +{ + public class BufferingMiddleware + { + private readonly RequestDelegate _next; + private readonly List _urls; + + public BufferingMiddleware(RequestDelegate next, List urls) + { + _next = next; + _urls = urls; + } + + public async Task InvokeAsync(HttpContext context) + { + if (_urls.Any(p => context.Request.Path.StartsWithSegments(p, StringComparison.OrdinalIgnoreCase))) + { + context.Request.EnableBuffering(); + } + + await _next(context); + } + } +} diff --git a/src/Sonarr.Http/Middleware/CacheHeaderMiddleware.cs b/src/Sonarr.Http/Middleware/CacheHeaderMiddleware.cs new file mode 100644 index 000000000..52bd41abd --- /dev/null +++ b/src/Sonarr.Http/Middleware/CacheHeaderMiddleware.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Sonarr.Http.Extensions; + +namespace Sonarr.Http.Middleware +{ + public class CacheHeaderMiddleware + { + private readonly RequestDelegate _next; + private readonly ICacheableSpecification _cacheableSpecification; + + public CacheHeaderMiddleware(RequestDelegate next, ICacheableSpecification cacheableSpecification) + { + _next = next; + _cacheableSpecification = cacheableSpecification; + } + + public async Task InvokeAsync(HttpContext context) + { + if (context.Request.Method != "OPTIONS") + { + if (_cacheableSpecification.IsCacheable(context.Request)) + { + context.Response.Headers.EnableCache(); + } + else + { + context.Response.Headers.DisableCache(); + } + } + + await _next(context); + } + } +} diff --git a/src/Sonarr.Http/Middleware/CacheableSpecification.cs b/src/Sonarr.Http/Middleware/CacheableSpecification.cs new file mode 100644 index 000000000..586e38a86 --- /dev/null +++ b/src/Sonarr.Http/Middleware/CacheableSpecification.cs @@ -0,0 +1,69 @@ +using System; +using Microsoft.AspNetCore.Http; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; + +namespace Sonarr.Http.Middleware +{ + public interface ICacheableSpecification + { + bool IsCacheable(HttpRequest request); + } + + public class CacheableSpecification : ICacheableSpecification + { + public bool IsCacheable(HttpRequest request) + { + if (!RuntimeInfo.IsProduction) + { + return false; + } + + if (request.Query.ContainsKey("h")) + { + return true; + } + + if (request.Path.StartsWithSegments("/api", StringComparison.CurrentCultureIgnoreCase)) + { + if (request.Path.ToString().ContainsIgnoreCase("/MediaCover")) + { + return true; + } + + return false; + } + + if (request.Path.StartsWithSegments("/signalr", StringComparison.CurrentCultureIgnoreCase)) + { + return false; + } + + var path = request.Path.Value ?? ""; + + if (path.EndsWith("/index.js")) + { + return false; + } + + if (path.EndsWith("/initialize.js")) + { + return false; + } + + if (path.StartsWith("/feed", StringComparison.CurrentCultureIgnoreCase)) + { + return false; + } + + if ((path.StartsWith("/logfile", StringComparison.CurrentCultureIgnoreCase) || + path.StartsWith("/updatelogfile", StringComparison.CurrentCultureIgnoreCase)) && + path.EndsWith(".txt", StringComparison.CurrentCultureIgnoreCase)) + { + return false; + } + + return true; + } + } +} diff --git a/src/Sonarr.Http/Middleware/IfModifiedMiddleware.cs b/src/Sonarr.Http/Middleware/IfModifiedMiddleware.cs new file mode 100644 index 000000000..80401d631 --- /dev/null +++ b/src/Sonarr.Http/Middleware/IfModifiedMiddleware.cs @@ -0,0 +1,43 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.StaticFiles; +using Sonarr.Http.Extensions; + +namespace Sonarr.Http.Middleware +{ + public class IfModifiedMiddleware + { + private readonly RequestDelegate _next; + private readonly ICacheableSpecification _cacheableSpecification; + private readonly IContentTypeProvider _mimeTypeProvider; + + public IfModifiedMiddleware(RequestDelegate next, ICacheableSpecification cacheableSpecification) + { + _next = next; + _cacheableSpecification = cacheableSpecification; + + _mimeTypeProvider = new FileExtensionContentTypeProvider(); + } + + public async Task InvokeAsync(HttpContext context) + { + if (_cacheableSpecification.IsCacheable(context.Request) && context.Request.Headers["IfModifiedSince"].Any()) + { + context.Response.StatusCode = 304; + context.Response.Headers.EnableCache(); + + if (!_mimeTypeProvider.TryGetContentType(context.Request.Path.ToString(), out var mimeType)) + { + mimeType = "application/octet-stream"; + } + + context.Response.ContentType = mimeType; + + return; + } + + await _next(context); + } + } +} diff --git a/src/Sonarr.Http/Middleware/LoggingMiddleware.cs b/src/Sonarr.Http/Middleware/LoggingMiddleware.cs new file mode 100644 index 000000000..7344c58b1 --- /dev/null +++ b/src/Sonarr.Http/Middleware/LoggingMiddleware.cs @@ -0,0 +1,92 @@ +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using NLog; +using NzbDrone.Common.Extensions; +using Sonarr.Http.ErrorManagement; +using Sonarr.Http.Extensions; + +namespace Sonarr.Http.Middleware +{ + public class LoggingMiddleware + { + private static readonly Logger _loggerHttp = LogManager.GetLogger("Http"); + private static readonly Logger _loggerApi = LogManager.GetLogger("Api"); + private static int _requestSequenceID; + + private readonly SonarrErrorPipeline _errorHandler; + private readonly RequestDelegate _next; + + public LoggingMiddleware(RequestDelegate next, + SonarrErrorPipeline errorHandler) + { + _next = next; + _errorHandler = errorHandler; + } + + public async Task InvokeAsync(HttpContext context) + { + LogStart(context); + + await _next(context); + + LogEnd(context); + } + + private void LogStart(HttpContext context) + { + var id = Interlocked.Increment(ref _requestSequenceID); + + context.Items["ApiRequestSequenceID"] = id; + context.Items["ApiRequestStartTime"] = DateTime.UtcNow; + + var reqPath = GetRequestPathAndQuery(context.Request); + + _loggerHttp.Trace("Req: {0} [{1}] {2} (from {3})", id, context.Request.Method, reqPath, GetOrigin(context)); + } + + private void LogEnd(HttpContext context) + { + var id = (int)context.Items["ApiRequestSequenceID"]; + var startTime = (DateTime)context.Items["ApiRequestStartTime"]; + + var endTime = DateTime.UtcNow; + var duration = endTime - startTime; + + var reqPath = GetRequestPathAndQuery(context.Request); + + _loggerHttp.Trace("Res: {0} [{1}] {2}: {3}.{4} ({5} ms)", id, context.Request.Method, reqPath, context.Response.StatusCode, (HttpStatusCode)context.Response.StatusCode, (int)duration.TotalMilliseconds); + + if (context.Request.IsApiRequest()) + { + _loggerApi.Debug("[{0}] {1}: {2}.{3} ({4} ms)", context.Request.Method, reqPath, context.Response.StatusCode, (HttpStatusCode)context.Response.StatusCode, (int)duration.TotalMilliseconds); + } + } + + private static string GetRequestPathAndQuery(HttpRequest request) + { + if (request.QueryString.Value.IsNotNullOrWhiteSpace() && request.QueryString.Value != "?") + { + return string.Concat(request.Path, request.QueryString); + } + else + { + return request.Path; + } + } + + private static string GetOrigin(HttpContext context) + { + if (context.Request.Headers["User-Agent"].ToString().IsNullOrWhiteSpace()) + { + return context.GetRemoteIP(); + } + else + { + return $"{context.GetRemoteIP()} {context.Request.Headers["User-Agent"]}"; + } + } + } +} diff --git a/src/Sonarr.Http/Middleware/UrlBaseMiddleware.cs b/src/Sonarr.Http/Middleware/UrlBaseMiddleware.cs new file mode 100644 index 000000000..9f85731d2 --- /dev/null +++ b/src/Sonarr.Http/Middleware/UrlBaseMiddleware.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using NzbDrone.Common.Extensions; + +namespace Sonarr.Http.Middleware +{ + public class UrlBaseMiddleware + { + private readonly RequestDelegate _next; + private readonly string _urlBase; + + public UrlBaseMiddleware(RequestDelegate next, string urlBase) + { + _next = next; + _urlBase = urlBase; + } + + public async Task InvokeAsync(HttpContext context) + { + if (_urlBase.IsNotNullOrWhiteSpace() && context.Request.PathBase.Value.IsNullOrWhiteSpace()) + { + context.Response.Redirect($"{_urlBase}{context.Request.Path}{context.Request.QueryString}"); + return; + } + + await _next(context); + } + } +} diff --git a/src/Sonarr.Http/Middleware/VersionMiddleware.cs b/src/Sonarr.Http/Middleware/VersionMiddleware.cs new file mode 100644 index 000000000..79a3f84d9 --- /dev/null +++ b/src/Sonarr.Http/Middleware/VersionMiddleware.cs @@ -0,0 +1,30 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using NzbDrone.Common.EnvironmentInfo; + +namespace Sonarr.Http.Middleware +{ + public class VersionMiddleware + { + private const string VERSIONHEADER = "X-Application-Version"; + + private readonly RequestDelegate _next; + private readonly string _version; + + public VersionMiddleware(RequestDelegate next) + { + _next = next; + _version = BuildInfo.Version.ToString(); + } + + public async Task InvokeAsync(HttpContext context) + { + if (!context.Response.Headers.ContainsKey(VERSIONHEADER)) + { + context.Response.Headers.Add(VERSIONHEADER, _version); + } + + await _next(context); + } + } +} diff --git a/src/Sonarr.Http/Ping/PingController.cs b/src/Sonarr.Http/Ping/PingController.cs new file mode 100644 index 000000000..cff0e9bfa --- /dev/null +++ b/src/Sonarr.Http/Ping/PingController.cs @@ -0,0 +1,39 @@ +using System; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Configuration; +using NzbDrone.Http.Ping; + +namespace NzbDrone.Http +{ + public class PingController : Controller + { + private readonly IConfigRepository _configRepository; + + public PingController(IConfigRepository configRepository) + { + _configRepository = configRepository; + } + + [HttpGet("/ping")] + public IActionResult GetStatus() + { + try + { + _configRepository.All(); + } + catch (Exception) + { + return StatusCode(StatusCodes.Status500InternalServerError, new PingResource + { + Status = "Error" + }); + } + + return StatusCode(StatusCodes.Status200OK, new PingResource + { + Status = "OK" + }); + } + } +} diff --git a/src/Sonarr.Http/Ping/PingModule.cs b/src/Sonarr.Http/Ping/PingModule.cs deleted file mode 100644 index 99f699343..000000000 --- a/src/Sonarr.Http/Ping/PingModule.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using Nancy; -using NzbDrone.Core.Configuration; -using NzbDrone.Http.Ping; -using Sonarr.Http.Extensions; - -namespace NzbDrone.Http -{ - public class PingModule : NancyModule - { - private readonly IConfigRepository _configRepository; - - public PingModule(IConfigRepository configRepository) - { - _configRepository = configRepository; - - Get("/ping", x => GetStatus()); - } - - private Response GetStatus() - { - try - { - _configRepository.All(); - } - catch (Exception e) - { - return new PingResource - { - Status = "Error" - }.AsResponse(Context, HttpStatusCode.InternalServerError); - } - - return new PingResource - { - Status = "OK" - }.AsResponse(Context, HttpStatusCode.OK); - } - } -} diff --git a/src/Sonarr.Http/REST/Attributes/RestDeleteByIdAttribute.cs b/src/Sonarr.Http/REST/Attributes/RestDeleteByIdAttribute.cs new file mode 100644 index 000000000..d3d9ef1e3 --- /dev/null +++ b/src/Sonarr.Http/REST/Attributes/RestDeleteByIdAttribute.cs @@ -0,0 +1,14 @@ +using System; +using Microsoft.AspNetCore.Mvc; + +namespace Sonarr.Http.REST.Attributes +{ + [AttributeUsage(AttributeTargets.Method)] + public class RestDeleteByIdAttribute : HttpDeleteAttribute + { + public RestDeleteByIdAttribute() + : base("{id:int}") + { + } + } +} diff --git a/src/Sonarr.Http/REST/Attributes/RestGetByIdAttribute.cs b/src/Sonarr.Http/REST/Attributes/RestGetByIdAttribute.cs new file mode 100644 index 000000000..a07f297fb --- /dev/null +++ b/src/Sonarr.Http/REST/Attributes/RestGetByIdAttribute.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.Routing; + +namespace Sonarr.Http.REST.Attributes +{ + [AttributeUsage(AttributeTargets.Method)] + public class RestGetByIdAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider + { + public IEnumerable HttpMethods => new[] { "GET" }; + public string Template => "{id:int}"; + public int? Order => 0; + public string Name { get; } + } +} diff --git a/src/Sonarr.Http/REST/Attributes/RestPostByIdAttribute.cs b/src/Sonarr.Http/REST/Attributes/RestPostByIdAttribute.cs new file mode 100644 index 000000000..e31ec158b --- /dev/null +++ b/src/Sonarr.Http/REST/Attributes/RestPostByIdAttribute.cs @@ -0,0 +1,10 @@ +using System; +using Microsoft.AspNetCore.Mvc; + +namespace Sonarr.Http.REST.Attributes +{ + [AttributeUsage(AttributeTargets.Method)] + public class RestPostByIdAttribute : HttpPostAttribute + { + } +} diff --git a/src/Sonarr.Http/REST/Attributes/RestPutByIdAttribute.cs b/src/Sonarr.Http/REST/Attributes/RestPutByIdAttribute.cs new file mode 100644 index 000000000..86cc98a0d --- /dev/null +++ b/src/Sonarr.Http/REST/Attributes/RestPutByIdAttribute.cs @@ -0,0 +1,14 @@ +using System; +using Microsoft.AspNetCore.Mvc; + +namespace Sonarr.Http.REST.Attributes +{ + [AttributeUsage(AttributeTargets.Method)] + public class RestPutByIdAttribute : HttpPutAttribute + { + public RestPutByIdAttribute() + : base("{id:int?}") + { + } + } +} diff --git a/src/Sonarr.Http/REST/Attributes/SkipValidationAttribute.cs b/src/Sonarr.Http/REST/Attributes/SkipValidationAttribute.cs new file mode 100644 index 000000000..926aadf5d --- /dev/null +++ b/src/Sonarr.Http/REST/Attributes/SkipValidationAttribute.cs @@ -0,0 +1,17 @@ +using System; + +namespace Sonarr.Http.REST.Attributes +{ + [AttributeUsage(AttributeTargets.Method)] + public class SkipValidationAttribute : Attribute + { + public SkipValidationAttribute(bool skip = true, bool skipShared = true) + { + Skip = skip; + SkipShared = skipShared; + } + + public bool Skip { get; } + public bool SkipShared { get; } + } +} diff --git a/src/Sonarr.Http/REST/BadRequestException.cs b/src/Sonarr.Http/REST/BadRequestException.cs index 4129ec588..ee84d2b87 100644 --- a/src/Sonarr.Http/REST/BadRequestException.cs +++ b/src/Sonarr.Http/REST/BadRequestException.cs @@ -1,4 +1,4 @@ -using Nancy; +using System.Net; using Sonarr.Http.Exceptions; namespace Sonarr.Http.REST diff --git a/src/Sonarr.Http/REST/MethodNotAllowedException.cs b/src/Sonarr.Http/REST/MethodNotAllowedException.cs index 6d34170fc..ec889172f 100644 --- a/src/Sonarr.Http/REST/MethodNotAllowedException.cs +++ b/src/Sonarr.Http/REST/MethodNotAllowedException.cs @@ -1,4 +1,4 @@ -using Nancy; +using System.Net; using Sonarr.Http.Exceptions; namespace Sonarr.Http.REST diff --git a/src/Sonarr.Http/REST/NotFoundException.cs b/src/Sonarr.Http/REST/NotFoundException.cs index 25e862420..ce2cd35c7 100644 --- a/src/Sonarr.Http/REST/NotFoundException.cs +++ b/src/Sonarr.Http/REST/NotFoundException.cs @@ -1,4 +1,4 @@ -using Nancy; +using System.Net; using Sonarr.Http.Exceptions; namespace Sonarr.Http.REST diff --git a/src/Sonarr.Http/REST/RestController.cs b/src/Sonarr.Http/REST/RestController.cs new file mode 100644 index 000000000..ebb9e1e00 --- /dev/null +++ b/src/Sonarr.Http/REST/RestController.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using FluentValidation.Results; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Filters; +using NzbDrone.Core.Datastore; +using Sonarr.Http.REST.Attributes; +using Sonarr.Http.Validation; + +namespace Sonarr.Http.REST +{ + public abstract class RestController : Controller + where TResource : RestResource, new() + { + private static readonly List VALIDATE_ID_ATTRIBUTES = new List { typeof(RestPutByIdAttribute), typeof(RestDeleteByIdAttribute) }; + + protected ResourceValidator PostValidator { get; private set; } + protected ResourceValidator PutValidator { get; private set; } + protected ResourceValidator SharedValidator { get; private set; } + + protected void ValidateId(int id) + { + if (id <= 0) + { + throw new BadRequestException(id + " is not a valid ID"); + } + } + + protected RestController() + { + PostValidator = new ResourceValidator(); + PutValidator = new ResourceValidator(); + SharedValidator = new ResourceValidator(); + + PutValidator.RuleFor(r => r.Id).ValidId(); + } + + [RestGetById] + public ActionResult GetResourceByIdWithErrorHandler(int id) + { + try + { + return GetResourceById(id); + } + catch (ModelNotFoundException) + { + return NotFound(); + } + } + + protected abstract TResource GetResourceById(int id); + + public override void OnActionExecuting(ActionExecutingContext context) + { + var descriptor = context.ActionDescriptor as ControllerActionDescriptor; + + var skipAttribute = (SkipValidationAttribute)Attribute.GetCustomAttribute(descriptor.MethodInfo, typeof(SkipValidationAttribute), true); + var skipValidate = skipAttribute?.Skip ?? false; + var skipShared = skipAttribute?.SkipShared ?? false; + + if (Request.Method == "POST" || Request.Method == "PUT") + { + var resourceArgs = context.ActionArguments.Values.Where(x => x.GetType() == typeof(TResource)) + .Select(x => x as TResource) + .ToList(); + + foreach (var resource in resourceArgs) + { + ValidateResource(resource, skipValidate, skipShared); + } + } + + var attributes = descriptor.MethodInfo.CustomAttributes; + if (attributes.Any(x => VALIDATE_ID_ATTRIBUTES.Contains(x.GetType())) && !skipValidate) + { + if (context.ActionArguments.TryGetValue("id", out var idObj)) + { + ValidateId((int)idObj); + } + } + + base.OnActionExecuting(context); + } + + protected void ValidateResource(TResource resource, bool skipValidate = false, bool skipSharedValidate = false) + { + if (resource == null) + { + throw new BadRequestException("Request body can't be empty"); + } + + var errors = new List(); + + if (!skipSharedValidate) + { + errors.AddRange(SharedValidator.Validate(resource).Errors); + } + + if (Request.Method.Equals("POST", StringComparison.InvariantCultureIgnoreCase) && !skipValidate && !Request.Path.ToString().EndsWith("/test", StringComparison.InvariantCultureIgnoreCase)) + { + errors.AddRange(PostValidator.Validate(resource).Errors); + } + else if (Request.Method.Equals("PUT", StringComparison.InvariantCultureIgnoreCase)) + { + errors.AddRange(PutValidator.Validate(resource).Errors); + } + + if (errors.Any()) + { + throw new ValidationException(errors); + } + } + + protected ActionResult Accepted(int id) + { + var result = GetResourceById(id); + return AcceptedAtAction(nameof(GetResourceByIdWithErrorHandler), new { id = id }, result); + } + + protected ActionResult Created(int id) + { + var result = GetResourceById(id); + return CreatedAtAction(nameof(GetResourceByIdWithErrorHandler), new { id = id }, result); + } + } +} diff --git a/src/Sonarr.Http/SonarrRestModuleWithSignalR.cs b/src/Sonarr.Http/REST/RestControllerWithSignalR.cs similarity index 62% rename from src/Sonarr.Http/SonarrRestModuleWithSignalR.cs rename to src/Sonarr.Http/REST/RestControllerWithSignalR.cs index 64183d447..408a97005 100644 --- a/src/Sonarr.Http/SonarrRestModuleWithSignalR.cs +++ b/src/Sonarr.Http/REST/RestControllerWithSignalR.cs @@ -1,33 +1,35 @@ +using System.Reflection; +using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.SignalR; -using Sonarr.Http.REST; -namespace Sonarr.Http +namespace Sonarr.Http.REST { - public abstract class SonarrRestModuleWithSignalR : SonarrRestModule, IHandle> + public abstract class RestControllerWithSignalR : RestController, IHandle> where TResource : RestResource, new() where TModel : ModelBase, new() { + protected string Resource { get; } private readonly IBroadcastSignalRMessage _signalRBroadcaster; - protected SonarrRestModuleWithSignalR(IBroadcastSignalRMessage signalRBroadcaster) + protected RestControllerWithSignalR(IBroadcastSignalRMessage signalRBroadcaster) { _signalRBroadcaster = signalRBroadcaster; + + var apiAttribute = GetType().GetCustomAttribute(); + if (apiAttribute != null && apiAttribute.Resource != VersionedApiControllerAttribute.CONTROLLER_RESOURCE) + { + Resource = apiAttribute.Resource; + } + else + { + Resource = new TResource().ResourceName.Trim('/'); + } } - protected SonarrRestModuleWithSignalR(IBroadcastSignalRMessage signalRBroadcaster, string resource) - : base(resource) - { - _signalRBroadcaster = signalRBroadcaster; - } - - protected virtual TResource GetResourceByIdForBroadcast(int id) - { - return GetResourceById(id); - } - + [NonAction] public void Handle(ModelEvent message) { if (!_signalRBroadcaster.IsConnected) @@ -40,7 +42,7 @@ namespace Sonarr.Http BroadcastResourceChange(message.Action); } - BroadcastResourceChange(message.Action, message.ModelId); + BroadcastResourceChange(message.Action, message.Model.Id); } protected void BroadcastResourceChange(ModelAction action, int id) @@ -56,7 +58,7 @@ namespace Sonarr.Http } else { - var resource = GetResourceByIdForBroadcast(id); + var resource = GetResourceById(id); BroadcastResourceChange(action, resource); } } @@ -71,11 +73,11 @@ namespace Sonarr.Http if (GetType().Namespace.Contains("V3")) { var signalRMessage = new SignalRMessage - { - Name = Resource, - Body = new ResourceChangeMessage(resource, action), - Action = action - }; + { + Name = Resource, + Body = new ResourceChangeMessage(resource, action), + Action = action + }; _signalRBroadcaster.BroadcastMessage(signalRMessage); } @@ -91,11 +93,11 @@ namespace Sonarr.Http if (GetType().Namespace.Contains("V3")) { var signalRMessage = new SignalRMessage - { - Name = Resource, - Body = new ResourceChangeMessage(action), - Action = action - }; + { + Name = Resource, + Body = new ResourceChangeMessage(action), + Action = action + }; _signalRBroadcaster.BroadcastMessage(signalRMessage); } diff --git a/src/Sonarr.Http/REST/RestModule.cs b/src/Sonarr.Http/REST/RestModule.cs deleted file mode 100644 index 94363df25..000000000 --- a/src/Sonarr.Http/REST/RestModule.cs +++ /dev/null @@ -1,350 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using FluentValidation; -using FluentValidation.Results; -using Nancy; -using Nancy.Responses.Negotiation; -using NzbDrone.Core.Datastore; -using Sonarr.Http.Extensions; - -namespace Sonarr.Http.REST -{ - public abstract class RestModule : NancyModule - where TResource : RestResource, new() - { - private const string ROOT_ROUTE = "/"; - private const string ID_ROUTE = @"/(?[\d]{1,10})"; - - private HashSet _excludedKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase) - { - "page", - "pageSize", - "sortKey", - "sortDirection", - "filterKey", - "filterValue", - }; - - private Action _deleteResource; - private Func _getResourceById; - private Func> _getResourceAll; - private Func, PagingResource> _getResourcePaged; - private Func _getResourceSingle; - private Func _createResource; - private Action _updateResource; - - protected ResourceValidator PostValidator { get; private set; } - protected ResourceValidator PutValidator { get; private set; } - protected ResourceValidator SharedValidator { get; private set; } - - protected void ValidateId(int id) - { - if (id <= 0) - { - throw new BadRequestException(id + " is not a valid ID"); - } - } - - protected RestModule(string modulePath) - : base(modulePath) - { - ValidateModule(); - - PostValidator = new ResourceValidator(); - PutValidator = new ResourceValidator(); - SharedValidator = new ResourceValidator(); - } - - private void ValidateModule() - { - if (GetResourceById != null) - { - return; - } - - if (CreateResource != null || UpdateResource != null) - { - throw new InvalidOperationException("GetResourceById route must be defined before defining Create/Update routes."); - } - } - - protected Action DeleteResource - { - private get - { - return _deleteResource; - } - - set - { - _deleteResource = value; - Delete(ID_ROUTE, options => - { - ValidateId(options.Id); - DeleteResource((int)options.Id); - - return new object(); - }); - } - } - - protected Func GetResourceById - { - get - { - return _getResourceById; - } - - set - { - _getResourceById = value; - Get(ID_ROUTE, options => - { - ValidateId(options.Id); - try - { - var resource = GetResourceById((int)options.Id); - - if (resource == null) - { - return new NotFoundResponse(); - } - - return resource; - } - catch (ModelNotFoundException) - { - return new NotFoundResponse(); - } - }); - } - } - - protected Func> GetResourceAll - { - private get - { - return _getResourceAll; - } - - set - { - _getResourceAll = value; - Get(ROOT_ROUTE, options => - { - var resource = GetResourceAll(); - return resource; - }); - } - } - - protected Func, PagingResource> GetResourcePaged - { - private get - { - return _getResourcePaged; - } - - set - { - _getResourcePaged = value; - Get(ROOT_ROUTE, options => - { - var resource = GetResourcePaged(ReadPagingResourceFromRequest()); - return resource; - }); - } - } - - protected Func GetResourceSingle - { - private get - { - return _getResourceSingle; - } - - set - { - _getResourceSingle = value; - Get(ROOT_ROUTE, options => - { - var resource = GetResourceSingle(); - return resource; - }); - } - } - - protected Func CreateResource - { - private get - { - return _createResource; - } - - set - { - _createResource = value; - Post(ROOT_ROUTE, options => - { - var id = CreateResource(ReadResourceFromRequest()); - return ResponseWithCode(GetResourceById(id), HttpStatusCode.Created); - }); - } - } - - protected Action UpdateResource - { - private get - { - return _updateResource; - } - - set - { - _updateResource = value; - Put(ROOT_ROUTE, options => - { - var resource = ReadResourceFromRequest(); - UpdateResource(resource); - return ResponseWithCode(GetResourceById(resource.Id), HttpStatusCode.Accepted); - }); - Put(ID_ROUTE, options => - { - var resource = ReadResourceFromRequest(); - resource.Id = options.Id; - UpdateResource(resource); - return ResponseWithCode(GetResourceById(resource.Id), HttpStatusCode.Accepted); - }); - } - } - - protected Negotiator ResponseWithCode(object model, HttpStatusCode statusCode) - { - return Negotiate.WithModel(model).WithStatusCode(statusCode); - } - - protected TResource ReadResourceFromRequest(bool skipValidate = false, bool skipSharedValidate = false) - { - TResource resource; - - try - { - resource = Request.Body.FromJson(); - } - catch (JsonException e) - { - throw new BadRequestException($"Invalid request body. {e.Message}"); - } - - if (resource == null) - { - throw new BadRequestException("Request body can't be empty"); - } - - var errors = new List(); - - if (!skipSharedValidate) - { - errors.AddRange(SharedValidator.Validate(resource).Errors); - } - - if (Request.Method.Equals("POST", StringComparison.InvariantCultureIgnoreCase) && !skipValidate && !Request.Url.Path.EndsWith("/test", StringComparison.InvariantCultureIgnoreCase)) - { - errors.AddRange(PostValidator.Validate(resource).Errors); - } - else if (Request.Method.Equals("PUT", StringComparison.InvariantCultureIgnoreCase)) - { - errors.AddRange(PutValidator.Validate(resource).Errors); - } - - if (errors.Any()) - { - throw new ValidationException(errors); - } - - return resource; - } - - private PagingResource ReadPagingResourceFromRequest() - { - int pageSize; - int.TryParse(Request.Query.PageSize.ToString(), out pageSize); - if (pageSize == 0) - { - pageSize = 10; - } - - int page; - int.TryParse(Request.Query.Page.ToString(), out page); - if (page == 0) - { - page = 1; - } - - var pagingResource = new PagingResource - { - PageSize = pageSize, - Page = page, - Filters = new List() - }; - - if (Request.Query.SortKey != null) - { - pagingResource.SortKey = Request.Query.SortKey.ToString(); - - // For backwards compatibility with v2 - if (Request.Query.SortDir != null) - { - pagingResource.SortDirection = Request.Query.SortDir.ToString() - .Equals("Asc", StringComparison.InvariantCultureIgnoreCase) - ? SortDirection.Ascending - : SortDirection.Descending; - } - - // v3 uses SortDirection instead of SortDir to be consistent with every other use of it - if (Request.Query.SortDirection != null) - { - pagingResource.SortDirection = Request.Query.SortDirection.ToString() - .Equals("ascending", StringComparison.InvariantCultureIgnoreCase) - ? SortDirection.Ascending - : SortDirection.Descending; - } - } - - // For backwards compatibility with v2 - if (Request.Query.FilterKey != null) - { - var filter = new PagingResourceFilter - { - Key = Request.Query.FilterKey.ToString() - }; - - if (Request.Query.FilterValue != null) - { - filter.Value = Request.Query.FilterValue?.ToString(); - } - - pagingResource.Filters.Add(filter); - } - - // v3 uses filters in key=value format - foreach (var key in Request.Query) - { - if (_excludedKeys.Contains(key)) - { - continue; - } - - pagingResource.Filters.Add(new PagingResourceFilter - { - Key = key, - Value = Request.Query[key] - }); - } - - return pagingResource; - } - } -} diff --git a/src/Sonarr.Http/REST/UnsupportedMediaTypeException.cs b/src/Sonarr.Http/REST/UnsupportedMediaTypeException.cs index 596d7823c..4831a5946 100644 --- a/src/Sonarr.Http/REST/UnsupportedMediaTypeException.cs +++ b/src/Sonarr.Http/REST/UnsupportedMediaTypeException.cs @@ -1,4 +1,4 @@ -using Nancy; +using System.Net; using Sonarr.Http.Exceptions; namespace Sonarr.Http.REST diff --git a/src/Sonarr.Http/Sonarr.Http.csproj b/src/Sonarr.Http/Sonarr.Http.csproj index ffa9568c3..7f6619b81 100644 --- a/src/Sonarr.Http/Sonarr.Http.csproj +++ b/src/Sonarr.Http/Sonarr.Http.csproj @@ -4,9 +4,7 @@ - - - + diff --git a/src/Sonarr.Http/SonarrBootstrapper.cs b/src/Sonarr.Http/SonarrBootstrapper.cs deleted file mode 100644 index 539254cbf..000000000 --- a/src/Sonarr.Http/SonarrBootstrapper.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Linq; -using Nancy; -using Nancy.Bootstrapper; -using Nancy.Diagnostics; -using Nancy.Responses.Negotiation; -using NLog; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Common.Instrumentation; -using NzbDrone.Core.Instrumentation; -using Sonarr.Http.Extensions.Pipelines; -using TinyIoC; - -namespace Sonarr.Http -{ - public class SonarrBootstrapper : TinyIoCNancyBootstrapper - { - private readonly TinyIoCContainer _tinyIoCContainer; - private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(SonarrBootstrapper)); - - public SonarrBootstrapper(TinyIoCContainer tinyIoCContainer) - { - _tinyIoCContainer = tinyIoCContainer; - } - - protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines) - { - Logger.Info("Starting Web Server"); - - if (RuntimeInfo.IsProduction) - { - DiagnosticsHook.Disable(pipelines); - } - - RegisterPipelines(pipelines); - - container.Resolve().Register(); - } - - private void RegisterPipelines(IPipelines pipelines) - { - var pipelineRegistrars = _tinyIoCContainer.ResolveAll().OrderBy(v => v.Order).ToList(); - - foreach (var registerNancyPipeline in pipelineRegistrars) - { - registerNancyPipeline.Register(pipelines); - } - } - - protected override TinyIoCContainer GetApplicationContainer() - { - return _tinyIoCContainer; - } - - protected override Func InternalConfiguration - { - get - { - // We don't support Xml Serialization atm - return NancyInternalConfiguration.WithOverrides(x => - { - x.ResponseProcessors.Remove(typeof(ViewProcessor)); - x.ResponseProcessors.Remove(typeof(XmlProcessor)); - }); - } - } - - public override void Configure(Nancy.Configuration.INancyEnvironment environment) - { - environment.Diagnostics(password: @"password"); - } - - protected override byte[] FavIcon => null; - } -} diff --git a/src/Sonarr.Http/SonarrModule.cs b/src/Sonarr.Http/SonarrModule.cs deleted file mode 100644 index 684d811c0..000000000 --- a/src/Sonarr.Http/SonarrModule.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Nancy; -using Nancy.Responses.Negotiation; - -namespace Sonarr.Http -{ - public abstract class SonarrModule : NancyModule - { - protected SonarrModule(string resource) - : base(resource) - { - } - - protected Negotiator ResponseWithCode(object model, HttpStatusCode statusCode) - { - return Negotiate.WithModel(model).WithStatusCode(statusCode); - } - } -} diff --git a/src/Sonarr.Http/SonarrRestModule.cs b/src/Sonarr.Http/SonarrRestModule.cs deleted file mode 100644 index 523db1646..000000000 --- a/src/Sonarr.Http/SonarrRestModule.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using NzbDrone.Core.Datastore; -using Sonarr.Http.REST; -using Sonarr.Http.Validation; - -namespace Sonarr.Http -{ - public abstract class SonarrRestModule : RestModule - where TResource : RestResource, new() - { - protected string Resource { get; private set; } - - private static string BaseUrl() - { - var isV3 = typeof(TResource).Namespace.Contains(".V3."); - if (isV3) - { - return "/api/v3/"; - } - - return "/api/"; - } - - private static string ResourceName() - { - return new TResource().ResourceName.Trim('/').ToLower(); - } - - protected SonarrRestModule() - : this(ResourceName()) - { - } - - protected SonarrRestModule(string resource) - : base(BaseUrl() + resource.Trim('/').ToLower()) - { - Resource = resource; - PostValidator.RuleFor(r => r.Id).IsZero(); - PutValidator.RuleFor(r => r.Id).ValidId(); - } - - protected PagingResource ApplyToPage(Func, PagingSpec> function, PagingSpec pagingSpec, Converter mapper) - { - pagingSpec = function(pagingSpec); - - return new PagingResource - { - Page = pagingSpec.Page, - PageSize = pagingSpec.PageSize, - SortDirection = pagingSpec.SortDirection, - SortKey = pagingSpec.SortKey, - TotalRecords = pagingSpec.TotalRecords, - Records = pagingSpec.Records.ConvertAll(mapper) - }; - } - } -} diff --git a/src/Sonarr.Http/TinyIoCNancyBootstrapper.cs b/src/Sonarr.Http/TinyIoCNancyBootstrapper.cs deleted file mode 100644 index dbdeec83f..000000000 --- a/src/Sonarr.Http/TinyIoCNancyBootstrapper.cs +++ /dev/null @@ -1,278 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Nancy; -using Nancy.Bootstrapper; -using Nancy.Configuration; -using Nancy.Diagnostics; -using TinyIoC; - -namespace Sonarr.Http -{ - /// - /// TinyIoC bootstrapper - registers default route resolver and registers itself as - /// INancyModuleCatalog for resolving modules but behaviour can be overridden if required. - /// - public class TinyIoCNancyBootstrapper : NancyBootstrapperWithRequestContainerBase - { - /// - /// Default assemblies that are ignored for autoregister - /// - public static IEnumerable> DefaultAutoRegisterIgnoredAssemblies = new Func[] - { - asm => !asm.FullName.StartsWith("Nancy.", StringComparison.InvariantCulture) - }; - - /// - /// Gets the assemblies to ignore when autoregistering the application container - /// Return true from the delegate to ignore that particular assembly, returning false - /// does not mean the assembly *will* be included, a true from another delegate will - /// take precedence. - /// - protected virtual IEnumerable> AutoRegisterIgnoredAssemblies => DefaultAutoRegisterIgnoredAssemblies; - - /// - /// Configures the container using AutoRegister followed by registration - /// of default INancyModuleCatalog and IRouteResolver. - /// - /// Container instance - protected override void ConfigureApplicationContainer(TinyIoCContainer container) - { - AutoRegister(container, this.AutoRegisterIgnoredAssemblies); - } - - /// - /// Resolve INancyEngine - /// - /// INancyEngine implementation - protected override sealed INancyEngine GetEngineInternal() - { - return this.ApplicationContainer.Resolve(); - } - - // Summary: - // Gets the Nancy.Configuration.INancyEnvironmentConfigurator used by th. - // - // Returns: - // An Nancy.Configuration.INancyEnvironmentConfigurator instance. - protected override INancyEnvironmentConfigurator GetEnvironmentConfigurator() - { - return this.ApplicationContainer.Resolve(); - } - - // Summary: - // Get the Nancy.Configuration.INancyEnvironment instance. - // - // Returns: - // An configured Nancy.Configuration.INancyEnvironment instance. - // - // Remarks: - // The boostrapper must be initialised (Nancy.Bootstrapper.INancyBootstrapper.Initialise) - // prior to calling this. - public override INancyEnvironment GetEnvironment() - { - return this.ApplicationContainer.Resolve(); - } - - // Summary: - // Registers an Nancy.Configuration.INancyEnvironment instance in the container. - // - // Parameters: - // container: - // The container to register into. - // - // environment: - // The Nancy.Configuration.INancyEnvironment instance to register. - protected override void RegisterNancyEnvironment(TinyIoCContainer container, INancyEnvironment environment) - { - ApplicationContainer.Register(environment); - } - - /// - /// Create a default, unconfigured, container - /// - /// Container instance - protected override TinyIoCContainer GetApplicationContainer() - { - return new TinyIoCContainer(); - } - - /// - /// Register the bootstrapper's implemented types into the container. - /// This is necessary so a user can pass in a populated container but not have - /// to take the responsibility of registering things like INancyModuleCatalog manually. - /// - /// Application container to register into - protected override sealed void RegisterBootstrapperTypes(TinyIoCContainer applicationContainer) - { - applicationContainer.Register(this); - } - - /// - /// Register the default implementations of internally used types into the container as singletons - /// - /// Container to register into - /// Type registrations to register - protected override sealed void RegisterTypes(TinyIoCContainer container, IEnumerable typeRegistrations) - { - foreach (var typeRegistration in typeRegistrations) - { - switch (typeRegistration.Lifetime) - { - case Lifetime.Transient: - container.Register(typeRegistration.RegistrationType, typeRegistration.ImplementationType).AsMultiInstance(); - break; - case Lifetime.Singleton: - container.Register(typeRegistration.RegistrationType, typeRegistration.ImplementationType).AsSingleton(); - break; - case Lifetime.PerRequest: - throw new InvalidOperationException("Unable to directly register a per request lifetime."); - default: - throw new ArgumentOutOfRangeException(); - } - } - } - - /// - /// Register the various collections into the container as singletons to later be resolved - /// by IEnumerable{Type} constructor dependencies. - /// - /// Container to register into - /// Collection type registrations to register - protected override sealed void RegisterCollectionTypes(TinyIoCContainer container, IEnumerable collectionTypeRegistrations) - { - foreach (var collectionTypeRegistration in collectionTypeRegistrations) - { - switch (collectionTypeRegistration.Lifetime) - { - case Lifetime.Transient: - container.RegisterMultiple(collectionTypeRegistration.RegistrationType, collectionTypeRegistration.ImplementationTypes).AsMultiInstance(); - break; - case Lifetime.Singleton: - container.RegisterMultiple(collectionTypeRegistration.RegistrationType, collectionTypeRegistration.ImplementationTypes).AsSingleton(); - break; - case Lifetime.PerRequest: - throw new InvalidOperationException("Unable to directly register a per request lifetime."); - default: - throw new ArgumentOutOfRangeException(); - } - } - } - - /// - /// Register the given module types into the container - /// - /// Container to register into - /// NancyModule types - protected override sealed void RegisterRequestContainerModules(TinyIoCContainer container, IEnumerable moduleRegistrationTypes) - { - foreach (var moduleRegistrationType in moduleRegistrationTypes) - { - container.Register( - typeof(INancyModule), - moduleRegistrationType.ModuleType, - moduleRegistrationType.ModuleType.FullName). - AsSingleton(); - } - } - - /// - /// Register the given instances into the container - /// - /// Container to register into - /// Instance registration types - protected override void RegisterInstances(TinyIoCContainer container, IEnumerable instanceRegistrations) - { - foreach (var instanceRegistration in instanceRegistrations) - { - container.Register( - instanceRegistration.RegistrationType, - instanceRegistration.Implementation); - } - } - - /// - /// Creates a per request child/nested container - /// - /// Current context - /// Request container instance - protected override TinyIoCContainer CreateRequestContainer(NancyContext context) - { - return this.ApplicationContainer.GetChildContainer(); - } - - /// - /// Gets the diagnostics for initialisation - /// - /// IDiagnostics implementation - protected override IDiagnostics GetDiagnostics() - { - return this.ApplicationContainer.Resolve(); - } - - /// - /// Gets all registered startup tasks - /// - /// An instance containing instances. - protected override IEnumerable GetApplicationStartupTasks() - { - return this.ApplicationContainer.ResolveAll(false); - } - - /// - /// Gets all registered request startup tasks - /// - /// An instance containing instances. - protected override IEnumerable RegisterAndGetRequestStartupTasks(TinyIoCContainer container, Type[] requestStartupTypes) - { - container.RegisterMultiple(typeof(IRequestStartup), requestStartupTypes); - - return container.ResolveAll(false); - } - - /// - /// Gets all registered application registration tasks - /// - /// An instance containing instances. - protected override IEnumerable GetRegistrationTasks() - { - return this.ApplicationContainer.ResolveAll(false); - } - - /// - /// Retrieve all module instances from the container - /// - /// Container to use - /// Collection of NancyModule instances - protected override sealed IEnumerable GetAllModules(TinyIoCContainer container) - { - var nancyModules = container.ResolveAll(false); - return nancyModules; - } - - /// - /// Retrieve a specific module instance from the container - /// - /// Container to use - /// Type of the module - /// NancyModule instance - protected override sealed INancyModule GetModule(TinyIoCContainer container, Type moduleType) - { - container.Register(typeof(INancyModule), moduleType); - - return container.Resolve(); - } - - /// - /// Executes auto registation with the given container. - /// - /// Container instance - private static void AutoRegister(TinyIoCContainer container, IEnumerable> ignoredAssemblies) - { - var assembly = typeof(NancyEngine).Assembly; - - container.AutoRegister(AppDomain.CurrentDomain.GetAssemblies().Where(a => !ignoredAssemblies.Any(ia => ia(a))), DuplicateImplementationActions.RegisterMultiple, t => t.Assembly != assembly); - } - } -} diff --git a/src/Sonarr.Http/Validation/DuplicateEndpointDetector.cs b/src/Sonarr.Http/Validation/DuplicateEndpointDetector.cs new file mode 100644 index 000000000..85821f646 --- /dev/null +++ b/src/Sonarr.Http/Validation/DuplicateEndpointDetector.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using ImpromptuInterface; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Matching; +using Microsoft.Extensions.DependencyInjection; + +namespace Sonarr.Http.Validation +{ + public interface IDfaMatcherBuilder + { + void AddEndpoint(RouteEndpoint endpoint); + object BuildDfaTree(bool includeLabel = false); + } + + // https://github.com/dotnet/aspnetcore/blob/cc3d47f5501cdfae3e5b5be509ef2c0fb8cca069/src/Http/Routing/src/Matching/DfaNode.cs + public interface IDfaNode + { + string Label { get; set; } + List Matches { get; } + IDictionary Literals { get; } + object Parameters { get; } + object CatchAll { get; } + IDictionary PolicyEdges { get; } + } + + public class DuplicateEndpointDetector + { + private readonly IServiceProvider _services; + + public DuplicateEndpointDetector(IServiceProvider services) + { + _services = services; + } + + public Dictionary> GetDuplicateEndpoints(EndpointDataSource dataSource) + { + // get the DfaMatcherBuilder - internal, so needs reflection :( + var matcherBuilder = typeof(IEndpointSelectorPolicy).Assembly + .GetType("Microsoft.AspNetCore.Routing.Matching.DfaMatcherBuilder"); + + var rawBuilder = _services.GetRequiredService(matcherBuilder); + var builder = rawBuilder.ActLike(); + + var endpoints = dataSource.Endpoints; + foreach (var t in endpoints) + { + if (t is RouteEndpoint endpoint && (endpoint.Metadata.GetMetadata()?.SuppressMatching ?? false) == false) + { + builder.AddEndpoint(endpoint); + } + } + + // Assign each node a sequential index. + var visited = new Dictionary(); + var duplicates = new Dictionary>(); + + var rawTree = builder.BuildDfaTree(includeLabel: true); + + Visit(rawTree, LogDuplicates); + + return duplicates; + + void LogDuplicates(IDfaNode node) + { + if (!visited.TryGetValue(node, out var label)) + { + label = visited.Count; + visited.Add(node, label); + } + + // We can safely index into visited because this is a post-order traversal, + // all of the children of this node are already in the dictionary. + var filteredMatches = node?.Matches?.Where(x => !x.DisplayName.StartsWith("Sonarr.Http.Frontend.StaticResourceController")).ToList(); + var matchCount = filteredMatches?.Count ?? 0; + if (matchCount > 1) + { + var duplicateEndpoints = filteredMatches.Select(x => x.DisplayName).ToList(); + duplicates[node.Label] = duplicateEndpoints; + } + } + } + + private static void Visit(object rawNode, Action visitor) + { + var node = rawNode.ActLike(); + if (node.Literals?.Values != null) + { + foreach (var dictValue in node.Literals.Values) + { + Visit(dictValue, visitor); + } + } + + // Break cycles + if (node.Parameters != null && !ReferenceEquals(rawNode, node.Parameters)) + { + Visit(node.Parameters, visitor); + } + + // Break cycles + if (node.CatchAll != null && !ReferenceEquals(rawNode, node.CatchAll)) + { + Visit(node.CatchAll, visitor); + } + + if (node.PolicyEdges?.Values != null) + { + foreach (var dictValue in node.PolicyEdges.Values) + { + Visit(dictValue, visitor); + } + } + + visitor(node); + } + } +} diff --git a/src/Sonarr.Http/VersionedApiControllerAttribute.cs b/src/Sonarr.Http/VersionedApiControllerAttribute.cs new file mode 100644 index 000000000..331494f47 --- /dev/null +++ b/src/Sonarr.Http/VersionedApiControllerAttribute.cs @@ -0,0 +1,34 @@ +using System; +using Microsoft.AspNetCore.Cors.Infrastructure; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; + +namespace Sonarr.Http +{ + public class VersionedApiControllerAttribute : Attribute, IRouteTemplateProvider, IEnableCorsAttribute, IApiBehaviorMetadata + { + public const string API_CORS_POLICY = "ApiCorsPolicy"; + public const string CONTROLLER_RESOURCE = "[controller]"; + + public VersionedApiControllerAttribute(int version, string resource = CONTROLLER_RESOURCE) + { + Resource = resource; + Template = $"api/v{version}/{resource}"; + PolicyName = API_CORS_POLICY; + } + + public string Resource { get; } + public string Template { get; } + public int? Order => 2; + public string Name { get; set; } + public string PolicyName { get; set; } + } + + public class V3ApiControllerAttribute : VersionedApiControllerAttribute + { + public V3ApiControllerAttribute(string resource = "[controller]") + : base(3, resource) + { + } + } +} diff --git a/src/Sonarr.Http/VersionedFeedControllerAttribute.cs b/src/Sonarr.Http/VersionedFeedControllerAttribute.cs new file mode 100644 index 000000000..239cd09d9 --- /dev/null +++ b/src/Sonarr.Http/VersionedFeedControllerAttribute.cs @@ -0,0 +1,27 @@ +using System; +using Microsoft.AspNetCore.Mvc.Routing; + +namespace Sonarr.Http +{ + public class VersionedFeedControllerAttribute : Attribute, IRouteTemplateProvider + { + public VersionedFeedControllerAttribute(int version, string resource = "[controller]") + { + Version = version; + Template = $"feed/v{Version}/{resource}"; + } + + public string Template { get; private set; } + public int? Order => 2; + public string Name { get; set; } + public int Version { get; private set; } + } + + public class V3FeedControllerAttribute : VersionedFeedControllerAttribute + { + public V3FeedControllerAttribute(string resource = "[controller]") + : base(3, resource) + { + } + } +}