Initial Push

This commit is contained in:
Qstick
2020-10-18 04:18:35 -04:00
parent 09ed2ab889
commit e1020f4107
1845 changed files with 1516 additions and 113975 deletions

View File

@@ -0,0 +1,74 @@
using System;
using Nancy;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
namespace Prowlarr.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;
}
}
}

View File

@@ -0,0 +1,77 @@
using System.IO;
using System.Text;
using Nancy;
using Nancy.Responses;
using NzbDrone.Common;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Analytics;
using NzbDrone.Core.Configuration;
namespace Prowlarr.Http.Frontend
{
public class InitializeJsModule : NancyModule
{
private readonly IConfigFileProvider _configFileProvider;
private readonly IAnalyticsService _analyticsService;
private static string _apiKey;
private static string _urlBase;
private string _generatedContent;
public InitializeJsModule(IConfigFileProvider configFileProvider,
IAnalyticsService analyticsService)
{
_configFileProvider = configFileProvider;
_analyticsService = analyticsService;
_apiKey = configFileProvider.ApiKey;
_urlBase = configFileProvider.UrlBase;
Get("/initialize.js", x => Index());
}
private Response 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;
}
private string GetContent()
{
if (RuntimeInfo.IsProduction && _generatedContent != null)
{
return _generatedContent;
}
var builder = new StringBuilder();
builder.AppendLine("window.Prowlarr = {");
builder.AppendLine($" apiRoot: '{_urlBase}/api/v1',");
builder.AppendLine($" apiKey: '{_apiKey}',");
builder.AppendLine($" release: '{BuildInfo.Release}',");
builder.AppendLine($" version: '{BuildInfo.Version.ToString()}',");
builder.AppendLine($" branch: '{_configFileProvider.Branch.ToLower()}',");
builder.AppendLine($" analytics: {_analyticsService.IsEnabled.ToString().ToLowerInvariant()},");
builder.AppendLine($" userHash: '{HashUtil.AnonymousToken()}',");
builder.AppendLine($" urlBase: '{_urlBase}',");
builder.AppendLine($" isProduction: {RuntimeInfo.IsProduction.ToString().ToLowerInvariant()}");
builder.AppendLine("};");
_generatedContent = builder.ToString();
return _generatedContent;
}
}
}

View File

@@ -0,0 +1,30 @@
using System.IO;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Backup;
namespace Prowlarr.Http.Frontend.Mappers
{
public class BackupFileMapper : StaticResourceMapperBase
{
private readonly IBackupService _backupService;
public BackupFileMapper(IBackupService backupService, IDiskProvider diskProvider, Logger logger)
: base(diskProvider, logger)
{
_backupService = backupService;
}
public override string Map(string resourceUrl)
{
var path = resourceUrl.Replace("/backup/", "").Replace('/', Path.DirectorySeparatorChar);
return Path.Combine(_backupService.GetBackupFolder(), path);
}
public override bool CanHandle(string resourceUrl)
{
return resourceUrl.StartsWith("/backup/") && BackupService.BackupFileRegex.IsMatch(resourceUrl);
}
}
}

View File

@@ -0,0 +1,34 @@
using System.IO;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration;
namespace Prowlarr.Http.Frontend.Mappers
{
public class BrowserConfig : StaticResourceMapperBase
{
private readonly IAppFolderInfo _appFolderInfo;
private readonly IConfigFileProvider _configFileProvider;
public BrowserConfig(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, IConfigFileProvider configFileProvider, Logger logger)
: base(diskProvider, logger)
{
_appFolderInfo = appFolderInfo;
_configFileProvider = configFileProvider;
}
public override string Map(string resourceUrl)
{
var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar);
path = path.Trim(Path.DirectorySeparatorChar);
return Path.ChangeExtension(Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, path), "xml");
}
public override bool CanHandle(string resourceUrl)
{
return resourceUrl.StartsWith("/content/images/icons/browserconfig");
}
}
}

View File

@@ -0,0 +1,43 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Crypto;
using NzbDrone.Common.Extensions;
namespace Prowlarr.Http.Frontend.Mappers
{
public interface ICacheBreakerProvider
{
string AddCacheBreakerToPath(string resourceUrl);
}
public class CacheBreakerProvider : ICacheBreakerProvider
{
private readonly IEnumerable<IMapHttpRequestsToDisk> _diskMappers;
private readonly IHashProvider _hashProvider;
public CacheBreakerProvider(IEnumerable<IMapHttpRequestsToDisk> diskMappers, IHashProvider hashProvider)
{
_diskMappers = diskMappers;
_hashProvider = hashProvider;
}
public string AddCacheBreakerToPath(string resourceUrl)
{
if (!ShouldBreakCache(resourceUrl))
{
return resourceUrl;
}
var mapper = _diskMappers.Single(m => m.CanHandle(resourceUrl));
var pathToFile = mapper.Map(resourceUrl);
var hash = _hashProvider.ComputeMd5(pathToFile).ToBase64();
return resourceUrl + "?h=" + hash.Trim('=');
}
private static bool ShouldBreakCache(string path)
{
return !path.EndsWith(".ics") && !path.EndsWith("main");
}
}
}

View File

@@ -0,0 +1,40 @@
using System.IO;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration;
namespace Prowlarr.Http.Frontend.Mappers
{
public class FaviconMapper : StaticResourceMapperBase
{
private readonly IAppFolderInfo _appFolderInfo;
private readonly IConfigFileProvider _configFileProvider;
public FaviconMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, IConfigFileProvider configFileProvider, Logger logger)
: base(diskProvider, logger)
{
_appFolderInfo = appFolderInfo;
_configFileProvider = configFileProvider;
}
public override string Map(string resourceUrl)
{
var fileName = "favicon.ico";
if (BuildInfo.IsDebug)
{
fileName = "favicon-debug.ico";
}
var path = Path.Combine("Content", "Images", "Icons", fileName);
return Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, path);
}
public override bool CanHandle(string resourceUrl)
{
return resourceUrl.Equals("/favicon.ico");
}
}
}

View File

@@ -0,0 +1,82 @@
using System;
using System.IO;
using System.Text.RegularExpressions;
using Nancy;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
namespace Prowlarr.Http.Frontend.Mappers
{
public abstract class HtmlMapperBase : StaticResourceMapperBase
{
private readonly IDiskProvider _diskProvider;
private readonly Func<ICacheBreakerProvider> _cacheBreakProviderFactory;
private static readonly Regex ReplaceRegex = new Regex(@"(?:(?<attribute>href|src)=\"")(?<path>.*?(?<extension>css|js|png|ico|ics|svg|json))(?:\"")(?:\s(?<nohash>data-no-hash))?", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private string _generatedContent;
protected HtmlMapperBase(IDiskProvider diskProvider,
Func<ICacheBreakerProvider> cacheBreakProviderFactory,
Logger logger)
: base(diskProvider, logger)
{
_diskProvider = diskProvider;
_cacheBreakProviderFactory = cacheBreakProviderFactory;
}
protected string HtmlPath;
protected string UrlBase;
protected override Stream GetContentStream(string filePath)
{
var text = GetHtmlText();
var stream = new MemoryStream();
var writer = new StreamWriter(stream);
writer.Write(text);
writer.Flush();
stream.Position = 0;
return stream;
}
public override Response GetResponse(string resourceUrl)
{
var response = base.GetResponse(resourceUrl);
response.Headers["X-UA-Compatible"] = "IE=edge";
return response;
}
protected string GetHtmlText()
{
if (RuntimeInfo.IsProduction && _generatedContent != null)
{
return _generatedContent;
}
var text = _diskProvider.ReadAllText(HtmlPath);
var cacheBreakProvider = _cacheBreakProviderFactory();
text = ReplaceRegex.Replace(text, match =>
{
string url;
if (match.Groups["nohash"].Success)
{
url = match.Groups["path"].Value;
}
else
{
url = cacheBreakProvider.AddCacheBreakerToPath(match.Groups["path"].Value);
}
return string.Format("{0}=\"{1}{2}\"", match.Groups["attribute"].Value, UrlBase, url);
});
_generatedContent = text;
return _generatedContent;
}
}
}

View File

@@ -0,0 +1,11 @@
using Nancy;
namespace Prowlarr.Http.Frontend.Mappers
{
public interface IMapHttpRequestsToDisk
{
string Map(string resourceUrl);
bool CanHandle(string resourceUrl);
Response GetResponse(string resourceUrl);
}
}

View File

@@ -0,0 +1,42 @@
using System;
using System.IO;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration;
namespace Prowlarr.Http.Frontend.Mappers
{
public class IndexHtmlMapper : HtmlMapperBase
{
private readonly IConfigFileProvider _configFileProvider;
public IndexHtmlMapper(IAppFolderInfo appFolderInfo,
IDiskProvider diskProvider,
IConfigFileProvider configFileProvider,
Func<ICacheBreakerProvider> cacheBreakProviderFactory,
Logger logger)
: base(diskProvider, cacheBreakProviderFactory, logger)
{
_configFileProvider = configFileProvider;
HtmlPath = Path.Combine(appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, "index.html");
UrlBase = configFileProvider.UrlBase;
}
public override string Map(string resourceUrl)
{
return HtmlPath;
}
public override bool CanHandle(string resourceUrl)
{
resourceUrl = resourceUrl.ToLowerInvariant();
return !resourceUrl.StartsWith("/content") &&
!resourceUrl.StartsWith("/mediacover") &&
!resourceUrl.Contains(".") &&
!resourceUrl.StartsWith("/login");
}
}
}

View File

@@ -0,0 +1,32 @@
using System.IO;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
namespace Prowlarr.Http.Frontend.Mappers
{
public class UpdateLogFileMapper : StaticResourceMapperBase
{
private readonly IAppFolderInfo _appFolderInfo;
public UpdateLogFileMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, Logger logger)
: base(diskProvider, logger)
{
_appFolderInfo = appFolderInfo;
}
public override string Map(string resourceUrl)
{
var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar);
path = Path.GetFileName(path);
return Path.Combine(_appFolderInfo.GetUpdateLogFolder(), path);
}
public override bool CanHandle(string resourceUrl)
{
return resourceUrl.StartsWith("/updatelogfile/") && resourceUrl.EndsWith(".txt");
}
}
}

View File

@@ -0,0 +1,33 @@
using System;
using System.IO;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration;
namespace Prowlarr.Http.Frontend.Mappers
{
public class LoginHtmlMapper : HtmlMapperBase
{
public LoginHtmlMapper(IAppFolderInfo appFolderInfo,
IDiskProvider diskProvider,
Func<ICacheBreakerProvider> cacheBreakProviderFactory,
IConfigFileProvider configFileProvider,
Logger logger)
: base(diskProvider, cacheBreakProviderFactory, logger)
{
HtmlPath = Path.Combine(appFolderInfo.StartUpFolder, configFileProvider.UiFolder, "login.html");
UrlBase = configFileProvider.UrlBase;
}
public override string Map(string resourceUrl)
{
return HtmlPath;
}
public override bool CanHandle(string resourceUrl)
{
return resourceUrl.StartsWith("/login");
}
}
}

View File

@@ -0,0 +1,34 @@
using System.IO;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration;
namespace Prowlarr.Http.Frontend.Mappers
{
public class ManifestMapper : StaticResourceMapperBase
{
private readonly IAppFolderInfo _appFolderInfo;
private readonly IConfigFileProvider _configFileProvider;
public ManifestMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, IConfigFileProvider configFileProvider, Logger logger)
: base(diskProvider, logger)
{
_appFolderInfo = appFolderInfo;
_configFileProvider = configFileProvider;
}
public override string Map(string resourceUrl)
{
var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar);
path = path.Trim(Path.DirectorySeparatorChar);
return Path.ChangeExtension(Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, path), "json");
}
public override bool CanHandle(string resourceUrl)
{
return resourceUrl.StartsWith("/Content/Images/Icons/manifest");
}
}
}

View File

@@ -0,0 +1,49 @@
using System;
using System.IO;
using System.Text.RegularExpressions;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
namespace Prowlarr.Http.Frontend.Mappers
{
public class MediaCoverMapper : StaticResourceMapperBase
{
private static readonly Regex RegexResizedImage = new Regex(@"-\d+\.jpg($|\?)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private readonly IAppFolderInfo _appFolderInfo;
private readonly IDiskProvider _diskProvider;
public MediaCoverMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, Logger logger)
: base(diskProvider, logger)
{
_appFolderInfo = appFolderInfo;
_diskProvider = diskProvider;
}
public override string Map(string resourceUrl)
{
var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar);
path = path.Trim(Path.DirectorySeparatorChar);
var resourcePath = Path.Combine(_appFolderInfo.GetAppDataPath(), path);
if (!_diskProvider.FileExists(resourcePath) || _diskProvider.GetFileSize(resourcePath) == 0)
{
var baseResourcePath = RegexResizedImage.Replace(resourcePath, ".jpg$1");
if (baseResourcePath != resourcePath)
{
return baseResourcePath;
}
}
return resourcePath;
}
public override bool CanHandle(string resourceUrl)
{
return resourceUrl.StartsWith("/MediaCover/", StringComparison.InvariantCultureIgnoreCase);
}
}
}

View File

@@ -0,0 +1,33 @@
using System.IO;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration;
namespace Prowlarr.Http.Frontend.Mappers
{
public class RobotsTxtMapper : StaticResourceMapperBase
{
private readonly IAppFolderInfo _appFolderInfo;
private readonly IConfigFileProvider _configFileProvider;
public RobotsTxtMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, IConfigFileProvider configFileProvider, Logger logger)
: base(diskProvider, logger)
{
_appFolderInfo = appFolderInfo;
_configFileProvider = configFileProvider;
}
public override string Map(string resourceUrl)
{
var path = Path.Combine("Content", "robots.txt");
return Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, path);
}
public override bool CanHandle(string resourceUrl)
{
return resourceUrl.Equals("/robots.txt");
}
}
}

View File

@@ -0,0 +1,48 @@
using System.IO;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration;
namespace Prowlarr.Http.Frontend.Mappers
{
public class StaticResourceMapper : StaticResourceMapperBase
{
private readonly IAppFolderInfo _appFolderInfo;
private readonly IConfigFileProvider _configFileProvider;
public StaticResourceMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, IConfigFileProvider configFileProvider, Logger logger)
: base(diskProvider, logger)
{
_appFolderInfo = appFolderInfo;
_configFileProvider = configFileProvider;
}
public override string Map(string resourceUrl)
{
var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar);
path = path.Trim(Path.DirectorySeparatorChar);
return Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, path);
}
public override bool CanHandle(string resourceUrl)
{
resourceUrl = resourceUrl.ToLowerInvariant();
if (resourceUrl.StartsWith("/content/images/icons/manifest") ||
resourceUrl.StartsWith("/content/images/icons/browserconfig"))
{
return false;
}
return resourceUrl.StartsWith("/content") ||
(resourceUrl.EndsWith(".js") && !resourceUrl.EndsWith("initialize.js")) ||
resourceUrl.EndsWith(".map") ||
resourceUrl.EndsWith(".css") ||
(resourceUrl.EndsWith(".ico") && !resourceUrl.Equals("/favicon.ico")) ||
resourceUrl.EndsWith(".swf") ||
resourceUrl.EndsWith("oauth.html");
}
}
}

View File

@@ -0,0 +1,51 @@
using System;
using System.IO;
using Nancy;
using Nancy.Responses;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
namespace Prowlarr.Http.Frontend.Mappers
{
public abstract class StaticResourceMapperBase : IMapHttpRequestsToDisk
{
private readonly IDiskProvider _diskProvider;
private readonly Logger _logger;
private readonly StringComparison _caseSensitive;
private static readonly NotFoundResponse NotFoundResponse = new NotFoundResponse();
protected StaticResourceMapperBase(IDiskProvider diskProvider, Logger logger)
{
_diskProvider = diskProvider;
_logger = logger;
_caseSensitive = RuntimeInfo.IsProduction ? DiskProviderBase.PathStringComparison : StringComparison.OrdinalIgnoreCase;
}
public abstract string Map(string resourceUrl);
public abstract bool CanHandle(string resourceUrl);
public virtual Response GetResponse(string resourceUrl)
{
var filePath = Map(resourceUrl);
if (_diskProvider.FileExists(filePath, _caseSensitive))
{
var response = new StreamResponse(() => GetContentStream(filePath), MimeTypes.GetMimeType(filePath));
return new MaterialisingResponse(response);
}
_logger.Warn("File {0} not found", filePath);
return NotFoundResponse;
}
protected virtual Stream GetContentStream(string filePath)
{
return File.OpenRead(filePath);
}
}
}

View File

@@ -0,0 +1,32 @@
using System.IO;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
namespace Prowlarr.Http.Frontend.Mappers
{
public class LogFileMapper : StaticResourceMapperBase
{
private readonly IAppFolderInfo _appFolderInfo;
public LogFileMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, Logger logger)
: base(diskProvider, logger)
{
_appFolderInfo = appFolderInfo;
}
public override string Map(string resourceUrl)
{
var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar);
path = Path.GetFileName(path);
return Path.Combine(_appFolderInfo.GetLogFolder(), path);
}
public override bool CanHandle(string resourceUrl)
{
return resourceUrl.StartsWith("/logfile/") && resourceUrl.EndsWith(".txt");
}
}
}

View File

@@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Nancy;
using NLog;
using Prowlarr.Http.Frontend.Mappers;
namespace Prowlarr.Http.Frontend
{
public class StaticResourceModule : NancyModule
{
private readonly IEnumerable<IMapHttpRequestsToDisk> _requestMappers;
private readonly Logger _logger;
public StaticResourceModule(IEnumerable<IMapHttpRequestsToDisk> requestMappers, Logger logger)
{
_requestMappers = requestMappers;
_logger = logger;
Get("/{resource*}", x => Index());
Get("/", x => Index());
}
private Response 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 mapper.GetResponse(path);
}
_logger.Warn("Couldn't find handler for {0}", path);
return new NotFoundResponse();
}
}
}